summaryrefslogtreecommitdiffhomepage
path: root/src/app/App.svelte
blob: cc9866e10416e8621e3638a24963873f1b1aef0d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<script lang="ts">
	import type { InvokeMessage } from "@dispatch/ui-contract";
	import { ChatView, Composer, ModelSelector } from "../features/chat";
	import { TabBar } from "../features/tabs";
	import { SurfaceView } from "../features/surface-host";
	import type { AppStore } from "./store.svelte";

	let { store }: { store: AppStore } = $props();

	function handleSelect(surfaceId: string) {
		store.select(surfaceId);
	}

	function handleInvoke(msg: InvokeMessage) {
		store.invoke(msg.surfaceId, msg.actionId, msg.payload);
	}

	function handleSend(text: string) {
		store.send(text);
	}

	function handleSelectModel(model: string) {
		store.selectModel(model);
	}
</script>

<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" class="alert alert-error mx-4 mt-2">
			<strong>Error:</strong>
			{store.lastError.message}
		</div>
	{/if}

	{#if store.activeChat.error}
		<div role="alert" class="alert alert-warning mx-4 mt-2">
			<strong>Chat error:</strong>
			{store.activeChat.error}
		</div>
	{/if}

	<TabBar
		tabs={store.tabs}
		activeConversationId={store.activeConversationId}
		onSelect={(id) => store.selectTab(id)}
		onClose={(id) => store.closeTab(id)}
		onNewDraft={() => store.newDraft()}
	/>

	<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} />
	</div>

	{#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)}
					<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}
			</div>
		</section>
	{/if}

	{#if store.selectedSpec}
		<section class="border-t border-base-300 p-4">
			<SurfaceView spec={store.selectedSpec} onInvoke={handleInvoke} />
		</section>
	{/if}
</main>