|
1 | 1 | <script lang="ts">
|
2 | 2 | import { onMount, tick } from 'svelte';
|
3 |
| - import { ChevronLeft, ChevronRight, User, Key, Settings, CheckCircle, AlertCircle, Terminal } from 'lucide-svelte'; |
4 |
| - import { guardianHelpers } from '$lib/stores/guardian.js'; |
5 |
| - import { aiAnalysisHelpers } from '$lib/stores/ai-analysis.js'; |
6 |
| - import { loadThemePreset, themePresets } from '$lib/stores/theme.js'; |
| 3 | + import { Terminal, ChevronRight } from 'lucide-svelte'; |
| 4 | + import { guardianHelpers } from '$lib/stores/guardian'; |
| 5 | + import { aiAnalysisHelpers } from '$lib/stores/ai-analysis'; |
| 6 | + import { loadThemePreset, themePresets } from '$lib/stores/theme'; |
7 | 7 | import { uiHelpers } from '$lib/stores/ui';
|
8 |
| - import { fetchOpenRouterModels } from '$lib/utils/ai-analysis.js'; |
| 8 | + import { fetchOpenRouterModels } from '$lib/utils/ai-analysis'; |
9 | 9 |
|
10 | 10 | let apiKeyInput = '';
|
11 | 11 | let prevApiKey = '';
|
|
17 | 17 | };
|
18 | 18 |
|
19 | 19 | let apiKeyStatus = 'unchecked'; // unchecked, checking, valid, invalid
|
20 |
| - |
| 20 | +
|
21 | 21 | // CLI-style editing states
|
22 | 22 | let editingField: string | null = null;
|
23 | 23 | const themeKeys = Object.keys(themePresets) as Array<keyof typeof themePresets>;
|
24 | 24 | let currentThemeKey: keyof typeof themePresets = 'everforest';
|
25 | 25 | let themeIndex = 0;
|
26 | 26 |
|
27 | 27 | const displayKey = (k: keyof typeof themePresets) => (k === 'tokyoNight' ? 'tokyo-night' : k);
|
28 |
| - |
| 28 | +
|
29 | 29 | // OpenRouter model selection (dynamic; fallback list shown until fetched)
|
30 | 30 | let modelList: string[] = [
|
31 | 31 | 'openai/gpt-oss-20b:free',
|
|
59 | 59 | if (dir === 'next') {
|
60 | 60 | currentModelIndex = modelList.length ? (currentModelIndex + 1) % modelList.length : 0;
|
61 | 61 | } else {
|
62 |
| - currentModelIndex = modelList.length ? (currentModelIndex - 1 + modelList.length) % modelList.length : 0; |
| 62 | + currentModelIndex = modelList.length |
| 63 | + ? (currentModelIndex - 1 + modelList.length) % modelList.length |
| 64 | + : 0; |
63 | 65 | }
|
64 | 66 | persistModel();
|
65 | 67 | }
|
|
87 | 89 | }
|
88 | 90 | }
|
89 | 91 |
|
90 |
| - onMount(() => { |
| 92 | + onMount(() => { |
91 | 93 | // Load saved guardian data
|
92 | 94 | const saved = guardianHelpers.load();
|
93 | 95 | if (saved) {
|
94 | 96 | guardianName = saved.name || '';
|
95 | 97 | apiKeyInput = saved.apiKey || '';
|
96 | 98 | preferences = { ...preferences, ...saved.preferences };
|
97 | 99 | }
|
98 |
| - |
99 |
| - // Get current theme from localStorage |
100 |
| - const savedTheme = (localStorage.getItem('petalytics-theme') as keyof typeof themePresets) || 'everforest'; |
| 100 | +
|
| 101 | + // Get current theme from localStorage |
| 102 | + const savedTheme = |
| 103 | + (localStorage.getItem('petalytics-theme') as keyof typeof themePresets) || 'everforest'; |
101 | 104 | currentThemeKey = themeKeys.includes(savedTheme) ? savedTheme : 'everforest';
|
102 | 105 | themeIndex = themeKeys.indexOf(currentThemeKey);
|
103 | 106 |
|
104 |
| - // Get model selection from guardian storage |
105 |
| - loadModelFromStorage(); |
106 |
| - // Fetch model list from OpenRouter if API key is present |
107 |
| - loadModels(); |
| 107 | + // Get model selection from guardian storage |
| 108 | + loadModelFromStorage(); |
| 109 | + // Fetch model list from OpenRouter if API key is present |
| 110 | + loadModels(); |
108 | 111 | });
|
109 | 112 |
|
110 | 113 | function toggleTheme(direction: 'prev' | 'next') {
|
|
152 | 155 | }
|
153 | 156 | }
|
154 | 157 |
|
155 |
| - function handleKeydown(event: KeyboardEvent, field: string) { |
| 158 | + function handleKeydown(event: KeyboardEvent, _field: string) { |
156 | 159 | if (event.key === 'Enter') {
|
157 | 160 | stopEdit();
|
158 | 161 | } else if (event.key === 'Escape') {
|
|
223 | 226 |
|
224 | 227 | <div class="guardian-panel h-full" style="background: var(--petalytics-bg);">
|
225 | 228 | <!-- CLI-style header -->
|
226 |
| - <div class="cli-header p-3 border-b font-mono text-sm" style="border-color: var(--petalytics-border); background: var(--petalytics-surface);"> |
| 229 | + <div |
| 230 | + class="cli-header p-3 border-b font-mono text-sm" |
| 231 | + style="border-color: var(--petalytics-border); background: var(--petalytics-surface);" |
| 232 | + > |
227 | 233 | <div class="flex items-center space-x-2" style="color: var(--petalytics-pine);">
|
228 | 234 | <Terminal size={14} />
|
229 | 235 | <span>guardian@petalytics:~$</span>
|
230 | 236 | </div>
|
231 | 237 | </div>
|
232 | 238 |
|
233 |
| - <div class="cli-content p-3 font-mono text-sm overflow-y-auto" style="color: var(--petalytics-text);"> |
234 |
| - |
| 239 | + <div |
| 240 | + class="cli-content p-3 font-mono text-sm overflow-y-auto" |
| 241 | + style="color: var(--petalytics-text);" |
| 242 | + > |
235 | 243 | <!-- Guardian name row -->
|
236 |
| - <div class="cli-row px-2 py-1" role="button" tabindex="0" onclick={() => startEdit('guardian')} onkeydown={(e) => handleActivate(e, () => startEdit('guardian'))}> |
| 244 | + <div |
| 245 | + class="cli-row px-2 py-1" |
| 246 | + role="button" |
| 247 | + tabindex="0" |
| 248 | + onclick={() => startEdit('guardian')} |
| 249 | + onkeydown={(e) => handleActivate(e, () => startEdit('guardian'))} |
| 250 | + > |
237 | 251 | <span class="label" style="color: var(--petalytics-foam);">guardian</span>
|
238 | 252 | <span class="value" style="color: var(--petalytics-text);">
|
239 | 253 | {#if editingField === 'guardian'}
|
|
252 | 266 | </div>
|
253 | 267 |
|
254 | 268 | <!-- API Key row -->
|
255 |
| - <div class="cli-row px-2 py-1" role="button" tabindex="0" onclick={() => startEdit('apiKey')} onkeydown={(e) => handleActivate(e, () => startEdit('apiKey'))}> |
| 269 | + <div |
| 270 | + class="cli-row px-2 py-1" |
| 271 | + role="button" |
| 272 | + tabindex="0" |
| 273 | + onclick={() => startEdit('apiKey')} |
| 274 | + onkeydown={(e) => handleActivate(e, () => startEdit('apiKey'))} |
| 275 | + > |
256 | 276 | <span class="label" style="color: var(--petalytics-foam);">api_key</span>
|
257 | 277 | <span class="value" style="color: var(--petalytics-text);">
|
258 | 278 | {#if editingField === 'apiKey'}
|
|
290 | 310 | <!-- Model selection (OpenRouter) -->
|
291 | 311 | <div class="cli-row px-2 py-1">
|
292 | 312 | <span class="label" style="color: var(--petalytics-foam);">model</span>
|
293 |
| - <span class="value" style="color: var(--petalytics-text);">{displayModel()} {#if modelsLoading}(loading...){/if}</span> |
| 313 | + <span class="value" style="color: var(--petalytics-text);" |
| 314 | + >{displayModel()} |
| 315 | + {#if modelsLoading}(loading...){/if}</span |
| 316 | + > |
294 | 317 | <div class="ml-2 flex items-center space-x-1">
|
295 |
| - <button type="button" class="arrow-btn" onclick={() => cycleModel('prev')} aria-label="Previous model"><</button> |
296 |
| - <button type="button" class="arrow-btn" onclick={() => cycleModel('next')} aria-label="Next model">></button> |
297 |
| - <button type="button" class="arrow-btn" onclick={loadModels} aria-label="Refresh models">↻</button> |
| 318 | + <button |
| 319 | + type="button" |
| 320 | + class="arrow-btn" |
| 321 | + onclick={() => cycleModel('prev')} |
| 322 | + aria-label="Previous model"><</button |
| 323 | + > |
| 324 | + <button |
| 325 | + type="button" |
| 326 | + class="arrow-btn" |
| 327 | + onclick={() => cycleModel('next')} |
| 328 | + aria-label="Next model">></button |
| 329 | + > |
| 330 | + <button type="button" class="arrow-btn" onclick={loadModels} aria-label="Refresh models" |
| 331 | + >↻</button |
| 332 | + > |
298 | 333 | </div>
|
299 | 334 | </div>
|
300 | 335 |
|
301 | 336 | <!-- Theme row -->
|
302 | 337 | <div class="cli-row px-2 py-1">
|
303 | 338 | <span class="label" style="color: var(--petalytics-foam);">theme</span>
|
304 |
| - <span class="value" style="color: var(--petalytics-text);">{displayKey(currentThemeKey)}</span> |
| 339 | + <span class="value" style="color: var(--petalytics-text);">{displayKey(currentThemeKey)}</span |
| 340 | + > |
305 | 341 | <div class="ml-2 flex items-center space-x-1">
|
306 |
| - <button type="button" class="arrow-btn" onclick={() => toggleTheme('prev')} aria-label="Previous theme"><</button> |
307 |
| - <button type="button" class="arrow-btn" onclick={() => toggleTheme('next')} aria-label="Next theme">></button> |
| 342 | + <button |
| 343 | + type="button" |
| 344 | + class="arrow-btn" |
| 345 | + onclick={() => toggleTheme('prev')} |
| 346 | + aria-label="Previous theme"><</button |
| 347 | + > |
| 348 | + <button |
| 349 | + type="button" |
| 350 | + class="arrow-btn" |
| 351 | + onclick={() => toggleTheme('next')} |
| 352 | + aria-label="Next theme">></button |
| 353 | + > |
308 | 354 | </div>
|
309 | 355 | </div>
|
310 | 356 |
|
311 | 357 | <!-- Preferences: only ai_insights -->
|
312 |
| - <div class="cli-row px-2 py-1" role="button" tabindex="0" aria-pressed={preferences.aiInsights} onclick={() => togglePreference('aiInsights')} onkeydown={(e) => handleActivate(e, () => togglePreference('aiInsights'))}> |
| 358 | + <div |
| 359 | + class="cli-row px-2 py-1" |
| 360 | + role="button" |
| 361 | + tabindex="0" |
| 362 | + aria-pressed={preferences.aiInsights} |
| 363 | + onclick={() => togglePreference('aiInsights')} |
| 364 | + onkeydown={(e) => handleActivate(e, () => togglePreference('aiInsights'))} |
| 365 | + > |
313 | 366 | <span class="label" style="color: var(--petalytics-foam);">ai_insights</span>
|
314 |
| - <span class="value" style="color: var(--petalytics-text);">{preferences.aiInsights ? 'enabled' : 'disabled'}</span> |
315 |
| - <span class="ml-2" style="color: {preferences.aiInsights ? 'var(--petalytics-pine)' : 'var(--petalytics-subtle)'};">{preferences.aiInsights ? '●' : '○'}</span> |
| 367 | + <span class="value" style="color: var(--petalytics-text);" |
| 368 | + >{preferences.aiInsights ? 'enabled' : 'disabled'}</span |
| 369 | + > |
| 370 | + <span |
| 371 | + class="ml-2" |
| 372 | + style="color: {preferences.aiInsights |
| 373 | + ? 'var(--petalytics-pine)' |
| 374 | + : 'var(--petalytics-subtle)'};">{preferences.aiInsights ? '●' : '○'}</span |
| 375 | + > |
316 | 376 | </div>
|
317 | 377 |
|
318 | 378 | <!-- Separator line -->
|
|
321 | 381 | </div>
|
322 | 382 |
|
323 | 383 | <!-- Data Manager launcher (opens in right panel) -->
|
324 |
| - <div class="cli-row px-2 py-1" role="button" tabindex="0" onclick={() => uiHelpers.setView('dataManager')} onkeydown={(e) => handleActivate(e, () => uiHelpers.setView('dataManager'))}> |
| 384 | + <div |
| 385 | + class="cli-row px-2 py-1" |
| 386 | + role="button" |
| 387 | + tabindex="0" |
| 388 | + onclick={() => uiHelpers.setView('dataManager')} |
| 389 | + onkeydown={(e) => handleActivate(e, () => uiHelpers.setView('dataManager'))} |
| 390 | + > |
325 | 391 | <span class="label" style="color: var(--petalytics-foam);">data_manager</span>
|
326 | 392 | <span class="value" style="color: var(--petalytics-text);">open</span>
|
327 | 393 | <ChevronRight size={14} style="color: var(--petalytics-subtle);" class="ml-2" />
|
|
330 | 396 | </div>
|
331 | 397 |
|
332 | 398 | <style>
|
333 |
| -/* Alacritty-inspired interactive rows */ |
334 |
| -.cli-row { |
335 |
| - display: flex; |
336 |
| - align-items: center; |
337 |
| - border: 1px solid transparent; |
338 |
| - border-radius: 6px; |
339 |
| - transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease; |
340 |
| -} |
341 |
| -.cli-row[role="button"] { |
342 |
| - cursor: pointer; |
343 |
| -} |
344 |
| -.cli-row:hover { |
345 |
| - background: var(--petalytics-highlight-low); |
346 |
| - border-color: var(--petalytics-border); |
347 |
| -} |
348 |
| -.cli-row:focus-within, |
349 |
| -.cli-row[role="button"]:focus-visible { |
350 |
| - outline: none; |
351 |
| - background: var(--petalytics-highlight-med); |
352 |
| - border-color: var(--petalytics-accent); |
353 |
| - box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 40%, transparent); |
354 |
| -} |
355 |
| -.cli-row[aria-pressed="true"] { |
356 |
| - background: var(--petalytics-highlight-high); |
357 |
| - border-color: var(--petalytics-accent); |
358 |
| -} |
359 |
| -.label { |
360 |
| - color: var(--petalytics-foam); |
361 |
| -} |
362 |
| -.value { |
363 |
| - margin-left: auto; |
364 |
| - text-align: right; |
365 |
| - flex: 1 1 auto; |
366 |
| -} |
367 |
| -.input-inline { |
368 |
| - padding: 0; |
369 |
| -} |
370 |
| -.arrow-btn { |
371 |
| - font-family: 'JetBrains Mono', monospace; |
372 |
| - font-size: 0.85rem; |
373 |
| - line-height: 1rem; |
374 |
| - background: transparent; |
375 |
| - border: 1px solid var(--petalytics-border); |
376 |
| - color: var(--petalytics-subtle); |
377 |
| - padding: 0.15rem 0.4rem; |
378 |
| - border-radius: 4px; |
379 |
| - cursor: pointer; |
380 |
| -} |
381 |
| -.arrow-btn:hover { |
382 |
| - background: var(--petalytics-highlight-low); |
383 |
| - color: var(--petalytics-text); |
384 |
| -} |
385 |
| -.arrow-btn:focus-visible { |
386 |
| - outline: none; |
387 |
| - border-color: var(--petalytics-accent); |
388 |
| - box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 35%, transparent); |
389 |
| -} |
| 399 | + /* Alacritty-inspired interactive rows */ |
| 400 | + .cli-row { |
| 401 | + display: flex; |
| 402 | + align-items: center; |
| 403 | + border: 1px solid transparent; |
| 404 | + border-radius: 6px; |
| 405 | + transition: |
| 406 | + background 140ms ease, |
| 407 | + border-color 140ms ease, |
| 408 | + box-shadow 140ms ease; |
| 409 | + } |
| 410 | + .cli-row[role='button'] { |
| 411 | + cursor: pointer; |
| 412 | + } |
| 413 | + .cli-row:hover { |
| 414 | + background: var(--petalytics-highlight-low); |
| 415 | + border-color: var(--petalytics-border); |
| 416 | + } |
| 417 | + .cli-row:focus-within, |
| 418 | + .cli-row[role='button']:focus-visible { |
| 419 | + outline: none; |
| 420 | + background: var(--petalytics-highlight-med); |
| 421 | + border-color: var(--petalytics-accent); |
| 422 | + box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 40%, transparent); |
| 423 | + } |
| 424 | + .cli-row[aria-pressed='true'] { |
| 425 | + background: var(--petalytics-highlight-high); |
| 426 | + border-color: var(--petalytics-accent); |
| 427 | + } |
| 428 | + .label { |
| 429 | + color: var(--petalytics-foam); |
| 430 | + } |
| 431 | + .value { |
| 432 | + margin-left: auto; |
| 433 | + text-align: right; |
| 434 | + flex: 1 1 auto; |
| 435 | + } |
| 436 | + .input-inline { |
| 437 | + padding: 0; |
| 438 | + } |
| 439 | + .arrow-btn { |
| 440 | + font-family: 'JetBrains Mono', monospace; |
| 441 | + font-size: 0.85rem; |
| 442 | + line-height: 1rem; |
| 443 | + background: transparent; |
| 444 | + border: 1px solid var(--petalytics-border); |
| 445 | + color: var(--petalytics-subtle); |
| 446 | + padding: 0.15rem 0.4rem; |
| 447 | + border-radius: 4px; |
| 448 | + cursor: pointer; |
| 449 | + } |
| 450 | + .arrow-btn:hover { |
| 451 | + background: var(--petalytics-highlight-low); |
| 452 | + color: var(--petalytics-text); |
| 453 | + } |
| 454 | + .arrow-btn:focus-visible { |
| 455 | + outline: none; |
| 456 | + border-color: var(--petalytics-accent); |
| 457 | + box-shadow: 0 0 0 2px color-mix(in oklab, var(--petalytics-accent) 35%, transparent); |
| 458 | + } |
390 | 459 | </style>
|
0 commit comments