Files
DemonGatanjieuandClaude Opus 5.5 8b549f463a AI workbench: every step an AI takes through MCP, kept, opened, undone
A new rail page keeps each MCP call as a step of a task (api/ai_tasks.py,
one file per task in the user folder, the newest 300): named by the AI
with the new begin_task tool, else one app's calls without a long pause.
A step keeps what it did and what it made (models, the combo, the canvas
change by its activity entry, the pictures a run made, watched in the
history until it ends, spoken audio, scans); the new report tool leaves
the AI's notes and pictures it made another way. The page shows the tasks
and one task's results strip and timeline with links (picture viewer,
model page, combo, find or undo on the canvas, audio), updates live, and
gives the rail a dot while you are elsewhere; Home lists the last tasks.

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

140 lines
6.0 KiB
JavaScript

/**
* The Combos page (搭配) as a workspace over the current page, filling it like Workflows: the
* list of combos (ui_combos.js), or one combo in its editor (ui_notebook_editor.js) under a bar
* with Back to the list (Esc presses it; the rail and Esc close the page);
* saving and deleting a combo (the files under workflows/anomalous_notebooks, which the
* code still calls notebooks: combos were Prompt Notes before).
*/
import { translate } from './locales.js';
import { renderComboList } from './ui_combos.js';
import { listCombos } from './image_keep.js';
const t = (key, params) => translate(key, params);
function buildComboWorkspace(owner) {
const container = document.createElement('div');
container.className = 'anomalous-nb-container anomalous-combo-container';
owner.comboHeader = document.createElement('div');
owner.comboHeader.className = 'anomalous-nb-header';
owner.comboBack = document.createElement('button');
owner.comboBack.type = 'button';
owner.comboBack.className = 'anomalous-btn-ghost anomalous-nb-back';
owner.comboBack.onclick = () => owner.showNotebooks();
owner.comboHeader.append(owner.comboBack);
const body = document.createElement('div');
body.className = 'anomalous-nb-body';
owner.comboList = document.createElement('div');
owner.comboList.className = 'anomalous-combo-page';
owner.nbEditor = document.createElement('div');
owner.nbEditor.className = 'anomalous-nb-editor';
body.append(owner.comboList, owner.nbEditor);
container.append(owner.comboHeader, body);
owner.nbPanel.appendChild(container);
owner.notebookContainer = container;
owner.notebookBody = body;
}
/**
* Opens the Combos workspace on its list, or with `editor` on `this.currentNotebook` in the
* editor.
*/
export async function showNotebooks({ editor = false } = {}) {
this.recipeDetailFinish?.('closed');
this.modal?.classList.add('visible');
if (this.nbPanel && this.nbPanel.style.display !== 'flex' && !this.workspaceReturnState) {
this.workspaceReturnState = Object.fromEntries([
['grid', this.grid], ['detail', this.detailPanel], ['gallery', this.galleryPanel],
['doctor', this.doctorPanel], ['assistant', this.assistantPanel], ['home', this.homePanel], ['activity', this.activityPanel], ['ai', this.aiPanel],
['scan', this.scanPanel],
].filter(([, panel]) => panel).map(([key, panel]) => [key, panel.style.display]));
}
for (const panel of [this.grid, this.detailPanel, this.galleryPanel, this.doctorPanel, this.assistantPanel, this.homePanel, this.activityPanel, this.aiPanel, this.scanPanel, this.paramPanel]) {
if (panel) panel.style.display = 'none';
}
if (this.recipeContainer) this.recipeContainer.style.display = 'none';
if (!this.notebookContainer) buildComboWorkspace(this);
this.nbPanel.style.display = 'flex';
this.notebookContainer.style.display = 'flex';
this.notebookBody.style.display = 'flex';
this.markWorkspace?.('combos');
const editing = editor && Boolean(this.currentNotebook);
this.comboBack.textContent = t('comboBack');
this.comboHeader.hidden = !editing;
this.comboList.hidden = editing;
this.nbEditor.hidden = !editing;
if (editing) {
this.renderNotebookEditor();
return;
}
this.currentNotebook = null;
await renderComboList(this, this.comboList);
}
/** Opens `note` in the editor. */
export function openCombo(note) {
this.currentNotebook = note;
return this.showNotebooks({ editor: true });
}
/** Opens the combo saved as `filename` (from a toast after keeping an image as a combo). */
export async function openComboByFilename(filename) {
this.show?.();
const note = (await listCombos()).find(item => item.filename === filename);
if (note) await this.openCombo(note);
else await this.showNotebooks();
}
/** Draws the list again when it is the view on screen (after a save). */
export async function refreshNotebooks() {
if (this.comboList && !this.comboList.hidden && this.nbPanel?.style.display === 'flex') {
await renderComboList(this, this.comboList);
}
}
export async function saveCurrentNotebook() {
if (!this.currentNotebook) return false;
const body = JSON.stringify(this.currentNotebook);
this.notebookSaveQueue = (this.notebookSaveQueue || Promise.resolve()).then(async () => {
try {
const response = await fetch('/anomalous/save_notebook', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body,
});
const result = await response.json();
if (!response.ok || result.status !== 'success') throw new Error('notebook save failed');
this.nbSaveStatus?.remove();
this.nbSaveStatus = null;
await this.refreshNotebooks();
return true;
} catch (error) {
if (this.nbEditor && !this.nbEditor.hidden && !this.nbSaveStatus?.isConnected) {
this.nbSaveStatus = document.createElement('p');
this.nbSaveStatus.setAttribute('role', 'alert');
this.nbEditor.prepend(this.nbSaveStatus);
}
if (this.nbSaveStatus) this.nbSaveStatus.textContent = t('notebookSaveError');
return false;
}
});
return this.notebookSaveQueue;
}
/** Deletes the open combo (its file goes to the Recycle Bin) and goes back to the list. */
export async function deleteCurrentNotebook() {
if (!this.currentNotebook) return;
try {
await this.notebookSaveQueue;
const response = await fetch('/anomalous/delete_notebook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: this.currentNotebook.filename }),
});
if (!response.ok || (await response.json()).status !== 'success') throw new Error('notebook delete failed');
await this.showNotebooks();
} catch (e) {
console.error('[AMB] Error deleting combo:', e);
}
}