@widgetic/chat 0.1.4 → 0.1.6

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.
@@ -169,7 +169,7 @@
169
169
  class:completed={isCompleted}
170
170
  onclick={handleCompletionToggle}
171
171
  disabled={disabled || !conversationId}
172
- title={isCompleted ? 'Mark as Active' : 'Mark as Complete'}
172
+ title={isCompleted ? 'Mark as Active — you can keep iterating either way' : 'Mark this chat as done. You can still send messages and iterate on the widget.'}
173
173
  aria-label={isCompleted ? 'Mark conversation as active' : 'Mark conversation as complete'}
174
174
  >
175
175
  {#if isCompleted}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy } from 'svelte';
2
+ import { onMount, onDestroy, untrack } from 'svelte';
3
3
  import { chatStore, getCurrentMessages } from '../stores/chatStore.js';
4
4
  import { createChatService } from '../services/chatService.js';
5
5
  import { setLogsEnabled, chatLog, chatWarn } from '../utils/logger.js';
@@ -20,7 +20,7 @@
20
20
  isCompleted?: boolean;
21
21
  onStatsUpdate?: (messageCount: number, firstMessage: string | null) => void;
22
22
  onChatReady?: (chatMethods: ChatMethods) => void;
23
- onCompletionToggle?: () => void;
23
+ onCompletionToggle?: (detail?: { conversationId: string | null; completed: boolean }) => void;
24
24
  /** When provided, called instead of the built-in mock service for sending messages.
25
25
  * The parent handles the actual API call and can push assistant messages via addAssistantMessage. */
26
26
  onSendMessage?: (
@@ -42,7 +42,7 @@
42
42
  conversationId: string,
43
43
  content: string,
44
44
  messageType: string,
45
- attachments?: Array<{ url?: string; file_type?: string; file_name?: string | null; file_size?: number | null; display_order?: number }>,
45
+ attachments?: Array<{ url?: string; file?: File; file_type?: string; file_name?: string | null; file_size?: number | null; display_order?: number }>,
46
46
  ) => Promise<any>;
47
47
  onRestoreCheckpointBackend?: (conversationId: string, checkpointMessageId: string) => Promise<any>;
48
48
  onTakeScreenshot?: () => Promise<string | null>;
@@ -77,8 +77,8 @@
77
77
  * internal messages area grows/shrinks within the current chat height.
78
78
  */
79
79
  resizeExpandsContainer?: boolean;
80
- /** Delta in pixels for the current drag frame (positive = grew downward). */
81
- onMessagesResize?: (deltaPx: number) => void;
80
+ /** Delta in pixels, plus the resulting chat container height (for parent-owned resize). */
81
+ onMessagesResize?: (deltaPx: number, nextContainerHeightPx?: number) => void;
82
82
  }
83
83
 
84
84
  // Expose methods to parent component
@@ -110,6 +110,10 @@
110
110
  updateLatestCodegenStatusMessage: (content: string) => void;
111
111
  /** Reload messages from backend and refresh widget HEAD marker. */
112
112
  reloadConversationMessages: () => Promise<void>;
113
+ /** Select an existing conversation (loads its messages into the chat). */
114
+ selectConversation: (conversationId: string) => Promise<void>;
115
+ /** Replace attachments on the latest user message (CDN URLs after persist). */
116
+ updateLastUserMessageAttachments: (attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>) => void;
113
117
  }
114
118
 
115
119
  let { context, config = {}, class: className = '', style = '', isCompleted = false, onStatsUpdate, onChatReady, onCompletionToggle, onSendMessage, onLoadConversations, onLoadMessages, onCreateConversation, onSaveMessage, onRestoreCheckpointBackend, onTakeScreenshot, onConversationChange, onDraftChange, onRetryMessage, onUploadChatFile, showHeader = true, headerTitle = '', showDescriptionHeader = true, showLogs = true, resizeExpandsContainer = false, onMessagesResize }: Props = $props();
@@ -170,67 +174,115 @@
170
174
  /** Prefer a conversation that already has messages (avoids empty auto-created convos). */
