@classic-homes/agent-chat 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +40 -0
  2. package/dist/components/AgentChat.svelte +65 -39
  3. package/dist/components/AgentChat.svelte.d.ts +2 -0
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +79 -96
  6. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  7. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  8. package/dist/components/ChatHeader.svelte +122 -0
  9. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  10. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  11. package/dist/components/Composer.svelte +78 -121
  12. package/dist/components/Composer.svelte.d.ts.map +1 -1
  13. package/dist/components/ConversationDialogs.svelte +134 -0
  14. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  15. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  16. package/dist/components/ConversationSidebar.svelte +135 -114
  17. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  18. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  19. package/dist/components/Markdown.svelte +48 -0
  20. package/dist/components/Markdown.svelte.d.ts +16 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  22. package/dist/components/MessageItem.svelte +27 -116
  23. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageList.svelte +23 -34
  25. package/dist/components/ModeBanner.svelte +7 -13
  26. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  27. package/dist/components/SandboxFrame.svelte +43 -0
  28. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  29. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  30. package/dist/components/ToolTimeline.svelte +17 -68
  31. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  32. package/dist/components/UiFrame.svelte +9 -55
  33. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  34. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  35. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  36. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  37. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  38. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  39. package/dist/components/markdown-prose.svelte.test.js +26 -0
  40. package/dist/composer-keys.d.ts +11 -0
  41. package/dist/composer-keys.d.ts.map +1 -0
  42. package/dist/composer-keys.js +10 -0
  43. package/dist/composer-keys.test.d.ts +2 -0
  44. package/dist/composer-keys.test.d.ts.map +1 -0
  45. package/dist/composer-keys.test.js +16 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/reducer.d.ts +10 -0
  50. package/dist/reducer.d.ts.map +1 -1
  51. package/dist/reducer.js +19 -1
  52. package/dist/reducer.test.js +29 -1
  53. package/dist/session.svelte.d.ts +6 -0
  54. package/dist/session.svelte.d.ts.map +1 -1
  55. package/dist/session.svelte.js +15 -1
  56. package/dist/ui-frame-protocol.d.ts +2 -2
  57. package/dist/ui-frame-protocol.js +2 -2
  58. package/dist/ui-frame-relay.d.ts +16 -0
  59. package/dist/ui-frame-relay.d.ts.map +1 -0
  60. package/dist/ui-frame-relay.js +22 -0
  61. package/dist/ui-frame-relay.test.d.ts +2 -0
  62. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  63. package/dist/ui-frame-relay.test.js +36 -0
  64. package/package.json +8 -5
@@ -1,6 +1,16 @@
1
1
  <script lang="ts">
2
- import { Button, Icon } from '@classic-homes/theme-svelte';
2
+ import {
3
+ Button,
4
+ cn,
5
+ DropdownMenu,
6
+ EmptyState,
7
+ Icon,
8
+ Input,
9
+ Skeleton,
10
+ Spinner,
11
+ } from '@classic-homes/theme-svelte';
3
12
  import type { ConversationSummary } from '@classic-homes/agent-contracts';
13
+ import ConversationDialogs, { type ConversationRef } from './ConversationDialogs.svelte';
4
14
 
5
15
  interface Props {
6
16
  conversations: ConversationSummary[];
@@ -10,10 +20,16 @@
10
20
  query?: string;
11
21
  onselect?: (conversationId: string) => void;
12
22
  onnew?: () => void;
23
+ /** Called with the trimmed query once typing pauses for `searchDelay` ms. */
13
24
  onsearch?: (query: string) => void;
25
+ searchDelay?: number;
14
26
  onmore?: () => void;
15
- onrename?: (conversationId: string, title: string) => void;
16
- ondelete?: (conversationId: string) => void;
27
+ /** May return a promise; a rejection keeps the rename dialog open with its message. */
28
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
29
+ /** Called after the user confirms; a rejection is passed to `onerror`. */
30
+ ondelete?: (conversationId: string) => void | Promise<void>;
31
+ onerror?: (message: string) => void;
32
+ class?: string;
17
33
  }
