75 lines
2.4 KiB
JavaScript
75 lines
2.4 KiB
JavaScript
/**
|
|
* Split Qwen-style thinking blocks from assistant text for UI.
|
|
* Supports complete and in-progress streams.
|
|
*
|
|
* Common tags: <think>…</think>, <think>…</think>, <thinking>…
|
|
*/
|
|
|
|
const TAG = String.raw`think|thinking|redacted[_-]?thinking|redacted[_-]?reasoning`
|
|
const THINK_OPEN = new RegExp(`<\\s*(?:${TAG})\\s*>`, 'i')
|
|
const THINK_PAIR = new RegExp(
|
|
`<\\s*(?:${TAG})\\s*>([\\s\\S]*?)<\\s*/\\s*(?:${TAG})\\s*>`,
|
|
'gi'
|
|
)
|
|
|
|
/**
|
|
* @param {string} text
|
|
* @returns {{ thinking: string, answer: string, thinkingOpen: boolean }}
|
|
*/
|
|
export function partitionThink(text) {
|
|
const full = String(text || '')
|
|
/** @type {string[]} */
|
|
const blocks = []
|
|
let answer = full.replace(THINK_PAIR, (_, body) => {
|
|
const t = String(body || '').trim()
|
|
if (t) blocks.push(t)
|
|
return ''
|
|
})
|
|
|
|
// Incomplete open block at end of stream
|
|
let thinkingOpen = false
|
|
const openMatch = answer.match(THINK_OPEN)
|
|
if (openMatch && openMatch.index != null) {
|
|
thinkingOpen = true
|
|
const openIdx = openMatch.index
|
|
const after = answer.slice(openIdx + openMatch[0].length)
|
|
if (after.trim()) blocks.push(after.trim())
|
|
answer = answer.slice(0, openIdx)
|
|
}
|
|
|
|
return {
|
|
thinking: blocks.join('\n\n').trim(),
|
|
answer: answer.replace(/^\s+/, '').replace(/\s+$/, ''),
|
|
thinkingOpen,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render assistant body HTML: optional collapsible think + answer.
|
|
* @param {string} raw
|
|
* @param {(s: string) => string} formatBody
|
|
* @param {{ openThink?: boolean }} [opts]
|
|
*/
|
|
export function renderAssistantHtml(raw, formatBody, opts = {}) {
|
|
const { thinking, answer, thinkingOpen } = partitionThink(raw)
|
|
const open = opts.openThink !== false && (thinkingOpen || Boolean(thinking))
|
|
let html = ''
|
|
if (thinking) {
|
|
html += `<details class="qvac-think"${open ? ' open' : ''}>
|
|
<summary class="qvac-think-summary">
|
|
<span class="qvac-think-ico" aria-hidden="true">◐</span>
|
|
<span>Thinking</span>
|
|
${thinkingOpen ? '<span class="qvac-think-live">live</span>' : ''}
|
|
</summary>
|
|
<div class="qvac-think-body">${formatBody(thinking)}</div>
|
|
</details>`
|
|
}
|
|
const ans = answer || (!thinking ? '…' : '')
|
|
if (ans) {
|
|
html += `<div class="qvac-msg-answer">${formatBody(ans)}</div>`
|
|
} else if (thinkingOpen) {
|
|
html += `<div class="qvac-msg-answer qvac-msg-pending muted">Working…</div>`
|
|
}
|
|
return html || formatBody(raw || '…')
|
|
}
|