diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 00:21:04 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 00:21:04 +0900 |
| commit | 979fd1aac559805e05b36369e0fb756a8ec517dd (patch) | |
| tree | d7d69d8a80a52a9cf14a54d7cb92e16cdb732a75 /src/features/chat/ui | |
| parent | 5d9ae1849337b64af1b0d47c23b8c4950a55f792 (diff) | |
| download | dispatch-web-979fd1aac559805e05b36369e0fb756a8ec517dd.tar.gz dispatch-web-979fd1aac559805e05b36369e0fb756a8ec517dd.zip | |
Slice 2 wave 2: IndexedDB cache adapter + chat feature
- adapters/idb: createIdbChunkStore implements the ConversationChunkStore port
over IndexedDB (compound [conversationId,seq] key, idempotent append, meta
store for lastAccess); 8 tests with fake-indexeddb
- features/chat: createChatStore (runes-thin over the core/chunks reducer, all
effects injected via ChatTransport/HistorySync/ConversationCache ports) +
ChatView/Composer svelte-thin UI; folds chat.delta, syncs on turn-sealed,
hydrates from cache then catches up; 25 tests
Verified green: svelte-check 0/0, vitest 202, biome clean, build ok.
Diffstat (limited to 'src/features/chat/ui')
| -rw-r--r-- | src/features/chat/ui/ChatView.svelte | 45 | ||||
| -rw-r--r-- | src/features/chat/ui/Composer.svelte | 33 |
2 files changed, 78 insertions, 0 deletions
diff --git a/src/features/chat/ui/ChatView.svelte b/src/features/chat/ui/ChatView.svelte new file mode 100644 index 0000000..a7c39cc --- /dev/null +++ b/src/features/chat/ui/ChatView.svelte @@ -0,0 +1,45 @@ +<script lang="ts"> + import type { RenderedChunk } from "../index"; + + let { chunks }: { chunks: readonly RenderedChunk[] } = $props(); +</script> + +<div class="chat-transcript" role="log" aria-live="polite"> + {#each chunks as rendered (rendered)} + <article + class="message message--{rendered.role}" + class:message--provisional={rendered.provisional} + > + <header class="message__role">{rendered.role}</header> + <div class="message__content"> + {#if rendered.chunk.type === "text"} + <p>{rendered.chunk.text}</p> + {:else if rendered.chunk.type === "thinking"} + <details> + <summary>Thinking</summary> + <p>{rendered.chunk.text}</p> + </details> + {:else if rendered.chunk.type === "tool-call"} + <div class="tool-call"> + <strong>{rendered.chunk.toolName}</strong> + <pre>{JSON.stringify(rendered.chunk.input, null, 2)}</pre> + </div> + {:else if rendered.chunk.type === "tool-result"} + <div class="tool-result" class:tool-result--error={rendered.chunk.isError}> + <strong>{rendered.chunk.toolName}</strong> + <pre>{rendered.chunk.content}</pre> + </div> + {:else if rendered.chunk.type === "error"} + <div class="error" role="alert"> + {rendered.chunk.message} + {#if rendered.chunk.code} + <span class="error__code">[{rendered.chunk.code}]</span> + {/if} + </div> + {:else if rendered.chunk.type === "system"} + <div class="system">{rendered.chunk.text}</div> + {/if} + </div> + </article> + {/each} +</div> diff --git a/src/features/chat/ui/Composer.svelte b/src/features/chat/ui/Composer.svelte new file mode 100644 index 0000000..dc71e11 --- /dev/null +++ b/src/features/chat/ui/Composer.svelte @@ -0,0 +1,33 @@ +<script lang="ts"> + let { onSend }: { onSend: (text: string) => void } = $props(); + + let text = $state(""); + + function handleSubmit(): void { + const trimmed = text.trim(); + if (trimmed.length === 0) return; + onSend(trimmed); + text = ""; + } + + function handleKeydown(e: KeyboardEvent): void { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + } +</script> + +<form class="composer" onsubmit={prevent => { prevent.preventDefault(); handleSubmit(); }}> + <textarea + class="composer__input" + bind:value={text} + onkeydown={handleKeydown} + placeholder="Type a message..." + rows="3" + aria-label="Message input" + ></textarea> + <button class="composer__send" type="submit" disabled={text.trim().length === 0}> + Send + </button> +</form> |
