feat(frontend): add Phase 3 UI components for observability
Components created: - ValidationWarningsAlert: Display validation warnings with collapsible list - StructuredErrorDisplay: Parse and display HTTP 502 errors as bullet points - AutoFixBadge: Show automatically applied fixes - ReasoningChainViewer: Collapsible panel for LLM thinking process - MetadataDebugPanel: Collapsible debug info (model, duration, token metrics) CSS changes: - Add .editorial-alert--warning and .editorial-alert--info variants Integration: - Update routines/suggest page to show warnings, auto-fixes, reasoning, and metadata - Update products/suggest page with same observability components - Replace plain error divs with StructuredErrorDisplay for better UX All components follow design system and pass svelte-check with 0 errors
This commit is contained in:
parent
3c3248c2ea
commit
5d3f876bec
8 changed files with 310 additions and 3 deletions
|
|
@ -277,6 +277,18 @@ body {
|
|||
color: hsl(136 48% 26%);
|
||||
}
|
||||
|
||||
.editorial-alert--warning {
|
||||
border-color: hsl(42 78% 68%);
|
||||
background: hsl(45 86% 92%);
|
||||
color: hsl(36 68% 28%);
|
||||
}
|
||||
|
||||
.editorial-alert--info {
|
||||
border-color: hsl(204 56% 70%);
|
||||
background: hsl(207 72% 93%);
|
||||
color: hsl(207 78% 28%);
|
||||
}
|
||||
|
||||
.products-table-shell {
|
||||
border: 1px solid hsl(35 24% 74% / 0.85);
|
||||
border-radius: 0.9rem;
|
||||
|
|
|
|||
25
frontend/src/lib/components/AutoFixBadge.svelte
Normal file
25
frontend/src/lib/components/AutoFixBadge.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { Sparkles } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
autoFixes: string[];
|
||||
}
|
||||
|
||||
let { autoFixes }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if autoFixes && autoFixes.length > 0}
|
||||
<div class="editorial-alert editorial-alert--success">
|
||||
<div class="flex items-start gap-2">
|
||||
<Sparkles class="size-5 shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="font-medium mb-1">Automatically adjusted</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
{#each autoFixes as fix}
|
||||
<li>{fix}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
83
frontend/src/lib/components/MetadataDebugPanel.svelte
Normal file
83
frontend/src/lib/components/MetadataDebugPanel.svelte
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts">
|
||||
import { Info, ChevronDown, ChevronRight } from 'lucide-svelte';
|
||||
import type { ResponseMetadata } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
metadata?: ResponseMetadata;
|
||||
}
|
||||
|
||||
let { metadata }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return num.toLocaleString();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if metadata}
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
class="w-full flex items-center gap-2 px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Info class="size-4 text-gray-600" />
|
||||
<span class="text-sm font-medium text-gray-700">Debug Information</span>
|
||||
<div class="ml-auto">
|
||||
{#if expanded}
|
||||
<ChevronDown class="size-4 text-gray-500" />
|
||||
{:else}
|
||||
<ChevronRight class="size-4 text-gray-500" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{#if expanded}
|
||||
<div class="p-4 bg-white border-t border-gray-200">
|
||||
<dl class="grid grid-cols-1 gap-3 text-sm">
|
||||
<div>
|
||||
<dt class="font-medium text-gray-700">Model</dt>
|
||||
<dd class="text-gray-600 font-mono text-xs mt-0.5">{metadata.model_used}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-medium text-gray-700">Duration</dt>
|
||||
<dd class="text-gray-600">{formatNumber(metadata.duration_ms)} ms</dd>
|
||||
</div>
|
||||
{#if metadata.token_metrics}
|
||||
<div>
|
||||
<dt class="font-medium text-gray-700">Token Usage</dt>
|
||||
<dd class="text-gray-600 space-y-1 mt-0.5">
|
||||
<div class="flex justify-between">
|
||||
<span>Prompt:</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatNumber(metadata.token_metrics.prompt_tokens)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Completion:</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatNumber(metadata.token_metrics.completion_tokens)}</span
|
||||
>
|
||||
</div>
|
||||
{#if metadata.token_metrics.thoughts_tokens}
|
||||
<div class="flex justify-between">
|
||||
<span>Thinking:</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatNumber(metadata.token_metrics.thoughts_tokens)}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-between font-medium border-t border-gray-200 pt-1 mt-1">
|
||||
<span>Total:</span>
|
||||
<span class="font-mono text-xs"
|
||||
>{formatNumber(metadata.token_metrics.total_tokens)}</span
|
||||
>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
37
frontend/src/lib/components/ReasoningChainViewer.svelte
Normal file
37
frontend/src/lib/components/ReasoningChainViewer.svelte
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { Brain, ChevronDown, ChevronRight } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
reasoningChain?: string;
|
||||
}
|
||||
|
||||
let { reasoningChain }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
{#if reasoningChain}
|
||||
<div class="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
class="w-full flex items-center gap-2 px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Brain class="size-4 text-gray-600" />
|
||||
<span class="text-sm font-medium text-gray-700">AI Reasoning Process</span>
|
||||
<div class="ml-auto">
|
||||
{#if expanded}
|
||||
<ChevronDown class="size-4 text-gray-500" />
|
||||
{:else}
|
||||
<ChevronRight class="size-4 text-gray-500" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{#if expanded}
|
||||
<div class="p-4 bg-gray-50 border-t border-gray-200">
|
||||
<pre
|
||||
class="text-xs font-mono whitespace-pre-wrap text-gray-700 leading-relaxed">{reasoningChain}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
48
frontend/src/lib/components/StructuredErrorDisplay.svelte
Normal file
48
frontend/src/lib/components/StructuredErrorDisplay.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import { XCircle } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
error: string;
|
||||
}
|
||||
|
||||
let { error }: Props = $props();
|
||||
|
||||
// Parse semicolon-separated errors from backend validation failures
|
||||
const errors = $derived(
|
||||
error.includes(';')
|
||||
? error
|
||||
.split(';')
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.length > 0)
|
||||
: [error]
|
||||
);
|
||||
|
||||
// Extract prefix if present (e.g., "Generated routine failed safety validation: ")
|
||||
const hasPrefix = $derived(errors.length === 1 && errors[0].includes(': '));
|
||||
const prefix = $derived(
|
||||
hasPrefix ? errors[0].substring(0, errors[0].indexOf(': ') + 1) : ''
|
||||
);
|
||||
const cleanedErrors = $derived(
|
||||
hasPrefix && prefix ? [errors[0].substring(prefix.length)] : errors
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="editorial-alert editorial-alert--error">
|
||||
<div class="flex items-start gap-2">
|
||||
<XCircle class="size-5 shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
{#if prefix}
|
||||
<p class="font-medium mb-2">{prefix.replace(':', '')}</p>
|
||||
{/if}
|
||||
{#if cleanedErrors.length === 1}
|
||||
<p>{cleanedErrors[0]}</p>
|
||||
{:else}
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
{#each cleanedErrors as err}
|
||||
<li>{err}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
40
frontend/src/lib/components/ValidationWarningsAlert.svelte
Normal file
40
frontend/src/lib/components/ValidationWarningsAlert.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { AlertTriangle } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
let { warnings }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
const shouldCollapse = $derived(warnings.length > 3);
|
||||
const displayedWarnings = $derived(
|
||||
expanded || !shouldCollapse ? warnings : warnings.slice(0, 3)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if warnings && warnings.length > 0}
|
||||
<div class="editorial-alert editorial-alert--warning">
|
||||
<div class="flex items-start gap-2">
|
||||
<AlertTriangle class="size-5 shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<p class="font-medium mb-1">Validation Warnings</p>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm">
|
||||
{#each displayedWarnings as warning}
|
||||
<li>{warning}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if shouldCollapse}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
class="text-sm underline mt-2 hover:no-underline"
|
||||
>
|
||||
{expanded ? 'Show less' : `Show ${warnings.length - 3} more`}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -1,17 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { ProductSuggestion } from '$lib/types';
|
||||
import type { ProductSuggestion, ResponseMetadata } from '$lib/types';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Sparkles, ArrowLeft } from 'lucide-svelte';
|
||||
import ValidationWarningsAlert from '$lib/components/ValidationWarningsAlert.svelte';
|
||||
import StructuredErrorDisplay from '$lib/components/StructuredErrorDisplay.svelte';
|
||||
import AutoFixBadge from '$lib/components/AutoFixBadge.svelte';
|
||||
import ReasoningChainViewer from '$lib/components/ReasoningChainViewer.svelte';
|
||||
import MetadataDebugPanel from '$lib/components/MetadataDebugPanel.svelte';
|
||||
|
||||
let suggestions = $state<ProductSuggestion[] | null>(null);
|
||||
let reasoning = $state('');
|
||||
let loading = $state(false);
|
||||
let errorMsg = $state<string | null>(null);
|
||||
// Phase 3: Observability state
|
||||
let validationWarnings = $state<string[] | undefined>(undefined);
|
||||
let autoFixes = $state<string[] | undefined>(undefined);
|
||||
let metadata = $state<ResponseMetadata | undefined>(undefined);
|
||||
|
||||
function enhanceForm() {
|
||||
loading = true;
|
||||
|
|
@ -21,6 +30,10 @@
|
|||
if (result.type === 'success' && result.data?.suggestions) {
|
||||
suggestions = result.data.suggestions as ProductSuggestion[];
|
||||
reasoning = result.data.reasoning as string;
|
||||
// Phase 3: Extract observability data
|
||||
validationWarnings = result.data.validation_warnings as string[] | undefined;
|
||||
autoFixes = result.data.auto_fixes_applied as string[] | undefined;
|
||||
metadata = result.data.metadata as ResponseMetadata | undefined;
|
||||
errorMsg = null;
|
||||
} else if (result.type === 'failure') {
|
||||
errorMsg = (result.data?.error as string) ?? m["suggest_errorDefault"]();
|
||||
|
|
@ -41,7 +54,7 @@
|
|||
</section>
|
||||
|
||||
{#if errorMsg}
|
||||
<div class="editorial-alert editorial-alert--error">{errorMsg}</div>
|
||||
<StructuredErrorDisplay error={errorMsg} />
|
||||
{/if}
|
||||
|
||||
<Card class="reveal-2">
|
||||
|
|
@ -72,6 +85,22 @@
|
|||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Phase 3: Observability components -->
|
||||
<div class="space-y-4 reveal-3">
|
||||
{#if autoFixes}
|
||||
<AutoFixBadge autoFixes={autoFixes} />
|
||||
{/if}
|
||||
{#if validationWarnings}
|
||||
<ValidationWarningsAlert warnings={validationWarnings} />
|
||||
{/if}
|
||||
{#if metadata?.reasoning_chain}
|
||||
<ReasoningChainViewer reasoningChain={metadata.reasoning_chain} />
|
||||
{/if}
|
||||
{#if metadata}
|
||||
<MetadataDebugPanel {metadata} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 reveal-3">
|
||||
<h3 class="text-lg font-semibold">{m["products_suggestResults"]()}</h3>
|
||||
{#each suggestions as s (s.product_type)}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
import SimpleSelect from '$lib/components/forms/SimpleSelect.svelte';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/components/ui/tabs';
|
||||
import { ChevronUp, ChevronDown, ArrowLeft, Sparkles } from 'lucide-svelte';
|
||||
import ValidationWarningsAlert from '$lib/components/ValidationWarningsAlert.svelte';
|
||||
import StructuredErrorDisplay from '$lib/components/StructuredErrorDisplay.svelte';
|
||||
import AutoFixBadge from '$lib/components/AutoFixBadge.svelte';
|
||||
import ReasoningChainViewer from '$lib/components/ReasoningChainViewer.svelte';
|
||||
import MetadataDebugPanel from '$lib/components/MetadataDebugPanel.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
|
|
@ -121,7 +126,7 @@
|
|||
</section>
|
||||
|
||||
{#if errorMsg}
|
||||
<div class="editorial-alert editorial-alert--error">{errorMsg}</div>
|
||||
<StructuredErrorDisplay error={errorMsg} />
|
||||
{/if}
|
||||
|
||||
<Tabs value="single" class="reveal-2 editorial-tabs">
|
||||
|
|
@ -215,6 +220,20 @@
|
|||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Phase 3: Observability components -->
|
||||
{#if suggestion.auto_fixes_applied}
|
||||
<AutoFixBadge autoFixes={suggestion.auto_fixes_applied} />
|
||||
{/if}
|
||||
{#if suggestion.validation_warnings}
|
||||
<ValidationWarningsAlert warnings={suggestion.validation_warnings} />
|
||||
{/if}
|
||||
{#if suggestion.metadata?.reasoning_chain}
|
||||
<ReasoningChainViewer reasoningChain={suggestion.metadata.reasoning_chain} />
|
||||
{/if}
|
||||
{#if suggestion.metadata}
|
||||
<MetadataDebugPanel metadata={suggestion.metadata} />
|
||||
{/if}
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="space-y-2">
|
||||
{#each suggestion.steps as step, i (i)}
|
||||
|
|
@ -323,6 +342,20 @@
|
|||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Phase 3: Observability components -->
|
||||
{#if batch.auto_fixes_applied}
|
||||
<AutoFixBadge autoFixes={batch.auto_fixes_applied} />
|
||||
{/if}
|
||||
{#if batch.validation_warnings}
|
||||
<ValidationWarningsAlert warnings={batch.validation_warnings} />
|
||||
{/if}
|
||||
{#if batch.metadata?.reasoning_chain}
|
||||
<ReasoningChainViewer reasoningChain={batch.metadata.reasoning_chain} />
|
||||
{/if}
|
||||
{#if batch.metadata}
|
||||
<MetadataDebugPanel metadata={batch.metadata} />
|
||||
{/if}
|
||||
|
||||
<!-- Day cards -->
|
||||
<div class="space-y-3">
|
||||
{#each batch.days as day (day.date)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue