diff options
| author | Jay V <[email protected]> | 2025-05-28 16:42:30 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-05-28 16:42:44 -0400 |
| commit | 041a080a139a06402d9c0ce4d37622f9eb49e729 (patch) | |
| tree | 9744525a8f46968bfccb7e6fd1ab76fbb02d2bdf /app | |
| parent | 9d7c5efb9b0b60c62aef3777b65b458a31ebbc88 (diff) | |
| download | opencode-041a080a139a06402d9c0ce4d37622f9eb49e729.tar.gz opencode-041a080a139a06402d9c0ce4d37622f9eb49e729.zip | |
refactor share
Diffstat (limited to 'app')
| -rw-r--r-- | app/packages/web/src/components/Share.tsx | 221 | ||||
| -rw-r--r-- | app/packages/web/src/components/share.module.css | 7 |
2 files changed, 153 insertions, 75 deletions
diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx index 78f901f67..c1c82d4b3 100644 --- a/app/packages/web/src/components/Share.tsx +++ b/app/packages/web/src/components/Share.tsx @@ -54,7 +54,7 @@ function getPartTitle(role: string, type: string): string | undefined { : role === "user" ? undefined : type === "text" - ? "AI" + ? undefined : type } @@ -69,36 +69,38 @@ function getStatusText(status: [Status, string?]): string { } } -function TextPart(props: { text: string, highlight?: boolean }) { +function TextPart( + props: { text: string, expand?: boolean, highlight?: boolean } +) { const [expanded, setExpanded] = createSignal(false) - const [overflowed, setOverflowed] = createSignal(false); - let preEl: HTMLPreElement | undefined; + const [overflowed, setOverflowed] = createSignal(false) + let preEl: HTMLPreElement | undefined - const checkOverflow = () => { - if (preEl) { - setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1); + function checkOverflow() { + if (preEl && !props.expand) { + setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1) } - }; + } onMount(() => { - checkOverflow(); - window.addEventListener('resize', checkOverflow); - }); + checkOverflow() + window.addEventListener("resize", checkOverflow) + }) createEffect(() => { - props.text; - setTimeout(checkOverflow, 0); - }); + props.text + setTimeout(checkOverflow, 0) + }) onCleanup(() => { - window.removeEventListener('resize', checkOverflow); - }); + window.removeEventListener("resize", checkOverflow) + }) return ( <div data-element-message-text - data-expanded={expanded()} data-highlight={props.highlight} + data-expanded={expanded() || props.expand === true} > <pre ref={el => (preEl = el)}>{props.text}</pre> {overflowed() && @@ -114,6 +116,16 @@ function TextPart(props: { text: string, highlight?: boolean }) { ) } +function PartFooter(props: { time: number }) { + return ( + <span title={ + DateTime.fromMillis(props.time).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS) + }> + {DateTime.fromMillis(props.time).toLocaleString(DateTime.TIME_WITH_SECONDS)} + </span> + ) +} + export default function Share(props: { api: string }) { let params = new URLSearchParams(document.location.search) const sessionId = params.get("id") @@ -224,16 +236,6 @@ export default function Share(props: { api: string }) { }) }) - function renderTime(time: number) { - return ( - <span title={ - DateTime.fromMillis(time).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS) - }> - {DateTime.fromMillis(time).toLocaleString(DateTime.TIME_WITH_SECONDS)} - </span> - ) - } - const metrics = createMemo(() => { const result = { cost: 0, @@ -268,8 +270,8 @@ export default function Share(props: { api: string }) { <ul data-section="stats"> <li> <span data-element-label>Cost</span> - {metrics().cost ? - <span>{metrics().cost}</span> + {metrics().cost !== undefined ? + <span>${metrics().cost.toFixed(2)}</span> : <span data-placeholder>—</span> } @@ -324,54 +326,125 @@ export default function Share(props: { api: string }) { > <div class={styles.parts}> <For each={messages()}> - {(msg) => ( + {(msg, msgIndex) => ( <For each={msg.parts}> - {(part) => ( - <div - data-section="part" - data-message-role={msg.role} - data-part-type={part.type} - > - <div data-section="decoration"> - <div> - <Switch fallback={ - <IconWrenchScrewdriver width={16} height={16} /> + {(part, partIndex) => { + const isLastPart = createMemo(() => + (messages().length === msgIndex() + 1) + && (msg.parts.length === partIndex() + 1) + ) + const time = msg.metadata?.time.completed + || msg.metadata?.time.created + || 0 + return ( + <div + data-section="part" + data-part-type={part.type} + data-message-role={msg.role} + > + <Switch> + { /* User text */} + <Match when={ + msg.role === "user" && part.type === "text" && part }> - <Match when={msg.role === "assistant" && (part.type === "text" || part.type === "step-start")}> - <IconSparkles width={18} height={18} /> - </Match> - <Match when={msg.role === "system"}> - <IconCpuChip width={18} height={18} /> - </Match> - <Match when={msg.role === "user"}> - <IconUserCircle width={18} height={18} /> - </Match> - </Switch> - </div> - <div></div> - </div> - <div data-section="content"> - {getPartTitle(msg.role, part.type) - ? <span data-element-label> - {getPartTitle(msg.role, part.type)} - </span> - : null - } - {part.type === "text" - ? <TextPart - text={part.text} - highlight={msg.role === "user"} - /> - : <TextPart text={JSON.stringify(part, null, 2)} /> - } - {renderTime( - msg.metadata?.time.completed - || msg.metadata?.time.created - || 0 - )} + {part => + <> + <div data-section="decoration"> + <div> + <IconUserCircle width={18} height={18} /> + </div> + <div></div> + </div> + <div data-section="content"> + <TextPart + highlight + text={part().text} + expand={isLastPart()} + /> + <PartFooter time={time} /> + </div> + </> + } + </Match> + { /* AI text */} + <Match when={ + msg.role === "assistant" + && part.type === "text" + && part + }> + {part => + <> + <div data-section="decoration"> + <div><IconSparkles width={18} height={18} /></div> + <div></div> + </div> + <div data-section="content"> + <TextPart + text={part().text} + expand={isLastPart()} + /> + <PartFooter time={time} /> + </div> + </> + } + </Match> + { /* System text */} + <Match when={ + msg.role === "system" + && part.type === "text" + && part + }> + {part => + <> + <div data-section="decoration"> + <div> + <IconCpuChip width={18} height={18} /> + </div> + <div></div> + </div> + <div data-section="content"> + <span data-element-label>System</span> + <TextPart + text={part().text} + expand={isLastPart()} + /> + <PartFooter time={time} /> + </div> + </> + } + </Match> + { /* Step start */} + <Match when={part.type === "step-start"}>{null}</Match> + { /* Fallback */} + <Match when={true}> + <div data-section="decoration"> + <div> + <Switch fallback={ + <IconWrenchScrewdriver width={16} height={16} /> + }> + <Match when={msg.role === "assistant" && part.type !== "tool-invocation"}> + <IconSparkles width={18} height={18} /> + </Match> + <Match when={msg.role === "system"}> + <IconCpuChip width={18} height={18} /> + </Match> + <Match when={msg.role === "user"}> + <IconUserCircle width={18} height={18} /> + </Match> + </Switch> + </div> + <div></div> + </div> + <div data-section="content"> + <span data-element-label>{part.type}</span> + <TextPart text={JSON.stringify(part, null, 2)} /> + <PartFooter time={time} /> + </div> + </Match> + </Switch> </div> - </div> - )} + ) + }} </For> )} </For> diff --git a/app/packages/web/src/components/share.module.css b/app/packages/web/src/components/share.module.css index 63a6232bc..0b5a80bfd 100644 --- a/app/packages/web/src/components/share.module.css +++ b/app/packages/web/src/components/share.module.css @@ -45,8 +45,11 @@ h1 { font-size: 1.75rem; font-weight: 500; + line-height: 1.125; + letter-spacing: -0.05em; } p { + flex: 0 0 auto; display: flex; gap: 0.375rem; font-size: 0.75rem; @@ -131,16 +134,18 @@ } [data-section="content"] { - padding: 3px 0 0.375rem; + padding: 1px 0 0.375rem; display: flex; flex-direction: column; gap: 0.5rem; span:first-child { + padding-top: 2px; font-size: 0.75rem; } span:last-child { + align-self: flex-start; font-size: 0.75rem; color: var(--sl-color-text-dimmed); } |
