Files
peardata/ui/qvac/think.js
T
Raven Scott 4b5b78892f
CI / test (push) Successful in 2m45s
Release rolling / release (push) Successful in 11m36s
FIX: QVAC Updates
2026-07-30 15:23:43 -04:00

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 || '…')
}