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"
? 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>&mdash;</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>
{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 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
)}
</div>
</div>
)}
)
}}
</For>
)}
</For>

View file

@ -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);
}