@ecsplico/editor 3.4.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/abilities.js +1 -0
- package/dist/core/abilities.js.map +1 -1
- package/dist/core/backend-capability.d.ts +1 -1
- package/dist/core/backend-capability.js +2 -1
- package/dist/core/backend-capability.js.map +1 -1
- package/dist/core/backend-types.d.ts +25 -0
- package/dist/core/backend-types.js.map +1 -1
- package/dist/core/backend.d.ts +9 -1
- package/dist/core/backend.js.map +1 -1
- package/dist/core/http-backend.js +17 -0
- package/dist/core/http-backend.js.map +1 -1
- package/dist/core/schema/abilities.d.ts +1 -0
- package/dist/core/schema/abilities.js +4 -0
- package/dist/core/schema/abilities.js.map +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/composites/nodes.js +1 -0
- package/dist/features/composites/nodes.js.map +1 -1
- package/dist/features/image-studio/components/ImageStudioChat.js +121 -0
- package/dist/features/image-studio/components/ImageStudioChat.js.map +1 -0
- package/dist/features/image-studio/components/ImageStudioDialog.js +371 -0
- package/dist/features/image-studio/components/ImageStudioDialog.js.map +1 -0
- package/dist/features/image-studio/components/ImageStudioDialog.vue.d.ts +25 -0
- package/dist/features/image-studio/components/ImageStudioTile.js +74 -0
- package/dist/features/image-studio/components/ImageStudioTile.js.map +1 -0
- package/dist/features/image-studio/components/ImageStudioView.js +117 -0
- package/dist/features/image-studio/components/ImageStudioView.js.map +1 -0
- package/dist/features/image-studio/feature.d.ts +24 -0
- package/dist/features/image-studio/feature.js +53 -0
- package/dist/features/image-studio/feature.js.map +1 -0
- package/dist/features/image-studio/index.d.ts +5 -0
- package/dist/features/image-studio/index.js +4 -0
- package/dist/features/image-studio/node-core.d.ts +46 -0
- package/dist/features/image-studio/node-core.js +79 -0
- package/dist/features/image-studio/node-core.js.map +1 -0
- package/dist/features/image-studio/node-store.d.ts +19 -0
- package/dist/features/image-studio/node-store.js +35 -0
- package/dist/features/image-studio/node-store.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioChat.js +101 -0
- package/dist/features/image-studio/react/ImageStudioChat.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioDialog.d.ts +19 -0
- package/dist/features/image-studio/react/ImageStudioDialog.js +357 -0
- package/dist/features/image-studio/react/ImageStudioDialog.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioTile.js +51 -0
- package/dist/features/image-studio/react/ImageStudioTile.js.map +1 -0
- package/dist/features/image-studio/react/ImageStudioView.js +126 -0
- package/dist/features/image-studio/react/ImageStudioView.js.map +1 -0
- package/dist/features/image-studio/react/store.d.ts +37 -0
- package/dist/features/image-studio/react/store.js +46 -0
- package/dist/features/image-studio/react/store.js.map +1 -0
- package/dist/features/image-studio/react.d.ts +18 -0
- package/dist/features/image-studio/react.js +21 -0
- package/dist/features/image-studio/react.js.map +1 -0
- package/dist/features/image-studio/schema.d.ts +29 -0
- package/dist/features/image-studio/schema.js +86 -0
- package/dist/features/image-studio/schema.js.map +1 -0
- package/dist/features/image-studio/state.d.ts +52 -0
- package/dist/features/image-studio/state.js +147 -0
- package/dist/features/image-studio/state.js.map +1 -0
- package/dist/features/image-studio/store.d.ts +29 -0
- package/dist/features/image-studio/vue.d.ts +17 -0
- package/dist/features/image-studio/vue.js +20 -0
- package/dist/features/image-studio/vue.js.map +1 -0
- package/dist/features/media/schema.d.ts +5 -1
- package/dist/features/media/schema.js +3 -3
- package/dist/features/media/schema.js.map +1 -1
- package/dist/features/transcript/segment-node.js +1 -0
- package/dist/features/transcript/segment-node.js.map +1 -1
- package/dist/i18n/de/image-studio.js +39 -0
- package/dist/i18n/de/image-studio.js.map +1 -0
- package/dist/i18n/de.d.ts +32 -0
- package/dist/i18n/de.js +13 -11
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en/image-studio.js +39 -0
- package/dist/i18n/en/image-studio.js.map +1 -0
- package/dist/i18n/en.js +13 -11
- package/dist/i18n/en.js.map +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
- package/dist/nuxt/module.d.ts +64 -0
- package/dist/server/handlers/ai.d.ts +2 -2
- package/dist/server/handlers/images.d.ts +22 -0
- package/dist/server/handlers/images.js +46 -0
- package/dist/server/handlers/images.js.map +1 -0
- package/dist/server/handlers/index.d.ts +2 -1
- package/dist/server/handlers/index.js +2 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/ports.d.ts +11 -1
- package/dist/server/ports.js +2 -1
- package/dist/server/ports.js.map +1 -1
- package/dist/server/routes.js +19 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/tree/tree-host.js +1 -1
- package/dist/tree/tree-items.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
- package/dist/vue/SimpleEditor.vue.d.ts +6 -6
- package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
- package/package.json +21 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createImageStudioNodeCore } from "../node-core.js";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/features/image-studio/react/store.ts
|
|
4
|
+
const noUnsubscribe = () => {};
|
|
5
|
+
/**
|
|
6
|
+
* The `ImageStudioReactStore` of one `imageStudio` node (the React wrapper of `createImageStudioNodeCore`): `state`
|
|
7
|
+
* follows the node's `studio` attribute, `update` writes it back as one transaction, the rest goes to
|
|
8
|
+
* `backend.images`. The core lives for as long as the component and the `editor`, `backend` and `documentId` stay the
|
|
9
|
+
* same; the first render returns `undefined` (the core is built in an effect), so render the dialog once there is a store.
|
|
10
|
+
*/
|
|
11
|
+
function useImageStudioNodeStore(options) {
|
|
12
|
+
const { editor, backend, documentId } = options;
|
|
13
|
+
const getPos = useRef(options.getPos);
|
|
14
|
+
getPos.current = options.getPos;
|
|
15
|
+
const [core, setCore] = useState();
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const own = createImageStudioNodeCore({
|
|
18
|
+
editor,
|
|
19
|
+
backend,
|
|
20
|
+
documentId,
|
|
21
|
+
getPos: () => getPos.current()
|
|
22
|
+
});
|
|
23
|
+
setCore(own);
|
|
24
|
+
return () => {
|
|
25
|
+
own.dispose();
|
|
26
|
+
setCore(void 0);
|
|
27
|
+
};
|
|
28
|
+
}, [
|
|
29
|
+
editor,
|
|
30
|
+
backend,
|
|
31
|
+
documentId
|
|
32
|
+
]);
|
|
33
|
+
const subscribe = useCallback((listener) => core ? core.subscribe(listener) : noUnsubscribe, [core]);
|
|
34
|
+
const getSnapshot = useCallback(() => core?.get(), [core]);
|
|
35
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
36
|
+
return useMemo(() => core && snapshot ? {
|
|
37
|
+
...snapshot,
|
|
38
|
+
update: core.update,
|
|
39
|
+
generate: core.generate,
|
|
40
|
+
upload: core.upload
|
|
41
|
+
} : void 0, [core, snapshot]);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { useImageStudioNodeStore };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../../../src/features/image-studio/react/store.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\nimport { createImageStudioNodeCore, type ImageStudioNodeCore, type ImageStudioNodeCoreOptions } from '../node-core'\nimport type { ImageStudioImage, ImageStudioPromptBlock, ImageStudioState } from '../state'\n\n/**\n * What the React Bild-Studio dialog drives; a host supplies it. The same contract as the Vue `ImageStudioStore`, with\n * plain values where Vue has refs: the host re-renders the dialog with a new `state` after `update`.\n *\n * The dialog applies the reducers of `../state` and hands the result to `update`; the store only persists it. The\n * store's methods never touch `state` themselves:\n * - `update` must lead to a new `state` (the dialog keeps working on its own latest value until `state` changes).\n * - `generate` produces one image from the block's `state.description`, the active Prompt-Bausteine and the active\n * images (the host builds the prompt). When it resolves, the dialog calls `update(addGeneration(state, image, turn))`;\n * when it rejects (and the signal was not aborted) the dialog appends a turn with `imageId: null` and the message as\n * `problem`. An aborted generation adds nothing.\n * - `upload` stores the file and returns the image; the dialog then calls `update(addReference(state, image))`.\n * - `problem` is a problem of the store itself (the Prompt-Bausteine could not be loaded, say); the dialog shows it as a notice.\n */\nexport interface ImageStudioReactStore {\n readonly state: ImageStudioState\n /** The managed list of Prompt-Bausteine, loaded by the host. */\n readonly promptBlocks: readonly ImageStudioPromptBlock[]\n /** Called with the state after a reducer; the host persists it. */\n update: (next: ImageStudioState) => void\n generate: (instruction: string, signal: AbortSignal) => Promise<ImageStudioImage>\n upload: (file: File) => Promise<ImageStudioImage>\n readonly problem: string | undefined\n}\n\nconst noUnsubscribe = () => {}\n\n/**\n * The `ImageStudioReactStore` of one `imageStudio` node (the React wrapper of `createImageStudioNodeCore`): `state`\n * follows the node's `studio` attribute, `update` writes it back as one transaction, the rest goes to\n * `backend.images`. The core lives for as long as the component and the `editor`, `backend` and `documentId` stay the\n * same; the first render returns `undefined` (the core is built in an effect), so render the dialog once there is a store.\n */\nexport function useImageStudioNodeStore(options: ImageStudioNodeCoreOptions): ImageStudioReactStore | undefined {\n const { editor, backend, documentId } = options\n const getPos = useRef(options.getPos)\n getPos.current = options.getPos\n const [core, setCore] = useState<ImageStudioNodeCore>()\n useEffect(() => {\n const own = createImageStudioNodeCore({ editor, backend, documentId, getPos: () => getPos.current() })\n setCore(own)\n return () => {\n own.dispose()\n setCore(undefined)\n }\n }, [editor, backend, documentId])\n const subscribe = useCallback((listener: () => void) => core ? core.subscribe(listener) : noUnsubscribe, [core])\n const getSnapshot = useCallback(() => core?.get(), [core])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n return useMemo(\n () => core && snapshot ? { ...snapshot, update: core.update, generate: core.generate, upload: core.upload } : undefined,\n [core, snapshot],\n )\n}\n"],"mappings":";;;AA6BA,MAAM,sBAAsB,CAAC;;;;;;;AAQ7B,SAAgB,wBAAwB,SAAwE;CAC9G,MAAM,EAAE,QAAQ,SAAS,eAAe;CACxC,MAAM,SAAS,OAAO,QAAQ,MAAM;CACpC,OAAO,UAAU,QAAQ;CACzB,MAAM,CAAC,MAAM,WAAW,SAA8B;CACtD,gBAAgB;EACd,MAAM,MAAM,0BAA0B;GAAE;GAAQ;GAAS;GAAY,cAAc,OAAO,QAAQ;EAAE,CAAC;EACrG,QAAQ,GAAG;EACX,aAAa;GACX,IAAI,QAAQ;GACZ,QAAQ,KAAA,CAAS;EACnB;CACF,GAAG;EAAC;EAAQ;EAAS;CAAU,CAAC;CAChC,MAAM,YAAY,aAAa,aAAyB,OAAO,KAAK,UAAU,QAAQ,IAAI,eAAe,CAAC,IAAI,CAAC;CAC/G,MAAM,cAAc,kBAAkB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC;CACzD,MAAM,WAAW,qBAAqB,WAAW,aAAa,WAAW;CACzE,OAAO,cACC,QAAQ,WAAW;EAAE,GAAG;EAAU,QAAQ,KAAK;EAAQ,UAAU,KAAK;EAAU,QAAQ,KAAK;CAAO,IAAI,KAAA,GAC9G,CAAC,MAAM,QAAQ,CACjB;AACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ImageStudioImage, ImageStudioPromptBlock } from "../../core/backend-types.js";
|
|
2
|
+
import { ReactFeature } from "../../react/EditorSurface.js";
|
|
3
|
+
import "../../react/index.js";
|
|
4
|
+
import { EMPTY_IMAGE_STUDIO_STATE, ImageStudioState, ImageStudioTurn, addGeneration, addReference, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock } from "./state.js";
|
|
5
|
+
import { ImageStudioNodeCore, ImageStudioNodeCoreOptions, ImageStudioNodeSnapshot, createImageStudioNodeCore } from "./node-core.js";
|
|
6
|
+
import { ImageStudioReactStore, useImageStudioNodeStore } from "./react/store.js";
|
|
7
|
+
import { ImageStudioDialog } from "./react/ImageStudioDialog.js";
|
|
8
|
+
//#region src/features/image-studio/react.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* The React Bild-Studio feature (shadcn): the `imageStudio` block with its React view (picture or description
|
|
11
|
+
* placeholder, opens the studio dialog on double-click and through the block action `image-studio.open`), the `/` entry
|
|
12
|
+
* `bild-studio` and the action. Generation, upload and the Prompt-Bausteine need a backend with the `images`
|
|
13
|
+
* capability; without it the view says that generation is not set up.
|
|
14
|
+
*/
|
|
15
|
+
export declare function imageStudioFeature(): ReactFeature;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { EMPTY_IMAGE_STUDIO_STATE, ImageStudioDialog, type ImageStudioImage, type ImageStudioNodeCore, type ImageStudioNodeCoreOptions, type ImageStudioNodeSnapshot, type ImageStudioPromptBlock, type ImageStudioReactStore, type ImageStudioState, type ImageStudioTurn, addGeneration, addReference, createImageStudioNodeCore, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock, useImageStudioNodeStore };
|
|
18
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EMPTY_IMAGE_STUDIO_STATE, addGeneration, addReference, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock } from "./state.js";
|
|
2
|
+
import { imageStudioFeatureCore } from "./feature.js";
|
|
3
|
+
import { ImageStudioDialog } from "./react/ImageStudioDialog.js";
|
|
4
|
+
import { createImageStudioNodeCore } from "./node-core.js";
|
|
5
|
+
import { useImageStudioNodeStore } from "./react/store.js";
|
|
6
|
+
import { ImageStudioView } from "./react/ImageStudioView.js";
|
|
7
|
+
import { ReactNodeViewRenderer } from "@tiptap/react";
|
|
8
|
+
//#region src/features/image-studio/react.ts
|
|
9
|
+
/**
|
|
10
|
+
* The React Bild-Studio feature (shadcn): the `imageStudio` block with its React view (picture or description
|
|
11
|
+
* placeholder, opens the studio dialog on double-click and through the block action `image-studio.open`), the `/` entry
|
|
12
|
+
* `bild-studio` and the action. Generation, upload and the Prompt-Bausteine need a backend with the `images`
|
|
13
|
+
* capability; without it the view says that generation is not set up.
|
|
14
|
+
*/
|
|
15
|
+
function imageStudioFeature() {
|
|
16
|
+
return imageStudioFeatureCore({ nodeView: ReactNodeViewRenderer(ImageStudioView) });
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { EMPTY_IMAGE_STUDIO_STATE, ImageStudioDialog, addGeneration, addReference, createImageStudioNodeCore, imageStudioFeature, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock, useImageStudioNodeStore };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../../../src/features/image-studio/react.ts"],"sourcesContent":["import { ReactNodeViewRenderer } from '@tiptap/react'\nimport type { ReactFeature } from '../../react'\nimport { imageStudioFeatureCore } from './feature'\nimport { ImageStudioView } from './react/ImageStudioView'\n\nexport { ImageStudioDialog } from './react/ImageStudioDialog'\nexport { type ImageStudioReactStore, useImageStudioNodeStore } from './react/store'\nexport { createImageStudioNodeCore, type ImageStudioNodeCore, type ImageStudioNodeCoreOptions, type ImageStudioNodeSnapshot } from './node-core'\nexport {\n addGeneration,\n addReference,\n EMPTY_IMAGE_STUDIO_STATE,\n type ImageStudioImage,\n type ImageStudioPromptBlock,\n type ImageStudioState,\n type ImageStudioTurn,\n isEmptyImageStudioState,\n normalizeImageStudioState,\n removeImage,\n select,\n selectedImage,\n setDescription,\n toggleImage,\n togglePromptBlock,\n} from './state'\n\n/**\n * The React Bild-Studio feature (shadcn): the `imageStudio` block with its React view (picture or description\n * placeholder, opens the studio dialog on double-click and through the block action `image-studio.open`), the `/` entry\n * `bild-studio` and the action. Generation, upload and the Prompt-Bausteine need a backend with the `images`\n * capability; without it the view says that generation is not set up.\n */\nexport function imageStudioFeature(): ReactFeature {\n return imageStudioFeatureCore({ nodeView: ReactNodeViewRenderer(ImageStudioView) })\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,SAAgB,qBAAmC;CACjD,OAAO,uBAAuB,EAAE,UAAU,sBAAsB,eAAe,EAAE,CAAC;AACpF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ImageStudioState } from "./state.js";
|
|
2
|
+
import { AnyExtension, Node } from "@tiptap/core";
|
|
3
|
+
//#region src/features/image-studio/schema.d.ts
|
|
4
|
+
/** What `insertImageStudio` takes: the description, chat and images live in `studio`, `src` follows its selected image. */
|
|
5
|
+
export interface ImageStudioAttrs {
|
|
6
|
+
alt?: string;
|
|
7
|
+
studio?: ImageStudioState;
|
|
8
|
+
}
|
|
9
|
+
declare module '@tiptap/core' {
|
|
10
|
+
interface Commands<ReturnType> {
|
|
11
|
+
imageStudio: {
|
|
12
|
+
/** Inserts a Bild-Studio block at the selection. */
|
|
13
|
+
insertImageStudio: (attrs?: ImageStudioAttrs) => ReturnType;
|
|
14
|
+
/** Replaces the studio state of the block at `pos` and mirrors the selected image into its `src`. */
|
|
15
|
+
setImageStudioState: (pos: number, state: ImageStudioState) => ReturnType;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The Bild-Studio node of the document schema. */
|
|
20
|
+
export declare const imageStudioSchemaExtensions: () => AnyExtension[];
|
|
21
|
+
/**
|
|
22
|
+
* The Bild-Studio block: a picture whose description, Prompt-Bausteine, references, previous generations and chat live in
|
|
23
|
+
* the `studio` attribute (`ImageStudioState`); `src` mirrors the selected image. HTML: `<img data-image-studio src alt
|
|
24
|
+
* data-studio="{json}">`. Markdown: `[Bild-Studio: alt](src) <!-- {"studio":{…}} -->` on its own line, the comment left
|
|
25
|
+
* out while the studio is empty. The label is the literal `Bild-Studio` in every UI language (it is what parsing looks for).
|
|
26
|
+
*/
|
|
27
|
+
export declare const ImageStudioNode: Node<any, any>;
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { TEXT_ABILITIES } from "../../core/schema/abilities.js";
|
|
2
|
+
import { attrComment, mediaTokenizer } from "../media/schema.js";
|
|
3
|
+
import { EMPTY_IMAGE_STUDIO_STATE, isEmptyImageStudioState, normalizeImageStudioState, selectedImage } from "./state.js";
|
|
4
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
5
|
+
//#region src/features/image-studio/schema.ts
|
|
6
|
+
/** The Bild-Studio node of the document schema. */
|
|
7
|
+
const imageStudioSchemaExtensions = () => [ImageStudioNode];
|
|
8
|
+
/** The `src` a block shows for `studio`: the url of its selected image, `null` without one. */
|
|
9
|
+
const srcOf = (studio) => selectedImage(studio)?.url ?? null;
|
|
10
|
+
function parseStudioJson(json) {
|
|
11
|
+
if (!json) return void 0;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(json);
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The Bild-Studio block: a picture whose description, Prompt-Bausteine, references, previous generations and chat live in
|
|
20
|
+
* the `studio` attribute (`ImageStudioState`); `src` mirrors the selected image. HTML: `<img data-image-studio src alt
|
|
21
|
+
* data-studio="{json}">`. Markdown: `[Bild-Studio: alt](src) <!-- {"studio":{…}} -->` on its own line, the comment left
|
|
22
|
+
* out while the studio is empty. The label is the literal `Bild-Studio` in every UI language (it is what parsing looks for).
|
|
23
|
+
*/
|
|
24
|
+
const ImageStudioNode = Node.create({
|
|
25
|
+
name: "imageStudio",
|
|
26
|
+
group: "block",
|
|
27
|
+
atom: true,
|
|
28
|
+
draggable: true,
|
|
29
|
+
editorBlock: true,
|
|
30
|
+
abilities: { revokes: TEXT_ABILITIES },
|
|
31
|
+
addAttributes: () => ({
|
|
32
|
+
src: { default: null },
|
|
33
|
+
alt: { default: "" },
|
|
34
|
+
studio: {
|
|
35
|
+
default: EMPTY_IMAGE_STUDIO_STATE,
|
|
36
|
+
parseHTML: (el) => normalizeImageStudioState(parseStudioJson(el.getAttribute("data-studio"))),
|
|
37
|
+
renderHTML: (attrs) => ({ "data-studio": JSON.stringify(attrs.studio) })
|
|
38
|
+
}
|
|
39
|
+
}),
|
|
40
|
+
parseHTML: () => [{
|
|
41
|
+
tag: "img[data-image-studio]",
|
|
42
|
+
priority: 100
|
|
43
|
+
}],
|
|
44
|
+
renderHTML: ({ HTMLAttributes }) => ["img", mergeAttributes(HTMLAttributes, { "data-image-studio": "" })],
|
|
45
|
+
markdownTokenizer: mediaTokenizer("imageStudio", "Bild-Studio"),
|
|
46
|
+
parseMarkdown: (token, helpers) => helpers.createNode("imageStudio", {
|
|
47
|
+
src: token.href || null,
|
|
48
|
+
alt: token.title ?? "",
|
|
49
|
+
studio: normalizeImageStudioState(token.attrs?.studio)
|
|
50
|
+
}),
|
|
51
|
+
renderMarkdown: (node) => {
|
|
52
|
+
const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
|
|
53
|
+
const studio = normalizeImageStudioState(node.attrs?.studio);
|
|
54
|
+
return `[${alt ? `Bild-Studio: ${alt.replace(/[[\]\\]/g, "\\$&")}` : "Bild-Studio"}](${typeof node.attrs?.src === "string" ? node.attrs.src : ""})${attrComment({ studio: isEmptyImageStudioState(studio) ? void 0 : studio })}`;
|
|
55
|
+
},
|
|
56
|
+
addCommands() {
|
|
57
|
+
return {
|
|
58
|
+
insertImageStudio: (attrs) => ({ commands }) => {
|
|
59
|
+
const studio = normalizeImageStudioState(attrs?.studio);
|
|
60
|
+
return commands.insertContent({
|
|
61
|
+
type: this.name,
|
|
62
|
+
attrs: {
|
|
63
|
+
src: srcOf(studio),
|
|
64
|
+
alt: attrs?.alt ?? "",
|
|
65
|
+
studio
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
setImageStudioState: (pos, state) => ({ tr, dispatch }) => {
|
|
70
|
+
const node = tr.doc.nodeAt(pos);
|
|
71
|
+
if (node?.type.name !== this.name) return false;
|
|
72
|
+
const studio = normalizeImageStudioState(state);
|
|
73
|
+
if (dispatch) tr.setNodeMarkup(pos, void 0, {
|
|
74
|
+
...node.attrs,
|
|
75
|
+
studio,
|
|
76
|
+
src: srcOf(studio)
|
|
77
|
+
});
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
//#endregion
|
|
84
|
+
export { ImageStudioNode, imageStudioSchemaExtensions };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/image-studio/schema.ts"],"sourcesContent":["import { type AnyExtension, mergeAttributes, Node } from '@tiptap/core'\nimport { TEXT_ABILITIES } from '../../core/schema'\nimport { attrComment, mediaTokenizer } from '../media/schema'\nimport { EMPTY_IMAGE_STUDIO_STATE, type ImageStudioState, isEmptyImageStudioState, normalizeImageStudioState, selectedImage } from './state'\n\n// Vue-free and server-importable (`@ecsplico/editor/image-studio/schema`): the stored document may contain the node, so\n// the server schema has it. The client adds the node view and the dialog.\n\n/** What `insertImageStudio` takes: the description, chat and images live in `studio`, `src` follows its selected image. */\nexport interface ImageStudioAttrs {\n alt?: string\n studio?: ImageStudioState\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n imageStudio: {\n /** Inserts a Bild-Studio block at the selection. */\n insertImageStudio: (attrs?: ImageStudioAttrs) => ReturnType\n /** Replaces the studio state of the block at `pos` and mirrors the selected image into its `src`. */\n setImageStudioState: (pos: number, state: ImageStudioState) => ReturnType\n }\n }\n}\n\n/** The Bild-Studio node of the document schema. */\nexport const imageStudioSchemaExtensions = (): AnyExtension[] => [ImageStudioNode]\n\n/** The `src` a block shows for `studio`: the url of its selected image, `null` without one. */\nconst srcOf = (studio: ImageStudioState): string | null => selectedImage(studio)?.url ?? null\n\nfunction parseStudioJson(json: string | null): unknown {\n if (!json) return undefined\n try {\n return JSON.parse(json)\n }\n catch {\n return undefined\n }\n}\n\n/**\n * The Bild-Studio block: a picture whose description, Prompt-Bausteine, references, previous generations and chat live in\n * the `studio` attribute (`ImageStudioState`); `src` mirrors the selected image. HTML: `<img data-image-studio src alt\n * data-studio=\"{json}\">`. Markdown: `[Bild-Studio: alt](src) <!-- {\"studio\":{…}} -->` on its own line, the comment left\n * out while the studio is empty. The label is the literal `Bild-Studio` in every UI language (it is what parsing looks for).\n */\nexport const ImageStudioNode = Node.create({\n name: 'imageStudio',\n group: 'block',\n atom: true,\n draggable: true,\n editorBlock: true,\n // A picture block holds no text: nothing text-related applies inside it.\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => ({\n src: { default: null },\n alt: { default: '' },\n studio: {\n default: EMPTY_IMAGE_STUDIO_STATE,\n parseHTML: el => normalizeImageStudioState(parseStudioJson(el.getAttribute('data-studio'))),\n renderHTML: attrs => ({ 'data-studio': JSON.stringify(attrs.studio) }),\n },\n }),\n // Before the stock image rule, which also matches an `<img src>`.\n parseHTML: () => [{ tag: 'img[data-image-studio]', priority: 100 }],\n renderHTML: ({ HTMLAttributes }) => ['img', mergeAttributes(HTMLAttributes, { 'data-image-studio': '' })],\n markdownTokenizer: mediaTokenizer('imageStudio', 'Bild-Studio'),\n parseMarkdown: (token, helpers) => helpers.createNode('imageStudio', {\n src: token.href || null,\n alt: token.title ?? '',\n studio: normalizeImageStudioState(token.attrs?.studio),\n }),\n renderMarkdown: (node) => {\n const alt = typeof node.attrs?.alt === 'string' ? node.attrs.alt : ''\n const studio = normalizeImageStudioState(node.attrs?.studio)\n const label = alt ? `Bild-Studio: ${alt.replace(/[[\\]\\\\]/g, '\\\\$&')}` : 'Bild-Studio'\n const src = typeof node.attrs?.src === 'string' ? node.attrs.src : ''\n return `[${label}](${src})${attrComment({ studio: isEmptyImageStudioState(studio) ? undefined : studio })}`\n },\n addCommands() {\n return {\n insertImageStudio: attrs => ({ commands }) => {\n const studio = normalizeImageStudioState(attrs?.studio)\n return commands.insertContent({ type: this.name, attrs: { src: srcOf(studio), alt: attrs?.alt ?? '', studio } })\n },\n setImageStudioState: (pos, state) => ({ tr, dispatch }) => {\n const node = tr.doc.nodeAt(pos)\n if (node?.type.name !== this.name) return false\n const studio = normalizeImageStudioState(state)\n if (dispatch) tr.setNodeMarkup(pos, undefined, { ...node.attrs, studio, src: srcOf(studio) })\n return true\n },\n }\n },\n})\n"],"mappings":";;;;;;AA0BA,MAAa,oCAAoD,CAAC,eAAe;;AAGjF,MAAM,SAAS,WAA4C,cAAc,MAAM,CAAC,EAAE,OAAO;AAEzF,SAAS,gBAAgB,MAA8B;CACrD,IAAI,CAAC,MAAM,OAAO,KAAA;CAClB,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,QACM;EACJ;CACF;AACF;;;;;;;AAQA,MAAa,kBAAkB,KAAK,OAAO;CACzC,MAAM;CACN,OAAO;CACP,MAAM;CACN,WAAW;CACX,aAAa;CAEb,WAAW,EAAE,SAAS,eAAe;CACrC,sBAAsB;EACpB,KAAK,EAAE,SAAS,KAAK;EACrB,KAAK,EAAE,SAAS,GAAG;EACnB,QAAQ;GACN,SAAS;GACT,YAAW,OAAM,0BAA0B,gBAAgB,GAAG,aAAa,aAAa,CAAC,CAAC;GAC1F,aAAY,WAAU,EAAE,eAAe,KAAK,UAAU,MAAM,MAAM,EAAE;EACtE;CACF;CAEA,iBAAiB,CAAC;EAAE,KAAK;EAA0B,UAAU;CAAI,CAAC;CAClE,aAAa,EAAE,qBAAqB,CAAC,OAAO,gBAAgB,gBAAgB,EAAE,qBAAqB,GAAG,CAAC,CAAC;CACxG,mBAAmB,eAAe,eAAe,aAAa;CAC9D,gBAAgB,OAAO,YAAY,QAAQ,WAAW,eAAe;EACnE,KAAK,MAAM,QAAQ;EACnB,KAAK,MAAM,SAAS;EACpB,QAAQ,0BAA0B,MAAM,OAAO,MAAM;CACvD,CAAC;CACD,iBAAiB,SAAS;EACxB,MAAM,MAAM,OAAO,KAAK,OAAO,QAAQ,WAAW,KAAK,MAAM,MAAM;EACnE,MAAM,SAAS,0BAA0B,KAAK,OAAO,MAAM;EAG3D,OAAO,IAFO,MAAM,gBAAgB,IAAI,QAAQ,YAAY,MAAM,MAAM,cAEvD,IADL,OAAO,KAAK,OAAO,QAAQ,WAAW,KAAK,MAAM,MAAM,GAC1C,GAAG,YAAY,EAAE,QAAQ,wBAAwB,MAAM,IAAI,KAAA,IAAY,OAAO,CAAC;CAC1G;CACA,cAAc;EACZ,OAAO;GACL,oBAAmB,WAAU,EAAE,eAAe;IAC5C,MAAM,SAAS,0BAA0B,OAAO,MAAM;IACtD,OAAO,SAAS,cAAc;KAAE,MAAM,KAAK;KAAM,OAAO;MAAE,KAAK,MAAM,MAAM;MAAG,KAAK,OAAO,OAAO;MAAI;KAAO;IAAE,CAAC;GACjH;GACA,sBAAsB,KAAK,WAAW,EAAE,IAAI,eAAe;IACzD,MAAM,OAAO,GAAG,IAAI,OAAO,GAAG;IAC9B,IAAI,MAAM,KAAK,SAAS,KAAK,MAAM,OAAO;IAC1C,MAAM,SAAS,0BAA0B,KAAK;IAC9C,IAAI,UAAU,GAAG,cAAc,KAAK,KAAA,GAAW;KAAE,GAAG,KAAK;KAAO;KAAQ,KAAK,MAAM,MAAM;IAAE,CAAC;IAC5F,OAAO;GACT;EACF;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ImageStudioImage, ImageStudioPromptBlock } from "../../core/backend-types.js";
|
|
2
|
+
//#region src/features/image-studio/state.d.ts
|
|
3
|
+
/** One chat turn: the instruction and the generation it produced (or the problem). */
|
|
4
|
+
export interface ImageStudioTurn {
|
|
5
|
+
id: string;
|
|
6
|
+
instruction: string;
|
|
7
|
+
imageId: string | null;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
problem?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ImageStudioState {
|
|
12
|
+
/** The text describing the image; '' shows the placeholder. */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Active Prompt-Bausteine, by host id. */
|
|
15
|
+
promptBlockIds: string[];
|
|
16
|
+
/** Uploaded or picked references. */
|
|
17
|
+
references: ImageStudioImage[];
|
|
18
|
+
/** Previous generations; an active one goes into the next generation as a reference. */
|
|
19
|
+
generations: ImageStudioImage[];
|
|
20
|
+
/** Id of the generation (or reference) that is the block's image. */
|
|
21
|
+
selected: string | null;
|
|
22
|
+
/** The chat log: instruction → generation. */
|
|
23
|
+
turns: ImageStudioTurn[];
|
|
24
|
+
}
|
|
25
|
+
/** The state of a block that has no description, no images and no chat yet. */
|
|
26
|
+
export declare const EMPTY_IMAGE_STUDIO_STATE: ImageStudioState;
|
|
27
|
+
/** The image that is the block's image (a generation or a reference), if the selection still points at one. */
|
|
28
|
+
export declare function selectedImage(state: ImageStudioState): ImageStudioImage | undefined;
|
|
29
|
+
export declare function setDescription(state: ImageStudioState, description: string): ImageStudioState;
|
|
30
|
+
/** Adds the Prompt-Baustein `id` to the active ones, or removes it when it is active. */
|
|
31
|
+
export declare function togglePromptBlock(state: ImageStudioState, id: string): ImageStudioState;
|
|
32
|
+
/** Flips `active` of the reference or generation `id`; an unknown id changes nothing. */
|
|
33
|
+
export declare function toggleImage(state: ImageStudioState, id: string): ImageStudioState;
|
|
34
|
+
/** Makes the reference or generation `id` the block's image; `null` clears the choice. An unknown id changes nothing. */
|
|
35
|
+
export declare function select(state: ImageStudioState, id: string | null): ImageStudioState;
|
|
36
|
+
/** Drops the image `id` from the block (references and generations); the selection goes with it, the chat log keeps its turns. */
|
|
37
|
+
export declare function removeImage(state: ImageStudioState, id: string): ImageStudioState;
|
|
38
|
+
/** Appends a reference; one with the same id is replaced where it stands. */
|
|
39
|
+
export declare function addReference(state: ImageStudioState, image: ImageStudioImage): ImageStudioState;
|
|
40
|
+
/** A chat generate means "this is the picture now": appends the image and its turn (pointing at the image) and selects it. */
|
|
41
|
+
export declare function addGeneration(state: ImageStudioState, image: ImageStudioImage, turn: ImageStudioTurn): ImageStudioState;
|
|
42
|
+
/**
|
|
43
|
+
* The state `value` stands for, for parsed attributes and stored JSON: a missing field, a wrong type or an entry without
|
|
44
|
+
* an id falls back to the empty value of that field, and a `selected` that points at no image becomes `null`. A valid
|
|
45
|
+
* state comes back unchanged (as a copy).
|
|
46
|
+
*/
|
|
47
|
+
export declare function normalizeImageStudioState(value: unknown): ImageStudioState;
|
|
48
|
+
/** Whether `state` holds nothing: no description, Prompt-Baustein, image, selection or chat turn. */
|
|
49
|
+
export declare function isEmptyImageStudioState(state: ImageStudioState): boolean;
|
|
50
|
+
//#endregion
|
|
51
|
+
export type { ImageStudioImage, ImageStudioPromptBlock };
|
|
52
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/features/image-studio/state.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Bild-Studio's state: what a block holds about its picture. Host-agnostic
|
|
5
|
+
* JSON; image ids are opaque to the library (a host uses its own asset ids).
|
|
6
|
+
*/
|
|
7
|
+
/** The state of a block that has no description, no images and no chat yet. */
|
|
8
|
+
const EMPTY_IMAGE_STUDIO_STATE = {
|
|
9
|
+
description: "",
|
|
10
|
+
promptBlockIds: [],
|
|
11
|
+
references: [],
|
|
12
|
+
generations: [],
|
|
13
|
+
selected: null,
|
|
14
|
+
turns: []
|
|
15
|
+
};
|
|
16
|
+
const hasImage = (state, id) => state.references.some((image) => image.id === id) || state.generations.some((image) => image.id === id);
|
|
17
|
+
const toggled = (images, id) => images.map((image) => ({
|
|
18
|
+
...image,
|
|
19
|
+
active: image.id === id ? !image.active : image.active
|
|
20
|
+
}));
|
|
21
|
+
/** The image that is the block's image (a generation or a reference), if the selection still points at one. */
|
|
22
|
+
function selectedImage(state) {
|
|
23
|
+
if (state.selected === null) return void 0;
|
|
24
|
+
return state.generations.find((image) => image.id === state.selected) ?? state.references.find((image) => image.id === state.selected);
|
|
25
|
+
}
|
|
26
|
+
function setDescription(state, description) {
|
|
27
|
+
return {
|
|
28
|
+
...state,
|
|
29
|
+
description
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Adds the Prompt-Baustein `id` to the active ones, or removes it when it is active. */
|
|
33
|
+
function togglePromptBlock(state, id) {
|
|
34
|
+
const active = state.promptBlockIds.includes(id);
|
|
35
|
+
return {
|
|
36
|
+
...state,
|
|
37
|
+
promptBlockIds: active ? state.promptBlockIds.filter((other) => other !== id) : [...state.promptBlockIds, id]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Flips `active` of the reference or generation `id`; an unknown id changes nothing. */
|
|
41
|
+
function toggleImage(state, id) {
|
|
42
|
+
return {
|
|
43
|
+
...state,
|
|
44
|
+
references: toggled(state.references, id),
|
|
45
|
+
generations: toggled(state.generations, id)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Makes the reference or generation `id` the block's image; `null` clears the choice. An unknown id changes nothing. */
|
|
49
|
+
function select(state, id) {
|
|
50
|
+
return {
|
|
51
|
+
...state,
|
|
52
|
+
selected: id === null || hasImage(state, id) ? id : state.selected
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Drops the image `id` from the block (references and generations); the selection goes with it, the chat log keeps its turns. */
|
|
56
|
+
function removeImage(state, id) {
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
references: state.references.filter((image) => image.id !== id),
|
|
60
|
+
generations: state.generations.filter((image) => image.id !== id),
|
|
61
|
+
selected: state.selected === id ? null : state.selected
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Appends a reference; one with the same id is replaced where it stands. */
|
|
65
|
+
function addReference(state, image) {
|
|
66
|
+
const known = state.references.some((other) => other.id === image.id);
|
|
67
|
+
return {
|
|
68
|
+
...state,
|
|
69
|
+
references: known ? state.references.map((other) => other.id === image.id ? { ...image } : other) : [...state.references, { ...image }]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A chat generate means "this is the picture now": appends the image and its turn (pointing at the image) and selects it. */
|
|
73
|
+
function addGeneration(state, image, turn) {
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
generations: [...state.generations, { ...image }],
|
|
77
|
+
turns: [...state.turns, {
|
|
78
|
+
...turn,
|
|
79
|
+
imageId: image.id
|
|
80
|
+
}],
|
|
81
|
+
selected: image.id
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const Fields = z.record(z.string(), z.unknown());
|
|
85
|
+
const textOf = (value) => typeof value === "string" ? value : "";
|
|
86
|
+
const idOf = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
87
|
+
/** The entries of the array `value` that `parse` accepts (it answers `null` for a refused one); a non-array has none. */
|
|
88
|
+
function entriesOf(value, parse) {
|
|
89
|
+
return Array.isArray(value) ? value.flatMap((entry) => parse(entry) ?? []) : [];
|
|
90
|
+
}
|
|
91
|
+
function imageOf(entry) {
|
|
92
|
+
const fields = Fields.safeParse(entry);
|
|
93
|
+
const id = fields.success ? idOf(fields.data.id) : null;
|
|
94
|
+
if (!fields.success || id === null) return null;
|
|
95
|
+
return {
|
|
96
|
+
id,
|
|
97
|
+
url: textOf(fields.data.url),
|
|
98
|
+
title: textOf(fields.data.title),
|
|
99
|
+
active: fields.data.active === true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function turnOf(entry) {
|
|
103
|
+
const fields = Fields.safeParse(entry);
|
|
104
|
+
const id = fields.success ? idOf(fields.data.id) : null;
|
|
105
|
+
if (!fields.success || id === null) return null;
|
|
106
|
+
const turn = {
|
|
107
|
+
id,
|
|
108
|
+
instruction: textOf(fields.data.instruction),
|
|
109
|
+
imageId: idOf(fields.data.imageId),
|
|
110
|
+
createdAt: textOf(fields.data.createdAt)
|
|
111
|
+
};
|
|
112
|
+
const problem = idOf(fields.data.problem);
|
|
113
|
+
return problem === null ? turn : {
|
|
114
|
+
...turn,
|
|
115
|
+
problem
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The state `value` stands for, for parsed attributes and stored JSON: a missing field, a wrong type or an entry without
|
|
120
|
+
* an id falls back to the empty value of that field, and a `selected` that points at no image becomes `null`. A valid
|
|
121
|
+
* state comes back unchanged (as a copy).
|
|
122
|
+
*/
|
|
123
|
+
function normalizeImageStudioState(value) {
|
|
124
|
+
const fields = Fields.safeParse(value);
|
|
125
|
+
const data = fields.success ? fields.data : {};
|
|
126
|
+
const state = {
|
|
127
|
+
description: textOf(data.description),
|
|
128
|
+
promptBlockIds: entriesOf(data.promptBlockIds, idOf),
|
|
129
|
+
references: entriesOf(data.references, imageOf),
|
|
130
|
+
generations: entriesOf(data.generations, imageOf),
|
|
131
|
+
selected: null,
|
|
132
|
+
turns: entriesOf(data.turns, turnOf)
|
|
133
|
+
};
|
|
134
|
+
const selected = idOf(data.selected);
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
selected: selected !== null && hasImage(state, selected) ? selected : null
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Whether `state` holds nothing: no description, Prompt-Baustein, image, selection or chat turn. */
|
|
141
|
+
function isEmptyImageStudioState(state) {
|
|
142
|
+
return state.description === "" && state.promptBlockIds.length === 0 && state.references.length === 0 && state.generations.length === 0 && state.selected === null && state.turns.length === 0;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { EMPTY_IMAGE_STUDIO_STATE, addGeneration, addReference, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","names":[],"sources":["../../../src/features/image-studio/state.ts"],"sourcesContent":["/**\n * The Bild-Studio's state: what a block holds about its picture. Host-agnostic\n * JSON; image ids are opaque to the library (a host uses its own asset ids).\n */\n\nimport { z } from 'zod'\nimport type { ImageStudioImage, ImageStudioPromptBlock } from '../../core/backend-types'\n\n// `ImageStudioImage` and `ImageStudioPromptBlock` are wire types of `EditorBackend.images`, so they live in\n// `core/backend-types.ts` (core never imports a feature); this module is their feature-side entry.\nexport type { ImageStudioImage, ImageStudioPromptBlock }\n\n/** One chat turn: the instruction and the generation it produced (or the problem). */\nexport interface ImageStudioTurn {\n id: string\n instruction: string\n imageId: string | null\n createdAt: string\n problem?: string\n}\n\nexport interface ImageStudioState {\n /** The text describing the image; '' shows the placeholder. */\n description: string\n /** Active Prompt-Bausteine, by host id. */\n promptBlockIds: string[]\n /** Uploaded or picked references. */\n references: ImageStudioImage[]\n /** Previous generations; an active one goes into the next generation as a reference. */\n generations: ImageStudioImage[]\n /** Id of the generation (or reference) that is the block's image. */\n selected: string | null\n /** The chat log: instruction → generation. */\n turns: ImageStudioTurn[]\n}\n\n/** The state of a block that has no description, no images and no chat yet. */\nexport const EMPTY_IMAGE_STUDIO_STATE: ImageStudioState = {\n description: '',\n promptBlockIds: [],\n references: [],\n generations: [],\n selected: null,\n turns: [],\n}\n\n// Reducers: pure, each returns a new state and never touches its argument. The host owns the files behind the images.\n\nconst hasImage = (state: ImageStudioState, id: string): boolean =>\n state.references.some(image => image.id === id) || state.generations.some(image => image.id === id)\n\nconst toggled = (images: readonly ImageStudioImage[], id: string): ImageStudioImage[] =>\n images.map(image => ({ ...image, active: image.id === id ? !image.active : image.active }))\n\n/** The image that is the block's image (a generation or a reference), if the selection still points at one. */\nexport function selectedImage(state: ImageStudioState): ImageStudioImage | undefined {\n if (state.selected === null) return undefined\n return state.generations.find(image => image.id === state.selected) ?? state.references.find(image => image.id === state.selected)\n}\n\nexport function setDescription(state: ImageStudioState, description: string): ImageStudioState {\n return { ...state, description }\n}\n\n/** Adds the Prompt-Baustein `id` to the active ones, or removes it when it is active. */\nexport function togglePromptBlock(state: ImageStudioState, id: string): ImageStudioState {\n const active = state.promptBlockIds.includes(id)\n return { ...state, promptBlockIds: active ? state.promptBlockIds.filter(other => other !== id) : [...state.promptBlockIds, id] }\n}\n\n/** Flips `active` of the reference or generation `id`; an unknown id changes nothing. */\nexport function toggleImage(state: ImageStudioState, id: string): ImageStudioState {\n return { ...state, references: toggled(state.references, id), generations: toggled(state.generations, id) }\n}\n\n/** Makes the reference or generation `id` the block's image; `null` clears the choice. An unknown id changes nothing. */\nexport function select(state: ImageStudioState, id: string | null): ImageStudioState {\n return { ...state, selected: id === null || hasImage(state, id) ? id : state.selected }\n}\n\n/** Drops the image `id` from the block (references and generations); the selection goes with it, the chat log keeps its turns. */\nexport function removeImage(state: ImageStudioState, id: string): ImageStudioState {\n return {\n ...state,\n references: state.references.filter(image => image.id !== id),\n generations: state.generations.filter(image => image.id !== id),\n selected: state.selected === id ? null : state.selected,\n }\n}\n\n/** Appends a reference; one with the same id is replaced where it stands. */\nexport function addReference(state: ImageStudioState, image: ImageStudioImage): ImageStudioState {\n const known = state.references.some(other => other.id === image.id)\n return { ...state, references: known ? state.references.map(other => other.id === image.id ? { ...image } : other) : [...state.references, { ...image }] }\n}\n\n/** A chat generate means \"this is the picture now\": appends the image and its turn (pointing at the image) and selects it. */\nexport function addGeneration(state: ImageStudioState, image: ImageStudioImage, turn: ImageStudioTurn): ImageStudioState {\n return {\n ...state,\n generations: [...state.generations, { ...image }],\n turns: [...state.turns, { ...turn, imageId: image.id }],\n selected: image.id,\n }\n}\n\n// Parsing: stored attributes and JSON from other tools are untrusted, a missing or mistyped field falls back to its empty value.\n// Fields are read one by one from a record: a zod object would reject an entry for a missing key, where this one defaults it.\nconst Fields = z.record(z.string(), z.unknown())\n\nconst textOf = (value: unknown): string => typeof value === 'string' ? value : ''\nconst idOf = (value: unknown): string | null => typeof value === 'string' && value !== '' ? value : null\n\n/** The entries of the array `value` that `parse` accepts (it answers `null` for a refused one); a non-array has none. */\nfunction entriesOf<T>(value: unknown, parse: (entry: unknown) => T | null): T[] {\n return Array.isArray(value) ? value.flatMap((entry: unknown) => parse(entry) ?? []) : []\n}\n\nfunction imageOf(entry: unknown): ImageStudioImage | null {\n const fields = Fields.safeParse(entry)\n const id = fields.success ? idOf(fields.data.id) : null\n if (!fields.success || id === null) return null\n return { id, url: textOf(fields.data.url), title: textOf(fields.data.title), active: fields.data.active === true }\n}\n\nfunction turnOf(entry: unknown): ImageStudioTurn | null {\n const fields = Fields.safeParse(entry)\n const id = fields.success ? idOf(fields.data.id) : null\n if (!fields.success || id === null) return null\n const turn: ImageStudioTurn = { id, instruction: textOf(fields.data.instruction), imageId: idOf(fields.data.imageId), createdAt: textOf(fields.data.createdAt) }\n const problem = idOf(fields.data.problem)\n return problem === null ? turn : { ...turn, problem }\n}\n\n/**\n * The state `value` stands for, for parsed attributes and stored JSON: a missing field, a wrong type or an entry without\n * an id falls back to the empty value of that field, and a `selected` that points at no image becomes `null`. A valid\n * state comes back unchanged (as a copy).\n */\nexport function normalizeImageStudioState(value: unknown): ImageStudioState {\n const fields = Fields.safeParse(value)\n const data = fields.success ? fields.data : {}\n const state: ImageStudioState = {\n description: textOf(data.description),\n promptBlockIds: entriesOf(data.promptBlockIds, idOf),\n references: entriesOf(data.references, imageOf),\n generations: entriesOf(data.generations, imageOf),\n selected: null,\n turns: entriesOf(data.turns, turnOf),\n }\n const selected = idOf(data.selected)\n return { ...state, selected: selected !== null && hasImage(state, selected) ? selected : null }\n}\n\n/** Whether `state` holds nothing: no description, Prompt-Baustein, image, selection or chat turn. */\nexport function isEmptyImageStudioState(state: ImageStudioState): boolean {\n return state.description === '' && state.promptBlockIds.length === 0 && state.references.length === 0 && state.generations.length === 0\n && state.selected === null && state.turns.length === 0\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ImageStudioImage, ImageStudioPromptBlock } from "../../core/backend-types.js";
|
|
2
|
+
import { ImageStudioState } from "./state.js";
|
|
3
|
+
import { Ref } from "vue";
|
|
4
|
+
//#region src/features/image-studio/store.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the Bild-Studio dialog drives; a host supplies it (the node view builds one over the node's `studio` attribute and
|
|
7
|
+
* `EditorBackend.images`, an app with its own image store builds its own).
|
|
8
|
+
*
|
|
9
|
+
* The dialog applies the reducers of `./state` and hands the result to `update`; the store only persists it. The
|
|
10
|
+
* store's methods never touch `state` themselves:
|
|
11
|
+
* - `generate` produces one image from the block's `state.description`, the active Prompt-Bausteine and the active
|
|
12
|
+
* images (the host builds the prompt). When it resolves, the dialog calls `update(addGeneration(state, image, turn))`;
|
|
13
|
+
* when it rejects (and the signal was not aborted) the dialog appends a turn with `imageId: null` and the message as
|
|
14
|
+
* `problem`. An aborted generation adds nothing.
|
|
15
|
+
* - `upload` stores the file and returns the image; the dialog then calls `update(addReference(state, image))`.
|
|
16
|
+
* - `problem` is a problem of the store itself (the Prompt-Bausteine could not be loaded, say); the dialog shows it as a notice.
|
|
17
|
+
*/
|
|
18
|
+
export interface ImageStudioStore {
|
|
19
|
+
readonly state: Ref<ImageStudioState>;
|
|
20
|
+
/** The managed list of Prompt-Bausteine, loaded by the host. */
|
|
21
|
+
readonly promptBlocks: Ref<readonly ImageStudioPromptBlock[]>;
|
|
22
|
+
/** Called with the state after a reducer; the host persists it. */
|
|
23
|
+
update: (next: ImageStudioState) => void;
|
|
24
|
+
generate: (instruction: string, signal: AbortSignal) => Promise<ImageStudioImage>;
|
|
25
|
+
upload: (file: File) => Promise<ImageStudioImage>;
|
|
26
|
+
readonly problem: Ref<string | undefined>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ImageStudioImage, ImageStudioPromptBlock } from "../../core/backend-types.js";
|
|
2
|
+
import { VueFeature } from "../../vue/index.js";
|
|
3
|
+
import { EMPTY_IMAGE_STUDIO_STATE, ImageStudioState, ImageStudioTurn, addGeneration, addReference, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock } from "./state.js";
|
|
4
|
+
import { ImageStudioStore } from "./store.js";
|
|
5
|
+
import _default from "./components/ImageStudioDialog.vue.js";
|
|
6
|
+
import { ImageStudioNodeStore, ImageStudioNodeStoreOptions, createImageStudioNodeStore } from "./node-store.js";
|
|
7
|
+
//#region src/features/image-studio/vue.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* The Vue Bild-Studio feature: the `imageStudio` block with its Vue view (picture or description placeholder, opens the
|
|
10
|
+
* studio dialog on double-click and through the block action `image-studio.open`), the `/` entry `bild-studio` and the
|
|
11
|
+
* action. Generation, upload and the Prompt-Bausteine need a backend with the `images` capability; without it the view
|
|
12
|
+
* says that generation is not set up.
|
|
13
|
+
*/
|
|
14
|
+
export declare function imageStudioFeature(): VueFeature;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { EMPTY_IMAGE_STUDIO_STATE, _default as ImageStudioDialog, type ImageStudioImage, type ImageStudioNodeStore, type ImageStudioNodeStoreOptions, type ImageStudioPromptBlock, type ImageStudioState, type ImageStudioStore, type ImageStudioTurn, addGeneration, addReference, createImageStudioNodeStore, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock };
|
|
17
|
+
//# sourceMappingURL=vue.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EMPTY_IMAGE_STUDIO_STATE, addGeneration, addReference, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock } from "./state.js";
|
|
2
|
+
import { imageStudioFeatureCore } from "./feature.js";
|
|
3
|
+
import { createImageStudioNodeStore } from "./node-store.js";
|
|
4
|
+
import _sfc_main from "./components/ImageStudioDialog.js";
|
|
5
|
+
import _sfc_main$1 from "./components/ImageStudioView.js";
|
|
6
|
+
import { VueNodeViewRenderer } from "@tiptap/vue-3";
|
|
7
|
+
//#region src/features/image-studio/vue.ts
|
|
8
|
+
/**
|
|
9
|
+
* The Vue Bild-Studio feature: the `imageStudio` block with its Vue view (picture or description placeholder, opens the
|
|
10
|
+
* studio dialog on double-click and through the block action `image-studio.open`), the `/` entry `bild-studio` and the
|
|
11
|
+
* action. Generation, upload and the Prompt-Bausteine need a backend with the `images` capability; without it the view
|
|
12
|
+
* says that generation is not set up.
|
|
13
|
+
*/
|
|
14
|
+
function imageStudioFeature() {
|
|
15
|
+
return imageStudioFeatureCore({ nodeView: VueNodeViewRenderer(_sfc_main$1) });
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { EMPTY_IMAGE_STUDIO_STATE, _sfc_main as ImageStudioDialog, addGeneration, addReference, createImageStudioNodeStore, imageStudioFeature, isEmptyImageStudioState, normalizeImageStudioState, removeImage, select, selectedImage, setDescription, toggleImage, togglePromptBlock };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue.js","names":["ImageStudioView"],"sources":["../../../src/features/image-studio/vue.ts"],"sourcesContent":["import { VueNodeViewRenderer } from '@tiptap/vue-3'\nimport type { VueFeature } from '../../vue'\nimport ImageStudioView from './components/ImageStudioView.vue'\nimport { imageStudioFeatureCore } from './feature'\n\nexport { default as ImageStudioDialog } from './components/ImageStudioDialog.vue'\nexport { createImageStudioNodeStore, type ImageStudioNodeStore, type ImageStudioNodeStoreOptions } from './node-store'\nexport {\n addGeneration,\n addReference,\n EMPTY_IMAGE_STUDIO_STATE,\n type ImageStudioImage,\n type ImageStudioPromptBlock,\n type ImageStudioState,\n type ImageStudioTurn,\n isEmptyImageStudioState,\n normalizeImageStudioState,\n removeImage,\n select,\n selectedImage,\n setDescription,\n toggleImage,\n togglePromptBlock,\n} from './state'\nexport type { ImageStudioStore } from './store'\n\n/**\n * The Vue Bild-Studio feature: the `imageStudio` block with its Vue view (picture or description placeholder, opens the\n * studio dialog on double-click and through the block action `image-studio.open`), the `/` entry `bild-studio` and the\n * action. Generation, upload and the Prompt-Bausteine need a backend with the `images` capability; without it the view\n * says that generation is not set up.\n */\nexport function imageStudioFeature(): VueFeature {\n return imageStudioFeatureCore({ nodeView: VueNodeViewRenderer(ImageStudioView) })\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,SAAgB,qBAAiC;CAC/C,OAAO,uBAAuB,EAAE,UAAU,oBAAoBA,WAAe,EAAE,CAAC;AAClF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ASSET_DOCUMENT_ID, ASSET_NAME, AssetManifest, AssetRecord, SCRATCH_DOCUMENT_ID, assetRefOf, assetUrl } from "./assets.js";
|
|
2
|
-
import { AnyExtension, Node } from "@tiptap/core";
|
|
2
|
+
import { AnyExtension, MarkdownTokenizer, Node } from "@tiptap/core";
|
|
3
3
|
//#region src/features/media/schema.d.ts
|
|
4
4
|
/** The media nodes (image, audio, video, file) of the document schema. */
|
|
5
5
|
export declare const mediaSchemaExtensions: () => AnyExtension[];
|
|
@@ -9,6 +9,10 @@ export declare const mediaSchemaExtensions: () => AnyExtension[];
|
|
|
9
9
|
* token), a plain image keeps the stock inline parse.
|
|
10
10
|
*/
|
|
11
11
|
export declare const ImageNode: Node<import("@tiptap/extension-image").ImageOptions, any>;
|
|
12
|
+
/** ` <!-- {"hash":"…"} -->` for the attributes that are set (empty string when none); `>` and the line separators U+2028/U+2029 (which `.` does not match) are escaped so the JSON can neither end the comment nor break the line. Shared with the other block features that keep attributes in a trailing comment. */
|
|
13
|
+
export declare function attrComment(attrs: Record<string, unknown>): string;
|
|
14
|
+
/** Block tokenizer for `[<label>: title](src)` lines of one media node; `labels` are the accepted labels (regex alternatives). */
|
|
15
|
+
export declare function mediaTokenizer(name: string, labels: string): MarkdownTokenizer;
|
|
12
16
|
export type MediaKind = 'image' | 'audio' | 'video' | 'file';
|
|
13
17
|
/** Uploadable MIME types → file extension on disk and media kind. `<uuid>.<ext>` asset names derive from this. */
|
|
14
18
|
export declare const MEDIA_TYPES: Readonly<Record<string, {
|
|
@@ -58,10 +58,10 @@ const ImageNode = Image.extend({
|
|
|
58
58
|
});
|
|
59
59
|
const ATTR_COMMENT_PATTERN = "(?: <!-- (\\{.*?\\}) -->)?[ \\t]*(?:\\n|$)";
|
|
60
60
|
const IMAGE_BLOCK_PATTERN = `^!\\[([^\\]\\n]*)\\]\\(([^)\\s]*)(?: "([^"\\n]*)")?\\) <!-- (\\{.*?\\}) -->[ \\t]*(?:\\n|$)`;
|
|
61
|
-
/** ` <!-- {"hash":"…"} -->` for the attributes that are set (empty string when none); `>`
|
|
61
|
+
/** ` <!-- {"hash":"…"} -->` for the attributes that are set (empty string when none); `>` and the line separators U+2028/U+2029 (which `.` does not match) are escaped so the JSON can neither end the comment nor break the line. Shared with the other block features that keep attributes in a trailing comment. */
|
|
62
62
|
function attrComment(attrs) {
|
|
63
63
|
const set = Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== void 0 && value !== "" && value !== 0));
|
|
64
|
-
return Object.keys(set).length ? ` <!-- ${JSON.stringify(set).replace(
|
|
64
|
+
return Object.keys(set).length ? ` <!-- ${JSON.stringify(set).replace(/[>\u2028\u2029]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`)} -->` : "";
|
|
65
65
|
}
|
|
66
66
|
/** The attributes of an `attrComment`; empty for a missing or malformed one. */
|
|
67
67
|
function parseAttrComment(json) {
|
|
@@ -304,6 +304,6 @@ const FileNode = Node.create({
|
|
|
304
304
|
}
|
|
305
305
|
});
|
|
306
306
|
//#endregion
|
|
307
|
-
export { ASSET_DOCUMENT_ID, ASSET_NAME, AudioNode, FileNode, ImageNode, MAX_MEDIA_BYTES, MEDIA_TYPES, SCRATCH_DOCUMENT_ID, VideoNode, acceptFor, assetRefOf, assetUrl, mediaKindOf, mediaMime, mediaSchemaExtensions };
|
|
307
|
+
export { ASSET_DOCUMENT_ID, ASSET_NAME, AudioNode, FileNode, ImageNode, MAX_MEDIA_BYTES, MEDIA_TYPES, SCRATCH_DOCUMENT_ID, VideoNode, acceptFor, assetRefOf, assetUrl, attrComment, mediaKindOf, mediaMime, mediaSchemaExtensions, mediaTokenizer };
|
|
308
308
|
|
|
309
309
|
//# sourceMappingURL=schema.js.map
|