-
🌐 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⬇
import ScratchpadModel from '../scratchpad-model.js';
import { scroller_of } from './scrollbars.js';
/** @typedef {import('../app.js').default} App */
let flash_el = null;
let flash_timer = null;
/**
* Returns the scratchpad revision a request operates on: the pending revision
* while a request runs, otherwise the latest revision.
* @param {App} app - The main application instance.
* @param {string} guid - The scratchpad conversation guid.
* @returns {{conversation: Object, history: Array, revision: Object, index: number}|null} The revision context, or null when absent.
*/
export function get_scratchpad_revision(app, guid) {
if (!guid) return null;
const conversation = app.storage.get_conversation(guid);
const history = conversation?.[app.storage.KEY_CONVERSATION_HISTORY];
if (!Array.isArray(history) || history.length === 0) return null;
const index = history.length - 1;
const revision = history[index];
if (!revision || typeof revision !== 'object') return null;
return { conversation, history, revision, index };
}
/**
* Applies an edit_scratchpad edit to the guid's scratchpad revision: mutates
* the revision's items and table of contents (a removed item drops the toc
* entries pointing at it), saves, and refreshes the live view.
* @param {App} app - The main application instance.
* @param {string} guid - The scratchpad conversation guid.
* @param {Object} edit - The normalized edit payload.
* @returns {{result: Object, state: {items: Array, toc: Array}}|null} The apply result and after-state, or null when the edit cannot apply.
*/
export function apply_scratchpad_edit(app, guid, edit) {
const ctx = get_scratchpad_revision(app, guid);
if (!ctx) return null;
const model = new ScratchpadModel(Array.isArray(ctx.revision.items) ? ctx.revision.items : [], Array.isArray(ctx.revision.toc) ? ctx.revision.toc : []);
const result = model.apply_edit(edit);
if (!result) return null;
const state = model.get_state();
ctx.revision.items = state.items;
ctx.revision.toc = state.toc;
app.storage.save_conversation(guid, ctx.conversation);
void refresh_scratchpad_view(app, guid, result.flashGuid);
return { result, state };
}
/**
* Applies an edit_scratchpad_toc edit to the guid's scratchpad revision,
* saves, and refreshes the live view. The drawer opens when the first
* entries arrive while it is closed.
* @param {App} app - The main application instance.
* @param {string} guid - The scratchpad conversation guid.
* @param {Object} edit - The normalized edit payload.
* @returns {{result: Object, state: {items: Array, toc: Array}}|null} The apply result and after-state, or null when the edit cannot apply.
*/
export function apply_scratchpad_toc_edit(app, guid, edit) {
const ctx = get_scratchpad_revision(app, guid);
if (!ctx) return null;
const model = new ScratchpadModel(Array.isArray(ctx.revision.items) ? ctx.revision.items : [], Array.isArray(ctx.revision.toc) ? ctx.revision.toc : []);
const had_entries = model.toc.length > 0;
const result = model.apply_toc_edit(edit);
if (!result) return null;
const state = model.get_state();
ctx.revision.items = state.items;
ctx.revision.toc = state.toc;
app.storage.save_conversation(guid, ctx.conversation);
if (!had_entries && state.toc.length > 0 && app.scratchpad_drawer && !app.scratchpad_drawer.is_open()) {
app.scratchpad_drawer.set_open(true);
}
void refresh_scratchpad_view(app, guid, result.flashGuid);
return { result, state };
}
/**
* Re-renders the scratchpad view when the guid is the displayed conversation
* (the render refreshes the table of contents drawer) and flashes the
* affected item. Background scratchpads only refresh their index entry.
* @param {App} app - The main application instance.
* @param {string} guid - The scratchpad conversation guid.
* @param {string|null} [flashGuid=null] - The item guid to flash after the render.
*/
export async function refresh_scratchpad_view(app, guid, flashGuid = null) {
const selected = app.storage.get_app_config()[app.storage.KEY_CONFIG_SELECTED_CONVERSATION_GUID];
if (guid !== selected) {
app.scratchpad_index?.on_conversation_index_updated?.();
return;
}
await app.chat_renderer.render_scratchpad_conversation(false);
if (flashGuid) {
flash_scratchpad_item(app, flashGuid);
}
}
/**
* Rebuilds the table of contents drawer content for a scratchpad.
* @param {App} app - The main application instance.
* @param {string} guid - The scratchpad conversation guid.
*/
export function refresh_scratchpad_drawer(app, guid) {
if (!app.scratchpad_drawer) return;
const ctx = get_scratchpad_revision(app, guid);
app.scratchpad_drawer.set_source(guid);
render_scratchpad_toc(app, ctx?.revision?.items || [], ctx?.revision?.toc || []);
}
/**
* Renders the scratchpad table of contents into the side drawer, sorted by
* the position of each entry's target item. Entries show their title with
* the optional short description beneath, and click scrolls to the target
* item with the flash-fade; entries whose target no longer exists render
* dimmed at the end.
* @param {App} app - The main application instance.
* @param {Array} items - The scratchpad items.
* @param {Array} toc - The table of contents entries.
*/
export function render_scratchpad_toc(app, items, toc) {
if (!app.scratchpad_drawer) return;
const entries = ScratchpadModel.sort_toc(toc, items);
if (entries.length === 0) {
app.scratchpad_drawer.clear({ close_if_empty: true });
return;
}
const panel = document.createElement('div');
panel.className = 'div-scratchpad-toc';
const title = document.createElement('div');
title.className = 'div-scratchpad-toc-title';
title.textContent = 'Table of Contents';
panel.appendChild(title);
const position = new Map((Array.isArray(items) ? items : []).map((item, index) => [item?.guid, index]));
for (const entry of entries) {
const target_index = position.get(entry?.target_guid);
const entry_el = document.createElement('button');
entry_el.type = 'button';
entry_el.className = 'div-scratchpad-toc-entry' + (target_index === undefined ? ' toc-entry-orphan' : '');
const title_el = document.createElement('span');
title_el.className = 'span-scratchpad-toc-title';
title_el.textContent = entry.title || 'Untitled';
entry_el.appendChild(title_el);
if (entry.description) {
const description_el = document.createElement('span');
description_el.className = 'span-scratchpad-toc-description';
description_el.textContent = entry.description;
entry_el.appendChild(description_el);
}
entry_el.onclick = () => {
if (target_index === undefined) return;
app.scratchpad_drawer.set_open(true);
flash_scratchpad_item(app, entry.target_guid);
};
panel.appendChild(entry_el);
}
app.scratchpad_drawer.set_content(panel);
}
/**
* Highlights a scratchpad item by guid and scrolls it into view, used as
* feedback for tool edits and table of contents clicks. The highlight fades
* out over three seconds through the scratchpad-item-flash animation.
* @param {App} app - The main application instance.
* @param {string} guid - The item guid.
*/
export function flash_scratchpad_item(app, guid) {
clear_scratchpad_flash();
if (!guid) return;
const el = app.div_response?.querySelector(`[data-guid="${guid}"]`) || null;
if (!el) return;
flash_el = el;
// restart the animation when the same item flashes again
void el.offsetWidth;
el.classList.add('scratchpad-item-flash');
scroll_item_into_view(el);
flash_timer = setTimeout(() => clear_scratchpad_flash(), 3100);
}
/**
* Removes the flash highlight from the previously flashed item.
*/
export function clear_scratchpad_flash() {
if (flash_timer) {
clearTimeout(flash_timer);
flash_timer = null;
}
if (flash_el) {
flash_el.classList.remove('scratchpad-item-flash');
flash_el = null;
}
}
/**
* Scrolls the chat container so an item is fully visible: an item coming in
* from below is anchored at its bottom edge, one coming from above at its
* top edge, and an item taller than the view is anchored at its bottom edge.
* A fully visible item does not scroll. The jump is instant so rapid edit
* batches cannot interrupt a smooth animation halfway.
* @param {HTMLElement} el - The item element.
*/
function scroll_item_into_view(el) {
const scroll = scroller_of(document.getElementById('id-chat-container-scroll'));
if (!scroll) {
el.scrollIntoView({ block: 'end' });
return;
}
const view = scroll.getBoundingClientRect();
const rect = el.getBoundingClientRect();
let top = null;
if (rect.height > view.height || rect.bottom > view.bottom) {
top = rect.bottom - view.bottom + scroll.scrollTop;
} else if (rect.top < view.top) {
top = rect.top - view.top + scroll.scrollTop;
}
if (top === null) return;
scroll.scrollTo({ top, behavior: 'auto' });
}
scratchpad-editor.js
×
Type: Web, text/x-java
8.64 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File
Type: Web, text/x-java
8.64 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File