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>
167 lines
7.1 KiB
JavaScript
167 lines
7.1 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; 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 };
|
||
}
|