@widgetic/chat 0.1.4
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/README.md +440 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/widgeticAdapter.d.ts +185 -0
- package/dist/adapters/widgeticAdapter.js +766 -0
- package/dist/components/ActionBar.svelte +342 -0
- package/dist/components/ActionBar.svelte.d.ts +37 -0
- package/dist/components/AttachmentDisplay.svelte +547 -0
- package/dist/components/AttachmentDisplay.svelte.d.ts +12 -0
- package/dist/components/Chat.svelte +1253 -0
- package/dist/components/Chat.svelte.d.ts +112 -0
- package/dist/components/ChatHeader.svelte +182 -0
- package/dist/components/ChatHeader.svelte.d.ts +12 -0
- package/dist/components/ChatInput.svelte +290 -0
- package/dist/components/ChatInput.svelte.d.ts +15 -0
- package/dist/components/ChatMessages.svelte +996 -0
- package/dist/components/ChatMessages.svelte.d.ts +28 -0
- package/dist/components/CodeBlock.svelte +286 -0
- package/dist/components/CodeBlock.svelte.d.ts +10 -0
- package/dist/components/ContextPreview.svelte +66 -0
- package/dist/components/ContextPreview.svelte.d.ts +8 -0
- package/dist/components/LoadingIndicator.svelte +151 -0
- package/dist/components/LoadingIndicator.svelte.d.ts +9 -0
- package/dist/components/StatusIndicator.svelte +116 -0
- package/dist/components/StatusIndicator.svelte.d.ts +9 -0
- package/dist/components/SuggestionButtons.svelte +244 -0
- package/dist/components/SuggestionButtons.svelte.d.ts +26 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/components/index.js +12 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +67 -0
- package/dist/constants/colors.d.ts +20 -0
- package/dist/constants/colors.js +16 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +20 -0
- package/dist/services/chatService.d.ts +72 -0
- package/dist/services/chatService.js +355 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/stores/chatStore.d.ts +46 -0
- package/dist/stores/chatStore.js +219 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.js +2 -0
- package/dist/types/adapter.d.ts +86 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/api-temp.d.ts +61 -0
- package/dist/types/api-temp.js +42 -0
- package/dist/types/attachment.d.ts +84 -0
- package/dist/types/attachment.js +33 -0
- package/dist/types/chat.d.ts +114 -0
- package/dist/types/chat.js +1 -0
- package/dist/types/config.d.ts +89 -0
- package/dist/types/config.js +63 -0
- package/dist/types/context.d.ts +108 -0
- package/dist/types/context.js +11 -0
- package/dist/types/conversation.d.ts +45 -0
- package/dist/types/conversation.js +1 -0
- package/dist/types/events.d.ts +141 -0
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.js +10 -0
- package/dist/types/message.d.ts +82 -0
- package/dist/types/message.js +1 -0
- package/dist/types/state.d.ts +117 -0
- package/dist/types/state.js +1 -0
- package/dist/utils/fileUtils.d.ts +93 -0
- package/dist/utils/fileUtils.js +299 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +28 -0
- package/package.json +104 -0
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChatMessage, ChatContext } from '../types/index.js';
|
|
3
|
+
import { MESSAGE_COLORS } from '../constants/colors.js';
|
|
4
|
+
import { chatLog } from '../utils/logger.js';
|
|
5
|
+
import { AttachmentDisplay, SuggestionButtons } from './index.js';
|
|
6
|
+
import CodeBlock from './CodeBlock.svelte';
|
|
7
|
+
import LoadingIndicator from './LoadingIndicator.svelte';
|
|
8
|
+
import { onMount, tick } from 'svelte';
|
|
9
|
+
|
|
10
|
+
// Props
|
|
11
|
+
interface Props {
|
|
12
|
+
className?: className;
|
|
13
|
+
messages: ChatMessage[];
|
|
14
|
+
isLoading?: boolean;
|
|
15
|
+
context: ChatContext;
|
|
16
|
+
isCompleted?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* When true, heuristic code pattern detection (`detectCodePatterns`) runs
|
|
19
|
+
* for messages without explicit ``` fences. Default false — keeps chat UI
|
|
20
|
+
* clean when assistants return summaries that mention file paths or
|
|
21
|
+
* extensions (e.g. "Updated 4 files: ...svelte") which would otherwise be
|
|
22
|
+
* misclassified as JavaScript code blocks. Enable only for debugging.
|
|
23
|
+
*/
|
|
24
|
+
enableAutoCodeDetection?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* When false, backend-generated "Updated N files: ..." summaries are
|
|
27
|
+
* hidden from the UI (they still exist in the DB as LLM context). Default
|
|
28
|
+
* false — noisy for end users. Typically bound to `debugMode` by hosts.
|
|
29
|
+
*/
|
|
30
|
+
showGenerationSummaries?: boolean;
|
|
31
|
+
onSuggestionSelected?: (suggestion: string) => void;
|
|
32
|
+
onRestoreCheckpoint?: (messageId: string, messageIndex: number) => void;
|
|
33
|
+
onRetryMessage?: (content: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
class: className = '',
|
|
38
|
+
messages,
|
|
39
|
+
isLoading = false,
|
|
40
|
+
context,
|
|
41
|
+
isCompleted = false,
|
|
42
|
+
enableAutoCodeDetection = false,
|
|
43
|
+
showGenerationSummaries = false,
|
|
44
|
+
onSuggestionSelected,
|
|
45
|
+
onRestoreCheckpoint,
|
|
46
|
+
onRetryMessage,
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
// Matches "Updated N file(s)[...]: ..." assistant summaries emitted by the
|
|
50
|
+
// widget codegen backend. Anchored to first line only — regular assistant
|
|
51
|
+
// chat replies that happen to contain this phrase mid-message are kept.
|
|
52
|
+
const GENERATION_SUMMARY_REGEX = /^Updated \d+ files?(?:\s*\([^)]*\))?:/;
|
|
53
|
+
|
|
54
|
+
function isGenerationSummary(message: ChatMessage): boolean {
|
|
55
|
+
if (message.messageType !== 'assistant' || !message.messageContent) return false;
|
|
56
|
+
const trimmed = message.messageContent.trim();
|
|
57
|
+
if (/^Code generation failed/i.test(trimmed)) return false;
|
|
58
|
+
if (/^Code generation completed/i.test(trimmed)) return false;
|
|
59
|
+
if (/^Preview recompiled/i.test(trimmed)) return false;
|
|
60
|
+
return GENERATION_SUMMARY_REGEX.test(trimmed);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isPreviewRecompiledMessage(message: ChatMessage): boolean {
|
|
64
|
+
return message.messageType === 'assistant'
|
|
65
|
+
&& !!message.messageContent
|
|
66
|
+
&& /^Preview recompiled/i.test(message.messageContent.trim());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function extractCommitFromContent(content: string): string | null {
|
|
70
|
+
const match = content.match(/(?:Commit:?\s+|\n\nCommit\s+|\(commit\s+)([a-f0-9]{7,40})/i);
|
|
71
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isBackendCodegenSummary(content: string): boolean {
|
|
75
|
+
const trimmed = content.trim();
|
|
76
|
+
return /^Updated \d+ files?/i.test(trimmed) && /\n\nCommit\s+[a-f0-9]{7,40}/i.test(trimmed);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Codegen completion with commit — not preview-rebuild status lines. */
|
|
80
|
+
function messageHasCommit(message: ChatMessage): boolean {
|
|
81
|
+
if (!message.messageContent || isPreviewRecompiledMessage(message)) return false;
|
|
82
|
+
const trimmed = message.messageContent.trim();
|
|
83
|
+
if (isCodegenFailure(message)) return false;
|
|
84
|
+
if (/^Code generation completed/i.test(trimmed) && /Commit:?\s+[a-f0-9]{7,40}/i.test(trimmed)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return isBackendCodegenSummary(trimmed);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function turnHasCodegenFailure(turn: ChatTurn): boolean {
|
|
91
|
+
return turn.replies.some(isCodegenFailure);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function rebalancePreviewRecompiledAcrossTurns(turns: ChatTurn[]): ChatTurn[] {
|
|
95
|
+
const result = turns.map((t) => ({ user: t.user, replies: [...t.replies] }));
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < result.length; i++) {
|
|
98
|
+
const turn = result[i];
|
|
99
|
+
const turnCodegenCommits = new Set(
|
|
100
|
+
turn.replies
|
|
101
|
+
.filter((r) => messageHasCommit(r))
|
|
102
|
+
.map((r) => extractCommitFromContent(r.messageContent || '')!)
|
|
103
|
+
.filter(Boolean),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const kept: ChatMessage[] = [];
|
|
107
|
+
const moved: ChatMessage[] = [];
|
|
108
|
+
|
|
109
|
+
for (const reply of turn.replies) {
|
|
110
|
+
if (!isPreviewRecompiledMessage(reply)) {
|
|
111
|
+
kept.push(reply);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const previewCommit = extractCommitFromContent(reply.messageContent || '');
|
|
115
|
+
const belongsHere =
|
|
116
|
+
!turnHasCodegenFailure(turn)
|
|
117
|
+
&& (!previewCommit || turnCodegenCommits.size === 0 || turnCodegenCommits.has(previewCommit));
|
|
118
|
+
if (belongsHere) {
|
|
119
|
+
kept.push(reply);
|
|
120
|
+
} else {
|
|
121
|
+
moved.push(reply);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
turn.replies = kept;
|
|
126
|
+
|
|
127
|
+
for (const msg of moved) {
|
|
128
|
+
const commit = extractCommitFromContent(msg.messageContent || '');
|
|
129
|
+
let placed = false;
|
|
130
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
131
|
+
const prev = result[j];
|
|
132
|
+
const prevHasCommit = prev.replies.some(
|
|
133
|
+
(r) => commit && extractCommitFromContent(r.messageContent || '') === commit,
|
|
134
|
+
);
|
|
135
|
+
if (prevHasCommit || (j === i - 1 && !turnHasCodegenFailure(prev))) {
|
|
136
|
+
prev.replies.push(msg);
|
|
137
|
+
placed = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!placed && i > 0) {
|
|
142
|
+
result[i - 1].replies.push(msg);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function turnHasWidgetHead(turn: ChatTurn): boolean {
|
|
151
|
+
return turn.replies.some((r) => r.isWidgetHead && messageHasCommit(r));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isCodegenFailure(message: ChatMessage): boolean {
|
|
155
|
+
return message.messageType === 'assistant'
|
|
156
|
+
&& !!message.messageContent
|
|
157
|
+
&& /^Code generation failed/i.test(message.messageContent.trim());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function canRestoreToMessage(message: ChatMessage): boolean {
|
|
161
|
+
if (message.isWidgetHead) return false;
|
|
162
|
+
if (!message.messageContent) return false;
|
|
163
|
+
const trimmed = message.messageContent.trim();
|
|
164
|
+
if (/^Preview recompiled/i.test(trimmed)) return false;
|
|
165
|
+
return message.messageType === 'assistant'
|
|
166
|
+
&& messageHasCommit(message)
|
|
167
|
+
&& !isCodegenFailure(message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function canUndoHeadUserTurn(turn: ChatTurn): boolean {
|
|
171
|
+
return !!turn.user && !!getTurnCommitReply(turn)?.isWidgetHead;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractCommitShort(content: string): string | null {
|
|
175
|
+
const match = content.match(/(?:Commit:?\s+|\n\nCommit\s+)([a-f0-9]{7,40})/i);
|
|
176
|
+
return match?.[1]?.slice(0, 8) ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Assistant reply that carries the widget commit for this turn (if any). */
|
|
180
|
+
function getTurnCommitReply(turn: ChatTurn): ChatMessage | null {
|
|
181
|
+
for (const reply of turn.replies) {
|
|
182
|
+
if (messageHasCommit(reply) && !isCodegenFailure(reply)) return reply;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Whether this assistant bubble should render (vs commit chrome on user message). */
|
|
188
|
+
function shouldShowAssistantReply(reply: ChatMessage): boolean {
|
|
189
|
+
if (isCodegenFailure(reply)) return true;
|
|
190
|
+
if (showGenerationSummaries) return true;
|
|
191
|
+
if (/^Code generation completed/i.test((reply.messageContent || '').trim())) return true;
|
|
192
|
+
if (/^Preview recompiled/i.test((reply.messageContent || '').trim())) return true;
|
|
193
|
+
return !isGenerationSummary(reply);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Show commit chrome + Restore Checkpoint on the user prompt whenever the turn has a commit.
|
|
197
|
+
* Previously this was gated to Debug OFF (assistant reply hidden), but we want the buttons
|
|
198
|
+
* available in Debug ON too — the assistant summary and the user-side chrome coexist. */
|
|
199
|
+
function showCommitMetaOnUserTurn(turn: ChatTurn): boolean {
|
|
200
|
+
return !!getTurnCommitReply(turn);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
type ChatTurn = {
|
|
204
|
+
user: ChatMessage | null;
|
|
205
|
+
replies: ChatMessage[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let chatTurns = $state<ChatTurn[]>([]);
|
|
209
|
+
|
|
210
|
+
$effect(() => {
|
|
211
|
+
const turns: ChatTurn[] = [];
|
|
212
|
+
let current: ChatTurn | null = null;
|
|
213
|
+
|
|
214
|
+
for (const message of messages) {
|
|
215
|
+
if (message.messageType === 'user') {
|
|
216
|
+
if (current) turns.push(current);
|
|
217
|
+
current = { user: message, replies: [] };
|
|
218
|
+
} else if (current) {
|
|
219
|
+
current.replies.push(message);
|
|
220
|
+
} else {
|
|
221
|
+
turns.push({ user: null, replies: [message] });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (current) turns.push(current);
|
|
225
|
+
chatTurns = rebalancePreviewRecompiledAcrossTurns(turns);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
function messageImpliesAttachedImage(content: string): boolean {
|
|
229
|
+
return /imaginea ata[sș]at[ăa]|attached (reference )?image|reference image|din imagine/i.test(content);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let visibleMessages = $derived(
|
|
233
|
+
showGenerationSummaries
|
|
234
|
+
? messages
|
|
235
|
+
: messages.filter((m) => !isGenerationSummary(m))
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Local state
|
|
239
|
+
let messagesContainer: HTMLElement;
|
|
240
|
+
let showSuggestionsInline = $state(false);
|
|
241
|
+
let autoScroll = $state(true);
|
|
242
|
+
|
|
243
|
+
// Auto-scroll to bottom when new messages arrive or on initial load.
|
|
244
|
+
// Also scrolls on mount/conversation-switch when messages are already present.
|
|
245
|
+
$effect(() => {
|
|
246
|
+
if (messagesContainer && autoScroll && visibleMessages.length > 0) {
|
|
247
|
+
tick().then(() => {
|
|
248
|
+
messagesContainer?.scrollTo({
|
|
249
|
+
top: messagesContainer.scrollHeight,
|
|
250
|
+
behavior: 'smooth'
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Force scroll to bottom on initial mount (messages may already be loaded)
|
|
257
|
+
onMount(() => {
|
|
258
|
+
tick().then(() => {
|
|
259
|
+
if (messagesContainer && visibleMessages.length > 0) {
|
|
260
|
+
messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: 'instant' });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
function handleScroll() {
|
|
266
|
+
if (!messagesContainer) return;
|
|
267
|
+
const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
|
|
268
|
+
autoScroll = scrollTop + clientHeight >= scrollHeight - 10;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function formatTime(date: Date): string {
|
|
272
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
273
|
+
hour: '2-digit',
|
|
274
|
+
minute: '2-digit'
|
|
275
|
+
}).format(date);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function handleSuggestionSelected(event: CustomEvent) {
|
|
279
|
+
const suggestion = event.detail.suggestion;
|
|
280
|
+
chatLog('Suggestion selected in ChatMessages:', suggestion);
|
|
281
|
+
if (onSuggestionSelected) {
|
|
282
|
+
onSuggestionSelected(suggestion);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function handleRestoreCheckpoint(messageId: string) {
|
|
287
|
+
// Find the index of the message to restore to
|
|
288
|
+
const messageIndex = messages.findIndex(msg => msg.id === messageId);
|
|
289
|
+
if (messageIndex === -1) return;
|
|
290
|
+
|
|
291
|
+
chatLog('Restore checkpoint requested for message:', messageId, 'at index:', messageIndex);
|
|
292
|
+
|
|
293
|
+
// Call the prop callback if provided
|
|
294
|
+
if (onRestoreCheckpoint) {
|
|
295
|
+
onRestoreCheckpoint(messageId, messageIndex);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function handleRetryTurn(turn: ChatTurn) {
|
|
300
|
+
const retryContent = turn.user?.messageContent?.trim();
|
|
301
|
+
if (!retryContent) return;
|
|
302
|
+
chatLog('Retry requested for failed codegen turn:', retryContent.slice(0, 80));
|
|
303
|
+
onRetryMessage?.(retryContent);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Auto-detect programming language based on content
|
|
307
|
+
function detectLanguage(content: string): string {
|
|
308
|
+
const trimmed = content.trim().toLowerCase();
|
|
309
|
+
|
|
310
|
+
// HTML detection
|
|
311
|
+
if (/<(!doctype|html|head|body|div|span|p|h[1-6]|img|a|ul|ol|li|table|tr|td|th|form|input|button|script|style)/i.test(content)) {
|
|
312
|
+
return 'html';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// CSS detection
|
|
316
|
+
if (/[.#][\w-]+\s*\{[^}]*\}|@(media|import|font-face|keyframes)|[\w-]+\s*:\s*[^;]+;/.test(content)) {
|
|
317
|
+
return 'css';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// JSON detection
|
|
321
|
+
if (/^\s*[\{\[]/.test(trimmed) && /[\}\]]\s*$/.test(trimmed)) {
|
|
322
|
+
try {
|
|
323
|
+
JSON.parse(content);
|
|
324
|
+
return 'json';
|
|
325
|
+
} catch (e) {
|
|
326
|
+
// Not valid JSON, continue checking
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// SQL detection
|
|
331
|
+
if (/\b(select|insert|update|delete|create|drop|alter|from|where|join|group by|order by|having)\b/i.test(content)) {
|
|
332
|
+
return 'sql';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// JavaScript/TypeScript detection
|
|
336
|
+
if (/\b(function|const|let|var|class|import|export|if|else|for|while|return|async|await|=>|console\.log)\b/.test(content) ||
|
|
337
|
+
/\.(js|ts|jsx|tsx|vue|svelte)/.test(content) ||
|
|
338
|
+
/[\{\}]\s*$/.test(content) ||
|
|
339
|
+
/;\s*$/.test(content.split('\n').slice(-1)[0] || '')) {
|
|
340
|
+
return 'javascript';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Python detection
|
|
344
|
+
if (/\b(def|class|import|from|if __name__|print|return)\b/.test(content) ||
|
|
345
|
+
/\.py\b/.test(content) ||
|
|
346
|
+
/:\s*$/.test(content.split('\n')[0] || '')) {
|
|
347
|
+
return 'python';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// XML detection
|
|
351
|
+
if (/<\?xml|<\/[\w:]+>/.test(content)) {
|
|
352
|
+
return 'xml';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return 'plaintext';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Enhanced code detection patterns
|
|
359
|
+
function detectCodePatterns(content: string): { isCode: boolean; confidence: number; language: string } {
|
|
360
|
+
const lines = content.split('\n');
|
|
361
|
+
let codeScore = 0;
|
|
362
|
+
let totalChecks = 0;
|
|
363
|
+
|
|
364
|
+
// Check for code-like patterns
|
|
365
|
+
const patterns = [
|
|
366
|
+
// Function definitions
|
|
367
|
+
{ regex: /\b(function|const|let|var|def|class)\s+\w+/, weight: 3 },
|
|
368
|
+
// Brackets and braces
|
|
369
|
+
{ regex: /[{\[\(][^}\]\)]*[}\]\)]/, weight: 1 },
|
|
370
|
+
// Semicolons at end of lines
|
|
371
|
+
{ regex: /;\s*$/, weight: 2 },
|
|
372
|
+
// Assignment operators
|
|
373
|
+
{ regex: /\w+\s*[=:]\s*/, weight: 1 },
|
|
374
|
+
// Common keywords
|
|
375
|
+
{ regex: /\b(if|else|for|while|return|import|export|from|class|function)\b/, weight: 2 },
|
|
376
|
+
// HTML tags
|
|
377
|
+
{ regex: /<\/?[\w:]+[^>]*>/, weight: 3 },
|
|
378
|
+
// CSS properties
|
|
379
|
+
{ regex: /[\w-]+\s*:\s*[^;]+;/, weight: 3 },
|
|
380
|
+
// Console/print statements
|
|
381
|
+
{ regex: /\b(console\.log|print|echo)\s*\(/, weight: 2 },
|
|
382
|
+
// Method calls
|
|
383
|
+
{ regex: /\w+\.\w+\s*\(/, weight: 1 },
|
|
384
|
+
// Array/object literals
|
|
385
|
+
{ regex: /[\[\{].*[\]\}]/, weight: 1 },
|
|
386
|
+
// Comments
|
|
387
|
+
{ regex: /^\s*(\/\/|#|<!--|\/\*|\*)/, weight: 1 },
|
|
388
|
+
// Indentation patterns (2+ spaces or tabs)
|
|
389
|
+
{ regex: /^(\s{2,}|\t+)\S/, weight: 1 }
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
// Analyze each line
|
|
393
|
+
for (const line of lines) {
|
|
394
|
+
for (const pattern of patterns) {
|
|
395
|
+
totalChecks++;
|
|
396
|
+
if (pattern.regex.test(line)) {
|
|
397
|
+
codeScore += pattern.weight;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Additional checks for multi-line patterns
|
|
403
|
+
const fullContent = content.trim();
|
|
404
|
+
|
|
405
|
+
// File extension mentions
|
|
406
|
+
if (/\.(js|ts|jsx|tsx|py|html|css|json|sql|xml|vue|svelte|php|java|cpp|c|go|rs|rb)\b/i.test(fullContent)) {
|
|
407
|
+
codeScore += 3;
|
|
408
|
+
totalChecks += 3;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Code block structure (multiple lines with consistent indentation)
|
|
412
|
+
if (lines.length > 2) {
|
|
413
|
+
const indentedLines = lines.filter(line => /^\s{2,}/.test(line));
|
|
414
|
+
if (indentedLines.length > lines.length * 0.3) {
|
|
415
|
+
codeScore += 2;
|
|
416
|
+
totalChecks += 2;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// JSON-like structure
|
|
421
|
+
if (/^\s*[\{\[]/.test(fullContent) && /[\}\]]\s*$/.test(fullContent)) {
|
|
422
|
+
codeScore += 3;
|
|
423
|
+
totalChecks += 3;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const confidence = totalChecks > 0 ? (codeScore / totalChecks) : 0;
|
|
427
|
+
const language = detectLanguage(fullContent);
|
|
428
|
+
|
|
429
|
+
// Consider it code if confidence is high enough
|
|
430
|
+
const isCode = confidence > 0.3 || codeScore >= 3;
|
|
431
|
+
|
|
432
|
+
return { isCode, confidence, language };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Code block detection and parsing
|
|
436
|
+
function parseMessageContent(content: string) {
|
|
437
|
+
const parts = [];
|
|
438
|
+
|
|
439
|
+
// First, try to match explicit code blocks (``` syntax)
|
|
440
|
+
const completeCodeBlockRegex = /```(\w+)?\s*([\s\S]*?)```/g;
|
|
441
|
+
let lastIndex = 0;
|
|
442
|
+
let match;
|
|
443
|
+
let foundExplicitBlocks = false;
|
|
444
|
+
|
|
445
|
+
while ((match = completeCodeBlockRegex.exec(content)) !== null) {
|
|
446
|
+
foundExplicitBlocks = true;
|
|
447
|
+
|
|
448
|
+
// Add text before code block
|
|
449
|
+
if (match.index > lastIndex) {
|
|
450
|
+
const textContent = content.slice(lastIndex, match.index).trim();
|
|
451
|
+
if (textContent) {
|
|
452
|
+
parts.push({
|
|
453
|
+
type: 'text',
|
|
454
|
+
content: textContent
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Add explicit code block
|
|
460
|
+
parts.push({
|
|
461
|
+
type: 'code',
|
|
462
|
+
language: match[1] || 'plaintext',
|
|
463
|
+
content: match[2].trim()
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
lastIndex = match.index + match[0].length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// If no explicit code blocks found, check for incomplete ones
|
|
470
|
+
if (!foundExplicitBlocks) {
|
|
471
|
+
const incompleteCodeBlockMatch = content.match(/```(\w+)?\s*([\s\S]*)$/);
|
|
472
|
+
if (incompleteCodeBlockMatch) {
|
|
473
|
+
const beforeCodeBlock = content.slice(0, incompleteCodeBlockMatch.index).trim();
|
|
474
|
+
if (beforeCodeBlock) {
|
|
475
|
+
parts.push({
|
|
476
|
+
type: 'text',
|
|
477
|
+
content: beforeCodeBlock
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
parts.push({
|
|
482
|
+
type: 'code',
|
|
483
|
+
language: incompleteCodeBlockMatch[1] || 'plaintext',
|
|
484
|
+
content: incompleteCodeBlockMatch[2].trim()
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return parts;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Add remaining text after explicit code blocks
|
|
492
|
+
if (lastIndex < content.length) {
|
|
493
|
+
const remainingContent = content.slice(lastIndex).trim();
|
|
494
|
+
if (remainingContent) {
|
|
495
|
+
// If we found explicit blocks, just add remaining as text
|
|
496
|
+
if (foundExplicitBlocks) {
|
|
497
|
+
parts.push({
|
|
498
|
+
type: 'text',
|
|
499
|
+
content: remainingContent
|
|
500
|
+
});
|
|
501
|
+
} else if (enableAutoCodeDetection) {
|
|
502
|
+
// No explicit blocks found — fall back to auto-detection only when debug flag is on
|
|
503
|
+
const detection = detectCodePatterns(remainingContent);
|
|
504
|
+
if (detection.isCode) {
|
|
505
|
+
parts.push({
|
|
506
|
+
type: 'code',
|
|
507
|
+
language: detection.language,
|
|
508
|
+
content: remainingContent
|
|
509
|
+
});
|
|
510
|
+
} else {
|
|
511
|
+
parts.push({
|
|
512
|
+
type: 'text',
|
|
513
|
+
content: remainingContent
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
parts.push({
|
|
518
|
+
type: 'text',
|
|
519
|
+
content: remainingContent
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// If no explicit blocks found and no remaining content processed, check entire content
|
|
526
|
+
if (parts.length === 0) {
|
|
527
|
+
if (enableAutoCodeDetection) {
|
|
528
|
+
const detection = detectCodePatterns(content);
|
|
529
|
+
if (detection.isCode) {
|
|
530
|
+
parts.push({
|
|
531
|
+
type: 'code',
|
|
532
|
+
language: detection.language,
|
|
533
|
+
content: content
|
|
534
|
+
});
|
|
535
|
+
} else {
|
|
536
|
+
parts.push({
|
|
537
|
+
type: 'text',
|
|
538
|
+
content: content
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
parts.push({
|
|
543
|
+
type: 'text',
|
|
544
|
+
content: content
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return parts;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function hasCodeBlocks(content: string): boolean {
|
|
553
|
+
const hasExplicitCodeBlocks = /```[\s\S]*?```/.test(content);
|
|
554
|
+
const hasIncompleteCodeBlocks = /```\w*\s*[\s\S]*$/.test(content) && !/```[\s\S]*?```/.test(content);
|
|
555
|
+
|
|
556
|
+
if (hasExplicitCodeBlocks || hasIncompleteCodeBlocks) return true;
|
|
557
|
+
|
|
558
|
+
// Heuristic auto-detection is opt-in (debug only). Without it, plain prose
|
|
559
|
+
// that happens to mention file paths or extensions stays as text.
|
|
560
|
+
if (!enableAutoCodeDetection) return false;
|
|
561
|
+
|
|
562
|
+
return detectCodePatterns(content).isCode;
|
|
563
|
+
}
|
|
564
|
+
</script>
|
|
565
|
+
|
|
566
|
+
<div
|
|
567
|
+
bind:this={messagesContainer}
|
|
568
|
+
class="{className ? className : 'prompt-chat-messages-ct'} flex flex-col overflow-y-auto p-4 space-y-4"
|
|
569
|
+
class:loading={isLoading}
|
|
570
|
+
style="scroll-behavior: smooth;"
|
|
571
|
+
onscroll={handleScroll}
|
|
572
|
+
>
|
|
573
|
+
<!-- Empty state with suggestions -->
|
|
574
|
+
{#if messages.length === 0 && !isLoading}
|
|
575
|
+
<div class="empty-state-ct flex flex-col items-center justify-center text-center max-w-sm w-full" style="align-self: center;">
|
|
576
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">💬 Start a conversation</h3>
|
|
577
|
+
<p class="text-gray-600 dark:text-gray-400 text-sm mb-6">
|
|
578
|
+
Ask a question about your {context.type} to get started.
|
|
579
|
+
</p>
|
|
580
|
+
|
|
581
|
+
<!-- Contextual suggestions -->
|
|
582
|
+
<SuggestionButtons
|
|
583
|
+
{context}
|
|
584
|
+
visible={true}
|
|
585
|
+
on:suggestionSelected={handleSuggestionSelected}
|
|
586
|
+
/>
|
|
587
|
+
</div>
|
|
588
|
+
{/if}
|
|
589
|
+
|
|
590
|
+
<!-- Conversation turns (user prompt + assistant status grouped) -->
|
|
591
|
+
{#each chatTurns as turn, turnIndex (turn.user?.id ?? turn.replies[0]?.id ?? turnIndex)}
|
|
592
|
+
<div class="wc-chat-turn-ct space-y-1.5 rounded-lg border border-transparent p-0.5"
|
|
593
|
+
class:wc-chat-turn-head={turnHasWidgetHead(turn)}
|
|
594
|
+
>
|
|
595
|
+
{#if turn.user}
|
|
596
|
+
{@const userMsg = turn.user}
|
|
597
|
+
<div class="message-container wc-chat-turn-user ml-auto"
|
|
598
|
+
class:opacity-75={userMsg._optimistic}
|
|
599
|
+
class:opacity-50={userMsg._failed}
|
|
600
|
+
class:has-attachments={userMsg.attachments && userMsg.attachments.length > 0}
|
|
601
|
+
>
|
|
602
|
+
<div
|
|
603
|
+
class="message-bubble relative rounded-lg shadow-sm text-sm leading-relaxed select-text"
|
|
604
|
+
class:p-3={!userMsg.attachments || userMsg.attachments.length === 0}
|
|
605
|
+
class:p-0={userMsg.attachments && userMsg.attachments.length > 0}
|
|
606
|
+
class:bubble-fit-content={userMsg.attachments && userMsg.attachments.length > 0}
|
|
607
|
+
style="background-color: {isCompleted ? MESSAGE_COLORS.USER_COMPLETED : MESSAGE_COLORS.USER_ACTIVE}; color: {MESSAGE_COLORS.USER_TEXT};"
|
|
608
|
+
>
|
|
609
|
+
{#if userMsg.messageContent}
|
|
610
|
+
<p class="whitespace-pre-wrap p-3">{userMsg.messageContent}</p>
|
|
611
|
+
{/if}
|
|
612
|
+
{#if userMsg.attachments && userMsg.attachments.length > 0}
|
|
613
|
+
<div class="message-attachments-container">
|
|
614
|
+
<AttachmentDisplay attachments={userMsg.attachments} variant="message" showPreview={true} />
|
|
615
|
+
</div>
|
|
616
|
+
{:else if userMsg.messageContent && messageImpliesAttachedImage(userMsg.messageContent)}
|
|
617
|
+
<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="Image not available" aria-label="Missing image attachment">
|
|
619
|
+
<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
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
621
|
+
<path d="M4 4l16 16" stroke-linecap="round" />
|
|
622
|
+
</svg>
|
|
623
|
+
</div>
|
|
624
|
+
<p class="wc-missing-attachment-caption max-w-[12rem] text-center text-xs text-gray-500">Image not saved in chat history. Re-attach with Upload before Send.</p>
|
|
625
|
+
</div>
|
|
626
|
+
{/if}
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{@const commitReply = getTurnCommitReply(turn)}
|
|
631
|
+
{#if commitReply && showCommitMetaOnUserTurn(turn)}
|
|
632
|
+
<div class="wc-user-commit-meta-ct ml-auto flex max-w-[85%] flex-col items-end gap-1">
|
|
633
|
+
{#if commitReply.isWidgetHead}
|
|
634
|
+
<div class="checkpoint-label wc-head-commit-label">
|
|
635
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
636
|
+
<path d="M20 6L9 17l-5-5"/>
|
|
637
|
+
</svg>
|
|
638
|
+
Head at commit
|
|
639
|
+
</div>
|
|
640
|
+
{/if}
|
|
641
|
+
{#if extractCommitShort(commitReply.messageContent || '')}
|
|
642
|
+
<span class="wc-user-commit-sha font-mono text-xs text-gray-500">
|
|
643
|
+
Commit {extractCommitShort(commitReply.messageContent || '')}
|
|
644
|
+
</span>
|
|
645
|
+
{/if}
|
|
646
|
+
{#if turn.user && canUndoHeadUserTurn(turn) && !isLoading}
|
|
647
|
+
<div class="restore-checkpoint-row flex justify-end">
|
|
648
|
+
<button
|
|
649
|
+
class="restore-checkpoint-btn wc-undo-head-btn"
|
|
650
|
+
onclick={() => handleRestoreCheckpoint(turn.user!.id)}
|
|
651
|
+
title="Undo this prompt and revert code to the previous commit"
|
|
652
|
+
>
|
|
653
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
654
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
|
655
|
+
<path d="M3 3v5h5"/>
|
|
656
|
+
</svg>
|
|
657
|
+
Undo last change
|
|
658
|
+
</button>
|
|
659
|
+
</div>
|
|
660
|
+
{:else if canRestoreToMessage(commitReply) && !isLoading}
|
|
661
|
+
<div class="restore-checkpoint-row flex justify-end">
|
|
662
|
+
<button
|
|
663
|
+
class="restore-checkpoint-btn"
|
|
664
|
+
onclick={() => handleRestoreCheckpoint(commitReply.id)}
|
|
665
|
+
title="Restore widget code to this prompt's commit"
|
|
666
|
+
>
|
|
667
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
668
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
|
669
|
+
<path d="M3 3v5h5"/>
|
|
670
|
+
</svg>
|
|
671
|
+
Restore Checkpoint
|
|
672
|
+
</button>
|
|
673
|
+
</div>
|
|
674
|
+
{/if}
|
|
675
|
+
</div>
|
|
676
|
+
{/if}
|
|
677
|
+
{/if}
|
|
678
|
+
|
|
679
|
+
{#each turn.replies as reply (reply.id)}
|
|
680
|
+
{#if shouldShowAssistantReply(reply)}
|
|
681
|
+
<div class="message-container wc-chat-turn-reply mr-auto"
|
|
682
|
+
class:opacity-75={reply._optimistic}
|
|
683
|
+
class:opacity-50={reply._failed}
|
|
684
|
+
class:current-checkpoint={reply.isWidgetHead && messageHasCommit(reply)}
|
|
685
|
+
>
|
|
686
|
+
<div
|
|
687
|
+
class="message-bubble relative rounded-lg shadow-sm text-sm leading-relaxed select-text p-3"
|
|
688
|
+
class:wc-codegen-failure={isCodegenFailure(reply)}
|
|
689
|
+
style="background-color: {reply.isWidgetHead && messageHasCommit(reply) ? '#e0f2fe' : MESSAGE_COLORS.ASSISTANT}; color: {MESSAGE_COLORS.ASSISTANT_TEXT};{reply.isWidgetHead && messageHasCommit(reply) ? ' border: 1.5px solid #7dd3fc;' : ''}"
|
|
690
|
+
>
|
|
691
|
+
{#if reply.messageContent}
|
|
692
|
+
<p class="whitespace-pre-wrap">{reply.messageContent}</p>
|
|
693
|
+
{/if}
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
{#if reply.isWidgetHead && messageHasCommit(reply)}
|
|
697
|
+
<div class="wc-head-commit-meta-ct flex flex-col items-start gap-0.5 mt-1">
|
|
698
|
+
<div class="checkpoint-label wc-head-commit-label">
|
|
699
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
700
|
+
<path d="M20 6L9 17l-5-5"/>
|
|
701
|
+
</svg>
|
|
702
|
+
Head at commit
|
|
703
|
+
</div>
|
|
704
|
+
{#if extractCommitShort(reply.messageContent || '')}
|
|
705
|
+
<span class="wc-assistant-commit-sha font-mono text-xs text-gray-500">
|
|
706
|
+
Commit {extractCommitShort(reply.messageContent || '')}
|
|
707
|
+
</span>
|
|
708
|
+
{/if}
|
|
709
|
+
</div>
|
|
710
|
+
{/if}
|
|
711
|
+
|
|
712
|
+
{#if isCodegenFailure(reply) && turn.user && !isLoading}
|
|
713
|
+
<div class="retry-codegen-row flex justify-start mt-1">
|
|
714
|
+
<button
|
|
715
|
+
class="retry-codegen-btn"
|
|
716
|
+
onclick={() => handleRetryTurn(turn)}
|
|
717
|
+
title="Retry this code generation prompt"
|
|
718
|
+
>
|
|
719
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
720
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36"/>
|
|
721
|
+
<path d="M21 3v6h-6"/>
|
|
722
|
+
</svg>
|
|
723
|
+
Retry
|
|
724
|
+
</button>
|
|
725
|
+
</div>
|
|
726
|
+
{/if}
|
|
727
|
+
|
|
728
|
+
{#if reply.isWidgetHead && messageHasCommit(reply) && !isLoading}
|
|
729
|
+
<div class="restore-checkpoint-row flex justify-start mt-1">
|
|
730
|
+
<button
|
|
731
|
+
class="restore-checkpoint-btn wc-undo-head-btn"
|
|
732
|
+
onclick={() => handleRestoreCheckpoint(turn.user?.id ?? reply.id)}
|
|
733
|
+
title="Undo this prompt and revert code to the previous commit"
|
|
734
|
+
>
|
|
735
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
736
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
|
737
|
+
<path d="M3 3v5h5"/>
|
|
738
|
+
</svg>
|
|
739
|
+
Undo last change
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
{:else if canRestoreToMessage(reply) && !isLoading}
|
|
743
|
+
<div class="restore-checkpoint-row flex justify-start mt-1">
|
|
744
|
+
<button
|
|
745
|
+
class="restore-checkpoint-btn"
|
|
746
|
+
onclick={() => handleRestoreCheckpoint(reply.id)}
|
|
747
|
+
title="Restore widget code to this prompt's commit"
|
|
748
|
+
>
|
|
749
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
750
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
|
751
|
+
<path d="M3 3v5h5"/>
|
|
752
|
+
</svg>
|
|
753
|
+
Restore Checkpoint
|
|
754
|
+
</button>
|
|
755
|
+
</div>
|
|
756
|
+
{/if}
|
|
757
|
+
</div>
|
|
758
|
+
{/if}
|
|
759
|
+
{/each}
|
|
760
|
+
</div>
|
|
761
|
+
{/each}
|
|
762
|
+
|
|
763
|
+
<!-- Show suggestions toggle (when there are messages) -->
|
|
764
|
+
{#if messages.length > 0 && !isLoading}
|
|
765
|
+
<div class="suggestions-toggle-ct flex justify-center py-2">
|
|
766
|
+
{#if showSuggestionsInline}
|
|
767
|
+
<SuggestionButtons
|
|
768
|
+
{context}
|
|
769
|
+
visible={true}
|
|
770
|
+
onHide={() => { showSuggestionsInline = false; }}
|
|
771
|
+
on:suggestionSelected={(e) => { showSuggestionsInline = false; handleSuggestionSelected(e); }}
|
|
772
|
+
/>
|
|
773
|
+
{:else}
|
|
774
|
+
<button
|
|
775
|
+
class="suggestions-toggle-btn text-xs text-gray-400 hover:text-gray-600 px-3 py-1.5 rounded-full border border-gray-200 hover:border-gray-300 bg-white hover:bg-gray-50 transition-colors cursor-pointer"
|
|
776
|
+
onclick={() => { showSuggestionsInline = true; }}
|
|
777
|
+
>
|
|
778
|
+
Show suggestions
|
|
779
|
+
</button>
|
|
780
|
+
{/if}
|
|
781
|
+
</div>
|
|
782
|
+
{/if}
|
|
783
|
+
|
|
784
|
+
<!-- Loading indicator -->
|
|
785
|
+
{#if isLoading}
|
|
786
|
+
<div class="loading-indicator-ct flex items-center justify-start p-3">
|
|
787
|
+
<div class="rounded-lg shadow-sm text-sm leading-relaxed p-3"
|
|
788
|
+
style="background-color: {MESSAGE_COLORS.ASSISTANT}; color: {MESSAGE_COLORS.ASSISTANT_TEXT}"
|
|
789
|
+
>
|
|
790
|
+
<LoadingIndicator
|
|
791
|
+
type="thinking"
|
|
792
|
+
text="Assistant is thinking..."
|
|
793
|
+
color="secondary"
|
|
794
|
+
size="md"
|
|
795
|
+
/>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
{/if}
|
|
799
|
+
</div>
|
|
800
|
+
|
|
801
|
+
<style>
|
|
802
|
+
.loading {
|
|
803
|
+
opacity: 0.75;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* Remove any height constraints from messages */
|
|
807
|
+
.space-y-2 > * {
|
|
808
|
+
max-height: none !important;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.message-container {
|
|
812
|
+
max-width: 80%;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/* Messages with attachments: text full-width on top, images on a row below */
|
|
816
|
+
.message-container.has-attachments {
|
|
817
|
+
max-width: 90%;
|
|
818
|
+
max-height: none !important;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.message-attachments-container {
|
|
822
|
+
margin: 0;
|
|
823
|
+
padding: 8px 12px 12px;
|
|
824
|
+
width: 100%;
|
|
825
|
+
max-height: none !important;
|
|
826
|
+
overflow: visible !important;
|
|
827
|
+
display: flex;
|
|
828
|
+
flex-wrap: wrap;
|
|
829
|
+
gap: 8px;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* Ensure message bubbles don't constrain attachment size */
|
|
833
|
+
.message-container.has-attachments > div {
|
|
834
|
+
max-height: none !important;
|
|
835
|
+
overflow: visible !important;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/* Message bubble with attachments — full width for text, images below */
|
|
839
|
+
.bubble-fit-content {
|
|
840
|
+
width: 100% !important;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* Ensure proper alignment for fit-content bubbles */
|
|
844
|
+
.message-container.has-attachments {
|
|
845
|
+
display: flex;
|
|
846
|
+
flex-direction: column;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.message-container.has-attachments.ml-auto {
|
|
850
|
+
align-items: flex-end; /* Right align for user messages */
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.message-container.has-attachments.mr-auto {
|
|
854
|
+
align-items: flex-start; /* Left align for assistant messages */
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Restore checkpoint — always visible below user messages */
|
|
858
|
+
.wc-chat-turn-head {
|
|
859
|
+
border-color: #bae6fd;
|
|
860
|
+
background-color: #f8fcff;
|
|
861
|
+
padding: 6px;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.wc-codegen-failure {
|
|
865
|
+
border: 1px solid #fecaca;
|
|
866
|
+
background-color: #fef2f2 !important;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.wc-head-commit-label {
|
|
870
|
+
margin-bottom: 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.wc-head-commit-meta-ct .wc-head-commit-label {
|
|
874
|
+
margin-top: 0;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.restore-checkpoint-row {
|
|
878
|
+
margin-top: 4px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.retry-codegen-row {
|
|
882
|
+
margin-top: 4px;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.restore-checkpoint-btn,
|
|
886
|
+
.retry-codegen-btn {
|
|
887
|
+
display: inline-flex;
|
|
888
|
+
align-items: center;
|
|
889
|
+
gap: 4px;
|
|
890
|
+
padding: 3px 10px;
|
|
891
|
+
background: transparent;
|
|
892
|
+
color: #888;
|
|
893
|
+
border: 1px solid #ddd;
|
|
894
|
+
border-radius: 12px;
|
|
895
|
+
font-size: 11px;
|
|
896
|
+
font-weight: 500;
|
|
897
|
+
cursor: pointer;
|
|
898
|
+
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
899
|
+
white-space: nowrap;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.restore-checkpoint-btn:hover,
|
|
903
|
+
.retry-codegen-btn:hover {
|
|
904
|
+
background: rgba(0, 0, 0, 0.06);
|
|
905
|
+
color: #555;
|
|
906
|
+
border-color: #bbb;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.retry-codegen-btn {
|
|
910
|
+
color: #b91c1c;
|
|
911
|
+
border-color: #fecaca;
|
|
912
|
+
background: #fff7f7;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.retry-codegen-btn:hover {
|
|
916
|
+
background: #fee2e2;
|
|
917
|
+
color: #991b1b;
|
|
918
|
+
border-color: #fca5a5;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.restore-checkpoint-btn svg,
|
|
922
|
+
.retry-codegen-btn svg {
|
|
923
|
+
flex-shrink: 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* Copy message button — top-right, visible on hover */
|
|
927
|
+
.copy-msg-btn {
|
|
928
|
+
position: absolute;
|
|
929
|
+
top: 4px;
|
|
930
|
+
right: 4px;
|
|
931
|
+
display: flex;
|
|
932
|
+
align-items: center;
|
|
933
|
+
justify-content: center;
|
|
934
|
+
width: 22px;
|
|
935
|
+
height: 22px;
|
|
936
|
+
border-radius: 4px;
|
|
937
|
+
border: none;
|
|
938
|
+
background: rgba(0, 0, 0, 0.06);
|
|
939
|
+
color: #888;
|
|
940
|
+
cursor: pointer;
|
|
941
|
+
opacity: 0;
|
|
942
|
+
transition: opacity 0.15s ease;
|
|
943
|
+
}
|
|
944
|
+
.message-bubble:hover .copy-msg-btn {
|
|
945
|
+
opacity: 1;
|
|
946
|
+
}
|
|
947
|
+
.copy-msg-btn:hover {
|
|
948
|
+
background: rgba(0, 0, 0, 0.12);
|
|
949
|
+
color: #555;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* Checkpoint label — shown above the restored message */
|
|
953
|
+
.checkpoint-label {
|
|
954
|
+
display: inline-flex;
|
|
955
|
+
align-items: center;
|
|
956
|
+
gap: 4px;
|
|
957
|
+
padding: 2px 8px;
|
|
958
|
+
background: #0ea5e9;
|
|
959
|
+
color: white;
|
|
960
|
+
border-radius: 8px;
|
|
961
|
+
font-size: 10px;
|
|
962
|
+
font-weight: 600;
|
|
963
|
+
letter-spacing: 0.02em;
|
|
964
|
+
margin-bottom: 4px;
|
|
965
|
+
align-self: flex-end;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/* Restored button state */
|
|
969
|
+
.restore-checkpoint-btn.restored {
|
|
970
|
+
opacity: 0.5;
|
|
971
|
+
cursor: default;
|
|
972
|
+
border-color: #7dd3fc;
|
|
973
|
+
color: #0ea5e9;
|
|
974
|
+
}
|
|
975
|
+
.restore-checkpoint-btn.restored:hover {
|
|
976
|
+
background: transparent;
|
|
977
|
+
color: #0ea5e9;
|
|
978
|
+
border-color: #7dd3fc;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* Cap huge messages (legacy full-file dumps) so bubbles stay readable */
|
|
982
|
+
.message-bubble {
|
|
983
|
+
max-width: 100%;
|
|
984
|
+
}
|
|
985
|
+
.message-bubble p,
|
|
986
|
+
.message-content-mixed {
|
|
987
|
+
max-height: 280px;
|
|
988
|
+
overflow-y: auto;
|
|
989
|
+
overflow-x: auto;
|
|
990
|
+
word-break: break-word;
|
|
991
|
+
}
|
|
992
|
+
.message-content-mixed :global(pre) {
|
|
993
|
+
max-height: 240px;
|
|
994
|
+
overflow: auto;
|
|
995
|
+
}
|
|
996
|
+
</style>
|