mirror of
https://github.com/sst/opencode.git
synced 2025-08-29 01:14:06 +00:00
refactor share
This commit is contained in:
parent
9d7c5efb9b
commit
041a080a13
2 changed files with 153 additions and 75 deletions
|
@ -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>
|
||||
{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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue