diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 02:20:51 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 02:20:51 +0900 |
| commit | 5f867c6711ed693aa2a029ae1fb07eb1106ee32c (patch) | |
| tree | 3d2942b455454d8c4e241b6d3fe22bb3526e7ed8 /src/app/App.svelte | |
| parent | 529c6a2bb56447fe93796111df3d4cc5a05fdd93 (diff) | |
| download | dispatch-web-5f867c6711ed693aa2a029ae1fb07eb1106ee32c.tar.gz dispatch-web-5f867c6711ed693aa2a029ae1fb07eb1106ee32c.zip | |
Slice 3 wave B: tabbed multi-conversation app + model selector (DaisyUI)
- store.svelte.ts: tabs store over injected localStorage; one chat store per
conversation (Map); single WS routes chat.delta/error by conversationId;
draft (null active) mints a conversationId and becomes a tab on first send
(title from deriveTitle); GET /models catalog; default model flash; close tab
= dispose + cache.delete (local forget) + neighbour activation; restore tabs
from storage + load() on construct
- App.svelte: DaisyUI tab strip (+ / close), model selector, chat, surfaces
- AppStore: tabs/activeConversationId/activeChat/models/activeModel +
send/selectModel/newDraft/selectTab/closeTab; +localStorage inject opt
Verified: svelte-check 0/0, vitest 281 (stable x2), biome clean, build ok.
Diffstat (limited to 'src/app/App.svelte')
| -rw-r--r-- | src/app/App.svelte | 111 |
1 files changed, 80 insertions, 31 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte index 92939c2..811dc75 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -1,7 +1,7 @@ <script lang="ts"> import type { InvokeMessage } from "@dispatch/ui-contract"; + import { ChatView, Composer, ModelSelector } from "../features/chat"; import { SurfaceView } from "../features/surface-host"; - import { ChatView, Composer } from "../features/chat"; import type { AppStore } from "./store.svelte"; let { store }: { store: AppStore } = $props(); @@ -15,56 +15,105 @@ } function handleSend(text: string) { - store.chat.send(text); + store.send(text); + } + + function handleSelectModel(model: string) { + store.selectModel(model); } </script> -<main> - <h1>Dispatch</h1> +<main class="flex h-screen flex-col"> + <div class="flex items-center justify-between border-b border-base-300 px-4 py-2"> + <h1 class="text-lg font-bold">Dispatch</h1> + </div> {#if store.lastError} - <div role="alert"> + <div role="alert" class="alert alert-error mx-4 mt-2"> <strong>Error:</strong> {store.lastError.message} </div> {/if} - {#if store.chat.error} - <div role="alert"> + {#if store.activeChat.error} + <div role="alert" class="alert alert-warning mx-4 mt-2"> <strong>Chat error:</strong> - {store.chat.error} + {store.activeChat.error} </div> {/if} - <section> - <h2>Chat</h2> - <ChatView chunks={store.chat.chunks} /> + <div class="flex items-center gap-2 border-b border-base-300 px-4"> + <div class="tabs tabs-border flex-1"> + {#each store.tabs as tab (tab.conversationId)} + <div + class="tab" + class:tab-active={tab.conversationId === store.activeConversationId} + role="tab" + tabindex="0" + onclick={() => store.selectTab(tab.conversationId)} + onkeydown={(e) => { if (e.key === "Enter") store.selectTab(tab.conversationId); }} + > + <span class="max-w-[120px] truncate">{tab.title}</span> + <button + class="btn btn-ghost btn-xs ml-1" + aria-label="Close tab" + onclick={(e) => { + e.stopPropagation(); + store.closeTab(tab.conversationId); + }} + > + × + </button> + </div> + {/each} + <button + class="tab" + class:tab-active={store.activeConversationId === null} + onclick={() => store.newDraft()} + aria-label="New chat" + > + + + </button> + </div> + </div> + + <div class="flex flex-1 flex-col overflow-hidden"> + <div class="flex items-center gap-2 px-4 py-2"> + <ModelSelector + models={store.models} + selected={store.activeModel} + onSelect={handleSelectModel} + /> + </div> + + <div class="flex-1 overflow-y-auto"> + <ChatView chunks={store.activeChat.chunks} /> + </div> + <Composer onSend={handleSend} /> - </section> + </div> - <section> - <h2>Surfaces</h2> - {#if store.catalog.length === 0} - <p>No surfaces available</p> - {:else} - <ul> + {#if store.catalog.length > 0} + <section class="border-t border-base-300 p-4"> + <h2 class="mb-2 text-sm font-semibold">Surfaces</h2> + <div class="flex flex-wrap gap-2"> {#each store.catalog as entry (entry.id)} - <li> - <button - aria-current={entry.id === store.selectedId ? "true" : undefined} - onclick={() => handleSelect(entry.id)} - > - {entry.title} - <span>({entry.region})</span> - </button> - </li> + <button + class="btn btn-sm" + class:btn-active={entry.id === store.selectedId} + aria-current={entry.id === store.selectedId ? "true" : undefined} + onclick={() => handleSelect(entry.id)} + > + {entry.title} + <span class="text-xs opacity-60">({entry.region})</span> + </button> {/each} - </ul> - {/if} - </section> + </div> + </section> + {/if} {#if store.selectedSpec} - <section> + <section class="border-t border-base-300 p-4"> <SurfaceView spec={store.selectedSpec} onInvoke={handleInvoke} /> </section> {/if} |
