@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.
Files changed (59) hide show
  1. package/README.md +165 -205
  2. package/dist/adapters/ai/OpenAIAdapter.d.ts +115 -0
  3. package/dist/adapters/ai/OpenAIAdapter.js +568 -0
  4. package/dist/adapters/ai/index.d.ts +3 -0
  5. package/dist/adapters/ai/index.js +3 -0
  6. package/dist/adapters/ai/types.d.ts +108 -0
  7. package/dist/adapters/ai/types.js +31 -0
  8. package/dist/adapters/storage/BaseAdapter.js +31 -31
  9. package/dist/ai/AIChatInterface.svelte +435 -0
  10. package/dist/ai/AIChatInterface.svelte.d.ts +18 -0
  11. package/dist/ai/ChatInput.svelte +211 -0
  12. package/dist/ai/ChatInput.svelte.d.ts +18 -0
  13. package/dist/ai/CodeRenderer.svelte +174 -0
  14. package/dist/ai/CodeRenderer.svelte.d.ts +8 -0
  15. package/dist/ai/ComposeDropdown.svelte +171 -0
  16. package/dist/ai/ComposeDropdown.svelte.d.ts +9 -0
  17. package/dist/ai/MermaidRenderer.svelte +89 -0
  18. package/dist/ai/MermaidRenderer.svelte.d.ts +7 -0
  19. package/dist/ai/MessageBox.svelte +403 -0
  20. package/dist/ai/MessageBox.svelte.d.ts +12 -0
  21. package/dist/ai/ThinkingDisplay.svelte +275 -0
  22. package/dist/ai/ThinkingDisplay.svelte.d.ts +9 -0
  23. package/dist/ai/ai-chat-interface.d.ts +161 -0
  24. package/dist/ai/ai-chat-interface.js +63 -0
  25. package/dist/ai/content-detector.d.ts +41 -0
  26. package/dist/ai/content-detector.js +153 -0
  27. package/dist/config/ai.d.ts +13 -0
  28. package/dist/config/ai.js +43 -0
  29. package/dist/elements/accordion/accordion.js +1 -1
  30. package/dist/elements/badge/Badge.svelte +14 -3
  31. package/dist/elements/dropdown/Dropdown.svelte +2 -2
  32. package/dist/elements/dropdown/Select.svelte +1 -1
  33. package/dist/elements/progress/Progress.svelte +7 -10
  34. package/dist/file-browser/FileBrowser.svelte +1 -1
  35. package/dist/forms/DateRange.svelte +18 -16
  36. package/dist/forms/NumberInput.svelte +1 -1
  37. package/dist/forms/RadioInputs.svelte +1 -1
  38. package/dist/forms/RadioPill.svelte +1 -1
  39. package/dist/forms/Tags.svelte +2 -2
  40. package/dist/helper/date.d.ts +1 -0
  41. package/dist/helper/date.js +6 -0
  42. package/dist/index.d.ts +65 -21
  43. package/dist/index.js +11 -0
  44. package/dist/layout/activity-list/ActivityList.svelte +94 -0
  45. package/dist/layout/activity-list/ActivityList.svelte.d.ts +4 -0
  46. package/dist/layout/activity-list/activity-list.d.ts +152 -0
  47. package/dist/layout/activity-list/activity-list.js +59 -0
  48. package/dist/layout/card/Card.svelte +1 -5
  49. package/dist/layout/card/metric-card.d.ts +18 -18
  50. package/dist/layout/table/Cells.svelte +1 -7
  51. package/dist/layout/table/Cells.svelte.d.ts +1 -1
  52. package/dist/modal/Modal.svelte +74 -174
  53. package/dist/modal/Modal.svelte.d.ts +2 -2
  54. package/dist/modal/modal.d.ts +61 -165
  55. package/dist/modal/modal.js +23 -64
  56. package/dist/sonner/sonner.svelte +1 -7
  57. package/dist/types/markdown.d.ts +14 -0
  58. package/dist/utils/Portal.svelte +1 -1
  59. 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;