summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 00:21:04 +0900
committerAdam Malczewski <[email protected]>2026-06-07 00:21:04 +0900
commit979fd1aac559805e05b36369e0fb756a8ec517dd (patch)
treed7d69d8a80a52a9cf14a54d7cb92e16cdb732a75 /src/features/chat/ui
parent5d9ae1849337b64af1b0d47c23b8c4950a55f792 (diff)
downloaddispatch-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.svelte45
-rw-r--r--src/features/chat/ui/Composer.svelte33
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>