-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.04 kB⬇
-
🌐 conversation-format-utils.jsWeb, 17.28 kB⬇
-
🌐 dom-utils.jsWeb, 2.92 kB⬇
-
🌐 format-utils.jsWeb, 20.91 kB⬇
-
🌐 overlay-utils.jsWeb, 1.99 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 22.87 kB⬇
-
🌐 theme-utils.jsWeb, 6.78 kB⬇
/**
* Conversation and response content formatting utilities for TinAI.
* Converts API response content types (text, code, tables, diagrams, etc.) and history turns into styled HTML.
*/
import Api from '../api.js';
import {
escapeHtml,
formatRelatedChipsHtml,
formatSuggestedChipsHtml,
formatDividerHtml,
formatThinkingBlockHtml,
formatCodeBlockHtml
} from './format-utils.js';
import { getModelRates, formatModelShortName } from './settings-utils.js';
/**
* Formats a header item.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the header.
*/
export function formatHeader(item) {
const val = (item && item.value !== undefined) ? item.value : '';
return '<h4>' + val + '</h4>';
}
/**
* Formats a text paragraph.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the paragraph.
*/
export function formatText(item) {
const val = (item && item.value !== undefined) ? item.value : '';
return '<p>' + val + '</p>';
}
/**
* Formats a code block with syntax highlighting support and escaping.
* @param {Object} item - The content item object containing code and language.
* @returns {string} HTML string for the code block.
*/
export function formatCode(item) {
const val = (item && item.value !== undefined) ? item.value : '';
return formatCodeBlockHtml(val, item?.language, item?.caption);
}
/**
* Formats a blockquote.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the blockquote.
*/
export function formatQuote(item) {
const val = (item && item.value !== undefined) ? item.value : '';
let html = '';
if (item?.caption) html += '<h7>' + item.caption + '</h7>';
html += '<blockquote>' + val + '</blockquote>';
return html;
}
/**
* Formats a hyperlink, using source metadata if available.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the link.
*/
export function formatLink(item) {
const link_title = item?.source?.[0]?.title;
const link_href = item?.source?.[0]?.url;
if (link_title && link_href) {
return '<a target="_blank" href="' + link_href + '">' + link_title + '\u2197</a>';
}
const val = (item && item.value !== undefined) ? item.value : '';
return '<a>' + val + '</a>';
}
/**
* Formats a color preview.
* @param {Object} item - The content item object containing a hex color.
* @returns {string} HTML string for the color span.
*/
export function formatColor(item) {
const val = (item && item.value !== undefined) ? item.value : '';
let html = '';
if (item?.caption) html += '<h7>' + item.caption + '</h7>';
html += '<span style="color: ' + val + '">' + val + '</span>';
return html;
}
/**
* Formats a list item (ordered or unordered).
* @param {Object|Array} item - The content item object or list of items.
* @returns {string} HTML string for the list item.
*/
export function formatList(item) {
if (Array.isArray(item)) {
if (item.length === 0) return '';
const first = item[0];
if (first && first.ordered) {
let html = '<ol>';
item.forEach(li => {
if (li) html += '<li value="' + (li.index || 1) + '">' + (li.value || '') + '</li>';
});
html += '</ol>';
return html;
} else {
let html = '<ul>';
item.forEach(li => {
if (li) html += '<li>' + (li.value || '') + '</li>';
});
html += '</ul>';
return html;
}
}
if (!item) return '';
if (item.ordered) {
return '<ol><li value="' + (item.index || 1) + '">' + (item.value || '') + '</li></ol>';
}
return '<ul><li>' + (item.value || '') + '</li></ul>';
}
/**
* Formats a Mermaid diagram container.
* @param {Object} item - The content item object containing diagram syntax.
* @returns {string} HTML string for the diagram.
*/
export function formatMermaid(item) {
const val = (item && item.value !== undefined) ? item.value : '';
let html = '';
if (item?.caption) html += '<h7>' + item.caption + '</h7><br/>';
html += '<div class="div-diagram-mermaid" data-mermaid="' + escapeHtml(val) + '">' + escapeHtml(val) + '</div>';
return html;
}
/**
* Formats a table structure.
* @param {Object} item - The content item object containing table rows and headers.
* @returns {string} HTML string for the table.
*/
export function formatTable(item) {
if (!item || !Array.isArray(item['table-rows'])) return '';
let html = '';
if (item.caption) html += '<h7>' + item.caption + '</h7>';
const has_th = Boolean(item['has-header']);
const rows = [...item['table-rows']];
html += '<table>';
if (has_th && rows.length > 0) {
const row = rows.shift();
html += '<thead><tr>';
for (const title of row) html += '<th>' + title + '</th>';
html += '</tr></thead>';
}
html += '<tbody>';
for (const row of rows) {
html += '<tr>';
if (Array.isArray(row)) {
for (const cell of row) html += '<td>' + cell + '</td>';
}
html += '</tr>';
}
html += '</tbody></table>';
return html;
}
/**
* Formats a mathematical LaTeX equation.
* @param {Object} item - The content item object containing equation text.
* @returns {string} HTML string for the equation.
*/
export function formatEquation(item) {
let html = '';
if (item?.caption) html += '<h7>' + item.caption + '</h7>';
let val = (item?.value || '').trim();
if (val.startsWith('$$') && val.endsWith('$$') && val.length >= 4) {
val = val.slice(2, -2).trim();
} else if (val.startsWith('\\[') && val.endsWith('\\]') && val.length >= 4) {
val = val.slice(2, -2).trim();
}
html += '$$ ' + val + ' $$';
return html;
}
/**
* Default formatter for unknown or unrecognized content types.
* @param {Object|string} item - The content item object or string.
* @returns {string} HTML string for the preformatted value.
*/
export function formatDefault(item) {
const val = (item && item.value !== undefined) ? item.value : (typeof item === 'string' ? item : (item ? JSON.stringify(item) : ''));
return '<pre>' + val + '</pre>';
}
/**
* Routes a content item to its specific formatter based on its type.
* @param {Object|Array|string} item - The content item object.
* @returns {string} The practical HTML string.
*/
export function formatItem(item) {
if (!item) return '';
if (typeof item === 'string') {
return formatText({ type: 'text', value: item });
}
if (Array.isArray(item)) {
return formatList(item);
}
switch (item.type) {
case 'header':
return formatHeader(item);
case 'text':
return formatText(item);
case 'code':
return formatCode(item);
case 'quote':
return formatQuote(item);
case 'link':
return formatLink(item);
case 'color-hex':
return formatColor(item);
case 'list-step':
return formatList(item);
case 'diagram-mermaid':
return formatMermaid(item);
case 'table':
return formatTable(item);
case 'equation':
return formatEquation(item);
default:
return formatDefault(item);
}
}
/**
* Formats suggested queries and related topics into chip elements.
* @param {Object} data - Turn response object.
* @param {Object} [conversation_settings={}] - Conversation configuration flags.
* @returns {string} The formatted HTML string for suggestions.
*/
export function formatSuggestions(data, conversation_settings = {}) {
let html = '';
const show_suggested = conversation_settings.show_suggested !== false;
const show_related = conversation_settings.show_related !== false;
if (show_suggested && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
const suggestion_spans = formatSuggestedChipsHtml(data.suggestions);
html += `<p><strong>Suggestions:</strong> <i>${suggestion_spans}</i></p>`;
}
if (show_related && Array.isArray(data.related) && data.related.length > 0) {
const related_spans = formatRelatedChipsHtml(data.related);
html += `<p><strong>Related:</strong> <i>${related_spans}</i></p>`;
}
return html;
}
/**
* Formats a content structure by rendering items and grouping consecutive list-steps into lists.
* @param {Array<Object>|Object|string|null} content - Response content items.
* @returns {string} Formatted HTML string.
*/
export function formatContentBlock(content) {
if (!content) return '';
if (typeof content === 'string') {
return formatText({ type: 'text', value: content }) + formatDividerHtml();
}
let response_html = '';
let collected_list_items = [];
const flush_list = () => {
if (collected_list_items.length > 0) {
response_html += formatItem(collected_list_items);
response_html += formatDividerHtml();
collected_list_items = [];
}
};
const content_items = Array.isArray(content) ? content : Object.values(content);
content_items.forEach(item => {
if (!item) return;
if (typeof item === 'string') {
flush_list();
response_html += formatText({ type: 'text', value: item });
response_html += formatDividerHtml();
return;
}
if (item.type === 'list-step') {
if (collected_list_items.length > 0) {
const last_collected = collected_list_items[collected_list_items.length - 1];
const same_order_type = item.ordered === last_collected.ordered;
let in_order = true;
if (item.ordered) {
in_order = (item.index === last_collected.index + 1);
}
if (same_order_type && in_order) {
collected_list_items.push(item);
} else {
flush_list();
collected_list_items.push(item);
}
} else {
collected_list_items.push(item);
}
} else {
flush_list();
response_html += formatItem(item);
response_html += formatDividerHtml();
}
});
flush_list();
return response_html;
}
/**
* Formats source citation links.
* @param {Array<Object>|null} annotations - Source citations array.
* @returns {string} Formatted HTML string.
*/
export function formatAnnotationsBlock(annotations) {
if (!Array.isArray(annotations) || annotations.length === 0) return '';
let sources_html = '<div class="div-response-sources" style="margin-top: 1em; opacity: 0.85;">';
sources_html += '<p><strong>Sources:</strong></p>';
sources_html += '<ul style="list-style: none; margin: 0; padding-left: 0.5em;">';
annotations.forEach(ann => {
if (ann && ann.url && ann.title) {
sources_html += `<li style="margin-bottom: 0.3em;"><a target="_blank" href="${ann.url}" style="text-decoration: underline;">${ann.title}\u2197</a></li>`;
}
});
sources_html += '</ul>';
sources_html += '</div>';
return sources_html;
}
/**
* Formats the cost and token usage summary for a response.
* @param {Object} data - The response data containing model, tokens, and costs.
* @returns {string} HTML string for the cost summary.
*/
export function formatCostSummary(data) {
if (!data) {
return '';
}
const promptTokens = data.tokens?.prompt || 0;
const replyTokens = data.tokens?.reply || data.tokens?.response || 0;
const thoughtTokens = data.tokens?.thought || 0;
const totalTokens = data.tokens?.total || (promptTokens + replyTokens + thoughtTokens);
if (!data.costs && !data.tokens && !data.model) {
return '';
}
let total_cost = '0.000';
let op = '?';
let feature_cost = '0.000';
if (data.costs && (data.costs.total !== undefined && data.costs.total !== null)) {
total_cost = Number(data.costs.total).toFixed(3);
op = data.costs.op !== undefined ? Number(data.costs.op).toFixed(3) : '?';
if (data.costs.special_features !== undefined && data.costs.special_features !== null) {
feature_cost = Number(data.costs.special_features).toFixed(3);
}
} else {
const modelKey = data.model || 'gemini-3.5-flash-lite';
const [rateInput, rateOutput] = getModelRates(modelKey);
const promptCost = (promptTokens / 1000000) * rateInput * 1.5;
const replyCost = (replyTokens / 1000000) * rateOutput * 2.0;
const thoughtCost = (thoughtTokens / 1000000) * rateOutput * 2.0;
const featCost = (data.annotations?.length || 0) * 0.014;
const computedTotal = promptCost + replyCost + thoughtCost + featCost;
total_cost = computedTotal.toFixed(3);
feature_cost = featCost.toFixed(3);
}
let model_name = data.model || 'Model';
if (Api.PRESETS && Api.PRESETS[data.model]) {
const p = Api.PRESETS[data.model];
model_name = `${p.name}${p.description ? ` (${p.description})` : ''}`;
} else if (Api.UTILITY_MODELS && Api.UTILITY_MODELS[data.model]) {
const p = Api.UTILITY_MODELS[data.model];
model_name = `${p.name}${p.description ? ` (${p.description})` : ''}`;
} else if (data.model_key && Api.UTILITY_MODELS && Api.UTILITY_MODELS[data.model_key]) {
const p = Api.UTILITY_MODELS[data.model_key];
model_name = `${p.name}${p.description ? ` (${p.description})` : ''}`;
} else {
model_name = formatModelShortName(data.family, data.model || data.model_version, Api.MODELS);
}
let tools_part = '';
const specialFeatures = data.specialFeatures || data.special_features || [];
const toolUsage = data.tools || [];
const counts = {};
if (Array.isArray(toolUsage)) {
for (const tool of toolUsage) {
if (tool) {
const name = typeof tool === 'object' ? (tool.name || tool.type || JSON.stringify(tool)) : String(tool);
counts[name] = (counts[name] || 0) + 1;
}
}
} else if (typeof toolUsage === 'object' && toolUsage !== null) {
for (const [name, count] of Object.entries(toolUsage)) {
counts[name] = (counts[name] || 0) + (typeof count === 'number' ? count : 1);
}
}
const toolsCounted = new Set(Object.keys(counts));
if (Array.isArray(specialFeatures)) {
for (const feat of specialFeatures) {
if (feat) {
const name = typeof feat === 'object' ? (feat.name || feat.type || JSON.stringify(feat)) : String(feat);
if (!toolsCounted.has(name)) {
counts[name] = (counts[name] || 0) + 1;
}
}
}
}
const list = Object.entries(counts).map(([name, count]) => count > 1 ? `${name} (${count})` : name).join(', ');
if (list) {
tools_part = `Tools: ${list}`;
}
let formattedTime = '';
if (data.time !== undefined && data.time !== null && data.time !== '' && !isNaN(Number(data.time))) {
const seconds = Number(data.time) / 1000;
formattedTime = `${seconds.toFixed(2)}s`;
}
const formattedOp = (op !== '?' && !String(op).startsWith('$')) ? `$${op}` : op;
const formattedFeatureCost = `$${feature_cost}`;
const formattedCost = `$${total_cost}`;
const timeDisplay = formattedTime || '-';
const secondaryRow = `<tr class="tr-cost-secondary-row"><td colspan="9"><div class="div-cost-secondary-grid"><div class="div-cost-secondary-cell"><span class="span-cost-metric-label">Total:</span><span class="span-cost-metric-val">${formattedCost}</span></div><div class="div-cost-secondary-cell"><span class="span-cost-metric-label">OP:</span><span class="span-cost-metric-val">${formattedOp}</span></div><div class="div-cost-secondary-cell"><span class="span-cost-metric-label">Time:</span><span class="span-cost-metric-val">${timeDisplay}</span></div></div></td></tr>`;
const toolsRow = tools_part ? `<tr class="tr-cost-tools"><td colspan="9">${escapeHtml(tools_part)}</td></tr>` : '';
return `<div class="div-cost-summary-container"><table class="table-cost-summary"><thead><tr><th>Model</th><th>Input</th><th>Replied</th><th>Thought</th><th>Tokens</th><th class="th-cost-secondary">Feature Cost</th><th class="th-cost-secondary">Total Cost</th><th class="th-cost-secondary">OP</th><th class="th-cost-secondary">Time</th></tr></thead><tbody><tr><td>${escapeHtml(model_name)}</td><td>${promptTokens}</td><td>${replyTokens}</td><td>${thoughtTokens}</td><td>${totalTokens}</td><td class="td-cost-secondary">${formattedFeatureCost}</td><td class="td-cost-secondary">${formattedCost}</td><td class="td-cost-secondary">${formattedOp}</td><td class="td-cost-secondary">${formattedTime}</td></tr>${secondaryRow}${toolsRow}</tbody></table></div>`;
}
/**
* Generates the HTML string for the history item header (query).
* @param {string} query - The user's query.
* @param {boolean} is_related - Whether the query is related to the previous turn.
* @param {number} index - The index of the item.
* @returns {string} The HTML string for the history item header.
*/
export function formatHistoryItemHeader(query, is_related, index) {
const prefix = is_related ? '... ' : '';
return `
<h5>${prefix}${query}</h5>
<div class="div-chat-response-buttons">
<div class="div-chat-response-buttons-left">
<button class="btn-copy-response" data-index="${index}">Copy</button>
</div>
<div class="div-chat-response-buttons-right">
<button class="btn-redo-response" data-index="${index}">Redo</button>
<button class="btn-delete-response" data-index="${index}">Delete</button>
</div>
</div>
`;
}
/**
* Formats an entire history item from the API, including the query, response content,
* suggestions, and token/cost metadata.
* @param {Object} data - The complete response object for a single turn.
* @param {number} [index=0] - The index of the item in the conversation history.
* @param {Object} [conversation_settings={}] - Object containing settings for the current conversation.
* @returns {string} The complete HTML representation of the history item.
*/
export function formatHistoryItem(data, index = 0, conversation_settings = {}) {
if (!data) return '';
let html = '';
html += formatDividerHtml(index);
html += formatHistoryItemHeader(data.query || '', data.chain, index);
html += formatDividerHtml();
html += formatThinkingBlockHtml(data.thinking, !!data.pending, index);
if (data.pending) {
return html;
}
let response_html = formatContentBlock(data.content);
response_html += formatAnnotationsBlock(data.annotations);
html += `<div class="div-response-content" data-index="${index}">${response_html}</div>`;
html += formatSuggestions(data, conversation_settings);
html += formatCostSummary(data);
return html;
}
conversation-format-utils.js
×
Type: Web, text/x-java
17.28 Kilobytes
Last Modified 2026-09-23 10:30:34
⬇ Download File
Type: Web, text/x-java
17.28 Kilobytes
Last Modified 2026-09-23 10:30:34
⬇ Download File