mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
fix(session-ui): fade in streamed inline code (#44899)
This commit is contained in:
committed by
GitHub
Unverified
parent
1623ac3ba9
commit
b7aea8b0ef
@@ -41,6 +41,32 @@ test("keeps completed block text compact", () => {
|
||||
])
|
||||
})
|
||||
|
||||
test("marks streamed inline code for animation", () => {
|
||||
expect(parseMarkdownNodes("<p><code>file.ts:29-43</code></p>", true, true)).toEqual([
|
||||
{
|
||||
key: "0",
|
||||
type: "element",
|
||||
tag: "p",
|
||||
attributes: {},
|
||||
children: [
|
||||
{
|
||||
key: "0.0",
|
||||
type: "element",
|
||||
tag: "code",
|
||||
attributes: {},
|
||||
children: Array.from("file.ts:29-43", (text, index) => ({
|
||||
key: `0.0.0:${index}`,
|
||||
type: "word",
|
||||
text,
|
||||
animate: true,
|
||||
})),
|
||||
animate: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("marks words for animation only when requested", () => {
|
||||
expect(
|
||||
parseMarkdownNodes("<p>Hello</p>", true).flatMap((node) => (node.type === "element" ? node.children : [node]))[0],
|
||||
|
||||
@@ -3,7 +3,14 @@ import { createStore, reconcile } from "solid-js/store"
|
||||
import { Dynamic, render } from "solid-js/web"
|
||||
|
||||
type MarkdownNode =
|
||||
| { key: string; type: "element"; tag: string; attributes: Record<string, string>; children: MarkdownNode[] }
|
||||
| {
|
||||
key: string
|
||||
type: "element"
|
||||
tag: string
|
||||
attributes: Record<string, string>
|
||||
children: MarkdownNode[]
|
||||
animate?: true
|
||||
}
|
||||
| { key: string; type: "text"; text: string }
|
||||
| { key: string; type: "word"; text: string; animate?: true }
|
||||
|
||||
@@ -38,8 +45,12 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean })
|
||||
</span>
|
||||
)
|
||||
}
|
||||
let ref: HTMLElement | undefined
|
||||
onMount(() => {
|
||||
if (props.animate() && node.animate) ref?.setAttribute("data-markdown-enter", "")
|
||||
})
|
||||
return (
|
||||
<Dynamic component={node.tag} {...node.attributes}>
|
||||
<Dynamic component={node.tag} ref={ref} {...node.attributes}>
|
||||
<For each={node.children}>{(node) => <MarkdownDomNode node={node} animate={props.animate} />}</For>
|
||||
</Dynamic>
|
||||
)
|
||||
@@ -51,10 +62,10 @@ export function parseMarkdownNodes(html: string, words: boolean, animate = false
|
||||
return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words, animate))
|
||||
}
|
||||
|
||||
function parseNode(node: Node, key: string, words: boolean, animate: boolean): MarkdownNode[] {
|
||||
function parseNode(node: Node, key: string, words: boolean, animate: boolean, inlineCode = false): MarkdownNode[] {
|
||||
if (node instanceof Text) {
|
||||
if (!words) return [{ key, type: "text", text: node.data }]
|
||||
return node.data.split(/(\s+)/).flatMap((text, index): MarkdownNode[] => {
|
||||
return (inlineCode ? Array.from(node.data) : node.data.split(/(\s+)/)).flatMap((text, index): MarkdownNode[] => {
|
||||
if (!text) return []
|
||||
if (/^\s+$/.test(text)) return [{ key: `${key}:${index}`, type: "text", text }]
|
||||
return [{ key: `${key}:${index}`, type: "word", text, ...(animate ? { animate: true as const } : {}) }]
|
||||
@@ -67,7 +78,18 @@ function parseNode(node: Node, key: string, words: boolean, animate: boolean): M
|
||||
type: "element",
|
||||
tag: node.tagName.toLowerCase(),
|
||||
attributes: Object.fromEntries(Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value])),
|
||||
children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words, animate)),
|
||||
children: Array.from(node.childNodes).flatMap((child, index) =>
|
||||
parseNode(
|
||||
child,
|
||||
`${key}.${index}`,
|
||||
words,
|
||||
animate,
|
||||
inlineCode || (node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"),
|
||||
),
|
||||
),
|
||||
...(words && animate && node.tagName === "CODE" && node.parentElement?.tagName !== "PRE"
|
||||
? { animate: true as const }
|
||||
: {}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
display: inline;
|
||||
}
|
||||
|
||||
[data-markdown-word][data-markdown-enter] {
|
||||
:is([data-markdown-word], code)[data-markdown-enter] {
|
||||
animation: markdown-word-enter 180ms ease-out both;
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-component="markdown"] [data-markdown-word][data-markdown-enter] {
|
||||
[data-component="markdown"] :is([data-markdown-word], code)[data-markdown-enter] {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,3 +52,36 @@ export const StreamingResponse = {
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
function StreamingInlineCodeMarkdown() {
|
||||
const chunks = [
|
||||
"Updated ",
|
||||
"`apps/cloud",
|
||||
"flare/src/",
|
||||
"editor/Cloud",
|
||||
"Auth.ts:29",
|
||||
"-43` and ",
|
||||
"`packages/",
|
||||
"session-ui/src/",
|
||||
"components/markdown",
|
||||
"-solid.tsx`, ",
|
||||
"then verified ",
|
||||
"the changes with ",
|
||||
"`bun type",
|
||||
"check` and ",
|
||||
"`bun te",
|
||||
"st`.",
|
||||
]
|
||||
const [count, setCount] = createSignal(1)
|
||||
const timer = setInterval(() => setCount((value) => (value >= chunks.length ? 1 : value + 1)), 220)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
return <Markdown text={chunks.slice(0, count()).join("")} streaming />
|
||||
}
|
||||
|
||||
export const StreamingInlineCode = {
|
||||
render: () => (
|
||||
<div class="mx-auto max-w-[760px] rounded-lg border border-border-weak-base bg-background-base px-5 py-4">
|
||||
<StreamingInlineCodeMarkdown />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user