|
1 | 1 | <script lang="ts"> |
2 | | - import { X, ArrowUp, Paperclip } from '@lucide/svelte'; |
| 2 | + import { X, ArrowUp, Paperclip, AlertTriangle } from '@lucide/svelte'; |
3 | 3 | 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'; |
7 | 6 | import { INPUT_CLASSES } from '$lib/constants/input-classes'; |
8 | 7 | import { AttachmentType, FileTypeCategory } from '$lib/enums'; |
9 | 8 | import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte'; |
| 9 | + import { setEditModeActive, clearEditMode } from '$lib/stores/chat.svelte'; |
10 | 10 | import { conversationsStore } from '$lib/stores/conversations.svelte'; |
11 | 11 | import { modelsStore } from '$lib/stores/models.svelte'; |
12 | 12 | import { isRouterMode } from '$lib/stores/server.svelte'; |
|
21 | 21 | editedContent: string; |
22 | 22 | editedExtras?: DatabaseMessageExtra[]; |
23 | 23 | editedUploadedFiles?: ChatUploadedFile[]; |
| 24 | + originalContent: string; |
| 25 | + originalExtras?: DatabaseMessageExtra[]; |
24 | 26 | showSaveOnlyOption?: boolean; |
25 | 27 | onCancelEdit: () => void; |
26 | 28 | onSaveEdit: () => void; |
|
37 | 39 | editedContent, |
38 | 40 | editedExtras = [], |
39 | 41 | editedUploadedFiles = [], |
| 42 | + originalContent, |
| 43 | + originalExtras = [], |
40 | 44 | showSaveOnlyOption = false, |
41 | 45 | onCancelEdit, |
42 | 46 | onSaveEdit, |
|
50 | 54 |
|
51 | 55 | let fileInputElement: HTMLInputElement | undefined = $state(); |
52 | 56 | let saveWithoutRegenerate = $state(false); |
| 57 | + let showDiscardDialog = $state(false); |
53 | 58 | let isRouter = $derived(isRouterMode()); |
54 | 59 |
|
| 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 | +
|
55 | 73 | let hasAttachments = $derived( |
56 | 74 | (editedExtras && editedExtras.length > 0) || |
57 | 75 | (editedUploadedFiles && editedUploadedFiles.length > 0) |
|
110 | 128 | } |
111 | 129 | }); |
112 | 130 |
|
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; |
115 | 134 |
|
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; |
118 | 151 | } else { |
119 | | - onSaveEdit(); |
| 152 | + onCancelEdit(); |
120 | 153 | } |
121 | | - saveWithoutRegenerate = false; |
122 | 154 | } |
123 | 155 |
|
124 | 156 | function handleRemoveExistingAttachment(index: number) { |
|
134 | 166 | onEditedUploadedFilesChange(newFiles); |
135 | 167 | } |
136 | 168 |
|
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; |
140 | 171 |
|
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; |
144 | 178 | } |
145 | 179 |
|
146 | 180 | async function processNewFiles(files: File[]) { |
|
156 | 190 | autoResizeTextarea(textareaElement); |
157 | 191 | } |
158 | 192 | }); |
| 193 | +
|
| 194 | + $effect(() => { |
| 195 | + setEditModeActive(processNewFiles); |
| 196 | +
|
| 197 | + return () => { |
| 198 | + clearEditMode(); |
| 199 | + }; |
| 200 | + }); |
159 | 201 | </script> |
160 | 202 |
|
| 203 | +<svelte:window onkeydown={handleGlobalKeydown} /> |
| 204 | + |
161 | 205 | <input |
162 | 206 | bind:this={fileInputElement} |
163 | 207 | type="file" |
|
242 | 286 | <div class="mt-2 flex w-full max-w-[80%] items-center justify-between"> |
243 | 287 | {#if showSaveOnlyOption && onSaveEditOnly} |
244 | 288 | <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> |
250 | 294 | </div> |
251 | 295 | {:else} |
252 | 296 | <div></div> |
253 | 297 | {/if} |
254 | 298 |
|
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"> |
256 | 300 | <X class="mr-1 h-3 w-3" /> |
257 | 301 |
|
258 | 302 | Cancel |
259 | 303 | </Button> |
260 | 304 | </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 | +/> |
0 commit comments