Skip to content

Commit a555a58

Browse files
committed
feat: UI & logic improvements
1 parent a6d85e6 commit a555a58

21 files changed

+2668
-376
lines changed

tools/server/webui/package-lock.json

Lines changed: 1570 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/server/webui/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "0.0.1",
55
"type": "module",
66
"scripts": {
7-
"dev": "vite dev & storybook dev -p 6006",
7+
"dev": "vite dev & storybook dev -p 6006 --ci",
88
"build": "vite build",
99
"preview": "vite preview",
1010
"prepare": "svelte-kit sync || echo ''",
@@ -66,6 +66,12 @@
6666
"vitest-browser-svelte": "^0.1.0"
6767
},
6868
"dependencies": {
69-
"mode-watcher": "^1.1.0"
69+
"mode-watcher": "^1.1.0",
70+
"remark": "^15.0.1",
71+
"remark-breaks": "^4.0.0",
72+
"remark-gfm": "^4.0.1",
73+
"remark-html": "^16.0.1",
74+
"shiki": "^3.8.1",
75+
"unist-util-visit": "^5.0.0"
7076
}
71-
}
77+
}

tools/server/webui/src/app.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
--accent: oklch(0.97 0 0);
2222
--accent-foreground: oklch(0.205 0 0);
2323
--destructive: oklch(0.577 0.245 27.325);
24-
--border: oklch(0.922 0 0);
25-
--input: oklch(0.922 0 0);
24+
--border: oklch(0.875 0 0);
25+
--input: oklch(0.92 0 0);
2626
--ring: oklch(0.708 0 0);
2727
--chart-1: oklch(0.646 0.222 41.116);
2828
--chart-2: oklch(0.6 0.118 184.704);
@@ -55,8 +55,8 @@
5555
--accent: oklch(0.269 0 0);
5656
--accent-foreground: oklch(0.985 0 0);
5757
--destructive: oklch(0.704 0.191 22.216);
58-
--border: oklch(1 0 0 / 15%);
59-
--input: oklch(1 0 0 / 15%);
58+
--border: oklch(1 0 0 / 30%);
59+
--input: oklch(1 0 0 / 30%);
6060
--ring: oklch(0.556 0 0);
6161
--chart-1: oklch(0.488 0.243 264.376);
6262
--chart-2: oklch(0.696 0.17 162.48);

tools/server/webui/src/lib/components/chat/ChatConversations/ChatConversationsItem.svelte

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@
99
onSelect?: (id: string) => void;
1010
onEdit?: (id: string) => void;
1111
onDelete?: (id: string) => void;
12+
showLastModified?: boolean;
1213
}
1314
14-
let { conversation, isActive = false, onSelect, onEdit, onDelete }: Props = $props();
15+
let {
16+
conversation,
17+
isActive = false,
18+
onSelect,
19+
onEdit,
20+
onDelete,
21+
showLastModified = false
22+
}: Props = $props();
1523
1624
function formatLastModified(timestamp: number) {
1725
const now = Date.now();
@@ -47,19 +55,21 @@
4755
: 'border border-transparent'}"
4856
onclick={handleSelect}
4957
>
50-
<div class="flex min-w-0 flex-1 items-center space-x-3">
58+
<div class="text flex min-w-0 flex-1 items-center space-x-3">
5159
<div class="min-w-0 flex-1">
5260
<p class="truncate text-sm font-medium">{conversation.name}</p>
5361

