@classic-homes/agent-chat 0.1.3 → 0.2.1

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 (70) hide show
  1. package/README.md +40 -0
  2. package/dist/body-lock.d.ts +2 -0
  3. package/dist/body-lock.d.ts.map +1 -0
  4. package/dist/body-lock.js +27 -0
  5. package/dist/components/AgentChat.svelte +85 -47
  6. package/dist/components/AgentChat.svelte.d.ts +8 -0
  7. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  8. package/dist/components/ArtifactPanel.svelte +79 -96
  9. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  10. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  11. package/dist/components/ChatHeader.svelte +122 -0
  12. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  13. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  14. package/dist/components/Composer.svelte +78 -121
  15. package/dist/components/Composer.svelte.d.ts.map +1 -1
  16. package/dist/components/ConversationDialogs.svelte +154 -0
  17. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  18. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  19. package/dist/components/ConversationSidebar.svelte +135 -114
  20. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  21. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  22. package/dist/components/Markdown.svelte +49 -0
  23. package/dist/components/Markdown.svelte.d.ts +17 -0
  24. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  25. package/dist/components/MessageItem.svelte +27 -116
  26. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  27. package/dist/components/MessageList.svelte +23 -34
  28. package/dist/components/ModeBanner.svelte +7 -13
  29. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  30. package/dist/components/SandboxFrame.svelte +43 -0
  31. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  32. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  33. package/dist/components/ToolTimeline.svelte +17 -68
  34. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  35. package/dist/components/UiFrame.svelte +9 -55
  36. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  37. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  38. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  39. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  40. package/dist/components/live-fixes.svelte.test.d.ts +2 -0
  41. package/dist/components/live-fixes.svelte.test.d.ts.map +1 -0
  42. package/dist/components/live-fixes.svelte.test.js +127 -0
  43. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  44. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  45. package/dist/components/markdown-prose.svelte.test.js +26 -0
  46. package/dist/composer-keys.d.ts +11 -0
  47. package/dist/composer-keys.d.ts.map +1 -0
  48. package/dist/composer-keys.js +10 -0
  49. package/dist/composer-keys.test.d.ts +2 -0
  50. package/dist/composer-keys.test.d.ts.map +1 -0
  51. package/dist/composer-keys.test.js +16 -0
  52. package/dist/index.d.ts +3 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/reducer.d.ts +10 -0
  56. package/dist/reducer.d.ts.map +1 -1
  57. package/dist/reducer.js +19 -1
  58. package/dist/reducer.test.js +29 -1
  59. package/dist/session.svelte.d.ts +15 -0
  60. package/dist/session.svelte.d.ts.map +1 -1
  61. package/dist/session.svelte.js +21 -1
  62. package/dist/ui-frame-protocol.d.ts +2 -2
  63. package/dist/ui-frame-protocol.js +2 -2
  64. package/dist/ui-frame-relay.d.ts +16 -0
  65. package/dist/ui-frame-relay.d.ts.map +1 -0
  66. package/dist/ui-frame-relay.js +22 -0
  67. package/dist/ui-frame-relay.test.d.ts +2 -0
  68. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  69. package/dist/ui-frame-relay.test.js +36 -0
  70. package/package.json +8 -5
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title?: string;
4
+ conversationId?: string | null;
5
+ /** Agent name shown as a badge, e.g. "Classic Agent". */
6
+ agentLabel?: string;
7
+ /** Opens the conversation list; the button only shows below `lg`. */
8
+ onopenrail?: () => void;
9
+ onnew?: () => void;
10
+ /** Opens this conversation as a full page (panel variant). */
11
+ onexpand?: () => void;
12
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
13
+ ondelete?: (conversationId: string) => void | Promise<void>;
14
+ onerror?: (message: string) => void;
15
+ /** Extra controls before the menu. */
16
+ actions?: Snippet;
17
+ class?: string;
18
+ }
19
+ /**
20
+ * Header for a chat: conversation title, agent badge and the conversation menu (rename,
21
+ * open full page, delete). Pass it to `AgentChat` through the `header` snippet. Actions only
22
+ * show when their handler is set; rename and delete also need a `conversationId`.
23
+ */
24
+ declare const ChatHeader: import("svelte").Component<Props, {}, "">;
25
+ type ChatHeader = ReturnType<typeof ChatHeader>;
26
+ export default ChatHeader;
27
+ //# sourceMappingURL=ChatHeader.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatHeader.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatHeader.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAIpC,UAAU,KAAK;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,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,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8EH;;;;GAIG;AACH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
- import { Button, Icon } from '@classic-homes/theme-svelte';
2
+ import { Button, Icon, Textarea } from '@classic-homes/theme-svelte';
3
3
  import type { FileRef } from '@classic-homes/agent-contracts';
