Skip to content

Commit b0a9a9f

Browse files
committed
feat: Improvements for data handling & UI
1 parent f6cede3 commit b0a9a9f

File tree

8 files changed

+322
-181
lines changed

8 files changed

+322
-181
lines changed

tools/server/webui/package-lock.json

Lines changed: 4 additions & 4 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@chromatic-com/storybook": "^4.1.2",
2727
"@eslint/compat": "^1.2.5",
2828
"@eslint/js": "^9.18.0",
29-
"@internationalized/date": "^3.8.2",
29+
"@internationalized/date": "^3.10.1",
3030
"@lucide/svelte": "^0.515.0",
3131
"@playwright/test": "^1.49.1",
3232
"@storybook/addon-a11y": "^10.0.7",

tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte

Lines changed: 78 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<script lang="ts">
2-
import { X, ArrowUp, Paperclip } from '@lucide/svelte';
2+
import { X, ArrowUp, Paperclip, AlertTriangle } from '@lucide/svelte';
33
import { Button } from '$lib/components/ui/button';
4-
import { Checkbox } from '$lib/components/ui/checkbox';
5-
import { Label } from '$lib/components/ui/label';
6-
import { ChatAttachmentsList, ModelsSelector } from '$lib/components/app';
4+
import { Switch } from '$lib/components/ui/switch';
5+
import { ChatAttachmentsList, DialogConfirmation, ModelsSelector } from '$lib/components/app';
76
import { INPUT_CLASSES } from '$lib/constants/input-classes';
87
import { AttachmentType, FileTypeCategory } from '$lib/enums';
98
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
9+
import { setEditModeActive, clearEditMode } from '$lib/stores/chat.svelte';
1010
import { conversationsStore } from '$lib/stores/conversations.svelte';
1111
import { modelsStore } from '$lib/stores/models.svelte';
1212
import { isRouterMode } from '$lib/stores/server.svelte';
@@ -21,6 +21,8 @@
2121
editedContent: string;
2222
editedExtras?: DatabaseMessageExtra[];
2323
editedUploadedFiles?: ChatUploadedFile[];
24+
originalContent: string;
25+
originalExtras?: DatabaseMessageExtra[];
2426
showSaveOnlyOption?: boolean;
2527
onCancelEdit: () => void;
2628
onSaveEdit: () => void;
@@ -37,6 +39,8 @@
3739
editedContent,
3840
editedExtras = [],
3941
editedUploadedFiles = [],
42+
originalContent,
43+
originalExtras = [],
4044
showSaveOnlyOption = false,
4145
onCancelEdit,
4246
onSaveEdit,
@@ -50,8 +54,22 @@
5054
5155
let fileInputElement: HTMLInputElement | undefined = $state();
5256
let saveWithoutRegenerate = $state(false);
57+
let showDiscardDialog = $state(false);
5358
let isRouter = $derived(isRouterMode());
5459
60+
let hasUnsavedChanges = $derived.by(() => {
61+
if (editedContent !== originalContent) return true;
62+
if (editedUploadedFiles.length > 0) return true;
63+
64+
const extrasChanged =
65+
editedExtras.length !== originalExtras.length ||
66+
editedExtras.some((extra, i) => extra !== originalExtras[i]);
67+
68+
if (extrasChanged) return true;
69+
70+
return false;
71+
});
72+
5573
let hasAttachments = $derived(
5674
(editedExtras && editedExtras.length > 0) ||
5775
(editedUploadedFiles && editedUploadedFiles.length > 0)
@@ -110,15 +128,29 @@
110128
}
111129
});
112130
113-
function handleSubmit() {
114-
if (!canSubmit) return;
131+
function handleFileInputChange(event: Event) {
132+
const input = event.target as HTMLInputElement;
133+
if (!input.files || input.files.length === 0) return;
115134
116-
if (saveWithoutRegenerate && onSaveEditOnly) {
117-
onSaveEditOnly();
135+
const files = Array.from(input.files);
136+
137+
processNewFiles(files);
138+
input.value = '';
139+
}
140+
141+
function handleGlobalKeydown(event: KeyboardEvent) {
142+
if (event.key === 'Escape') {
143+
event.preventDefault();
144+
attemptCancel();
145+
}
146+
}
147+
148+
function attemptCancel() {
149+
if (hasUnsavedChanges) {
150+
showDiscardDialog = true;
118151
} else {
119-
onSaveEdit();
152+
onCancelEdit();
120153
}
121-
saveWithoutRegenerate = false;
122154
}
123155
124156
function handleRemoveExistingAttachment(index: number) {
@@ -134,13 +166,15 @@
134166
onEditedUploadedFilesChange(newFiles);
135167
}
136168
137-
function handleFileInputChange(event: Event) {
138-
const input = event.target as HTMLInputElement;
139-
if (!input.files || input.files.length === 0) return;
169+
function handleSubmit() {
170+
if (!canSubmit) return;
140171
141-
const files = Array.from(input.files);
142-
processNewFiles(files);
143-
input.value = '';
172+
if (saveWithoutRegenerate && onSaveEditOnly) {
173+
onSaveEditOnly();
174+
} else {
175+
onSaveEdit();
176+
}
177+
saveWithoutRegenerate = false;
144178
}
145179
146180
async function processNewFiles(files: File[]) {
@@ -156,8 +190,18 @@
156190
autoResizeTextarea(textareaElement);
157191
}
158192
});
193+
194+
$effect(() => {
195+
setEditModeActive(processNewFiles);
196+
197+
return () => {
198+
clearEditMode();
199+
};
200+
});
159201
</script>
160202

203+
<svelte:window onkeydown={handleGlobalKeydown} />
204+
161205
<input
162206
bind:this={fileInputElement}
163207
type="file"
@@ -242,19 +286,31 @@
242286
<div class="mt-2 flex w-full max-w-[80%] items-center justify-between">
243287
{#if showSaveOnlyOption && onSaveEditOnly}
244288
<div class="flex items-center gap-2">
245-
<Checkbox id="save-without-regenerate" bind:checked={saveWithoutRegenerate} class="h-4 w-4" />
246-
1
247-
<Label for="save-without-regenerate" class="cursor-pointer text-xs text-muted-foreground">
248-
Save only
249-
</Label>
289+
<Switch id="save-only-switch" bind:checked={saveWithoutRegenerate} class="scale-75" />
290+
291+
<label for="save-only-switch" class="cursor-pointer text-xs text-muted-foreground">
292+
Update without re-sending
293+
</label>
250294
</div>
251295
{:else}
252296
<div></div>
253297
{/if}
254298

255-
<Button class="h-7 px-3 text-xs" onclick={onCancelEdit} size="sm" variant="ghost">
299+
<Button class="h-7 px-3 text-xs" onclick={attemptCancel} size="sm" variant="ghost">
256300
<X class="mr-1 h-3 w-3" />
257301

258302
Cancel
259303
</Button>
260304
</div>
305+
306+
<DialogConfirmation
307+
bind:open={showDiscardDialog}
308+
title="Discard changes?"
309+
description="You have unsaved changes. Are you sure you want to discard them?"
310+
confirmText="Discard"
311+
cancelText="Keep editing"
312+
variant="destructive"
313+
icon={AlertTriangle}
314+
onConfirm={onCancelEdit}
315+
onCancel={() => (showDiscardDialog = false)}
316+
/>

tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@
103103
{editedContent}
104104
{editedExtras}
105105
{editedUploadedFiles}
106+
originalContent={message.content}
107+
originalExtras={message.extra}
106108
showSaveOnlyOption={!!onSaveEditOnly}
107109
{onCancelEdit}
108110
{onSaveEdit}

tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
AUTO_SCROLL_INTERVAL,
1818
INITIAL_SCROLL_DELAY
1919
} from '$lib/constants/auto-scroll';
20-
import { chatStore, errorDialog, isLoading } from '$lib/stores/chat.svelte';
20+
import {
21+
chatStore,
22+
errorDialog,
23+
isLoading,
24+
isEditModeActive,
25+
getAddFilesHandler
26+
} from '$lib/stores/chat.svelte';
2127
import {
2228
conversationsStore,
2329
activeMessages,
@@ -181,7 +187,18 @@
181187
dragCounter = 0;
182188
183189
if (event.dataTransfer?.files) {
184-
processFiles(Array.from(event.dataTransfer.files));
190+
const files = Array.from(event.dataTransfer.files);
191+
192+
if (isEditModeActive()) {
193+
const handler = getAddFilesHandler();
194+
195+
if (handler) {
196+
handler(files);
197+
return;
198+
}
199+
}
200+
201+
processFiles(files);
185202
}
186203
}
187204
@@ -410,7 +427,7 @@
410427

411428
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
412429
<ChatForm
413-
disabled={hasPropsError}
430+
disabled={hasPropsError || isEditModeActive()}
414431
isLoading={isCurrentConversationLoading}
415432
onFileRemove={handleFileRemove}
416433
onFileUpload={handleFileUpload}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Root from './switch.svelte';
2+
3+
export {
4+
Root,
5+
//
6+
Root as Switch
7+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import { Switch as SwitchPrimitive } from 'bits-ui';
3+
import { cn, type WithoutChildrenOrChild } from '$lib/components/ui/utils.js';
4+
5+
let {
6+
ref = $bindable(null),
7+
class: className,
8+
checked = $bindable(false),
9+
...restProps
10+
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
11+
</script>
12+
13+
<SwitchPrimitive.Root
14+
bind:ref
15+
bind:checked
16+
data-slot="switch"
17+
class={cn(
18+
'peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80',
19+
className
20+
)}
21+
{...restProps}
22+
>
23+
<SwitchPrimitive.Thumb
24+
data-slot="switch-thumb"
25+
class={cn(
26+
'pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground'
27+
)}
28+
/>
29+
</SwitchPrimitive.Root>

0 commit comments

Comments
 (0)