-
🌐 async-utils.jsWeb, 3.69 kB⬇
-
🌐 auth-storage.jsWeb, 735 B⬇
-
🌐 chat-helpers.jsWeb, 8.85 kB⬇
-
🌐 chat-renderer.jsWeb, 70.00 kB⬇
-
🌐 code-overlay.jsWeb, 21.77 kB⬇
-
🌐 confirm-dialog.jsWeb, 3.17 kB⬇
-
🌐 conversation-format-utils.jsWeb, 21.51 kB⬇
-
🌐 diagram-overlay.jsWeb, 7.93 kB⬇
-
🌐 dialog.jsWeb, 2.50 kB⬇
-
🌐 dom-utils.jsWeb, 8.33 kB⬇
-
🌐 drag-reorder.jsWeb, 4.82 kB⬇
-
🌐 elements.jsWeb, 11.06 kB⬇
-
🌐 export-utils.jsWeb, 1.53 kB⬇
-
🌐 format-utils.jsWeb, 29.60 kB⬇
-
🌐 history-navigation.jsWeb, 1.95 kB⬇
-
🌐 hljs-yaml.jsWeb, 5.07 kB⬇
-
🌐 inline-format.jsWeb, 7.35 kB⬇
-
🌐 instant-answer.jsWeb, 19.82 kB⬇
-
🌐 interactive-tools.jsWeb, 38.77 kB⬇
-
🌐 mermaid-fixes.jsWeb, 4.60 kB⬇
-
🌐 mermaid-render.jsWeb, 5.10 kB⬇
-
🌐 mermaid.jsWeb, 195 B⬇
-
🌐 migrations.jsWeb, 12.11 kB⬇
-
🌐 multi-choice-dialog.jsWeb, 1.33 kB⬇
-
🌐 notification-stack.jsWeb, 2.50 kB⬇
-
🌐 options-manager.jsWeb, 45.56 kB⬇
-
🌐 overlay-utils.jsWeb, 4.14 kB⬇
-
🌐 pdf-export.jsWeb, 23.59 kB⬇
-
🌐 pinch-zoom.jsWeb, 3.21 kB⬇
-
🌐 plugin-dialog.jsWeb, 2.17 kB⬇
-
🌐 read-aloud.jsWeb, 33.84 kB⬇
-
🌐 response-pipeline.jsWeb, 55.19 kB⬇
-
🌐 scratchpad-editor.jsWeb, 8.64 kB⬇
-
🌐 scratchpad-export.jsWeb, 3.56 kB⬇
-
🌐 scrollbars.jsWeb, 3.12 kB⬇
-
🌐 selection-manager.jsWeb, 3.76 kB⬇
-
🌐 settings-utils.jsWeb, 31.08 kB⬇
-
🌐 share.jsWeb, 10.75 kB⬇
-
🌐 side-drawer.jsWeb, 9.37 kB⬇
-
🌐 speech-text.jsWeb, 6.60 kB⬇
-
🌐 swipe-navigation.jsWeb, 6.16 kB⬇
-
🌐 table-overlay.jsWeb, 8.10 kB⬇
-
🌐 theme-utils.jsWeb, 8.34 kB⬇
-
🌐 thinking-panel-base.jsWeb, 14.88 kB⬇
-
🌐 ui-sets.jsWeb, 1.00 kB⬇
-
🌐 version-overlay.jsWeb, 4.63 kB⬇
/**
* Overlay layer for viewing code blocks enlarged with line numbers and
* optional highlighting of a line range or of code analysis findings.
*/
import { showOverlay, hideOverlay } from './overlay-utils.js';
import { addSafeEventListener, getEl, queryEl, queryAll } from './dom-utils.js';
import { build_thinking_block, escapeHtml, hljs, formatThinkingLineHtml, typesetMathjax } from './format-utils.js';
import { formatAnnotationsBlock, formatCostSummary } from './conversation-format-utils.js';
import { customConfirm } from './confirm-dialog.js';
import { append_following_bottom, content_of, scroller_of } from './scrollbars.js';
let div_overlay = null;
let div_view = null;
let div_thinking = null;
let div_items = null;
let div_side = null;
let current_lines = [];
let current_code = '';
let current_language = '';
let current_hash = '';
let current_items = [];
let current_analysis_els = [];
let selected_index = -1;
let side_open = false;
let on_analyze = null;
let load_analysis = null;
let save_analysis = null;
let single_column_query = window.matchMedia('(max-width: 780px)');
/**
* Checks whether the code overlay is currently open.
* @returns {boolean} True if the overlay is active.
*/
export function is_code_overlay_open() {
return !!div_overlay && div_overlay.classList.contains('active');
}
/**
* Checks whether the app layout is in single-column mode. Mirrors the CSS
* breakpoint that collapses the app layout to a single column, so overlay
* behavior keyed on this stays in sync with the stylesheet.
* @returns {boolean} True when the viewport is at or below the breakpoint.
*/
function is_single_column() {
return single_column_query.matches;
}
/**
* Opens the overlay showing the given code with line numbers. Restores any
* previously cached analysis for this code block.
* @param {string} code - The raw code text.
* @param {string} language - The highlight.js language name, may be empty.
*/
export function open_code_overlay(code, language) {
div_overlay = getEl('id-code-overlay');
div_view = getEl('id-code-overlay-view');
div_thinking = getEl('id-code-overlay-thinking');
div_items = getEl('id-code-overlay-items');
div_side = queryEl('.div-code-overlay-side', div_overlay);
if (!div_overlay || !div_view) return;
current_code = (code || '').replace(/\n$/, '');
current_language = language || '';
current_hash = hash_analysis_key(current_language, current_code);
current_lines = build_code_lines(current_code, current_language);
content_of(div_view).innerHTML = '';
const lines_wrap = document.createElement('div');
lines_wrap.className = 'div-code-overlay-lines' + (current_language ? ` language-${current_language}` : '');
for (const row of current_lines) {
lines_wrap.appendChild(row);
}
content_of(div_view).appendChild(lines_wrap);
const cached = typeof load_analysis === 'function' ? load_analysis(current_hash) : null;
if (cached && Array.isArray(cached.items)) {
restore_analysis(cached);
} else {
clear_results();
}
showOverlay(div_overlay, 'flex');
}
/**
* Computes the stable cache key for a code block from its language and text.
* @param {string} language - The highlight.js language name.
* @param {string} code - The normalized code text.
* @returns {string} The hash key.
*/
function hash_analysis_key(language, code) {
const text = (language || '') + '\u0000' + (code || '');
let hash = 5381;
for (let i = 0; i < text.length; i++) {
hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
}
return (hash >>> 0).toString(36) + '_' + text.length.toString(36);
}
/**
* Restores a previously cached analysis into the right column.
* @param {{items: Array, annotations?: Array, thinking?: string|string[], costs?: Object, model?: string, model_key?: string, tokens?: Object, tools?: Array, specialFeatures?: Array}} cached - The cached analysis data.
*/
function restore_analysis(cached) {
current_items = cached.items;
current_analysis_els = [];
selected_index = -1;
set_side_open(false);
div_overlay.classList.add('column-open', 'results-shown');
const lines = normalize_thinking(cached.thinking);
if (lines.length > 0) {
div_thinking.style.display = 'block';
content_of(div_thinking).innerHTML = build_thinking_block('Analysis Consideration', lines.map(l => formatThinkingLineHtml(l)).join(''), false);
} else {
div_thinking.style.display = 'none';
content_of(div_thinking).innerHTML = '';
}
div_items.style.display = 'block';
content_of(div_items).innerHTML = '';
render_analysis_items(cached.items);
render_analysis_sources(Array.isArray(cached.annotations) ? cached.annotations : []);
render_analysis_costs(cached);
void typesetMathjax(div_items);
const analyze_btn = getEl('btn-code-overlay-analyze');
if (analyze_btn) {
analyze_btn.textContent = 'Re-Analyze';
analyze_btn.disabled = false;
}
}
/**
* Closes the code overlay if it is open.
*/
export function close_code_overlay() {
if (!is_code_overlay_open()) return;
hideOverlay(div_overlay);
}
/**
* Binds all overlay interaction events. Call once during app initialization.
* @param {Object} [handlers={}] - Overlay handlers.
* @param {Function} [handlers.on_analyze] - Async handler called with
* (code, language, on_thinking) when Analyze is clicked; resolves to the
* analyze API response.
* @param {Function} [handlers.load_analysis] - Returns cached analysis data
* for a code block hash, or null.
* @param {Function} [handlers.save_analysis] - Persists analysis data for a
* code block hash.
* @param {number} [handlers.single_column_breakpoint] - Viewport width below
* which the layout is single-column; must match the stylesheet breakpoint.
*/
export function init_code_overlay(handlers = {}) {
div_overlay = getEl('id-code-overlay');
div_view = getEl('id-code-overlay-view');
div_thinking = getEl('id-code-overlay-thinking');
div_items = getEl('id-code-overlay-items');
div_side = queryEl('.div-code-overlay-side', div_overlay);
if (!div_overlay || !div_view) return;
on_analyze = typeof handlers.on_analyze === 'function' ? handlers.on_analyze : null;
load_analysis = typeof handlers.load_analysis === 'function' ? handlers.load_analysis : null;
save_analysis = typeof handlers.save_analysis === 'function' ? handlers.save_analysis : null;
if (Number.isFinite(handlers.single_column_breakpoint)) {
single_column_query = window.matchMedia('(max-width: ' + handlers.single_column_breakpoint + 'px)');
}
addSafeEventListener(getEl('btn-code-overlay-close'), 'click', close_code_overlay);
addSafeEventListener(getEl('btn-code-overlay-analyze'), 'click', on_analyze_click);
addSafeEventListener(getEl('btn-code-overlay-side'), 'click', on_side_toggle);
addSafeEventListener(div_side, 'click', on_side_click, true);
for (const btn of document.querySelectorAll('[data-code-nav]')) {
addSafeEventListener(btn, 'click', () => on_nav_click(btn.dataset.codeNav));
}
}
/**
* Adds an expand button to every code block in the container. The button
* opens the code overlay for that block.
* @param {HTMLElement} container - Container holding pre.code-block elements.
*/
export function attach_code_expand_buttons(container) {
if (!container) return;
for (const pre of queryAll('pre.code-block', container)) {
if (pre.closest('.div-diagram-mermaid, .mermaid')) continue;
const code = pre.querySelector('code');
if (!code || pre.querySelector('.div-code-expand-btn')) continue;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'div-code-expand-btn as-icon';
btn.title = 'Expand Code';
btn.textContent = '🔍';
btn.onclick = () => open_code_overlay(code.textContent || '', get_code_language(code));
pre.appendChild(btn);
}
}
/**
* Reads the highlight.js language from a code element's language- class.
* @param {HTMLElement} code - The code element.
* @returns {string} The language name or an empty string.
*/
function get_code_language(code) {
const match = (code.className || '').match(/language-([\w-]+)/);
return match ? match[1] : '';
}
/**
* Builds one row element per code line with a line number and highlighted
* content. Highlighting runs per line, so multi-line tokens are not colored
* across lines.
* @param {string} code - The raw code text.
* @param {string} language - The highlight.js language name.
* @returns {HTMLElement[]} The row elements.
*/
function build_code_lines(code, language) {
return code.split('\n').map((line, i) => {
const row = document.createElement('div');
row.className = 'div-code-overlay-line';
const num = document.createElement('span');
num.className = 'div-code-overlay-line-num';
num.textContent = String(i + 1);
const codeEl = document.createElement('code');
codeEl.innerHTML = highlight_line(line, language);
row.append(num, codeEl);
return row;
});
}
/**
* Highlights a single code line, falling back to escaped plain text.
* @param {string} line - The code line.
* @param {string} language - The highlight.js language name.
* @returns {string} HTML for the line content.
*/
function highlight_line(line, language) {
if (language) {
try {
return hljs.highlight(line, { language: language, ignoreIllegals: true }).value;
} catch (e) {
console.warn('Highlight.js failed to highlight line:', e);
}
}
return escapeHtml(line);
}
/**
* Slides the results panel in or out (single-column mode only).
* @param {boolean} open - True to show the panel, false to leave only its edge visible.
*/
function set_side_open(open) {
side_open = open;
if (div_side) {
div_side.classList.toggle('side-open', open);
}
const toggle_btn = getEl('btn-code-overlay-side');
if (toggle_btn) {
toggle_btn.textContent = open ? 'Close Analysis' : 'Open Analysis';
toggle_btn.title = open ? 'Close Analysis' : 'Open Analysis';
}
}
/**
* Handles the Analysis button in the navigation bar: toggles the sliding
* results panel (single-column mode only).
*/
function on_side_toggle() {
if (!is_single_column()) return;
set_side_open(!side_open);
}
/**
* Handles a horizontal swipe while the code overlay is open by opening or
* closing the sliding analysis column (single-column mode only).
* @param {boolean} open - True to open the analysis column, false to close it.
* @returns {boolean} True if the overlay is open and handled the swipe.
*/
export function swipe_code_overlay_analysis(open) {
if (!is_code_overlay_open() || !is_single_column()) return false;
set_side_open(open);
return true;
}
/**
* Handles taps on the results panel in single-column mode: tapping the visible
* edge sliver opens the panel, tapping its left edge strip closes it. Taps on
* the panel body while open fall through to the items underneath.
* @param {MouseEvent} e - The click event.
*/
function on_side_click(e) {
if (!div_side || !div_overlay || !is_single_column()) return;
if (!div_overlay.classList.contains('column-open')) return;
if (!side_open) {
set_side_open(true);
e.stopPropagation();
return;
}
const rect = div_side.getBoundingClientRect();
const edge = parseFloat(getComputedStyle(div_side).fontSize) * 2;
if (e.clientX - rect.left <= edge) {
set_side_open(false);
e.stopPropagation();
}
}
/**
* Removes the highlight from all code lines.
*/
function clear_line_highlights() {
for (const row of current_lines) {
row.classList.remove('code-line-highlighted');
}
}
/**
* Applies the highlight to a 1-based inclusive range of code lines.
* @param {number} from - First line number.
* @param {number} to - Last line number.
*/
function apply_line_highlight(from, to) {
for (let i = Math.max(1, from); i <= Math.min(current_lines.length, to); i++) {
current_lines[i - 1].classList.add('code-line-highlighted');
}
}
/**
* Hides and clears the thinking and analysis results column, resets the
* header state, and re-enables the Analyze button.
*/
function clear_results() {
if (div_overlay) {
div_overlay.classList.remove('column-open', 'results-shown');
}
set_side_open(false);
if (div_thinking) {
div_thinking.style.display = 'none';
content_of(div_thinking).innerHTML = '';
}
if (div_items) {
div_items.style.display = 'none';
content_of(div_items).innerHTML = '';
}
const analyze_btn = getEl('btn-code-overlay-analyze');
if (analyze_btn) {
analyze_btn.disabled = false;
analyze_btn.textContent = 'Analyze';
}
current_items = [];
current_analysis_els = [];
selected_index = -1;
}
/**
* Handles the Previous/Next/Clear navigation buttons.
* @param {string} action - One of 'prev', 'next', 'clear'.
*/
function on_nav_click(action) {
if (!div_items) return;
if (action === 'clear') {
clear_selection();
return;
}
if (current_analysis_els.length === 0) return;
if (action === 'prev') {
select_analysis_item(selected_index < 0 ? current_analysis_els.length - 1 : selected_index - 1);
} else if (action === 'next') {
select_analysis_item(selected_index + 1);
}
}
/**
* Removes the selection from the analysis items and the code lines.
*/
function clear_selection() {
for (const el of current_analysis_els) {
el.classList.remove('selected');
}
clear_line_highlights();
selected_index = -1;
}
/**
* Normalizes the thinking field into an array of lines.
* @param {string|string[]|undefined} thinking - Thought lines or newline-separated thoughts.
* @returns {string[]} The thought lines.
*/
function normalize_thinking(thinking) {
if (Array.isArray(thinking)) return thinking.map(String);
if (typeof thinking === 'string' && thinking.length > 0) return thinking.split('\n').filter(l => l.length > 0);
return [];
}
/**
* Handles the Analyze button click: opens the results column, submits the
* code to the analyze handler, streams the thinking, and renders the findings.
* Once results are shown, asks for confirmation before re-analyzing.
*/
async function on_analyze_click() {
if (!on_analyze || !div_items || !div_thinking || !div_overlay) return;
if (div_overlay.classList.contains('results-shown')) {
const confirmed = await customConfirm('Re-Analyze', 'Do you want to re-analyze this code with the current conversation settings?');
if (!confirmed) return;
}
const analyze_btn = getEl('btn-code-overlay-analyze');
if (analyze_btn) {
analyze_btn.disabled = true;
}
div_overlay.classList.add('column-open');
current_items = [];
current_analysis_els = [];
selected_index = -1;
set_side_open(false);
clear_line_highlights();
div_items.style.display = 'block';
content_of(div_items).innerHTML = '';
div_thinking.style.display = 'block';
content_of(div_thinking).innerHTML = build_thinking_block('Analyzing...');
const thinking_content = div_thinking.querySelector('.instant-answer-thoughts-content');
const streamed_lines = [];
const on_thinking = (thoughtText) => {
if (!thoughtText || !thinking_content || !div_thinking) return;
const lines = String(thoughtText).split('\n').filter(l => l.length > 0);
streamed_lines.push(...lines);
append_following_bottom(scroller_of(div_thinking), () => thinking_content.insertAdjacentHTML('beforeend', lines.map(l => formatThinkingLineHtml(l)).join('')));
};
try {
const response = await on_analyze(current_code, current_language, on_thinking);
const details = div_thinking.querySelector('details');
if (details) {
const summary = details.querySelector('summary');
if (summary) {
summary.textContent = 'Analysis Consideration';
}
details.open = false;
}
if (response && response.error) {
content_of(div_items).innerHTML = `<span class="span-status-error">${escapeHtml(response.error)}</span>`;
return;
}
const final_lines = normalize_thinking(response?.thinking);
const lines = final_lines.length > 0 ? final_lines : streamed_lines;
if (thinking_content) {
thinking_content.classList.remove('streaming');
if (lines.length > 0) {
thinking_content.innerHTML = lines.map(l => formatThinkingLineHtml(l)).join('');
} else {
div_thinking.style.display = 'none';
}
}
const items = Array.isArray(response) ? response : (Array.isArray(response?.items) ? response.items : []);
current_items = items;
current_analysis_els = [];
selected_index = -1;
render_analysis_items(items);
render_analysis_sources(Array.isArray(response?.annotations) ? response.annotations : []);
render_analysis_costs(response);
await typesetMathjax(div_items);
div_overlay.classList.add('results-shown');
if (analyze_btn) {
analyze_btn.textContent = 'Re-Analyze';
}
if (typeof save_analysis === 'function' && current_hash) {
save_analysis(current_hash, {
items: items,
annotations: Array.isArray(response?.annotations) ? response.annotations : [],
thinking: lines,
costs: response?.costs,
model: response?.model,
model_key: response?.model_key,
tokens: response?.tokens,
tools: response?.tools,
specialFeatures: response?.specialFeatures,
timestamp: Date.now()
});
}
} catch (e) {
console.error('Code analysis failed:', e);
content_of(div_items).innerHTML = '<span class="span-status-error">Code analysis failed.</span>';
} finally {
if (analyze_btn) {
analyze_btn.disabled = false;
}
}
}
/**
* Renders the analysis findings as clickable items.
* @param {Array<{lineStart: number, lineEnd: number, type: string, title: string, analysis: string}>} items
*/
function render_analysis_items(items) {
content_of(div_items).innerHTML = '';
if (!Array.isArray(items) || items.length === 0) {
content_of(div_items).innerHTML = '<em>No findings.</em>';
return;
}
for (const item of items) {
if (!item || typeof item !== 'object') continue;
const el = document.createElement('div');
el.className = 'code-analysis-item';
el.dataset.type = get_item_type(item);
const title = document.createElement('div');
title.className = 'code-analysis-item-title';
const type = document.createElement('span');
type.className = 'code-analysis-item-type';
type.textContent = get_item_type(item);
title.append(type, document.createTextNode(item.title || 'Finding'));
const body = document.createElement('div');
body.className = 'code-analysis-item-analysis';
body.innerHTML = sanitize_analysis_html(item.analysis);
el.append(title, body);
const index = current_analysis_els.length;
current_analysis_els.push(el);
el.onclick = () => select_analysis_item(index);
content_of(div_items).appendChild(el);
}
}
/**
* Returns the normalized item type, one of EXPLANATION, CAUTION, WARNING.
* @param {{type?: string}} item - The analysis item.
* @returns {string} The item type.
*/
function get_item_type(item) {
const type = String(item.type || '').toUpperCase();
return ['EXPLANATION', 'CAUTION', 'WARNING'].includes(type) ? type : 'EXPLANATION';
}
/**
* Renders the search source links below the items list.
* @param {Array<{title: string, url: string}>} annotations - Source citations.
*/
function render_analysis_sources(annotations) {
const sources_html = formatAnnotationsBlock(annotations);
if (sources_html) {
content_of(div_items).insertAdjacentHTML('beforeend', sources_html);
}
}
/**
* Renders the cost summary below the items list.
* @param {object} response - The full analyze API response.
*/
function render_analysis_costs(response) {
const costs_html = formatCostSummary(response);
if (costs_html) {
content_of(div_items).insertAdjacentHTML('beforeend', costs_html);
}
}
/**
* Selects an analysis item by index and highlights its code lines.
* @param {number} index - The item index.
*/
function select_analysis_item(index) {
if (index < 0 || index >= current_analysis_els.length) return;
for (const other of current_analysis_els) {
other.classList.remove('selected');
}
const el = current_analysis_els[index];
const item = current_items[index];
el.classList.add('selected');
clear_line_highlights();
if (item) {
const from = Math.min(item.lineStart, item.lineEnd);
const to = Math.max(item.lineStart, item.lineEnd);
apply_line_highlight(from, to);
scroll_code_to_range(from, to);
}
selected_index = index;
scroll_item_into_view(el);
}
/**
* Scrolls the findings list so the given item is visible. Done manually
* because scrollIntoView would also scroll the overflow:hidden overlay
* ancestors, shifting the layout and the sliding results panel.
* @param {HTMLElement} el - The analysis item element.
*/
function scroll_item_into_view(el) {
if (!div_items) return;
const sc = scroller_of(div_items);
const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop;
if (top < sc.scrollTop) {
sc.scrollTop = top;
} else if (top + el.offsetHeight > sc.scrollTop + sc.clientHeight) {
sc.scrollTop = top + el.offsetHeight - sc.clientHeight;
}
}
/**
* Scrolls the code view so the highlighted line range is in view, vertically
* centered when the whole region fits in the viewport, and aligned to the top
* of the viewport otherwise.
* @param {number} from - First highlighted line number.
* @param {number} to - Last highlighted line number.
*/
function scroll_code_to_range(from, to) {
if (!div_view || current_lines.length === 0) return;
const first = current_lines[Math.max(1, from) - 1];
const last = current_lines[Math.min(current_lines.length, to) - 1];
if (!first || !last) return;
const sc = scroller_of(div_view);
const view_rect = sc.getBoundingClientRect();
const first_rect = first.getBoundingClientRect();
const last_rect = last.getBoundingClientRect();
const region_top = first_rect.top - view_rect.top + sc.scrollTop;
const region_height = last_rect.bottom - first_rect.top;
let target;
if (region_height <= view_rect.height) {
target = region_top - (view_rect.height - region_height) / 2;
} else {
target = region_top;
}
sc.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
}
/**
* Sanitizes analysis HTML to the instant answer element whitelist minus
* links: drops a elements and inline event handlers.
* @param {string} html - The raw analysis HTML.
* @returns {string} The sanitized HTML.
*/
function sanitize_analysis_html(html) {
return String(html || '')
.replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, '$1')
.replace(/<\/?a\b[^>]*>/gi, '')
.replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, '');
}
code-overlay.js
×
Type: Web, text/x-java
21.77 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
21.77 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File