refactor share

This commit is contained in:
Jay V 2025-05-28 16:42:30 -04:00
parent 9d7c5efb9b
commit 041a080a13
2 changed files with 153 additions and 75 deletions

View file

@ -54,7 +54,7 @@ function getPartTitle(role: string, type: string): string | undefined {
: role === "user" : role === "user"
? undefined ? undefined
: type === "text" : type === "text"
? "AI" ? undefined
: type : 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 [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false); const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLPreElement | undefined; let preEl: HTMLPreElement | undefined
const checkOverflow = () => { function checkOverflow() {
if (preEl) { if (preEl && !props.expand) {
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1); setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
}
} }
};
onMount(() => { onMount(() => {
checkOverflow(); checkOverflow()
window.addEventListener('resize', checkOverflow); window.addEventListener("resize", checkOverflow)
}); })
createEffect(() => { createEffect(() => {
props.text; props.text
setTimeout(checkOverflow, 0); setTimeout(checkOverflow, 0)
}); })
onCleanup(() => { onCleanup(() => {
window.removeEventListener('resize', checkOverflow); window.removeEventListener("resize", checkOverflow)
}); })
return ( return (
<div <div
data-element-message-text data-element-message-text
data-expanded={expanded()}
data-highlight={props.highlight} data-highlight={props.highlight}
data-expanded={expanded() || props.expand === true}
> >
<pre ref={el => (preEl = el)}>{props.text}</pre> <pre ref={el => (preEl = el)}>{props.text}</pre>
{overflowed() && {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 }) { export default function Share(props: { api: string }) {
let params = new URLSearchParams(document.location.search) let params = new URLSearchParams(document.location.search)
const sessionId = params.get("id") 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 metrics = createMemo(() => {
const result = { const result = {
cost: 0, cost: 0,
@ -268,8 +270,8 @@ export default function Share(props: { api: string }) {
<ul data-section="stats"> <ul data-section="stats">
<li> <li>
<span data-element-label>Cost</span> <span data-element-label>Cost</span>
{metrics().cost ? {metrics().cost !== undefined ?
<span>{metrics().cost}</span> <span>${metrics().cost.toFixed(2)}</span>
: :
<span data-placeholder>&mdash;</span> <span data-placeholder>&mdash;</span>
} }
@ -324,20 +326,103 @@ export default function Share(props: { api: string }) {
> >
<div class={styles.parts}> <div class={styles.parts}>
<For each={messages()}> <For each={messages()}>
{(msg) => ( {(msg, msgIndex) => (
<For each={msg.parts}> <For each={msg.parts}>
{(part) => ( {(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 <div
data-section="part" data-section="part"
data-message-role={msg.role}
data-part-type={part.type} data-part-type={part.type}
data-message-role={msg.role}
> >
<Switch>
{ /* User text */}
<Match when={
msg.role === "user" && part.type === "text" && part
}>
{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 data-section="decoration">
<div> <div>
<Switch fallback={ <Switch fallback={
<IconWrenchScrewdriver width={16} height={16} /> <IconWrenchScrewdriver width={16} height={16} />
}> }>
<Match when={msg.role === "assistant" && (part.type === "text" || part.type === "step-start")}> <Match when={msg.role === "assistant" && part.type !== "tool-invocation"}>
<IconSparkles width={18} height={18} /> <IconSparkles width={18} height={18} />
</Match> </Match>
<Match when={msg.role === "system"}> <Match when={msg.role === "system"}>
@ -351,27 +436,15 @@ export default function Share(props: { api: string }) {
<div></div> <div></div>
</div> </div>
<div data-section="content"> <div data-section="content">
{getPartTitle(msg.role, part.type) <span data-element-label>{part.type}</span>
? <span data-element-label> <TextPart text={JSON.stringify(part, null, 2)} />
{getPartTitle(msg.role, part.type)} <PartFooter time={time} />
</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
)}
</div> </div>
</Match>
</Switch>
</div> </div>
)} )
}}
</For> </For>
)} )}
</For> </For>

View file

@ -45,8 +45,11 @@
h1 { h1 {
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 500; font-weight: 500;
line-height: 1.125;
letter-spacing: -0.05em;
} }
p { p {
flex: 0 0 auto;
display: flex; display: flex;
gap: 0.375rem; gap: 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
@ -131,16 +134,18 @@
} }
[data-section="content"] { [data-section="content"] {
padding: 3px 0 0.375rem; padding: 1px 0 0.375rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
span:first-child { span:first-child {
padding-top: 2px;
font-size: 0.75rem; font-size: 0.75rem;
} }
span:last-child { span:last-child {
align-self: flex-start;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--sl-color-text-dimmed); color: var(--sl-color-text-dimmed);
} }