ui: migrate chat playground and stats to shadcn

- ChatInterface controls, settings, input use Button/Input/Textarea/Label
- ExpandableTextarea and ModelSelector restyled on shadcn tokens
- ActivityStats wrapped in Card; Tooltip uses shadcn tooltip

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmuGqwNBJNEAMaWsdCDqUC
This commit is contained in:
Claude
2026-06-27 11:56:31 +00:00
parent 8dd91e99e8
commit 746c083a87
5 changed files with 101 additions and 131 deletions
@@ -7,6 +7,11 @@
import ChatMessageComponent from "./ChatMessage.svelte";
import ModelSelector from "./ModelSelector.svelte";
import ExpandableTextarea from "./ExpandableTextarea.svelte";
import { Settings, Paperclip } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { Label } from "$lib/components/ui/label/index.js";
const selectedModelStore = persistentStore<string>("playground-selected-model", "");
const systemPromptStore = persistentStore<string>("playground-system-prompt", "");
@@ -303,29 +308,23 @@
<div class="shrink-0 flex flex-wrap gap-2 mb-4">
<ModelSelector bind:value={$selectedModelStore} placeholder="Select a model..." disabled={isStreaming} />
<div class="flex gap-2">
<button
class="btn"
onclick={() => (showSettings = !showSettings)}
title="Settings"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M8.34 1.804A1 1 0 0 1 9.32 1h1.36a1 1 0 0 1 .98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 0 1 1.262.125l.962.962a1 1 0 0 1 .125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.295a1 1 0 0 1 .804.98v1.36a1 1 0 0 1-.804.98l-1.473.295a6.95 6.95 0 0 1-.587 1.416l.834 1.25a1 1 0 0 1-.125 1.262l-.962.962a1 1 0 0 1-1.262.125l-1.25-.834a6.953 6.953 0 0 1-1.416.587l-.295 1.473a1 1 0 0 1-.98.804H9.32a1 1 0 0 1-.98-.804l-.295-1.473a6.957 6.957 0 0 1-1.416-.587l-1.25.834a1 1 0 0 1-1.262-.125l-.962-.962a1 1 0 0 1-.125-1.262l.834-1.25a6.957 6.957 0 0 1-.587-1.416l-1.473-.295A1 1 0 0 1 1 10.68V9.32a1 1 0 0 1 .804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 0 1 .125-1.262l.962-.962A1 1 0 0 1 5.38 3.03l1.25.834a6.957 6.957 0 0 1 1.416-.587l.294-1.473ZM13 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" clip-rule="evenodd" />
</svg>
</button>
<button class="btn" onclick={newChat} disabled={messages.length === 0 && !isStreaming}>
<Button variant="outline" size="icon" onclick={() => (showSettings = !showSettings)} title="Settings">
<Settings />
</Button>
<Button variant="outline" onclick={newChat} disabled={messages.length === 0 && !isStreaming}>
New Chat
</button>
</Button>
</div>
</div>
<!-- Settings panel -->
{#if showSettings}
<div class="shrink-0 mb-4 p-4 bg-surface border border-gray-200 dark:border-white/10 rounded">
<div class="bg-muted/40 mb-4 shrink-0 rounded-lg border p-4">
<div class="mb-4">
<label class="block text-sm font-medium mb-1" for="endpoint">Endpoint</label>
<Label class="mb-1" for="endpoint">Endpoint</Label>
<select
id="endpoint"
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary"
class="border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 w-full rounded-md border px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-[3px] disabled:opacity-50"
bind:value={$endpointStore}
disabled={isStreaming}
>
@@ -335,64 +334,57 @@
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1" for="system-prompt">System Prompt</label>
<textarea
<Label class="mb-1" for="system-prompt">System Prompt</Label>
<Textarea
id="system-prompt"
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary resize-none"
class="resize-none"
placeholder="You are a helpful assistant..."
rows="3"
rows={3}
bind:value={$systemPromptStore}
disabled={isStreaming}
></textarea>
/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1" for="temperature">
<Label class="mb-1" for="temperature">
Temperature: {$temperatureStore.toFixed(2)}
</label>
</Label>
<input
id="temperature"
type="range"
min="0"
max="2"
step="0.05"
class="w-full"
class="accent-primary w-full"
bind:value={$temperatureStore}
disabled={isStreaming}
/>
<div class="flex justify-between text-xs text-txtsecondary mt-1">
<div class="text-muted-foreground mt-1 flex justify-between text-xs">
<span>Precise (0)</span>
<span>Creative (2)</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1" for="max-tokens">Max Tokens</label>
<input
id="max-tokens"
type="number"
min="1"
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary"
bind:value={$maxTokensStore}
disabled={isStreaming}
/>
<p class="text-xs text-txtsecondary mt-1">Required for /v1/messages.</p>
<Label class="mb-1" for="max-tokens">Max Tokens</Label>
<Input id="max-tokens" type="number" min="1" bind:value={$maxTokensStore} disabled={isStreaming} />
<p class="text-muted-foreground mt-1 text-xs">Required for /v1/messages.</p>
</div>
</div>
{/if}
<!-- Empty state for no models configured -->
{#if !hasModels}
<div class="flex-1 flex items-center justify-center text-txtsecondary">
<div class="text-muted-foreground flex flex-1 items-center justify-center">
<p>No models configured. Add models to your configuration to start chatting.</p>
</div>
{:else}
<!-- Messages area -->
<div
class="flex-1 overflow-y-auto mb-4 px-2"
class="mb-4 flex-1 overflow-y-auto px-2"
bind:this={messagesContainer}
onscroll={handleMessagesScroll}
>
{#if messages.length === 0}
<div class="h-full flex items-center justify-center text-txtsecondary">
<div class="text-muted-foreground flex h-full items-center justify-center">
<p>Start a conversation by typing a message below.</p>
</div>
{:else}
@@ -419,14 +411,14 @@
{#if attachedImages.length > 0}
<div class="mb-2 flex flex-wrap gap-2">
{#each attachedImages as imageUrl, idx (idx)}
<div class="relative group">
<div class="group relative">
<img
src={imageUrl}
alt="Attached image {idx + 1}"
class="w-20 h-20 object-cover rounded border border-gray-200 dark:border-white/10"
class="h-20 w-20 rounded border object-cover"
/>
<button
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
class="bg-destructive text-destructive-foreground absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full opacity-0 transition-opacity group-hover:opacity-100"
onclick={() => removeImage(idx)}
title="Remove image"
>
@@ -439,7 +431,7 @@
<!-- Error message -->
{#if imageError}
<div class="mb-2 p-2 bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-400 rounded text-sm">
<div class="bg-destructive/10 text-destructive mb-2 rounded p-2 text-sm">
{imageError}
</div>
{/if}
@@ -464,27 +456,23 @@
/>
<div class="flex flex-col gap-2">
{#if isStreaming}
<button class="btn bg-red-500 hover:bg-red-600 text-white" onclick={cancelStreaming}>
Cancel
</button>
<Button variant="destructive" onclick={cancelStreaming}>Cancel</Button>
{:else}
<button
class="btn"
<Button
variant="outline"
size="icon"
onclick={() => fileInput?.click()}
disabled={isStreaming || !$selectedModelStore}
title="Attach image"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.219a.75.75 0 0 0-1.06 0l-1.91 1.909.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.091a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
</svg>
</button>
<button
class="btn bg-primary text-btn-primary-text hover:opacity-90"
<Paperclip />
</Button>
<Button
onclick={sendMessage}
disabled={(!userInput.trim() && attachedImages.length === 0) || !$selectedModelStore}
>
Send
</button>
</Button>
{/if}
</div>
</div>
@@ -1,6 +1,8 @@
<script lang="ts">
import { untrack } from "svelte";
import { Maximize2, X } from "lucide-svelte";
import { Maximize2, X } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
interface Props {
value: string;
@@ -52,69 +54,54 @@
});
</script>
<div class="flex-1 relative group flex items-stretch min-h-0">
<textarea
class="w-full px-3 py-2 pr-10 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary resize-none"
<div class="group relative flex min-h-0 flex-1 items-stretch">
<Textarea
class="resize-none pr-10"
{placeholder}
{rows}
bind:value
{onkeydown}
{disabled}
></textarea>
<button
class="absolute top-2 right-2 p-1.5 rounded-lg opacity-60 md:opacity-0 group-hover:opacity-100 transition-opacity bg-surface/90 hover:bg-surface border border-gray-200 dark:border-white/10 shadow-sm"
/>
<Button
variant="outline"
size="icon-sm"
class="absolute right-2 top-2 opacity-60 transition-opacity group-hover:opacity-100 md:opacity-0"
onclick={openExpanded}
title="Expand to edit"
type="button"
{disabled}
>
<Maximize2 class="w-4 h-4" />
</button>
<Maximize2 />
</Button>
</div>
{#if isExpanded}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div class="w-full max-w-4xl h-[80vh] flex flex-col bg-surface rounded-lg shadow-xl border border-gray-200 dark:border-white/10">
<div class="bg-card flex h-[80vh] w-full max-w-4xl flex-col rounded-lg border shadow-xl">
<!-- Header -->
<div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-white/10">
<h3 class="font-medium">Edit Text</h3>
<button
class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-white/10"
onclick={closeExpanded}
title="Close"
type="button"
>
<X class="w-5 h-5" />
</button>
<div class="flex items-center justify-between border-b p-4">
<h3 class="pb-0 font-medium">Edit Text</h3>
<Button variant="ghost" size="icon-sm" onclick={closeExpanded} title="Close" type="button">
<X />
</Button>
</div>
<!-- Textarea -->
<div class="flex-1 p-4">
<textarea
bind:this={expandedTextarea}
class="w-full h-full px-4 py-3 rounded border border-gray-200 dark:border-white/10 bg-card focus:outline-none focus:ring-2 focus:ring-primary resize-none"
placeholder={placeholder}
<Textarea
bind:ref={expandedTextarea}
class="h-full resize-none"
{placeholder}
bind:value={expandedValue}
onkeydown={handleKeyDown}
></textarea>
/>
</div>
<!-- Footer -->
<div class="flex justify-end gap-2 p-4 border-t border-gray-200 dark:border-white/10">
<button
class="btn"
onclick={closeExpanded}
type="button"
>
Cancel
</button>
<button
class="btn bg-primary text-btn-primary-text hover:opacity-90"
onclick={saveExpanded}
type="button"
>
Done
</button>
<div class="flex justify-end gap-2 border-t p-4">
<Button variant="outline" onclick={closeExpanded} type="button">Cancel</Button>
<Button onclick={saveExpanded} type="button">Done</Button>
</div>
</div>
</div>
@@ -19,7 +19,7 @@
{#if hasModels}
<select
class="min-w-0 flex-1 basis-48 px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface focus:outline-none focus:ring-2 focus:ring-primary"
class="border-input bg-background focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 min-w-0 flex-1 basis-48 rounded-md border px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50"
bind:value
{disabled}
>