feat(ui): Composer Shortcuts tab lists, adds, edits and deletes the shortcut library
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
bf480e4a15
commit
86dafe9b10
@@ -1,6 +1,7 @@
|
||||
// ◆ Composer: the floating "prompt craft" window. Opens from the timeline header; its tabs are the five v4 had.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { applyStory, joinStory } from "./story.js";
|
||||
import { shortcuts } from "./shortcuts.js";
|
||||
|
||||
const TABS = [{ value: "story", label: "Story" }, { value: "shortcuts", label: "Shortcuts" }, { value: "cuts", label: "Cuts" }, { value: "enhance", label: "Enhance" }, { value: "chat", label: "Chat" }];
|
||||
const inert = (label, tone = "ghost") => c.button.sm({ label, tone, disabled: true });
|
||||
@@ -37,7 +38,7 @@ const story = (app, own) => {
|
||||
] });
|
||||
};
|
||||
|
||||
const sheets = { story, shortcuts: () => later("Your trigger → replacement library."), cuts: () => later("Where a story splits into shots."),
|
||||
const sheets = { story, shortcuts, cuts: () => later("Where a story splits into shots."),
|
||||
enhance: () => later("Rewrite a prompt with a language model."), chat: () => later("Talk a scene through with the enhancer.") };
|
||||
|
||||
export default {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Shortcuts: the library of trigger -> replacement phrases, expanded into a prompt when it is generated.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
const lines = (text) => String(text || "").split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
|
||||
export function shortcuts(app, own) {
|
||||
const page = c.region.stack({ gap: "sm" });
|
||||
let alive = true;
|
||||
own(() => { alive = false; });
|
||||
const tell = (text) => c.toast.warn({ text });
|
||||
|
||||
function edit(existing) {
|
||||
const draft = { name: "", triggers: [], replacements: [], enabled: true, category: "", sub_category: "", ...(existing || {}) };
|
||||
const field = (label, control, hint) => c.field.default({ label, hint, control });
|
||||
const win = c.modal.generic({ title: existing ? `Edit “${existing.name}”` : "New shortcut", size: "md", body: c.region.stack({ gap: "sm", children: [
|
||||
field("Name", c.input.md({ label: "Name", value: draft.name, onInput: (v) => { draft.name = v; } })),
|
||||
field("Triggers", c.textarea.md({ label: "Triggers", rows: 3, value: draft.triggers.join("\n"), onInput: (v) => { draft.triggers = lines(v); } }), "One word or phrase per line. Any of them fires the shortcut."),
|
||||
field("Replacements", c.textarea.md({ label: "Replacements", rows: 5, value: draft.replacements.join("\n"), onInput: (v) => { draft.replacements = lines(v); } }), "One per line; with several, one is drawn each time."),
|
||||
c.field.row({ fields: [field("Category", c.input.md({ label: "Category", value: draft.category, onInput: (v) => { draft.category = v; } })), field("Sub-category", c.input.md({ label: "Sub-category", value: draft.sub_category, onInput: (v) => { draft.sub_category = v; } }))] }),
|
||||
c.toggle.default({ label: "Enabled", checked: draft.enabled, onChange: (v) => { draft.enabled = v; } }),
|
||||
] }) });
|
||||
const save = async () => {
|
||||
try { draw(await app.api.saveShortcut(draft, existing && existing.name)); win.close("done"); } catch (err) { tell(err.message); }
|
||||
};
|
||||
const remove = async () => {
|
||||
if (!(await c.modal.dialogue({ title: "Delete shortcut", message: `Delete “${existing.name}”?`, tone: "danger", confirmLabel: "Delete" }).result)) return;
|
||||
try { draw(await app.api.deleteShortcut(existing.name)); win.close("done"); } catch (err) { tell(err.message); }
|
||||
};
|
||||
win.setFooter({ actions: [existing ? c.button.sm({ label: "Delete", tone: "danger", onClick: remove }) : null, c.button.sm({ label: "Save", tone: "primary", onClick: save })].filter(Boolean) });
|
||||
}
|
||||
|
||||
function draw(lib) {
|
||||
if (!alive) return;
|
||||
const items = (lib.shortcuts || []).map((s) => ({ id: s.name, label: `${s.enabled ? "" : "⏸ "}${s.name}`, hint: s.triggers.join(", "), keywords: `${s.category} ${s.sub_category} ${s.replacements.join(" ")}`, group: s.category || "" }));
|
||||
items.sort((x, y) => x.group.localeCompare(y.group) || x.label.localeCompare(y.label));
|
||||
page.set([
|
||||
c.toolbar.default({ items: [c.label.section({ text: "Shortcuts" })], trailing: [c.button.sm({ label: "+ New", tone: "primary", onClick: () => edit(null) })] }),
|
||||
c.filterList.md({ items, placeholder: "Search shortcuts", empty: "No shortcuts yet. Add one with + New.", onChange: (id) => edit((lib.shortcuts || []).find((s) => s.name === id)) }),
|
||||
c.hint.default({ text: "Type a trigger in any prompt and it is replaced when you generate." }),
|
||||
]);
|
||||
}
|
||||
app.api.shortcuts().then(draw).catch((err) => page.set([c.banner.warn({ text: `Could not load shortcuts: ${err.message}` })]));
|
||||
return page;
|
||||
}
|
||||
@@ -27,6 +27,11 @@ export const api = {
|
||||
system: () => call("GET", "/api/system"),
|
||||
storyMarkers: () => call("GET", "/api/story/markers"),
|
||||
storySplit: (text) => call("POST", "/api/story/split", { text }),
|
||||
shortcuts: () => call("GET", "/api/shortcuts"),
|
||||
saveShortcut: (shortcut, originalName) => call("POST", "/api/shortcuts", { ...shortcut, original_name: originalName }),
|
||||
deleteShortcut: (name) => call("DELETE", `/api/shortcuts/${encodeURIComponent(name)}`),
|
||||
revolver: () => call("GET", "/api/shortcuts/revolver"),
|
||||
setRevolver: (enabled, random) => call("POST", "/api/shortcuts/revolver", { enabled, random }),
|
||||
packs: () => call("GET", "/api/packs"),
|
||||
pack: (action, body) => call("POST", `/api/packs/${action}`, body || {}),
|
||||
git: (action, body) => call("POST", `/api/git/${action}`, body || {}),
|
||||
|
||||
Reference in New Issue
Block a user