diff options
| author | Jay V <[email protected]> | 2025-05-28 18:32:38 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-05-28 18:32:40 -0400 |
| commit | 3d61cc5d2b6550aa22e2c2cad75b32a74b769559 (patch) | |
| tree | c95c6712fb3abdda597e020ccfd2ae9d5988f44d | |
| parent | a22a2f0f374dbcd7efe891a8fe881b9a3758f15a (diff) | |
| download | opencode-3d61cc5d2b6550aa22e2c2cad75b32a74b769559.tar.gz opencode-3d61cc5d2b6550aa22e2c2cad75b32a74b769559.zip | |
styling share
| -rw-r--r-- | app/packages/web/src/components/Share.tsx | 117 | ||||
| -rw-r--r-- | app/packages/web/src/components/icons/custom.tsx | 22 | ||||
| -rw-r--r-- | app/packages/web/src/components/share.module.css | 30 |
3 files changed, 148 insertions, 21 deletions
diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx index c1c82d4b3..aba324d2f 100644 --- a/app/packages/web/src/components/Share.tsx +++ b/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>—</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> diff --git a/app/packages/web/src/components/icons/custom.tsx b/app/packages/web/src/components/icons/custom.tsx new file mode 100644 index 000000000..f016b83cf --- /dev/null +++ b/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> + ) +} diff --git a/app/packages/web/src/components/share.module.css b/app/packages/web/src/components/share.module.css index 0b5a80bfd..fa4606f30 100644 --- a/app/packages/web/src/components/share.module.css +++ b/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; + } } } |
