import { bar, probColor } from './scoring.js'; // Locale-independent thousands grouping, matching the Python app's f"{n:,}". export const formatNumber = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); const escapeHtml = (s) => s.replace(/&/g, '&').replace(//g, '>'); export function highlightHtml(text, spans, scores) { const parts = ['
']; let cursor = 0; spans.forEach((span, i) => { parts.push(escapeHtml(text.slice(cursor, span.start)).replace(/\n/g, '
')); const score = scores[i]; parts.push( `` + `${escapeHtml(span.sentence)}`, ); cursor = span.end; }); parts.push(escapeHtml(text.slice(cursor)).replace(/\n/g, '
')); parts.push('
'); return parts.join(''); } export function reportMarkdown( result, { threshold, aggregation, modelRepo, device, dtype, chars }, ) { const label = aggregation === 'max' ? 'Max' : 'Mean'; const note = result.windows === 1 ? 'one window, so mean and max are identical' : aggregation === 'max' ? 'highest-scoring window' : `average across ${result.windows} windows`; const scores = result.sentences.map((s) => s.score); const atOrAbove = scores.filter((s) => s >= threshold).length; return [ '# AI Text Detection Report', '', `**Model:** \`${modelRepo}\``, `**Backend:** ${dtype} ONNX on ${device}`, `**Characters:** ${formatNumber(chars)} | **Sentences:** ${scores.length} | **Windows scored:** ${result.windows}`, `**Aggregation:** ${label} (${note}) | **Threshold:** ${threshold.toFixed(2)}`, '', '## Verdict', '', `### ${result.verdict}`, '', `\`${bar(result.score)}\` **score = ${result.score.toFixed(3)}** (${result.aboveThreshold ? 'above' : 'below'} ${threshold.toFixed(2)})`, '', '| Metric | Value |', '| --- | --- |', `| Score (${label} of windows) | ${result.score.toFixed(3)} |`, `| Mean of windows | ${result.scoreMean.toFixed(3)} |`, `| Max of windows | ${result.scoreMax.toFixed(3)} |`, `| Sentences at or above threshold | ${atOrAbove}/${scores.length} |`, `| Max sentence score | ${Math.max(...scores).toFixed(3)} |`, `| Min sentence score | ${Math.min(...scores).toFixed(3)} |`, `| Decision threshold | ${threshold.toFixed(2)} |`, '', '## Sentence-level breakdown', '', ...result.sentences.map((s) => { const snippet = s.sentence.length <= 160 ? s.sentence : `${s.sentence.slice(0, 157)}...`; return `- \`${s.score.toFixed(3)}\` \`${bar(s.score, 12)}\` ${snippet}`; }), '', '## Most AI-like passages', '', ...[...result.sentences] .sort((a, b) => b.score - a.score) .slice(0, 5) .map((s) => `> **${s.score.toFixed(3)}** - ${s.sentence}`), '', '---', '', '_Scores come from an ONNX model running in your browser, and nothing is uploaded. They are probabilities, not proof: the model card warns against using this detector as the sole basis for high-stakes decisions. The threshold is a display choice, only 0.50 comes from the model card, and no false-positive rate is claimed for any other value._', ].join('\n'); }