@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.
- package/dist/components/ActionBar.svelte +1 -1
- package/dist/components/Chat.svelte +218 -90
- package/dist/components/Chat.svelte.d.ts +16 -3
- package/dist/components/ChatMessages.svelte +68 -15
- package/dist/config.js +6 -6
- package/dist/constants/colors.js +1 -1
- package/dist/services/chatService.js +169 -280
- package/dist/stores/chatStore.d.ts +1 -0
- package/dist/stores/chatStore.js +214 -113
- package/dist/types/attachment.js +1 -1
- package/dist/types/config.d.ts +5 -0
- package/dist/types/config.js +2 -1
- package/dist/utils/fileUtils.js +57 -108
- package/dist/utils/logger.js +7 -19
- package/package.json +1 -1
- package/dist/types/api-temp.d.ts +0 -61
- package/dist/types/api-temp.js +0 -42
|
@@ -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
|
|
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
|
|
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(
|
|
175
|
-
: await chatService.getMessages(
|
|
180
|
+
? await onLoadMessages(conversationId)
|
|
181
|
+
: await chatService.getMessages(conversationId);
|
|
176
182
|
const messageList = loadedMessages || [];
|
|
177
|
-
chatStore.actions.setMessages(
|
|
183
|
+
chatStore.actions.setMessages(conversationId, messageList);
|
|
178
184
|
|
|
179
185
|
if (messageList.length > 0) {
|
|
180
|
-
currentConversationId =
|
|
186
|
+
currentConversationId = conversationId;
|
|
181
187
|
markWidgetHeadAtLatestCommit();
|
|
182
|
-
chatLog('[Chat] Restored conversation with history:',
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
|
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
|
|
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 ||
|
|
858
|
-
if (
|
|
859
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1197
|
+
e.stopPropagation();
|
|
1090
1198
|
const handle = e.currentTarget as HTMLElement;
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
|
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
|
-
|
|
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?.
|
|
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-
|
|
1242
|
-
<span>{ui.errorMessage}</span>
|
|
1243
|
-
<
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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?: (
|
|
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
|
|
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>;
|