-
🌐 admin_panel.phpWeb, 1.54 kB⬇
-
🌐 api.jsWeb, 5.89 kB⬇
-
🌐 api.phpWeb, 6.54 kB⬇
-
🌐 app.jsWeb, 84.72 kB⬇
-
🌐 base.cssWeb, 2.23 kB⬇
-
🌐 context.jsWeb, 9.37 kB⬇
-
🌐 conversation-index.jsWeb, 10.19 kB⬇
-
🌐 conversation.jsWeb, 16.61 kB⬇
-
🌐 conversations.jsWeb, 15.16 kB⬇
-
🌐 diction.jsWeb, 935 B⬇
-
🌐 document.jsWeb, 947 B⬇
-
🌐 dynamic.phpWeb, 430 B⬇
-
📷 favicon-lg.pngImage, 9.11 kB⬇
-
📷 favicon.pngImage, 3.12 kB⬇
-
🌐 index.phpWeb, 17.38 kB⬇
-
🌐 indexjs.jsWeb, 572 B⬇
-
🌐 login.phpWeb, 767 B⬇
-
❓ manifest.webmanifestUnknown, 262 B⬇
-
🌐 memory.jsWeb, 407 B⬇
-
🌐 models.jsonWeb, 4.27 kB⬇
-
🌐 notebook-index.jsWeb, 721 B⬇
-
⚠️ php.iniOS File, 356 B⬇
-
🌐 scratchpad-conversation.jsWeb, 2.09 kB⬇
-
🌐 scratchpad-index.jsWeb, 10.19 kB⬇
-
🌐 storage.jsWeb, 15.25 kB⬇
-
🌐 style.cssWeb, 20.81 kB⬇
-
🌐 theme_dark.jsonWeb, 266 B⬇
-
🌐 theme_light.jsonWeb, 267 B⬇
-
🌐 users.jsWeb, 18.18 kB⬇
import Api from './api.js';
import { escapeHtml, formatRelatedChipsHtml, formatSuggestedChipsHtml, formatDividerHtml } from './util/format-utils.js';
/**
* class Conversation
* Handles the formatting and rendering of conversation history items into HTML.
* Converts various API response content types (text, code, tables, diagrams, etc.) into styled DOM elements.
*/
class Conversation {
//region Initialization
/**
* Initializes a new instance of the Conversation class.
*/
constructor() {
}
//endregion
//region Formatting Helpers
/**
* Formats a header item.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the header.
*/
format_header(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.
*/
format_text(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.
*/
format_code(item) {
const val = (item && item.value !== undefined) ? item.value : '';
const escaped = escapeHtml(val);
const lang_class = item.language ? ' class="code-block language-' + item.language + '"' : '';
let html = '';
if (item.caption) html += '<h7>' + item.caption + '</h7>';
html += '<code' + lang_class + '><pre>' + escaped + '</pre></code>';
return html;
}
/**
* Formats a blockquote.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the blockquote.
*/
format_quote(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.
*/
format_link(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.
*/
format_color(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.
*/
format_list(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 Mermaid syntax.
* @returns {string} HTML string for the diagram.
*/
format_mermaid(item) {
const val = (item && item.value !== undefined) ? item.value : '';
let html = '';
if (item?.caption) html += '<h7>' + item.caption + '</h7><br/>';
html += '<pre class="div-diagram-mermaid">' + val + '</pre>';
return html;
}
/**
* Formats a data table.
* @param {Object} item - The content item object containing table rows and configuration.
* @returns {string} HTML string for the table.
*/
format_table(item) {
if (!item || !Array.isArray(item['table-rows'])) return '';
let html = '';
if (item.caption) html += '<h7>' + item.caption + '</h7>';
const has_th = 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 TeX Equation container.
* @param {Object} item - The content item object containing TeX syntax.
* @returns {string} HTML string for the equation.
*/
format_equation(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;
}
/**
* Fallback formatter for unknown content types.
* @param {Object} item - The content item object.
* @returns {string} HTML string for the preformatted value.
*/
format_default(item) {
const val = (item && item.value !== undefined) ? item.value : (typeof item === 'string' ? item : (item ? JSON.stringify(item) : ''));
return '<pre>' + val + '</pre>';
}
//endregion
//region Core Routing
/**
* Routes a content item to its specific formatter based on its type.
* @param {Object|Array|string} item - The content item object.
* @returns {string} The formatted HTML string.
*/
format_item(item) {
if (!item) return '';
if (typeof item === 'string') {
return this.format_text({ type: 'text', value: item });
}
if (Array.isArray(item)) {
return this.format_list(item);
}
switch (item.type) {
case 'header':
return this.format_header(item);
case 'text':
return this.format_text(item);
case 'code':
return this.format_code(item);
case 'quote':
return this.format_quote(item);
case 'link':
return this.format_link(item);
case 'color-hex':
return this.format_color(item);
case 'list-step':
return this.format_list(item);
case 'diagram-mermaid':
return this.format_mermaid(item);
case 'table':
return this.format_table(item);
case 'equation':
return this.format_equation(item);
default:
return this.format_default(item);
}
}
//endregion
//region Response Metadata & Layout
/**
* Returns a standard horizontal divider for chat history.
* @param {string|number|null} [id=null] - Optional ID to assign to the divider.
* @returns {string} HTML string for the divider.
*/
format_divider(id = null) {
return formatDividerHtml(id);
}
/**
* Formats an expandable thought process block.
* @param {Array<string>|null} thinking - Thinking lines array.
* @param {boolean} [is_pending=false] - Whether response is currently streaming.
* @param {number} [index=0] - Turn index.
* @returns {string} HTML string for thinking block.
*/
format_thinking_block(thinking, is_pending = false, index = 0) {
if ((!Array.isArray(thinking) || thinking.length === 0) && !is_pending) {
return '';
}
let thinking_text = 'Thinking...';
if (Array.isArray(thinking) && thinking.length > 0) {
thinking_text = thinking.map(line => `<div class="div-thinking-line">${line}</div>`).join('');
}
const collapse_class = is_pending ? 'thinking-expanded' : 'thinking-collapsed';
const display_style = is_pending ? 'display: block;' : 'display: none;';
return `
<div class="div-thinking-content ${collapse_class}" data-index="${index}">
<p class="p-thinking-header" style="cursor: pointer; user-select: none;">
<strong>Thought Process</strong> <span class="span-thinking-arrow" style="font-size: 0.8em; margin-left: 5px;">${is_pending ? '\u25bc' : '\u25b6'}</span>
</p>
<div class="div-thinking-text" style="${display_style}">
${thinking_text}
</div>
</div>` + this.format_divider();
}
/**
* 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.
*/
format_content_block(content) {
if (!content) return '';
if (typeof content === 'string') {
return this.format_text({ type: 'text', value: content }) + this.format_divider();
}
let response_html = '';
let collected_list_items = [];
const flush_list = () => {
if (collected_list_items.length > 0) {
response_html += this.format_item(collected_list_items);
response_html += this.format_divider();
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 += this.format_text({ type: 'text', value: item });
response_html += this.format_divider();
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 += this.format_item(item);
response_html += this.format_divider();
}
});
flush_list();
return response_html;
}
/**
* Formats source citation links.
* @param {Array<Object>|null} annotations - Source citations array.
* @returns {string} Formatted HTML string.
*/
format_annotations_block(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 related topics and suggested follow-up prompts.
* @param {Object} data - The response data object.
* @param {Object} [conversation_settings={}] - Object containing settings for the current conversation.
* @returns {string} HTML string for the related/suggestions section.
*/
format_suggestions(data, conversation_settings = {}) {
if (!data) return '';
let html = '';
let has_content = false;
const show_related = conversation_settings.showRelatedQueries;
const show_suggestions = conversation_settings.showSuggestedQueries;
if (show_related && Array.isArray(data.related) && data.related.length > 0) {
html += this._get_related_html(data.related);
has_content = true;
}
if (show_suggestions && Array.isArray(data.suggestions) && data.suggestions.length > 0) {
html += this._get_suggestions_html(data.suggestions);
has_content = true;
}
if (has_content) {
html += this.format_divider();
}
return html;
}
/**
* Generates the HTML string for related topics.
* @param {Array<string>} related_topics - An array of related topic strings.
* @returns {string} The HTML string for related topics.
*/
_get_related_html(related_topics) {
const related_spans = formatRelatedChipsHtml(related_topics);
return `<p><strong>Related:</strong> <i>${related_spans}</i></p>`;
}
/**
* Generates the HTML string for suggested follow-up prompts.
* @param {Array<string>} suggestions - An array of suggested prompt strings.
* @returns {string} The HTML string for suggested prompts.
*/
_get_suggestions_html(suggestions) {
const suggestion_spans = formatSuggestedChipsHtml(suggestions);
return `<p><strong>Suggestions:</strong> <i>${suggestion_spans}</i></p>`;
}
/**
* 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.
*/
format_cost_summary(data) {
if (!data || !data.costs || !data.tokens) {
return '';
}
const total_cost = (data.costs.total !== undefined && data.costs.total !== null) ? Number(data.costs.total).toFixed(6) : '0.000000';
const op = data.costs.op !== undefined ? Number(data.costs.op).toFixed(6) : '?';
const features_part = (data.costs.special_features && data.costs.special_features > 0) ? `, features $${Number(data.costs.special_features).toFixed(3)}` : '';
const model_config = Api.MODELS[data.model];
const model_name = model_config ? `${model_config.name} (${model_config.description})` : (data.model || 'Unknown');
let tools_part = '';
if (data.tools) {
if (Array.isArray(data.tools) && data.tools.length > 0) {
const toolList = data.tools.map(t => typeof t === 'object' && t.name ? `${t.name} (${t.count || 1})` : `${t}`).join(', ');
if (toolList) {
tools_part = ` Tools: ${toolList}`;
}
} else if (typeof data.tools === 'object' && Object.keys(data.tools).length > 0) {
const toolList = Object.entries(data.tools).map(([name, count]) => `${name} (${count})`).join(', ');
if (toolList) {
tools_part = ` Tools: ${toolList}`;
}
}
}
return `<p style="opacity: 0.5;"><strong>${model_name}:</strong> Input ${data.tokens.prompt || 0}, replied ${data.tokens.reply || 0}, thought ${data.tokens.thought || 0}, totalling ${data.tokens.total || 0}${features_part}, costing \u2248 $${total_cost} (op: $${op}).${tools_part}</p>`;
}
/**
* 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.
*/
format_history_item(data, index = 0, conversation_settings = {}) {
if (!data) return '';
let html = '';
html += this.format_divider(index);
html += this._get_history_item_header_html(data.query || '', data.chain, index);
html += this.format_divider();
html += this.format_thinking_block(data.thinking, !!data.pending, index);
if (data.pending) {
return html;
}
let response_html = this.format_content_block(data.content);
response_html += this.format_annotations_block(data.annotations);
html += `<div class="div-response-content" data-index="${index}">${response_html}</div>`;
html += this.format_suggestions(data, conversation_settings);
html += this.format_cost_summary(data);
return html;
}
/**
* 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.
*/
_get_history_item_header_html(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>
`;
}
//endregion
}
export default Conversation;
conversation.js
×
Type: Web, text/plain
16.61 Kilobytes
Last Modified 2026-09-04 14:23:38
⬇ Download File
Type: Web, text/plain
16.61 Kilobytes
Last Modified 2026-09-04 14:23:38
⬇ Download File