main
html 358 lines 11.1 KB
Raw
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Plugin Validator</title>
5 <script type="module">
6 import { store } from "/plugins/_plugin_validator/webui/plugin-validator-store.js";
7 </script>
8 </head>
9 <body>
10 <div x-data>
11 <template x-if="$store.pluginValidator">
12 <div x-create="$store.pluginValidator.onOpen()" x-destroy="$store.pluginValidator.cleanup()" class="plugin-validator">
13 <ul class="nav nav-tabs" role="tablist">
14 <li class="nav-item" role="presentation">
15 <button class="nav-link"
16 :class="{ active: $store.pluginValidator.source === 'local' }"
17 id="plugin-validator-local-tab"
18 type="button"
19 role="tab"
20 :aria-selected="$store.pluginValidator.source === 'local'"
21 aria-controls="plugin-validator-local-panel"
22 @click="$store.pluginValidator.setSource('local')">
23 <x-icon class="pv-tab-icon" name="folder"></x-icon> Local
24 </button>
25 </li>
26 <li class="nav-item" role="presentation">
27 <button class="nav-link"
28 :class="{ active: $store.pluginValidator.source === 'git' }"
29 id="plugin-validator-git-tab"
30 type="button"
31 role="tab"
32 :aria-selected="$store.pluginValidator.source === 'git'"
33 aria-controls="plugin-validator-git-panel"
34 @click="$store.pluginValidator.setSource('git')">
35 <x-icon class="pv-tab-icon" name="terminal"></x-icon> Git
36 </button>
37 </li>
38 <li class="nav-item" role="presentation">
39 <button class="nav-link"
40 :class="{ active: $store.pluginValidator.source === 'zip' }"
41 id="plugin-validator-zip-tab"
42 type="button"
43 role="tab"
44 :aria-selected="$store.pluginValidator.source === 'zip'"
45 aria-controls="plugin-validator-zip-panel"
46 @click="$store.pluginValidator.setSource('zip')">
47 <x-icon class="pv-tab-icon" name="upload_file"></x-icon> ZIP
48 </button>
49 </li>
50 </ul>
51
52 <template x-if="$store.pluginValidator.source === 'local'">
53 <div class="pv-panel" id="plugin-validator-local-panel" role="tabpanel" aria-labelledby="plugin-validator-local-tab">
54 <div class="pv-field">
55 <label>Installed Plugin</label>
56 <select class="pv-select"
57 :value="$store.pluginValidator.localPluginName"
58 @change="$store.pluginValidator.selectLocalPlugin($event.target.value)">
59 <template x-if="$store.pluginValidator.localPlugins.length === 0">
60 <option value="">No custom plugins found</option>
61 </template>
62 <template x-for="plugin in $store.pluginValidator.localPlugins" :key="plugin.name">
63 <option :value="plugin.name" x-text="plugin.display_name ? `${plugin.display_name} (${plugin.name})` : plugin.name"></option>
64 </template>
65 </select>
66 <div class="pv-hint">Validates plugins installed under <code>usr/plugins/</code>.</div>
67 </div>
68 </div>
69 </template>
70
71 <template x-if="$store.pluginValidator.source === 'git'">
72 <div class="pv-panel" id="plugin-validator-git-panel" role="tabpanel" aria-labelledby="plugin-validator-git-tab">
73 <div class="pv-field">
74 <label>Git Repository URL</label>
75 <input type="text"
76 class="pv-input"
77 x-model="$store.pluginValidator.gitUrl"
78 @input.debounce.300ms="$store.pluginValidator.buildPrompt()"
79 placeholder="https://github.com/user/plugin-repo.git" />
80 <div class="pv-hint">Validation clones the repository to a temporary directory and cleans it up after review.</div>
81 </div>
82 </div>
83 </template>
84
85 <template x-if="$store.pluginValidator.source === 'zip'">
86 <div class="pv-panel" id="plugin-validator-zip-panel" role="tabpanel" aria-labelledby="plugin-validator-zip-tab">
87 <div class="pv-upload-section">
88 <label for="plugin-validator-zip-file"
89 class="pv-upload-btn button confirm"
90 :class="{ 'pv-has-file': $store.pluginValidator.zipFile }">
91 <x-icon class="icon" name="upload_file"></x-icon>
92 <span x-text="$store.pluginValidator.zipFileName || 'Select Plugin ZIP File'"></span>
93 </label>
94 <input type="file"
95 id="plugin-validator-zip-file"
96 accept=".zip"
97 style="display:none"
98 @change="$store.pluginValidator.handleZipUpload($event)">
99 <div class="pv-hint">The ZIP is extracted to a temporary directory only for validation. It is not installed.</div>
100 </div>
101 </div>
102 </template>
103
104 <div class="pv-field">
105 <label>Validation Phases</label>
106 <div class="pv-checks">
107 <template x-for="[key, meta] of Object.entries($store.pluginValidator.checksMeta)" :key="key">
108 <label class="pv-check">
109 <input type="checkbox"
110 x-model="$store.pluginValidator.checks[key]"
111 @change="$store.pluginValidator.buildPrompt()" />
112 <span x-text="meta.label"></span>
113 </label>
114 </template>
115 </div>
116 </div>
117
118 <div class="pv-field">
119 <label>Agent Prompt <span class="pv-label-note">(editable)</span></label>
120 <textarea x-model="$store.pluginValidator.prompt" class="pv-textarea"></textarea>
121 </div>
122
123 <div class="pv-actions">
124 <button class="button" @click="$store.pluginValidator.copyPrompt()">Copy Prompt</button>
125 <button class="button confirm"
126 @click="$store.pluginValidator.runValidation()"
127 :disabled="$store.pluginValidator.validating || $store.pluginValidator.queued">
128 <span x-show="$store.pluginValidator.queued"><span class="pv-spinner"></span>Queued...</span>
129 <span x-show="$store.pluginValidator.validating && !$store.pluginValidator.queued"><span class="pv-spinner"></span>Validating...</span>
130 <span x-show="!$store.pluginValidator.validating && !$store.pluginValidator.queued">Run Validation</span>
131 </button>
132 <button class="button"
133 @click="$store.pluginValidator.openChatInNewWindow()"
134 x-show="$store.pluginValidator.validationCtxId"
135 title="Open this validation chat in a new tab">
136 Open in Chat ->
137 </button>
138 </div>
139
140 <div x-show="$store.pluginValidator.output" class="pv-output">
141 <label class="pv-output-label">Validation Results</label>
142 <div class="pv-output-html" x-html="$store.pluginValidator.renderedOutput"></div>
143 </div>
144 </div>
145 </template>
146 </div>
147
148 <style>
149 @import url("/plugins/_plugin_installer/webui/install-shared.css");
150
151 .plugin-validator {
152 display: flex;
153 flex-direction: column;
154 gap: 1rem;
155 padding: 0.5rem;
156 }
157
158 .nav {
159 display: flex;
160 padding-left: 0;
161 margin: 0;
162 list-style: none;
163 border-bottom: 1px solid var(--color-border);
164 gap: 0.25rem;
165 }
166
167 .nav-link {
168 font-family: "Rubik", Arial, Helvetica, sans-serif;
169 border: 1px solid transparent;
170 border-top-left-radius: 4px;
171 border-top-right-radius: 4px;
172 padding: 0.4rem 0.7rem;
173 background: transparent;
174 color: var(--color-text-secondary);
175 cursor: pointer;
176 display: inline-flex;
177 align-items: center;
178 gap: 0.3rem;
179 }
180
181 .nav-link.active {
182 color: var(--color-text-primary);
183 border-color: var(--color-border);
184 border-bottom-color: transparent;
185 background: var(--color-background);
186 }
187
188 .pv-tab-icon {
189 font-size: 1.1rem;
190 }
191
192 .pv-panel {
193 display: flex;
194 flex-direction: column;
195 gap: 0.75rem;
196 }
197
198 .pv-field {
199 display: flex;
200 flex-direction: column;
201 gap: 0.35rem;
202 }
203
204 .pv-field label,
205 .pv-output-label {
206 font-size: 0.85rem;
207 font-weight: 600;
208 opacity: 0.8;
209 }
210
211 .pv-label-note {
212 font-weight: 400;
213 opacity: 0.6;
214 }
215
216 .pv-input,
217 .pv-select,
218 .pv-textarea {
219 width: 100%;
220 border: 1px solid var(--color-border);
221 border-radius: 6px;
222 padding: 0.5rem 0.75rem;
223 font-family: inherit;
224 font-size: 0.875rem;
225 background: var(--color-panel);
226 color: var(--color-text);
227 box-sizing: border-box;
228 }
229
230 .pv-textarea {
231 min-height: 16rem;
232 resize: none;
233 font-family: monospace;
234 font-size: 0.8rem;
235 }
236
237 .pv-input:focus,
238 .pv-select:focus,
239 .pv-textarea:focus {
240 outline: none;
241 border-color: var(--color-primary);
242 }
243
244 .pv-checks {
245 display: flex;
246 flex-wrap: wrap;
247 gap: 0.5rem 1.25rem;
248 }
249
250 .pv-check {
251 display: flex;
252 align-items: center;
253 gap: 0.35rem;
254 font-size: 0.85rem;
255 cursor: pointer;
256 user-select: none;
257 }
258
259 .pv-check input[type="checkbox"] {
260 accent-color: var(--color-primary);
261 }
262
263 .pv-actions {
264 display: flex;
265 gap: 0.5rem;
266 flex-wrap: wrap;
267 }
268
269 .pv-output {
270 border-top: 1px solid var(--color-border);
271 padding-top: 1rem;
272 }
273
274 .pv-output-html {
275 line-height: 1.5;
276 }
277
278 .pv-output-html table {
279 border-collapse: collapse;
280 width: 100%;
281 margin: 0.75rem 0;
282 }
283
284 .pv-output-html th,
285 .pv-output-html td {
286 border: 1px solid var(--color-border);
287 padding: 0.4rem 0.6rem;
288 text-align: left;
289 font-size: 0.85rem;
290 }
291
292 .pv-output-html th {
293 background: var(--color-panel);
294 font-weight: 600;
295 }
296
297 .pv-output-html hr {
298 border: 1px solid var(--color-border);
299 }
300
301 .pv-output-html pre {
302 background: var(--color-panel);
303 border: 1px solid var(--color-border);
304 border-radius: 6px;
305 padding: 0.75rem;
306 overflow-x: auto;
307 }
308
309 .pv-output-html code {
310 font-size: 0.8rem;
311 }
312
313 .pv-upload-section {
314 text-align: center;
315 padding: 2rem 1rem;
316 border: 2px dashed var(--color-border);
317 border-radius: 8px;
318 }
319
320 .pv-upload-btn {
321 display: inline-flex;
322 align-items: center;
323 gap: 0.5rem;
324 padding: 0.75rem 1.5rem;
325 font-size: 1rem;
326 cursor: pointer;
327 }
328
329 .pv-upload-btn.pv-has-file {
330 background: var(--color-panel);
331 border-color: var(--color-highlight);
332 }
333
334 .pv-hint {
335 color: var(--color-text-secondary);
336 font-size: 0.85rem;
337 }
338
339 .pv-spinner {
340 display: inline-block;
341 width: 1em;
342 height: 1em;
343 border: 2px solid var(--color-border);
344 border-top-color: var(--color-primary);
345 border-radius: 50%;
346 animation: pv-spin 0.6s linear infinite;
347 vertical-align: middle;
348 margin-right: 0.4em;
349 }
350
351 @keyframes pv-spin {
352 to {
353 transform: rotate(360deg);
354 }
355 }
356 </style>
357 </body>
358 </html>