diff --git a/web/src/lib/components/kit/SchemaPreview.svelte b/web/src/lib/components/kit/SchemaPreview.svelte index a95d3e3..fdc4bb0 100644 --- a/web/src/lib/components/kit/SchemaPreview.svelte +++ b/web/src/lib/components/kit/SchemaPreview.svelte @@ -1,6 +1,8 @@ @@ -44,7 +63,7 @@ {running ? "Generating…" : "Generate"} - {:else} + {:else if !isTextInput} {/if} - + {#if isImageDownload} + + {/if} @@ -70,7 +91,16 @@
SOURCE
- {#if sourceUrl} + {#if isTextInput} +
+ {})} + onrender={onrendertext ?? (() => {})} + /> +
+ {:else if sourceUrl} source {:else} choose an image @@ -78,10 +108,28 @@
{/if} -
+
RESULT {running ? "…" : ""}
-
- {#if resultUrl} +
+ {#if resultKind === "text" && textResult} +
+ {})} + ondownload={ondownloadtxt ?? (() => {})} + /> +
+ {:else if resultKind === "verdict" && textResult} +
+ {})} + ondownload={ondownloadtxt ?? (() => {})} + /> +
+ {:else if resultKind === "image" && resultUrl} result {:else if running} @@ -99,11 +147,25 @@ items={[ { caption: "SOURCE", - value: source ? `${source.width} × ${source.height} px` : "—", + value: + !isGenerator && !isTextInput + ? source + ? `${source.width} × ${source.height} px` + : "—" + : isTextInput + ? "text" + : "—", }, { caption: "RESULT", - value: result ? `${result.width} × ${result.height} px` : "—", + value: + resultKind === "image" + ? result + ? `${result.width} × ${result.height} px` + : "—" + : textResult + ? "text" + : "—", }, { caption: "FORMAT", value: "PNG" }, ]} @@ -202,6 +264,12 @@ .empty { font: var(--font-size-s) var(--font-mono); } + .text-source-wrap, + .text-result-wrap { + width: 100%; + padding: var(--space-l); + box-sizing: border-box; + } .meta { margin-top: var(--space-l); } diff --git a/web/src/lib/components/kit/SchemaTextResult.svelte b/web/src/lib/components/kit/SchemaTextResult.svelte new file mode 100644 index 0000000..4aa3e64 --- /dev/null +++ b/web/src/lib/components/kit/SchemaTextResult.svelte @@ -0,0 +1,129 @@ + + +{#if kind === "verdict"} +
+ RESULT +
+ {value} + +
+
+{:else} +
+
+ {fileName} +
+ + +
+
+
{value}
+
+{/if} + + diff --git a/web/src/lib/components/kit/SchemaTextSource.svelte b/web/src/lib/components/kit/SchemaTextSource.svelte new file mode 100644 index 0000000..f7378ab --- /dev/null +++ b/web/src/lib/components/kit/SchemaTextSource.svelte @@ -0,0 +1,107 @@ + + +
+ +
+ {#if onsample} + + {/if} + +
+
+ + diff --git a/web/src/lib/components/kit/SchemaToolView.svelte b/web/src/lib/components/kit/SchemaToolView.svelte index f990f3e..0296fba 100644 --- a/web/src/lib/components/kit/SchemaToolView.svelte +++ b/web/src/lib/components/kit/SchemaToolView.svelte @@ -4,7 +4,13 @@ import { debounce } from "$lib/core/debounce"; import { decodeFile, encode } from "$lib/core/io"; import type { PixelImage } from "$lib/core/types"; - import { executeGenerate, executeStep } from "$lib/preview/executor"; + import { + executeFromText, + executeGenerate, + executeStep, + executeTextToText, + executeToText, + } from "$lib/preview/executor"; import type { ToolEntry } from "$lib/registry-new"; import { defaultSchemaParams, @@ -22,10 +28,14 @@ ); const isGenerator = $derived(Boolean(tool.generate && !tool.run)); + const inputMode = $derived(tool.input ?? "file"); + const resultKind = $derived(tool.result ?? "image"); let values = $state>({}); let source = $state(null); let result = $state(null); + let textSource = $state(""); + let textResult = $state(null); let running = $state(false); let error = $state(""); let started = $state(false); @@ -45,6 +55,7 @@ function reset() { values = schema ? defaultSchemaParams(schema) : {}; result = null; + textResult = null; error = ""; } @@ -64,12 +75,29 @@ await runGenerate(); return; } - if (!source) return; error = ""; - running = true; const params = sanitizeSchemaParams(schema, values); + running = true; try { - result = await executeStep(tool, source, params); + if (inputMode === "text") { + if (!textSource.trim()) return; + if (tool.textToText) { + textResult = await executeTextToText(tool, textSource); + result = null; + } else { + result = await executeFromText(tool, textSource, params); + textResult = null; + } + } else { + if (!source) return; + if (resultKind !== "image" && tool.toText) { + textResult = await executeToText(tool, source, params); + result = null; + } else { + result = await executeStep(tool, source, params); + textResult = null; + } + } } catch (e) { error = errorText(e); } finally { @@ -106,12 +134,32 @@ URL.revokeObjectURL(url); } + async function copyText() { + if (!textResult) return; + try { + await navigator.clipboard.writeText(textResult); + } catch (e) { + error = errorText(e); + } + } + + async function downloadText() { + if (!textResult) return; + const blob = new Blob([textResult], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${tool.id}.txt`; + a.click(); + URL.revokeObjectURL(url); + } + $effect(() => { init(); }); $effect(() => { - if (isGenerator || !source || !started) return; + if (inputMode === "text" || isGenerator || !source || !started) return; void values; debouncedRun(); return () => debouncedRun.cancel(); @@ -146,11 +194,20 @@ { + textSource = t; + }} + onrendertext={run} + oncopytext={copyText} + ondownloadtxt={downloadText} ondownload={download} /> diff --git a/web/src/lib/preview/executor/executor.ts b/web/src/lib/preview/executor/executor.ts index a7fafe1..a48009a 100644 --- a/web/src/lib/preview/executor/executor.ts +++ b/web/src/lib/preview/executor/executor.ts @@ -55,6 +55,53 @@ export async function executeGenerate( return await tool.generate(params); } +type MaybeTextRunnable = MaybeRunnable & { + runFromText?: ( + text: string, + params: Record, + ) => Promise | PixelImage; + toText?: ( + img: PixelImage, + params: Record, + ) => Promise | string; + textToText?: (text: string) => Promise | string; +}; + +/** Текст → изображение (text-source конвертеры). Выполняется напрямую. */ +export async function executeFromText( + tool: MaybeTextRunnable, + text: string, + params: Record, +): Promise { + if (!tool.runFromText) { + throw new Error("errors.noTextInput"); + } + return await tool.runFromText(text, params); +} + +/** Изображение → текст (текстовые конвертеры и вердикты). */ +export async function executeToText( + tool: MaybeTextRunnable, + img: PixelImage, + params: Record, +): Promise { + if (!tool.toText) { + throw new Error("errors.noTextResult"); + } + return await tool.toText(img, params); +} + +/** Текст → текст (напр. verify-is-png). */ +export async function executeTextToText( + tool: MaybeTextRunnable, + text: string, +): Promise { + if (!tool.textToText) { + throw new Error("errors.noTextResult"); + } + return await tool.textToText(text); +} + async function runDirect( tool: MaybeRunnable, img: PixelImage, diff --git a/web/src/lib/preview/executor/index.ts b/web/src/lib/preview/executor/index.ts index 02cb028..81e0890 100644 --- a/web/src/lib/preview/executor/index.ts +++ b/web/src/lib/preview/executor/index.ts @@ -1 +1,7 @@ -export { executeGenerate, executeStep } from "./executor"; +export { + executeFromText, + executeGenerate, + executeStep, + executeTextToText, + executeToText, +} from "./executor"; diff --git a/web/src/lib/registry-new/types.ts b/web/src/lib/registry-new/types.ts index c261b68..4b5b6f4 100644 --- a/web/src/lib/registry-new/types.ts +++ b/web/src/lib/registry-new/types.ts @@ -23,6 +23,10 @@ export type ToolEntry

> = { schema: ToolSchema

; /** Требует DOM (canvas/document); превью-executor запускает напрямую, не в worker. */ domOnly?: boolean; + /** Чем управляется инструмент: файлом (по умолчанию), текстом или ничем (генератор). */ + input?: "file" | "text" | "none"; + /** Тип результата: картинка (по умолчанию), большой текст или короткий вердикт. */ + result?: "image" | "text" | "verdict"; /** Применение к входному изображению. Для чистых генераторов отсутствует. */ run?(img: PixelImage, params: P): Promise | PixelImage; generate?(params: P): Promise | PixelImage;