Browse Source

styling share

Jay V 9 months ago
parent
commit
3d61cc5d2b

+ 101 - 16
app/packages/web/src/components/Share.tsx

@@ -10,7 +10,17 @@ import {
   createSignal,
 } from "solid-js"
 import { DateTime } from "luxon"
-import { IconCpuChip, IconSparkles, IconUserCircle, IconWrenchScrewdriver } from "./icons"
+import {
+  IconOpenAI,
+  IconGemini,
+  IconAnthropic,
+} from "./icons/custom"
+import {
+  IconCpuChip,
+  IconSparkles,
+  IconUserCircle,
+  IconWrenchScrewdriver,
+} from "./icons"
 import styles from "./share.module.css"
 import { type UIMessage } from "ai"
 import { createStore, reconcile } from "solid-js/store"
@@ -48,14 +58,23 @@ type SessionInfo = {
   cost?: number
 }
 
-function getPartTitle(role: string, type: string): string | undefined {
-  return role === "system"
-    ? role
-    : role === "user"
-      ? undefined
-      : type === "text"
-        ? undefined
-        : type
+function ProviderIcon(props: { provider: string, size?: number }) {
+  const size = props.size || 16
+  return (
+    <Switch fallback={
+      <IconSparkles width={size} height={size} />
+    }>
+      <Match when={props.provider === "openai"}>
+        <IconOpenAI width={size} height={size} />
+      </Match>
+      <Match when={props.provider === "anthropic"}>
+        <IconAnthropic width={size} height={size} />
+      </Match>
+      <Match when={props.provider === "gemini"}>
+        <IconGemini width={size} height={size} />
+      </Match>
+    </Switch>
+  )
 }
 
 function getStatusText(status: [Status, string?]): string {
@@ -118,9 +137,14 @@ function TextPart(
 
 function PartFooter(props: { time: number }) {
   return (
-    <span title={
-      DateTime.fromMillis(props.time).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)
-    }>
+    <span
+      data-part-footer
+      title={
+        DateTime.fromMillis(props.time).toLocaleString(
+          DateTime.DATETIME_FULL_WITH_SECONDS
+        )
+      }
+    >
       {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)}
     </span>
   )
@@ -236,6 +260,16 @@ export default function Share(props: { api: string }) {
     })
   })
 
