Spaces:
Running
Running
can update previous message
Browse files- src/lib/components/chat/Message.svelte +67 -5
- src/lib/components/chat/User.svelte +38 -2
- src/lib/components/chat/markdown/Code.svelte +2 -2
- src/lib/components/chat/markdown/Codespan.svelte +1 -1
- src/lib/components/chat/markdown/Heading.svelte +4 -4
- src/lib/components/chat/markdown/List.svelte +2 -2
- src/lib/components/chat/markdown/Paragraph.svelte +1 -1
- src/lib/components/chat/markdown/think/Heading.svelte +4 -4
- src/lib/components/ui/button/button.svelte +3 -1
src/lib/components/chat/Message.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
<script lang="ts">
|
|
|
|
| 2 |
import SvelteMarkdown from 'svelte-markdown';
|
| 3 |
|
| 4 |
import type { ChatMessage } from '$lib/helpers/types';
|
|
@@ -12,8 +13,13 @@
|
|
| 12 |
import Link from './markdown/Link.svelte';
|
| 13 |
import Hr from './markdown/Hr.svelte';
|
| 14 |
import Think from './markdown/Think.svelte';
|
|
|
|
| 15 |
|
| 16 |
-
let {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
const renderers = {
|
| 19 |
paragraph: Paragraph,
|
|
@@ -26,15 +32,71 @@
|
|
| 26 |
link: Link,
|
| 27 |
hr: Hr
|
| 28 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</script>
|
| 30 |
|
| 31 |
<main
|
| 32 |
-
class="pointer-events-auto cursor-auto p-1 text-lg leading-relaxed text-accent-foreground select-text"
|
| 33 |
>
|
| 34 |
{#if message?.role === 'user'}
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
{:else}
|
| 39 |
{#if message.reasoning}
|
| 40 |
<Think
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { Check, PenLine, X } from '@lucide/svelte';
|
| 3 |
import SvelteMarkdown from 'svelte-markdown';
|
| 4 |
|
| 5 |
import type { ChatMessage } from '$lib/helpers/types';
|
|
|
|
| 13 |
import Link from './markdown/Link.svelte';
|
| 14 |
import Hr from './markdown/Hr.svelte';
|
| 15 |
import Think from './markdown/Think.svelte';
|
| 16 |
+
import Button from '../ui/button/button.svelte';
|
| 17 |
|
| 18 |
+
let {
|
| 19 |
+
message,
|
| 20 |
+
nodeId,
|
| 21 |
+
onEdit
|
| 22 |
+
}: { message: ChatMessage; nodeId: string; onEdit?: (newContent: string) => void } = $props();
|
| 23 |
|
| 24 |
const renderers = {
|
| 25 |
paragraph: Paragraph,
|
|
|
|
| 32 |
link: Link,
|
| 33 |
hr: Hr
|
| 34 |
};
|
| 35 |
+
|
| 36 |
+
let isEditing = $state(false);
|
| 37 |
+
let editContent = $state('');
|
| 38 |
+
|
| 39 |
+
function handleEdit() {
|
| 40 |
+
editContent = message.content as string;
|
| 41 |
+
isEditing = true;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function handleSubmitEdit() {
|
| 45 |
+
const trimmed = editContent.trim();
|
| 46 |
+
if (trimmed) {
|
| 47 |
+
onEdit?.(trimmed);
|
| 48 |
+
}
|
| 49 |
+
isEditing = false;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function handleCancelEdit() {
|
| 53 |
+
isEditing = false;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function handleKeydown(e: KeyboardEvent) {
|
| 57 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 58 |
+
e.preventDefault();
|
| 59 |
+
handleSubmitEdit();
|
| 60 |
+
} else if (e.key === 'Escape') {
|
| 61 |
+
handleCancelEdit();
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
</script>
|
| 65 |
|
| 66 |
<main
|
| 67 |
+
class="group/message pointer-events-auto cursor-auto p-1 text-lg leading-relaxed text-accent-foreground select-text"
|
| 68 |
>
|
| 69 |
{#if message?.role === 'user'}
|
| 70 |
+
{#if isEditing}
|
| 71 |
+
<div class="w-full">
|
| 72 |
+
<p
|
| 73 |
+
contenteditable="true"
|
| 74 |
+
bind:textContent={editContent}
|
| 75 |
+
class="w-full rounded-lg bg-accent px-2 py-1 outline-none"
|
| 76 |
+
onkeydown={handleKeydown}
|
| 77 |
+
></p>
|
| 78 |
+
<div class="mt-1.5 flex items-center justify-end gap-1">
|
| 79 |
+
<Button variant="transparent" size="icon-2xs" onclick={handleCancelEdit}>
|
| 80 |
+
<X class="size-3" />
|
| 81 |
+
</Button>
|
| 82 |
+
<Button variant="transparent" size="icon-2xs" onclick={handleSubmitEdit}>
|
| 83 |
+
<Check class="size-3 text-primary" />
|
| 84 |
+
</Button>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
{:else}
|
| 88 |
+
<p class="relative max-w-max pr-8">
|
| 89 |
+
{message.content}
|
| 90 |
+
<Button
|
| 91 |
+
variant="transparent"
|
| 92 |
+
size="icon-2xs"
|
| 93 |
+
class="absolute top-0.5 right-0 opacity-0 group-hover/message:opacity-100"
|
| 94 |
+
onclick={handleEdit}
|
| 95 |
+
>
|
| 96 |
+
<PenLine class="size-3" />
|
| 97 |
+
</Button>
|
| 98 |
+
</p>
|
| 99 |
+
{/if}
|
| 100 |
{:else}
|
| 101 |
{#if message.reasoning}
|
| 102 |
<Think
|
src/lib/components/chat/User.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import { Send, X } from '@lucide/svelte';
|
| 3 |
import {
|
| 4 |
Handle,
|
| 5 |
useEdges,
|
|
@@ -43,6 +43,7 @@
|
|
| 43 |
let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
|
| 44 |
let showWelcome = $derived((nodeData.current?.data.showWelcome as boolean) ?? true);
|
| 45 |
let isParentNode = $derived((nodeData.current?.data.isParentNode as boolean) ?? false);
|
|
|
|
| 46 |
|
| 47 |
let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
|
| 48 |
let loading = $state.raw<boolean>(false);
|
|
@@ -153,6 +154,33 @@
|
|
| 153 |
deleteElements({ nodes: [{ id: id }] });
|
| 154 |
}
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
onMount(() => {
|
| 157 |
if (prompt.trim() !== '' && prompt) {
|
| 158 |
handleTriggerAction();
|
|
@@ -165,6 +193,14 @@
|
|
| 165 |
class="group/user relative z-10 w-[calc(100dvw-2rem)] rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
|
| 166 |
>
|
| 167 |
<div class="nodrag pointer-events-auto cursor-auto">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
<header class="mb-3 flex items-center justify-between">
|
| 169 |
<div class="flex flex-wrap items-center gap-1">
|
| 170 |
<ListModels {selectedModels} showSelector={!lastMessage} onToggleModel={toggleModel} />
|
|
@@ -175,7 +211,7 @@
|
|
| 175 |
</header>
|
| 176 |
<ErrorMessage bind:error={errorMessage} />
|
| 177 |
{#if lastMessage}
|
| 178 |
-
<Message message={lastMessage} />
|
| 179 |
{:else}
|
| 180 |
<footer class="flex flex-col items-end transition-all duration-300">
|
| 181 |
<textarea
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { PenLine, Send, X } from '@lucide/svelte';
|
| 3 |
import {
|
| 4 |
Handle,
|
| 5 |
useEdges,
|
|
|
|
| 43 |
let isFirstNode = $derived((nodeData.current?.data.isFirstNode as boolean) ?? false);
|
| 44 |
let showWelcome = $derived((nodeData.current?.data.showWelcome as boolean) ?? true);
|
| 45 |
let isParentNode = $derived((nodeData.current?.data.isParentNode as boolean) ?? false);
|
| 46 |
+
let isFromEdit = $derived((nodeData.current?.data.isFromEdit as boolean) ?? false);
|
| 47 |
|
| 48 |
let prompt = $state.raw<string>((nodeData.current?.data.prompt as string) ?? '');
|
| 49 |
let loading = $state.raw<boolean>(false);
|
|
|
|
| 154 |
deleteElements({ nodes: [{ id: id }] });
|
| 155 |
}
|
| 156 |
|
| 157 |
+
function handleEditMessage(newContent: string) {
|
| 158 |
+
const newNodeId = `user-${crypto.randomUUID()}`;
|
| 159 |
+
const prevMessages = messages.slice(0, -1);
|
| 160 |
+
const newNode: Node = {
|
| 161 |
+
id: newNodeId,
|
| 162 |
+
type: 'user',
|
| 163 |
+
position: { x: 0, y: 0 },
|
| 164 |
+
data: {
|
| 165 |
+
role: 'user',
|
| 166 |
+
selectedModels,
|
| 167 |
+
messages: prevMessages,
|
| 168 |
+
isFirstNode: false,
|
| 169 |
+
isFromEdit: true,
|
| 170 |
+
showWelcome: false,
|
| 171 |
+
isParentNode: true,
|
| 172 |
+
prompt: newContent
|
| 173 |
+
}
|
| 174 |
+
};
|
| 175 |
+
const newEdge: Edge = {
|
| 176 |
+
id: `edge-${crypto.randomUUID()}`,
|
| 177 |
+
source: id,
|
| 178 |
+
target: newNodeId
|
| 179 |
+
};
|
| 180 |
+
updateNodes((currentNodes) => [...currentNodes, newNode]);
|
| 181 |
+
updateEdges((currentEdges) => [...currentEdges, newEdge]);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
onMount(() => {
|
| 185 |
if (prompt.trim() !== '' && prompt) {
|
| 186 |
handleTriggerAction();
|
|
|
|
| 193 |
class="group/user relative z-10 w-[calc(100dvw-2rem)] rounded-3xl border border-border bg-background p-5 shadow-lg/5 lg:w-[600px]"
|
| 194 |
>
|
| 195 |
<div class="nodrag pointer-events-auto cursor-auto">
|
| 196 |
+
{#if isFromEdit}
|
| 197 |
+
<span
|
| 198 |
+
class="mb-2 inline-flex items-center justify-center gap-1 rounded-md bg-accent px-2 py-1 text-[11px] text-muted-foreground"
|
| 199 |
+
>
|
| 200 |
+
<PenLine class="size-2.5" />
|
| 201 |
+
Edited message
|
| 202 |
+
</span>
|
| 203 |
+
{/if}
|
| 204 |
<header class="mb-3 flex items-center justify-between">
|
| 205 |
<div class="flex flex-wrap items-center gap-1">
|
| 206 |
<ListModels {selectedModels} showSelector={!lastMessage} onToggleModel={toggleModel} />
|
|
|
|
| 211 |
</header>
|
| 212 |
<ErrorMessage bind:error={errorMessage} />
|
| 213 |
{#if lastMessage}
|
| 214 |
+
<Message nodeId={id} message={lastMessage} onEdit={handleEditMessage} />
|
| 215 |
{:else}
|
| 216 |
<footer class="flex flex-col items-end transition-all duration-300">
|
| 217 |
<textarea
|
src/lib/components/chat/markdown/Code.svelte
CHANGED
|
@@ -38,11 +38,11 @@
|
|
| 38 |
<div
|
| 39 |
class="flex items-center justify-between border-b border-border/60 bg-muted px-3 py-1.5 dark:bg-accent/30"
|
| 40 |
>
|
| 41 |
-
<span class="font-mono text-
|
| 42 |
</div>
|
| 43 |
{/if}
|
| 44 |
<div class="group relative">
|
| 45 |
-
<HighlightAuto code={text} class="font-mono text-
|
| 46 |
<Button
|
| 47 |
variant="outline"
|
| 48 |
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100"
|
|
|
|
| 38 |
<div
|
| 39 |
class="flex items-center justify-between border-b border-border/60 bg-muted px-3 py-1.5 dark:bg-accent/30"
|
| 40 |
>
|
| 41 |
+
<span class="font-mono text-sm text-muted-foreground">{lang}</span>
|
| 42 |
</div>
|
| 43 |
{/if}
|
| 44 |
<div class="group relative">
|
| 45 |
+
<HighlightAuto code={text} class="font-mono text-sm leading-relaxed" />
|
| 46 |
<Button
|
| 47 |
variant="outline"
|
| 48 |
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100"
|
src/lib/components/chat/markdown/Codespan.svelte
CHANGED
|
@@ -3,6 +3,6 @@
|
|
| 3 |
</script>
|
| 4 |
|
| 5 |
<code
|
| 6 |
-
class="rounded-md border border-border/40 bg-accent-foreground/5 px-1.5 py-0.5 font-mono text-
|
| 7 |
>{raw.replace(/`/g, '')}</code
|
| 8 |
>
|
|
|
|
| 3 |
</script>
|
| 4 |
|
| 5 |
<code
|
| 6 |
+
class="rounded-md border border-border/40 bg-accent-foreground/5 px-1.5 py-0.5 font-mono text-xs text-foreground/85"
|
| 7 |
>{raw.replace(/`/g, '')}</code
|
| 8 |
>
|
src/lib/components/chat/markdown/Heading.svelte
CHANGED
|
@@ -5,19 +5,19 @@
|
|
| 5 |
</script>
|
| 6 |
|
| 7 |
{#if depth === 1}
|
| 8 |
-
<h1 class="mt-4 mb-2 text-
|
| 9 |
{@render children?.()}
|
| 10 |
</h1>
|
| 11 |
{:else if depth === 2}
|
| 12 |
-
<h2 class="mt-3.5 mb-2 text-
|
| 13 |
{@render children?.()}
|
| 14 |
</h2>
|
| 15 |
{:else if depth === 3}
|
| 16 |
-
<h3 class="mt-3 mb-1.5 text-
|
| 17 |
{@render children?.()}
|
| 18 |
</h3>
|
| 19 |
{:else}
|
| 20 |
-
<h4 class="mt-2.5 mb-1 text-
|
| 21 |
{@render children?.()}
|
| 22 |
</h4>
|
| 23 |
{/if}
|
|
|
|
| 5 |
</script>
|
| 6 |
|
| 7 |
{#if depth === 1}
|
| 8 |
+
<h1 class="mt-4 mb-2 text-2xl font-semibold text-foreground first:mt-0">
|
| 9 |
{@render children?.()}
|
| 10 |
</h1>
|
| 11 |
{:else if depth === 2}
|
| 12 |
+
<h2 class="mt-3.5 mb-2 text-xl font-semibold text-foreground first:mt-0">
|
| 13 |
{@render children?.()}
|
| 14 |
</h2>
|
| 15 |
{:else if depth === 3}
|
| 16 |
+
<h3 class="mt-3 mb-1.5 text-lg font-semibold text-foreground first:mt-0">
|
| 17 |
{@render children?.()}
|
| 18 |
</h3>
|
| 19 |
{:else}
|
| 20 |
+
<h4 class="mt-2.5 mb-1 text-base font-semibold text-foreground first:mt-0">
|
| 21 |
{@render children?.()}
|
| 22 |
</h4>
|
| 23 |
{/if}
|
src/lib/components/chat/markdown/List.svelte
CHANGED
|
@@ -6,14 +6,14 @@
|
|
| 6 |
|
| 7 |
{#if ordered}
|
| 8 |
<ol
|
| 9 |
-
class="my-2 list-outside list-decimal space-y-1 pl-5 text-
|
| 10 |
{start}
|
| 11 |
>
|
| 12 |
{@render children?.()}
|
| 13 |
</ol>
|
| 14 |
{:else}
|
| 15 |
<ul
|
| 16 |
-
class="my-2 list-outside list-disc space-y-1 pl-5 text-
|
| 17 |
>
|
| 18 |
{@render children?.()}
|
| 19 |
</ul>
|
|
|
|
| 6 |
|
| 7 |
{#if ordered}
|
| 8 |
<ol
|
| 9 |
+
class="my-2 list-outside list-decimal space-y-1 pl-5 text-sm text-foreground/90 marker:text-muted-foreground"
|
| 10 |
{start}
|
| 11 |
>
|
| 12 |
{@render children?.()}
|
| 13 |
</ol>
|
| 14 |
{:else}
|
| 15 |
<ul
|
| 16 |
+
class="my-2 list-outside list-disc space-y-1 pl-5 text-sm text-foreground/90 marker:text-muted-foreground"
|
| 17 |
>
|
| 18 |
{@render children?.()}
|
| 19 |
</ul>
|
src/lib/components/chat/markdown/Paragraph.svelte
CHANGED
|
@@ -3,4 +3,4 @@
|
|
| 3 |
let { children }: { children?: Snippet } = $props();
|
| 4 |
</script>
|
| 5 |
|
| 6 |
-
<p class="mb-3 text-
|
|
|
|
| 3 |
let { children }: { children?: Snippet } = $props();
|
| 4 |
</script>
|
| 5 |
|
| 6 |
+
<p class="mb-3 text-sm leading-relaxed text-foreground/90 last:mb-0">{@render children?.()}</p>
|
src/lib/components/chat/markdown/think/Heading.svelte
CHANGED
|
@@ -5,19 +5,19 @@
|
|
| 5 |
</script>
|
| 6 |
|
| 7 |
{#if depth === 1}
|
| 8 |
-
<h1 class="mt-4 mb-2 text-
|
| 9 |
{@render children?.()}
|
| 10 |
</h1>
|
| 11 |
{:else if depth === 2}
|
| 12 |
-
<h2 class="mt-3.5 mb-2 text-
|
| 13 |
{@render children?.()}
|
| 14 |
</h2>
|
| 15 |
{:else if depth === 3}
|
| 16 |
-
<h3 class="mt-3 mb-1.5 text-
|
| 17 |
{@render children?.()}
|
| 18 |
</h3>
|
| 19 |
{:else}
|
| 20 |
-
<h4 class="mt-2.5 mb-1 text-
|
| 21 |
{@render children?.()}
|
| 22 |
</h4>
|
| 23 |
{/if}
|
|
|
|
| 5 |
</script>
|
| 6 |
|
| 7 |
{#if depth === 1}
|
| 8 |
+
<h1 class="mt-4 mb-2 text-base font-semibold text-foreground first:mt-0">
|
| 9 |
{@render children?.()}
|
| 10 |
</h1>
|
| 11 |
{:else if depth === 2}
|
| 12 |
+
<h2 class="mt-3.5 mb-2 text-sm font-semibold text-foreground first:mt-0">
|
| 13 |
{@render children?.()}
|
| 14 |
</h2>
|
| 15 |
{:else if depth === 3}
|
| 16 |
+
<h3 class="mt-3 mb-1.5 text-sm font-semibold text-foreground first:mt-0">
|
| 17 |
{@render children?.()}
|
| 18 |
</h3>
|
| 19 |
{:else}
|
| 20 |
+
<h4 class="mt-2.5 mb-1 text-xs font-semibold text-foreground first:mt-0">
|
| 21 |
{@render children?.()}
|
| 22 |
</h4>
|
| 23 |
{/if}
|
src/lib/components/ui/button/button.svelte
CHANGED
|
@@ -23,7 +23,9 @@
|
|
| 23 |
'bg-blue-500/10 text-blue-500 border border-blue-500/20 hover:bg-blue-500/20 shadow-xs',
|
| 24 |
'outline-destructive':
|
| 25 |
'bg-rose-500/10 hover:bg-rose-500/20 text-rose-600 border border-rose-500/20 shadow-xs',
|
| 26 |
-
amber: 'bg-amber-500 text-white hover:brightness-110 shadow-xs'
|
|
|
|
|
|
|
| 27 |
},
|
| 28 |
size: {
|
| 29 |
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
|
|
| 23 |
'bg-blue-500/10 text-blue-500 border border-blue-500/20 hover:bg-blue-500/20 shadow-xs',
|
| 24 |
'outline-destructive':
|
| 25 |
'bg-rose-500/10 hover:bg-rose-500/20 text-rose-600 border border-rose-500/20 shadow-xs',
|
| 26 |
+
amber: 'bg-amber-500 text-white hover:brightness-110 shadow-xs',
|
| 27 |
+
transparent:
|
| 28 |
+
'bg-gray-200/50 text-gray-500 hover:bg-gray-200/80 border-transparent! dark:bg-gray-800/50 dark:text-gray-400 dark:hover:bg-gray-800/80'
|
| 29 |
},
|
| 30 |
size: {
|
| 31 |
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|