Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_prompt_reference.js
T
DemonGatanjieuandClaude Opus 5.5 653386ad6e Prompt Studio: a clicked card opens as a reference to take single tags
A user asked to pick tags from a saved prompt instead of pouring the
whole card in. A clicked card now opens between the cards and the prompt
being edited (ui_prompt_reference.js): a click on one of its tags adds
that tag, Add all the whole card, ticks show what the box has. The
preview's Add and dragging a card work as before; beside the canvas the
drawer widens by the column while it is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:31:20 +08:00

162 lines
6.7 KiB
JavaScript

/**
* Prompt Studio's reference column: a clicked card opens here, between the cards and the prompt
* boxes, as its tags (with their meanings when a language is picked), so single tags can be
* taken from it: a click puts that tag into the box the card goes into (ui_prompt_target.js),
* "Add all" the whole card. Tags the box has already are ticked. Read only: the card itself is
* never changed here. In the drawer it widens the drawer by the column while it is open.
*/
import { translate as t } from './locales.js';
import { fetchGlosses, glossOf } from './prompt_gloss.js';
import { promptTagsOf, tagKey, tagWeight } from './prompt_tags.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
const POLL_MS = 400;
const EXTRA_WIDTH = 280; // the drawer grows by this while the column is open
const HINT_KEY = 'anomalous_prompt_reference_hint';
const ROLE_KEYS = { positive: 'recipePromptRolePositive', negative: 'recipePromptRoleNegative' };
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;
}
/** Says once, the first time a card opens here, that a click no longer adds the whole card. */
function hintOnce() {
try {
if (localStorage.getItem(HINT_KEY)) return;
localStorage.setItem(HINT_KEY, '1');
} catch { /* no storage here: say it each visit */ }
showWorkbenchToast(t('promptStudioRefHint'));
}
/**
* Fills `grid` (between the cards and the boxes). `target` is the prompt side ({ addCard(card,
* text), valueFor(card) }); `prefs` the studio's { glossLang }. Returns { open(card), close(), render() }.
*/
export function createPromptReference(grid, { drawer, scope, prefs, target }) {
const panel = el('section', 'anomalous-ps-ref');
panel.hidden = true;
const head = el('header', 'anomalous-ps-target-head');
const label = el('div', 'anomalous-ps-target-label');
const title = el('span', 'anomalous-ps-target-name');
const closeButton = button('anomalous-ps-target-x', '✕', t('promptStudioRefClose'), () => close());
closeButton.setAttribute('aria-label', t('promptStudioRefClose'));
label.append(title, closeButton);
const addAll = button('anomalous-ps-mini', t('promptStudioRefAddAll'), t('promptStudioRefAddAllHint'), () => {
if (card) void target.addCard(card).then(render);
});
head.append(label, addAll);
const role = el('span', 'anomalous-ps-role');
const note = el('p', 'anomalous-ps-target-note', t('promptStudioRefNote'));
const tagsEl = el('div', 'anomalous-ps-tags');
panel.append(head, role, note, tagsEl);
grid.insertBefore(panel, grid.children[1] || null); // right after the cards
let card = null;
let seen = null; // the box text the ticks were drawn from
let taken = new Set(); // tags taken from this card here (a translated one reads differently in the box)
let widened = null; // { from, to } drawer widths while the column is open
let glossTimer = null;
let glossFailed = '';
const timer = setInterval(() => {
if (card && target.valueFor(card) !== seen) render();
}, POLL_MS);
scope.onDispose(() => { clearInterval(timer); clearTimeout(glossTimer); });
function widen(open) {
if (!drawer.classList.contains('anomalous-prompt-studio-drawer')) return;
if (open && !widened) {
const from = drawer.getBoundingClientRect().width;
const to = Math.round(Math.min(window.innerWidth * 0.85, from + EXTRA_WIDTH));
drawer.style.width = `${to}px`;
widened = { from, to };
} else if (!open && widened) {
// Dragged wider or narrower meanwhile: that width is the user's now.
if (Math.round(drawer.getBoundingClientRect().width) === widened.to) drawer.style.width = `${widened.from}px`;
widened = null;
}
}
function glossText(tag) {
const meaning = glossOf(tag, prefs.glossLang);
return meaning === undefined ? (glossFailed === prefs.glossLang ? '' : '…') : meaning;
}
function wantGlosses(tags) {
clearTimeout(glossTimer);
const lang = prefs.glossLang;
if (!lang || glossFailed === lang || tags.every(tag => glossOf(tag, lang) !== undefined)) return;
glossTimer = setTimeout(async () => {
const ok = await fetchGlosses(tags, lang, scope.signal);
if (scope.signal.aborted || !card) return;
if (!ok) glossFailed = lang;
render();
}, 250);
}
function chipFor(tag, have) {
const { core, weight } = tagWeight(tag);
const there = have.has(tagKey(tag)) || taken.has(tagKey(tag));
const chip = el('button', `anomalous-ps-tag is-ref${there ? ' is-there' : ''}`);
chip.type = 'button';
chip.title = t(there ? 'promptStudioRefThere' : 'promptStudioRefTake');
const main = el('span', 'anomalous-ps-tag-main');
if (there) main.append(el('span', 'anomalous-ps-ref-tick', '✓'));
main.append(el('span', 'anomalous-ps-tag-text', core));
if (weight !== 1) main.append(el('span', 'anomalous-ps-tag-weight', String(weight)));
chip.append(main);
if (prefs.glossLang) chip.append(el('span', 'anomalous-ps-tag-gloss', glossText(tag)));
chip.onclick = async () => {
if (there) return;
chip.disabled = true;
if (await target.addCard(card, tag)) taken.add(tagKey(tag));
render();
};
return chip;
}
/** Draws the open card's tags, ticking those its box has. */
function render() {
if (!card) return;
seen = target.valueFor(card);
const have = new Set(promptTagsOf(seen ?? '').map(tagKey));
const tags = promptTagsOf(card.content);
title.textContent = t('promptStudioRefTitle', { name: card.title });
const cardRole = card.role === 'negative' ? 'negative' : 'positive';
role.className = `anomalous-ps-role is-${cardRole}`;
role.textContent = t(ROLE_KEYS[cardRole]);
tagsEl.replaceChildren(...tags.map(tag => chipFor(tag, have)));
wantGlosses(tags);
}
function open(next) {
hintOnce();
if (card?.id !== next.id) taken = new Set();
card = next;
panel.hidden = false;
grid.classList.add('has-ref');
widen(true);
render();
}
function close() {
card = null;
panel.hidden = true;
grid.classList.remove('has-ref');
widen(false);
}
return { open, close, render };
}