54-
<div class="mt-2 flex flex-wrap items-center space-x-2 space-y-2">
55-
<span class="text-muted-foreground w-full text-xs">
56-
{formatLastModified(conversation.lastModified)}
57-
</span>
58-
</div>
62+
{#if showLastModified}
63+
<div class="mt-2 flex flex-wrap items-center space-x-2 space-y-2">
64+
<span class="text-muted-foreground w-full text-xs">
65+
{formatLastModified(conversation.lastModified)}
66+
</span>
67+
</div>
68+
{/if}
5969
</div>
6070
</div>
6171

62-
<div class="flex items-center space-x-1 opacity-0 transition-opacity group-hover:opacity-100">
72+
<div class="actions flex items-center space-x-1">
6373
<Button size="sm" variant="ghost" class="h-6 w-6 p-0" onclick={handleEdit}>
6474
<Pencil class="h-3 w-3" />
6575
</Button>
@@ -73,3 +83,17 @@
7383
</Button>
7484
</div>
7585
</button>
86+
87+
<style lang="postcss">
88+
.actions {
89+
button & {
90+
width: 0;
91+
opacity: 0;
92+
}
93+
94+
button:hover & {
95+
width: auto;
96+
opacity: 1;
97+
}
98+
}
99+
</style>

tools/server/webui/src/lib/components/chat/ChatForm.svelte

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Button } from '$lib/components/ui/button';
33
import { Textarea } from '$lib/components/ui/textarea';
44
import autoResizeTextarea from '$lib/utils/autoresize-textarea';
5-
import { Send, Square, Paperclip, Mic } from '@lucide/svelte';
5+
import { Send, Square, Paperclip, Mic, ArrowUp } from '@lucide/svelte';
66
77
interface Props {
88
class?: string;
@@ -84,7 +84,7 @@
8484
<Button
8585
type="button"
8686
variant="ghost"
87-
class="text-muted-foreground hover:text-foreground h-9 w-9 rounded-full p-0"
87+
class="text-muted-foreground hover:text-foreground h-8 w-8 rounded-full p-0"
8888
disabled={disabled || isLoading}
8989
>
9090
<Paperclip class="h-4 w-4" />
@@ -96,25 +96,25 @@
9696
type="button"
9797
variant="ghost"
9898
onclick={handleStop}
99-
class="text-muted-foreground hover:text-destructive h-9 w-9 rounded-full p-0"
99+
class="text-muted-foreground hover:text-destructive h-8 w-8 rounded-full p-0"
100100
>
101-
<Square class="h-6 w-6" />
101+
<Square class="h-8 w-8" />
102102
</Button>
103103
{:else}
104104
<Button
105105
type="button"
106106
variant="ghost"
107-
class="text-muted-foreground hover:text-foreground h-9 w-9 rounded-full p-0"
107+
class="text-muted-foreground hover:text-foreground h-8 w-8 rounded-full p-0"
108108
disabled={disabled || isLoading}
109109
>
110-
<Mic class="h-6 w-6" />
110+
<Mic class="h-8 w-8" />
111111
</Button>
112112
<Button
113113
type="submit"
114114
disabled={!message.trim() || disabled || isLoading}
115-
class="h-9 w-9 rounded-full p-0"
115+
class="h-8 w-8 rounded-full p-0"
116116
>
117-
<Send class="h-6 w-6" />
117+
<ArrowUp class="h-12 w-12" />
118118
</Button>
119119
{/if}
120120
</div>
Lines changed: 162 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,173 @@
11
<script lang="ts">
22
import { Card } from '$lib/components/ui/card';
3-
import { User, Bot } from '@lucide/svelte';
3+
import { Button } from '$lib/components/ui/button';
4+
import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/components/ui/tooltip';
5+
import { User, Bot, Edit, Copy, Trash2, RefreshCw } from '@lucide/svelte';
6+
import type { ChatRole } from '$lib/types/chat';
7+
import type { ChatMessageData } from '$lib/types/chat';
8+
import ThinkingSection from './ThinkingSection.svelte';
9+
import MarkdownContent from './MarkdownContent.svelte';
10+
import { parseThinkingContent } from '$lib/utils/thinking';
411
5-
let { class: className = '', message }: { class?: string; message: ChatMessageData } = $props();
12+
interface Props {
13+
class?: string;
14+
message: ChatMessageData;
15+
onEdit?: (message: ChatMessageData) => void;
16+
onDelete?: (message: ChatMessageData) => void;
17+
onCopy?: (message: ChatMessageData) => void;
18+
onRegenerate?: (message: ChatMessageData) => void;
19+
}
20+
21+
let {
22+
class: className = '',
23+
message,
24+
onEdit,
25+
onDelete,
26+
onCopy,
27+
onRegenerate
28+
}: Props = $props();
29+
30+
// Parse thinking content for assistant messages
31+
const parsedContent = $derived(() => {
32+
if (message.role === 'assistant') {
33+
const parsed = parseThinkingContent(message.content);
34+
return {
35+
thinking: message.thinking || parsed.thinking,
36+
content: parsed.cleanContent || message.content
37+
};
38+
}
39+
return { thinking: null, content: message.content };
40+
});
41+
42+
// Handle copy to clipboard
43+
function handleCopy() {
44+
navigator.clipboard.writeText(message.content);
45+
onCopy?.(message);
46+
}
47+
48+
// Handle edit action
49+
function handleEdit() {
50+
onEdit?.(message);
51+
}
52+
53+
// Handle delete action
54+
function handleDelete() {
55+
onDelete?.(message);
56+
}
57+
58+
// Handle regenerate action
59+
function handleRegenerate() {
60+
onRegenerate?.(message);
61+
}
662
</script>
763

8-
<div class="flex gap-3 {className} {message.role === 'user' ? 'justify-end' : 'justify-start'}">
9-
<!-- {#if message.role === 'assistant'}
10-
<div
11-
class="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow"
12-
>
13-
<Bot class="h-4 w-4" />
14-
</div>
15-
{/if} -->
64+
{#snippet messageActions(config?: { role: ChatRole })}
65+
<div
66+
class="pointer-events-none inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:pointer-events-auto group-hover:opacity-100"
67+
>
68+
<Tooltip>
69+
<TooltipTrigger>
70+
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" onclick={handleCopy}>
71+
<Copy class="h-3 w-3" />
72+
</Button>
73+
</TooltipTrigger>
74+
<TooltipContent>
75+
<p>Copy</p>
76+
</TooltipContent>
77+
</Tooltip>
78+
{#if config?.role === 'user'}
79+
<Tooltip>
80+
<TooltipTrigger>
81+
<Button variant="ghost" size="sm" class="h-6 w-6 p-0" onclick={handleEdit}>
82+
<Edit class="h-3 w-3" />
83+
</Button>
84+
</TooltipTrigger>
85+
<TooltipContent>
86+
<p>Edit</p>
87+
</TooltipContent>
88+
</Tooltip>
89+
{:else if config?.role === 'assistant'}
90+
<Tooltip>
91+
<TooltipTrigger>
92+
<Button
93+
variant="ghost"
94+
size="sm"
95+
class="h-6 w-6 p-0"
96+
onclick={handleRegenerate}
97+
>
98+
<RefreshCw class="h-3 w-3" />
99+
</Button>
100+
</TooltipTrigger>
101+
<TooltipContent>
102+
<p>Regenerate</p>
103+
</TooltipContent>
104+
</Tooltip>
105+
{/if}
106+
<!-- This will be implemented after fully migrating to SvelteKit -->
16107

17-
<Card
18-
class="max-w-[80%] gap-2 px-4 py-3 {message.role === 'user'
19-
? 'bg-primary text-primary-foreground'
20-
: 'bg-muted'}"
108+
<!-- <Tooltip>
109+
<TooltipTrigger>
110+
<Button
111+
variant="ghost"
112+
size="sm"
113+
class="text-destructive hover:text-destructive h-6 w-6 p-0"
114+
onclick={handleDelete}
115+
>
116+
<Trash2 class="h-3 w-3" />
117+
</Button>
118+
</TooltipTrigger>
119+
<TooltipContent>
120+
<p>Delete</p>
121+
</TooltipContent>
122+
</Tooltip> -->
123+
</div>
124+
125+
<div
126+
class="{config?.role === 'user'
127+
? 'right-0'
128+
: 'left-0'} text-muted-foreground absolute text-xs transition-all duration-150 group-hover:pointer-events-none group-hover:opacity-0"
21129
>
22-
<div class="whitespace-pre-wrap text-sm">
23-
{message.content}
130+
{message.timestamp ? new Date(message.timestamp).toLocaleTimeString() : ''}
131+
</div>
132+
{/snippet}
133+
134+
{#if message.role === 'user'}
135+
<div
136+
class="group flex flex-col items-end gap-2 {className}"
137+
role="group"
138+
aria-label="User message with actions"
139+
>
140+
<Card class="bg-primary text-primary-foreground max-w-[80%] rounded-2xl px-2.5 py-1.5">
141+
<div class="text-md whitespace-pre-wrap">
142+
{message.content}
143+
</div>
144+
</Card>
145+
146+
<div class="relative flex h-6 items-center">
147+
{@render messageActions({ role: 'user' })}
24148
</div>
25-
{#if message.timestamp}
26-
<div class="text-xs opacity-70">
27-
{new Date(message.timestamp).toLocaleTimeString()}
149+
</div>
150+
{:else}
151+
<div
152+
class="text-md leading-7.5 group w-full {className}"
153+
role="group"
154+
aria-label="Assistant message with actions"
155+
>
156+
{#if parsedContent().thinking}
157+
<ThinkingSection thinking={parsedContent().thinking || ''} />
158+
{/if}
159+
{#if message.role === 'assistant'}
160+
<MarkdownContent content={parsedContent().content} />
161+
{:else}
162+
<div class="whitespace-pre-wrap text-sm">
163+
{parsedContent().content}
28164
</div>
29165
{/if}
30-
</Card>
31166

32-
<!-- {#if message.role === 'user'}
33-
<div
34-
class="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow"
35-
>
36-
<User class="h-4 w-4" />
37-
</div>
38-
{/if} -->
39-
</div>
167+
{#if message.timestamp}
168+
<div class="relative mt-2 flex h-6 items-center">
169+
{@render messageActions({ role: 'assistant' })}
170+
</div>
171+
{/if}
172+
</div>
173+
{/if}

tools/server/webui/src/lib/components/chat/ChatMessageLoading.svelte

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)