171
175
  async function activateBestConversation(conversations: any[]): Promise<void> {
172
176
  for (const conversation of conversations) {
177
+ const conversationId = conversation.id || conversation.conversation_id || conversation.conversationId;
178
+ if (!conversationId) continue;
173
179
  const loadedMessages = onLoadMessages
174
- ? await onLoadMessages(conversation.id)
175
- : await chatService.getMessages(conversation.id);
180
+ ? await onLoadMessages(conversationId)
181
+ : await chatService.getMessages(conversationId);
176
182
  const messageList = loadedMessages || [];
177
- chatStore.actions.setMessages(conversation.id, messageList);
183
+ chatStore.actions.setMessages(conversationId, messageList);
178
184
 
179
185
  if (messageList.length > 0) {
180
- currentConversationId = conversation.id;
186
+ currentConversationId = conversationId;
181
187
  markWidgetHeadAtLatestCommit();
182
- chatLog('[Chat] Restored conversation with history:', conversation.id, messageList.length, 'messages');
188
+ chatLog('[Chat] Restored conversation with history:', conversationId, messageList.length, 'messages');
183
189
  return;
184
190
  }
185
191
  }
186
192
 
187
193
  const fallbackConversation = conversations[0];
188
- chatLog('[Chat] All conversations empty — using latest:', fallbackConversation.id);
189
- currentConversationId = fallbackConversation.id;
194
+ const fallbackId =
195
+ fallbackConversation?.id ||
196
+ fallbackConversation?.conversation_id ||
197
+ fallbackConversation?.conversationId;
198
+ chatLog('[Chat] All conversations empty — using latest:', fallbackId);
199
+ currentConversationId = fallbackId || null;
200
+ }
201
+
202
+ let lastLoadedContextId = $state('');
203
+
204
+ async function initializeForContext(contextId: string) {
205
+ if (!contextId) return;
206
+ lastLoadedContextId = contextId;
207
+ chatLog('[Chat] Initializing with context:', contextId);
208
+ chatStore.actions.setActiveContext(contextId, true);
209
+
210
+ let loaded: any[] | null = null;
211
+ for (let attempt = 0; attempt < 4; attempt++) {
212
+ loaded = await loadConversations();
213
+ if (loaded !== null) break;
214
+ if (attempt < 3) {
215
+ await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
216
+ }
217
+ }
218
+
219
+ if (loaded === null) {
220
+ chatStore.actions.updateUI({
221
+ errorMessage: 'Could not load chat history.',
222
+ });
223
+ return;
224
+ }
225
+
226
+ if (loaded.length === 0) {
227
+ chatLog('[Chat] No existing conversations, creating new one');
228
+ await createNewConversation();
229
+ } else {
230
+ chatLog('[Chat] Selecting from', loaded.length, 'conversation(s)');
231
+ await activateBestConversation(loaded);
232
+ }
233
+ chatLog('[Chat] Initialized successfully');
234
+ }
235
+
236
+ async function retryLoadHistory() {
237
+ chatLog('[Chat] Retry load history for', context?.id);
238
+ chatStore.actions.updateUI({ errorMessage: null });
239
+ lastLoadedContextId = '';
240
+ try {
241
+ await initializeForContext(context.id);
242
+ } catch (error) {
243
+ console.error('[Chat] Retry load history failed:', error);
244
+ chatStore.actions.updateUI({
245
+ errorMessage: 'Could not load chat history.',
246
+ });
247
+ }
190
248
  }
191
249
 
192
250
  // Lifecycle
