refac
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
import KatexRenderer from './KatexRenderer.svelte';
|
import KatexRenderer from './KatexRenderer.svelte';
|
||||||
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
|
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
|
||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
|
import ToolCallDisplay from '$lib/components/common/ToolCallDisplay.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Download from '$lib/components/icons/Download.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
|
|
||||||
@@ -323,7 +324,15 @@
|
|||||||
.replace(/<summary>.*?<\/summary>/gi, '')
|
.replace(/<summary>.*?<\/summary>/gi, '')
|
||||||
.trim()}
|
.trim()}
|
||||||
|
|
||||||
{#if textContent.length > 0}
|
{#if token?.attributes?.type === 'tool_calls'}
|
||||||
|
<!-- Tool calls have dedicated handling with ToolCallDisplay component -->
|
||||||
|
<ToolCallDisplay
|
||||||
|
id={`${id}-${tokenIdx}-tc`}
|
||||||
|
attributes={token.attributes}
|
||||||
|
open={false}
|
||||||
|
className="w-full space-y-1"
|
||||||
|
/>
|
||||||
|
{:else if textContent.length > 0}
|
||||||
<Collapsible
|
<Collapsible
|
||||||
title={token.summary}
|
title={token.summary}
|
||||||
open={$settings?.expandDetails ?? false}
|
open={$settings?.expandDetails ?? false}
|
||||||
|
|||||||
@@ -35,11 +35,6 @@
|
|||||||
import ChevronUp from '../icons/ChevronUp.svelte';
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
import CodeBlock from '../chat/Messages/CodeBlock.svelte';
|
|
||||||
import Markdown from '../chat/Messages/Markdown.svelte';
|
|
||||||
import Image from './Image.svelte';
|
|
||||||
import FullHeightIframe from './FullHeightIframe.svelte';
|
|
||||||
import { settings } from '$lib/stores';
|
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
@@ -62,162 +57,9 @@
|
|||||||
$: onChange(open);
|
$: onChange(open);
|
||||||
|
|
||||||
const collapsibleId = uuidv4();
|
const collapsibleId = uuidv4();
|
||||||
|
|
||||||
function parseJSONString(str) {
|
|
||||||
try {
|
|
||||||
return parseJSONString(JSON.parse(str));
|
|
||||||
} catch (e) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatJSONString(str) {
|
|
||||||
try {
|
|
||||||
const parsed = parseJSONString(str);
|
|
||||||
// If parsed is an object/array, then it's valid JSON
|
|
||||||
if (typeof parsed === 'object') {
|
|
||||||
return JSON.stringify(parsed, null, 2);
|
|
||||||
} else {
|
|
||||||
// It's a primitive value like a number, boolean, etc.
|
|
||||||
return `${JSON.stringify(String(parsed))}`;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Not valid JSON, return as-is
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {id} class={className}>
|
<div {id} class={className}>
|
||||||
{#if attributes?.type === 'tool_calls'}
|
|
||||||
{@const args = decode(attributes?.arguments)}
|
|
||||||
{@const result = decode(attributes?.result ?? '')}
|
|
||||||
{@const files = parseJSONString(decode(attributes?.files ?? ''))}
|
|
||||||
{@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
|
|
||||||
|
|
||||||
{#if embeds && Array.isArray(embeds) && embeds.length > 0}
|
|
||||||
<div class="py-1 w-full cursor-pointer">
|
|
||||||
<div class=" w-full text-xs text-gray-500">
|
|
||||||
<div class="">
|
|
||||||
{attributes.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#each embeds as embed, idx}
|
|
||||||
<div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
|
|
||||||
<FullHeightIframe
|
|
||||||
src={embed}
|
|
||||||
{args}
|
|
||||||
allowScripts={true}
|
|
||||||
allowForms={true}
|
|
||||||
allowSameOrigin={true}
|
|
||||||
allowPopups={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="{buttonClassName} cursor-pointer"
|
|
||||||
on:pointerup={() => {
|
|
||||||
if (!disabled) {
|
|
||||||
open = !open;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
|
|
||||||
attributes?.done !== 'true'
|
|
||||||
? 'shimmer'
|
|
||||||
: ''}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{#if attributes?.done && attributes?.done !== 'true'}
|
|
||||||
<div>
|
|
||||||
<Spinner className="size-4" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="">
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}`}
|
|
||||||
content={$i18n.t('View Result from **{{NAME}}**', {
|
|
||||||
NAME: attributes.name
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
|
|
||||||
content={$i18n.t('Executing **{{NAME}}**...', {
|
|
||||||
NAME: attributes.name
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex self-center translate-y-[1px]">
|
|
||||||
{#if open}
|
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
|
||||||
{:else}
|
|
||||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if !grow}
|
|
||||||
{#if open && !hide}
|
|
||||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
|
||||||
{#if attributes?.type === 'tool_calls'}
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
|
||||||
content={`> \`\`\`json
|
|
||||||
> ${formatJSONString(args)}
|
|
||||||
> ${formatJSONString(result)}
|
|
||||||
> \`\`\``}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
|
||||||
content={`> \`\`\`json
|
|
||||||
> ${formatJSONString(args)}
|
|
||||||
> \`\`\``}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<slot name="content" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
{#if typeof files === 'object'}
|
|
||||||
{#each files ?? [] as file, idx}
|
|
||||||
{#if typeof file === 'string'}
|
|
||||||
{#if file.startsWith('data:image/')}
|
|
||||||
<Image
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
|
|
||||||
src={file}
|
|
||||||
alt="Image"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else if typeof file === 'object'}
|
|
||||||
{#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url}
|
|
||||||
<Image
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
|
|
||||||
src={file.url}
|
|
||||||
alt="Image"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
{#if title !== null}
|
{#if title !== null}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
@@ -333,5 +175,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { decode } from 'html-entities';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { quintOut } from 'svelte/easing';
|
||||||
|
|
||||||
|
import ChevronUp from '../icons/ChevronUp.svelte';
|
||||||
|
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||||
|
import Spinner from './Spinner.svelte';
|
||||||
|
import Markdown from '../chat/Messages/Markdown.svelte';
|
||||||
|
import Image from './Image.svelte';
|
||||||
|
import FullHeightIframe from './FullHeightIframe.svelte';
|
||||||
|
|
||||||
|
export let id: string = '';
|
||||||
|
export let attributes: {
|
||||||
|
type?: string;
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
arguments?: string;
|
||||||
|
result?: string;
|
||||||
|
files?: string;
|
||||||
|
embeds?: string;
|
||||||
|
done?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
export let className = '';
|
||||||
|
export let buttonClassName =
|
||||||
|
'w-fit text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition';
|
||||||
|
|
||||||
|
const componentId = id || uuidv4();
|
||||||
|
|
||||||
|
function parseJSONString(str: string) {
|
||||||
|
try {
|
||||||
|
return parseJSONString(JSON.parse(str));
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJSONString(str: string) {
|
||||||
|
try {
|
||||||
|
const parsed = parseJSONString(str);
|
||||||
|
// If parsed is an object/array, then it's valid JSON
|
||||||
|
if (typeof parsed === 'object') {
|
||||||
|
return JSON.stringify(parsed, null, 2);
|
||||||
|
} else {
|
||||||
|
// It's a primitive value like a number, boolean, etc.
|
||||||
|
return `${JSON.stringify(String(parsed))}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not valid JSON, return as-is
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and parse attributes
|
||||||
|
$: args = decode(attributes?.arguments ?? '');
|
||||||
|
$: result = decode(attributes?.result ?? '');
|
||||||
|
$: files = parseJSONString(decode(attributes?.files ?? ''));
|
||||||
|
$: embeds = parseJSONString(decode(attributes?.embeds ?? ''));
|
||||||
|
$: isDone = attributes?.done === 'true';
|
||||||
|
$: isExecuting = attributes?.done && attributes?.done !== 'true';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {id} class={className}>
|
||||||
|
{#if embeds && Array.isArray(embeds) && embeds.length > 0}
|
||||||
|
<!-- Embed Mode: Show iframes without collapsible behavior -->
|
||||||
|
<div class="py-1 w-full cursor-pointer">
|
||||||
|
<div class="w-full text-xs text-gray-500">
|
||||||
|
<div class="">
|
||||||
|
{attributes.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each embeds as embed, idx}
|
||||||
|
<div class="my-2" id={`${componentId}-tool-call-embed-${idx}`}>
|
||||||
|
<FullHeightIframe
|
||||||
|
src={embed}
|
||||||
|
{args}
|
||||||
|
allowScripts={true}
|
||||||
|
allowForms={true}
|
||||||
|
allowSameOrigin={true}
|
||||||
|
allowPopups={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Standard collapsible tool call display -->
|
||||||
|
<div
|
||||||
|
class="{buttonClassName} cursor-pointer"
|
||||||
|
on:pointerup={() => {
|
||||||
|
open = !open;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full font-medium flex items-center justify-between gap-2 {isExecuting
|
||||||
|
? 'shimmer'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
|
{#if isExecuting}
|
||||||
|
<div>
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
{#if isDone}
|
||||||
|
<Markdown
|
||||||
|
id={`${componentId}-tool-call-title`}
|
||||||
|
content={$i18n.t('View Result from **{{NAME}}**', {
|
||||||
|
NAME: attributes.name
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Markdown
|
||||||
|
id={`${componentId}-tool-call-executing`}
|
||||||
|
content={$i18n.t('Executing **{{NAME}}**...', {
|
||||||
|
NAME: attributes.name
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex self-center translate-y-[1px]">
|
||||||
|
{#if open}
|
||||||
|
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||||
|
{#if isDone}
|
||||||
|
<Markdown
|
||||||
|
id={`${componentId}-tool-call-result`}
|
||||||
|
content={`> \`\`\`json
|
||||||
|
> ${formatJSONString(args)}
|
||||||
|
> ${formatJSONString(result)}
|
||||||
|
> \`\`\``}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Markdown
|
||||||
|
id={`${componentId}-tool-call-args`}
|
||||||
|
content={`> \`\`\`json
|
||||||
|
> ${formatJSONString(args)}
|
||||||
|
> \`\`\``}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Files display (images etc.) when done -->
|
||||||
|
{#if isDone}
|
||||||
|
{#if typeof files === 'object'}
|
||||||
|
{#each files ?? [] as file, idx}
|
||||||
|
{#if typeof file === 'string'}
|
||||||
|
{#if file.startsWith('data:image/')}
|
||||||
|
<Image
|
||||||
|
id={`${componentId}-tool-call-result-${idx}`}
|
||||||
|
src={file}
|
||||||
|
alt="Image"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else if typeof file === 'object'}
|
||||||
|
{#if (file.type === 'image' || (file?.content_type ?? '').startsWith('image/')) && file.url}
|
||||||
|
<Image
|
||||||
|
id={`${componentId}-tool-call-result-${idx}`}
|
||||||
|
src={file.url}
|
||||||
|
alt="Image"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user