Files
DemonGatanjieuandClaude Opus 5.5 ea6b29d783 Prompt Studio: in a narrow window the reference takes the cards' place
Three columns in a side-panel window left each about 200px, tags one per
line and a sideways scroll. Below 980px the open reference now replaces
the card list (two columns of half the width each), and "‹ Cards" brings
the list back; wide windows keep three columns.

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

167 lines
7.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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; where
* three columns do not fit it takes the cards' place, and "‹ Cards" goes back (the stylesheet).
*/
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 }; `onShow(card)` hears which card is
* open (null once closed). Returns { open(card), close(), render() }.
*/
export function createPromptReference(grid, { drawer, scope, prefs, target, onShow }) {
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);
});
const back = button('anomalous-ps-mini anomalous-ps-ref-back', t('promptStudioRefBack'), t('promptStudioRefClose'), () => close());
head.append(back, 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();
onShow(card);
}
function close() {
card = null;
panel.hidden = true;
grid.classList.remove('has-ref');
widen(false);
onShow(null);
}
return { open, close, render };
}