fix(ui): make streamed markdown feel more continuous (#19404)

pull/19434/head
Shoubhit Dash 2026-03-27 22:06:47 +05:30 committed by GitHub
parent af2ccc94eb
commit a93374c48f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 63 additions and 25 deletions

View File

@ -156,37 +156,75 @@ export type PartComponent = Component<MessagePartProps>
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
const TEXT_RENDER_THROTTLE_MS = 100
const TEXT_RENDER_PACE_MS = 24
const TEXT_RENDER_SNAP = /[\s.,!?;:)\]]/
function createThrottledValue(getValue: () => string) {
function step(size: number) {
if (size <= 12) return 2
if (size <= 48) return 4
if (size <= 96) return 8
return Math.min(24, Math.ceil(size / 8))
}
function next(text: string, start: number) {
const end = Math.min(text.length, start + step(text.length - start))
const max = Math.min(text.length, end + 8)
for (let i = end; i < max; i++) {
if (TEXT_RENDER_SNAP.test(text[i] ?? "")) return i + 1
}
return end
}
function createPacedValue(getValue: () => string, live?: () => boolean) {
const [value, setValue] = createSignal(getValue())
let shown = getValue()
let timeout: ReturnType<typeof setTimeout> | undefined
let last = 0
createEffect(() => {
const next = getValue()
const now = Date.now()
const clear = () => {
if (!timeout) return
clearTimeout(timeout)
timeout = undefined
}
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout)
timeout = undefined
}
last = now
setValue(next)
const sync = (text: string) => {
shown = text
setValue(text)
}
const run = () => {
timeout = undefined
const text = getValue()
if (!live?.()) {
sync(text)
return
}
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
last = Date.now()
setValue(next)
timeout = undefined
}, remaining)
if (!text.startsWith(shown) || text.length <= shown.length) {
sync(text)
return
}
const end = next(text, shown.length)
sync(text.slice(0, end))
if (end < text.length) timeout = setTimeout(run, TEXT_RENDER_PACE_MS)
}
createEffect(() => {
const text = getValue()
if (!live?.()) {
clear()
sync(text)
return
}
if (!text.startsWith(shown) || text.length < shown.length) {
clear()
sync(text)
return
}
if (text.length === shown.length || timeout) return
timeout = setTimeout(run, TEXT_RENDER_PACE_MS)
})
onCleanup(() => {
if (timeout) clearTimeout(timeout)
clear()
})
return value
@ -1332,11 +1370,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
return items.filter((x) => !!x).join(" \u00B7 ")
})
const displayText = () => (part().text ?? "").trim()
const throttledText = createThrottledValue(displayText)
const streaming = createMemo(
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
)
const displayText = () => (part().text ?? "").trim()
const throttledText = createPacedValue(displayText, streaming)
const isLastTextPart = createMemo(() => {
const last = (data.store.part?.[props.message.id] ?? [])
.filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim())
@ -1395,11 +1433,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
const part = () => props.part as ReasoningPart
const text = () => part().text.trim()
const throttledText = createThrottledValue(text)
const streaming = createMemo(
() => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number",
)
const text = () => part().text.trim()
const throttledText = createPacedValue(text, streaming)
return (
<Show when={throttledText()}>