Download webui/src/features/chatHistory/chatHistoryUtils.js from a3216/ds2api: direct link, hf CLI and curl.
- Browser
- Download file 8.85 kB
-
https://huggingface.co/spaces/a3216/ds2api/resolve/ce5a019598af8a3a8fa70d3880098df3e850ecd7/webui/src/features/chatHistory/chatHistoryUtils.js
- Command line
-
hf download hf://spaces/a3216/ds2api@ce5a019598af8a3a8fa70d3880098df3e850ecd7/webui/src/features/chatHistory/chatHistoryUtils.js
-
curl -L -o chatHistoryUtils.js https://huggingface.co/spaces/a3216/ds2api/resolve/ce5a019598af8a3a8fa70d3880098df3e850ecd7/webui/src/features/chatHistory/chatHistoryUtils.js
8.85 kB
| export const LIMIT_OPTIONS = [0, 10, 20, 50] | |
| export const DISABLED_LIMIT = 0 | |
| export const MESSAGE_COLLAPSE_AT = 700 | |
| export const VIEW_MODE_KEY = 'ds2api_chat_history_view_mode' | |
| const BEGIN_SENTENCE_MARKER = '' | |
| const SYSTEM_MARKER = 'System: ' | |
| const USER_MARKER = 'User: ' | |
| const ASSISTANT_MARKER = 'Assistant: ' | |
| const TOOL_MARKER = 'Tool: ' | |
| const END_INSTRUCTIONS_MARKER = '\n' | |
| const END_SENTENCE_MARKER = '\n' | |
| const END_TOOL_RESULTS_MARKER = '\n' | |
| const CURRENT_INPUT_FILE_PROMPT_RE = /(已附上下文文件|上下文已存入|请从文件|使用附件).+\.txt/iu | |
| const LEGACY_CURRENT_INPUT_FILE_PROMPTS = new Set([ | |
| 'The current request and prior conversation context have already been provided. Answer the latest user request directly.', | |
| ]) | |
| const HISTORY_TRANSCRIPT_TITLE = '# context_context.txt' | |
| const HISTORY_TRANSCRIPT_ENTRY_RE = /^===\s+\d+\s+===\s*$/gm | |
| const HISTORY_TRANSCRIPT_ROLE_RE = /^\[?r\s*=\s*(\d+)\]?\s*$/i | |
| function isCurrentInputFilePrompt(value) { | |
| const text = String(value || '').trim() | |
| return CURRENT_INPUT_FILE_PROMPT_RE.test(text) || LEGACY_CURRENT_INPUT_FILE_PROMPTS.has(text) | |
| } | |
| function normalizeHistoryRole(role) { | |
| if (typeof role === 'number' && Number.isFinite(role)) { | |
| switch (role) { | |
| case 0: | |
| return 'system' | |
| case 1: | |
| return 'user' | |
| case 2: | |
| return 'assistant' | |
| case 3: | |
| return 'tool' | |
| default: | |
| return 'user' | |
| } | |
| } | |
| const normalized = String(role || '').trim().toLowerCase() | |
| if (normalized === 'function') return 'tool' | |
| if (normalized === 'developer') return 'system' | |
| if (normalized === 'system' || normalized === 'user' || normalized === 'assistant' || normalized === 'tool') { | |
| return normalized | |
| } | |
| return normalized || 'system' | |
| } | |
| export function formatDateTime(value, lang) { | |
| if (!value) return '-' | |
| try { | |
| return new Intl.DateTimeFormat(lang === 'zh' ? 'zh-CN' : 'en-US', { | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit', | |
| }).format(new Date(value)) | |
| } catch { | |
| return '-' | |
| } | |
| } | |
| export function formatElapsed(ms, t) { | |
| if (!ms) return t('chatHistory.metaUnknown') | |
| if (ms < 1000) return `${ms}ms` | |
| return `${(ms / 1000).toFixed(ms < 10_000 ? 2 : 1)}s` | |
| } | |
| export function previewText(item) { | |
| return item?.preview || item?.content || item?.reasoning_content || item?.error || item?.user_input || '' | |
| } | |
| export function statusTone(status) { | |
| switch (status) { | |
| case 'success': | |
| return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-600' | |
| case 'error': | |
| return 'border-destructive/20 bg-destructive/10 text-destructive' | |
| case 'stopped': | |
| return 'border-amber-500/20 bg-amber-500/10 text-amber-600' | |
| default: | |
| return 'border-border bg-secondary/60 text-muted-foreground' | |
| } | |
| } | |
| export function downloadTextFile(filename, text) { | |
| const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }) | |
| const url = URL.createObjectURL(blob) | |
| const link = document.createElement('a') | |
| link.href = url | |
| link.download = filename | |
| document.body.appendChild(link) | |
| link.click() | |
| document.body.removeChild(link) | |
| URL.revokeObjectURL(url) | |
| } | |
| function fallbackCopyText(text) { | |
| const textArea = document.createElement('textarea') | |
| textArea.value = text | |
| textArea.setAttribute('readonly', '') | |
| textArea.style.position = 'fixed' | |
| textArea.style.top = '-9999px' | |
| textArea.style.left = '-9999px' | |
| document.body.appendChild(textArea) | |
| textArea.focus() | |
| textArea.select() | |
| let copied = false | |
| try { | |
| copied = document.execCommand('copy') | |
| } finally { | |
| document.body.removeChild(textArea) | |
| } | |
| if (!copied) { | |
| throw new Error('copy failed') | |
| } | |
| } | |
| export async function copyTextWithFallback(text) { | |
| try { | |
| if (navigator.clipboard?.writeText) { | |
| await navigator.clipboard.writeText(text) | |
| return | |
| } | |
| } catch { | |
| // Fall through to execCommand fallback. | |
| } | |
| fallbackCopyText(text) | |
| } | |
| function skipWhitespace(text, start) { | |
| let cursor = start | |
| while (cursor < text.length && /\s/.test(text[cursor])) { | |
| cursor += 1 | |
| } | |
| return cursor | |
| } | |
| export function parseStrictHistoryMessages(historyText) { | |
| const rawText = String(historyText || '') | |
| const transcript = rawText.slice(rawText.indexOf(BEGIN_SENTENCE_MARKER)) | |
| const markers = [ | |
| { role: 'system', marker: SYSTEM_MARKER }, | |
| { role: 'user', marker: USER_MARKER }, | |
| { role: 'assistant', marker: ASSISTANT_MARKER }, | |
| { role: 'tool', marker: TOOL_MARKER }, | |
| ] | |
| const findNextMarker = (start) => { | |
| let best = null | |
| for (const item of markers) { | |
| const idx = transcript.indexOf(item.marker, start) | |
| if (idx < 0) continue | |
| if (!best || idx < best.index) { | |
| best = { ...item, index: idx } | |
| } | |
| } | |
| return best | |
| } | |
| const first = findNextMarker(0) | |
| if (!first) return null | |
| const parsed = [] | |
| let cursor = first.index | |
| let trailingAssistantPromptOnly = false | |
| while (cursor < transcript.length) { | |
| const current = markers.find((item) => transcript.startsWith(item.marker, cursor)) | |
| if (!current) { | |
| if (transcript.slice(cursor).trim() === '') break | |
| return null | |
| } | |
| cursor += current.marker.length | |
| const next = findNextMarker(cursor) | |
| const content = (next ? transcript.slice(cursor, next.index) : transcript.slice(cursor)).trim() | |
| if (current.role === 'assistant' && !next && content === '') { | |
| trailingAssistantPromptOnly = true | |
| break | |
| } | |
| if (content) { | |
| parsed.push({ role: current.role, content }) | |
| } | |
| cursor = next ? next.index : transcript.length | |
| } | |
| if (!parsed.length) return null | |
| if (!trailingAssistantPromptOnly && parsed[parsed.length - 1]?.role !== 'assistant') return null | |
| return parsed | |
| } | |
| export function parseTranscriptHistoryMessages(historyText) { | |
| const rawText = String(historyText || '') | |
| const titleIndex = rawText.indexOf(HISTORY_TRANSCRIPT_TITLE) | |
| const transcript = titleIndex >= 0 ? rawText.slice(titleIndex) : rawText | |
| const matches = [...transcript.matchAll(HISTORY_TRANSCRIPT_ENTRY_RE)] | |
| if (!matches.length) return null | |
| const parsed = [] | |
| for (let i = 0; i < matches.length; i += 1) { | |
| const match = matches[i] | |
| const next = matches[i + 1] | |
| const start = (match.index || 0) + match[0].length | |
| const end = next ? next.index : transcript.length | |
| const rawContent = transcript.slice(start, end).replace(/^\r?\n/, '').trim() | |
| if (!rawContent) continue | |
| const lines = rawContent.split(/\r?\n/) | |
| let role = 'user' | |
| let contentLines = lines | |
| if (lines.length > 0) { | |
| const roleMatch = lines[0].trim().match(HISTORY_TRANSCRIPT_ROLE_RE) | |
| if (roleMatch) { | |
| role = normalizeHistoryRole(Number(roleMatch[1])) | |
| contentLines = lines.slice(1) | |
| } | |
| } | |
| const content = contentLines.join('\n').trim() | |
| if (!content) continue | |
| parsed.push({ role, content }) | |
| } | |
| return parsed.length ? parsed : null | |
| } | |
| export function parseHistoryMessages(historyText) { | |
| return parseStrictHistoryMessages(historyText) || parseTranscriptHistoryMessages(historyText) | |
| } | |
| export function buildListModeMessages(item, t) { | |
| const liveMessages = Array.isArray(item?.messages) && item.messages.length > 0 | |
| ? item.messages | |
| : [{ role: 'user', content: item?.user_input || t('chatHistory.emptyUserInput') }] | |
| const historyMessages = parseHistoryMessages(item?.history_text) | |
| if (!historyMessages?.length) { | |
| return { messages: liveMessages, historyMerged: false } | |
| } | |
| const placeholderOnly = liveMessages.length === 1 | |
| && String(liveMessages[0]?.role || '').trim().toLowerCase() === 'user' | |
| && isCurrentInputFilePrompt(liveMessages[0]?.content) | |
| if (placeholderOnly) { | |
| return { messages: historyMessages, historyMerged: true } | |
| } | |
| const insertAt = liveMessages.findIndex(message => { | |
| const role = String(message?.role || '').trim().toLowerCase() | |
| return role !== 'system' && role !== 'developer' | |
| }) | |
| const mergedMessages = [...liveMessages] | |
| mergedMessages.splice(insertAt < 0 ? mergedMessages.length : insertAt, 0, ...historyMessages) | |
| return { messages: mergedMessages, historyMerged: true } | |
| } | |