+  const models = createMemo(() => {
+    const result: string[][] = []
+    for (const msg of messages()) {
+      if (msg.role === "assistant" && msg.metadata?.assistant) {
+        result.push([msg.metadata.assistant.providerID, msg.metadata.assistant.modelID])
+      }
+    }
+    return result
+  })
+
   const metrics = createMemo(() => {
     const result = {
       cost: 0,
@@ -301,6 +335,25 @@ export default function Share(props: { api: string }) {
               }
             </li>
           </ul>
+          <ul data-section="stats" data-section-models>
+            {models().length > 0 ?
+              <For each={Array.from(models())}>
+                {([provider, model]) => (
+                  <li>
+                    <div data-stat-model-icon title={provider}>
+                      <ProviderIcon provider={provider} />
+                    </div>
+                    <span data-stat-model>{model}</span>
+                  </li>
+                )}
+              </For>
+              :
+              <li>
+                <span data-element-label>Models</span>
+                <span data-placeholder>&mdash;</span>
+              </li>
+            }
+          </ul>
           <div data-section="date">
             {messages().length > 0 && messages()[0].metadata?.time.created ?
               <span title={
@@ -329,6 +382,8 @@ export default function Share(props: { api: string }) {
               {(msg, msgIndex) => (
                 <For each={msg.parts}>
                   {(part, partIndex) => {
+                    if (part.type === "step-start" && (partIndex() > 0 || !msg.metadata?.assistant)) return null
+
                     const isLastPart = createMemo(() =>
                       (messages().length === msgIndex() + 1)
                       && (msg.parts.length === partIndex() + 1)
@@ -388,6 +443,34 @@ export default function Share(props: { api: string }) {
                               </>
                             }
                           </Match>
+                          { /* AI model */}
+                          <Match when={
+                            msg.role === "assistant"
+                            && part.type === "step-start"
+                            && msg.metadata?.assistant
+                          }>
+                            {assistant =>
+                              <>
+                                <div data-section="decoration">
+                                  <div>
+                                    <ProviderIcon
+                                      size={18}
+                                      provider={assistant().providerID}
+                                    />
+                                  </div>
+                                  <div></div>
+                                </div>
+                                <div data-section="content">
+                                  <span data-element-label data-part-title>
+                                    {assistant().providerID}
+                                  </span>
+                                  <span data-part-model>
+                                    {assistant().modelID}
+                                  </span>
+                                </div>
+                              </>
+                            }
+                          </Match>
                           { /* System text */}
                           <Match when={
                             msg.role === "system"
@@ -403,7 +486,9 @@ export default function Share(props: { api: string }) {
                                   <div></div>
                                 </div>
                                 <div data-section="content">
-                                  <span data-element-label>System</span>
+                                  <span data-element-label data-part-title>
+                                    System
+                                  </span>
                                   <TextPart
                                     text={part().text}
                                     expand={isLastPart()}
@@ -413,8 +498,6 @@ export default function Share(props: { api: string }) {
                               </>
                             }
                           </Match>
-                          { /* Step start */}
-                          <Match when={part.type === "step-start"}>{null}</Match>
                           { /* Fallback */}
                           <Match when={true}>
                             <div data-section="decoration">
@@ -436,7 +519,9 @@ export default function Share(props: { api: string }) {
                               <div></div>
                             </div>
                             <div data-section="content">
-                              <span data-element-label>{part.type}</span>
+                              <span data-element-label data-part-title>
+                                {part.type}
+                              </span>
                               <TextPart text={JSON.stringify(part, null, 2)} />
                               <PartFooter time={time} />
                             </div>

+ 22 - 0
app/packages/web/src/components/icons/custom.tsx

@@ -0,0 +1,22 @@
+import { type JSX } from "solid-js"
+
+// https://icones.js.org/collection/ri?s=openai&icon=ri:openai-fill
+export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+  return (
+    <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M20.562 10.188c.25-.688.313-1.376.25-2.063c-.062-.687-.312-1.375-.625-2c-.562-.937-1.375-1.687-2.312-2.125c-1-.437-2.063-.562-3.125-.312c-.5-.5-1.063-.938-1.688-1.25S11.687 2 11 2a5.17 5.17 0 0 0-3 .938c-.875.624-1.5 1.5-1.813 2.5c-.75.187-1.375.5-2 .875c-.562.437-1 1-1.375 1.562c-.562.938-.75 2-.625 3.063a5.44 5.44 0 0 0 1.25 2.874a4.7 4.7 0 0 0-.25 2.063c.063.688.313 1.375.625 2c.563.938 1.375 1.688 2.313 2.125c1 .438 2.062.563 3.125.313c.5.5 1.062.937 1.687 1.25S12.312 22 13 22a5.17 5.17 0 0 0 3-.937c.875-.625 1.5-1.5 1.812-2.5a4.54 4.54 0 0 0 1.938-.875c.562-.438 1.062-.938 1.375-1.563c.562-.937.75-2 .625-3.062c-.125-1.063-.5-2.063-1.188-2.876m-7.5 10.5c-1 0-1.75-.313-2.437-.875c0 0 .062-.063.125-.063l4-2.312a.5.5 0 0 0 .25-.25a.57.57 0 0 0 .062-.313V11.25l1.688 1v4.625a3.685 3.685 0 0 1-3.688 3.813M5 17.25c-.438-.75-.625-1.625-.438-2.5c0 0 .063.063.125.063l4 2.312a.56.56 0 0 0 .313.063c.125 0 .25 0 .312-.063l4.875-2.812v1.937l-4.062 2.375A3.7 3.7 0 0 1 7.312 19c-1-.25-1.812-.875-2.312-1.75M3.937 8.563a3.8 3.8 0 0 1 1.938-1.626v4.751c0 .124 0 .25.062.312a.5.5 0 0 0 .25.25l4.875 2.813l-1.687 1l-4-2.313a3.7 3.7 0 0 1-1.75-2.25c-.25-.937-.188-2.062.312-2.937M17.75 11.75l-4.875-2.812l1.687-1l4 2.312c.625.375 1.125.875 1.438 1.5s.5 1.313.437 2.063a3.7 3.7 0 0 1-.75 1.937c-.437.563-1 1-1.687 1.25v-4.75c0-.125 0-.25-.063-.312c0 0-.062-.126-.187-.188m1.687-2.5s-.062-.062-.125-.062l-4-2.313c-.125-.062-.187-.062-.312-.062s-.25 0-.313.062L9.812 9.688V7.75l4.063-2.375c.625-.375 1.312-.5 2.062-.5c.688 0 1.375.25 2 .688c.563.437 1.063 1 1.313 1.625s.312 1.375.187 2.062m-10.5 3.5l-1.687-1V7.063c0-.688.187-1.438.562-2C8.187 4.438 8.75 4 9.375 3.688a3.37 3.37 0 0 1 2.062-.313c.688.063 1.375.375 1.938.813c0 0-.063.062-.125.062l-4 2.313a.5.5 0 0 0-.25.25c-.063.125-.063.187-.063.312zm.875-2L12 9.5l2.187 1.25v2.5L12 14.5l-2.188-1.25z" /></svg>
+  )
+}
+
+// https://icones.js.org/collection/ri?s=anthropic&icon=ri:anthropic-fill
+export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+  return (
+    <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16.765 5h-3.308l5.923 15h3.23zM7.226 5L1.38 20h3.308l1.307-3.154h6.154l1.23 3.077h3.309L10.688 5zm-.308 9.077l2-5.308l2.077 5.308z" /></svg>
+  )
+}
+
+// https://icones.js.org/collection/ri?s=gemini&icon=ri:gemini-fill
+export function IconGemini(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+  return (
+    <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M24 12.024c-6.437.388-11.59 5.539-11.977 11.976h-.047C11.588 17.563 6.436 12.412 0 12.024v-.047C6.437 11.588 11.588 6.437 11.976 0h.047c.388 6.437 5.54 11.588 11.977 11.977z" /></svg>
+  )
+}

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

@@ -78,14 +78,29 @@
       gap: 0.5rem;
       font-size: 0.875rem;
 
-      span:last-child {
-        &[data-placeholder] {
+      span[data-placeholder] {
           color: var(--sl-color-text-dimmed);
-        }
       }
     }
   }
 
+  [data-section="stats"][data-section-models] {
+    gap: 0.5rem;
+
+    [data-stat-model-icon] {
+      flex: 0 0 auto;
+      color: var(--sl-color-text-dimmed);
+      opacity: 0.85;
+      svg {
+        display: block;
+      }
+    }
+
+    span[data-stat-model] {
+      color: var(sl-color-text);
+    }
+  }
+
   [data-section="date"] {
     span {
       font-size: 0.875rem;
@@ -139,16 +154,21 @@
     flex-direction: column;
     gap: 0.5rem;
 
-    span:first-child {
+    span[data-part-title] {
       padding-top: 2px;
       font-size: 0.75rem;
     }
 
-    span:last-child {
+    span[data-part-footer] {
       align-self: flex-start;
       font-size: 0.75rem;
       color: var(--sl-color-text-dimmed);
     }
+
+    span[data-part-model] {
+      line-height: 1.5;
+      font-weight: 500;
+    }
   }
 }