Browse Source

styling tool calls

Jay V 9 months ago
parent
commit
7a29af4e30

+ 110 - 20
app/packages/web/src/components/Share.tsx

@@ -1,3 +1,4 @@
+import { type JSX } from "solid-js"
 import {
 import {
   For,
   For,
   Show,
   Show,
@@ -58,6 +59,39 @@ type SessionInfo = {
   cost?: number
   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 }) {
 function ProviderIcon(props: { provider: string, size?: number }) {
   const size = props.size || 16
   const size = props.size || 16
   return (
   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 [expanded, setExpanded] = createSignal(false)
   const [overflowed, setOverflowed] = createSignal(false)
   const [overflowed, setOverflowed] = createSignal(false)
   let preEl: HTMLPreElement | undefined
   let preEl: HTMLPreElement | undefined
 
 
   function checkOverflow() {
   function checkOverflow() {
-    if (preEl && !props.expand) {
+    if (preEl && !expand) {
       setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
       setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
     }
     }
   }
   }
@@ -107,7 +133,7 @@ function TextPart(
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
-    props.text
+    text
     setTimeout(checkOverflow, 0)
     setTimeout(checkOverflow, 0)
   })
   })
 
 
@@ -118,10 +144,11 @@ function TextPart(
   return (
   return (
     <div
     <div
       data-element-message-text
       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() &&
       {overflowed() &&
         <button
         <button
           type="button"
           type="button"
@@ -461,7 +488,11 @@ export default function Share(props: { api: string }) {
                                   <div></div>
                                   <div></div>
                                 </div>
                                 </div>
                                 <div data-section="content">
                                 <div data-section="content">
-                                  <span data-element-label data-part-title>
+                                  <span
+                                    data-size="md"
+                                    data-part-title
+                                    data-element-label
+                                  >
                                     {assistant().providerID}
                                     {assistant().providerID}
                                   </span>
                                   </span>
                                   <span data-part-model>
                                   <span data-part-model>
@@ -490,14 +521,73 @@ export default function Share(props: { api: string }) {
                                     System
                                     System
                                   </span>
                                   </span>
                                   <TextPart
                                   <TextPart
+                                    data-size="sm"
                                     text={part().text}
                                     text={part().text}
-                                    expand={isLastPart()}
+                                    data-color="dimmed"
                                   />
                                   />
                                   <PartFooter time={time} />
                                   <PartFooter time={time} />
                                 </div>
                                 </div>
                               </>
                               </>
                             }
                             }
                           </Match>
                           </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 */}
                           { /* Fallback */}
                           <Match when={true}>
                           <Match when={true}>
                             <div data-section="decoration">
                             <div data-section="decoration">

+ 55 - 5
app/packages/web/src/components/share.module.css

@@ -122,7 +122,7 @@
 
 
   [data-section="part"] {
   [data-section="part"] {
     display: flex;
     display: flex;
-    gap: 0.5rem;
+    gap: 0.625rem;
   }
   }
 
 
   [data-section="decoration"] {
   [data-section="decoration"] {
@@ -151,14 +151,18 @@
   }
   }
 
 
   [data-section="content"] {
   [data-section="content"] {
-    padding: 1px 0 0.375rem;
+    padding: 0 0 0.375rem;
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     gap: 0.5rem;
     gap: 0.5rem;
 
 
     span[data-part-title] {
     span[data-part-title] {
-      padding-top: 2px;
+      line-height: 18px;
       font-size: 0.75rem;
       font-size: 0.75rem;
+
+      &[data-size="md"] {
+        font-size: 0.875rem;
+      }
     }
     }
 
 
     span[data-part-footer] {
     span[data-part-footer] {
@@ -170,6 +174,37 @@
     span[data-part-model] {
     span[data-part-model] {
       line-height: 1.5;
       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;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: flex-start;
   align-items: flex-start;
-  color: var(--sl-color-text);
   gap: 1rem;
   gap: 1rem;
 
 
   pre {
   pre {
@@ -188,6 +222,19 @@
     font-size: 0.875rem;
     font-size: 0.875rem;
     white-space: pre-wrap;
     white-space: pre-wrap;
     overflow-wrap: anywhere;
     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 {
   button {
@@ -198,7 +245,10 @@
 
 
   &[data-highlight="true"] {
   &[data-highlight="true"] {
     background-color: var(--sl-color-blue-high);
     background-color: var(--sl-color-blue-high);
-    color: var(--sl-color-text-invert);
+
+    pre {
+      color: var(--sl-color-text-invert);
+    }
 
 
     button {
     button {
       opacity: 0.85;
       opacity: 0.85;