9 directories, 58 files

tinai

Home / tinai
import { escapeHtml } from './util/format-utils.js';
import { formatSuggestedChipsHtml, formatRelatedChipsHtml } from './util/format-utils.js';

/**
 * Class RankingsView
 * Read-only renderer for the Rankings tab of ranking conversations: one card
 * per recorded ranking, showing the prompt, the ranked items in order (with
 * their copied text, score, and reason), optional additional item
 * suggestions, query chips, and the reply. The items are copies taken at
 * rank time, so later edits to the choices never alter a recorded ranking.
 */
class RankingsView {

	/**
	 * Renders the rank history blocks.
	 * @param {Array<Object>} [blocks=[]] - The recorded rank blocks.
	 * @param {object} [settings={}] - Conversation flags { show_suggested, show_related }.
	 * @returns {string} The html string.
	 */
	render(blocks = [], settings = {}) {
		if (!Array.isArray(blocks) || blocks.length === 0) {
			return '<p class="p-rankings-empty">No rankings yet. Use Rank to order the choices by relevance to a prompt.</p>';
		}
		const existing = Array.isArray(settings.existing_choices) ? settings.existing_choices : [];
		return blocks.map(block => this.#block_html(block, settings, existing)).join('');
	}

	/**
	 * Builds the html for one rank block.
	 * @param {Object} block - The rank block.
	 * @param {object} settings - Conversation flags.
	 * @returns {string} The block html.
	 */
	#block_html(block, settings, existing = []) {
		const prompt = escapeHtml(block.prompt || '');
		const timestamp = block.timestamp ? new Date(block.timestamp).toLocaleString() : '';
		const items = (Array.isArray(block.items) ? block.items : [])
			.map(item => this.#item_html(item))
			.join('');
		const additional = this.#additional_html(block.additional_items, existing, settings);
		const reply = block.reply
			? `<div class="div-rankings-explanation"><div class="div-rankings-section-label">Explanation</div><div class="div-rankings-reply">${block.reply}</div></div>`
			: '';
		const queries = this.#queries_html(block, settings);
		const actions = settings.read_only ? '' : `
					<button type="button" class="as-icon div-rankings-revisit" data-guid="${escapeHtml(block.guid || '')}" title="Revisit: load these choices and their prompt">&#8635;</button>
					<button type="button" class="as-icon div-rankings-delete" data-guid="${escapeHtml(block.guid || '')}" title="Delete this ranking">&#128465;</button>`;
		return `
			<div class="div-rankings-block" data-guid="${escapeHtml(block.guid || '')}">
				<div class="div-rankings-block-header">
					<div class="div-rankings-prompt">${prompt}</div>
					<span class="span-rankings-timestamp">${escapeHtml(timestamp)}</span>${actions}
				</div>
				<div class="div-rankings-items">${items}</div>
				${additional}
				${reply}
				${queries}
			</div>
		`;
	}

	/**
	 * Builds the html for one ranked item.
	 * @param {Object} item - The ranked item { index, text, score, reason }.
	 * @returns {string} The item html.
	 */
	#item_html(item) {
		const index = Number(item.index) || 0;
		const score = Number(item.score) || 0;
		const text = escapeHtml(item.text || '');
		const reason = escapeHtml(item.reason || '');
		return `
			<div class="div-rankings-item" data-guid="${escapeHtml(item.guid || '')}">
				<span class="span-rankings-index">${index}</span>
				<div class="div-rankings-item-body">
					<div class="div-rankings-item-text">${text}</div>
					<div class="div-rankings-item-reason">${reason}</div>
				</div>
				<span class="span-rankings-score" title="Ranking confidence">${score}</span>
			</div>
		`;
	}

	/**
	 * Builds the additional-items section when the model proposed new
	 * candidates that would outrank the first item. Each row carries an add
	 * button that prepends the choice to the current list; it is disabled
	 * when the choice is already present (case-insensitive).
	 * @param {string[]} [items=[]] - The suggested additional items.
	 * @param {string[]} [existing=[]] - The current choices as lowercase plain text.
	 * @param {object} [settings={}] - Conversation flags; read_only omits the add buttons.
	 * @returns {string} The section html, empty when there are none.
	 */
	#additional_html(items = [], existing = [], settings = {}) {
		if (!Array.isArray(items) || items.length === 0) return '';
		const rows = items
			.map(item => {
				const present = existing.includes(item.trim().toLowerCase());
				const button = settings.read_only ? '' : `<button type="button" class="as-icon div-rankings-add"${present ? ' disabled="disabled" title="Already in the list"' : ' title="Add to the top of the list"'} data-choice="${escapeHtml(item)}">&#10133;</button>`;
				return `<div class="div-rankings-additional-item">${escapeHtml(item)}${button}</div>`;
			})
			.join('');
		return `
			<div class="div-rankings-additional">
				<div class="div-rankings-section-label">Additional items that could outrank the first</div>
				${rows}
			</div>
		`;
	}

	/**
	 * Builds the suggested/related query chip block, gated by the
	 * conversation flags like chat responses.
	 * @param {Object} block - The rank block.
	 * @param {object} settings - Conversation flags.
	 * @returns {string} The queries html, empty when nothing to show.
	 */
	#queries_html(block, settings = {}) {
		let html = '';
		if (settings.show_suggested !== false && Array.isArray(block.suggestions) && block.suggestions.length > 0) {
			html += `<p><strong>Suggestions:</strong> <i>${formatSuggestedChipsHtml(block.suggestions)}</i></p>`;
		}
		if (settings.show_related !== false && Array.isArray(block.related) && block.related.length > 0) {
			html += `<p><strong>Related:</strong> <i>${formatRelatedChipsHtml(block.related)}</i></p>`;
		}
		return html ? `<div class="div-rankings-queries">${html}</div>` : '';
	}

}

export default RankingsView;
🌐
rankings-view.js ×
Type: Web, text/plain
5.67 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File