18
34
  let {
19
35
  conversations,
@@ -24,9 +40,12 @@
24
40
  onselect,
25
41
  onnew,
26
42
  onsearch,
43
+ searchDelay = 250,
27
44
  onmore,
28
45
  onrename,
29
46
  ondelete,
47
+ onerror,
48
+ class: className,
30
49
  }: Props = $props();
31
50
 
32
51
  function groupLabel(iso: string): string {
@@ -49,140 +68,142 @@
49
68
  }
50
69
  return [...map.entries()];
51
70
  });
52
- function rename(c: ConversationSummary): void {
53
- const title = window.prompt('Rename conversation', c.title);
54
- if (title && title.trim() && title !== c.title) onrename?.(c.conversationId, title.trim());
71
+ let renaming = $state<ConversationRef | null>(null);
72
+ let deleting = $state<ConversationRef | null>(null);
73
+ let navEl = $state<HTMLElement | null>(null);
74
+ let returnFocus = $state<HTMLElement | null>(null);
75
+ /** Remember the row's menu trigger before a dialog takes focus from the closing menu. */
76
+ function open(id: string, set: () => void): void {
77
+ returnFocus =
78
+ navEl?.querySelector<HTMLElement>(
79
+ `[data-conversation-id="${CSS.escape(id)}"] [aria-haspopup="menu"]`,
80
+ ) ?? null;
81
+ set();
82
+ }
83
+
84
+ let searchTimer: ReturnType<typeof setTimeout> | undefined;
85
+ function search(): void {
86
+ clearTimeout(searchTimer);
87
+ searchTimer = setTimeout(() => onsearch?.(query.trim()), searchDelay);
88
+ }
89
+ $effect(() => () => clearTimeout(searchTimer));
90
+
91
+ function rowItems(c: ConversationSummary) {
92
+ const ref = { conversationId: c.conversationId, title: c.title };
93
+ return [
94
+ ...(onrename
95
+ ? [
96
+ {
97
+ label: 'Rename',
98
+ icon: 'edit' as const,
99
+ onSelect: () => open(c.conversationId, () => (renaming = ref)),
100
+ },
101
+ ]
102
+ : []),
103
+ ...(ondelete
104
+ ? [
105
+ {
106
+ label: 'Delete',
107
+ icon: 'trash' as const,
108
+ destructive: true,
109
+ onSelect: () => open(c.conversationId, () => (deleting = ref)),
110
+ },
111
+ ]
112
+ : []),
113
+ ];
55
114
  }
56
115
  </script>
57
116
 
58
- <nav class="agent-sidebar" aria-label="Conversations">
59
- <div class="agent-sidebar__top">
117
+ <nav
118
+ bind:this={navEl}
119
+ class={cn('flex h-full w-72 flex-col border-r border-border bg-card', className)}
120
+ aria-label="Conversations"
121
+ >
122
+ <div class="grid gap-2 p-3">
60
123
  <Button size="sm" onclick={() => onnew?.()}><Icon name="plus" size={14} /> New chat</Button>
61
- <input
62
- class="agent-sidebar__search"
124
+ <Input
63
125
  type="search"
64
- placeholder="Search"
126
+ class="h-9"
127
+ placeholder="Search conversations"
65
128
  aria-label="Search conversations"
66
129
  bind:value={query}
67
- oninput={() => onsearch?.(query)}
130
+ oninput={search}
68
131
  />
69
132
  </div>
70
- <div class="agent-sidebar__list">
133
+ <div class="flex-1 overflow-y-auto px-2 pb-3">
134
+ {#if loading && conversations.length === 0}
135
+ <div class="grid gap-2 px-2 pt-3" aria-busy="true" aria-label="Loading conversations">
136
+ {#each [80, 65, 90, 55, 75] as width, i (i)}
137
+ <Skeleton variant="text" class="h-7" style={`width:${width}%`} />
138
+ {/each}
139
+ </div>
140
+ {:else if conversations.length === 0}
141
+ <EmptyState
142
+ size="sm"
143
+ icon={query.trim() ? 'search' : 'chat'}
144
+ headingLevel="p"
145
+ title={query.trim() ? 'No matching conversations' : 'No conversations yet'}
146
+ description={query.trim() ? 'Try another search.' : 'Start a chat to see it here.'}
147
+ class="py-8"
148
+ />
149
+ {/if}
71
150
  {#each groups as [label, items] (label)}
72
- <div class="agent-sidebar__group">{label}</div>
151
+ <div
152
+ class="sticky top-0 z-[1] bg-card px-2 pb-1 pt-3 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
153
+ >
154
+ {label}
155
+ </div>
73
156
  {#each items as c (c.conversationId)}
157
+ {@const active = c.conversationId === selectedId}
158
+ {@const menu = rowItems(c)}
74
159
  <div
75
- class="agent-sidebar__row"
76
- class:agent-sidebar__row--active={c.conversationId === selectedId}
160
+ data-conversation-id={c.conversationId}
161
+ class={cn(
162
+ 'group flex items-center gap-1 rounded-md pr-1 hover:bg-muted',
163
+ active && 'bg-muted font-medium',
164
+ )}
77
165
  >
78
166
  <button
79
167
  type="button"
80
- class="agent-sidebar__title"
168
+ class="flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
169
+ aria-current={active ? 'page' : undefined}
81
170
  onclick={() => onselect?.(c.conversationId)}
82
171
  title={c.title}
83
172
  >
84
- {c.title}{c.activeRequestId ? ' …' : ''}
173
+ <span class="truncate">{c.title}</span>
174
+ {#if c.activeRequestId}<Spinner
175
+ size="sm"
176
+ class="size-3 shrink-0"
177
+ label="Answering"
178
+ />{/if}
85
179
  </button>
86
- <div class="agent-sidebar__actions">
87
- {#if onrename}<button type="button" aria-label="Rename" onclick={() => rename(c)}
88
- ><Icon name="edit" size={13} /></button
89
- >{/if}
90
- {#if ondelete}<button
91
- type="button"
92
- aria-label="Delete"
93
- onclick={() => ondelete(c.conversationId)}><Icon name="trash" size={13} /></button
94
- >{/if}
95
- </div>
180
+ {#if menu.length}
181
+ <DropdownMenu
182
+ items={menu}
183
+ align="end"
184
+ triggerVariant="ghost"
185
+ triggerSize="icon"
186
+ triggerLabel={`Options for ${c.title}`}
187
+ triggerClass={cn(
188
+ 'size-7 shrink-0 text-muted-foreground opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[state=open]:opacity-100 [@media(hover:none)]:opacity-100',
189
+ active && 'opacity-100',
190
+ )}
191
+ showChevron={false}
192
+ >
193
+ {#snippet trigger()}<Icon name="more-horizontal" size={16} />{/snippet}
194
+ </DropdownMenu>
195
+ {/if}
96
196
  </div>
97
197
  {/each}
98
198
  {/each}
99
- {#if !loading && conversations.length === 0}<div class="agent-sidebar__empty">
100
- No conversations yet.
101
- </div>{/if}
102
- {#if hasMore}<Button variant="ghost" size="sm" {loading} onclick={() => onmore?.()}
103
- >Load more</Button
199
+ {#if hasMore}<Button
200
+ variant="ghost"
201
+ size="sm"
202
+ class="w-full"
203
+ {loading}
204
+ onclick={() => onmore?.()}>Load more</Button
104
205
  >{/if}
105
206
  </div>
106
207
  </nav>
107
208
 
108
- <style>
109
- .agent-sidebar {
110
- display: flex;
111
- flex-direction: column;
112
- height: 100%;
113
- border-right: 1px solid hsl(var(--border));
114
- background: hsl(var(--card));
115
- width: 16rem;
116
- }
117
- .agent-sidebar__top {
118
- display: grid;
119
- gap: 0.5rem;
120
- padding: 0.75rem;
121
- }
122
- .agent-sidebar__search {
123
- font: inherit;
124
- font-size: 0.875rem;
125
- padding: 0.375rem 0.625rem;
126
- border: 1px solid hsl(var(--border));
127
- border-radius: 0.5rem;
128
- background: hsl(var(--background));
129
- color: inherit;
130
- }
131
- .agent-sidebar__list {
132
- flex: 1;
133
- overflow-y: auto;
134
- padding: 0 0.5rem 0.75rem;
135
- }
136
- .agent-sidebar__group {
137
- font-size: 0.6875rem;
138
- text-transform: uppercase;
139
- letter-spacing: 0.06em;
140
- color: hsl(var(--muted-foreground));
141
- padding: 0.75rem 0.5rem 0.25rem;
142
- }
143
- .agent-sidebar__row {
144
- display: flex;
145
- align-items: center;
146
- border-radius: 0.5rem;
147
- }
148
- .agent-sidebar__row:hover,
149
- .agent-sidebar__row--active {
150
- background: hsl(var(--muted));
151
- }
152
- .agent-sidebar__title {
153
- flex: 1;
154
- min-width: 0;
155
- text-align: left;
156
- background: none;
157
- border: 0;
158
- padding: 0.375rem 0.5rem;
159
- font: inherit;
160
- font-size: 0.875rem;
161
- color: inherit;
162
- cursor: pointer;
163
- overflow: hidden;
164
- text-overflow: ellipsis;
165
- white-space: nowrap;
166
- }
167
- .agent-sidebar__actions {
168
- display: none;
169
- gap: 0.125rem;
170
- padding-right: 0.25rem;
171
- }
172
- .agent-sidebar__row:hover .agent-sidebar__actions,
173
- .agent-sidebar__row--active .agent-sidebar__actions {
174
- display: flex;
175
- }
176
- .agent-sidebar__actions button {
177
- border: 0;
178
- background: none;
179
- cursor: pointer;
180
- color: hsl(var(--muted-foreground));
181
- padding: 0.25rem;
182
- }
183
- .agent-sidebar__empty {
184
- font-size: 0.8125rem;
185
- color: hsl(var(--muted-foreground));
186
- padding: 0.5rem;
187
- }
188
- </style>
209
+ <ConversationDialogs bind:renaming bind:deleting {onrename} {ondelete} {onerror} {returnFocus} />
@@ -7,10 +7,16 @@ interface Props {
7
7
  query?: string;
8
8
  onselect?: (conversationId: string) => void;
9
9
  onnew?: () => void;
10
+ /** Called with the trimmed query once typing pauses for `searchDelay` ms. */
10
11
  onsearch?: (query: string) => void;
12
+ searchDelay?: number;
11
13
  onmore?: () => void;
12
- onrename?: (conversationId: string, title: string) => void;
13
- ondelete?: (conversationId: string) => void;
14
+ /** May return a promise; a rejection keeps the rename dialog open with its message. */
15
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
16
+ /** Called after the user confirms; a rejection is passed to `onerror`. */
17
+ ondelete?: (conversationId: string) => void | Promise<void>;
18
+ onerror?: (message: string) => void;
19
+ class?: string;
14
20
  }
15
21
  declare const ConversationSidebar: import("svelte").Component<Props, {}, "query">;
16
22
  type ConversationSidebar = ReturnType<typeof ConversationSidebar>;
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationSidebar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConversationSidebar.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAGxE,UAAU,KAAK;IACb,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7C;AA4EH,QAAA,MAAM,mBAAmB,gDAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"ConversationSidebar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConversationSidebar.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAIxE,UAAU,KAAK;IACb,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0IH,QAAA,MAAM,mBAAmB,gDAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,48 @@
1
+ <!--
2
+ @component
3
+ Renders sanitized markdown HTML from `parseMarkdown`, with the chat prose rules. The rules are
4
+ Tailwind arbitrary variants on the container rather than a stylesheet: the Classic App's
5
+ cascade-layers polyfill raises the specificity of its base styles above any plain component
6
+ CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat.
7
+ -->
8
+ <script lang="ts" module>
9
+ const PROSE = [
10
+ 'agent-markdown leading-[1.65] text-foreground [overflow-wrap:anywhere]',
11
+ '[&>:first-child]:mt-0 [&>:last-child]:mb-0',
12
+ '[&_p]:mb-3 [&_p]:mt-0 [&_p:empty]:hidden',
13
+ '[&_h2]:mb-2 [&_h2]:mt-5 [&_h2]:text-[1.25em] [&_h2]:font-semibold [&_h2]:leading-snug',
14
+ '[&_h3]:mb-2 [&_h3]:mt-5 [&_h3]:text-[1.125em] [&_h3]:font-semibold [&_h3]:leading-snug',
15
+ '[&_h4]:mb-2 [&_h4]:mt-4 [&_h4]:text-[1em] [&_h4]:font-semibold [&_h4]:leading-snug',
16
+ '[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_a:hover]:text-primary/80',
17
+ '[&_ul]:mb-3 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:mb-3 [&_ol]:list-decimal [&_ol]:pl-6',
18
+ '[&_li]:mb-1 [&_li>ul]:mb-0 [&_li>ul]:mt-1 [&_li>ol]:mb-0 [&_li>ol]:mt-1',
19
+ '[&_ul.agent-tasklist]:list-none [&_ul.agent-tasklist]:pl-1',
20
+ '[&_.agent-tasklist_input]:mr-1.5 [&_.agent-tasklist_input]:accent-primary',
21
+ '[&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.875em]',
22
+ '[&_pre]:mb-3 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:border [&_pre]:border-border [&_pre]:bg-muted/60 [&_pre]:px-4 [&_pre]:py-3 [&_pre]:text-[13px] [&_pre]:leading-relaxed',
23
+ '[&_pre_code]:rounded-none [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[length:inherit]',
24
+ '[&_blockquote]:mb-3 [&_blockquote]:border-l-4 [&_blockquote]:border-primary [&_blockquote]:bg-muted/50 [&_blockquote]:px-4 [&_blockquote]:py-2 [&_blockquote]:text-muted-foreground',
25
+ '[&_hr]:my-5 [&_hr]:border-border',
26
+ '[&_table]:mb-3 [&_table]:block [&_table]:max-w-full [&_table]:border-collapse [&_table]:overflow-x-auto [&_table]:text-[0.875em]',
27
+ '[&_th]:border [&_th]:border-border [&_th]:px-3 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top',
28
+ '[&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-1.5 [&_td]:text-left [&_td]:align-top',
29
+ '[&_th]:bg-muted [&_th]:font-semibold',
30
+ '[&_img]:max-w-full [&_img]:rounded-lg',
31
+ ].join(' ');
32
+ </script>
33
+
34
+ <script lang="ts">
35
+ import { cn } from '@classic-homes/theme-svelte';
36
+
37
+ interface Props {
38
+ /** Output of `parseMarkdown` (already sanitized by DOMPurify). */
39
+ html: string;
40
+ class?: string;
41
+ onclick?: (event: MouseEvent) => void;
42
+ }
43
+ let { html, class: className, onclick }: Props = $props();
44
+ </script>
45
+
46
+ <!-- Clicks are delegated to internal links inside the HTML; the links themselves are focusable. -->
47
+ <!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
48
+ <div class={className ? `${PROSE} ${cn(className)}` : PROSE} {onclick}>{@html html}</div>
@@ -0,0 +1,16 @@
1
+ interface Props {
2
+ /** Output of `parseMarkdown` (already sanitized by DOMPurify). */
3
+ html: string;
4
+ class?: string;
5
+ onclick?: (event: MouseEvent) => void;
6
+ }
7
+ /**
8
+ * Renders sanitized markdown HTML from `parseMarkdown`, with the chat prose rules. The rules are
9
+ * Tailwind arbitrary variants on the container rather than a stylesheet: the Classic App's
10
+ * cascade-layers polyfill raises the specificity of its base styles above any plain component
11
+ * CSS, while utilities still win. Values follow theme-docs `.markdown-content`, sized for chat.
12
+ */
13
+ declare const Markdown: import("svelte").Component<Props, {}, "">;
14
+ type Markdown = ReturnType<typeof Markdown>;
15
+ export default Markdown;
16
+ //# sourceMappingURL=Markdown.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Markdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Markdown.svelte.ts"],"names":[],"mappings":"AA+BE,UAAU,KAAK;IACb,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAgBH;;;;;GAKG;AACH,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Button, Icon } from '@classic-homes/theme-svelte';
3
+ import Markdown from './Markdown.svelte';
3
4
  import type { ChatMessage } from '../reducer.js';
4
5
  import { errorTitle } from '../reducer.js';
5
6
  import { parseMarkdown } from '../markdown.js';
@@ -78,14 +79,18 @@
78
79
  </script>
79
80
 
80
81
  {#if message.role === 'user'}
81
- <div class="agent-msg agent-msg--user">
82
- {#if message.contextLabel}<div class="agent-msg__context">
82
+ <div class="ml-auto flex max-w-[85%] flex-col items-end gap-1 px-4 py-2">
83
+ {#if message.contextLabel}<div class="text-xs text-muted-foreground">
83
84
  Context: {message.contextLabel}
84
85
  </div>{/if}
85
- <div class="agent-msg__bubble">{message.text}</div>
86
+ <div
87
+ class="whitespace-pre-wrap rounded-2xl rounded-br-md bg-primary px-3.5 py-2 text-primary-foreground [overflow-wrap:anywhere]"
88
+ >
89
+ {message.text}
90
+ </div>
86
91
  {#if message.attachments.length}
87
- <ul class="agent-msg__attachments">
88
- {#each message.attachments as file (file.fileId)}<li>
92
+ <ul class="flex flex-wrap justify-end gap-2 text-xs text-muted-foreground">
93
+ {#each message.attachments as file (file.fileId)}<li class="inline-flex items-center gap-1">
89
94
  <Icon name="file" size={12} />
90
95
  {file.name}
91
96
  </li>{/each}
@@ -93,15 +98,12 @@
93
98
  {/if}
94
99
  </div>
95
100
  {:else}
96
- <div class="agent-msg agent-msg--assistant" data-status={message.status}>
101
+ <div class="min-w-0 px-4 py-2" data-status={message.status}>
97
102
  <ToolTimeline tools={message.tools} delegations={message.delegations} {streaming} />
98
- <!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
99
- <div class="agent-markdown" onclick={handleClick}>
100
- {@html html}
101
- {#if streaming && !message.text}<span class="agent-msg__thinking">Thinking…</span>{/if}
102
- </div>
103
+ {#if html}<Markdown {html} onclick={handleClick} />{/if}
104
+ {#if streaming && !message.text}<span class="italic text-muted-foreground">Thinking…</span>{/if}
103
105
  {#if message.artifacts.length}
104
- <div class="agent-msg__chips">
106
+ <div class="my-2 flex flex-wrap gap-1.5">
105
107
  {#each message.artifacts as a (a.artifactId)}
106
108
  <Button variant="outline" size="sm" onclick={() => onopenartifact?.(a.artifactId)}>
107
109
  <Icon name="file-text" size={14} />
@@ -114,18 +116,21 @@
114
116
  <UiFrame {resource} {sandboxOrigin} {onprompt} {ontool} {onnotify} />
115
117
  {/each}
116
118
  {#if message.files.length}
117
- <ul class="agent-msg__files">
119
+ <ul class="mt-1 flex flex-wrap gap-2 text-sm">
118
120
  {#each message.files as file (file.fileId)}
119
121
  <li>
120
122
  {#if file.mimeType.startsWith('image/')}
121
123
  <img
122
- class="agent-msg__image"
124
+ class="mb-1 block max-h-96 max-w-full rounded-lg"
123
125
  src={fileUrl(file.fileId)}
124
126
  alt={file.name}
125
127
  loading="lazy"
126
128
  />
127
129
  {/if}
128
- <a href={fileUrl(file.fileId)} download={file.name}
130
+ <a
131
+ class="inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline"
132
+ href={fileUrl(file.fileId)}
133
+ download={file.name}
129
134
  ><Icon name="download" size={14} /> {file.name} ({formatBytes(file.bytes)})</a
130
135
  >
131
136
  </li>
@@ -133,14 +138,17 @@
133
138
  </ul>
134
139
  {/if}
135
140
  {#if message.proposals.length}
136
- <div class="agent-msg__note">
141
+ <div class="mt-1 text-xs text-muted-foreground">
137
142
  Proposed {message.proposals.length} document edit{message.proposals.length === 1
138
143
  ? ''
139
144
  : 's'}; apply them from the Excel or Word add-in.
140
145
  </div>
141
146
  {/if}
142
147
  {#if message.error}
143
- <div class="agent-msg__error" role="alert">
148
+ <div
149
+ class="mt-2 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive"
150
+ role="alert"
151
+ >
144
152
  <strong>{errorTitle(message.error.code)}.</strong>
145
153
  {message.error.message}
146
154
  {#if message.error.retryable && onretry}<Button variant="link" size="sm" onclick={onretry}
@@ -148,106 +156,9 @@
148
156
  >{/if}
149
157
  </div>
150
158
  {:else if message.status === 'cancelled'}
151
- <div class="agent-msg__note">Stopped.</div>
159
+ <div class="mt-1 text-xs text-muted-foreground">Stopped.</div>
152
160
  {:else if message.status === 'incomplete'}
153
- <div class="agent-msg__note">The answer was cut short.</div>
161
+ <div class="mt-1 text-xs text-muted-foreground">The answer was cut short.</div>
154
162
  {/if}
155
163
  </div>
156
164
  {/if}
157
-
158
- <style>
159
- .agent-msg {
160
- padding: 0.5rem 1rem;
161
- max-width: 48rem;
162
- }
163
- .agent-msg--user {
164
- margin-left: auto;
165
- display: flex;
166
- flex-direction: column;
167
- align-items: flex-end;
168
- }
169
- .agent-msg__bubble {
170
- background: hsl(var(--primary));
171
- color: hsl(var(--primary-foreground));
172
- border-radius: 1rem 1rem 0.25rem 1rem;
173
- padding: 0.5rem 0.875rem;
174
- white-space: pre-wrap;
175
- overflow-wrap: anywhere;
176
- }
177
- .agent-msg__context,
178
- .agent-msg__attachments,
179
- .agent-msg__note {
180
- font-size: 0.75rem;
181
- color: hsl(var(--muted-foreground));
182
- }
183
- .agent-msg__attachments,
184
- .agent-msg__files {
185
- list-style: none;
186
- padding: 0;
187
- margin: 0.25rem 0 0;
188
- display: flex;
189
- flex-wrap: wrap;
190
- gap: 0.5rem;
191
- }
192
- .agent-msg__chips {
193
- display: flex;
194
- flex-wrap: wrap;
195
- gap: 0.375rem;
196
- margin: 0.5rem 0;
197
- }
198
- .agent-msg__image {
199
- display: block;
200
- max-width: 100%;
201
- max-height: 24rem;
202
- border-radius: 0.5rem;
203
- margin-bottom: 0.25rem;
204
- }
205
- .agent-msg__thinking {
206
- color: hsl(var(--muted-foreground));
207
- font-style: italic;
208
- }
209
- .agent-msg__error {
210
- margin-top: 0.5rem;
211
- padding: 0.5rem 0.75rem;
212
- border-radius: 0.5rem;
213
- background: hsl(var(--destructive) / 0.1);
214
- color: hsl(var(--destructive));
215
- font-size: 0.875rem;
216
- }
217
- :global(.agent-markdown) {
218
- overflow-wrap: anywhere;
219
- line-height: 1.6;
220
- }
221
- :global(.agent-markdown pre) {
222
- background: hsl(var(--muted));
223
- padding: 0.75rem;
224
- border-radius: 0.5rem;
225
- overflow: auto;
226
- font-size: 0.8125rem;
227
- }
228
- :global(.agent-markdown code) {
229
- font-family: ui-monospace, monospace;
230
- font-size: 0.875em;
231
- }
232
- :global(.agent-markdown table) {
233
- border-collapse: collapse;
234
- margin: 0.5rem 0;
235
- font-size: 0.875rem;
236
- }
237
- :global(.agent-markdown th),
238
- :global(.agent-markdown td) {
239
- border: 1px solid hsl(var(--border));
240
- padding: 0.25rem 0.5rem;
241
- text-align: left;
242
- }
243
- :global(.agent-markdown blockquote) {
244
- border-left: 3px solid hsl(var(--border));
245
- margin: 0.5rem 0;
246
- padding-left: 0.75rem;
247
- color: hsl(var(--muted-foreground));
248
- }
249
- :global(.agent-markdown img) {
250
- max-width: 100%;
251
- border-radius: 0.5rem;
252
- }
253
- </style>
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AA6IH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AA2IH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}