4
4
  import { LIMITS } from '@classic-homes/agent-contracts';
5
+ import { composerKeyAction } from '../composer-keys.js';
5
6
 
6
7
  interface Props {
7
8
  disabled?: boolean;
@@ -30,7 +31,7 @@
30
31
  context,
31
32
  }: Props = $props();
32
33
 
33
- let textarea: HTMLTextAreaElement | undefined = $state();
34
+ let textarea = $state<HTMLTextAreaElement | null>(null);
34
35
  let fileInput: HTMLInputElement | undefined = $state();
35
36
  let uploading = $state(0);
36
37
  let uploadError = $state<string | undefined>();
@@ -43,20 +44,18 @@
43
44
  const text = value.trim();
44
45
  if (!text || disabled || streaming || uploading > 0) return;
45
46
  value = '';
46
- if (textarea) textarea.style.height = 'auto';
47
47
  onsend?.(text);
48
48
  }
49
49
  function onkeydown(event: KeyboardEvent): void {
50
- if (event.key === 'Enter' && !event.shiftKey) {
50
+ const action = composerKeyAction(event, streaming);
51
+ if (action === 'send') {
51
52
  event.preventDefault();
52
53
  submit();
54
+ } else if (action === 'cancel') {
55
+ event.preventDefault();
56
+ oncancel?.();
53
57
  }
54
58
  }
55
- function autosize(): void {
56
- if (!textarea) return;
57
- textarea.style.height = 'auto';
58
- textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
59
- }
60
59
  async function onfiles(event: Event): Promise<void> {
61
60
  const input = event.currentTarget as HTMLInputElement;
62
61
  const chosen = [...(input.files ?? [])];
@@ -81,131 +80,89 @@
81
80
  </script>
82
81
 
83
82
  <form
84
- class="agent-composer"
83
+ class="bg-background px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-2"
85
84
  onsubmit={(e) => {
86
85
  e.preventDefault();
87
86
  submit();
88
87
  }}
89
88
  aria-label="Send a message"
90
89
  >
91
- {#if context}<div class="agent-composer__context">{@render context()}</div>{/if}
92
- {#if files.length || uploadError}
93
- <div class="agent-composer__files">
94
- {#each files as file (file.fileId)}
95
- <span class="agent-composer__file">
96
- <Icon name="file" size={12} />
97
- {file.name}
98
- <button
99
- type="button"
100
- aria-label={`Remove ${file.name}`}
101
- onclick={() => {
102
- files = files.filter((f) => f.fileId !== file.fileId);
103
- onremovefile?.(file.fileId);
104
- }}>×</button
90
+ <div
91
+ class="mx-auto w-full max-w-3xl rounded-xl border border-input bg-background shadow-sm transition-shadow focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/20"
92
+ >
93
+ {#if files.length || uploadError}
94
+ <div class="flex flex-wrap items-center gap-1.5 px-3 pt-3 text-xs">
95
+ {#each files as file (file.fileId)}
96
+ <span
97
+ class="inline-flex max-w-full items-center gap-1 rounded-full bg-muted py-0.5 pl-2 pr-0.5"
105
98
  >
106
- </span>
107
- {/each}
108
- {#if uploadError}<span class="agent-composer__error" role="alert">{uploadError}</span>{/if}
109
- </div>
110
- {/if}
111
- <div class="agent-composer__row">
112
- {#if upload}
113
- <input bind:this={fileInput} type="file" multiple hidden onchange={onfiles} />
114
- <Button
115
- type="button"
116
- variant="ghost"
117
- size="icon"
118
- aria-label="Attach files"
119
- disabled={disabled || uploading > 0}
120
- loading={uploading > 0}
121
- onclick={() => fileInput?.click()}
122
- >
123
- {#if uploading === 0}<Icon name="upload" size={18} />{/if}
124
- </Button>
99
+ <Icon name="file" size={12} class="shrink-0 text-muted-foreground" />
100
+ <span class="truncate">{file.name}</span>
101
+ <button
102
+ type="button"
103
+ class="inline-flex size-5 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
104
+ aria-label={`Remove ${file.name}`}
105
+ onclick={() => {
106
+ files = files.filter((f) => f.fileId !== file.fileId);
107
+ onremovefile?.(file.fileId);
108
+ }}><Icon name="close" size={12} /></button
109
+ >
110
+ </span>
111
+ {/each}
112
+ {#if uploadError}<span class="text-destructive" role="alert">{uploadError}</span>{/if}
113
+ </div>
125
114
  {/if}
126
- <textarea
127
- bind:this={textarea}
115
+ <Textarea
116
+ bind:ref={textarea}
128
117
  bind:value
129
- class="agent-composer__field"
130
- rows="1"
118
+ autosize
119
+ rows={1}
120
+ maxRows={8}
121
+ class="border-0 bg-transparent px-3 pb-1 pt-3 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
131
122
  {placeholder}
132
123
  aria-label="Message"
133
124
  maxlength={LIMITS.messageChars}
134
125
  {disabled}
135
126
  {onkeydown}
136
- oninput={autosize}></textarea>
137
- {#if streaming}
138
- <Button
139
- type="button"
140
- variant="outline"
141
- size="icon"
142
- aria-label="Stop"
143
- onclick={() => oncancel?.()}><Icon name="square" size={16} /></Button
144
- >
145
- {:else}
146
- <Button
147
- type="submit"
148
- size="icon"
149
- aria-label="Send"
150
- disabled={disabled || !value.trim() || uploading > 0}><Icon name="send" size={18} /></Button
151
- >
152
- {/if}
127
+ />
128
+ <div class="flex items-center gap-1 px-2 pb-2">
129
+ {#if upload}
130
+ <input bind:this={fileInput} type="file" multiple hidden onchange={onfiles} />
131
+ <Button
132
+ type="button"
133
+ variant="ghost"
134
+ size="icon"
135
+ class="size-8 text-muted-foreground"
136
+ aria-label="Attach files"
137
+ disabled={disabled || uploading > 0}
138
+ loading={uploading > 0}
139
+ onclick={() => fileInput?.click()}
140
+ >
141
+ {#if uploading === 0}<Icon name="paperclip" size={18} />{/if}
142
+ </Button>
143
+ {/if}
144
+ {#if context}<div class="min-w-0">{@render context()}</div>{/if}
145
+ <div class="ml-auto">
146
+ {#if streaming}
147
+ <Button
148
+ type="button"
149
+ variant="outline"
150
+ size="icon"
151
+ class="size-8"
152
+ aria-label="Stop"
153
+ onclick={() => oncancel?.()}><Icon name="stop" size={14} /></Button
154
+ >
155
+ {:else}
156
+ <Button
157
+ type="submit"
158
+ size="icon"
159
+ class="size-8"
160
+ aria-label="Send"
161
+ disabled={disabled || !value.trim() || uploading > 0}
162
+ ><Icon name="send" size={16} /></Button
163
+ >
164
+ {/if}
165
+ </div>
166
+ </div>
153
167
  </div>
154
168
  </form>
155
-
156
- <style>
157
- .agent-composer {
158
- padding: 0.75rem 1rem max(0.75rem, env(safe-area-inset-bottom));
159
- border-top: 1px solid hsl(var(--border));
160
- background: hsl(var(--background));
161
- display: grid;
162
- gap: 0.5rem;
163
- }
164
- .agent-composer__row {
165
- display: flex;
166
- align-items: flex-end;
167
- gap: 0.5rem;
168
- }
169
- .agent-composer__field {
170
- flex: 1;
171
- resize: none;
172
- min-height: 2.75rem;
173
- max-height: 200px;
174
- padding: 0.625rem 0.875rem;
175
- border: 1px solid hsl(var(--border));
176
- border-radius: 1rem;
177
- background: hsl(var(--background));
178
- color: hsl(var(--foreground));
179
- font: inherit;
180
- font-size: 16px;
181
- line-height: 1.5;
182
- }
183
- .agent-composer__field:focus {
184
- outline: none;
185
- border-color: hsl(var(--primary));
186
- box-shadow: 0 0 0 3px hsl(var(--primary) / 0.15);
187
- }
188
- .agent-composer__files {
189
- display: flex;
190
- flex-wrap: wrap;
191
- gap: 0.375rem;
192
- font-size: 0.75rem;
193
- }
194
- .agent-composer__file {
195
- display: inline-flex;
196
- align-items: center;
197
- gap: 0.25rem;
198
- padding: 0.125rem 0.5rem;
199
- border-radius: 9999px;
200
- background: hsl(var(--muted));
201
- }
202
- .agent-composer__file button {
203
- border: 0;
204
- background: none;
205
- cursor: pointer;
206
- font: inherit;
207
- }
208
- .agent-composer__error {
209
- color: hsl(var(--destructive));
210
- }
211
- </style>
@@ -1 +1 @@
1
- {"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAI5D,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CACpC;AA6GH,QAAA,MAAM,QAAQ;iBArFO,IAAI;qBAqF6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Composer.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/Composer.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAK5D,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CACpC;AA8GH,QAAA,MAAM,QAAQ;iBArFO,IAAI;qBAqF6B,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,154 @@
1
+ <!--
2
+ @component
3
+ Rename and delete dialogs for one conversation, shared by `ConversationSidebar` and
4
+ `ChatHeader`. Set `renaming` or `deleting` to a conversation to open the matching dialog.
5
+ A rejected `onrename` keeps the dialog open with the message; a rejected `ondelete` is
6
+ reported through `onerror` (the confirm dialog has already closed). Both dialogs usually open
7
+ from a menu item that is gone by the time they close, so focus returns to `returnFocus` (the
8
+ menu trigger) when it is still on the page.
9
+ -->
10
+ <script lang="ts" module>
11
+ export interface ConversationRef {
12
+ conversationId: string;
13
+ title: string;
14
+ }
15
+ </script>
16
+
17
+ <script lang="ts">
18
+ import { AlertDialog, Button, Dialog, Input } from '@classic-homes/theme-svelte';
19
+ import { tick } from 'svelte';
20
+ import { releaseStuckBodyLock } from '../body-lock.js';
21
+
22
+ interface Props {
23
+ renaming?: ConversationRef | null;
24
+ deleting?: ConversationRef | null;
25
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
26
+ ondelete?: (conversationId: string) => void | Promise<void>;
27
+ onerror?: (message: string) => void;
28
+ /** Element to focus when a dialog closes, e.g. the menu trigger that opened it. */
29
+ returnFocus?: HTMLElement | null;
30
+ }
31
+ let {
32
+ renaming = $bindable(null),
33
+ deleting = $bindable(null),
34
+ onrename,
35
+ ondelete,
36
+ onerror,
37
+ returnFocus = null,
38
+ }: Props = $props();
39
+
40
+ const uid = $props.id();
41
+ const formId = `${uid}-rename`;
42
+ const errorId = `${uid}-rename-error`;
43
+ const MAX_TITLE = 200;
44
+ let draft = $state('');
45
+ let saving = $state(false);
46
+ let renameError = $state<string | undefined>();
47
+
48
+ $effect(() => {
49
+ if (renaming) {
50
+ draft = renaming.title;
51
+ renameError = undefined;
52
+ }
53
+ });
54
+
55
+ function message(err: unknown, fallback: string): string {
56
+ return err instanceof Error && err.message ? err.message : fallback;
57
+ }
58
+
59
+ async function saveRename(event: SubmitEvent): Promise<void> {
60
+ event.preventDefault();
61
+ const target = renaming;
62
+ const title = draft.trim();
63
+ if (!target || !title || saving) return;
64
+ if (title === target.title) {
65
+ renaming = null;
66
+ return;
67
+ }
68
+ saving = true;
69
+ renameError = undefined;
70
+ try {
71
+ await onrename?.(target.conversationId, title);
72
+ renaming = null;
73
+ } catch (err) {
74
+ renameError = message(err, 'Could not rename the conversation.');
75
+ } finally {
76
+ saving = false;
77
+ }
78
+ }
79
+
80
+ let renameForm = $state<HTMLFormElement | null>(null);
81
+ /** Start in the title field with the text selected, not on the dialog's close button. */
82
+ function focusTitle(event: Event): void {
83
+ const field = renameForm?.querySelector('input');
84
+ if (!field) return;
85
+ event.preventDefault();
86
+ field.focus();
87
+ // The title is written into the field by an effect after this runs; select once it's there.
88
+ void tick().then(() => field.select());
89
+ }
90
+
91
+ function restoreFocus(event: Event): void {
92
+ if (!returnFocus?.isConnected) return;
93
+ event.preventDefault();
94
+ returnFocus.focus();
95
+ }
96
+
97
+ async function confirmDelete(): Promise<void> {
98
+ const target = deleting;
99
+ deleting = null;
100
+ if (!target) return;
101
+ try {
102
+ await ondelete?.(target.conversationId);
103
+ } catch (err) {
104
+ onerror?.(message(err, 'Could not delete the conversation.'));
105
+ }
106
+ }
107
+ </script>
108
+
109
+ <Dialog
110
+ open={renaming !== null}
111
+ onOpenChange={(open) => {
112
+ if (!open && !saving) renaming = null;
113
+ }}
114
+ title="Rename conversation"
115
+ size="sm"
116
+ onOpenAutoFocus={focusTitle}
117
+ onCloseAutoFocus={(event) => {
118
+ restoreFocus(event);
119
+ releaseStuckBodyLock();
120
+ }}
121
+ >
122
+ <form bind:this={renameForm} id={formId} class="grid gap-2" onsubmit={saveRename}>
123
+ <Input
124
+ bind:value={draft}
125
+ aria-label="Conversation title"
126
+ maxlength={MAX_TITLE}
127
+ aria-invalid={renameError ? true : undefined}
128
+ aria-describedby={renameError ? errorId : undefined}
129
+ />
130
+ {#if renameError}
131
+ <p id={errorId} class="text-sm text-destructive" role="alert">{renameError}</p>
132
+ {/if}
133
+ </form>
134
+ {#snippet footer()}
135
+ <Button variant="outline" disabled={saving} onclick={() => (renaming = null)}>Cancel</Button>
136
+ <Button type="submit" form={formId} loading={saving} disabled={!draft.trim()}>Save</Button>
137
+ {/snippet}
138
+ </Dialog>
139
+
140
+ <AlertDialog
141
+ open={deleting !== null}
142
+ onOpenChange={(open) => {
143
+ if (!open) deleting = null;
144
+ }}
145
+ title="Delete this conversation?"
146
+ description={deleting ? `“${deleting.title}” will be deleted. This cannot be undone.` : undefined}
147
+ confirmText="Delete"
148
+ confirmVariant="destructive"
149
+ onConfirm={() => void confirmDelete()}
150
+ onCloseAutoFocus={(event) => {
151
+ restoreFocus(event);
152
+ releaseStuckBodyLock();
153
+ }}
154
+ />
@@ -0,0 +1,25 @@
1
+ export interface ConversationRef {
2
+ conversationId: string;
3
+ title: string;
4
+ }
5
+ interface Props {
6
+ renaming?: ConversationRef | null;
7
+ deleting?: ConversationRef | null;
8
+ onrename?: (conversationId: string, title: string) => void | Promise<void>;
9
+ ondelete?: (conversationId: string) => void | Promise<void>;
10
+ onerror?: (message: string) => void;
11
+ /** Element to focus when a dialog closes, e.g. the menu trigger that opened it. */
12
+ returnFocus?: HTMLElement | null;
13
+ }
14
+ /**
15
+ * Rename and delete dialogs for one conversation, shared by `ConversationSidebar` and
16
+ * `ChatHeader`. Set `renaming` or `deleting` to a conversation to open the matching dialog.
17
+ * A rejected `onrename` keeps the dialog open with the message; a rejected `ondelete` is
18
+ * reported through `onerror` (the confirm dialog has already closed). Both dialogs usually open
19
+ * from a menu item that is gone by the time they close, so focus returns to `returnFocus` (the
20
+ * menu trigger) when it is still on the page.
21
+ */
22
+ declare const ConversationDialogs: import("svelte").Component<Props, {}, "renaming" | "deleting">;
23
+ type ConversationDialogs = ReturnType<typeof ConversationDialogs>;
24
+ export default ConversationDialogs;
25
+ //# sourceMappingURL=ConversationDialogs.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationDialogs.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ConversationDialogs.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf;AAQD,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,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,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC;AAkHH;;;;;;;GAOG;AACH,QAAA,MAAM,mBAAmB,gEAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}