Skip to content

Commit 9cf4d34

Browse files
committed
refac
1 parent 284b97b commit 9cf4d34

File tree

3 files changed

+196
-160
lines changed

3 files changed

+196
-160
lines changed

src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import KatexRenderer from './KatexRenderer.svelte';
1818
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
1919
import Collapsible from '$lib/components/common/Collapsible.svelte';
20+
import ToolCallDisplay from '$lib/components/common/ToolCallDisplay.svelte';
2021
import Tooltip from '$lib/components/common/Tooltip.svelte';
2122
import Download from '$lib/components/icons/Download.svelte';
2223
@@ -323,7 +324,15 @@
323324
.replace(/<summary>.*?<\/summary>/gi, '')
324325
.trim()}
325326

326-
{#if textContent.length > 0}
327+
{#if token?.attributes?.type === 'tool_calls'}
328+
<!-- Tool calls have dedicated handling with ToolCallDisplay component -->
329+
<ToolCallDisplay
330+
id={`${id}-${tokenIdx}-tc`}
331+
attributes={token.attributes}
332+
open={false}
333+
className="w-full space-y-1"
334+
/>
335+
{:else if textContent.length > 0}
327336
<Collapsible
328337
title={token.summary}
329338
open={$settings?.expandDetails ?? false}

src/lib/components/common/Collapsible.svelte

Lines changed: 0 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@
3535
import ChevronUp from '../icons/ChevronUp.svelte';
3636
import ChevronDown from '../icons/ChevronDown.svelte';
3737
import Spinner from './Spinner.svelte';
38-
import CodeBlock from '../chat/Messages/CodeBlock.svelte';
39-
import Markdown from '../chat/Messages/Markdown.svelte';
40-
import Image from './Image.svelte';
41-
import FullHeightIframe from './FullHeightIframe.svelte';
42-
import { settings } from '$lib/stores';
4338
4439
export let open = false;
4540
@@ -62,162 +57,9 @@
6257
$: onChange(open);
6358
6459
const collapsibleId = uuidv4();
65-
66-
function parseJSONString(str) {
67-
try {
68-
return parseJSONString(JSON.parse(str));
69-
} catch (e) {
70-
return str;
71-
}
72-
}
73-
74-
function formatJSONString(str) {
75-
try {
76-
const parsed = parseJSONString(str);
77-
// If parsed is an object/array, then it's valid JSON
78-
if (typeof parsed === 'object') {
79-
return JSON.stringify(parsed, null, 2);
80-
} else {
81-
// It's a primitive value like a number, boolean, etc.
82-
return `${JSON.stringify(String(parsed))}`;
83-
}
84-
} catch (e) {
85-
// Not valid JSON, return as-is
86-
return str;
87-
}
88-
}
8960
</script>
9061

9162
<div {id} class={className}>
92-
{#if attributes?.type === 'tool_calls'}
93-
{@const args = decode(attributes?.arguments)}
94-
{@const result = decode(attributes?.result ?? '')}
95-
{@const files = parseJSONString(decode(attributes?.files ?? ''))}
96-
{@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
97-
98-
{#if embeds && Array.isArray(embeds) && embeds.length > 0}
99-
<div class="py-1 w-full cursor-pointer">
100-
<div class=" w-full text-xs text-gray-500">
101-
<div class="">
102-
{attributes.name}
103-
</div>
104-
</div>
105-
106-
{#each embeds as embed, idx}
107-
<div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
108-
<FullHeightIframe
109-
src={embed}
110-
{args}
111-
allowScripts={true}
112-
allowForms={true}
113-
allowSameOrigin={true}
114-
allowPopups={true}
115-
/>
116-
</div>
117-
{/each}
118-
</div>
119-
{:else}
120-
<div
121-
class="{buttonClassName} cursor-pointer"
122-
on:pointerup={() => {
123-
if (!disabled) {
124-
open = !open;
125-
}
126-
}}
127-
>
128-
<div
129-
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
130-
attributes?.done !== 'true'
131-
? 'shimmer'
132-
: ''}
133-
"
134-
>
135-
{#if attributes?.done && attributes?.done !== 'true'}
136-
<div>
137-
<Spinner className="size-4" />
138-
</div>
139-
{/if}
140-
141-
<div class="">
142-
{#if attributes?.done === 'true'}
143-
<Markdown
144-
id={`${collapsibleId}-tool-calls-${attributes?.id}`}
145-
content={$i18n.t('View Result from **{{NAME}}**', {
146-
NAME: attributes.name
147-
})}
148-
/>
149-
{:else}
150-
<Markdown
151-
id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
152-
content={$i18n.t('Executing **{{NAME}}**...', {
153-
NAME: attributes.name
154-
})}
155-
/>
156-
{/if}
157-
</div>
158-
159-
<div class="flex self-center translate-y-[1px]">
160-
{#if open}
161-
<ChevronUp strokeWidth="3.5" className="size-3.5" />
162-
{:else}
163-
<ChevronDown strokeWidth="3.5" className="size-3.5" />
164-
{/if}
165-
</div>
166-
</div>
167-
</div>
168-
169-
{#if !grow}
170-
{#if open && !hide}
171-
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
172-
{#if attributes?.type === 'tool_calls'}
173-
{#if attributes?.done === 'true'}
174-
<Markdown
175-
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
176-
content={`> \`\`\`json
177-
> ${formatJSONString(args)}
178-
> ${formatJSONString(result)}
179-
> \`\`\``}
180-
/>
181-
{:else}
182-
<Markdown
183-
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
184-
content={`> \`\`\`json
185-
> ${formatJSONString(args)}
186-
> \`\`\``}
187-
/>
188-
{/if}
189-
{:else}
190-
<slot name="content" />
191-
{/if}
192-
</div>
193-
{/if}
194-
{/if}
195-
{/if}
196-
197-
{#if attributes?.done === 'true'}
198-
{#if typeof files === 'object'}
199-
{#each files ?? [] as file, idx}
200-
{#if typeof file === 'string'}
201-
{#if file.startsWith('data:image/')}
202-
<Image
203-
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
204-
src={file}
205-
alt="Image"
206-
/>
207-
{/if}
208-
{:else if typeof file === 'object'}
209-
{#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url}
210-
<Image
211-
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
212-
src={file.url}
213-
alt="Image"
214-
/>
215-
{/if}
216-
{/if}
217-
{/each}
218-
{/if}
219-
{/if}
220-
{:else}
22163
{#if title !== null}
22264
<!-- svelte-ignore a11y-no-static-element-interactions -->
22365
<!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -333,5 +175,4 @@
333175
</div>
334176
{/if}
335177
{/if}
336-
{/if}
337178
</div>
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<script lang="ts">
2+
import { decode } from 'html-entities';
3+
import { v4 as uuidv4 } from 'uuid';
4+
5+
import { getContext } from 'svelte';
6+
const i18n = getContext('i18n');
7+
8+
import { slide } from 'svelte/transition';
9+
import { quintOut } from 'svelte/easing';
10+
11+
import ChevronUp from '../icons/ChevronUp.svelte';
12+
import ChevronDown from '../icons/ChevronDown.svelte';
13+
import Spinner from './Spinner.svelte';
14+
import Markdown from '../chat/Messages/Markdown.svelte';
15+
import Image from './Image.svelte';
16+
import FullHeightIframe from './FullHeightIframe.svelte';
17+
18+
export let id: string = '';
19+
export let attributes: {
20+
type?: string;
21+
id?: string;
22+
name?: string;
23+
arguments?: string;
24+
result?: string;
25+
files?: string;
26+
embeds?: string;
27+
done?: string;
28+
} = {};
29+
30+
export let open = false;
31+
export let className = '';
32+
export let buttonClassName =
33+
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
34+
35+
const componentId = id || uuidv4();
36+
37+
function parseJSONString(str: string) {
38+
try {
39+
return parseJSONString(JSON.parse(str));
40+
} catch (e) {
41+
return str;
42+
}
43+
}
44+
45+
function formatJSONString(str: string) {
46+
try {
47+
const parsed = parseJSONString(str);
48+
// If parsed is an object/array, then it's valid JSON
49+
if (typeof parsed === 'object') {
50+
return JSON.stringify(parsed, null, 2);
51+
} else {
52+
// It's a primitive value like a number, boolean, etc.
53+
return `${JSON.stringify(String(parsed))}`;
54+
}
55+
} catch (e) {
56+
// Not valid JSON, return as-is
57+
return str;
58+
}
59+
}
60+
61+
// Decode and parse attributes
62+
$: args = decode(attributes?.arguments ?? '');
63+
$: result = decode(attributes?.result ?? '');
64+
$: files = parseJSONString(decode(attributes?.files ?? ''));
65+
$: embeds = parseJSONString(decode(attributes?.embeds ?? ''));
66+
$: isDone = attributes?.done === 'true';
67+
$: isExecuting = attributes?.done && attributes?.done !== 'true';
68+
</script>
69+
70+
<div {id} class={className}>
71+
{#if embeds && Array.isArray(embeds) && embeds.length > 0}
72+
<!-- Embed Mode: Show iframes without collapsible behavior -->
73+
<div class="py-1 w-full cursor-pointer">
74+
<div class="w-full text-xs text-gray-500">
75+
<div class="">
76+
{attributes.name}
77+
</div>
78+
</div>
79+
80+
{#each embeds as embed, idx}
81+
<div class="my-2" id={`${componentId}-tool-call-embed-${idx}`}>
82+
<FullHeightIframe
83+
src={embed}
84+
{args}
85+
allowScripts={true}
86+
allowForms={true}
87+
allowSameOrigin={true}
88+
allowPopups={true}
89+
/>
90+
</div>
91+
{/each}
92+
</div>
93+
{:else}
94+
<!-- Standard collapsible tool call display -->
95+
<div
96+
class="{buttonClassName} cursor-pointer"
97+
on:pointerup={() => {
98+
open = !open;
99+
}}
100+
>
101+
<div
102+
class="w-full font-medium flex items-center justify-between gap-2 {isExecuting
103+
? 'shimmer'
104+
: ''}"
105+
>
106+
{#if isExecuting}
107+
<div>
108+
<Spinner className="size-4" />
109+
</div>
110+
{/if}
111+
112+
<div class="">
113+
{#if isDone}
114+
<Markdown
115+
id={`${componentId}-tool-call-title`}
116+
content={$i18n.t('View Result from **{{NAME}}**', {
117+
NAME: attributes.name
118+
})}
119+
/>
120+
{:else}
121+
<Markdown
122+
id={`${componentId}-tool-call-executing`}
123+
content={$i18n.t('Executing **{{NAME}}**...', {
124+
NAME: attributes.name
125+
})}
126+
/>
127+
{/if}
128+
</div>
129+
130+
<div class="flex self-center translate-y-[1px]">
131+
{#if open}
132+
<ChevronUp strokeWidth="3.5" className="size-3.5" />
133+
{:else}
134+
<ChevronDown strokeWidth="3.5" className="size-3.5" />
135+
{/if}
136+
</div>
137+
</div>
138+
</div>
139+
140+
{#if open}
141+
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
142+
{#if isDone}
143+
<Markdown
144+
id={`${componentId}-tool-call-result`}
145+
content={`> \`\`\`json
146+
> ${formatJSONString(args)}
147+
> ${formatJSONString(result)}
148+
> \`\`\``}
149+
/>
150+
{:else}
151+
<Markdown
152+
id={`${componentId}-tool-call-args`}
153+
content={`> \`\`\`json
154+
> ${formatJSONString(args)}
155+
> \`\`\``}
156+
/>
157+
{/if}
158+
</div>
159+
{/if}
160+
{/if}
161+
162+
<!-- Files display (images etc.) when done -->
163+
{#if isDone}
164+
{#if typeof files === 'object'}
165+
{#each files ?? [] as file, idx}
166+
{#if typeof file === 'string'}
167+
{#if file.startsWith('data:image/')}
168+
<Image
169+
id={`${componentId}-tool-call-result-${idx}`}
170+
src={file}
171+
alt="Image"
172+
/>
173+
{/if}
174+
{:else if typeof file === 'object'}
175+
{#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url}
176+
<Image
177+
id={`${componentId}-tool-call-result-${idx}`}
178+
src={file.url}
179+
alt="Image"
180+
/>
181+
{/if}
182+
{/if}
183+
{/each}
184+
{/if}
185+
{/if}
186+
</div>

0 commit comments

Comments
 (0)