Files
DemonGatanjieuandClaude Opus 5.5 48ad658d0c Prompt Studio: cards drawn as notebook pages
The card list read as plain grey rows. Each card is now a page with a
bound spine in its role's colour, its title, word count and role label,
and its first tags on two ruled lines; Common and My prompts are index
tabs with their counts, and the card open as the reference wears a
ribbon (deck.setOpen, fed by the reference column's onShow).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 01:16:27 +08:00

148 lines
6.6 KiB
JavaScript

/**
* Prompt Studio's content (the Prompts page or the drawer beside the canvas, ui_prompt_composer.js):
* a top bar with the view switch (tags or the whole text), the language tags' meanings are shown
* in (or none), and on the page "Beside the canvas", in the drawer its dock side, "Back to the
* window" and Close; the cards on the drawer's outer side (ui_prompt_source_deck.js) and
* the prompt boxes beside the canvas (ui_prompt_target.js). A clicked card opens between them as
* a reference (ui_prompt_reference.js) to take its tags one by one; Save on a box keeps its text
* as a card under My prompts. The view and the meaning
* language are remembered in this browser.
*/
import { translate as t } from './locales.js';
import { GLOSS_LANGUAGES } from './prompt_gloss.js';
import { promptTitle } from './prompt_composition.js';
import { savePromptCard } from './prompt_material_source.js';
import { anomalousPrompt } from './ui_dialog.js';
import { createPromptReference } from './ui_prompt_reference.js';
import { createPromptSourceDeck } from './ui_prompt_source_deck.js';
import { createPromptTarget } from './ui_prompt_target.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
const PREFS_KEY = 'anomalous_prompt_studio_prefs';
const ARROW_RIGHT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>';
const ARROW_LEFT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>';
function el(tag, className, content) {
const node = document.createElement(tag);
if (className) node.className = className;
if (content !== undefined) node.textContent = content;
return node;
}
function button(className, label, title, onClick) {
const node = el('button', className, label);
node.type = 'button';
if (title) node.title = title;
node.onclick = onClick;
return node;
}
function loadPrefs() {
try {
const saved = JSON.parse(localStorage.getItem(PREFS_KEY) || '{}');
// An older switch kept only "Chinese on".
const lang = saved.glossLang ?? (saved.gloss === true ? 'zh-CN' : '');
return { view: saved.view === 'text' ? 'text' : 'tags', glossLang: GLOSS_LANGUAGES.some(([code]) => code === lang) ? lang : '' };
} catch {
return { view: 'tags', glossLang: '' };
}
}
function savePrefs(prefs) {
try { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } catch { /* remembered for this visit only */ }
}
/**
* Fills `drawer` (the page's host or the drawer). `mode`: 'page' (`onDock()` moves it beside the
* canvas) or 'docked' (`onToggleDockSide()` moves the drawer and returns whether it is now on the
* left, `onUndock()` brings it back to the page, `onClose()` closes it).
*/
export function createPromptWorkbench(owner, drawer, scope, { mode = 'docked', onClose, onToggleDockSide, onDock, onUndock }) {
const prefs = loadPrefs();
const view = el('section', 'anomalous-ps');
const top = el('header', 'anomalous-ps-top');
// On the page the browser's header already names it.
if (mode === 'docked') top.append(el('h3', 'anomalous-ps-title', t('promptStudioTitle')));
const controls = el('div', 'anomalous-ps-top-controls');
const viewSwitch = el('div', 'anomalous-ps-switch');
viewSwitch.setAttribute('role', 'group');
viewSwitch.title = t('promptStudioViewHint');
const viewButtons = ['tags', 'text'].map(mode => {
const item = button('anomalous-ps-switch-btn', t(mode === 'tags' ? 'promptStudioViewTags' : 'promptStudioViewText'), '', () => {
prefs.view = mode;
savePrefs(prefs);
drawSwitches();
target.render();
});
item.dataset.mode = mode;
return item;
});
viewSwitch.append(...viewButtons);
controls.append(viewSwitch);
const glossPicker = el('select', 'anomalous-ps-gloss');
glossPicker.title = t('promptStudioGlossHint');
glossPicker.setAttribute('aria-label', t('promptStudioGlossHint'));
glossPicker.append(new Option(t('promptStudioGlossOff'), ''),
...GLOSS_LANGUAGES.map(([code, name]) => new Option(t('promptStudioGlossIn', { language: name }), code)));
glossPicker.onchange = () => {
prefs.glossLang = glossPicker.value;
savePrefs(prefs);
drawSwitches();
target.render();
reference.render();
};
controls.append(glossPicker);
if (mode === 'page') {
controls.append(button('anomalous-ps-mini', t('promptStudioDock'), t('promptStudioDockHint'), onDock));
} else {
const dock = button('anomalous-ps-icon', '', '', () => {
onToggleDockSide();
drawDock();
});
const drawDock = () => {
const left = drawer.classList.contains('is-dock-left');
dock.innerHTML = left ? ARROW_RIGHT : ARROW_LEFT;
dock.title = t(left ? 'promptStudioDockRight' : 'promptStudioDockLeft');
};
drawDock();
controls.append(dock, button('anomalous-ps-mini', t('promptStudioUndock'), t('promptStudioUndockHint'), onUndock),
button('anomalous-ps-icon anomalous-ps-close', '✕', t('close'), onClose));
}
top.append(controls);
function drawSwitches() {
for (const item of viewButtons) item.classList.toggle('is-active', item.dataset.mode === prefs.view);
glossPicker.value = prefs.glossLang;
glossPicker.classList.toggle('is-active', Boolean(prefs.glossLang));
}
drawSwitches();
const grid = el('div', 'anomalous-ps-grid');
view.append(top, grid);
drawer.append(view);
async function save(content, role) {
const name = String(await anomalousPrompt(t('promptStudioSaveName'), promptTitle(content), t('promptStudioSave')) || '').trim();
if (!name || scope.signal.aborted) return;
try {
const result = await savePromptCard({ name, content, role });
showWorkbenchToast(result.status === 'duplicate' ? t('promptStudioSavedBefore', { name: result.name }) : t('promptStudioSaved', { name }));
if (!scope.signal.aborted) void deck.sync();
} catch {
showWorkbenchToast(t('promptStudioSaveFailed'));
}
}
const deck = createPromptSourceDeck(grid, drawer, scope, {
onPick: card => reference.open(card),
onAdd: card => void target.addCard(card),
});
const target = createPromptTarget(grid, { owner, drawer, scope, prefs, onSave: save });
// Between the cards and the boxes, whichever side the drawer is docked on.
const reference = createPromptReference(grid, { drawer, scope, prefs, target, onShow: card => deck.setOpen(card?.id ?? null) });
}