diff --git a/.gitignore b/.gitignore index 105cfdc..af3d308 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,8 @@ tests/* !tests/translation_service_contracts.mjs !tests/studio_contracts.mjs !tests/material_workspace_flow.mjs +!tests/export_availability.mjs +!tests/test_export_availability.py !tests/model_sources_hub.mjs # Local diagnostic/test artifacts diff --git a/CHANGELOG.md b/CHANGELOG.md index 36fe598..da557b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ ## Unreleased +### Export availability +- Removed Prompt Studio's JSON export action; copying prompts and saving combinations to the Material Library remain available. Existing prompt JSON files can still be imported. +- Temporarily disabled Recipe package export in both the UI and API pending further validation. Package imports, workflow share codes, and ordinary image downloads remain available. + ### Material Library - Fixed grouped material filters, Recipe scope/tag filters, and prompt studio imports using actual detail payloads. Fixed side-studio draft reset/return state and guarded recipe drops against canvas changes. Recipe model status no longer treats saved hash verification as current availability. - Separated the lower-left Material Library from the Recipes/Prompt Notes workspace. Added canvas drag-and-drop: parameter materials replace matching node values, while the simplified prompt panel inserts text at the beginning or end, with undo. - Material Library now opens from the lower-left control; Workflow Recipes stays at the upper right. Import/export tools are available from the library's More menu. -- Added Prompt Combinations: ordered general/specific fragments, enable switches, separate positive/negative content, saved snapshots, and JSON import/export. +- Added Prompt Combinations: ordered general/specific fragments, enable switches, separate positive/negative content, saved snapshots, and JSON import. - Library cards can apply matching parameters to one selected node through the same transaction as Node Assistant, with seed preservation, native-option validation and guarded undo. Prompt combinations support explicit text replacement or append. - Material details initially expand prompt cards only; other node parameters render on first expansion. Details omit the complete source workflow, which loads separately when opening a full snapshot. Selected-node materials never return their hidden source graph. - Material capture supports PNG workflows, Recipe parameters and Prompt Notes. Applying reference images to LoadImage remains future work. diff --git a/README.md b/README.md index 7817067..60d87b7 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Open it with **Ctrl + Shift + M**, the floating **📦** button, or **Extensions #### 4. Workflow Recipes & Parameter Notebooks 🧰 * **Location**: Open **Creative Workspace (📑)** and switch to **Workflow Recipes**. -* **Features**: Save graphs with covers, notes, and model hashes. Append recipes to current canvas, inspect parameter differences against outputs, compare versions, and import/export portable packages. +* **Features**: Save graphs with covers, notes, and model hashes. Append recipes to current canvas, inspect parameter differences against outputs, compare versions, and import portable packages. Package export is temporarily unavailable. #### 5. Settings Panel ⚙️ * **Location**: The Gear icon (**⚙️**) at the bottom-left. diff --git a/api/recipe_packages.py b/api/recipe_packages.py index 558b59d..305242e 100644 --- a/api/recipe_packages.py +++ b/api/recipe_packages.py @@ -19,6 +19,9 @@ from .recipes import get_recipes_dir from .utils import require_filename, resolve_within +# Release gate: export stays closed until package validation is complete. +# Keep the format implementation for existing package/import compatibility. +RECIPE_PACKAGE_EXPORT_ENABLED = False PACKAGE_VERSION = 1 MAX_UPLOAD_BYTES = 32 * 1024 * 1024 MAX_ENTRY_COUNT = 256 @@ -457,6 +460,12 @@ def _commit_import(record, payload): async def api_export_recipe_package(request): + if not RECIPE_PACKAGE_EXPORT_ENABLED: + return web.json_response({ + "status": "error", + "code": "recipe_export_disabled", + "message": "Recipe package export is temporarily unavailable", + }, status=503) try: payload = await request.json() filename = require_filename(payload.get("filename", "")) diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index fb5d219..9b19f89 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -149,6 +149,10 @@ browsing, editing, or already stored data. ## Prompt Studio ownership +The studio supports copying prompt text and saving combinations to the local +Material Library. Standalone prompt-plan JSON export has been removed; importing +existing plan files remains supported by the Material Library transfer center. + The studio has one standalone drawer; the former embedded side/full composer and material import drawer are removed. Existing browser integration continues to call `openPromptStudio(owner)`, and external prompt dispatch uses @@ -157,7 +161,7 @@ to call `openPromptStudio(owner)`, and external prompt dispatch uses | Module | Owned state and responsibilities | | --- | --- | | `ui_prompt_composer.js` | Active drawer, docking width/side, trigger visibility and plan-loading request | -| `ui_prompt_workbench.js` | Owner-backed draft, active role, block editing, ordering, write/copy/save/export | +| `ui_prompt_workbench.js` | Owner-backed draft, active role, block editing, ordering, write/copy/save | | `ui_prompt_source_deck.js` | Source cards, filters, new-card form, preview popover and current sync request | | `ui_prompt_inspector.js` | Full-text inspection window, role tab and its translation lifetime | | `prompt_studio_data.js` | Starter cards, display categories, draft/block initialization and synthesis | diff --git a/docs/architecture/material-library.md b/docs/architecture/material-library.md index 813b5e8..bbcdae8 100644 --- a/docs/architecture/material-library.md +++ b/docs/architecture/material-library.md @@ -278,8 +278,8 @@ the fields; unknown workflow roles are not silently assigned a role. Saving creates an independent new snapshot. There is no automatic source sync or model binding. A page-session draft survives library navigation and is replaced by an existing saved plan only after confirmation; it must be saved before page -reload. Stale detail requests cannot replace the current draft. Export/import use -JSON format `anomalous-prompt-plan-v1`; imports are bounded to 2 MiB and validated +reload. Stale detail requests cannot replace the current draft. Standalone JSON +export has been removed. Existing prompt JSON imports are bounded to 2 MiB and validated by the same save endpoint. The panel also exposes role and target-widget selection, beginning/end buttons for a selected node, copy controls and guarded undo. diff --git a/docs/architecture/recipes.md b/docs/architecture/recipes.md index ef94ff5..0f36bc1 100644 --- a/docs/architecture/recipes.md +++ b/docs/architecture/recipes.md @@ -4,6 +4,21 @@ Read this document for recipe schemas, cards/detail behavior, package handling, result galleries, Parameter Notebooks, prompt roles, and the recipe-powered Node Assistant. +## Export availability + +Recipe package export is temporarily closed pending validation. Card/detail +buttons are disabled with a localized explanation. The registered +`POST /anomalous/export_recipe_package` route returns HTTP 503 with code +`recipe_export_disabled` before reading request JSON or recipe files. +`RECIPE_PACKAGE_EXPORT_ENABLED` in `api/recipe_packages.py` is a release gate, +not a user setting. Package format helpers remain available for import and +compatibility tests; existing imports and local saves are unaffected. + +Reopening requires an explicit release decision, package round-trip and failure +validation, then restoring the frontend export action and enabling the backend +gate together. Do not reopen exports as a side effect of UX work. Workflow +share codes are a separate feature and remain available. + ## Product and data model Workspace contains Prompt Notes, Workflow Recipes, and the Material Library. diff --git a/tests/export_availability.mjs b/tests/export_availability.mjs new file mode 100644 index 0000000..4f848ed --- /dev/null +++ b/tests/export_availability.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { fixture, Element, all } from './ui_fixture.mjs'; + +const f = fixture(); +const studio = await f.module('ui_prompt_composer.js'); +const owner = {}; +await studio.openPromptStudio(owner); +await f.flush(); +const workbench = f.document.querySelector('.anomalous-prompt-workbench'); +assert.equal(all(workbench).some(el => el.tagName === 'button' && /export|导出/i.test(el.textContent)), false); +owner.sidePromptComposerControl.addBlock({ content: 'cinematic light', role: 'positive' }); +await f.button(workbench, '📋').click(); +assert.equal(f.clipboard.at(-1), 'cinematic light'); +const name = workbench.querySelector('.anomalous-prompt-name-input'); +name.value = 'Local combination'; name.oninput(); +f.fetch = async () => ({ ok: true, json: async () => ({ status: 'success', material: { filename: 'local.json' } }) }); +await f.button(workbench, '💾').click(); +const save = f.requests.findLast(([url]) => url.includes('/save_prompt_plan')); +assert.equal(JSON.parse(save[1].body).plan.positive, 'cinematic light'); +studio.closePromptStudio(owner); + +const recipes = await f.module('ui_recipes.js'); +const recipeOwner = { recipeListContainer: new Element('div') }; +recipes.renderRecipeList.call(recipeOwner, [{ filename: 'example.json', data: { name: 'Example', params: {} } }]); +const exportButton = all(recipeOwner.recipeListContainer).find(el => el.attrs?.['aria-label'] === 'Recipe package export is temporarily unavailable'); +assert.ok(exportButton); +assert.equal(exportButton.disabled, true); +await exportButton.click(); +assert.equal(f.requests.some(([url]) => url.includes('/export_recipe_package')), false); +assert.deepEqual(f.errors, []); +console.log('Export availability: prompt export absent; copy/local save work; recipe export cannot issue a request.'); diff --git a/tests/test_export_availability.py b/tests/test_export_availability.py new file mode 100644 index 0000000..aaaac10 --- /dev/null +++ b/tests/test_export_availability.py @@ -0,0 +1,26 @@ +import json +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +PLUGIN_DIR = Path(__file__).resolve().parents[1] +sys.path[:0] = [str(PLUGIN_DIR), str(PLUGIN_DIR.parents[1])] +from api import recipe_packages + + +class ExportAvailabilityTests(unittest.IsolatedAsyncioTestCase): + async def test_export_rejected_before_request_or_files_are_read(self): + class UnreadRequest: + async def json(self): + raise AssertionError('Disabled export must not read request data') + + with patch.object(recipe_packages, 'get_recipes_dir', side_effect=AssertionError('No filesystem access')): + response = await recipe_packages.api_export_recipe_package(UnreadRequest()) + self.assertEqual(response.status, 503) + self.assertEqual(json.loads(response.text)['code'], 'recipe_export_disabled') + self.assertNotIn('Content-Disposition', response.headers) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ui_fixture.mjs b/tests/ui_fixture.mjs index 2dae420..ce0e9b7 100644 --- a/tests/ui_fixture.mjs +++ b/tests/ui_fixture.mjs @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import vm from 'node:vm'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; export const all = node => [node, ...node.children.flatMap(all)]; export class Events { @@ -95,11 +95,11 @@ export function fixture() { if (modules.has(filename)) return modules.get(filename); let source; if (filename.endsWith('/scripts/app.js')) source = 'export const app = globalThis.app;'; - else if (filename.endsWith('/ui_dialog.js')) source = 'export async function anomalousAlert(message) { errors.push(message); } export async function anomalousConfirm() { return true; }'; + else if (filename.endsWith('/ui_dialog.js')) source = 'export async function anomalousAlert(message) { errors.push(message); } export async function anomalousConfirm() { return true; } export async function anomalousPrompt() { return null; }'; else if (filename.endsWith('/material_feedback.js')) source = 'export function showMaterialSaved() {}'; else if (filename.endsWith('/ui_gallery_detail.js')) source = 'export function showImageWorkbench() {}'; else source = fs.readFileSync(filename, 'utf8'); - const mod = new vm.SourceTextModule(source, { context, identifier: filename }); + const mod = new vm.SourceTextModule(source, { context, identifier: filename, initializeImportMeta: meta => { meta.url = pathToFileURL(filename).href; } }); modules.set(filename, mod); return mod; }; state.module = async name => { diff --git a/web/modules/locales.js b/web/modules/locales.js index ad51d55..938d6be 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -15,7 +15,6 @@ export const i18n = { promptInsert_after: '放在后面', promptDragText: '拖到提示词节点', promptChooseTarget: '选择要写入的文本框', - promptExportError: '无法导出:文件需小于 2 MiB,标签最多 20 个、每个不超过 60 字。', materialApplyFailed: '应用失败,节点参数已恢复。', materialUndo: "撤销这次应用", materialUndone: "已撤销", @@ -70,7 +69,6 @@ export const i18n = { promptAppend: "追加到节点", promptReplace: "替换节点文本", promptSavePlan: "保存组合", - promptExportPlan: "导出组合", promptImportPlan: "导入提示词方案", promptAlreadyImported: "素材库中已有相同的提示词方案。", promptImportError: "导入失败,请选择有效的提示词方案 JSON 文件(不超过 2 MiB)。", @@ -227,7 +225,7 @@ export const i18n = {

1. 🔄 前置准备 (扫描向导)
具体位置:界面左侧底部的 扫描向导 (🔄)。
这是最重要的前置操作!请先打开扫描向导执行扫描。切记保持网络畅通。部分 C站限制级模型信息需要在设置中填入 API-KEY。扫描完成后,插件的各项功能基本激活。

2. 🩺 拯救爆红 (模型医生)
具体位置:左侧底部第二个 听诊器 (🩺) 按钮。
当导入别人使用该插件输出的工作流或者图片时,若模型路径爆红,点击模型医生就能智能识别当前所有节点实现正确路径的替换。

3. 🤖 选中交互与参数预设 (节点助手)
具体位置:左侧第三个 机器人 (🤖) 按钮。
在画布上选中节点后,“动作”页可以可视化替换模型,或在兼容的 MODEL + CLIP 链前后插入 LoRA。“参数方案”页会按配方分组读取工作流配方保存的同类型节点参数;点击一条记录即可把参数应用到当前选中节点。例如选中 KSampler 后,可一键载入配方中的采样器、调度器、步数、CFG 与降噪等值;种子等每次运行变化的参数会被忽略。

-

4. 工作流配方、参数方案与素材库
具体位置:打开 创作工作台。
配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入/导出配方包。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。

+

4. 工作流配方、参数方案与素材库
具体位置:打开 创作工作台。
配方会保存工作流、封面、标签、备注、模型引用及常用参数。未闭合的局部配方默认追加到当前画布,可独立运行的整体配方会在新画布打开;两者都支持导入配方包,配方包导出暂未开放。详情页包含概览、参数方案、图库和版本:可匹配本地模型、查看与复制完整参数、比较历史出图参数、比较或恢复配方版本。主图库或配方图库的图片悬浮后可进入参数详情,把原图、完整工作流和节点参数保存为一个素材快照;素材库可打开完整工作流,节点助手则按当前节点类型筛选并应用其中的参数。参数方案可从当前画布读取同一骨架的新参数,也可编辑安全控件并保存;“应用到当前工作流”只会在骨架匹配后写入参数。

5. 个性化配置 (设置面板)
具体位置:左侧底部的 齿轮 () 按钮。
可以调节语言、主页面字体大小;进入“模型设置”可选择视频封面始终播放或悬停播放,以及卡片使用流畅缩略图或原始封面。详情页始终保留原图画质。

6. 📑 顶部导航与提示词笔记
具体位置:右侧顶部的按钮分别为 模型 ()、图库 ()、创作工作台 (📑)、停靠侧边栏 (◧)。
进入创作工作台的“提示词笔记”,点击新建 。选择基础模型、主模型和 LoRA 后,可通过 📝纯文本/粘贴 填入提示词并进行双语对照、分块、查找替换和标签编辑,最后点击 发送到画布 一键组装节点。

7. 文件夹管理 (Folder Manager)
具体位置:设置 () 面板内的 文件夹管理。
可以用来隐藏不需要的内置文件夹。隐藏后的文件夹不仅从侧边栏消失,后端的静默扫描和本地读取也会完全跳过,实现真正的“零性能损耗”。并且可以在这里拖拽调整文件夹在侧边栏显示的顺序。

@@ -864,12 +862,8 @@ export const i18n = { recipeFilterByTag: '按此标签筛选', recipeImport: '导入配方包', recipeExport: '导出配方包', - recipeExportError: '导出工作流配方包失败。', + recipeExportUnavailable: '配方包导出暂未开放', recipeImportError: '导入工作流配方包失败。', - recipeExportSnapshotsConfirm: '导出时包含模型预览快照吗?\n\n快照是展示数据,不包含模型文件;视频封面只保留轻量静态快照,不会打包原视频。', - recipeExportHistoryConfirm: '导出时包含历史版本吗?\n\n历史可能包含旧提示词和备注。', - recipeExportModelNotesConfirm: '导出时包含每个模型的个人备注吗?\n\n这些备注可能包含你的私有使用习惯;选择“否”将仅从导出包中移除,原配方不受影响。', - recipeExportRedactIdentityConfirm: '是否移除模型 SHA-256、大小和来源信息?\n\n点击“确定”将移除这些身份信息。', recipeImportSummary: '配方包检查完成。确认导入以下内容吗?', recipeImportAssets: '预览资产', recipeImportHistory: '历史版本', @@ -1150,7 +1144,6 @@ export const i18n = { promptInsert_after: 'Add to end', promptDragText: 'Drag to a prompt node', promptChooseTarget: 'Choose the text field to update', - promptExportError: 'Cannot export: the file must be at most 2 MiB, with up to 20 tags of 60 characters each.', materialApplyFailed: 'Application failed; node parameters were restored.', materialUndo: "Undo this application", materialUndone: "Undone", @@ -1205,7 +1198,6 @@ export const i18n = { promptAppend: "Append to node", promptReplace: "Replace node text", promptSavePlan: "Save Plan", - promptExportPlan: "Export Plan", promptImportPlan: "Import prompt plan", promptAlreadyImported: "An identical prompt plan is already in the library.", promptImportError: "Import failed. Choose a valid prompt plan JSON file (up to 2 MiB).", @@ -1362,7 +1354,7 @@ export const i18n = {

1. 🔄 Prerequisites (Scan Wizard)
Location: The Scan (🔄) icon at the bottom left.
This is the most crucial step! Open the Scan Wizard and execute a scan. Keep your network connected. (API-KEY may be needed in Settings for NSFW Civitai models). Scanning activates all core features.

2. 🩺 Fix Broken Nodes (Model Doctor)
Location: The 2nd icon (Stethoscope 🩺) from the bottom left.
When importing a workflow/image exported by this plugin and nodes turn red, open the Model Doctor to intelligently identify and replace them with correct local paths.

3. 🤖 Node actions and parameter presets (Node Assistant)
Location: The 3rd icon (Robot 🤖) from the bottom left.
After selecting a canvas node, Actions can visually replace its model or insert a LoRA before/after a compatible MODEL + CLIP chain. Parameter Presets reads same-type node values saved by Workflow Recipes and groups them by recipe. Click one record to apply it to the selected node. For example, selecting a KSampler can load its sampler, scheduler, steps, CFG, and denoise values in one click; volatile values such as seeds are ignored.

-

4. Workflow Recipes and Parameter Sets
Location: Open the Creative Workspace and switch to Workflow Recipes.
A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both can be shared as import/export packages. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.

+

4. Workflow Recipes and Parameter Sets
Location: Open the Creative Workspace and switch to Workflow Recipes.
A recipe saves the workflow, cover, tags, notes, model references, and common parameters. Open partial recipes append to the current canvas, while independently runnable complete recipes open in a new canvas; both support importing recipe packages; package export is temporarily unavailable. Details include Overview, Parameter Set, Gallery, and Versions for local model matching, full parameter viewing/copying, output comparison, and version comparison/restoration. Parameter notes can capture fresh values from a matching live canvas or edit safe controls; Apply to Current Workflow writes only after the recipe skeleton matches. Third-party prompt nodes that cannot be classified reliably can be labelled manually.

5. Settings Panel
Location: The Gear () icon at the bottom left.
Adjust language and UI scale. Open “Model Settings” to choose always-play or hover-play video covers and optimized thumbnails or original covers. Detail pages always retain original image quality.

6. 📑 Top Navigation & Prompt Notes
Location: Top-right tabs Models (), Gallery (), Creative Workspace (📑), and Dock (◧).
Open Prompt Notes in the Creative Workspace and click . Select a Base Model, Main Model, and LoRAs; use 📝 Edit Raw/Paste for bilingual prompts, chunking, find/replace, and tag editing; then click Send to Canvas to assemble the nodes.

7. Folder Manager
Location: Inside the Settings () panel, click Manage Folders.
Use this to hide unneeded built-in folders. Hidden folders will be completely skipped by background scans and I/O reads, achieving true "zero performance cost". You can also drag and drop to reorder how folders appear in the sidebar.

@@ -1998,12 +1990,8 @@ export const i18n = { recipeFilterByTag: 'Filter by this tag', recipeImport: 'Import package', recipeExport: 'Export package', - recipeExportError: 'Could not export the Workflow Recipe package.', + recipeExportUnavailable: 'Recipe package export is temporarily unavailable', recipeImportError: 'Could not import the Workflow Recipe package.', - recipeExportSnapshotsConfirm: 'Include model preview snapshots in the export?\n\nSnapshots are presentation data; video covers are reduced to lightweight stills and original videos are never packed.', - recipeExportHistoryConfirm: 'Include historical versions in the export?\n\nHistory may contain older prompts and notes.', - recipeExportModelNotesConfirm: 'Include personal notes for each model in the export?\n\nThese notes may contain private usage habits. Choosing No removes them only from the exported package.', - recipeExportRedactIdentityConfirm: 'Remove model SHA-256, size, and provenance information?\n\nChoose OK to remove those identity fields.', recipeDialogNo: 'No', recipeImportSummary: 'The recipe package passed inspection. Import these contents?', recipeImportAssets: 'Preview assets', diff --git a/web/modules/ui_materials.js b/web/modules/ui_materials.js index 430a4be..0d18557 100644 --- a/web/modules/ui_materials.js +++ b/web/modules/ui_materials.js @@ -1222,7 +1222,6 @@ function showTransferCenter(owner) { } catch (error) { status.textContent = t('promptImportError'); } finally { file.value = ''; importPlan.disabled = false; } }; - text(dialog, 'p', t('promptExportLocation')); const close = text(dialog, 'button', t('close'), 'anomalous-btn-ghost'); close.onclick = () => dialog.close(); dialog.onclose = () => dialog.remove(); document.body.appendChild(dialog); dialog.showModal(); } diff --git a/web/modules/ui_prompt_workbench.js b/web/modules/ui_prompt_workbench.js index af4a030..a86b8eb 100644 --- a/web/modules/ui_prompt_workbench.js +++ b/web/modules/ui_prompt_workbench.js @@ -87,7 +87,7 @@ export function createPromptWorkbench(owner, container, scope, options) { }; } - // "More" Dropdown Menu (holds Save, New Draft, Tags, Export, Material Sync) + // "More" Dropdown Menu (holds Save, New Draft, Tags, Material Sync) const moreWrap = text(topActions, 'div', '', 'anomalous-prompt-more-wrap'); const moreBtn = text(moreWrap, 'button', window.anomalous_browser_lang === 'zh' ? '··· 更多' : '··· More', 'anomalous-btn-ghost anomalous-btn-sm anomalous-prompt-more-btn'); const moreMenu = text(moreWrap, 'div', '', 'anomalous-prompt-more-menu'); @@ -123,8 +123,6 @@ export function createPromptWorkbench(owner, container, scope, options) { } }; - const exportBtn = text(moreMenu, 'button', `📤 ${t('promptExportPlan')}`, 'anomalous-prompt-more-item'); - const syncLibBtn = text(moreMenu, 'button', `📥 ${window.anomalous_browser_lang === 'zh' ? '同步素材库' : 'Sync Library'}`, 'anomalous-prompt-more-item'); syncLibBtn.onclick = () => { closeMoreMenu(); @@ -855,37 +853,6 @@ export function createPromptWorkbench(owner, container, scope, options) { } }; - // Export Plan JSON - exportBtn.onclick = () => { - closeMoreMenu(); - if (!draft.name?.trim()) { - nameInput.focus(); - nameInput.classList.add('is-invalid'); - setTimeout(() => nameInput.classList.remove('is-invalid'), 1200); - showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '请先在顶栏输入方案名称!' : 'Please enter preset name first!'); - return; - } - syncDraftSynthesizedText(draft); - const saved = workbenchDraftToSavedPlan(draft); - const exportData = { - format: 'anomalous-prompt-plan-v1', - version: 2, - name: draft.name, - tags: draft.tags, - plan: saved.plan, - }; - const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); - if (blob.size > 2 * 1024 * 1024 || draft.tags.length > 20 || draft.tags.some(tag => tag.length > 60)) { - return anomalousAlert(t('promptExportError')); - } - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${draft.name || 'prompt-mixer-preset'}.json`; - a.click(); - setTimeout(() => URL.revokeObjectURL(url), 1000); - }; - // New Draft newBtn.onclick = async () => { closeMoreMenu(); diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js index de0722a..cf27204 100644 --- a/web/modules/ui_recipe_detail.js +++ b/web/modules/ui_recipe_detail.js @@ -1111,15 +1111,9 @@ function renderOverview(content, owner, recipe, references, finish) { const heroExport = button(more, '', 'anomalous-btn-ghost'); heroExport.innerHTML = `${t('recipeExport')}`; - heroExport.title = t('recipeExport'); - heroExport.onclick = async () => { - try { - await exportRecipePackage(owner.recipeDetailFilename, recipe); - } catch (error) { - console.error('Could not export recipe package:', error); - await anomalousAlert(t('recipeExportError')); - } - }; + heroExport.title = t('recipeExportUnavailable'); + heroExport.setAttribute('aria-label', t('recipeExportUnavailable')); + heroExport.disabled = true; copy.appendChild(overviewActions); diff --git a/web/modules/ui_recipes.js b/web/modules/ui_recipes.js index 0032b7e..7e3c99f 100644 --- a/web/modules/ui_recipes.js +++ b/web/modules/ui_recipes.js @@ -183,40 +183,6 @@ function appendRecipeCover(parent, url, alt, seed = '') { parent.appendChild(image); } -async function exportRecipePackage(filename) { - const choice = { noLabel: t('recipeDialogNo') }; - const includeSnapshots = await anomalousConfirm(t('recipeExportSnapshotsConfirm'), 'Anomalous', choice); - if (includeSnapshots === null) return; - const includeHistory = await anomalousConfirm(t('recipeExportHistoryConfirm'), 'Anomalous', choice); - if (includeHistory === null) return; - const includeModelNotes = await anomalousConfirm(t('recipeExportModelNotesConfirm'), 'Anomalous', choice); - if (includeModelNotes === null) return; - const redactIdentity = await anomalousConfirm(t('recipeExportRedactIdentityConfirm'), 'Anomalous', choice); - if (redactIdentity === null) return; - const includeIdentity = !redactIdentity; - const response = await fetch('/anomalous/export_recipe_package', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - filename, - include_snapshots: includeSnapshots, - include_history: includeHistory, - include_identity: includeIdentity, - include_model_notes: includeModelNotes, - }), - }); - if (!response.ok) throw new Error('recipe export failed'); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `${filename.replace(/\.json$/i, '')}.anomalous-recipe.zip`; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); -} - async function importRecipePackage(owner, file) { const inspectResponse = await fetch('/anomalous/import_recipe_package_inspect', { method: 'POST', @@ -1156,12 +1122,10 @@ function createRecipeCard(owner, recipe) { exportBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target'; exportBtn.innerHTML = ``; exportBtn.removeAttribute('title'); - exportBtn.setAttribute('data-tooltip', t('recipeExport')); + exportBtn.setAttribute('data-tooltip', t('recipeExportUnavailable')); + exportBtn.setAttribute('aria-label', t('recipeExportUnavailable')); + exportBtn.disabled = true; exportBtn.setAttribute('data-tooltip-pos', 'top'); - exportBtn.onclick = (e) => { - e.stopPropagation(); - runRecipeCardAction(exportBtn, () => exportRecipePackage(recipe.filename), 'recipeExportError'); - }; const removeBtn = document.createElement('button'); removeBtn.type = 'button';