parakon-runtime / tools /ui /src /lib /components /app /chat /ChatMessages /ChatMessageEditForm.svelte
kat0012's picture
Parakon runtime: llama.cpp fork with fused 1-bit CUDA/Metal/CPU kernels (part 7)
d2816aa verified
Raw History Blame Contribute Delete
4.57 kB
<script lang="ts">
import { AlertTriangle, X } from '@lucide/svelte';
import { ChatForm, DialogConfirmation } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import { Switch } from '$lib/components/ui/switch';
import { getChatMessageEditContext } from '$lib/contexts';
import { KeyboardKey, MessageRole } from '$lib/enums';
import { chatStore } from '$lib/stores';
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
const editCtx = getChatMessageEditContext();
let saveWithoutRegenerate = $state(false);
let showDiscardDialog = $state(false);
let branchAfterEdit = $state(false);
let isUserMessage = $derived(editCtx.messageRole === MessageRole.USER);
let isAssistantMessage = $derived(editCtx.messageRole === MessageRole.ASSISTANT);
let hasUnsavedChanges = $derived.by(() => {
if (editCtx.editedContent !== editCtx.originalContent) return true;
if (editCtx.editedUploadedFiles.length > 0) return true;
const extrasChanged =
editCtx.editedExtras.length !== editCtx.originalExtras.length ||
editCtx.editedExtras.some((extra, i) => extra !== editCtx.originalExtras[i]);
if (extrasChanged) return true;
return false;
});
let hasAttachments = $derived(
(editCtx.editedExtras && editCtx.editedExtras.length > 0) ||
(editCtx.editedUploadedFiles && editCtx.editedUploadedFiles.length > 0)
);
let canSubmit = $derived(editCtx.editedContent.trim().length > 0 || hasAttachments);
function handleGlobalKeydown(event: KeyboardEvent) {
if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
attemptCancel();
}
}
function attemptCancel() {
if (hasUnsavedChanges) {
showDiscardDialog = true;
} else {
editCtx.cancel();
}
}
function handleSubmit() {
if (!canSubmit) return;
if (isUserMessage && saveWithoutRegenerate && editCtx.showSaveOnlyOption) {
editCtx.saveOnly();
} else {
if (isAssistantMessage && editCtx.setShouldBranchAfterEdit) {
editCtx.setShouldBranchAfterEdit(branchAfterEdit);
}
editCtx.save();
}
saveWithoutRegenerate = false;
branchAfterEdit = false;
}
function handleAttachmentRemove(index: number) {
const newExtras = [...editCtx.editedExtras];
newExtras.splice(index, 1);
editCtx.setExtras(newExtras);
}
function handleUploadedFileRemove(fileId: string) {
const newFiles = editCtx.editedUploadedFiles.filter((f) => f.id !== fileId);
editCtx.setUploadedFiles(newFiles);
}
async function handleFilesAdd(files: File[]) {
const processed = await processFilesToChatUploaded(files);
editCtx.setUploadedFiles([...editCtx.editedUploadedFiles, ...processed]);
}
$effect(() => {
chatStore.setEditModeActive(handleFilesAdd);
return () => {
chatStore.clearEditMode();
};
});
</script>
<svelte:window onkeydown={handleGlobalKeydown} />
<div class="relative w-full max-w-[80%]">
<ChatForm
bind:uploadedFiles={editCtx.editedUploadedFiles}
attachments={editCtx.editedExtras}
onAttachmentRemove={handleAttachmentRemove}
onFilesAdd={handleFilesAdd}
onSubmit={handleSubmit}
onUploadedFileRemove={handleUploadedFileRemove}
onValueChange={editCtx.setContent}
placeholder="Edit your message..."
showAddButton={editCtx.messageRole === MessageRole.USER}
showModelSelector={editCtx.messageRole === MessageRole.USER}
value={editCtx.editedContent}
/>
</div>
<div class="mt-2 flex w-full max-w-[80%] items-center justify-between">
{#if isUserMessage && editCtx.showSaveOnlyOption}
<div class="flex items-center gap-2">
<Switch bind:checked={saveWithoutRegenerate} class="scale-75" id="save-only-switch" />
<label class="cursor-pointer text-xs text-muted-foreground" for="save-only-switch">
Update without re-sending
</label>
</div>
{:else if isAssistantMessage}
<div class="flex items-center gap-2">
<Switch bind:checked={branchAfterEdit} class="scale-75" id="branch-after-edit" />
<label class="cursor-pointer text-xs text-muted-foreground" for="branch-after-edit">
Branch conversation after edit
</label>
</div>
{:else}
<div></div>
{/if}
<Button class="h-7 px-3 text-xs" onclick={attemptCancel} size="sm" variant="ghost">
<X class="mr-1 h-3 w-3" />
Cancel
</Button>
</div>
<DialogConfirmation
bind:open={showDiscardDialog}
cancelText="Keep editing"
confirmText="Discard"
description="You have unsaved changes. Are you sure you want to discard them?"
icon={AlertTriangle}
onCancel={() => (showDiscardDialog = false)}
onConfirm={editCtx.cancel}
title="Discard changes?"
variant="destructive"
/>