feat: dark/light theme toggle (#18) #38
@@ -19,3 +19,4 @@
|
|||||||
| #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) |
|
| #15 | Agent lineage visualization | COMPLETED | [issue-015.md](issue-015.md) |
|
||||||
| #16 | Memory candidates viewer | COMPLETED | [issue-016.md](issue-016.md) |
|
| #16 | Memory candidates viewer | COMPLETED | [issue-016.md](issue-016.md) |
|
||||||
| #17 | Audit/activity log view | COMPLETED | [issue-017.md](issue-017.md) |
|
| #17 | Audit/activity log view | COMPLETED | [issue-017.md](issue-017.md) |
|
||||||
|
| #18 | Dark/light theme toggle | COMPLETED | [issue-018.md](issue-018.md) |
|
||||||
|
|||||||
68
implementation-plans/issue-018.md
Normal file
68
implementation-plans/issue-018.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Issue #18: Dark/Light Theme Toggle
|
||||||
|
|
||||||
|
## Status: COMPLETED
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Add a dark/light theme toggle to the UI with three modes (system, light, dark), persisted in localStorage, defaulting to system preference.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Tailwind v4 Dark Mode Configuration
|
||||||
|
- Updated `src/app.css` with `@variant dark (&:where(.dark, .dark *));` for class-based dark mode
|
||||||
|
|
||||||
|
### 2. Theme Store (`src/lib/stores/theme.svelte.ts`)
|
||||||
|
- Svelte 5 runes-based store with three modes: 'light', 'dark', 'system'
|
||||||
|
- Loads preference from localStorage on init, defaults to 'system'
|
||||||
|
- Listens to `prefers-color-scheme` media query when in 'system' mode
|
||||||
|
- Applies/removes `dark` class on `document.documentElement`
|
||||||
|
- Adds temporary `theme-transition` class for smooth CSS transitions
|
||||||
|
|
||||||
|
### 3. ThemeToggle Component (`src/lib/components/ThemeToggle.svelte`)
|
||||||
|
- Compact button that cycles: system -> light -> dark -> system
|
||||||
|
- SVG icons: monitor (system), sun (light), moon (dark)
|
||||||
|
- Text label showing current mode
|
||||||
|
- Dark mode aware styling
|
||||||
|
|
||||||
|
### 4. Layout Integration
|
||||||
|
- Theme store initialized via `$effect` in `+layout.svelte`
|
||||||
|
- Global CSS transition styles for smooth theme switching
|
||||||
|
- ThemeToggle added to all page headers (chat, lineage, memory, audit, home)
|
||||||
|
|
||||||
|
### 5. Dark Mode Classes Applied To
|
||||||
|
- `src/routes/+page.svelte` (home page)
|
||||||
|
- `src/routes/chat/+page.svelte` (chat page)
|
||||||
|
- `src/routes/lineage/+page.svelte` (lineage page)
|
||||||
|
- `src/routes/memory/+page.svelte` (memory page)
|
||||||
|
- `src/routes/audit/+page.svelte` (audit page)
|
||||||
|
- `src/lib/components/SessionSidebar.svelte`
|
||||||
|
- `src/lib/components/ConfigSidebar.svelte`
|
||||||
|
- `src/lib/components/MessageBubble.svelte`
|
||||||
|
- `src/lib/components/MessageList.svelte`
|
||||||
|
- `src/lib/components/MessageInput.svelte`
|
||||||
|
- `src/lib/components/OrchestrationProgress.svelte`
|
||||||
|
- `src/lib/components/ThinkingSection.svelte`
|
||||||
|
- `src/lib/components/FinalResult.svelte`
|
||||||
|
- `src/lib/components/LineageTree.svelte`
|
||||||
|
- `src/lib/components/MemoryCandidateCard.svelte`
|
||||||
|
- `src/lib/components/AuditTimeline.svelte`
|
||||||
|
|
||||||
|
### Color Mapping Applied
|
||||||
|
- `bg-white` -> `dark:bg-gray-900`
|
||||||
|
- `bg-gray-50` -> `dark:bg-gray-800`
|
||||||
|
- `bg-gray-100` -> `dark:bg-gray-700`
|
||||||
|
- `bg-gray-200` -> `dark:bg-gray-600`/`dark:bg-gray-700`
|
||||||
|
- `text-gray-900` -> `dark:text-gray-100`
|
||||||
|
- `text-gray-700` -> `dark:text-gray-300`
|
||||||
|
- `text-gray-500` -> `dark:text-gray-400`
|
||||||
|
- `text-gray-400` -> `dark:text-gray-500`
|
||||||
|
- `border-gray-200` -> `dark:border-gray-700`
|
||||||
|
- `border-gray-300` -> `dark:border-gray-600`
|
||||||
|
- Colored backgrounds (blue, green, amber, red, purple) use opacity-based dark variants (e.g., `dark:bg-blue-900/40`)
|
||||||
|
- SVG elements in LineageTree use reactive color values based on `themeStore.isDark`
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- `src/app.css` - Added dark mode variant configuration
|
||||||
|
- `src/lib/stores/theme.svelte.ts` - New theme store
|
||||||
|
- `src/lib/components/ThemeToggle.svelte` - New toggle component
|
||||||
|
- `src/routes/+layout.svelte` - Theme init and transition styles
|
||||||
|
- All page and component files listed above - Added dark: variants
|
||||||
@@ -1 +1,3 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@variant dark (&:where(.dark, .dark *));
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
function typeBadgeClasses(eventType: string): string {
|
function typeBadgeClasses(eventType: string): string {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'state_change':
|
case 'state_change':
|
||||||
return 'bg-blue-100 text-blue-700';
|
return 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300';
|
||||||
case 'tool_invocation':
|
case 'tool_invocation':
|
||||||
return 'bg-green-100 text-green-700';
|
return 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300';
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'bg-red-100 text-red-700';
|
return 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300';
|
||||||
case 'message':
|
case 'message':
|
||||||
return 'bg-gray-100 text-gray-600';
|
return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-100 text-gray-600';
|
return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +59,14 @@
|
|||||||
|
|
||||||
{#if events.length === 0}
|
{#if events.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div class="mb-3 text-4xl text-gray-300">📄</div>
|
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">📄</div>
|
||||||
<p class="text-sm font-medium text-gray-500">No events for this session</p>
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No events for this session</p>
|
||||||
<p class="mt-1 text-xs text-gray-400">Events will appear here as orchestration runs.</p>
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">Events will appear here as orchestration runs.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="relative ml-4">
|
<div class="relative ml-4">
|
||||||
<!-- Vertical line -->
|
<!-- Vertical line -->
|
||||||
<div class="absolute top-0 bottom-0 left-3 w-0.5 bg-gray-200"></div>
|
<div class="absolute top-0 bottom-0 left-3 w-0.5 bg-gray-200 dark:bg-gray-700"></div>
|
||||||
|
|
||||||
<ol class="space-y-4">
|
<ol class="space-y-4">
|
||||||
{#each events as event, i (event.id)}
|
{#each events as event, i (event.id)}
|
||||||
@@ -74,34 +74,34 @@
|
|||||||
|
|
||||||
{#if showDate}
|
{#if showDate}
|
||||||
<li class="relative pl-10 pt-2">
|
<li class="relative pl-10 pt-2">
|
||||||
<span class="text-xs font-medium text-gray-400">{formatDate(event.timestamp)}</span>
|
<span class="text-xs font-medium text-gray-400 dark:text-gray-500">{formatDate(event.timestamp)}</span>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<li class="group relative flex items-start pl-10">
|
<li class="group relative flex items-start pl-10">
|
||||||
<!-- Dot on the timeline -->
|
<!-- Dot on the timeline -->
|
||||||
<div
|
<div
|
||||||
class="absolute left-1.5 top-1.5 h-3 w-3 rounded-full ring-2 ring-white {typeDotClasses(event.eventType)}"
|
class="absolute left-1.5 top-1.5 h-3 w-3 rounded-full ring-2 ring-white dark:ring-gray-900 {typeDotClasses(event.eventType)}"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<!-- Event card -->
|
<!-- Event card -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm transition-shadow group-hover:shadow-md"
|
class="flex-1 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3 shadow-sm transition-shadow group-hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="text-xs text-gray-400">{formatTimestamp(event.timestamp)}</span>
|
<span class="text-xs text-gray-400 dark:text-gray-500">{formatTimestamp(event.timestamp)}</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {typeBadgeClasses(event.eventType)}"
|
class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {typeBadgeClasses(event.eventType)}"
|
||||||
>
|
>
|
||||||
{typeLabel(event.eventType)}
|
{typeLabel(event.eventType)}
|
||||||
</span>
|
</span>
|
||||||
{#if event.state}
|
{#if event.state}
|
||||||
<span class="rounded-md bg-blue-50 px-1.5 py-0.5 text-xs font-mono text-blue-600">
|
<span class="rounded-md bg-blue-50 dark:bg-blue-900/30 px-1.5 py-0.5 text-xs font-mono text-blue-600 dark:text-blue-400">
|
||||||
{event.state}
|
{event.state}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-1.5 text-sm text-gray-700">{event.details}</p>
|
<p class="mt-1.5 text-sm text-gray-700 dark:text-gray-300">{event.details}</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -115,10 +115,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="flex h-full w-72 flex-col border-l border-gray-200 bg-white">
|
<aside class="flex h-full w-72 flex-col border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||||
<div class="flex items-center justify-between border-b border-gray-200 px-4 py-3">
|
<div class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 px-4 py-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2 class="text-sm font-semibold text-gray-900">Session Config</h2>
|
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Session Config</h2>
|
||||||
{#if isNonDefault}
|
{#if isNonDefault}
|
||||||
<span class="h-2 w-2 rounded-full bg-amber-500" title="Non-default config active"></span>
|
<span class="h-2 w-2 rounded-full bg-amber-500" title="Non-default config active"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={resetConfig}
|
onclick={resetConfig}
|
||||||
class="text-xs text-gray-500 hover:text-gray-700"
|
class="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
@@ -135,27 +135,27 @@
|
|||||||
<div class="flex-1 overflow-y-auto p-4 space-y-5">
|
<div class="flex-1 overflow-y-auto p-4 space-y-5">
|
||||||
<!-- Presets -->
|
<!-- Presets -->
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs font-medium text-gray-700">Presets</p>
|
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Presets</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#each presetStore.getAllPresets() as preset (preset.name)}
|
{#each presetStore.getAllPresets() as preset (preset.name)}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handleLoadPreset(preset.name)}
|
onclick={() => handleLoadPreset(preset.name)}
|
||||||
class="flex-1 rounded-lg px-3 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-50"
|
class="flex-1 rounded-lg px-3 py-1.5 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
{preset.name}
|
{preset.name}
|
||||||
{#if preset.builtIn}
|
{#if preset.builtIn}
|
||||||
<span class="text-[10px] text-gray-400">built-in</span>
|
<span class="text-[10px] text-gray-400 dark:text-gray-500">built-in</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if !preset.builtIn}
|
{#if !preset.builtIn}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => handleDeletePreset(preset.name)}
|
onclick={() => handleDeletePreset(preset.name)}
|
||||||
class="rounded px-1.5 py-1 text-xs text-gray-400 hover:text-red-500"
|
class="rounded px-1.5 py-1 text-xs text-gray-400 dark:text-gray-500 hover:text-red-500"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,22 +168,22 @@
|
|||||||
bind:value={newPresetName}
|
bind:value={newPresetName}
|
||||||
onkeydown={(e) => { if (e.key === 'Enter') handleSavePreset(); }}
|
onkeydown={(e) => { if (e.key === 'Enter') handleSavePreset(); }}
|
||||||
placeholder="Preset name"
|
placeholder="Preset name"
|
||||||
class="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs focus:border-blue-500 focus:outline-none"
|
class="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1.5 text-xs text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={handleSavePreset}
|
onclick={handleSavePreset}
|
||||||
disabled={!newPresetName.trim()}
|
disabled={!newPresetName.trim()}
|
||||||
class="rounded-lg bg-blue-50 px-2 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-100 disabled:opacity-50"
|
class="rounded-lg bg-blue-50 dark:bg-blue-900/40 px-2 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/60 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => { showSavePreset = false; saveError = ''; newPresetName = ''; }}
|
onclick={() => { showSavePreset = false; saveError = ''; newPresetName = ''; }}
|
||||||
class="rounded-lg px-1.5 py-1.5 text-xs text-gray-400 hover:text-gray-600"
|
class="rounded-lg px-1.5 py-1.5 text-xs text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if saveError}
|
{#if saveError}
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => (showSavePreset = true)}
|
onclick={() => (showSavePreset = true)}
|
||||||
class="mt-2 w-full rounded-lg border border-dashed border-gray-300 px-3 py-1.5 text-xs text-gray-500 hover:border-gray-400 hover:text-gray-700"
|
class="mt-2 w-full rounded-lg border border-dashed border-gray-300 dark:border-gray-600 px-3 py-1.5 text-xs text-gray-500 dark:text-gray-400 hover:border-gray-400 dark:hover:border-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
Save current as preset
|
Save current as preset
|
||||||
</button>
|
</button>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
|
|
||||||
<!-- Override Level -->
|
<!-- Override Level -->
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs font-medium text-gray-700">Override Level</p>
|
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Override Level</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#each overrideLevels as level (level.value)}
|
{#each overrideLevels as level (level.value)}
|
||||||
<button
|
<button
|
||||||
@@ -210,11 +210,11 @@
|
|||||||
onclick={() => setOverrideLevel(level.value)}
|
onclick={() => setOverrideLevel(level.value)}
|
||||||
class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm
|
class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm
|
||||||
{config.overrideLevel === level.value
|
{config.overrideLevel === level.value
|
||||||
? 'bg-blue-50 text-blue-700 ring-1 ring-blue-200'
|
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 ring-1 ring-blue-200 dark:ring-blue-700'
|
||||||
: 'text-gray-700 hover:bg-gray-50'}"
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'}"
|
||||||
>
|
>
|
||||||
<span class="font-medium">{level.label}</span>
|
<span class="font-medium">{level.label}</span>
|
||||||
<span class="text-xs text-gray-500">— {level.description}</span>
|
<span class="text-xs text-gray-500 dark:text-gray-400">— {level.description}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -222,17 +222,17 @@
|
|||||||
|
|
||||||
<!-- Disabled Tools -->
|
<!-- Disabled Tools -->
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs font-medium text-gray-700">Disabled Tools</p>
|
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Disabled Tools</p>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#each toolTypes as tool (tool.value)}
|
{#each toolTypes as tool (tool.value)}
|
||||||
<label class="flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-gray-50">
|
<label class="flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={config.disabledTools.includes(tool.label)}
|
checked={config.disabledTools.includes(tool.label)}
|
||||||
onchange={() => toggleTool(tool.label)}
|
onchange={() => toggleTool(tool.label)}
|
||||||
class="rounded border-gray-300 text-blue-600"
|
class="rounded border-gray-300 dark:border-gray-600 text-blue-600"
|
||||||
/>
|
/>
|
||||||
<span class="text-gray-700">{tool.label}</span>
|
<span class="text-gray-700 dark:text-gray-300">{tool.label}</span>
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -240,19 +240,19 @@
|
|||||||
|
|
||||||
<!-- Granted Permissions -->
|
<!-- Granted Permissions -->
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-xs font-medium text-gray-700">Granted Permissions</p>
|
<p class="mb-2 text-xs font-medium text-gray-700 dark:text-gray-300">Granted Permissions</p>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newPermission}
|
bind:value={newPermission}
|
||||||
onkeydown={(e) => { if (e.key === 'Enter') addPermission(); }}
|
onkeydown={(e) => { if (e.key === 'Enter') addPermission(); }}
|
||||||
placeholder="agent_type:tool"
|
placeholder="agent_type:tool"
|
||||||
class="flex-1 rounded-lg border border-gray-300 px-2 py-1.5 text-xs focus:border-blue-500 focus:outline-none"
|
class="flex-1 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1.5 text-xs text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={addPermission}
|
onclick={addPermission}
|
||||||
class="rounded-lg bg-gray-100 px-2 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200"
|
class="rounded-lg bg-gray-100 dark:bg-gray-700 px-2 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</button>
|
</button>
|
||||||
@@ -260,14 +260,14 @@
|
|||||||
{#if config.grantedPermissions.length > 0}
|
{#if config.grantedPermissions.length > 0}
|
||||||
<div class="mt-2 space-y-1">
|
<div class="mt-2 space-y-1">
|
||||||
{#each config.grantedPermissions as perm (perm)}
|
{#each config.grantedPermissions as perm (perm)}
|
||||||
<div class="flex items-center justify-between rounded bg-gray-50 px-2 py-1">
|
<div class="flex items-center justify-between rounded bg-gray-50 dark:bg-gray-800 px-2 py-1">
|
||||||
<code class="text-xs text-gray-700">{perm}</code>
|
<code class="text-xs text-gray-700 dark:text-gray-300">{perm}</code>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => removePermission(perm)}
|
onclick={() => removePermission(perm)}
|
||||||
class="text-xs text-gray-400 hover:text-red-500"
|
class="text-xs text-gray-400 dark:text-gray-500 hover:text-red-500"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
const statusConfig = $derived.by(() => {
|
const statusConfig = $derived.by(() => {
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
case ResultStatus.SUCCESS:
|
case ResultStatus.SUCCESS:
|
||||||
return { label: 'Success', bg: 'bg-green-100', text: 'text-green-800', border: 'border-green-200' };
|
return { label: 'Success', bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-800 dark:text-green-300', border: 'border-green-200 dark:border-green-800' };
|
||||||
case ResultStatus.PARTIAL:
|
case ResultStatus.PARTIAL:
|
||||||
return { label: 'Partial', bg: 'bg-amber-100', text: 'text-amber-800', border: 'border-amber-200' };
|
return { label: 'Partial', bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-800 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800' };
|
||||||
case ResultStatus.FAILED:
|
case ResultStatus.FAILED:
|
||||||
return { label: 'Failed', bg: 'bg-red-100', text: 'text-red-800', border: 'border-red-200' };
|
return { label: 'Failed', bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-800 dark:text-red-300', border: 'border-red-200 dark:border-red-800' };
|
||||||
default:
|
default:
|
||||||
return { label: 'Unknown', bg: 'bg-gray-100', text: 'text-gray-800', border: 'border-gray-200' };
|
return { label: 'Unknown', bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-800 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-700' };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
{statusConfig.label}
|
{statusConfig.label}
|
||||||
</span>
|
</span>
|
||||||
{#if qualityLabel}
|
{#if qualityLabel}
|
||||||
<span class="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
|
<span class="rounded-full bg-blue-100 dark:bg-blue-900/40 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-300">
|
||||||
{qualityLabel}
|
{qualityLabel}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if sourceLabel}
|
{#if sourceLabel}
|
||||||
<span class="rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800">
|
<span class="rounded-full bg-purple-100 dark:bg-purple-900/40 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:text-purple-300">
|
||||||
{sourceLabel}
|
{sourceLabel}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -58,17 +58,17 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if result.failureReason}
|
{#if result.failureReason}
|
||||||
<p class="mt-2 text-sm text-red-700">Reason: {result.failureReason}</p>
|
<p class="mt-2 text-sm text-red-700 dark:text-red-400">Reason: {result.failureReason}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if result.artifacts.length > 0}
|
{#if result.artifacts.length > 0}
|
||||||
<div class="mt-3 border-t {statusConfig.border} pt-3">
|
<div class="mt-3 border-t {statusConfig.border} pt-3">
|
||||||
<p class="mb-1.5 text-xs font-medium text-gray-600">Artifacts</p>
|
<p class="mb-1.5 text-xs font-medium text-gray-600 dark:text-gray-400">Artifacts</p>
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
{#each result.artifacts as artifact (artifact)}
|
{#each result.artifacts as artifact (artifact)}
|
||||||
<li class="flex items-center gap-2 text-sm">
|
<li class="flex items-center gap-2 text-sm">
|
||||||
<span class="text-gray-400">📄</span>
|
<span class="text-gray-400 dark:text-gray-500">📄</span>
|
||||||
<span class="font-mono text-xs text-gray-700">{artifact}</span>
|
<span class="font-mono text-xs text-gray-700 dark:text-gray-300">{artifact}</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LineageNode } from '$lib/types/lineage';
|
import type { LineageNode } from '$lib/types/lineage';
|
||||||
import { agentTypeLabel, agentTypeColor } from '$lib/types/lineage';
|
import { agentTypeLabel, agentTypeColor } from '$lib/types/lineage';
|
||||||
|
import { themeStore } from '$lib/stores/theme.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
nodes,
|
nodes,
|
||||||
@@ -167,6 +168,12 @@
|
|||||||
const svgWidth = $derived(layout.totalWidth + 40);
|
const svgWidth = $derived(layout.totalWidth + 40);
|
||||||
const svgHeight = $derived(layout.totalHeight + 40);
|
const svgHeight = $derived(layout.totalHeight + 40);
|
||||||
|
|
||||||
|
// Dark-mode aware colors for SVG elements
|
||||||
|
const edgeColor = $derived(themeStore.isDark ? '#4b5563' : '#d1d5db');
|
||||||
|
const defaultStroke = $derived(themeStore.isDark ? '#4b5563' : '#e5e7eb');
|
||||||
|
const idTextColor = $derived(themeStore.isDark ? '#9ca3af' : '#6b7280');
|
||||||
|
const depthTextColor = $derived(themeStore.isDark ? '#6b7280' : '#9ca3af');
|
||||||
|
|
||||||
function handleNodeClick(node: LineageNode) {
|
function handleNodeClick(node: LineageNode) {
|
||||||
selectedNodeId = selectedNodeId === node.id ? null : node.id;
|
selectedNodeId = selectedNodeId === node.id ? null : node.id;
|
||||||
onSelectNode?.(node);
|
onSelectNode?.(node);
|
||||||
@@ -188,9 +195,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="overflow-auto rounded-lg border border-gray-200 bg-white">
|
<div class="overflow-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||||
{#if nodes.length === 0}
|
{#if nodes.length === 0}
|
||||||
<div class="flex items-center justify-center p-12 text-sm text-gray-400">
|
<div class="flex items-center justify-center p-12 text-sm text-gray-400 dark:text-gray-500">
|
||||||
No lineage data available
|
No lineage data available
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -206,7 +213,7 @@
|
|||||||
<path
|
<path
|
||||||
d={edgePath(edge)}
|
d={edgePath(edge)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#d1d5db"
|
stroke={edgeColor}
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
@@ -235,7 +242,7 @@
|
|||||||
rx="8"
|
rx="8"
|
||||||
ry="8"
|
ry="8"
|
||||||
fill={colors.fill}
|
fill={colors.fill}
|
||||||
stroke={isSelected ? colors.stroke : '#e5e7eb'}
|
stroke={isSelected ? colors.stroke : defaultStroke}
|
||||||
stroke-width={isSelected ? 2.5 : 1.5}
|
stroke-width={isSelected ? 2.5 : 1.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -256,7 +263,7 @@
|
|||||||
x={NODE_WIDTH / 2}
|
x={NODE_WIDTH / 2}
|
||||||
y="38"
|
y="38"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
fill="#6b7280"
|
fill={idTextColor}
|
||||||
font-size="10"
|
font-size="10"
|
||||||
font-family="monospace"
|
font-family="monospace"
|
||||||
>
|
>
|
||||||
@@ -268,7 +275,7 @@
|
|||||||
x={NODE_WIDTH / 2}
|
x={NODE_WIDTH / 2}
|
||||||
y="54"
|
y="54"
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
fill="#9ca3af"
|
fill={depthTextColor}
|
||||||
font-size="10"
|
font-size="10"
|
||||||
>
|
>
|
||||||
depth: {pn.node.spawnDepth}
|
depth: {pn.node.spawnDepth}
|
||||||
|
|||||||
@@ -7,26 +7,26 @@
|
|||||||
const sourceBadge = $derived.by(() => {
|
const sourceBadge = $derived.by(() => {
|
||||||
switch (candidate.source) {
|
switch (candidate.source) {
|
||||||
case ResultSource.TOOL_OUTPUT:
|
case ResultSource.TOOL_OUTPUT:
|
||||||
return { label: 'Tool Output', bg: 'bg-blue-100', text: 'text-blue-800' };
|
return { label: 'Tool Output', bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-800 dark:text-blue-300' };
|
||||||
case ResultSource.MODEL_KNOWLEDGE:
|
case ResultSource.MODEL_KNOWLEDGE:
|
||||||
return { label: 'Model Knowledge', bg: 'bg-purple-100', text: 'text-purple-800' };
|
return { label: 'Model Knowledge', bg: 'bg-purple-100 dark:bg-purple-900/40', text: 'text-purple-800 dark:text-purple-300' };
|
||||||
case ResultSource.WEB:
|
case ResultSource.WEB:
|
||||||
return { label: 'Web', bg: 'bg-green-100', text: 'text-green-800' };
|
return { label: 'Web', bg: 'bg-green-100 dark:bg-green-900/40', text: 'text-green-800 dark:text-green-300' };
|
||||||
default:
|
default:
|
||||||
return { label: 'Unspecified', bg: 'bg-gray-100', text: 'text-gray-800' };
|
return { label: 'Unspecified', bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-800 dark:text-gray-300' };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const confidencePct = $derived(Math.round(candidate.confidence * 100));
|
const confidencePct = $derived(Math.round(candidate.confidence * 100));
|
||||||
|
|
||||||
const confidenceColor = $derived.by(() => {
|
const confidenceColor = $derived.by(() => {
|
||||||
if (candidate.confidence >= 0.8) return { bar: 'bg-green-500', text: 'text-green-700' };
|
if (candidate.confidence >= 0.8) return { bar: 'bg-green-500', text: 'text-green-700 dark:text-green-400' };
|
||||||
if (candidate.confidence >= 0.5) return { bar: 'bg-amber-500', text: 'text-amber-700' };
|
if (candidate.confidence >= 0.5) return { bar: 'bg-amber-500', text: 'text-amber-700 dark:text-amber-400' };
|
||||||
return { bar: 'bg-red-500', text: 'text-red-700' };
|
return { bar: 'bg-red-500', text: 'text-red-700 dark:text-red-400' };
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-4">
|
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
|
||||||
<div class="mb-2 flex items-center justify-between gap-2">
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
<span
|
<span
|
||||||
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {sourceBadge.bg} {sourceBadge.text}"
|
class="shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {sourceBadge.bg} {sourceBadge.text}"
|
||||||
@@ -38,10 +38,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-3 text-sm text-gray-700">{candidate.content}</p>
|
<p class="mb-3 text-sm text-gray-700 dark:text-gray-300">{candidate.content}</p>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="h-2 flex-1 overflow-hidden rounded-full bg-gray-100">
|
<div class="h-2 flex-1 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-700">
|
||||||
<div
|
<div
|
||||||
class="h-full rounded-full transition-all {confidenceColor.bar}"
|
class="h-full rounded-full transition-all {confidenceColor.bar}"
|
||||||
style="width: {confidencePct}%"
|
style="width: {confidencePct}%"
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
<div
|
<div
|
||||||
class="max-w-[75%] rounded-2xl px-4 py-2.5 {isUser
|
class="max-w-[75%] rounded-2xl px-4 py-2.5 {isUser
|
||||||
? 'bg-blue-600 text-white rounded-br-md'
|
? 'bg-blue-600 text-white rounded-br-md'
|
||||||
: 'bg-gray-200 text-gray-900 rounded-bl-md'}"
|
: 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-bl-md'}"
|
||||||
>
|
>
|
||||||
<p class="text-sm whitespace-pre-wrap">{message.content}</p>
|
<p class="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||||
<time class="mt-1 block text-xs {isUser ? 'text-blue-200' : 'text-gray-500'}">
|
<time class="mt-1 block text-xs {isUser ? 'text-blue-200' : 'text-gray-500 dark:text-gray-400'}">
|
||||||
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border-t border-gray-200 bg-white p-4">
|
<div class="border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4">
|
||||||
<form onsubmit={handleSubmit} class="flex items-end gap-2">
|
<form onsubmit={handleSubmit} class="flex items-end gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
bind:this={textarea}
|
bind:this={textarea}
|
||||||
@@ -45,9 +45,10 @@
|
|||||||
{disabled}
|
{disabled}
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
rows="1"
|
rows="1"
|
||||||
class="flex-1 resize-none rounded-xl border border-gray-300 px-4 py-2.5 text-sm
|
class="flex-1 resize-none rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2.5 text-sm text-gray-900 dark:text-gray-100
|
||||||
focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none
|
focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none
|
||||||
disabled:bg-gray-100 disabled:text-gray-400"
|
disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500
|
||||||
|
placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
disabled={disabled || !input.trim()}
|
disabled={disabled || !input.trim()}
|
||||||
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white
|
class="rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white
|
||||||
hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none
|
hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none
|
||||||
disabled:bg-gray-300 disabled:cursor-not-allowed"
|
disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={container} class="flex-1 overflow-y-auto p-4">
|
<div bind:this={container} class="flex-1 overflow-y-auto p-4 bg-white dark:bg-gray-900">
|
||||||
{#if messages.length === 0}
|
{#if messages.length === 0}
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<div class="text-center text-gray-400">
|
<div class="text-center text-gray-400 dark:text-gray-500">
|
||||||
<p class="text-lg font-medium">No messages yet</p>
|
<p class="text-lg font-medium">No messages yet</p>
|
||||||
<p class="mt-1 text-sm">Send a message to start a conversation</p>
|
<p class="mt-1 text-sm">Send a message to start a conversation</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-4 mb-2 rounded-xl bg-gray-50 px-4 py-3">
|
<div class="mx-4 mb-2 rounded-xl bg-gray-50 dark:bg-gray-800 px-4 py-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
{#each phases as phase, i (phase.state)}
|
{#each phases as phase, i (phase.state)}
|
||||||
{@const status = getStatus(phase.state)}
|
{@const status = getStatus(phase.state)}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
{status === 'completed'
|
{status === 'completed'
|
||||||
? 'bg-green-500 text-white'
|
? 'bg-green-500 text-white'
|
||||||
: status === 'active'
|
: status === 'active'
|
||||||
? 'bg-blue-500 text-white ring-2 ring-blue-300 ring-offset-1'
|
? 'bg-blue-500 text-white ring-2 ring-blue-300 dark:ring-blue-600 ring-offset-1 dark:ring-offset-gray-800'
|
||||||
: 'bg-gray-200 text-gray-500'}"
|
: 'bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400'}"
|
||||||
>
|
>
|
||||||
{#if status === 'completed'}
|
{#if status === 'completed'}
|
||||||
✓
|
✓
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="text-xs transition-colors duration-300
|
class="text-xs transition-colors duration-300
|
||||||
{status === 'active' ? 'font-medium text-blue-600' : 'text-gray-500'}"
|
{status === 'active' ? 'font-medium text-blue-600 dark:text-blue-400' : 'text-gray-500 dark:text-gray-400'}"
|
||||||
>
|
>
|
||||||
{phase.label}
|
{phase.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
{#if i < phases.length - 1}
|
{#if i < phases.length - 1}
|
||||||
<div
|
<div
|
||||||
class="mb-5 h-0.5 flex-1 transition-colors duration-300
|
class="mb-5 h-0.5 flex-1 transition-colors duration-300
|
||||||
{getStatus(phases[i + 1].state) !== 'pending' ? 'bg-green-500' : 'bg-gray-200'}"
|
{getStatus(phases[i + 1].state) !== 'pending' ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-600'}"
|
||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="flex h-full w-64 flex-col border-r border-gray-200 bg-gray-50">
|
<aside class="flex h-full w-64 flex-col border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||||
<div class="border-b border-gray-200 p-3">
|
<div class="border-b border-gray-200 dark:border-gray-700 p-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={onNewChat}
|
onclick={onNewChat}
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{#if sessions.length === 0}
|
{#if sessions.length === 0}
|
||||||
<p class="p-4 text-center text-sm text-gray-400">No sessions yet</p>
|
<p class="p-4 text-center text-sm text-gray-400 dark:text-gray-500">No sessions yet</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each sessions as session (session.id)}
|
{#each sessions as session (session.id)}
|
||||||
<div
|
<div
|
||||||
@@ -56,23 +56,23 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
onclick={() => onSelectSession(session.id)}
|
onclick={() => onSelectSession(session.id)}
|
||||||
onkeydown={(e) => { if (e.key === 'Enter') onSelectSession(session.id); }}
|
onkeydown={(e) => { if (e.key === 'Enter') onSelectSession(session.id); }}
|
||||||
class="group flex w-full cursor-pointer items-start gap-2 border-b border-gray-100 px-3 py-3 text-left hover:bg-gray-100
|
class="group flex w-full cursor-pointer items-start gap-2 border-b border-gray-100 dark:border-gray-700 px-3 py-3 text-left hover:bg-gray-100 dark:hover:bg-gray-700
|
||||||
{activeId === session.id ? 'bg-blue-50 border-l-2 border-l-blue-500' : ''}"
|
{activeId === session.id ? 'bg-blue-50 dark:bg-blue-900/30 border-l-2 border-l-blue-500' : ''}"
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm font-medium text-gray-900">{session.title}</p>
|
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{session.title}</p>
|
||||||
<p class="mt-0.5 text-xs text-gray-500">{formatDate(session.createdAt)}</p>
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">{formatDate(session.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={(e) => handleDelete(e, session.id)}
|
onclick={(e) => handleDelete(e, session.id)}
|
||||||
class="shrink-0 rounded p-1 text-xs opacity-0 group-hover:opacity-100
|
class="shrink-0 rounded p-1 text-xs opacity-0 group-hover:opacity-100
|
||||||
{confirmDeleteId === session.id
|
{confirmDeleteId === session.id
|
||||||
? 'bg-red-100 text-red-600'
|
? 'bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-400'
|
||||||
: 'text-gray-400 hover:bg-gray-200 hover:text-gray-600'}"
|
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-600 dark:hover:text-gray-300'}"
|
||||||
title={confirmDeleteId === session.id ? 'Click again to confirm' : 'Delete session'}
|
title={confirmDeleteId === session.id ? 'Click again to confirm' : 'Delete session'}
|
||||||
>
|
>
|
||||||
{confirmDeleteId === session.id ? '✓' : '✕'}
|
{confirmDeleteId === session.id ? '\u2713' : '\u2715'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
37
src/lib/components/ThemeToggle.svelte
Normal file
37
src/lib/components/ThemeToggle.svelte
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { themeStore } from '$lib/stores/theme.svelte';
|
||||||
|
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
system: 'System',
|
||||||
|
light: 'Light',
|
||||||
|
dark: 'Dark'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => themeStore.cycle()}
|
||||||
|
class="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
|
||||||
|
title="Theme: {labels[themeStore.mode]} (click to cycle)"
|
||||||
|
>
|
||||||
|
{#if themeStore.mode === 'light'}
|
||||||
|
<!-- Sun icon -->
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||||
|
</svg>
|
||||||
|
{:else if themeStore.mode === 'dark'}
|
||||||
|
<!-- Moon icon -->
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<!-- Monitor/system icon -->
|
||||||
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||||
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span class="text-xs">{labels[themeStore.mode]}</span>
|
||||||
|
</button>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => (expanded = !expanded)}
|
onclick={() => (expanded = !expanded)}
|
||||||
class="flex w-full items-center gap-2 rounded-lg bg-amber-50 px-3 py-2 text-left text-sm text-amber-700 hover:bg-amber-100 transition-colors"
|
class="flex w-full items-center gap-2 rounded-lg bg-amber-50 dark:bg-amber-900/30 px-3 py-2 text-left text-sm text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="inline-block transition-transform duration-200 {expanded
|
class="inline-block transition-transform duration-200 {expanded
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<div
|
<div
|
||||||
class="mt-1 rounded-b-lg border border-t-0 border-amber-200 bg-amber-50/50 px-4 py-3 text-sm text-amber-800 whitespace-pre-wrap"
|
class="mt-1 rounded-b-lg border border-t-0 border-amber-200 dark:border-amber-700 bg-amber-50/50 dark:bg-amber-900/20 px-4 py-3 text-sm text-amber-800 dark:text-amber-200 whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
86
src/lib/stores/theme.svelte.ts
Normal file
86
src/lib/stores/theme.svelte.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'llm-multiverse-theme';
|
||||||
|
|
||||||
|
function createThemeStore() {
|
||||||
|
let mode: ThemeMode = $state('system');
|
||||||
|
let resolvedDark = $state(false);
|
||||||
|
|
||||||
|
function applyTheme(isDark: boolean) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const root = document.documentElement;
|
||||||
|
// Add transition class for smooth switching
|
||||||
|
root.classList.add('theme-transition');
|
||||||
|
if (isDark) {
|
||||||
|
root.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
root.classList.remove('dark');
|
||||||
|
}
|
||||||
|
// Remove transition class after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
root.classList.remove('theme-transition');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemPreference(): boolean {
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Load saved preference
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY) as ThemeMode | null;
|
||||||
|
if (saved === 'light' || saved === 'dark' || saved === 'system') {
|
||||||
|
mode = saved;
|
||||||
|
} else {
|
||||||
|
mode = 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for system preference changes
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
mediaQuery.addEventListener('change', (e) => {
|
||||||
|
if (mode === 'system') {
|
||||||
|
resolvedDark = e.matches;
|
||||||
|
applyTheme(resolvedDark);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply initial theme
|
||||||
|
resolvedDark =
|
||||||
|
mode === 'dark' ? true : mode === 'light' ? false : getSystemPreference();
|
||||||
|
applyTheme(resolvedDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMode(newMode: ThemeMode) {
|
||||||
|
mode = newMode;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, newMode);
|
||||||
|
}
|
||||||
|
resolvedDark =
|
||||||
|
newMode === 'dark' ? true : newMode === 'light' ? false : getSystemPreference();
|
||||||
|
applyTheme(resolvedDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycle() {
|
||||||
|
const order: ThemeMode[] = ['system', 'light', 'dark'];
|
||||||
|
const idx = order.indexOf(mode);
|
||||||
|
const next = order[(idx + 1) % order.length];
|
||||||
|
setMode(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get mode() {
|
||||||
|
return mode;
|
||||||
|
},
|
||||||
|
get isDark() {
|
||||||
|
return resolvedDark;
|
||||||
|
},
|
||||||
|
init,
|
||||||
|
setMode,
|
||||||
|
cycle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeStore = createThemeStore();
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import { themeStore } from '$lib/stores/theme.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
themeStore.init();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -10,3 +15,13 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.theme-transition),
|
||||||
|
:global(.theme-transition *) {
|
||||||
|
transition:
|
||||||
|
background-color 0.3s ease,
|
||||||
|
border-color 0.3s ease,
|
||||||
|
color 0.3s ease !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { resolveRoute } from '$app/paths';
|
import { resolveRoute } from '$app/paths';
|
||||||
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
|
|
||||||
const chatHref = resolveRoute('/chat');
|
const chatHref = resolveRoute('/chat');
|
||||||
const lineageHref = resolveRoute('/lineage');
|
const lineageHref = resolveRoute('/lineage');
|
||||||
@@ -7,15 +8,20 @@
|
|||||||
const auditHref = resolveRoute('/audit');
|
const auditHref = resolveRoute('/audit');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-3xl font-bold text-center mt-8">LLM Multiverse UI</h1>
|
<div class="flex min-h-screen flex-col items-center bg-white dark:bg-gray-900">
|
||||||
<p class="text-center text-gray-600 mt-2">Orchestration interface</p>
|
<div class="absolute right-4 top-4">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-center mt-8 text-gray-900 dark:text-gray-100">LLM Multiverse UI</h1>
|
||||||
|
<p class="text-center text-gray-600 dark:text-gray-400 mt-2">Orchestration interface</p>
|
||||||
<nav class="flex justify-center gap-4 mt-6">
|
<nav class="flex justify-center gap-4 mt-6">
|
||||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a href={chatHref} class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">Chat</a>
|
<a href={chatHref} class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">Chat</a>
|
||||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a href={lineageHref} class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200">Agent Lineage</a>
|
<a href={lineageHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Agent Lineage</a>
|
||||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a href={memoryHref} class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200">Memory Candidates</a>
|
<a href={memoryHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Memory Candidates</a>
|
||||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a href={auditHref} class="rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200">Audit Log</a>
|
<a href={auditHref} class="rounded-lg bg-gray-100 dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600">Audit Log</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { resolveRoute } from '$app/paths';
|
import { resolveRoute } from '$app/paths';
|
||||||
import AuditTimeline from '$lib/components/AuditTimeline.svelte';
|
import AuditTimeline from '$lib/components/AuditTimeline.svelte';
|
||||||
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
import { auditStore } from '$lib/stores/audit.svelte';
|
import { auditStore } from '$lib/stores/audit.svelte';
|
||||||
import type { AuditEventType } from '$lib/stores/audit.svelte';
|
import type { AuditEventType } from '$lib/stores/audit.svelte';
|
||||||
|
|
||||||
@@ -38,30 +39,33 @@
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen flex-col bg-gray-50">
|
<div class="flex h-screen flex-col bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-3">
|
<header class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-3">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a
|
<a
|
||||||
href={chatHref}
|
href={chatHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
← Chat
|
← Chat
|
||||||
</a>
|
</a>
|
||||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||||
<h1 class="text-lg font-semibold text-gray-900">Audit Log</h1>
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Audit Log</h1>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-md bg-amber-50 px-2 py-1 text-xs text-amber-700">
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<span class="rounded-md bg-amber-50 dark:bg-amber-900/30 px-2 py-1 text-xs text-amber-700 dark:text-amber-300">
|
||||||
Sample Data
|
Sample Data
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="border-b border-gray-200 bg-white px-6 py-3">
|
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-3">
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="session-select" class="text-xs font-medium text-gray-500">Session</label>
|
<label for="session-select" class="text-xs font-medium text-gray-500 dark:text-gray-400">Session</label>
|
||||||
<select
|
<select
|
||||||
id="session-select"
|
id="session-select"
|
||||||
value={selectedSessionId ?? ''}
|
value={selectedSessionId ?? ''}
|
||||||
@@ -69,7 +73,7 @@
|
|||||||
const target = e.target as HTMLSelectElement;
|
const target = e.target as HTMLSelectElement;
|
||||||
if (target.value) selectSession(target.value);
|
if (target.value) selectSession(target.value);
|
||||||
}}
|
}}
|
||||||
class="rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-sm text-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2.5 py-1.5 text-sm text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select a session</option>
|
<option value="" disabled>Select a session</option>
|
||||||
{#each allSessions as session (session.sessionId)}
|
{#each allSessions as session (session.sessionId)}
|
||||||
@@ -82,11 +86,11 @@
|
|||||||
|
|
||||||
{#if selectedSessionId}
|
{#if selectedSessionId}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="type-filter" class="text-xs font-medium text-gray-500">Type</label>
|
<label for="type-filter" class="text-xs font-medium text-gray-500 dark:text-gray-400">Type</label>
|
||||||
<select
|
<select
|
||||||
id="type-filter"
|
id="type-filter"
|
||||||
bind:value={typeFilter}
|
bind:value={typeFilter}
|
||||||
class="rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-sm text-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2.5 py-1.5 text-sm text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
{#each filterOptions as opt (opt.value)}
|
{#each filterOptions as opt (opt.value)}
|
||||||
<option value={opt.value}>{opt.label}</option>
|
<option value={opt.value}>{opt.label}</option>
|
||||||
@@ -94,7 +98,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
{totalEvents} event{totalEvents !== 1 ? 's' : ''}
|
{totalEvents} event{totalEvents !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -106,17 +110,17 @@
|
|||||||
{#if !selectedSessionId}
|
{#if !selectedSessionId}
|
||||||
{#if allSessions.length === 0}
|
{#if allSessions.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div class="mb-3 text-4xl text-gray-300">📋</div>
|
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">📋</div>
|
||||||
<p class="text-sm font-medium text-gray-500">No audit events recorded</p>
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No audit events recorded</p>
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
Events will appear here as orchestration sessions run.
|
Events will appear here as orchestration sessions run.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div class="mb-3 text-4xl text-gray-300">🔍</div>
|
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">🔍</div>
|
||||||
<p class="text-sm font-medium text-gray-500">Select a session to view its audit log</p>
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Select a session to view its audit log</p>
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
Choose a session from the dropdown above.
|
Choose a session from the dropdown above.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import FinalResult from '$lib/components/FinalResult.svelte';
|
import FinalResult from '$lib/components/FinalResult.svelte';
|
||||||
import SessionSidebar from '$lib/components/SessionSidebar.svelte';
|
import SessionSidebar from '$lib/components/SessionSidebar.svelte';
|
||||||
import ConfigSidebar from '$lib/components/ConfigSidebar.svelte';
|
import ConfigSidebar from '$lib/components/ConfigSidebar.svelte';
|
||||||
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
|
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
|
||||||
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
||||||
import { sessionStore } from '$lib/stores/sessions.svelte';
|
import { sessionStore } from '$lib/stores/sessions.svelte';
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
const idx = messages.length - 1;
|
const idx = messages.length - 1;
|
||||||
messages[idx] = {
|
messages[idx] = {
|
||||||
...messages[idx],
|
...messages[idx],
|
||||||
content: `⚠ ${msg}`
|
content: `\u26A0 ${msg}`
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
isStreaming = false;
|
isStreaming = false;
|
||||||
@@ -162,38 +163,39 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen">
|
<div class="flex h-screen bg-white dark:bg-gray-900">
|
||||||
<SessionSidebar onSelectSession={handleSelectSession} onNewChat={handleNewChat} />
|
<SessionSidebar onSelectSession={handleSelectSession} onNewChat={handleNewChat} />
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<header class="flex items-center justify-between border-b border-gray-200 bg-white px-4 py-3">
|
<header class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-3">
|
||||||
<h1 class="text-lg font-semibold text-gray-900">Chat</h1>
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Chat</h1>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a
|
<a
|
||||||
href={lineageHref}
|
href={lineageHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
Lineage
|
Lineage
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={memoryHref}
|
href={memoryHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
Memory
|
Memory
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={auditHref}
|
href={auditHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
Audit
|
Audit
|
||||||
</a>
|
</a>
|
||||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||||
|
<ThemeToggle />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => (showConfig = !showConfig)}
|
onclick={() => (showConfig = !showConfig)}
|
||||||
class="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-sm
|
class="flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-sm
|
||||||
{showConfig ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}"
|
{showConfig ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300' : 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'}"
|
||||||
>
|
>
|
||||||
{#if isNonDefaultConfig}
|
{#if isNonDefaultConfig}
|
||||||
<span class="h-2 w-2 rounded-full bg-amber-500"></span>
|
<span class="h-2 w-2 rounded-full bg-amber-500"></span>
|
||||||
@@ -212,14 +214,14 @@
|
|||||||
|
|
||||||
{#if isStreaming && messages.length > 0 && messages[messages.length - 1].content === ''}
|
{#if isStreaming && messages.length > 0 && messages[messages.length - 1].content === ''}
|
||||||
<div class="flex justify-start px-4 pb-2">
|
<div class="flex justify-start px-4 pb-2">
|
||||||
<div class="flex items-center gap-1.5 rounded-2xl bg-gray-200 px-4 py-2.5">
|
<div class="flex items-center gap-1.5 rounded-2xl bg-gray-200 dark:bg-gray-700 px-4 py-2.5">
|
||||||
<span class="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:0ms]"
|
<span class="h-2 w-2 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400 [animation-delay:0ms]"
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:150ms]"
|
class="h-2 w-2 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400 [animation-delay:150ms]"
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 animate-bounce rounded-full bg-gray-500 [animation-delay:300ms]"
|
class="h-2 w-2 animate-bounce rounded-full bg-gray-500 dark:bg-gray-400 [animation-delay:300ms]"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +232,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="mx-4 mb-2 rounded-lg bg-red-50 px-4 py-2 text-sm text-red-600">
|
<div class="mx-4 mb-2 rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-2 text-sm text-red-600 dark:text-red-400">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { resolveRoute } from '$app/paths';
|
import { resolveRoute } from '$app/paths';
|
||||||
import { AgentType } from '$lib/proto/llm_multiverse/v1/common_pb';
|
import { AgentType } from '$lib/proto/llm_multiverse/v1/common_pb';
|
||||||
import LineageTree from '$lib/components/LineageTree.svelte';
|
import LineageTree from '$lib/components/LineageTree.svelte';
|
||||||
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
import type { LineageNode, SimpleAgentIdentifier } from '$lib/types/lineage';
|
import type { LineageNode, SimpleAgentIdentifier } from '$lib/types/lineage';
|
||||||
import {
|
import {
|
||||||
buildLineageTree,
|
buildLineageTree,
|
||||||
@@ -30,23 +31,26 @@
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen flex-col bg-gray-50">
|
<div class="flex h-screen flex-col bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-3">
|
<header class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-3">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a
|
<a
|
||||||
href={chatHref}
|
href={chatHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
← Chat
|
← Chat
|
||||||
</a>
|
</a>
|
||||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||||
<h1 class="text-lg font-semibold text-gray-900">Agent Lineage</h1>
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Agent Lineage</h1>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-md bg-amber-50 px-2 py-1 text-xs text-amber-700">
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<span class="rounded-md bg-amber-50 dark:bg-amber-900/30 px-2 py-1 text-xs text-amber-700 dark:text-amber-300">
|
||||||
Sample Data
|
Sample Data
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex flex-1 overflow-hidden">
|
<div class="flex flex-1 overflow-hidden">
|
||||||
@@ -55,8 +59,8 @@
|
|||||||
<LineageTree nodes={treeNodes} onSelectNode={handleSelectNode} />
|
<LineageTree nodes={treeNodes} onSelectNode={handleSelectNode} />
|
||||||
|
|
||||||
<!-- Legend -->
|
<!-- Legend -->
|
||||||
<div class="mt-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white px-4 py-3">
|
<div class="mt-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-4 py-3">
|
||||||
<span class="text-xs font-medium text-gray-500">Agent Types:</span>
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Agent Types:</span>
|
||||||
{#each agentTypeLegend as type (type)}
|
{#each agentTypeLegend as type (type)}
|
||||||
{@const colors = agentTypeColor(type)}
|
{@const colors = agentTypeColor(type)}
|
||||||
<span
|
<span
|
||||||
@@ -76,14 +80,14 @@
|
|||||||
{#if selectedNode}
|
{#if selectedNode}
|
||||||
{@const colors = agentTypeColor(selectedNode.agentType)}
|
{@const colors = agentTypeColor(selectedNode.agentType)}
|
||||||
<aside
|
<aside
|
||||||
class="w-72 shrink-0 border-l border-gray-200 bg-white p-4 overflow-y-auto"
|
class="w-72 shrink-0 border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-4 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<h2 class="text-sm font-semibold text-gray-900">Agent Details</h2>
|
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Agent Details</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => (selectedNode = null)}
|
onclick={() => (selectedNode = null)}
|
||||||
class="rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
|
class="rounded p-1 text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
aria-label="Close detail panel"
|
aria-label="Close detail panel"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
@@ -92,12 +96,12 @@
|
|||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium text-gray-500">Agent ID</p>
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Agent ID</p>
|
||||||
<p class="mt-0.5 break-all font-mono text-sm text-gray-900">{selectedNode.id}</p>
|
<p class="mt-0.5 break-all font-mono text-sm text-gray-900 dark:text-gray-100">{selectedNode.id}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium text-gray-500">Agent Type</p>
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Agent Type</p>
|
||||||
<span
|
<span
|
||||||
class="mt-0.5 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium {colors.badge}"
|
class="mt-0.5 inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium {colors.badge}"
|
||||||
>
|
>
|
||||||
@@ -110,29 +114,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium text-gray-500">Spawn Depth</p>
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Spawn Depth</p>
|
||||||
<p class="mt-0.5 text-sm text-gray-900">{selectedNode.spawnDepth}</p>
|
<p class="mt-0.5 text-sm text-gray-900 dark:text-gray-100">{selectedNode.spawnDepth}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium text-gray-500">Children</p>
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Children</p>
|
||||||
<p class="mt-0.5 text-sm text-gray-900">{selectedNode.children.length}</p>
|
<p class="mt-0.5 text-sm text-gray-900 dark:text-gray-100">{selectedNode.children.length}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectedNode.children.length > 0}
|
{#if selectedNode.children.length > 0}
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium text-gray-500">Child Agents</p>
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Child Agents</p>
|
||||||
<ul class="mt-1 space-y-1">
|
<ul class="mt-1 space-y-1">
|
||||||
{#each selectedNode.children as child (child.id)}
|
{#each selectedNode.children as child (child.id)}
|
||||||
{@const childColors = agentTypeColor(child.agentType)}
|
{@const childColors = agentTypeColor(child.agentType)}
|
||||||
<li
|
<li
|
||||||
class="flex items-center gap-2 rounded-md border border-gray-100 px-2 py-1.5"
|
class="flex items-center gap-2 rounded-md border border-gray-100 dark:border-gray-700 px-2 py-1.5"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 shrink-0 rounded-full"
|
class="h-2 w-2 shrink-0 rounded-full"
|
||||||
style="background-color: {childColors.stroke}"
|
style="background-color: {childColors.stroke}"
|
||||||
></span>
|
></span>
|
||||||
<span class="truncate font-mono text-xs text-gray-700">{child.id}</span>
|
<span class="truncate font-mono text-xs text-gray-700 dark:text-gray-300">{child.id}</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { resolveRoute } from '$app/paths';
|
import { resolveRoute } from '$app/paths';
|
||||||
import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb';
|
import { ResultSource } from '$lib/proto/llm_multiverse/v1/common_pb';
|
||||||
import MemoryCandidateCard from '$lib/components/MemoryCandidateCard.svelte';
|
import MemoryCandidateCard from '$lib/components/MemoryCandidateCard.svelte';
|
||||||
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||||
import { memoryStore } from '$lib/stores/memory.svelte';
|
import { memoryStore } from '$lib/stores/memory.svelte';
|
||||||
|
|
||||||
const chatHref = resolveRoute('/chat');
|
const chatHref = resolveRoute('/chat');
|
||||||
@@ -37,34 +38,37 @@
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen flex-col bg-gray-50">
|
<div class="flex h-screen flex-col bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="flex items-center justify-between border-b border-gray-200 bg-white px-6 py-3">
|
<header class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-3">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||||
<a
|
<a
|
||||||
href={chatHref}
|
href={chatHref}
|
||||||
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
class="rounded-lg px-2.5 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
>
|
>
|
||||||
← Chat
|
← Chat
|
||||||
</a>
|
</a>
|
||||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||||
<h1 class="text-lg font-semibold text-gray-900">Memory Candidates</h1>
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Memory Candidates</h1>
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded-md bg-amber-50 px-2 py-1 text-xs text-amber-700">
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
<span class="rounded-md bg-amber-50 dark:bg-amber-900/30 px-2 py-1 text-xs text-amber-700 dark:text-amber-300">
|
||||||
Sample Data
|
Sample Data
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="border-b border-gray-200 bg-white px-6 py-3">
|
<div class="border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 py-3">
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="source-filter" class="text-xs font-medium text-gray-500">Source</label>
|
<label for="source-filter" class="text-xs font-medium text-gray-500 dark:text-gray-400">Source</label>
|
||||||
<select
|
<select
|
||||||
id="source-filter"
|
id="source-filter"
|
||||||
bind:value={sourceFilter}
|
bind:value={sourceFilter}
|
||||||
class="rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-sm text-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2.5 py-1.5 text-sm text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||||
>
|
>
|
||||||
{#each sourceOptions as opt (opt.value)}
|
{#each sourceOptions as opt (opt.value)}
|
||||||
<option value={opt.value}>{opt.label}</option>
|
<option value={opt.value}>{opt.label}</option>
|
||||||
@@ -73,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="confidence-threshold" class="text-xs font-medium text-gray-500">
|
<label for="confidence-threshold" class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
Min Confidence
|
Min Confidence
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -83,14 +87,14 @@
|
|||||||
max="1"
|
max="1"
|
||||||
step="0.05"
|
step="0.05"
|
||||||
bind:value={confidenceThreshold}
|
bind:value={confidenceThreshold}
|
||||||
class="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-600"
|
class="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700 accent-blue-600"
|
||||||
/>
|
/>
|
||||||
<span class="w-10 text-right text-xs font-medium text-gray-600">
|
<span class="w-10 text-right text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
{Math.round(confidenceThreshold * 100)}%
|
{Math.round(confidenceThreshold * 100)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="text-xs text-gray-400">
|
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
{totalCandidates} candidate{totalCandidates !== 1 ? 's' : ''}
|
{totalCandidates} candidate{totalCandidates !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,9 +104,9 @@
|
|||||||
<main class="flex-1 overflow-auto p-6">
|
<main class="flex-1 overflow-auto p-6">
|
||||||
{#if filteredSessions.length === 0}
|
{#if filteredSessions.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-16 text-center">
|
<div class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<div class="mb-3 text-4xl text-gray-300">📋</div>
|
<div class="mb-3 text-4xl text-gray-300 dark:text-gray-600">📋</div>
|
||||||
<p class="text-sm font-medium text-gray-500">No memory candidates found</p>
|
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">No memory candidates found</p>
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
{#if sourceFilter !== 'all' || confidenceThreshold > 0}
|
{#if sourceFilter !== 'all' || confidenceThreshold > 0}
|
||||||
Try adjusting your filters.
|
Try adjusting your filters.
|
||||||
{:else}
|
{:else}
|
||||||
@@ -115,10 +119,10 @@
|
|||||||
{#each filteredSessions as session (session.sessionId)}
|
{#each filteredSessions as session (session.sessionId)}
|
||||||
<section>
|
<section>
|
||||||
<div class="mb-3 flex items-center gap-3">
|
<div class="mb-3 flex items-center gap-3">
|
||||||
<h2 class="text-sm font-semibold text-gray-900">
|
<h2 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
Session: <span class="font-mono text-xs text-gray-600">{session.sessionId}</span>
|
Session: <span class="font-mono text-xs text-gray-600 dark:text-gray-400">{session.sessionId}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600">
|
<span class="rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
{session.candidates.length}
|
{session.candidates.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user