193
251
  onMount(async () => {
194
252
  try {
195
- chatLog('[Chat] Initializing with context:', context.id);
196
-
197
- chatStore.actions.setActiveContext(context.id, true);
198
-
199
- let loaded: any[] | null = null;
200
- for (let attempt = 0; attempt < 4; attempt++) {
201
- loaded = await loadConversations();
202
- if (loaded !== null) break;
203
- if (attempt < 3) {
204
- await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
205
- }
206
- }
207
-
208
- if (loaded === null) {
209
- chatStore.actions.updateUI({
210
- errorMessage: 'Could not load chat history. Try refreshing the page.',
211
- });
212
- isInitialized = true;
213
- return;
214
- }
215
-
216
- if (loaded.length === 0) {
217
- chatLog('[Chat] No existing conversations, creating new one');
218
- await createNewConversation();
219
- } else {
220
- chatLog('[Chat] Selecting from', loaded.length, 'conversation(s)');
221
- await activateBestConversation(loaded);
222
- }
223
-
253
+ await initializeForContext(context.id);
224
254
  isInitialized = true;
225
- chatLog('[Chat] Initialized successfully');
226
255
  } catch (error) {
227
256
  console.error('[Chat] Failed to initialize:', error);
228
- chatStore.actions.updateUI({
229
- errorMessage: 'Failed to initialize chat. Please try again.'
257
+ chatStore.actions.updateUI({
258
+ errorMessage: 'Failed to initialize chat. Please try again.',
230
259
  });
260
+ isInitialized = true;
231
261
  }
232
262
  });
233
263
 
264
+ $effect(() => {
265
+ const nextId = context?.id;
266
+ if (!isInitialized || !nextId) return;
267
+ if (nextId === lastLoadedContextId) return;
268
+ untrack(() => {
269
+ const prevConv = currentConversationId;
270
+ const prevMsgs = prevConv ? [...(getCurrentMessages(prevConv) || [])] : [];
271
+ const prevUserWithImage = [...prevMsgs].reverse().find(
272
+ (m: any) => m.messageType === 'user' && Array.isArray(m.attachments) && m.attachments.length > 0,
273
+ );
274
+ void initializeForContext(nextId).then(() => {
275
+ if (!prevUserWithImage?.attachments?.length) return;
276
+ const nextMsgs = getCurrentMessages(currentConversationId) || [];
277
+ const nextUser = [...nextMsgs].reverse().find((m: any) => m.messageType === 'user');
278
+ if (!nextUser) return;
279
+ if (nextUser.attachments && nextUser.attachments.length > 0) return;
280
+ updateLastUserMessageAttachments(prevUserWithImage.attachments);
281
+ chatLog('[Chat] Restored image attachments after widget id migrate:', prevUserWithImage.attachments.length);
282
+ });
283
+ });
284
+ });
285
+
234
286
  // Methods
235
287
  async function loadConversations(): Promise<any[] | null> {
236
288
  try {
@@ -335,15 +387,28 @@
335
387
  // Persist user message to backend and update local ID with DB UUID
336
388
  if (onSaveMessage) {
337
389
  try {
390
+ const persistAttachments = (submittedAttachments || []).map((att: any, i: number) => ({
391
+ url: att.uploadedUrl || userMessage.attachments?.[i]?.url,
392
+ file: att.file,
393
+ file_type: att.file?.type || userMessage.attachments?.[i]?.file_type,
394
+ file_name: att.file?.name || userMessage.attachments?.[i]?.file_name,
395
+ file_size: att.file?.size || userMessage.attachments?.[i]?.file_size,
396
+ display_order: i + 1,
397
+ })).filter((att: { url?: string; file?: File }) => !!att.url || att.file instanceof File);
338
398
  const saved = await onSaveMessage(
339
399
  currentConversationId,
340
400
  submittedContent,
341
401
  'user',
342
- userMessage.attachments,
402
+ persistAttachments.length > 0 ? persistAttachments : userMessage.attachments,
343
403
  );
344
404
  if (saved?.id && saved.id !== userMessage.id) {
345
405
  chatStore.actions.updateMessageId(currentConversationId, userMessage.id, saved.id);
346
406
  }
407
+ const savedAttachments = saved?.attachments || saved?.data?.attachments;
408
+ if (Array.isArray(savedAttachments) && savedAttachments.length > 0) {
409
+ const targetId = saved?.id || userMessage.id;
410
+ chatStore.actions.updateMessageAttachments(currentConversationId, targetId, savedAttachments);
411
+ }
347
412
  } catch (err) {
348
413
  chatWarn('[Chat] Failed to persist user message:', err);
349
414
  }
@@ -352,7 +417,7 @@
352
417
  if (onSendMessage) {
353
418
  const outgoing = (submittedAttachments || []).map((att) => ({
354
419
  file: att.file,
355
- url: att.uploadedUrl,
420
+ url: att.uploadedUrl || att.preview,
356
421
  uploadId: att.uploadId,
357
422
  fileType: att.file.type,
358
423
  fileName: att.file.name,
@@ -546,11 +611,7 @@
546
611
  }
547
612
 
548
613
  function isCdnUploadableFile(file: File): boolean {
549
- return (
550
- file.type.startsWith('image/') ||
551
- file.type.startsWith('video/') ||
552
- file.type === 'application/pdf'
553
- );
614
+ return !!file && file.size > 0;
554
615
  }
555
616
 
556
617
  function bumpAttachmentProgress(attachmentIndex: number, percentage: number) {
@@ -566,7 +627,7 @@
566
627
  options: { source?: string } = {},
567
628
  ): Promise<void> {
568
629
  if (!onUploadChatFile || !isCdnUploadableFile(file)) {
569
- if (file.type.startsWith('image/')) {
630
+ if (file.type.startsWith('image/') || file.type.startsWith('video/') || file.type === 'application/pdf') {
570
631
  entry.preview = URL.createObjectURL(file);
571
632
  }
572
633
  return;
@@ -580,7 +641,7 @@
580
641
  });
581
642
  entry.uploadedUrl = uploaded.url;
582
643
  entry.uploadId = uploaded.id;
583
- if (file.type.startsWith('image/')) {
644
+ if (file.type.startsWith('image/') || file.type.startsWith('video/') || file.type === 'application/pdf') {
584
645
  entry.preview = uploaded.url;
585
646
  }
586
647
  entry.uploadProgress = 100;
@@ -590,7 +651,7 @@
590
651
  chatLog('[Chat] Attachment uploaded to CDN:', uploaded.url.slice(0, 80), 'id:', uploaded.id);
591
652
  } catch (uploadError) {
592
653
  console.error('[Chat] Upload failed, using local preview where possible:', uploadError);
593
- if (file.type.startsWith('image/')) {
654
+ if (file.type.startsWith('image/') || file.type.startsWith('video/') || file.type === 'application/pdf') {
594
655
  entry.preview = URL.createObjectURL(file);
595
656
  }
596
657
  entry.uploadProgress = 0;
@@ -608,7 +669,7 @@
608
669
  const fileInput = document.createElement('input');
609
670
  fileInput.type = 'file';
610
671
  fileInput.multiple = true;
611
- fileInput.accept = 'image/*,video/*,.pdf,.txt,.md,.mermaid';
672
+ fileInput.accept = '*/*';
612
673
 
613
674
  fileInput.onchange = async (event) => {
614
675
  const files = Array.from((event.target as HTMLInputElement).files || []);
@@ -646,26 +707,30 @@
646
707
  }
647
708
 
648
709
  async function handleComplete({ detail }: { detail: { completed: boolean } }) {
649
- chatLog('Conversation completion toggle requested');
650
-
710
+ chatLog('Conversation completion toggle requested', {
711
+ conversationId: currentConversationId,
712
+ completed: detail?.completed,
713
+ });
714
+
651
715
  if (!currentConversationId) {
652
716
  console.error('No active conversation to mark as complete');
653
717
  return;
654
718
  }
655
-
719
+
656
720
  try {
657
- // Call parent's completion toggle function
658
721
  if (onCompletionToggle) {
659
- onCompletionToggle();
722
+ onCompletionToggle({
723
+ conversationId: currentConversationId,
724
+ completed: detail?.completed,
725
+ });
660
726
  chatLog('Completion toggled via parent callback');
661
727
  } else {
662
728
  chatWarn('No onCompletionToggle callback provided');
663
729
  }
664
-
665
730
  } catch (error) {
666
731
  console.error('Failed to update conversation status:', error);
667
- chatStore.actions.updateUI({
668
- errorMessage: 'Failed to update conversation status'
732
+ chatStore.actions.updateUI({
733
+ errorMessage: 'Failed to update conversation status'
669
734
  });
670
735
  }
671
736
  }
@@ -812,8 +877,9 @@
812
877
 
813
878
  function getAttachmentDataUrls(): string[] {
814
879
  return localAttachments
815
- .filter((att) => att.type === 'image' && (att.preview || att.file.type.startsWith('image/')))
816
- .map((att) => att.preview || '');
880
+ .filter((att) => att.type === 'image' && (att.uploadedUrl || att.preview || att.file?.type?.startsWith('image/')))
881
+ .map((att) => att.uploadedUrl || att.preview || '')
882
+ .filter((url) => !!url);
817
883
  }
818
884
 
819
885
  function getAttachmentUploadIds(): string[] {
@@ -851,12 +917,18 @@
851
917
  return /^Updated \d+ files?/i.test(trimmed) && /\n\nCommit\s+[a-f0-9]{7,40}/i.test(trimmed);
852
918
  }
853
919
 
854
- /** Widget HEAD applies only to codegen completion lines, not preview-rebuild status. */
920
+ /** Widget HEAD: codegen completion, saved-code summaries, or visible preview-recompiled bubbles. */
855
921
  function messageHasWidgetCommit(content?: string | null): boolean {
856
922
  const trimmed = (content || '').trim();
857
- if (!trimmed || isPreviewRecompiledMessage(trimmed)) return false;
858
- if (isCodegenFailureMessage(trimmed)) return false;
859
- if (/^Code generation completed/i.test(trimmed) && CODEGEN_COMMIT_REGEX.test(trimmed)) return true;
923
+ if (!trimmed || isCodegenFailureMessage(trimmed)) return false;
924
+ if (isPreviewRecompiledMessage(trimmed) && CODEGEN_COMMIT_REGEX.test(trimmed)) return true;
925
+ if (CODEGEN_COMMIT_REGEX.test(trimmed) && (
926
+ /^Code generation completed/i.test(trimmed) ||
927
+ /^Code saved/i.test(trimmed) ||
928
+ isBackendCodegenSummary(trimmed)
929
+ )) {
930
+ return true;
931
+ }
860
932
  return isBackendCodegenSummary(trimmed);
861
933
  }
862
934
 
@@ -896,7 +968,12 @@
896
968
 
897
969
  function isCodegenFailureMessage(content?: string | null): boolean {
898
970
  const t = (content || '').trim();
899
- return /^Code generation failed/i.test(t);
971
+ return (
972
+ /^Code generation failed/i.test(t) ||
973
+ /^⚠️/.test(t) ||
974
+ /Too many generation requests/i.test(t) ||
975
+ /temporarily unavailable/i.test(t)
976
+ );
900
977
  }
901
978
 
902
979
  function markWidgetHeadAtLatestCommit(preferredMessageId?: string) {
@@ -981,13 +1058,40 @@
981
1058
  }
982
1059
  }
983
1060
 
1061
+ function updateLastUserMessageAttachments(
1062
+ attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>,
1063
+ ) {
1064
+ if (!currentConversationId || !attachments?.length) return;
1065
+ const msgs = getCurrentMessages(currentConversationId);
1066
+ const idx = getLastUserMessageIndex(msgs);
1067
+ if (idx < 0) return;
1068
+ const mapped = attachments.map((a, i) => ({
1069
+ id: a.id || `att_${Date.now()}_${i}`,
1070
+ url: a.url,
1071
+ file_type: a.file_type || 'application/octet-stream',
1072
+ file_name: a.file_name || 'attachment',
1073
+ file_size: null,
1074
+ display_order: i + 1,
1075
+ created_at: new Date().toISOString(),
1076
+ }));
1077
+ chatStore.actions.updateMessageAttachments(currentConversationId, msgs[idx].id, mapped);
1078
+ chatLog('[Chat] Patched last user message attachments:', mapped.length);
1079
+ }
1080
+
984
1081
  /** Reload messages from backend and re-mark widget HEAD (picks up server-persisted summaries). */
985
1082
  async function reloadConversationMessages() {
986
1083
  if (!currentConversationId || !onLoadMessages) return;
987
1084
  try {
1085
+ const localMsgs = getCurrentMessages(currentConversationId);
1086
+ const localHasHead = localMsgs.some((m) => m.isWidgetHead || messageHasWidgetCommit(m.messageContent));
988
1087
  const loaded = await onLoadMessages(currentConversationId);
1088
+ const loadedHasHead = (loaded || []).some((m) => messageHasWidgetCommit(m.messageContent));
989
1089
  if (loaded?.length) {
990
- chatStore.actions.setMessages(currentConversationId, loaded);
1090
+ if (localHasHead && !loadedHasHead) {
1091
+ chatLog('[Chat] Keep local HEAD commit message — DB reload has no commit yet');
1092
+ } else {
1093
+ chatStore.actions.setMessages(currentConversationId, loaded);
1094
+ }
991
1095
  }
992
1096
  markWidgetHeadAtLatestCommit();
993
1097
  chatLog('[Chat] Reloaded conversation messages:', loaded?.length ?? 0);
@@ -1035,6 +1139,10 @@
1035
1139
  markWidgetHeadAtLatestCommit,
1036
1140
  updateLatestCodegenStatusMessage,
1037
1141
  reloadConversationMessages,
1142
+ updateLastUserMessageAttachments,
1143
+ selectConversation: async (conversationId: string) => {
1144
+ await loadConversation(conversationId);
1145
+ },
1038
1146
  };
1039
1147
  onChatReady(chatMethods);
1040
1148
  });
@@ -1086,29 +1194,38 @@
1086
1194
 
1087
1195
  function handleMessagesResizeStart(e: PointerEvent) {
1088
1196
  e.preventDefault();
1089
- isResizingMessages = true;
1197
+ e.stopPropagation();
1090
1198
  const handle = e.currentTarget as HTMLElement;
1091
- const messagesEl = handle.previousElementSibling as HTMLElement | null;
1092
- if (!messagesEl) return;
1199
+ try {
1200
+ handle.setPointerCapture?.(e.pointerId);
1201
+ } catch {
1202
+ /* pointer capture is best-effort */
1203
+ }
1204
+ isResizingMessages = true;
1205
+ const messagesEl =
1206
+ (handle.previousElementSibling as HTMLElement | null) ||
1207
+ (chatContainer?.querySelector('.prompt-chat-messages-area') as HTMLElement | null);
1093
1208
  const startY = e.clientY;
1094
- const startHeight = messagesEl.getBoundingClientRect().height;
1209
+ const startHeight = messagesEl?.getBoundingClientRect().height ?? MIN_MESSAGES_HEIGHT;
1095
1210
  let lastDy = 0;
1096
1211
 
1097
1212
  const onMove = (ev: PointerEvent) => {
1098
1213
  const dy = ev.clientY - startY;
1099
1214
  if (resizeExpandsContainer) {
1100
- // Report the incremental delta to the parent so it can enlarge the outer chat
1101
- // container. The messages area stays flex:1 within the new container height.
1102
1215
  const frameDelta = dy - lastDy;
1103
1216
  lastDy = dy;
1104
1217
  if (frameDelta !== 0) onMessagesResize?.(frameDelta);
1105
1218
  } else {
1106
- const next = Math.max(MIN_MESSAGES_HEIGHT, startHeight + dy);
1107
- messagesAreaHeight = next;
1219
+ messagesAreaHeight = Math.max(MIN_MESSAGES_HEIGHT, startHeight + dy);
1108
1220
  }
1109
1221
  };
1110
1222
  const onUp = () => {
1111
1223
  isResizingMessages = false;
1224
+ try {
1225
+ handle.releasePointerCapture?.(e.pointerId);
1226
+ } catch {
1227
+ /* already released */
1228
+ }
1112
1229
  window.removeEventListener('pointermove', onMove);
1113
1230
  window.removeEventListener('pointerup', onUp);
1114
1231
  };
@@ -1119,7 +1236,7 @@
1119
1236
 
1120
1237
  <div
1121
1238
  bind:this={chatContainer}
1122
- class="{className ? className : 'prompt-chat-ct'} relative flex flex-col overflow-hidden bg-white border border-gray-300 rounded-medium h-full"
1239
+ class="{className ? className : 'prompt-chat-ct'} relative flex flex-col overflow-hidden bg-white border border-gray-300 rounded-medium {resizeExpandsContainer ? 'shrink-0' : 'h-full'}"
1123
1240
  style={style}
1124
1241
  >
1125
1242
  {#if showHeader}
@@ -1155,7 +1272,7 @@
1155
1272
  <!-- Chat Interface -->
1156
1273
  {#if isInitialized}
1157
1274
  <div
1158
- class="prompt-chat-messages-area overflow-hidden"
1275
+ class="prompt-chat-messages-area flex min-h-0 flex-col overflow-hidden"
1159
1276
  style="{messagesAreaHeight !== null
1160
1277
  ? `flex: 0 0 ${messagesAreaHeight}px; height: ${messagesAreaHeight}px;`
1161
1278
  : 'flex: 1 1 auto; min-height: 0;'}"
@@ -1197,7 +1314,7 @@
1197
1314
  showScreenshot={config?.features?.screenshot !== false}
1198
1315
  showRecord={config?.features?.screenRecording !== false}
1199
1316
  showUpload={config?.features?.fileUpload !== false}
1200
- showComplete={config?.features?.restoreCheckpoint !== false}
1317
+ showComplete={config?.features?.markComplete !== false}
1201
1318
  on:screenshot={handleScreenshot}
1202
1319
  on:recordStart={handleRecordStart}
1203
1320
  on:recordStop={handleRecordStop}
@@ -1238,14 +1355,25 @@
1238
1355
 
1239
1356
  <!-- Error Display -->
1240
1357
  {#if ui?.errorMessage}
1241
- <div class="error-message-ct absolute top-0 left-0 right-0 bg-red-50 border-l-4 border-red-400 p-3 flex items-center justify-between text-sm text-red-700 rounded-none z-50">
1242
- <span>{ui.errorMessage}</span>
1243
- <button
1244
- class="ml-2 text-red-400 hover:text-red-600 font-semibold w-5 h-5 flex items-center justify-center"
1245
- onclick={() => chatStore.actions.updateUI({ errorMessage: null })}
1246
- >
1247
-
1248
- </button>
1358
+ <div class="error-message-ct absolute top-0 left-0 right-0 bg-red-50 border-l-4 border-red-400 p-3 flex items-center justify-between gap-2 text-sm text-red-700 rounded-none z-10">
1359
+ <span class="error-message-text min-w-0 flex-1">{ui.errorMessage}</span>
1360
+ <div class="error-message-actions flex items-center gap-2 shrink-0">
1361
+ {#if ui.errorMessage.startsWith('Could not load chat history') || ui.errorMessage.startsWith('Failed to load')}
1362
+ <button
1363
+ type="button"
1364
+ class="retry-load-history-btn px-2 py-1 text-xs font-semibold rounded border border-red-300 bg-white text-red-700 hover:bg-red-100"
1365
+ onclick={() => void retryLoadHistory()}
1366
+ >
1367
+ Retry load history
1368
+ </button>
1369
+ {/if}
1370
+ <button
1371
+ class="ml-0 text-red-400 hover:text-red-600 font-semibold w-5 h-5 flex items-center justify-center"
1372
+ onclick={() => chatStore.actions.updateUI({ errorMessage: null })}
1373
+ >
1374
+
1375
+ </button>
1376
+ </div>
1249
1377
  </div>
1250
1378
  {/if}
1251
1379
  </div>
@@ -33,6 +33,15 @@ interface ChatMethods {
33
33
  updateLatestCodegenStatusMessage: (content: string) => void;
34
34
  /** Reload messages from backend and refresh widget HEAD marker. */
35
35
  reloadConversationMessages: () => Promise<void>;
36
+ /** Select an existing conversation (loads its messages into the chat). */
37
+ selectConversation: (conversationId: string) => Promise<void>;
38
+ /** Replace attachments on the latest user message (CDN URLs after persist). */
39
+ updateLastUserMessageAttachments: (attachments: Array<{
40
+ id?: string;
41
+ url: string;
42
+ file_type?: string;
43
+ file_name?: string | null;
44
+ }>) => void;
36
45
  }
37
46
  interface Props {
38
47
  context: ChatContext;
@@ -43,7 +52,10 @@ interface Props {
43
52
  isCompleted?: boolean;
44
53
  onStatsUpdate?: (messageCount: number, firstMessage: string | null) => void;
45
54
  onChatReady?: (chatMethods: ChatMethods) => void;
46
- onCompletionToggle?: () => void;
55
+ onCompletionToggle?: (detail?: {
56
+ conversationId: string | null;
57
+ completed: boolean;
58
+ }) => void;
47
59
  /** When provided, called instead of the built-in mock service for sending messages.
48
60
  * The parent handles the actual API call and can push assistant messages via addAssistantMessage. */
49
61
  onSendMessage?: (content: string, attachments?: Array<{
@@ -60,6 +72,7 @@ interface Props {
60
72
  onCreateConversation?: (contextId: string, title: string) => Promise<any>;
61
73
  onSaveMessage?: (conversationId: string, content: string, messageType: string, attachments?: Array<{
62
74
  url?: string;
75
+ file?: File;
63
76
  file_type?: string;
64
77
  file_name?: string | null;
65
78
  file_size?: number | null;
@@ -104,8 +117,8 @@ interface Props {
104
117
  * internal messages area grows/shrinks within the current chat height.
105
118
  */
106
119
  resizeExpandsContainer?: boolean;
107
- /** Delta in pixels for the current drag frame (positive = grew downward). */
108
- onMessagesResize?: (deltaPx: number) => void;
120
+ /** Delta in pixels, plus the resulting chat container height (for parent-owned resize). */
121
+ onMessagesResize?: (deltaPx: number, nextContainerHeightPx?: number) => void;
109
122
  }
110
123
  declare const Chat: import("svelte").Component<Props, {}, "">;
111
124
  type Chat = ReturnType<typeof Chat>;