@makolabs/ripple 0.4.1-0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -205
- package/dist/adapters/ai/OpenAIAdapter.d.ts +115 -0
- package/dist/adapters/ai/OpenAIAdapter.js +568 -0
- package/dist/adapters/ai/index.d.ts +3 -0
- package/dist/adapters/ai/index.js +3 -0
- package/dist/adapters/ai/types.d.ts +108 -0
- package/dist/adapters/ai/types.js +31 -0
- package/dist/adapters/storage/BaseAdapter.js +31 -31
- package/dist/ai/AIChatInterface.svelte +435 -0
- package/dist/ai/AIChatInterface.svelte.d.ts +18 -0
- package/dist/ai/ChatInput.svelte +211 -0
- package/dist/ai/ChatInput.svelte.d.ts +18 -0
- package/dist/ai/CodeRenderer.svelte +174 -0
- package/dist/ai/CodeRenderer.svelte.d.ts +8 -0
- package/dist/ai/ComposeDropdown.svelte +171 -0
- package/dist/ai/ComposeDropdown.svelte.d.ts +9 -0
- package/dist/ai/MermaidRenderer.svelte +89 -0
- package/dist/ai/MermaidRenderer.svelte.d.ts +7 -0
- package/dist/ai/MessageBox.svelte +403 -0
- package/dist/ai/MessageBox.svelte.d.ts +12 -0
- package/dist/ai/ThinkingDisplay.svelte +275 -0
- package/dist/ai/ThinkingDisplay.svelte.d.ts +9 -0
- package/dist/ai/ai-chat-interface.d.ts +161 -0
- package/dist/ai/ai-chat-interface.js +63 -0
- package/dist/ai/content-detector.d.ts +41 -0
- package/dist/ai/content-detector.js +153 -0
- package/dist/config/ai.d.ts +13 -0
- package/dist/config/ai.js +43 -0
- package/dist/elements/accordion/accordion.js +1 -1
- package/dist/elements/badge/Badge.svelte +14 -3
- package/dist/elements/dropdown/Dropdown.svelte +2 -2
- package/dist/elements/dropdown/Select.svelte +1 -1
- package/dist/elements/progress/Progress.svelte +7 -10
- package/dist/file-browser/FileBrowser.svelte +1 -1
- package/dist/forms/DateRange.svelte +18 -16
- package/dist/forms/NumberInput.svelte +1 -1
- package/dist/forms/RadioInputs.svelte +1 -1
- package/dist/forms/RadioPill.svelte +1 -1
- package/dist/forms/Tags.svelte +2 -2
- package/dist/helper/date.d.ts +1 -0
- package/dist/helper/date.js +6 -0
- package/dist/index.d.ts +65 -21
- package/dist/index.js +11 -0
- package/dist/layout/activity-list/ActivityList.svelte +94 -0
- package/dist/layout/activity-list/ActivityList.svelte.d.ts +4 -0
- package/dist/layout/activity-list/activity-list.d.ts +152 -0
- package/dist/layout/activity-list/activity-list.js +59 -0
- package/dist/layout/card/Card.svelte +1 -5
- package/dist/layout/card/metric-card.d.ts +18 -18
- package/dist/layout/table/Cells.svelte +1 -7
- package/dist/layout/table/Cells.svelte.d.ts +1 -1
- package/dist/modal/Modal.svelte +74 -174
- package/dist/modal/Modal.svelte.d.ts +2 -2
- package/dist/modal/modal.d.ts +61 -165
- package/dist/modal/modal.js +23 -64
- package/dist/sonner/sonner.svelte +1 -7
- package/dist/types/markdown.d.ts +14 -0
- package/dist/utils/Portal.svelte +1 -1
- package/package.json +128 -121
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { formatTime } from '../helper/date.js';
|
|
4
|
+
import { marked } from 'marked';
|
|
5
|
+
import { tv } from 'tailwind-variants';
|
|
6
|
+
import type { VariantColors } from '../index.js';
|
|
7
|
+
import { parseContentSegments } from './content-detector.js';
|
|
8
|
+
import MermaidRenderer from './MermaidRenderer.svelte';
|
|
9
|
+
import CodeRenderer from './CodeRenderer.svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
content: string;
|
|
13
|
+
isUser: boolean;
|
|
14
|
+
color?: VariantColors;
|
|
15
|
+
timestamp?: Date;
|
|
16
|
+
userMessageClass?: string;
|
|
17
|
+
aiMessageClass?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
content,
|
|
22
|
+
isUser,
|
|
23
|
+
color = 'default',
|
|
24
|
+
timestamp,
|
|
25
|
+
userMessageClass = '',
|
|
26
|
+
aiMessageClass = ''
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const messageBox = tv({
|
|
30
|
+
base: 'min-w-fit max-w-[80%] w-auto px-4 py-3 rounded-md text-sm leading-relaxed shadow-sm',
|
|
31
|
+
variants: {
|
|
32
|
+
type: {
|
|
33
|
+
user: 'inline-block',
|
|
34
|
+
ai: 'bg-white border text-default-900'
|
|
35
|
+
},
|
|
36
|
+
color: {
|
|
37
|
+
default: {},
|
|
38
|
+
primary: {},
|
|
39
|
+
secondary: {},
|
|
40
|
+
info: {},
|
|
41
|
+
success: {},
|
|
42
|
+
warning: {},
|
|
43
|
+
danger: {}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
compoundVariants: [
|
|
47
|
+
// User message variants
|
|
48
|
+
{ type: 'user', color: 'default', class: 'bg-default-500 text-white' },
|
|
49
|
+
{ type: 'user', color: 'primary', class: 'bg-primary-500 text-white' },
|
|
50
|
+
{ type: 'user', color: 'secondary', class: 'bg-secondary-500 text-white' },
|
|
51
|
+
{ type: 'user', color: 'info', class: 'bg-info-500 text-white' },
|
|
52
|
+
{ type: 'user', color: 'success', class: 'bg-success-500 text-white' },
|
|
53
|
+
{ type: 'user', color: 'warning', class: 'bg-warning-500 text-white' },
|
|
54
|
+
{ type: 'user', color: 'danger', class: 'bg-danger-500 text-white' },
|
|
55
|
+
|
|
56
|
+
// AI message variants - border colors
|
|
57
|
+
{ type: 'ai', color: 'default', class: 'border-default-200' },
|
|
58
|
+
{ type: 'ai', color: 'primary', class: 'border-primary-200' },
|
|
59
|
+
{ type: 'ai', color: 'secondary', class: 'border-secondary-200' },
|
|
60
|
+
{ type: 'ai', color: 'info', class: 'border-info-200' },
|
|
61
|
+
{ type: 'ai', color: 'success', class: 'border-success-200' },
|
|
62
|
+
{ type: 'ai', color: 'warning', class: 'border-warning-200' },
|
|
63
|
+
{ type: 'ai', color: 'danger', class: 'border-danger-200' }
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let copyButtonRef: HTMLButtonElement | undefined = $state();
|
|
68
|
+
let showCopied = $state(false);
|
|
69
|
+
|
|
70
|
+
function renderMarkdown(text: string): string {
|
|
71
|
+
const result = marked(text);
|
|
72
|
+
return typeof result === 'string' ? result : '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse content into segments for enhanced rendering
|
|
76
|
+
const contentSegments = $derived(parseContentSegments(content));
|
|
77
|
+
|
|
78
|
+
async function copyToClipboard(text: string, asRichText = false) {
|
|
79
|
+
try {
|
|
80
|
+
if (asRichText) {
|
|
81
|
+
// Copy as rich text (HTML)
|
|
82
|
+
const htmlText = renderMarkdown(text);
|
|
83
|
+
const clipboardItem = new ClipboardItem({
|
|
84
|
+
'text/html': new Blob([htmlText], { type: 'text/html' }),
|
|
85
|
+
'text/plain': new Blob([text], { type: 'text/plain' })
|
|
86
|
+
});
|
|
87
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
88
|
+
} else {
|
|
89
|
+
// Copy as plain text (markdown)
|
|
90
|
+
await navigator.clipboard.writeText(text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
showCopied = true;
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
showCopied = false;
|
|
96
|
+
}, 2000);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('Failed to copy: ', err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleCopyClick(event: MouseEvent) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
copyToClipboard(content, false); // Single click = plain markdown
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleCopyDoubleClick(event: MouseEvent) {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
copyToClipboard(content, true); // Double click = rich text
|
|
110
|
+
}
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<div class="group relative flex flex-col {isUser ? 'justify-end' : 'justify-start'}">
|
|
114
|
+
<div
|
|
115
|
+
class={cn(
|
|
116
|
+
messageBox({
|
|
117
|
+
type: isUser ? 'user' : 'ai',
|
|
118
|
+
color: color as
|
|
119
|
+
| 'default'
|
|
120
|
+
| 'primary'
|
|
121
|
+
| 'secondary'
|
|
122
|
+
| 'info'
|
|
123
|
+
| 'success'
|
|
124
|
+
| 'warning'
|
|
125
|
+
| 'danger'
|
|
126
|
+
}),
|
|
127
|
+
!isUser && 'markdown-content',
|
|
128
|
+
isUser ? userMessageClass || '' : aiMessageClass || ''
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{#if isUser}
|
|
132
|
+
{content}
|
|
133
|
+
{:else}
|
|
134
|
+
<!-- Enhanced rendering for AI messages with Mermaid and code highlighting -->
|
|
135
|
+
{#each contentSegments as segment}
|
|
136
|
+
{#if segment.type === 'mermaid'}
|
|
137
|
+
<MermaidRenderer diagram={segment.content} />
|
|
138
|
+
{:else if segment.type === 'code'}
|
|
139
|
+
<CodeRenderer
|
|
140
|
+
code={segment.content}
|
|
141
|
+
language={segment.language ?? 'text'}
|
|
142
|
+
/>
|
|
143
|
+
{:else if segment.type === 'text'}
|
|
144
|
+
<!-- Regular markdown content -->
|
|
145
|
+
{@html renderMarkdown(segment.content)}
|
|
146
|
+
{/if}
|
|
147
|
+
{/each}
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="flex flex-row items-center gap-2 justify-between">
|
|
152
|
+
<!-- Timestamp below message bubble -->
|
|
153
|
+
{#if timestamp}
|
|
154
|
+
<div class={cn('mt-1 text-xs text-gray-500', isUser ? 'text-right ml-auto' : 'text-left mr-auto')}>
|
|
155
|
+
{formatTime(timestamp)}
|
|
156
|
+
</div>
|
|
157
|
+
{/if}
|
|
158
|
+
|
|
159
|
+
<!-- Copy button for AI messages -->
|
|
160
|
+
{#if !isUser}
|
|
161
|
+
<div class="mt-1 flex justify-start">
|
|
162
|
+
<button
|
|
163
|
+
bind:this={copyButtonRef}
|
|
164
|
+
onclick={handleCopyClick}
|
|
165
|
+
ondblclick={handleCopyDoubleClick}
|
|
166
|
+
class="flex items-center gap-1 rounded-md bg-gray-100 px-3 py-1 text-xs font-medium text-gray-600 opacity-0 transition-opacity duration-200 group-hover:opacity-100 hover:bg-gray-200 hover:text-gray-800"
|
|
167
|
+
title="Click: Copy markdown • Double-click: Copy rich text"
|
|
168
|
+
>
|
|
169
|
+
{#if showCopied}
|
|
170
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
171
|
+
<path
|
|
172
|
+
stroke-linecap="round"
|
|
173
|
+
stroke-linejoin="round"
|
|
174
|
+
stroke-width="2"
|
|
175
|
+
d="M5 13l4 4L19 7"
|
|
176
|
+
></path>
|
|
177
|
+
</svg>
|
|
178
|
+
Copied!
|
|
179
|
+
{:else}
|
|
180
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
181
|
+
<path
|
|
182
|
+
stroke-linecap="round"
|
|
183
|
+
stroke-linejoin="round"
|
|
184
|
+
stroke-width="2"
|
|
185
|
+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
186
|
+
></path>
|
|
187
|
+
</svg>
|
|
188
|
+
Copy
|
|
189
|
+
{/if}
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
{/if}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<style>
|
|
197
|
+
/* Custom markdown styling for AI messages */
|
|
198
|
+
:global(.markdown-content) {
|
|
199
|
+
line-height: 1.6;
|
|
200
|
+
color: rgb(55 65 81);
|
|
201
|
+
word-wrap: break-word;
|
|
202
|
+
overflow-wrap: break-word;
|
|
203
|
+
max-width: 100%;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Headers */
|
|
207
|
+
:global(.markdown-content h1),
|
|
208
|
+
:global(.markdown-content h2),
|
|
209
|
+
:global(.markdown-content h3),
|
|
210
|
+
:global(.markdown-content h4),
|
|
211
|
+
:global(.markdown-content h5),
|
|
212
|
+
:global(.markdown-content h6) {
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
margin: 1.25em 0 0.5em 0;
|
|
215
|
+
color: rgb(17 24 39);
|
|
216
|
+
line-height: 1.25;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
:global(.markdown-content h1:first-child),
|
|
220
|
+
:global(.markdown-content h2:first-child),
|
|
221
|
+
:global(.markdown-content h3:first-child),
|
|
222
|
+
:global(.markdown-content h4:first-child),
|
|
223
|
+
:global(.markdown-content h5:first-child),
|
|
224
|
+
:global(.markdown-content h6:first-child) {
|
|
225
|
+
margin-top: 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
:global(.markdown-content h1) {
|
|
229
|
+
font-size: 1.875em;
|
|
230
|
+
}
|
|
231
|
+
:global(.markdown-content h2) {
|
|
232
|
+
font-size: 1.5em;
|
|
233
|
+
}
|
|
234
|
+
:global(.markdown-content h3) {
|
|
235
|
+
font-size: 1.25em;
|
|
236
|
+
}
|
|
237
|
+
:global(.markdown-content h4) {
|
|
238
|
+
font-size: 1.125em;
|
|
239
|
+
}
|
|
240
|
+
:global(.markdown-content h5) {
|
|
241
|
+
font-size: 1em;
|
|
242
|
+
}
|
|
243
|
+
:global(.markdown-content h6) {
|
|
244
|
+
font-size: 0.875em;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Paragraphs */
|
|
248
|
+
:global(.markdown-content p) {
|
|
249
|
+
margin: 0.75em 0;
|
|
250
|
+
line-height: 1.6;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
:global(.markdown-content p:first-child) {
|
|
254
|
+
margin-top: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
:global(.markdown-content p:last-child) {
|
|
258
|
+
margin-bottom: 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
:global(.markdown-content p:only-child) {
|
|
262
|
+
margin: 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Typography */
|
|
266
|
+
:global(.markdown-content strong) {
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
color: rgb(17 24 39);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
:global(.markdown-content em) {
|
|
272
|
+
font-style: italic;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Code */
|
|
276
|
+
:global(.markdown-content code) {
|
|
277
|
+
background-color: rgb(243 244 246);
|
|
278
|
+
color: rgb(239 68 68);
|
|
279
|
+
padding: 0.125rem 0.375rem;
|
|
280
|
+
border-radius: 0.25rem;
|
|
281
|
+
font-size: 0.875em;
|
|
282
|
+
font-family:
|
|
283
|
+
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
:global(.markdown-content pre) {
|
|
287
|
+
background-color: rgb(31 41 55);
|
|
288
|
+
color: rgb(243 244 246);
|
|
289
|
+
padding: 1rem;
|
|
290
|
+
border-radius: 0.5rem;
|
|
291
|
+
overflow-x: auto;
|
|
292
|
+
margin: 1rem 0;
|
|
293
|
+
font-family:
|
|
294
|
+
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
295
|
+
font-size: 0.875em;
|
|
296
|
+
line-height: 1.5;
|
|
297
|
+
max-width: 100%;
|
|
298
|
+
white-space: pre-wrap;
|
|
299
|
+
word-break: break-all;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
:global(.markdown-content pre code) {
|
|
303
|
+
background: none;
|
|
304
|
+
color: inherit;
|
|
305
|
+
padding: 0;
|
|
306
|
+
font-size: inherit;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Lists */
|
|
310
|
+
:global(.markdown-content ul),
|
|
311
|
+
:global(.markdown-content ol) {
|
|
312
|
+
margin: 0.75em 0;
|
|
313
|
+
padding-left: 1.5rem;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
:global(.markdown-content ul) {
|
|
317
|
+
list-style-type: disc;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
:global(.markdown-content ol) {
|
|
321
|
+
list-style-type: decimal;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
:global(.markdown-content li) {
|
|
325
|
+
margin: 0.25em 0;
|
|
326
|
+
line-height: 1.5;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
:global(.markdown-content li > ul),
|
|
330
|
+
:global(.markdown-content li > ol) {
|
|
331
|
+
margin: 0.25em 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Blockquotes */
|
|
335
|
+
:global(.markdown-content blockquote) {
|
|
336
|
+
border-left: 4px solid rgb(209 213 219);
|
|
337
|
+
padding: 0.5rem 1rem;
|
|
338
|
+
margin: 1rem 0;
|
|
339
|
+
background-color: rgb(249 250 251);
|
|
340
|
+
font-style: italic;
|
|
341
|
+
color: rgb(75 85 99);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
:global(.markdown-content blockquote p) {
|
|
345
|
+
margin: 0.5em 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
:global(.markdown-content blockquote p:first-child) {
|
|
349
|
+
margin-top: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
:global(.markdown-content blockquote p:last-child) {
|
|
353
|
+
margin-bottom: 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Links */
|
|
357
|
+
:global(.markdown-content a) {
|
|
358
|
+
color: rgb(37 99 235);
|
|
359
|
+
text-decoration: underline;
|
|
360
|
+
text-underline-offset: 2px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
:global(.markdown-content a:hover) {
|
|
364
|
+
color: rgb(29 78 216);
|
|
365
|
+
text-decoration-thickness: 2px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Horizontal rules */
|
|
369
|
+
:global(.markdown-content hr) {
|
|
370
|
+
border: none;
|
|
371
|
+
border-top: 1px solid rgb(229 231 235);
|
|
372
|
+
margin: 1.5rem 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Tables */
|
|
376
|
+
:global(.markdown-content table) {
|
|
377
|
+
border-collapse: collapse;
|
|
378
|
+
width: 100%;
|
|
379
|
+
margin: 1rem 0;
|
|
380
|
+
font-size: 0.875em;
|
|
381
|
+
max-width: 100%;
|
|
382
|
+
overflow-x: auto;
|
|
383
|
+
display: block;
|
|
384
|
+
white-space: nowrap;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
:global(.markdown-content th),
|
|
388
|
+
:global(.markdown-content td) {
|
|
389
|
+
border: 1px solid rgb(229 231 235);
|
|
390
|
+
padding: 0.5rem 0.75rem;
|
|
391
|
+
text-align: left;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
:global(.markdown-content th) {
|
|
395
|
+
background-color: rgb(249 250 251);
|
|
396
|
+
font-weight: 600;
|
|
397
|
+
color: rgb(17 24 39);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
:global(.markdown-content tbody tr:nth-child(even)) {
|
|
401
|
+
background-color: rgb(249 250 251);
|
|
402
|
+
}
|
|
403
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { VariantColors } from '../index.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
content: string;
|
|
4
|
+
isUser: boolean;
|
|
5
|
+
color?: VariantColors;
|
|
6
|
+
timestamp?: Date;
|
|
7
|
+
userMessageClass?: string;
|
|
8
|
+
aiMessageClass?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const MessageBox: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type MessageBox = ReturnType<typeof MessageBox>;
|
|
12
|
+
export default MessageBox;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tv } from 'tailwind-variants';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface ThinkingDisplayProps {
|
|
6
|
+
content: string;
|
|
7
|
+
isComplete?: boolean;
|
|
8
|
+
isVisible?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
content,
|
|
14
|
+
isComplete = false,
|
|
15
|
+
isVisible = true,
|
|
16
|
+
class: className = ''
|
|
17
|
+
}: ThinkingDisplayProps = $props();
|
|
18
|
+
|
|
19
|
+
let isExpanded = $state(false);
|
|
20
|
+
let containerRef: HTMLDivElement | undefined = $state();
|
|
21
|
+
|
|
22
|
+
// Parse thinking content into structured steps
|
|
23
|
+
const parsedSteps = $derived.by(() => {
|
|
24
|
+
if (!content) return [];
|
|
25
|
+
|
|
26
|
+
// Split content by markdown headers or double asterisks
|
|
27
|
+
const sections = content.split(/\*\*([^*]+)\*\*/g);
|
|
28
|
+
const steps = [];
|
|
29
|
+
|
|
30
|
+
// When split by regex with capture groups, we get: [text, title, text, title, ...]
|
|
31
|
+
for (let i = 0; i < sections.length; i++) {
|
|
32
|
+
if (i % 2 === 0) {
|
|
33
|
+
// Even indices are text content
|
|
34
|
+
const text = sections[i]?.trim();
|
|
35
|
+
if (text && text.length > 10) {
|
|
36
|
+
// Check if there's a title after this text
|
|
37
|
+
const title = sections[i + 1]?.trim();
|
|
38
|
+
steps.push({
|
|
39
|
+
title: title || 'Thinking...',
|
|
40
|
+
content: text,
|
|
41
|
+
isComplete: isComplete
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
// Odd indices are titles - check if there's content after
|
|
46
|
+
const title = sections[i]?.trim();
|
|
47
|
+
const nextContent = sections[i + 1]?.trim();
|
|
48
|
+
if (title && nextContent && nextContent.length > 10) {
|
|
49
|
+
steps.push({
|
|
50
|
+
title: title,
|
|
51
|
+
content: nextContent,
|
|
52
|
+
isComplete: isComplete
|
|
53
|
+
});
|
|
54
|
+
i++; // Skip the next content since we've processed it
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If no structured content found, treat as single step
|
|
60
|
+
if (steps.length === 0 && content.trim()) {
|
|
61
|
+
steps.push({
|
|
62
|
+
title: isComplete ? 'Reasoning Complete' : 'Thinking...',
|
|
63
|
+
content: content.trim(),
|
|
64
|
+
isComplete: isComplete
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return steps;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Find the longest/most descriptive title for collapsed view
|
|
72
|
+
const bestTitle = $derived.by(() => {
|
|
73
|
+
if (parsedSteps.length === 0) return 'Reasoning Complete';
|
|
74
|
+
|
|
75
|
+
// Find the step with the longest title (most descriptive)
|
|
76
|
+
let longest = parsedSteps[0];
|
|
77
|
+
for (const step of parsedSteps) {
|
|
78
|
+
if (step.title.length > longest.title.length && step.title !== 'Thinking...') {
|
|
79
|
+
longest = step;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return longest.title;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const thinkingContainer = tv({
|
|
87
|
+
base: 'relative mb-4 rounded-md border transition-all duration-200',
|
|
88
|
+
variants: {
|
|
89
|
+
state: {
|
|
90
|
+
thinking: 'border-blue-200 bg-blue-50/50',
|
|
91
|
+
complete: 'border-gray-200 bg-gray-50/50'
|
|
92
|
+
},
|
|
93
|
+
expanded: {
|
|
94
|
+
true: 'shadow-sm',
|
|
95
|
+
false: ''
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const thinkingHeader = tv({
|
|
101
|
+
base: 'flex items-center justify-between p-3 cursor-pointer hover:bg-black/5 transition-colors rounded-t-md',
|
|
102
|
+
variants: {
|
|
103
|
+
state: {
|
|
104
|
+
thinking: 'text-blue-700',
|
|
105
|
+
complete: 'text-gray-700'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const thinkingContent = tv({
|
|
111
|
+
base: 'px-3 pb-3 pt-1 text-sm leading-relaxed transition-all duration-200',
|
|
112
|
+
variants: {
|
|
113
|
+
state: {
|
|
114
|
+
thinking: 'text-blue-800',
|
|
115
|
+
complete: 'text-gray-700'
|
|
116
|
+
},
|
|
117
|
+
expanded: {
|
|
118
|
+
true: 'max-h-none',
|
|
119
|
+
false: 'max-h-20 overflow-hidden'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const containerClasses = $derived(
|
|
125
|
+
thinkingContainer({
|
|
126
|
+
state: isComplete ? 'complete' : 'thinking',
|
|
127
|
+
expanded: isExpanded
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const headerClasses = $derived(
|
|
132
|
+
thinkingHeader({
|
|
133
|
+
state: isComplete ? 'complete' : 'thinking'
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const contentClasses = $derived(
|
|
138
|
+
thinkingContent({
|
|
139
|
+
state: isComplete ? 'complete' : 'thinking',
|
|
140
|
+
expanded: isExpanded
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
function toggleExpanded() {
|
|
145
|
+
isExpanded = !isExpanded;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Don't auto-expand when complete - let user choose to expand
|
|
149
|
+
// $effect(() => {
|
|
150
|
+
// if (isComplete && parsedSteps.length > 1) {
|
|
151
|
+
// // Small delay to make the transition smoother
|
|
152
|
+
// setTimeout(() => {
|
|
153
|
+
// isExpanded = true;
|
|
154
|
+
// }, 300);
|
|
155
|
+
// }
|
|
156
|
+
// });
|
|
157
|
+
|
|
158
|
+
</script>
|
|
159
|
+
|
|
160
|
+
{#if isVisible && content}
|
|
161
|
+
<div bind:this={containerRef} class={`${containerClasses} ${className}`}>
|
|
162
|
+
<!-- Header -->
|
|
163
|
+
<div
|
|
164
|
+
class={headerClasses}
|
|
165
|
+
onclick={toggleExpanded}
|
|
166
|
+
onkeydown={(e) => e.key === 'Enter' || e.key === ' ' ? toggleExpanded() : null}
|
|
167
|
+
role="button"
|
|
168
|
+
tabindex="0"
|
|
169
|
+
>
|
|
170
|
+
<div class="flex items-center gap-2">
|
|
171
|
+
<!-- Thinking Icon -->
|
|
172
|
+
<div class="flex items-center gap-2">
|
|
173
|
+
{#if !isComplete}
|
|
174
|
+
<!-- Animated thinking dots -->
|
|
175
|
+
<div class="flex items-center gap-1">
|
|
176
|
+
<div class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse"></div>
|
|
177
|
+
<div class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" style="animation-delay: 0.2s"></div>
|
|
178
|
+
<div class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse" style="animation-delay: 0.4s"></div>
|
|
179
|
+
</div>
|
|
180
|
+
{:else}
|
|
181
|
+
<!-- Completed icon -->
|
|
182
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-gray-500" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
183
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.37 3.37 0 0 0 14 18.469V19a2 2 0 1 1-4 0v-.531c0-.895-.356-1.754-.988-2.386z"/>
|
|
184
|
+
</svg>
|
|
185
|
+
{/if}
|
|
186
|
+
<span class="font-medium text-sm">
|
|
187
|
+
{isComplete ? bestTitle : 'Thinking...'}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Expand/Collapse Icon -->
|
|
193
|
+
<svg
|
|
194
|
+
class={`w-4 h-4 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
|
195
|
+
fill="none"
|
|
196
|
+
stroke="currentColor"
|
|
197
|
+
viewBox="0 0 24 24"
|
|
198
|
+
>
|
|
199
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
200
|
+
</svg>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- Content -->
|
|
204
|
+
{#if isExpanded}
|
|
205
|
+
<div class={contentClasses}>
|
|
206
|
+
<!-- Timeline Container -->
|
|
207
|
+
<div class="relative">
|
|
208
|
+
{#each parsedSteps as step, index}
|
|
209
|
+
<div class="relative flex">
|
|
210
|
+
<!-- Timeline Line and Icon -->
|
|
211
|
+
<div class="flex flex-col items-center mr-4">
|
|
212
|
+
<!-- Timeline Icon -->
|
|
213
|
+
<div class="flex items-center justify-center w-5 h-5 shrink-0 rounded-full bg-white border border-gray-300 z-10">
|
|
214
|
+
{#if step.isComplete && index === parsedSteps.length - 1}
|
|
215
|
+
<!-- Checkmark only for the last completed step -->
|
|
216
|
+
<svg class="w-3 h-3 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
|
217
|
+
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
|
218
|
+
</svg>
|
|
219
|
+
{:else if step.isComplete}
|
|
220
|
+
<!-- Simple filled circle for other completed steps -->
|
|
221
|
+
<div class="w-2.5 h-2.5 bg-gray-500 rounded-full"></div>
|
|
222
|
+
{:else}
|
|
223
|
+
<!-- Pulsing dot for in-progress step -->
|
|
224
|
+
<div class="w-2.5 h-2.5 bg-gray-400 rounded-full animate-pulse"></div>
|
|
225
|
+
{/if}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Vertical Line (except for last item) -->
|
|
229
|
+
{#if index < parsedSteps.length - 1}
|
|
230
|
+
<div class="w-0.5 h-full min-h-8 bg-gray-200 mt-1"></div>
|
|
231
|
+
{/if}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Step Content -->
|
|
235
|
+
<div class="flex-1 pb-6 last:pb-0">
|
|
236
|
+
<!-- Step Title -->
|
|
237
|
+
<div class="mb-2">
|
|
238
|
+
<h4 class="font-medium text-sm text-gray-800 leading-tight">
|
|
239
|
+
{step.title}
|
|
240
|
+
</h4>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Step Description -->
|
|
244
|
+
<div class="text-sm text-gray-600 leading-relaxed">
|
|
245
|
+
{#if step.content.includes('•') || step.content.includes('-')}
|
|
246
|
+
<!-- Parse bullet points -->
|
|
247
|
+
<ul class="space-y-1">
|
|
248
|
+
{#each step.content.split('\n') as line}
|
|
249
|
+
{@const trimmedLine = line.trim()}
|
|
250
|
+
{#if trimmedLine.startsWith('•') || trimmedLine.startsWith('-')}
|
|
251
|
+
<li class="flex items-start">
|
|
252
|
+
<span class="w-1 h-1 bg-gray-400 rounded-full mt-2 mr-2 flex-shrink-0"></span>
|
|
253
|
+
<span>{trimmedLine.substring(1).trim()}</span>
|
|
254
|
+
</li>
|
|
255
|
+
{:else if trimmedLine && !trimmedLine.startsWith('•') && !trimmedLine.startsWith('-')}
|
|
256
|
+
<p class="mb-2">{trimmedLine}</p>
|
|
257
|
+
{/if}
|
|
258
|
+
{/each}
|
|
259
|
+
</ul>
|
|
260
|
+
{:else}
|
|
261
|
+
<p class="whitespace-pre-wrap">{step.content}</p>
|
|
262
|
+
{/if}
|
|
263
|
+
|
|
264
|
+
{#if !step.isComplete && index === parsedSteps.length - 1}
|
|
265
|
+
<span class="inline-block w-0.5 h-4 bg-blue-500 animate-pulse ml-1"></span>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
{/each}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface ThinkingDisplayProps {
|
|
2
|
+
content: string;
|
|
3
|
+
isComplete?: boolean;
|
|
4
|
+
isVisible?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
declare const ThinkingDisplay: import("svelte").Component<ThinkingDisplayProps, {}, "">;
|
|
8
|
+
type ThinkingDisplay = ReturnType<typeof ThinkingDisplay>;
|
|
9
|
+
export default ThinkingDisplay;
|