@widgetic/chat 0.1.4 → 0.1.5
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/Chat.svelte +205 -81
- package/dist/components/Chat.svelte.d.ts +12 -2
- 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
|
@@ -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';
|
|
@@ -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 || []);
|
|
@@ -812,8 +873,9 @@
|
|
|
812
873
|
|
|
813
874
|
function getAttachmentDataUrls(): string[] {
|
|
814
875
|
return localAttachments
|
|
815
|
-
.filter((att) => att.type === 'image' && (att.preview || att.file
|
|
816
|
-
.map((att) => att.preview || '')
|
|
876
|
+
.filter((att) => att.type === 'image' && (att.uploadedUrl || att.preview || att.file?.type?.startsWith('image/')))
|
|
877
|
+
.map((att) => att.uploadedUrl || att.preview || '')
|
|
878
|
+
.filter((url) => !!url);
|
|
817
879
|
}
|
|
818
880
|
|
|
819
881
|
function getAttachmentUploadIds(): string[] {
|
|
@@ -851,12 +913,18 @@
|
|
|
851
913
|
return /^Updated \d+ files?/i.test(trimmed) && /\n\nCommit\s+[a-f0-9]{7,40}/i.test(trimmed);
|
|
852
914
|
}
|
|
853
915
|
|
|
854
|
-
/** Widget HEAD
|
|
916
|
+
/** Widget HEAD: codegen completion, saved-code summaries, or visible preview-recompiled bubbles. */
|
|
855
917
|
function messageHasWidgetCommit(content?: string | null): boolean {
|
|
856
918
|
const trimmed = (content || '').trim();
|
|
857
|
-
if (!trimmed ||
|
|
858
|
-
if (
|
|
859
|
-
if (
|
|
919
|
+
if (!trimmed || isCodegenFailureMessage(trimmed)) return false;
|
|
920
|
+
if (isPreviewRecompiledMessage(trimmed) && CODEGEN_COMMIT_REGEX.test(trimmed)) return true;
|
|
921
|
+
if (CODEGEN_COMMIT_REGEX.test(trimmed) && (
|
|
922
|
+
/^Code generation completed/i.test(trimmed) ||
|
|
923
|
+
/^Code saved/i.test(trimmed) ||
|
|
924
|
+
isBackendCodegenSummary(trimmed)
|
|
925
|
+
)) {
|
|
926
|
+
return true;
|
|
927
|
+
}
|
|
860
928
|
return isBackendCodegenSummary(trimmed);
|
|
861
929
|
}
|
|
862
930
|
|
|
@@ -896,7 +964,12 @@
|
|
|
896
964
|
|
|
897
965
|
function isCodegenFailureMessage(content?: string | null): boolean {
|
|
898
966
|
const t = (content || '').trim();
|
|
899
|
-
return
|
|
967
|
+
return (
|
|
968
|
+
/^Code generation failed/i.test(t) ||
|
|
969
|
+
/^⚠️/.test(t) ||
|
|
970
|
+
/Too many generation requests/i.test(t) ||
|
|
971
|
+
/temporarily unavailable/i.test(t)
|
|
972
|
+
);
|
|
900
973
|
}
|
|
901
974
|
|
|
902
975
|
function markWidgetHeadAtLatestCommit(preferredMessageId?: string) {
|
|
@@ -981,13 +1054,40 @@
|
|
|
981
1054
|
}
|
|
982
1055
|
}
|
|
983
1056
|
|
|
1057
|
+
function updateLastUserMessageAttachments(
|
|
1058
|
+
attachments: Array<{ id?: string; url: string; file_type?: string; file_name?: string | null }>,
|
|
1059
|
+
) {
|
|
1060
|
+
if (!currentConversationId || !attachments?.length) return;
|
|
1061
|
+
const msgs = getCurrentMessages(currentConversationId);
|
|
1062
|
+
const idx = getLastUserMessageIndex(msgs);
|
|
1063
|
+
if (idx < 0) return;
|
|
1064
|
+
const mapped = attachments.map((a, i) => ({
|
|
1065
|
+
id: a.id || `att_${Date.now()}_${i}`,
|
|
1066
|
+
url: a.url,
|
|
1067
|
+
file_type: a.file_type || 'application/octet-stream',
|
|
1068
|
+
file_name: a.file_name || 'attachment',
|
|
1069
|
+
file_size: null,
|
|
1070
|
+
display_order: i + 1,
|
|
1071
|
+
created_at: new Date().toISOString(),
|
|
1072
|
+
}));
|
|
1073
|
+
chatStore.actions.updateMessageAttachments(currentConversationId, msgs[idx].id, mapped);
|
|
1074
|
+
chatLog('[Chat] Patched last user message attachments:', mapped.length);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
984
1077
|
/** Reload messages from backend and re-mark widget HEAD (picks up server-persisted summaries). */
|
|
985
1078
|
async function reloadConversationMessages() {
|
|
986
1079
|
if (!currentConversationId || !onLoadMessages) return;
|
|
987
1080
|
try {
|
|
1081
|
+
const localMsgs = getCurrentMessages(currentConversationId);
|
|
1082
|
+
const localHasHead = localMsgs.some((m) => m.isWidgetHead || messageHasWidgetCommit(m.messageContent));
|
|
988
1083
|
const loaded = await onLoadMessages(currentConversationId);
|
|
1084
|
+
const loadedHasHead = (loaded || []).some((m) => messageHasWidgetCommit(m.messageContent));
|
|
989
1085
|
if (loaded?.length) {
|
|
990
|
-
|
|
1086
|
+
if (localHasHead && !loadedHasHead) {
|
|
1087
|
+
chatLog('[Chat] Keep local HEAD commit message — DB reload has no commit yet');
|
|
1088
|
+
} else {
|
|
1089
|
+
chatStore.actions.setMessages(currentConversationId, loaded);
|
|
1090
|
+
}
|
|
991
1091
|
}
|
|
992
1092
|
markWidgetHeadAtLatestCommit();
|
|
993
1093
|
chatLog('[Chat] Reloaded conversation messages:', loaded?.length ?? 0);
|
|
@@ -1035,6 +1135,10 @@
|
|
|
1035
1135
|
markWidgetHeadAtLatestCommit,
|
|
1036
1136
|
updateLatestCodegenStatusMessage,
|
|
1037
1137
|
reloadConversationMessages,
|
|
1138
|
+
updateLastUserMessageAttachments,
|
|
1139
|
+
selectConversation: async (conversationId: string) => {
|
|
1140
|
+
await loadConversation(conversationId);
|
|
1141
|
+
},
|
|
1038
1142
|
};
|
|
1039
1143
|
onChatReady(chatMethods);
|
|
1040
1144
|
});
|
|
@@ -1086,29 +1190,38 @@
|
|
|
1086
1190
|
|
|
1087
1191
|
function handleMessagesResizeStart(e: PointerEvent) {
|
|
1088
1192
|
e.preventDefault();
|
|
1089
|
-
|
|
1193
|
+
e.stopPropagation();
|
|
1090
1194
|
const handle = e.currentTarget as HTMLElement;
|
|
1091
|
-
|
|
1092
|
-
|
|
1195
|
+
try {
|
|
1196
|
+
handle.setPointerCapture?.(e.pointerId);
|
|
1197
|
+
} catch {
|
|
1198
|
+
/* pointer capture is best-effort */
|
|
1199
|
+
}
|
|
1200
|
+
isResizingMessages = true;
|
|
1201
|
+
const messagesEl =
|
|
1202
|
+
(handle.previousElementSibling as HTMLElement | null) ||
|
|
1203
|
+
(chatContainer?.querySelector('.prompt-chat-messages-area') as HTMLElement | null);
|
|
1093
1204
|
const startY = e.clientY;
|
|
1094
|
-
const startHeight = messagesEl
|
|
1205
|
+
const startHeight = messagesEl?.getBoundingClientRect().height ?? MIN_MESSAGES_HEIGHT;
|
|
1095
1206
|
let lastDy = 0;
|
|
1096
1207
|
|
|
1097
1208
|
const onMove = (ev: PointerEvent) => {
|
|
1098
1209
|
const dy = ev.clientY - startY;
|
|
1099
1210
|
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
1211
|
const frameDelta = dy - lastDy;
|
|
1103
1212
|
lastDy = dy;
|
|
1104
1213
|
if (frameDelta !== 0) onMessagesResize?.(frameDelta);
|
|
1105
1214
|
} else {
|
|
1106
|
-
|
|
1107
|
-
messagesAreaHeight = next;
|
|
1215
|
+
messagesAreaHeight = Math.max(MIN_MESSAGES_HEIGHT, startHeight + dy);
|
|
1108
1216
|
}
|
|
1109
1217
|
};
|
|
1110
1218
|
const onUp = () => {
|
|
1111
1219
|
isResizingMessages = false;
|
|
1220
|
+
try {
|
|
1221
|
+
handle.releasePointerCapture?.(e.pointerId);
|
|
1222
|
+
} catch {
|
|
1223
|
+
/* already released */
|
|
1224
|
+
}
|
|
1112
1225
|
window.removeEventListener('pointermove', onMove);
|
|
1113
1226
|
window.removeEventListener('pointerup', onUp);
|
|
1114
1227
|
};
|
|
@@ -1119,7 +1232,7 @@
|
|
|
1119
1232
|
|
|
1120
1233
|
<div
|
|
1121
1234
|
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"
|
|
1235
|
+
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
1236
|
style={style}
|
|
1124
1237
|
>
|
|
1125
1238
|
{#if showHeader}
|
|
@@ -1155,7 +1268,7 @@
|
|
|
1155
1268
|
<!-- Chat Interface -->
|
|
1156
1269
|
{#if isInitialized}
|
|
1157
1270
|
<div
|
|
1158
|
-
class="prompt-chat-messages-area overflow-hidden"
|
|
1271
|
+
class="prompt-chat-messages-area flex min-h-0 flex-col overflow-hidden"
|
|
1159
1272
|
style="{messagesAreaHeight !== null
|
|
1160
1273
|
? `flex: 0 0 ${messagesAreaHeight}px; height: ${messagesAreaHeight}px;`
|
|
1161
1274
|
: 'flex: 1 1 auto; min-height: 0;'}"
|
|
@@ -1197,7 +1310,7 @@
|
|
|
1197
1310
|
showScreenshot={config?.features?.screenshot !== false}
|
|
1198
1311
|
showRecord={config?.features?.screenRecording !== false}
|
|
1199
1312
|
showUpload={config?.features?.fileUpload !== false}
|
|
1200
|
-
showComplete={config?.features?.
|
|
1313
|
+
showComplete={config?.features?.markComplete === true}
|
|
1201
1314
|
on:screenshot={handleScreenshot}
|
|
1202
1315
|
on:recordStart={handleRecordStart}
|
|
1203
1316
|
on:recordStop={handleRecordStop}
|
|
@@ -1238,14 +1351,25 @@
|
|
|
1238
1351
|
|
|
1239
1352
|
<!-- Error Display -->
|
|
1240
1353
|
{#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
|
-
|
|
1354
|
+
<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">
|
|
1355
|
+
<span class="error-message-text min-w-0 flex-1">{ui.errorMessage}</span>
|
|
1356
|
+
<div class="error-message-actions flex items-center gap-2 shrink-0">
|
|
1357
|
+
{#if ui.errorMessage.startsWith('Could not load chat history') || ui.errorMessage.startsWith('Failed to load')}
|
|
1358
|
+
<button
|
|
1359
|
+
type="button"
|
|
1360
|
+
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"
|
|
1361
|
+
onclick={() => void retryLoadHistory()}
|
|
1362
|
+
>
|
|
1363
|
+
Retry load history
|
|
1364
|
+
</button>
|
|
1365
|
+
{/if}
|
|
1366
|
+
<button
|
|
1367
|
+
class="ml-0 text-red-400 hover:text-red-600 font-semibold w-5 h-5 flex items-center justify-center"
|
|
1368
|
+
onclick={() => chatStore.actions.updateUI({ errorMessage: null })}
|
|
1369
|
+
>
|
|
1370
|
+
✕
|
|
1371
|
+
</button>
|
|
1372
|
+
</div>
|
|
1249
1373
|
</div>
|
|
1250
1374
|
{/if}
|
|
1251
1375
|
</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;
|
|
@@ -60,6 +69,7 @@ interface Props {
|
|
|
60
69
|
onCreateConversation?: (contextId: string, title: string) => Promise<any>;
|
|
61
70
|
onSaveMessage?: (conversationId: string, content: string, messageType: string, attachments?: Array<{
|
|
62
71
|
url?: string;
|
|
72
|
+
file?: File;
|
|
63
73
|
file_type?: string;
|
|
64
74
|
file_name?: string | null;
|
|
65
75
|
file_size?: number | null;
|
|
@@ -104,8 +114,8 @@ interface Props {
|
|
|
104
114
|
* internal messages area grows/shrinks within the current chat height.
|
|
105
115
|
*/
|
|
106
116
|
resizeExpandsContainer?: boolean;
|
|
107
|
-
/** Delta in pixels
|
|
108
|
-
onMessagesResize?: (deltaPx: number) => void;
|
|
117
|
+
/** Delta in pixels, plus the resulting chat container height (for parent-owned resize). */
|
|
118
|
+
onMessagesResize?: (deltaPx: number, nextContainerHeightPx?: number) => void;
|
|
109
119
|
}
|
|
110
120
|
declare const Chat: import("svelte").Component<Props, {}, "">;
|
|
111
121
|
type Chat = ReturnType<typeof Chat>;
|
|
@@ -76,11 +76,14 @@
|
|
|
76
76
|
return /^Updated \d+ files?/i.test(trimmed) && /\n\nCommit\s+[a-f0-9]{7,40}/i.test(trimmed);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Codegen completion
|
|
79
|
+
/** Codegen completion or preview-recompiled bubble that carries a commit hash. */
|
|
80
80
|
function messageHasCommit(message: ChatMessage): boolean {
|
|
81
|
-
if (!message.messageContent
|
|
81
|
+
if (!message.messageContent) return false;
|
|
82
82
|
const trimmed = message.messageContent.trim();
|
|
83
83
|
if (isCodegenFailure(message)) return false;
|
|
84
|
+
if (isPreviewRecompiledMessage(message) && /commit:?\s+[a-f0-9]{7,40}/i.test(trimmed)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
84
87
|
if (/^Code generation completed/i.test(trimmed) && /Commit:?\s+[a-f0-9]{7,40}/i.test(trimmed)) {
|
|
85
88
|
return true;
|
|
86
89
|
}
|
|
@@ -152,9 +155,15 @@
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
function isCodegenFailure(message: ChatMessage): boolean {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
if (message.messageType !== 'assistant' || !message.messageContent) return false;
|
|
159
|
+
const trimmed = message.messageContent.trim();
|
|
160
|
+
return (
|
|
161
|
+
/^Code generation failed/i.test(trimmed) ||
|
|
162
|
+
/^⚠️/.test(trimmed) ||
|
|
163
|
+
/Too many generation requests/i.test(trimmed) ||
|
|
164
|
+
/temporarily unavailable/i.test(trimmed) ||
|
|
165
|
+
/Generation Failed/i.test(trimmed)
|
|
166
|
+
);
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
function canRestoreToMessage(message: ChatMessage): boolean {
|
|
@@ -197,6 +206,10 @@
|
|
|
197
206
|
* Previously this was gated to Debug OFF (assistant reply hidden), but we want the buttons
|
|
198
207
|
* available in Debug ON too — the assistant summary and the user-side chrome coexist. */
|
|
199
208
|
function showCommitMetaOnUserTurn(turn: ChatTurn): boolean {
|
|
209
|
+
const visibleHead = turn.replies.some(
|
|
210
|
+
(r) => r.isWidgetHead && shouldShowAssistantReply(r) && messageHasCommit(r),
|
|
211
|
+
);
|
|
212
|
+
if (visibleHead) return false;
|
|
200
213
|
return !!getTurnCommitReply(turn);
|
|
201
214
|
}
|
|
202
215
|
|
|
@@ -225,8 +238,18 @@
|
|
|
225
238
|
chatTurns = rebalancePreviewRecompiledAcrossTurns(turns);
|
|
226
239
|
});
|
|
227
240
|
|
|
228
|
-
function
|
|
229
|
-
return /imaginea ata[sș]at[ăa]|attached (reference )?image|reference image|din imagine/i.test(
|
|
241
|
+
function messageImpliesAttachedFile(content: string): boolean {
|
|
242
|
+
return /imaginea ata[sș]at[ăa]|attached (reference )?(image|document|file|pdf|video)|reference image|din imagine|ata[sș]at[ăa]?\s+(document|pdf|fi[sș]ier|video)|attached document/i.test(
|
|
243
|
+
content,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function turnNeedsRetry(turn: ChatTurn): boolean {
|
|
248
|
+
if (turnHasCodegenFailure(turn)) return true;
|
|
249
|
+
const userMsg = turn.user;
|
|
250
|
+
if (!userMsg?.messageContent) return false;
|
|
251
|
+
const noAttachments = !userMsg.attachments || userMsg.attachments.length === 0;
|
|
252
|
+
return noAttachments && messageImpliesAttachedFile(userMsg.messageContent) && !getTurnCommitReply(turn);
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
let visibleMessages = $derived(
|
|
@@ -296,8 +319,23 @@
|
|
|
296
319
|
}
|
|
297
320
|
}
|
|
298
321
|
|
|
299
|
-
function
|
|
300
|
-
const
|
|
322
|
+
function getRetryPromptForTurn(turn: ChatTurn, turnIndex: number): string | null {
|
|
323
|
+
const own = turn.user?.messageContent?.trim();
|
|
324
|
+
if (own) return own;
|
|
325
|
+
for (let i = turnIndex - 1; i >= 0; i--) {
|
|
326
|
+
const previous = chatTurns[i]?.user?.messageContent?.trim();
|
|
327
|
+
if (previous) return previous;
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function isLastCodegenFailureInTurn(turn: ChatTurn, reply: ChatMessage): boolean {
|
|
333
|
+
const failures = turn.replies.filter(isCodegenFailure);
|
|
334
|
+
return failures.length > 0 && failures[failures.length - 1]?.id === reply.id;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function handleRetryTurn(turn: ChatTurn, turnIndex: number) {
|
|
338
|
+
const retryContent = getRetryPromptForTurn(turn, turnIndex);
|
|
301
339
|
if (!retryContent) return;
|
|
302
340
|
chatLog('Retry requested for failed codegen turn:', retryContent.slice(0, 80));
|
|
303
341
|
onRetryMessage?.(retryContent);
|
|
@@ -613,18 +651,33 @@
|
|
|
613
651
|
<div class="message-attachments-container">
|
|
614
652
|
<AttachmentDisplay attachments={userMsg.attachments} variant="message" showPreview={true} />
|
|
615
653
|
</div>
|
|
616
|
-
{:else if userMsg.messageContent &&
|
|
654
|
+
{:else if userMsg.messageContent && messageImpliesAttachedFile(userMsg.messageContent)}
|
|
617
655
|
<div class="wc-missing-attachment-placeholder message-attachments-container flex flex-col items-center gap-1 px-3 pb-3">
|
|
618
|
-
<div class="wc-missing-attachment-thumb flex h-20 w-28 flex-col items-center justify-center gap-1 rounded-md border border-dashed border-gray-300 bg-gray-100 text-gray-400" title="
|
|
656
|
+
<div class="wc-missing-attachment-thumb flex h-20 w-28 flex-col items-center justify-center gap-1 rounded-md border border-dashed border-gray-300 bg-gray-100 text-gray-400" title="Attachment not available" aria-label="Missing file attachment">
|
|
619
657
|
<svg class="h-8 w-8 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
620
658
|
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
621
659
|
<path d="M4 4l16 16" stroke-linecap="round" />
|
|
622
660
|
</svg>
|
|
623
661
|
</div>
|
|
624
|
-
<p class="wc-missing-attachment-caption max-w-[12rem] text-center text-xs text-gray-500">
|
|
662
|
+
<p class="wc-missing-attachment-caption max-w-[12rem] text-center text-xs text-gray-500">Attachment not saved. Re-attach with Upload, then Retry.</p>
|
|
625
663
|
</div>
|
|
626
664
|
{/if}
|
|
627
665
|
</div>
|
|
666
|
+
{#if turnNeedsRetry(turn) && !isLoading}
|
|
667
|
+
<div class="retry-codegen-row flex justify-end mt-1">
|
|
668
|
+
<button
|
|
669
|
+
class="retry-codegen-btn"
|
|
670
|
+
onclick={() => handleRetryTurn(turn, turnIndex)}
|
|
671
|
+
title="Retry this prompt (same as Retry on the error below)"
|
|
672
|
+
>
|
|
673
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
674
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36"/>
|
|
675
|
+
<path d="M21 3v6h-6"/>
|
|
676
|
+
</svg>
|
|
677
|
+
Retry
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
{/if}
|
|
628
681
|
</div>
|
|
629
682
|
|
|
630
683
|
{@const commitReply = getTurnCommitReply(turn)}
|
|
@@ -709,12 +762,12 @@
|
|
|
709
762
|
</div>
|
|
710
763
|
{/if}
|
|
711
764
|
|
|
712
|
-
{#if isCodegenFailure(reply) && turn
|
|
765
|
+
{#if isCodegenFailure(reply) && isLastCodegenFailureInTurn(turn, reply) && getRetryPromptForTurn(turn, turnIndex) && !isLoading}
|
|
713
766
|
<div class="retry-codegen-row flex justify-start mt-1">
|
|
714
767
|
<button
|
|
715
768
|
class="retry-codegen-btn"
|
|
716
|
-
onclick={() => handleRetryTurn(turn)}
|
|
717
|
-
title="Retry
|
|
769
|
+
onclick={() => handleRetryTurn(turn, turnIndex)}
|
|
770
|
+
title="Retry the last user prompt (same generation request)"
|
|
718
771
|
>
|
|
719
772
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
720
773
|
<path d="M21 12a9 9 0 1 1-2.64-6.36"/>
|