styling tool calls

This commit is contained in:
Jay V 2025-05-29 14:24:21 -04:00
parent d398001f96
commit 7a29af4e30
2 changed files with 165 additions and 25 deletions

View file

@ -1,3 +1,4 @@
import { type JSX } from "solid-js"
import {
For,
Show,
@ -58,6 +59,39 @@ type SessionInfo = {
cost?: number
}
// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
const entries: Array<[string, any]> = [];
for (const [key, value] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
if (
value !== null &&
typeof value === "object" &&
!Array.isArray(value)
) {
entries.push(...flattenToolArgs(value, path));
}
else {
entries.push([path, value]);
}
}
return entries;
}
function getStatusText(status: [Status, string?]): string {
switch (status[0]) {
case "connected": return "Connected"
case "connecting": return "Connecting..."
case "disconnected": return "Disconnected"
case "reconnecting": return "Reconnecting..."
case "error": return status[1] || "Error"
default: return "Unknown"
}
}
function ProviderIcon(props: { provider: string, size?: number }) {
const size = props.size || 16
return (
@ -77,26 +111,18 @@ function ProviderIcon(props: { provider: string, size?: number }) {
)
}
function getStatusText(status: [Status, string?]): string {
switch (status[0]) {
case "connected": return "Connected"
case "connecting": return "Connecting..."
case "disconnected": return "Disconnected"
case "reconnecting": return "Reconnecting..."
case "error": return status[1] || "Error"
default: return "Unknown"
}
interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
highlight?: boolean
}
function TextPart(
props: { text: string, expand?: boolean, highlight?: boolean }
) {
function TextPart({ text, expand, highlight, ...props }: TextPartProps) {
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLPreElement | undefined
function checkOverflow() {
if (preEl && !props.expand) {
if (preEl && !expand) {
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
}
}
@ -107,7 +133,7 @@ function TextPart(
})
createEffect(() => {
props.text
text
setTimeout(checkOverflow, 0)
})
@ -118,10 +144,11 @@ function TextPart(
return (
<div
data-element-message-text
data-highlight={props.highlight}
data-expanded={expanded() || props.expand === true}
data-highlight={highlight}
data-expanded={expanded() || expand === true}
{...props}
>
<pre ref={el => (preEl = el)}>{props.text}</pre>
<pre ref={el => (preEl = el)}>{text}</pre>
{overflowed() &&
<button
type="button"
@ -461,7 +488,11 @@ export default function Share(props: { api: string }) {
<div></div>
</div>
<div data-section="content">
<span data-element-label data-part-title>
<span
data-size="md"
data-part-title
data-element-label
>
{assistant().providerID}
</span>
<span data-part-model>
@ -490,14 +521,73 @@ export default function Share(props: { api: string }) {
System
</span>
<TextPart
data-size="sm"
text={part().text}
expand={isLastPart()}
data-color="dimmed"
/>
<PartFooter time={time} />
</div>
</>
}
</Match>
{ /* Tool call */}
<Match when={
msg.role === "assistant"
&& part.type === "tool-invocation"
&& part
}>
{part =>
<>
<div data-section="decoration">
<div>
<IconWrenchScrewdriver width={18} height={18} />
</div>
<div></div>
</div>
<div data-section="content">
<span data-part-title data-size="md">
{part().toolInvocation.toolName}
</span>
<div data-part-tool-args>
<For each={
flattenToolArgs(part().toolInvocation.args)
}>
{([name, value]) =>
<>
<div></div>
<div>{name}</div>
<div>{value}</div>
</>
}
</For>
</div>
<Switch>
<Match when={
part().toolInvocation.state === "result"
&& part().toolInvocation.result
}>
<TextPart
data-size="sm"
data-color="dimmed"
text={part().toolInvocation.result}
expand={isLastPart()}
/>
</Match>
<Match when={
part().toolInvocation.state === "call"
}>
<TextPart
data-size="sm"
data-color="dimmed"
text="Calling..."
/>
</Match>
</Switch>
<PartFooter time={time} />
</div>
</>
}
</Match>
{ /* Fallback */}
<Match when={true}>
<div data-section="decoration">

View file

@ -122,7 +122,7 @@
[data-section="part"] {
display: flex;
gap: 0.5rem;
gap: 0.625rem;
}
[data-section="decoration"] {
@ -151,14 +151,18 @@
}
[data-section="content"] {
padding: 1px 0 0.375rem;
padding: 0 0 0.375rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
span[data-part-title] {
padding-top: 2px;
line-height: 18px;
font-size: 0.75rem;
&[data-size="md"] {
font-size: 0.875rem;
}
}
span[data-part-footer] {
@ -170,6 +174,37 @@
span[data-part-model] {
line-height: 1.5;
}
[data-part-tool-args] {
display: inline-grid;
align-items: center;
grid-template-columns: max-content max-content minmax(0, 1fr);
max-width: 100%;
gap: 0.25rem 0.375rem;
& > div:nth-child(3n+1) {
width: 8px;
height: 2px;
border-radius: 1px;
background: var(--sl-color-divider);
}
& > div:nth-child(3n+2),
& > div:nth-child(3n+3) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.75rem;
line-height: 1.5;
}
& > div:nth-child(3n+3) {
padding-left: 0.125rem;
color: var(--sl-color-text-dimmed);
}
}
}
}
@ -180,7 +215,6 @@
display: flex;
flex-direction: column;
align-items: flex-start;
color: var(--sl-color-text);
gap: 1rem;
pre {
@ -188,6 +222,19 @@
font-size: 0.875rem;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--sl-color-text);
}
&[data-size="sm"] {
pre {
font-size: 0.75rem;
}
}
&[data-color="dimmed"] {
pre {
color: var(--sl-color-text-dimmed);
}
}
button {
@ -198,7 +245,10 @@
&[data-highlight="true"] {
background-color: var(--sl-color-blue-high);
color: var(--sl-color-text-invert);
pre {
color: var(--sl-color-text-invert);
}
button {
opacity: 0.85;