@ecsplico/editor 3.3.0 → 3.5.0
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 +24 -0
- 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/features/ai/protocol/generate.d.ts +1 -1
- package/dist/features/composites/components/CompositeChrome.js +10 -9
- package/dist/features/composites/components/CompositeChrome.js.map +1 -1
- package/dist/features/composites/kinds.d.ts +4 -0
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/merge.d.ts +1 -1
- package/dist/features/composites/merge.js +5 -2
- package/dist/features/composites/merge.js.map +1 -1
- package/dist/features/composites/nodes.js +1 -0
- package/dist/features/composites/nodes.js.map +1 -1
- package/dist/features/composites/outline.d.ts +9 -2
- package/dist/features/composites/outline.js +41 -3
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeChrome.js +11 -9
- package/dist/features/composites/react/CompositeChrome.js.map +1 -1
- package/dist/features/composites/rows.d.ts +2 -0
- package/dist/features/composites/rows.js +1 -0
- package/dist/features/composites/rows.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-store.d.ts +27 -0
- package/dist/features/image-studio/node-store.js +64 -0
- package/dist/features/image-studio/node-store.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/nuxt/module.d.ts +64 -0
- 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/DocumentTree.js +5 -4
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/react/DocumentTree.js +2 -1
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/tree-items.js +1 -0
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +1 -1
- package/package.json +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.0 - 2026-10-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Bild-Studio (`@ecsplico/editor/image-studio`, `/image-studio/vue`, `/image-studio/schema`): an atom block `imageStudio` (attrs `src`, `alt`, `studio`; ability `ABILITY.imageStudio` = `insert.imageStudio`, revokes text abilities) that holds the text describing an image, the active Prompt-Bausteine, references, previous generations, the selected image and a chat log (`ImageStudioState`, `ImageStudioImage`, `ImageStudioTurn`, `ImageStudioPromptBlock`). `src` always mirrors the selected image. Markdown form `[Bild-Studio: <alt>](<src>) <!-- {"studio": …} -->`, round-trip stable; HTML `<img data-image-studio data-studio>`. Commands `insertImageStudio(attrs?)` and `setImageStudioState(pos, state)` (one transaction, `src` follows), `/` entry „Bild-Studio“, block action `image-studio.open` „Im Bild-Studio öffnen“ (placement `both`, event `IMAGE_STUDIO_OPEN`). Pure reducers `EMPTY_IMAGE_STUDIO_STATE`, `setDescription`, `togglePromptBlock`, `toggleImage`, `select`, `removeImage`, `addReference`, `addGeneration` (appends and selects), `selectedImage`, `normalizeImageStudioState`, `isEmptyImageStudioState`.
|
|
8
|
+
- `ImageStudioDialog` (Vue): three columns (description and Prompt-Bausteine as switches; references with upload and drop zone, previous generations, tiles to activate, select or remove; the selected image with a chat that generates, `Enter` sends, closing aborts) driven by an `ImageStudioStore` (`state`, `promptBlocks`, `update`, `generate`, `upload`, `problem`), so a host can keep the state outside the document. `imageStudioFeature()` adds the node view (`ImageStudioView`) and the node's own store `createImageStudioNodeStore({ editor, getPos, backend, documentId })`.
|
|
9
|
+
- Backend port `EditorBackend.images?` (`promptBlocks()`, `generate(body: ImageStudioGenerateBody, signal?)`, `upload(documentId, file)`), capability `images` (probed by the HTTP backend; the memory backend has none and the node view shows „Bildgenerierung ist nicht eingerichtet“). Server port `EditorPorts.images?: ImageProvider` with routes `GET /api/editor/images/prompt-blocks`, `POST /api/editor/images/generate`, `POST /api/editor/images/upload`, mounted and listed in the capabilities only with the port. Guide „Bild-Studio“.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `attrComment` and `mediaTokenizer` are exported from `@ecsplico/editor/media/schema` (shared with the Bild-Studio's Markdown form); `attrComment` escapes U+2028 and U+2029 in the JSON comment so such text survives a Markdown round trip.
|
|
14
|
+
|
|
15
|
+
React mirror of the dialog and node view follows in 3.5.1.
|
|
16
|
+
|
|
17
|
+
## 3.4.0 - 2026-10-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `CompositeKind.chrome.controls?: boolean` (default `true`). With `controls: false` the chrome of a Baustein of that kind is read-only although the editor is editable (Vue and React `CompositeChrome`; `CompositeRow.controls`): no basket menu, status menu, title-visibility button, tag removal or `+` chip and no drag handle on the kind icon, and a read-only details popover; existing basket, status and tags stay visible as chips. `mergeCompositeWithNext` also refuses (so `TreeModel.canMerge`, the row button and the block action disappear) when the Baustein or the one it would absorb has such a kind. For kinds whose attributes a host guard refuses to change.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Outline label of a Baustein without a title: instead of „(ohne Titel)“ the document tree (Vue and React rows) shows the text of the first heading of the Baustein's own body, else the start of the first non-empty text of its own body (cut at 60 characters with „…“); text inside a nested Baustein is not used, and only a Baustein with an empty body keeps „(ohne Titel)“. `compositeOutline` marks such a label with `OutlineNode.labelFromBody` and `TreeItem.labelFromBody` (rows render it muted and italic, `data-label-from-body`), and the title filter matches the derived label too. A Baustein with a title is unchanged.
|
|
26
|
+
|
|
3
27
|
## 3.3.0 - 2026-10-21
|
|
4
28
|
|
|
5
29
|
### Added
|
package/dist/core/abilities.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abilities.js","names":[],"sources":["../../src/core/abilities.ts"],"sourcesContent":["import { type AnyExtension, type Editor, Extension, getExtensionField, type InputRule, type KeyboardShortcutCommand } from '@tiptap/core'\nimport { keydownHandler } from '@tiptap/pm/keymap'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport {\n ABILITY,\n type Ability,\n type AbilityRuleSource,\n resolveAbilities,\n ROOT_ABILITIES,\n TEXT_ABILITIES,\n TYPE_ABILITIES,\n} from './schema/abilities'\nimport type { AbilitiesOptions } from './extension-points'\n\nexport interface AbilitiesStorage {\n rules: Map<string, AbilityRuleSource>\n root: readonly Ability[]\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n abilities: AbilitiesStorage\n }\n}\n\n/** Rules for StarterKit/extension nodes that cannot carry `NodeConfig.abilities` themselves. */\nexport const DEFAULT_ABILITY_RULES: Record<string, AbilityRuleSource> = {\n codeBlock: {\n revokes: [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.rule, ABILITY.blockContent, ABILITY.composite, ABILITY.transcript,\n ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiAutocomplete,\n ],\n },\n image: { revokes: TEXT_ABILITIES },\n horizontalRule: { revokes: TEXT_ABILITIES },\n}\n\n/**\n * Abilities at `pos`: chain = ancestors at depth 1…depth, plus `node` when given (an atom starting at `pos`).\n * A `ResolvedPos` addresses a document other than `editor.state.doc`, e.g. a transaction's doc inside plugin `apply`.\n */\nexport function abilitiesAt(editor: Editor, pos: number | ResolvedPos, node?: PMNode): ReadonlySet<Ability> {\n const $pos = typeof pos === 'number' ? editor.state.doc.resolve(pos) : pos\n const chain: PMNode[] = []\n for (let depth = 1; depth <= $pos.depth; depth++) chain.push($pos.node(depth))\n if (node) chain.push(node)\n const { rules, root } = editor.storage.abilities\n return resolveAbilities(rules, root, chain)\n}\n\n/** Abilities of the selection: the selected node, or what both ends of a range allow. */\nexport function abilitiesAtSelection(editor: Editor): ReadonlySet<Ability> {\n const { selection } = editor.state\n if (selection instanceof NodeSelection) return abilitiesAt(editor, selection.from, selection.node)\n const from = abilitiesAt(editor, selection.$from.pos)\n if (selection.empty) return from\n const to = abilitiesAt(editor, selection.$to.pos)\n return new Set([...from].filter(ability => to.has(ability)))\n}\n\nexport function hasAbility(editor: Editor, ability: Ability): boolean {\n return abilitiesAtSelection(editor).has(ability)\n}\n\n/** Only modifier shortcuts are gated; plain Enter/Tab/Backspace keep their structural behaviour everywhere. */\nconst GATED_KEY = /(^|-)(Mod|Alt)-/\n/** How much text before the caret input rules get to see (mirrors TipTap's input-rule plugin). */\nconst MAX_MATCH = 500\n\ninterface GatedInputRule {\n find: InputRule['find']\n ability: Ability\n}\n\n/** Same `this` the ExtensionManager hands to `addKeyboardShortcuts` / `addInputRules` (core dist, `get plugins()`). */\nfunction extensionContext(ext: AnyExtension, editor: Editor) {\n return {\n name: ext.name,\n options: ext.options,\n storage: editor.extensionStorage[ext.name as keyof typeof editor.extensionStorage],\n editor,\n type: editor.schema.nodes[ext.name] ?? editor.schema.marks[ext.name] ?? null,\n }\n}\n\nfunction gatedExtensions(editor: Editor): [AnyExtension, Ability][] {\n return editor.extensionManager.extensions.flatMap((ext): [AnyExtension, Ability][] => {\n const ability = TYPE_ABILITIES[ext.name]\n return ability ? [[ext, ability]] : []\n })\n}\n\nfunction buildKeyBindings(editor: Editor) {\n const abilitiesByKey = new Map<string, Ability[]>()\n for (const [ext, ability] of gatedExtensions(editor)) {\n const addKeyboardShortcuts = getExtensionField<(() => Record<string, KeyboardShortcutCommand>) | undefined>(\n ext, 'addKeyboardShortcuts', extensionContext(ext, editor),\n )\n for (const key of Object.keys(addKeyboardShortcuts?.() ?? {})) {\n if (!GATED_KEY.test(key)) continue\n abilitiesByKey.set(key, [...(abilitiesByKey.get(key) ?? []), ability])\n }\n }\n // Returning true swallows the key, so the type's own binding never runs.\n return Object.fromEntries([...abilitiesByKey].map(([key, abilities]) => [\n key,\n () => {\n const present = abilitiesAtSelection(editor)\n return abilities.some(ability => !present.has(ability))\n },\n ]))\n}\n\nfunction buildInputRules(editor: Editor): GatedInputRule[] {\n return gatedExtensions(editor).flatMap(([ext, ability]) => {\n const addInputRules = getExtensionField<(() => InputRule[]) | undefined>(ext, 'addInputRules', extensionContext(ext, editor))\n return (addInputRules?.() ?? []).map(rule => ({ find: rule.find, ability }))\n })\n}\n\nfunction matches(find: InputRule['find'], text: string): boolean {\n if (typeof find === 'function') return find(text) != null\n find.lastIndex = 0\n return find.exec(text) !== null\n}\n\n/**\n * Hierarchical abilities (see shared/editor/abilities.ts). Resolution reads `editor.storage.abilities`; the plugin\n * swallows `Mod-`/`Alt-` shortcuts and pre-empts input rules of node/mark types whose ability is missing at the caret.\n * Paste, drop and remote changes are not filtered.\n */\nexport const Abilities = Extension.create<AbilitiesOptions, AbilitiesStorage>({\n name: 'abilities',\n // Highest priority: its plugin's handleKeyDown / handleTextInput run before the gated types' keymaps and input rules.\n priority: 1000,\n\n addOptions() {\n return { root: ROOT_ABILITIES, rules: DEFAULT_ABILITY_RULES }\n },\n\n addStorage() {\n return { rules: new Map(), root: this.options.root }\n },\n\n onBeforeCreate() {\n const rules = new Map<string, AbilityRuleSource>()\n for (const ext of this.editor.extensionManager.extensions) {\n const rule = getExtensionField<AbilityRuleSource | undefined>(ext, 'abilities')\n if (rule) rules.set(ext.name, rule)\n }\n // Host/default rules replace a node's own rule as a whole.\n for (const [name, rule] of Object.entries(this.options.rules)) rules.set(name, rule)\n this.storage.rules = rules\n this.storage.root = this.options.root\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n // Built lazily on first use, once per plugin: needs the final extension list and schema.\n let handleKey: ((view: EditorView, event: KeyboardEvent) => boolean) | undefined\n let inputRules: GatedInputRule[] | undefined\n\n return [\n new Plugin({\n key: new PluginKey('abilities'),\n props: {\n handleKeyDown(view, event) {\n handleKey ??= keydownHandler(buildKeyBindings(editor))\n return handleKey(view, event)\n },\n handleTextInput(view, from, to, text) {\n inputRules ??= buildInputRules(editor)\n if (!inputRules.length) return false\n const $from = view.state.doc.resolve(from)\n if (!$from.parent.isTextblock) return false\n const textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, undefined, '\\ufffc') + text\n let present: ReadonlySet<Ability> | undefined\n for (const rule of inputRules) {\n present ??= abilitiesAt(editor, from)\n if (present.has(rule.ability) || !matches(rule.find, textBefore)) continue\n // Type the character literally; the rule's own plugin never sees this input.\n view.dispatch(view.state.tr.insertText(text, from, to))\n return true\n }\n return false\n },\n },\n }),\n ]\n },\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"abilities.js","names":[],"sources":["../../src/core/abilities.ts"],"sourcesContent":["import { type AnyExtension, type Editor, Extension, getExtensionField, type InputRule, type KeyboardShortcutCommand } from '@tiptap/core'\nimport { keydownHandler } from '@tiptap/pm/keymap'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state'\nimport type { EditorView } from '@tiptap/pm/view'\nimport {\n ABILITY,\n type Ability,\n type AbilityRuleSource,\n resolveAbilities,\n ROOT_ABILITIES,\n TEXT_ABILITIES,\n TYPE_ABILITIES,\n} from './schema/abilities'\nimport type { AbilitiesOptions } from './extension-points'\n\nexport interface AbilitiesStorage {\n rules: Map<string, AbilityRuleSource>\n root: readonly Ability[]\n}\n\ndeclare module '@tiptap/core' {\n interface Storage {\n abilities: AbilitiesStorage\n }\n}\n\n/** Rules for StarterKit/extension nodes that cannot carry `NodeConfig.abilities` themselves. */\nexport const DEFAULT_ABILITY_RULES: Record<string, AbilityRuleSource> = {\n codeBlock: {\n revokes: [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.rule, ABILITY.blockContent, ABILITY.composite, ABILITY.transcript,\n ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiAutocomplete,\n ],\n },\n image: { revokes: TEXT_ABILITIES },\n horizontalRule: { revokes: TEXT_ABILITIES },\n}\n\n/**\n * Abilities at `pos`: chain = ancestors at depth 1…depth, plus `node` when given (an atom starting at `pos`).\n * A `ResolvedPos` addresses a document other than `editor.state.doc`, e.g. a transaction's doc inside plugin `apply`.\n */\nexport function abilitiesAt(editor: Editor, pos: number | ResolvedPos, node?: PMNode): ReadonlySet<Ability> {\n const $pos = typeof pos === 'number' ? editor.state.doc.resolve(pos) : pos\n const chain: PMNode[] = []\n for (let depth = 1; depth <= $pos.depth; depth++) chain.push($pos.node(depth))\n if (node) chain.push(node)\n const { rules, root } = editor.storage.abilities\n return resolveAbilities(rules, root, chain)\n}\n\n/** Abilities of the selection: the selected node, or what both ends of a range allow. */\nexport function abilitiesAtSelection(editor: Editor): ReadonlySet<Ability> {\n const { selection } = editor.state\n if (selection instanceof NodeSelection) return abilitiesAt(editor, selection.from, selection.node)\n const from = abilitiesAt(editor, selection.$from.pos)\n if (selection.empty) return from\n const to = abilitiesAt(editor, selection.$to.pos)\n return new Set([...from].filter(ability => to.has(ability)))\n}\n\nexport function hasAbility(editor: Editor, ability: Ability): boolean {\n return abilitiesAtSelection(editor).has(ability)\n}\n\n/** Only modifier shortcuts are gated; plain Enter/Tab/Backspace keep their structural behaviour everywhere. */\nconst GATED_KEY = /(^|-)(Mod|Alt)-/\n/** How much text before the caret input rules get to see (mirrors TipTap's input-rule plugin). */\nconst MAX_MATCH = 500\n\ninterface GatedInputRule {\n find: InputRule['find']\n ability: Ability\n}\n\n/** Same `this` the ExtensionManager hands to `addKeyboardShortcuts` / `addInputRules` (core dist, `get plugins()`). */\nfunction extensionContext(ext: AnyExtension, editor: Editor) {\n return {\n name: ext.name,\n options: ext.options,\n storage: editor.extensionStorage[ext.name as keyof typeof editor.extensionStorage],\n editor,\n type: editor.schema.nodes[ext.name] ?? editor.schema.marks[ext.name] ?? null,\n }\n}\n\nfunction gatedExtensions(editor: Editor): [AnyExtension, Ability][] {\n return editor.extensionManager.extensions.flatMap((ext): [AnyExtension, Ability][] => {\n const ability = TYPE_ABILITIES[ext.name]\n return ability ? [[ext, ability]] : []\n })\n}\n\nfunction buildKeyBindings(editor: Editor) {\n const abilitiesByKey = new Map<string, Ability[]>()\n for (const [ext, ability] of gatedExtensions(editor)) {\n const addKeyboardShortcuts = getExtensionField<(() => Record<string, KeyboardShortcutCommand>) | undefined>(\n ext, 'addKeyboardShortcuts', extensionContext(ext, editor),\n )\n for (const key of Object.keys(addKeyboardShortcuts?.() ?? {})) {\n if (!GATED_KEY.test(key)) continue\n abilitiesByKey.set(key, [...(abilitiesByKey.get(key) ?? []), ability])\n }\n }\n // Returning true swallows the key, so the type's own binding never runs.\n return Object.fromEntries([...abilitiesByKey].map(([key, abilities]) => [\n key,\n () => {\n const present = abilitiesAtSelection(editor)\n return abilities.some(ability => !present.has(ability))\n },\n ]))\n}\n\nfunction buildInputRules(editor: Editor): GatedInputRule[] {\n return gatedExtensions(editor).flatMap(([ext, ability]) => {\n const addInputRules = getExtensionField<(() => InputRule[]) | undefined>(ext, 'addInputRules', extensionContext(ext, editor))\n return (addInputRules?.() ?? []).map(rule => ({ find: rule.find, ability }))\n })\n}\n\nfunction matches(find: InputRule['find'], text: string): boolean {\n if (typeof find === 'function') return find(text) != null\n find.lastIndex = 0\n return find.exec(text) !== null\n}\n\n/**\n * Hierarchical abilities (see shared/editor/abilities.ts). Resolution reads `editor.storage.abilities`; the plugin\n * swallows `Mod-`/`Alt-` shortcuts and pre-empts input rules of node/mark types whose ability is missing at the caret.\n * Paste, drop and remote changes are not filtered.\n */\nexport const Abilities = Extension.create<AbilitiesOptions, AbilitiesStorage>({\n name: 'abilities',\n // Highest priority: its plugin's handleKeyDown / handleTextInput run before the gated types' keymaps and input rules.\n priority: 1000,\n\n addOptions() {\n return { root: ROOT_ABILITIES, rules: DEFAULT_ABILITY_RULES }\n },\n\n addStorage() {\n return { rules: new Map(), root: this.options.root }\n },\n\n onBeforeCreate() {\n const rules = new Map<string, AbilityRuleSource>()\n for (const ext of this.editor.extensionManager.extensions) {\n const rule = getExtensionField<AbilityRuleSource | undefined>(ext, 'abilities')\n if (rule) rules.set(ext.name, rule)\n }\n // Host/default rules replace a node's own rule as a whole.\n for (const [name, rule] of Object.entries(this.options.rules)) rules.set(name, rule)\n this.storage.rules = rules\n this.storage.root = this.options.root\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n // Built lazily on first use, once per plugin: needs the final extension list and schema.\n let handleKey: ((view: EditorView, event: KeyboardEvent) => boolean) | undefined\n let inputRules: GatedInputRule[] | undefined\n\n return [\n new Plugin({\n key: new PluginKey('abilities'),\n props: {\n handleKeyDown(view, event) {\n handleKey ??= keydownHandler(buildKeyBindings(editor))\n return handleKey(view, event)\n },\n handleTextInput(view, from, to, text) {\n inputRules ??= buildInputRules(editor)\n if (!inputRules.length) return false\n const $from = view.state.doc.resolve(from)\n if (!$from.parent.isTextblock) return false\n const textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, undefined, '\\ufffc') + text\n let present: ReadonlySet<Ability> | undefined\n for (const rule of inputRules) {\n present ??= abilitiesAt(editor, from)\n if (present.has(rule.ability) || !matches(rule.find, textBefore)) continue\n // Type the character literally; the rule's own plugin never sees this input.\n view.dispatch(view.state.tr.insertText(text, from, to))\n return true\n }\n return false\n },\n },\n }),\n ]\n },\n})\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/core/backend-capability.d.ts
|
|
2
2
|
/** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */
|
|
3
|
-
export declare const BACKEND_CAPABILITIES: readonly ["documents", "collaboration", "assets", "users", "tags", "settings", "themes", "ai", "comments"];
|
|
3
|
+
export declare const BACKEND_CAPABILITIES: readonly ["documents", "collaboration", "assets", "users", "tags", "settings", "themes", "ai", "comments", "images"];
|
|
4
4
|
/** What a backend can do. */
|
|
5
5
|
export type BackendCapability = typeof BACKEND_CAPABILITIES[number];
|
|
6
6
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-capability.js","names":[],"sources":["../../src/core/backend-capability.ts"],"sourcesContent":["/** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */\nexport const BACKEND_CAPABILITIES = ['documents', 'collaboration', 'assets', 'users', 'tags', 'settings', 'themes', 'ai', 'comments'] as const\n\n/** What a backend can do. */\nexport type BackendCapability = typeof BACKEND_CAPABILITIES[number]\n"],"mappings":";;AACA,MAAa,uBAAuB;CAAC;CAAa;CAAiB;CAAU;CAAS;CAAQ;CAAY;CAAU;CAAM;
|
|
1
|
+
{"version":3,"file":"backend-capability.js","names":[],"sources":["../../src/core/backend-capability.ts"],"sourcesContent":["/** Every capability of a backend, in the canonical order; the server's `EditorPorts` use the same names. Its own module so server code imports it without reaching the DOM-using core. */\nexport const BACKEND_CAPABILITIES = ['documents', 'collaboration', 'assets', 'users', 'tags', 'settings', 'themes', 'ai', 'comments', 'images'] as const\n\n/** What a backend can do. */\nexport type BackendCapability = typeof BACKEND_CAPABILITIES[number]\n"],"mappings":";;AACA,MAAa,uBAAuB;CAAC;CAAa;CAAiB;CAAU;CAAS;CAAQ;CAAY;CAAU;CAAM;CAAY;AAAQ"}
|
|
@@ -241,5 +241,30 @@ export interface CommentsModel extends CommentsActions {
|
|
|
241
241
|
refresh: () => void;
|
|
242
242
|
dispose: () => void;
|
|
243
243
|
}
|
|
244
|
+
/** A reference or a previous generation of the Bild-Studio (`images.generate` and `images.upload` answer one). `active` = goes into the next generation. */
|
|
245
|
+
export interface ImageStudioImage {
|
|
246
|
+
/** Host-opaque. */
|
|
247
|
+
id: string;
|
|
248
|
+
url: string;
|
|
249
|
+
title: string;
|
|
250
|
+
active: boolean;
|
|
251
|
+
}
|
|
252
|
+
/** A managed Prompt-Baustein (style, brand, …) of the host. */
|
|
253
|
+
export interface ImageStudioPromptBlock {
|
|
254
|
+
id: string;
|
|
255
|
+
label: string;
|
|
256
|
+
text: string;
|
|
257
|
+
}
|
|
258
|
+
/** Body of the image-generation route (`images.generate`): what the Bild-Studio sends for one chat turn. */
|
|
259
|
+
export interface ImageStudioGenerateBody {
|
|
260
|
+
/** The block's description of the image. */
|
|
261
|
+
description: string;
|
|
262
|
+
/** This turn's instruction; `''` generates from the description alone. */
|
|
263
|
+
instruction: string;
|
|
264
|
+
/** The active Prompt-Bausteine, by the host's ids. */
|
|
265
|
+
promptBlockIds: string[];
|
|
266
|
+
/** The URLs of the active references and generations that go into the generation as reference images. */
|
|
267
|
+
referenceUrls: string[];
|
|
268
|
+
}
|
|
244
269
|
//#endregion
|
|
245
270
|
//# sourceMappingURL=backend-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-types.js","names":[],"sources":["../../src/core/backend-types.ts"],"sourcesContent":["import type { ReadonlyStore } from './store'\nimport type { AuthorIdentity } from './feature'\n\n// The wire and model types that `EditorBackend` (`./backend`) names in its methods. They live in core, not in the\n// features they belong to, so the port does not depend on any feature or integration; each feature's public entry\n// (`@ecsplico/editor/mentions/schema`, `…/themes/schema`, `…/ai/protocol`, `…/comments/schema`, …) exports the names\n// it always had. Types only: DOM-free, importable from server code.\n\n/** A user as offered for `@` mentions (`GET /api/editor/users`); never carries credentials. */\nexport interface MentionUser {\n id: string\n username: string\n color: string\n}\n\n/** Profile as the client sees it; presets never expose their key. */\nexport interface ModelProfile { id: string, label: string, endpoint: string, model: string, source: 'preset' | 'user' }\n\n/** Model an AI request runs on: a server preset by id, or an OpenAI-compatible endpoint with its own key. */\nexport type ModelRef = { preset: string } | { endpoint: string, model: string, apiKey: string }\n\n/** A theme in the shared library; `builtin` themes are seeded by the host and cannot be deleted. */\nexport interface ThemeRecord {\n id: string\n name: string\n version?: string\n author?: string\n authorUrl?: string\n builtin: boolean\n}\n\n/** One transcript segment sent for cleanup. */\nexport interface TranscriptCleanupSegment {\n /** Block ID of the segment (`blockIdOf`). */\n id: string\n speaker: string\n /** The segment's inline content as Markdown (mentions as `@name`, tags as `#name`). */\n text: string\n}\n/** Body of the transcript-cleanup route. */\nexport interface TranscriptCleanupRequest { segments: TranscriptCleanupSegment[], model: ModelRef }\n/** Cleaned text (single-line inline Markdown) of the segment with block ID `id`. */\nexport interface TranscriptCleanupResult { id: string, text: string }\n/** `cleaned`: only segments whose text changed. `failed`: segments of chunks whose model answer was unusable (left as they are). */\nexport interface TranscriptCleanupResponse { cleaned: TranscriptCleanupResult[], failed: number }\n\n/** A CSL name: `family`/`given` for a person, `literal` for an institution or an unparsed name. */\nexport interface CslName { family?: string, given?: string, literal?: string }\nexport interface CslDate { 'date-parts': number[][] }\n\n/** The CSL item types the editor offers (select options, server-side schema). */\nexport const CSL_TYPES = ['article-journal', 'book', 'chapter', 'paper-conference', 'thesis', 'report', 'webpage', 'article', 'dataset'] as const\nexport type CslType = typeof CSL_TYPES[number]\n\n/** A CSL-JSON item without `id` and `title`: the Baustein's block id and title are authoritative for those and never stored in `meta.bib`. */\nexport interface CslItem {\n type: CslType\n author?: CslName[]\n editor?: CslName[]\n issued?: CslDate\n 'container-title'?: string\n volume?: string\n issue?: string\n page?: string\n publisher?: string\n 'publisher-place'?: string\n DOI?: string\n ISBN?: string\n URL?: string\n abstract?: string\n language?: string\n}\n\n/** What `ai.extract` makes of a stored asset; `auto` picks by the asset's MIME type. */\nexport type ExtractMethod = 'auto' | 'transcription' | 'pdf-text' | 'ocr' | 'description' | 'bibliography'\n/** Body of the extract route: the stored asset `documentId`/`name` and what to make of it. */\nexport interface ExtractRequest {\n documentId: string\n /** The asset to read; absent only for `bibliography` with a `doi` (the work is looked up by DOI alone). */\n name?: string\n method: ExtractMethod\n /** The model of `ocr`, `description` and model-based bibliography extraction. */\n model: ModelRef\n /** Also extract bibliographic data after `pdf-text`/`ocr`. */\n bibliography?: boolean\n /** DOI to look up for `bibliography`; absent = found in the first pages' text. */\n doi?: string\n /** PDF page cap (default 200). `ocr` reads at most 50 pages whatever this allows (one model call per page; `ocr-truncated` warns). */\n maxPages?: number\n}\nexport interface ExtractResponse {\n method: Exclude<ExtractMethod, 'auto'>\n /** The readable content as Markdown (empty for `bibliography`). */\n markdown: string\n /** Transcription only: the timed segments `markdown` was made of. */\n segments?: { start: number, end: number, text: string }[]\n /** When bibliographic data was requested or the method is `bibliography`; `title` is the work's title when found (never stored in `meta.bib`). */\n bib?: CslItem & { title?: string }\n /** Transcription only: the speech-to-text model that wrote it (`TranscriptionConfig.model`), which the client cannot know. */\n model?: string\n /** SHA-256 (hex) of the asset bytes. */\n inputHash: string\n /** SHA-256 (hex) of the stored raw output (`markdown` as the asset `output` holds it). The client does not compare it: it hashes the body it writes (its re-serialised Markdown) into `Derivation.outputHash` itself. */\n outputHash: string\n /** Asset name (same document) of the stored raw Markdown; `null` for `bibliography`. */\n output: string | null\n warnings: string[]\n}\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/** A text range in the editor document. */\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n"],"mappings":";;AAmDA,MAAa,YAAY;CAAC;CAAmB;CAAQ;CAAW;CAAoB;CAAU;CAAU;CAAW;CAAW;AAAS"}
|
|
1
|
+
{"version":3,"file":"backend-types.js","names":[],"sources":["../../src/core/backend-types.ts"],"sourcesContent":["import type { ReadonlyStore } from './store'\nimport type { AuthorIdentity } from './feature'\n\n// The wire and model types that `EditorBackend` (`./backend`) names in its methods. They live in core, not in the\n// features they belong to, so the port does not depend on any feature or integration; each feature's public entry\n// (`@ecsplico/editor/mentions/schema`, `…/themes/schema`, `…/ai/protocol`, `…/comments/schema`, …) exports the names\n// it always had. Types only: DOM-free, importable from server code.\n\n/** A user as offered for `@` mentions (`GET /api/editor/users`); never carries credentials. */\nexport interface MentionUser {\n id: string\n username: string\n color: string\n}\n\n/** Profile as the client sees it; presets never expose their key. */\nexport interface ModelProfile { id: string, label: string, endpoint: string, model: string, source: 'preset' | 'user' }\n\n/** Model an AI request runs on: a server preset by id, or an OpenAI-compatible endpoint with its own key. */\nexport type ModelRef = { preset: string } | { endpoint: string, model: string, apiKey: string }\n\n/** A theme in the shared library; `builtin` themes are seeded by the host and cannot be deleted. */\nexport interface ThemeRecord {\n id: string\n name: string\n version?: string\n author?: string\n authorUrl?: string\n builtin: boolean\n}\n\n/** One transcript segment sent for cleanup. */\nexport interface TranscriptCleanupSegment {\n /** Block ID of the segment (`blockIdOf`). */\n id: string\n speaker: string\n /** The segment's inline content as Markdown (mentions as `@name`, tags as `#name`). */\n text: string\n}\n/** Body of the transcript-cleanup route. */\nexport interface TranscriptCleanupRequest { segments: TranscriptCleanupSegment[], model: ModelRef }\n/** Cleaned text (single-line inline Markdown) of the segment with block ID `id`. */\nexport interface TranscriptCleanupResult { id: string, text: string }\n/** `cleaned`: only segments whose text changed. `failed`: segments of chunks whose model answer was unusable (left as they are). */\nexport interface TranscriptCleanupResponse { cleaned: TranscriptCleanupResult[], failed: number }\n\n/** A CSL name: `family`/`given` for a person, `literal` for an institution or an unparsed name. */\nexport interface CslName { family?: string, given?: string, literal?: string }\nexport interface CslDate { 'date-parts': number[][] }\n\n/** The CSL item types the editor offers (select options, server-side schema). */\nexport const CSL_TYPES = ['article-journal', 'book', 'chapter', 'paper-conference', 'thesis', 'report', 'webpage', 'article', 'dataset'] as const\nexport type CslType = typeof CSL_TYPES[number]\n\n/** A CSL-JSON item without `id` and `title`: the Baustein's block id and title are authoritative for those and never stored in `meta.bib`. */\nexport interface CslItem {\n type: CslType\n author?: CslName[]\n editor?: CslName[]\n issued?: CslDate\n 'container-title'?: string\n volume?: string\n issue?: string\n page?: string\n publisher?: string\n 'publisher-place'?: string\n DOI?: string\n ISBN?: string\n URL?: string\n abstract?: string\n language?: string\n}\n\n/** What `ai.extract` makes of a stored asset; `auto` picks by the asset's MIME type. */\nexport type ExtractMethod = 'auto' | 'transcription' | 'pdf-text' | 'ocr' | 'description' | 'bibliography'\n/** Body of the extract route: the stored asset `documentId`/`name` and what to make of it. */\nexport interface ExtractRequest {\n documentId: string\n /** The asset to read; absent only for `bibliography` with a `doi` (the work is looked up by DOI alone). */\n name?: string\n method: ExtractMethod\n /** The model of `ocr`, `description` and model-based bibliography extraction. */\n model: ModelRef\n /** Also extract bibliographic data after `pdf-text`/`ocr`. */\n bibliography?: boolean\n /** DOI to look up for `bibliography`; absent = found in the first pages' text. */\n doi?: string\n /** PDF page cap (default 200). `ocr` reads at most 50 pages whatever this allows (one model call per page; `ocr-truncated` warns). */\n maxPages?: number\n}\nexport interface ExtractResponse {\n method: Exclude<ExtractMethod, 'auto'>\n /** The readable content as Markdown (empty for `bibliography`). */\n markdown: string\n /** Transcription only: the timed segments `markdown` was made of. */\n segments?: { start: number, end: number, text: string }[]\n /** When bibliographic data was requested or the method is `bibliography`; `title` is the work's title when found (never stored in `meta.bib`). */\n bib?: CslItem & { title?: string }\n /** Transcription only: the speech-to-text model that wrote it (`TranscriptionConfig.model`), which the client cannot know. */\n model?: string\n /** SHA-256 (hex) of the asset bytes. */\n inputHash: string\n /** SHA-256 (hex) of the stored raw output (`markdown` as the asset `output` holds it). The client does not compare it: it hashes the body it writes (its re-serialised Markdown) into `Derivation.outputHash` itself. */\n outputHash: string\n /** Asset name (same document) of the stored raw Markdown; `null` for `bibliography`. */\n output: string | null\n warnings: string[]\n}\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/** A text range in the editor document. */\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A reference or a previous generation of the Bild-Studio (`images.generate` and `images.upload` answer one). `active` = goes into the next generation. */\nexport interface ImageStudioImage {\n /** Host-opaque. */\n id: string\n url: string\n title: string\n active: boolean\n}\n\n/** A managed Prompt-Baustein (style, brand, …) of the host. */\nexport interface ImageStudioPromptBlock {\n id: string\n label: string\n text: string\n}\n\n/** Body of the image-generation route (`images.generate`): what the Bild-Studio sends for one chat turn. */\nexport interface ImageStudioGenerateBody {\n /** The block's description of the image. */\n description: string\n /** This turn's instruction; `''` generates from the description alone. */\n instruction: string\n /** The active Prompt-Bausteine, by the host's ids. */\n promptBlockIds: string[]\n /** The URLs of the active references and generations that go into the generation as reference images. */\n referenceUrls: string[]\n}\n"],"mappings":";;AAmDA,MAAa,YAAY;CAAC;CAAmB;CAAQ;CAAW;CAAoB;CAAU;CAAU;CAAW;CAAW;AAAS"}
|
package/dist/core/backend.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ModelSlot } from "../settings/values.js";
|
|
|
2
2
|
import { EditorSettings } from "../settings/index.js";
|
|
3
3
|
import { BackendCapability } from "./backend-capability.js";
|
|
4
4
|
import { AuthorIdentity } from "./feature.js";
|
|
5
|
-
import { CommentsModel, ExtractRequest, ExtractResponse, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from "./backend-types.js";
|
|
5
|
+
import { CommentsModel, ExtractRequest, ExtractResponse, ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from "./backend-types.js";
|
|
6
6
|
import { Editor, JSONContent } from "@tiptap/core";
|
|
7
7
|
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
8
8
|
import * as Y from "yjs";
|
|
@@ -100,6 +100,14 @@ export interface EditorBackend {
|
|
|
100
100
|
/** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */
|
|
101
101
|
extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>;
|
|
102
102
|
};
|
|
103
|
+
images?: {
|
|
104
|
+
/** The Prompt-Bausteine (style, brand, …) the host manages, in the order it lists them. */
|
|
105
|
+
promptBlocks: () => Promise<ImageStudioPromptBlock[]>;
|
|
106
|
+
/** Generates one image from the description, the instruction, the active Prompt-Bausteine and the reference images; resolves with the stored image (`active: false`). */
|
|
107
|
+
generate: (body: ImageStudioGenerateBody, signal?: AbortSignal) => Promise<ImageStudioImage>;
|
|
108
|
+
/** Stores `file` as a reference image for document `documentId`; resolves with the stored image (`active: true`). */
|
|
109
|
+
upload: (documentId: string, file: File) => Promise<ImageStudioImage>;
|
|
110
|
+
};
|
|
103
111
|
/** The comments model of document `documentId`. */
|
|
104
112
|
comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel;
|
|
105
113
|
}
|
package/dist/core/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","names":[],"sources":["../../src/core/backend.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type * as Y from 'yjs'\nimport type { EditorSettings, ModelSlot } from '../settings'\nimport type { BackendCapability } from './backend-capability'\nimport type { CommentsModel, ExtractRequest, ExtractResponse, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from './backend-types'\nimport type { AuthorIdentity } from './feature'\n\n// The client side of the data layer: everything the editor and its features ask of the host's backend (documents,\n// uploads, mention lookups, settings, themes, AI, collaboration, comments) is one method of `EditorBackend`. Two\n// adapters implement it: HTTP (`@ecsplico/editor/adapters/http`, the `/api/editor` and `/api/ai` routes the Nuxt module\n// and `installEditorServer` serve) and the host's own. A capability the backend does not offer is `undefined`.\n\n/** What a backend can do; the server's `EditorPorts` use the same names. */\nexport type { BackendCapability }\n\n/** One server-sent event of an AI stream (`delta` `{ text }`, `ops` the proposed edit operations, `done`, `error` `{ message }`). `data` is the parsed JSON payload (`{}` without one). */\nexport interface AiStreamEvent {\n event: 'delta' | 'ops' | 'done' | 'error'\n data: unknown\n}\n\n/** The token a collaboration server's `authenticate` receives: a string, or a function (also async) that returns it on each connect. */\nexport type CollaborationToken = string | (() => string) | (() => Promise<string>)\n\n/** A refused or failed backend request; `status` is the HTTP status (or its equivalent) and `message` the server's, when it sent one. */\nexport class BackendError extends Error {\n constructor(message: string, readonly status: number, options?: ErrorOptions) {\n super(message, options)\n this.name = 'BackendError'\n }\n}\n\n/** What a backend needs from the host to give a comments model its anchors and its author. */\nexport interface BackendCommentsOptions {\n /** Who writes: the author of new comments; `null` while nobody is signed in. */\n user: () => AuthorIdentity | null\n /** Whether the user may compose and change comments now. */\n canComment: () => boolean\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Called with every failed request. */\n onError?: (error: Error) => void\n}\n\nexport interface EditorBackend {\n /** Names the backend in error messages (`'http'`, `'memory'`, …). */\n readonly name: string\n /**\n * Settles once the backend knows which capabilities it really has (the HTTP adapter's `probe`, which drops what the\n * server lacks). `createEditorContext` awaits it before it reports `ready`, so a composition that prunes by capability\n * (`composeEditor(…, { backend })`) should run after it too. Never rejects; absent: usable at once.\n */\n readonly ready?: Promise<void>\n documents?: {\n /** The stored content; `null` when the document does not exist (yet). */\n load: (id: string) => Promise<JSONContent | null>\n save: (id: string, content: JSONContent) => Promise<void>\n }\n collaboration?: {\n /** Opens the live session of document `id`; `dispose()` closes the provider and the Y.Doc. */\n connect: (id: string, token?: CollaborationToken) => { ydoc: Y.Doc, provider: HocuspocusProvider, dispose: () => void }\n }\n assets?: {\n /** Stores `file` for document `documentId`; resolves with the URL it is addressed by and, when the backend hashes uploads, the SHA-256 (hex) of its bytes. */\n upload: (documentId: string, file: File) => Promise<{ url: string, sha256?: string }>\n }\n users?: {\n search: (query: string, signal?: AbortSignal) => Promise<MentionUser[]>\n }\n tags?: {\n search: (query: string, signal?: AbortSignal) => Promise<string[]>\n }\n settings?: {\n /** The account's settings; `null` when it has none stored yet. */\n load: () => Promise<EditorSettings | null>\n save: (settings: EditorSettings) => Promise<void>\n }\n themes?: {\n list: () => Promise<ThemeRecord[]>\n /** The stylesheet of theme `id` (unscoped). */\n css: (id: string) => Promise<string>\n /** Adds an Obsidian theme to the shared library: the stylesheet and, optionally, its `manifest.json`; the backend derives name and id. */\n store: (files: { css: File, manifest?: File }) => Promise<ThemeRecord>\n /** Deletes an uploaded theme; rejects for builtins and unknown ids. */\n remove: (id: string) => Promise<boolean>\n }\n ai?: {\n models: () => Promise<{ presets: ModelProfile[], slots: Record<ModelSlot, string> }>\n complete: (body: { before: string, model: ModelRef }, signal: AbortSignal) => Promise<{ text: string }>\n /** Streams the SSE events of `generate` or `chat`; throws `BackendError` (with `status`) on a non-2xx answer before the stream starts. */\n stream: (path: 'generate' | 'chat', body: object, signal: AbortSignal) => AsyncIterable<AiStreamEvent>\n /** Speaker-segment cleanup of a transcript (one JSON answer, not a stream). */\n transcriptCleanup: (body: TranscriptCleanupRequest, signal: AbortSignal) => Promise<TranscriptCleanupResponse>\n /** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */\n extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>\n }\n /** The comments model of document `documentId`. */\n comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel\n}\n"],"mappings":";;AA0BA,IAAa,eAAb,cAAkC,MAAM;CACA;CAAtC,YAAY,SAAiB,QAAyB,SAAwB;EAC5E,MAAM,SAAS,OAAO;EADc,KAAA,SAAA;EAEpC,KAAK,OAAO;CACd;AACF"}
|
|
1
|
+
{"version":3,"file":"backend.js","names":[],"sources":["../../src/core/backend.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type * as Y from 'yjs'\nimport type { EditorSettings, ModelSlot } from '../settings'\nimport type { BackendCapability } from './backend-capability'\nimport type { CommentsModel, ExtractRequest, ExtractResponse, ImageStudioGenerateBody, ImageStudioImage, ImageStudioPromptBlock, MentionUser, ModelProfile, ModelRef, ThemeRecord, TranscriptCleanupRequest, TranscriptCleanupResponse } from './backend-types'\nimport type { AuthorIdentity } from './feature'\n\n// The client side of the data layer: everything the editor and its features ask of the host's backend (documents,\n// uploads, mention lookups, settings, themes, AI, collaboration, comments) is one method of `EditorBackend`. Two\n// adapters implement it: HTTP (`@ecsplico/editor/adapters/http`, the `/api/editor` and `/api/ai` routes the Nuxt module\n// and `installEditorServer` serve) and the host's own. A capability the backend does not offer is `undefined`.\n\n/** What a backend can do; the server's `EditorPorts` use the same names. */\nexport type { BackendCapability }\n\n/** One server-sent event of an AI stream (`delta` `{ text }`, `ops` the proposed edit operations, `done`, `error` `{ message }`). `data` is the parsed JSON payload (`{}` without one). */\nexport interface AiStreamEvent {\n event: 'delta' | 'ops' | 'done' | 'error'\n data: unknown\n}\n\n/** The token a collaboration server's `authenticate` receives: a string, or a function (also async) that returns it on each connect. */\nexport type CollaborationToken = string | (() => string) | (() => Promise<string>)\n\n/** A refused or failed backend request; `status` is the HTTP status (or its equivalent) and `message` the server's, when it sent one. */\nexport class BackendError extends Error {\n constructor(message: string, readonly status: number, options?: ErrorOptions) {\n super(message, options)\n this.name = 'BackendError'\n }\n}\n\n/** What a backend needs from the host to give a comments model its anchors and its author. */\nexport interface BackendCommentsOptions {\n /** Who writes: the author of new comments; `null` while nobody is signed in. */\n user: () => AuthorIdentity | null\n /** Whether the user may compose and change comments now. */\n canComment: () => boolean\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Called with every failed request. */\n onError?: (error: Error) => void\n}\n\nexport interface EditorBackend {\n /** Names the backend in error messages (`'http'`, `'memory'`, …). */\n readonly name: string\n /**\n * Settles once the backend knows which capabilities it really has (the HTTP adapter's `probe`, which drops what the\n * server lacks). `createEditorContext` awaits it before it reports `ready`, so a composition that prunes by capability\n * (`composeEditor(…, { backend })`) should run after it too. Never rejects; absent: usable at once.\n */\n readonly ready?: Promise<void>\n documents?: {\n /** The stored content; `null` when the document does not exist (yet). */\n load: (id: string) => Promise<JSONContent | null>\n save: (id: string, content: JSONContent) => Promise<void>\n }\n collaboration?: {\n /** Opens the live session of document `id`; `dispose()` closes the provider and the Y.Doc. */\n connect: (id: string, token?: CollaborationToken) => { ydoc: Y.Doc, provider: HocuspocusProvider, dispose: () => void }\n }\n assets?: {\n /** Stores `file` for document `documentId`; resolves with the URL it is addressed by and, when the backend hashes uploads, the SHA-256 (hex) of its bytes. */\n upload: (documentId: string, file: File) => Promise<{ url: string, sha256?: string }>\n }\n users?: {\n search: (query: string, signal?: AbortSignal) => Promise<MentionUser[]>\n }\n tags?: {\n search: (query: string, signal?: AbortSignal) => Promise<string[]>\n }\n settings?: {\n /** The account's settings; `null` when it has none stored yet. */\n load: () => Promise<EditorSettings | null>\n save: (settings: EditorSettings) => Promise<void>\n }\n themes?: {\n list: () => Promise<ThemeRecord[]>\n /** The stylesheet of theme `id` (unscoped). */\n css: (id: string) => Promise<string>\n /** Adds an Obsidian theme to the shared library: the stylesheet and, optionally, its `manifest.json`; the backend derives name and id. */\n store: (files: { css: File, manifest?: File }) => Promise<ThemeRecord>\n /** Deletes an uploaded theme; rejects for builtins and unknown ids. */\n remove: (id: string) => Promise<boolean>\n }\n ai?: {\n models: () => Promise<{ presets: ModelProfile[], slots: Record<ModelSlot, string> }>\n complete: (body: { before: string, model: ModelRef }, signal: AbortSignal) => Promise<{ text: string }>\n /** Streams the SSE events of `generate` or `chat`; throws `BackendError` (with `status`) on a non-2xx answer before the stream starts. */\n stream: (path: 'generate' | 'chat', body: object, signal: AbortSignal) => AsyncIterable<AiStreamEvent>\n /** Speaker-segment cleanup of a transcript (one JSON answer, not a stream). */\n transcriptCleanup: (body: TranscriptCleanupRequest, signal: AbortSignal) => Promise<TranscriptCleanupResponse>\n /** Turns a stored asset into readable content: transcription (with segment timestamps), PDF text or OCR, an image description, bibliographic data. */\n extract: (body: ExtractRequest, signal: AbortSignal) => Promise<ExtractResponse>\n }\n images?: {\n /** The Prompt-Bausteine (style, brand, …) the host manages, in the order it lists them. */\n promptBlocks: () => Promise<ImageStudioPromptBlock[]>\n /** Generates one image from the description, the instruction, the active Prompt-Bausteine and the reference images; resolves with the stored image (`active: false`). */\n generate: (body: ImageStudioGenerateBody, signal?: AbortSignal) => Promise<ImageStudioImage>\n /** Stores `file` as a reference image for document `documentId`; resolves with the stored image (`active: true`). */\n upload: (documentId: string, file: File) => Promise<ImageStudioImage>\n }\n /** The comments model of document `documentId`. */\n comments?: (documentId: string, options: BackendCommentsOptions) => CommentsModel\n}\n"],"mappings":";;AA0BA,IAAa,eAAb,cAAkC,MAAM;CACA;CAAtC,YAAY,SAAiB,QAAyB,SAAwB;EAC5E,MAAM,SAAS,OAAO;EADc,KAAA,SAAA;EAEpC,KAAK,OAAO;CACd;AACF"}
|
|
@@ -186,6 +186,23 @@ function createFetchBackend(options = {}) {
|
|
|
186
186
|
body,
|
|
187
187
|
signal
|
|
188
188
|
})
|
|
189
|
+
},
|
|
190
|
+
images: {
|
|
191
|
+
promptBlocks: () => json(editor("/images/prompt-blocks")),
|
|
192
|
+
generate: (body, signal) => json(editor("/images/generate"), {
|
|
193
|
+
method: "POST",
|
|
194
|
+
body,
|
|
195
|
+
signal
|
|
196
|
+
}),
|
|
197
|
+
upload(documentId, file) {
|
|
198
|
+
const body = new FormData();
|
|
199
|
+
body.append("documentId", documentId);
|
|
200
|
+
body.append("file", file);
|
|
201
|
+
return json(editor("/images/upload"), {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body
|
|
204
|
+
});
|
|
205
|
+
}
|
|
189
206
|
}
|
|
190
207
|
};
|
|
191
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-backend.js","names":[],"sources":["../../src/core/http-backend.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { $Fetch, FetchOptions } from 'ofetch'\nimport type { EditorSettings } from '../settings'\nimport { AI_API, documentUrl, EDITOR_API } from './api-routes'\nimport { type AiStreamEvent, BackendError, type EditorBackend } from './backend'\nimport type { ExtractResponse, MentionUser } from './backend-types'\n\n// The request-response half of the HTTP backend: documents, assets, users, tags, settings, themes and AI over the\n// `/api/editor` and `/api/ai` routes. `createHttpBackend` (`@ecsplico/editor/adapters/http`) adds the two capabilities that\n// need Yjs and the comments client; `createEditorContext` defaults to this half so no editor bundle carries those.\n// Requests go out through the platform `fetch`; a `$Fetch` (ofetch, Nuxt's `$fetch`) is used only when the host hands one\n// in, so no bundle pulls `ofetch` in unasked (the type import above is erased).\n\nexport interface FetchBackendOptions {\n /** HTTP client (default: the platform `fetch`); Nuxt hosts pass `useNuxtApp().$fetch` (it resolves relative paths while rendering on the server). */\n fetch?: $Fetch\n /** Prefix of every path (`https://api.example.com`); default `''` (same origin). */\n base?: string\n}\n\n/** One request of the transport: `body` is sent as it is when it is a string or `FormData`, else JSON-serialised; `query` becomes the query string. */\nexport interface HttpRequest {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n query?: Record<string, string>\n signal?: AbortSignal | undefined\n}\n\nexport interface HttpTransport {\n /** Sends the request and resolves with the response whatever its status; rejects only on a network failure or abort. */\n send: (url: string, request?: HttpRequest) => Promise<Response>\n /** `response` itself when it is 2xx; otherwise rejects with a `BackendError` (the server's `statusMessage`/`message`, else `HTTP <status>`). */\n check: (response: Response) => Promise<Response>\n /** `send` then `check`. */\n request: (url: string, request?: HttpRequest) => Promise<Response>\n}\n\nfunction platformSend(url: string, { method, headers: extra, body, query, signal }: HttpRequest = {}): Promise<Response> {\n const headers = new Headers(extra)\n let payload: BodyInit | undefined\n if (typeof body === 'string' || (typeof FormData !== 'undefined' && body instanceof FormData)) payload = body\n else if (body !== undefined) {\n headers.set('content-type', 'application/json')\n payload = JSON.stringify(body)\n }\n const target = query ? `${url}?${new URLSearchParams(query)}` : url\n return globalThis.fetch(target, { ...(method && { method }), headers, ...(payload !== undefined && { body: payload }), ...(signal && { signal }) })\n}\n\n/** The request through `fetcher.raw`: the response comes back unread (`stream`), a non-2xx one is a result, not an exception. */\nfunction fetcherSend(fetcher: $Fetch) {\n return (url: string, { method, headers, body, query, signal }: HttpRequest = {}): Promise<Response> => {\n const options: FetchOptions<'stream'> = { responseType: 'stream', ignoreResponseError: true }\n if (method) options.method = method\n if (headers) options.headers = headers\n if (body !== undefined) options.body = body as Record<string, unknown>\n if (query) options.query = query\n if (signal) options.signal = signal\n return fetcher.raw(url, options)\n }\n}\n\n/** The transport of `fetch` (default: the platform `fetch`). */\nexport function createTransport(fetcher?: $Fetch): HttpTransport {\n const send = fetcher ? fetcherSend(fetcher) : platformSend\n async function check(response: Response) {\n if (response.ok) return response\n let message = `HTTP ${response.status}`\n try {\n const json = await response.json() as { statusMessage?: string, message?: string }\n message = json.statusMessage || json.message || message\n }\n catch { /* non-JSON error body */ }\n throw new BackendError(message, response.status)\n }\n return { send, check, request: async (url, request) => check(await send(url, request)) }\n}\n\n/** Drops a response whose body nobody reads, so the connection is released. */\nconst discard = (response: Response) => response.body?.cancel().catch(() => undefined)\n\n/** The event of an SSE block (`event: x\\ndata: {json}`); `data` is `{}` without a payload or with a malformed one. */\nfunction streamEvent(raw: string): AiStreamEvent {\n let event = 'message'\n let data = ''\n for (const line of raw.split('\\n')) {\n if (line.startsWith('event:')) event = line.slice(6).trim()\n else if (line.startsWith('data:')) data += line.slice(5).trimStart()\n }\n let payload: unknown = {}\n if (data) {\n try {\n payload = JSON.parse(data)\n }\n catch {\n payload = {}\n }\n }\n return { event: event as AiStreamEvent['event'], data: payload }\n}\n\n/** The editor's HTTP routes as an `EditorBackend` without `collaboration` and `comments`. */\nexport function createFetchBackend(options: FetchBackendOptions = {}): EditorBackend {\n const base = options.base ?? ''\n const { request, send, check } = createTransport(options.fetch)\n const editor = (path: string) => `${base}${EDITOR_API}${path}`\n const ai = (path: string) => `${base}${AI_API}${path}`\n /** The JSON answer of a request that must succeed. */\n const json = async <T>(url: string, init?: HttpRequest) => (await request(url, init)).json() as Promise<T>\n /** A request that must succeed and whose answer is of no interest. */\n const sent = async (url: string, init?: HttpRequest) => { await discard(await request(url, init)) }\n\n return {\n name: 'http',\n documents: {\n async load(id) {\n const response = await send(`${base}${documentUrl(id)}`)\n if (response.status === 404) {\n await discard(response)\n return null\n }\n return (await check(response)).json() as Promise<JSONContent>\n },\n save: (id, content) => sent(`${base}${documentUrl(id)}`, { method: 'PUT', body: content }),\n },\n assets: {\n upload(documentId, file) {\n const body = new FormData()\n body.append('file', file)\n return json<{ url: string, sha256?: string }>(editor(`/assets/${documentId}`), { method: 'POST', body })\n },\n },\n users: {\n search: (query, signal) => json<MentionUser[]>(editor('/users'), { query: { q: query }, signal }),\n },\n tags: {\n search: (query, signal) => json<string[]>(editor('/tags'), { query: { q: query }, signal }),\n },\n settings: {\n async load() {\n const response = await request(editor('/settings'))\n // 204: the host keeps no settings for this account.\n return response.status === 200 ? await response.json() as EditorSettings | null : null\n },\n save: settings => sent(editor('/settings'), { method: 'PUT', body: settings }),\n },\n themes: {\n list: () => json(editor('/themes')),\n css: async id => (await request(editor(`/themes/${encodeURIComponent(id)}`))).text(),\n store({ css, manifest }) {\n const body = new FormData()\n body.append('css', css)\n if (manifest) body.append('manifest', manifest)\n return json(editor('/themes'), { method: 'POST', body })\n },\n async remove(id) {\n await sent(editor(`/themes/${encodeURIComponent(id)}`), { method: 'DELETE' })\n return true\n },\n },\n ai: {\n models: () => json(ai('/models')),\n complete: (body, signal) => json<{ text: string }>(ai('/complete'), { method: 'POST', body, signal }),\n async* stream(path, body, signal) {\n const response = await request(ai(`/${path}`), { method: 'POST', body, signal })\n if (!response.body) throw new BackendError(`HTTP ${response.status}`, response.status)\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n')\n let index = buffer.indexOf('\\n\\n')\n while (index !== -1) {\n const event = streamEvent(buffer.slice(0, index))\n yield event\n if (event.event === 'done' || event.event === 'error') return\n buffer = buffer.slice(index + 2)\n index = buffer.indexOf('\\n\\n')\n }\n }\n if (buffer.trim()) yield streamEvent(buffer)\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n },\n transcriptCleanup: (body, signal) => json(ai('/transcript-cleanup'), { method: 'POST', body, signal }),\n extract: (body, signal) => json<ExtractResponse>(ai('/extract'), { method: 'POST', body, signal }),\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http-backend.js","names":[],"sources":["../../src/core/http-backend.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport type { $Fetch, FetchOptions } from 'ofetch'\nimport type { EditorSettings } from '../settings'\nimport { AI_API, documentUrl, EDITOR_API } from './api-routes'\nimport { type AiStreamEvent, BackendError, type EditorBackend } from './backend'\nimport type { ExtractResponse, MentionUser } from './backend-types'\n\n// The request-response half of the HTTP backend: documents, assets, users, tags, settings, themes, AI and images over the\n// `/api/editor` and `/api/ai` routes. `createHttpBackend` (`@ecsplico/editor/adapters/http`) adds the two capabilities that\n// need Yjs and the comments client; `createEditorContext` defaults to this half so no editor bundle carries those.\n// Requests go out through the platform `fetch`; a `$Fetch` (ofetch, Nuxt's `$fetch`) is used only when the host hands one\n// in, so no bundle pulls `ofetch` in unasked (the type import above is erased).\n\nexport interface FetchBackendOptions {\n /** HTTP client (default: the platform `fetch`); Nuxt hosts pass `useNuxtApp().$fetch` (it resolves relative paths while rendering on the server). */\n fetch?: $Fetch\n /** Prefix of every path (`https://api.example.com`); default `''` (same origin). */\n base?: string\n}\n\n/** One request of the transport: `body` is sent as it is when it is a string or `FormData`, else JSON-serialised; `query` becomes the query string. */\nexport interface HttpRequest {\n method?: string\n headers?: Record<string, string>\n body?: unknown\n query?: Record<string, string>\n signal?: AbortSignal | undefined\n}\n\nexport interface HttpTransport {\n /** Sends the request and resolves with the response whatever its status; rejects only on a network failure or abort. */\n send: (url: string, request?: HttpRequest) => Promise<Response>\n /** `response` itself when it is 2xx; otherwise rejects with a `BackendError` (the server's `statusMessage`/`message`, else `HTTP <status>`). */\n check: (response: Response) => Promise<Response>\n /** `send` then `check`. */\n request: (url: string, request?: HttpRequest) => Promise<Response>\n}\n\nfunction platformSend(url: string, { method, headers: extra, body, query, signal }: HttpRequest = {}): Promise<Response> {\n const headers = new Headers(extra)\n let payload: BodyInit | undefined\n if (typeof body === 'string' || (typeof FormData !== 'undefined' && body instanceof FormData)) payload = body\n else if (body !== undefined) {\n headers.set('content-type', 'application/json')\n payload = JSON.stringify(body)\n }\n const target = query ? `${url}?${new URLSearchParams(query)}` : url\n return globalThis.fetch(target, { ...(method && { method }), headers, ...(payload !== undefined && { body: payload }), ...(signal && { signal }) })\n}\n\n/** The request through `fetcher.raw`: the response comes back unread (`stream`), a non-2xx one is a result, not an exception. */\nfunction fetcherSend(fetcher: $Fetch) {\n return (url: string, { method, headers, body, query, signal }: HttpRequest = {}): Promise<Response> => {\n const options: FetchOptions<'stream'> = { responseType: 'stream', ignoreResponseError: true }\n if (method) options.method = method\n if (headers) options.headers = headers\n if (body !== undefined) options.body = body as Record<string, unknown>\n if (query) options.query = query\n if (signal) options.signal = signal\n return fetcher.raw(url, options)\n }\n}\n\n/** The transport of `fetch` (default: the platform `fetch`). */\nexport function createTransport(fetcher?: $Fetch): HttpTransport {\n const send = fetcher ? fetcherSend(fetcher) : platformSend\n async function check(response: Response) {\n if (response.ok) return response\n let message = `HTTP ${response.status}`\n try {\n const json = await response.json() as { statusMessage?: string, message?: string }\n message = json.statusMessage || json.message || message\n }\n catch { /* non-JSON error body */ }\n throw new BackendError(message, response.status)\n }\n return { send, check, request: async (url, request) => check(await send(url, request)) }\n}\n\n/** Drops a response whose body nobody reads, so the connection is released. */\nconst discard = (response: Response) => response.body?.cancel().catch(() => undefined)\n\n/** The event of an SSE block (`event: x\\ndata: {json}`); `data` is `{}` without a payload or with a malformed one. */\nfunction streamEvent(raw: string): AiStreamEvent {\n let event = 'message'\n let data = ''\n for (const line of raw.split('\\n')) {\n if (line.startsWith('event:')) event = line.slice(6).trim()\n else if (line.startsWith('data:')) data += line.slice(5).trimStart()\n }\n let payload: unknown = {}\n if (data) {\n try {\n payload = JSON.parse(data)\n }\n catch {\n payload = {}\n }\n }\n return { event: event as AiStreamEvent['event'], data: payload }\n}\n\n/** The editor's HTTP routes as an `EditorBackend` without `collaboration` and `comments`. */\nexport function createFetchBackend(options: FetchBackendOptions = {}): EditorBackend {\n const base = options.base ?? ''\n const { request, send, check } = createTransport(options.fetch)\n const editor = (path: string) => `${base}${EDITOR_API}${path}`\n const ai = (path: string) => `${base}${AI_API}${path}`\n /** The JSON answer of a request that must succeed. */\n const json = async <T>(url: string, init?: HttpRequest) => (await request(url, init)).json() as Promise<T>\n /** A request that must succeed and whose answer is of no interest. */\n const sent = async (url: string, init?: HttpRequest) => { await discard(await request(url, init)) }\n\n return {\n name: 'http',\n documents: {\n async load(id) {\n const response = await send(`${base}${documentUrl(id)}`)\n if (response.status === 404) {\n await discard(response)\n return null\n }\n return (await check(response)).json() as Promise<JSONContent>\n },\n save: (id, content) => sent(`${base}${documentUrl(id)}`, { method: 'PUT', body: content }),\n },\n assets: {\n upload(documentId, file) {\n const body = new FormData()\n body.append('file', file)\n return json<{ url: string, sha256?: string }>(editor(`/assets/${documentId}`), { method: 'POST', body })\n },\n },\n users: {\n search: (query, signal) => json<MentionUser[]>(editor('/users'), { query: { q: query }, signal }),\n },\n tags: {\n search: (query, signal) => json<string[]>(editor('/tags'), { query: { q: query }, signal }),\n },\n settings: {\n async load() {\n const response = await request(editor('/settings'))\n // 204: the host keeps no settings for this account.\n return response.status === 200 ? await response.json() as EditorSettings | null : null\n },\n save: settings => sent(editor('/settings'), { method: 'PUT', body: settings }),\n },\n themes: {\n list: () => json(editor('/themes')),\n css: async id => (await request(editor(`/themes/${encodeURIComponent(id)}`))).text(),\n store({ css, manifest }) {\n const body = new FormData()\n body.append('css', css)\n if (manifest) body.append('manifest', manifest)\n return json(editor('/themes'), { method: 'POST', body })\n },\n async remove(id) {\n await sent(editor(`/themes/${encodeURIComponent(id)}`), { method: 'DELETE' })\n return true\n },\n },\n ai: {\n models: () => json(ai('/models')),\n complete: (body, signal) => json<{ text: string }>(ai('/complete'), { method: 'POST', body, signal }),\n async* stream(path, body, signal) {\n const response = await request(ai(`/${path}`), { method: 'POST', body, signal })\n if (!response.body) throw new BackendError(`HTTP ${response.status}`, response.status)\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n try {\n while (true) {\n const { value, done } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true }).replace(/\\r\\n/g, '\\n')\n let index = buffer.indexOf('\\n\\n')\n while (index !== -1) {\n const event = streamEvent(buffer.slice(0, index))\n yield event\n if (event.event === 'done' || event.event === 'error') return\n buffer = buffer.slice(index + 2)\n index = buffer.indexOf('\\n\\n')\n }\n }\n if (buffer.trim()) yield streamEvent(buffer)\n }\n finally {\n await reader.cancel().catch(() => undefined)\n }\n },\n transcriptCleanup: (body, signal) => json(ai('/transcript-cleanup'), { method: 'POST', body, signal }),\n extract: (body, signal) => json<ExtractResponse>(ai('/extract'), { method: 'POST', body, signal }),\n },\n images: {\n promptBlocks: () => json(editor('/images/prompt-blocks')),\n generate: (body, signal) => json(editor('/images/generate'), { method: 'POST', body, signal }),\n upload(documentId, file) {\n const body = new FormData()\n body.append('documentId', documentId)\n body.append('file', file)\n return json(editor('/images/upload'), { method: 'POST', body })\n },\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@ export declare const ABILITY: {
|
|
|
12
12
|
readonly composite: "format.composite";
|
|
13
13
|
readonly transcript: "transcript.segment";
|
|
14
14
|
readonly media: "insert.media";
|
|
15
|
+
readonly imageStudio: "insert.imageStudio";
|
|
15
16
|
readonly record: "insert.record";
|
|
16
17
|
readonly mention: "insert.mention";
|
|
17
18
|
readonly reference: "insert.reference";
|
|
@@ -11,6 +11,7 @@ const ABILITY = {
|
|
|
11
11
|
composite: "format.composite",
|
|
12
12
|
transcript: "transcript.segment",
|
|
13
13
|
media: "insert.media",
|
|
14
|
+
imageStudio: "insert.imageStudio",
|
|
14
15
|
record: "insert.record",
|
|
15
16
|
mention: "insert.mention",
|
|
16
17
|
reference: "insert.reference",
|
|
@@ -38,6 +39,7 @@ const ROOT_ABILITIES = [
|
|
|
38
39
|
ABILITY.composite,
|
|
39
40
|
ABILITY.transcript,
|
|
40
41
|
ABILITY.media,
|
|
42
|
+
ABILITY.imageStudio,
|
|
41
43
|
ABILITY.record,
|
|
42
44
|
ABILITY.mention,
|
|
43
45
|
ABILITY.reference,
|
|
@@ -61,6 +63,7 @@ const TEXT_ABILITIES = [
|
|
|
61
63
|
ABILITY.composite,
|
|
62
64
|
ABILITY.transcript,
|
|
63
65
|
ABILITY.media,
|
|
66
|
+
ABILITY.imageStudio,
|
|
64
67
|
ABILITY.record,
|
|
65
68
|
ABILITY.mention,
|
|
66
69
|
ABILITY.reference,
|
|
@@ -90,6 +93,7 @@ const TYPE_ABILITIES = {
|
|
|
90
93
|
audio: ABILITY.media,
|
|
91
94
|
video: ABILITY.media,
|
|
92
95
|
file: ABILITY.media,
|
|
96
|
+
imageStudio: ABILITY.imageStudio,
|
|
93
97
|
mention: ABILITY.mention,
|
|
94
98
|
reference: ABILITY.reference,
|
|
95
99
|
tag: ABILITY.tag
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abilities.js","names":[],"sources":["../../../src/core/schema/abilities.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\n\n// Vue-free and server-importable: node schemas shared with the server declare their rule via `NodeConfig.abilities`.\n\n/** Ability ids. Hosts may add their own strings; these are the ones the editor itself checks. */\nexport const ABILITY = {\n marks: 'format.marks',\n headings: 'format.headings',\n lists: 'format.lists',\n blockquote: 'format.blockquote',\n codeBlock: 'format.codeBlock',\n rule: 'format.rule',\n blockContent: 'block.content',\n composite: 'format.composite',\n transcript: 'transcript.segment',\n media: 'insert.media',\n record: 'insert.record',\n mention: 'insert.mention',\n reference: 'insert.reference',\n tag: 'insert.tag',\n aiRewrite: 'ai.rewrite',\n aiGenerate: 'ai.generate',\n aiAutocomplete: 'ai.autocomplete',\n blockDelete: 'block.delete',\n blockDuplicate: 'block.duplicate',\n blockConvert: 'block.convert',\n compositeMeta: 'composite.meta',\n compositeSummarize: 'composite.summarize',\n compositeExport: 'composite.export',\n compositeDerive: 'composite.derive',\n} as const\n\nexport type Ability = (typeof ABILITY)[keyof typeof ABILITY] | (string & {})\n\n/** How a node changes the abilities of everything inside it: `grants`, then `revokes`; `own` only applies when it is the innermost node. */\nexport interface AbilityRule {\n grants?: readonly Ability[]\n revokes?: readonly Ability[]\n own?: readonly Ability[]\n}\n\nexport type AbilityRuleSource = AbilityRule | ((node: PMNode) => AbilityRule)\n\n/** Everything a plain paragraph at document root can do. `composite.*` are `own` abilities of composites only. */\nexport const ROOT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete, ABILITY.blockDelete, ABILITY.blockDuplicate, ABILITY.blockConvert,\n]\n\n/** Text-editing abilities an atom block (image/audio/video) or a tags row can never have. */\nexport const TEXT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete,\n]\n\n/** Node/mark type → ability its creation/toggling needs (toolbar fallback, shortcut + input-rule gate). */\nexport const TYPE_ABILITIES: Readonly<Record<string, Ability>> = {\n bold: ABILITY.marks,\n italic: ABILITY.marks,\n strike: ABILITY.marks,\n code: ABILITY.marks,\n heading: ABILITY.headings,\n bulletList: ABILITY.lists,\n orderedList: ABILITY.lists,\n listItem: ABILITY.lists,\n taskList: ABILITY.lists,\n taskItem: ABILITY.lists,\n blockquote: ABILITY.blockquote,\n codeBlock: ABILITY.codeBlock,\n horizontalRule: ABILITY.rule,\n composite: ABILITY.composite,\n transcriptSegment: ABILITY.transcript,\n image: ABILITY.media,\n audio: ABILITY.media,\n video: ABILITY.media,\n file: ABILITY.media,\n mention: ABILITY.mention,\n reference: ABILITY.reference,\n tag: ABILITY.tag,\n}\n\n/**\n * Abilities at the end of `chain` (ancestors from depth 1 down to and including the innermost node):\n * start with `root`; per node add its `grants`, remove its `revokes`; finally add the innermost node's `own`.\n */\nexport function resolveAbilities(rules: ReadonlyMap<string, AbilityRuleSource>, root: readonly Ability[], chain: readonly PMNode[]): ReadonlySet<Ability> {\n const set = new Set<Ability>(root)\n let own: readonly Ability[] | undefined\n for (const node of chain) {\n const source = rules.get(node.type.name)\n const rule = typeof source === 'function' ? source(node) : source\n for (const ability of rule?.grants ?? []) set.add(ability)\n for (const ability of rule?.revokes ?? []) set.delete(ability)\n own = rule?.own\n }\n for (const ability of own ?? []) set.add(ability)\n return set\n}\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /** Abilities this node grants/revokes for its content (`own`: only when it is the innermost node). Host rules override it. */\n abilities?: AbilityRuleSource\n }\n}\n"],"mappings":";;AAKA,MAAa,UAAU;CACrB,OAAO;CACP,UAAU;CACV,OAAO;CACP,YAAY;CACZ,WAAW;CACX,MAAM;CACN,cAAc;CACd,WAAW;CACX,YAAY;CACZ,OAAO;CACP,QAAQ;CACR,SAAS;CACT,WAAW;CACX,KAAK;CACL,WAAW;CACX,YAAY;CACZ,gBAAgB;CAChB,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;AACnB;;AAcA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;
|
|
1
|
+
{"version":3,"file":"abilities.js","names":[],"sources":["../../../src/core/schema/abilities.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\n\n// Vue-free and server-importable: node schemas shared with the server declare their rule via `NodeConfig.abilities`.\n\n/** Ability ids. Hosts may add their own strings; these are the ones the editor itself checks. */\nexport const ABILITY = {\n marks: 'format.marks',\n headings: 'format.headings',\n lists: 'format.lists',\n blockquote: 'format.blockquote',\n codeBlock: 'format.codeBlock',\n rule: 'format.rule',\n blockContent: 'block.content',\n composite: 'format.composite',\n transcript: 'transcript.segment',\n media: 'insert.media',\n imageStudio: 'insert.imageStudio',\n record: 'insert.record',\n mention: 'insert.mention',\n reference: 'insert.reference',\n tag: 'insert.tag',\n aiRewrite: 'ai.rewrite',\n aiGenerate: 'ai.generate',\n aiAutocomplete: 'ai.autocomplete',\n blockDelete: 'block.delete',\n blockDuplicate: 'block.duplicate',\n blockConvert: 'block.convert',\n compositeMeta: 'composite.meta',\n compositeSummarize: 'composite.summarize',\n compositeExport: 'composite.export',\n compositeDerive: 'composite.derive',\n} as const\n\nexport type Ability = (typeof ABILITY)[keyof typeof ABILITY] | (string & {})\n\n/** How a node changes the abilities of everything inside it: `grants`, then `revokes`; `own` only applies when it is the innermost node. */\nexport interface AbilityRule {\n grants?: readonly Ability[]\n revokes?: readonly Ability[]\n own?: readonly Ability[]\n}\n\nexport type AbilityRuleSource = AbilityRule | ((node: PMNode) => AbilityRule)\n\n/** Everything a plain paragraph at document root can do. `composite.*` are `own` abilities of composites only. */\nexport const ROOT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete, ABILITY.blockDelete, ABILITY.blockDuplicate, ABILITY.blockConvert,\n]\n\n/** Text-editing abilities an atom block (image/audio/video) or a tags row can never have. */\nexport const TEXT_ABILITIES: readonly Ability[] = [\n ABILITY.marks, ABILITY.headings, ABILITY.lists, ABILITY.blockquote, ABILITY.codeBlock, ABILITY.rule, ABILITY.blockContent, ABILITY.composite,\n ABILITY.transcript, ABILITY.media, ABILITY.imageStudio, ABILITY.record, ABILITY.mention, ABILITY.reference, ABILITY.tag, ABILITY.aiRewrite, ABILITY.aiGenerate,\n ABILITY.aiAutocomplete,\n]\n\n/** Node/mark type → ability its creation/toggling needs (toolbar fallback, shortcut + input-rule gate). */\nexport const TYPE_ABILITIES: Readonly<Record<string, Ability>> = {\n bold: ABILITY.marks,\n italic: ABILITY.marks,\n strike: ABILITY.marks,\n code: ABILITY.marks,\n heading: ABILITY.headings,\n bulletList: ABILITY.lists,\n orderedList: ABILITY.lists,\n listItem: ABILITY.lists,\n taskList: ABILITY.lists,\n taskItem: ABILITY.lists,\n blockquote: ABILITY.blockquote,\n codeBlock: ABILITY.codeBlock,\n horizontalRule: ABILITY.rule,\n composite: ABILITY.composite,\n transcriptSegment: ABILITY.transcript,\n image: ABILITY.media,\n audio: ABILITY.media,\n video: ABILITY.media,\n file: ABILITY.media,\n imageStudio: ABILITY.imageStudio,\n mention: ABILITY.mention,\n reference: ABILITY.reference,\n tag: ABILITY.tag,\n}\n\n/**\n * Abilities at the end of `chain` (ancestors from depth 1 down to and including the innermost node):\n * start with `root`; per node add its `grants`, remove its `revokes`; finally add the innermost node's `own`.\n */\nexport function resolveAbilities(rules: ReadonlyMap<string, AbilityRuleSource>, root: readonly Ability[], chain: readonly PMNode[]): ReadonlySet<Ability> {\n const set = new Set<Ability>(root)\n let own: readonly Ability[] | undefined\n for (const node of chain) {\n const source = rules.get(node.type.name)\n const rule = typeof source === 'function' ? source(node) : source\n for (const ability of rule?.grants ?? []) set.add(ability)\n for (const ability of rule?.revokes ?? []) set.delete(ability)\n own = rule?.own\n }\n for (const ability of own ?? []) set.add(ability)\n return set\n}\n\ndeclare module '@tiptap/core' {\n interface NodeConfig<Options, Storage> {\n /** Abilities this node grants/revokes for its content (`own`: only when it is the innermost node). Host rules override it. */\n abilities?: AbilityRuleSource\n }\n}\n"],"mappings":";;AAKA,MAAa,UAAU;CACrB,OAAO;CACP,UAAU;CACV,OAAO;CACP,YAAY;CACZ,WAAW;CACX,MAAM;CACN,cAAc;CACd,WAAW;CACX,YAAY;CACZ,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;CACT,WAAW;CACX,KAAK;CACL,WAAW;CACX,YAAY;CACZ,gBAAgB;CAChB,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,oBAAoB;CACpB,iBAAiB;CACjB,iBAAiB;AACnB;;AAcA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAa,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CACpJ,QAAQ;CAAgB,QAAQ;CAAa,QAAQ;CAAgB,QAAQ;AAC/E;;AAGA,MAAa,iBAAqC;CAChD,QAAQ;CAAO,QAAQ;CAAU,QAAQ;CAAO,QAAQ;CAAY,QAAQ;CAAW,QAAQ;CAAM,QAAQ;CAAc,QAAQ;CACnI,QAAQ;CAAY,QAAQ;CAAO,QAAQ;CAAa,QAAQ;CAAQ,QAAQ;CAAS,QAAQ;CAAW,QAAQ;CAAK,QAAQ;CAAW,QAAQ;CACpJ,QAAQ;AACV;;AAGA,MAAa,iBAAoD;CAC/D,MAAM,QAAQ;CACd,QAAQ,QAAQ;CAChB,QAAQ,QAAQ;CAChB,MAAM,QAAQ;CACd,SAAS,QAAQ;CACjB,YAAY,QAAQ;CACpB,aAAa,QAAQ;CACrB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,UAAU,QAAQ;CAClB,YAAY,QAAQ;CACpB,WAAW,QAAQ;CACnB,gBAAgB,QAAQ;CACxB,WAAW,QAAQ;CACnB,mBAAmB,QAAQ;CAC3B,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,OAAO,QAAQ;CACf,MAAM,QAAQ;CACd,aAAa,QAAQ;CACrB,SAAS,QAAQ;CACjB,WAAW,QAAQ;CACnB,KAAK,QAAQ;AACf;;;;;AAMA,SAAgB,iBAAiB,OAA+C,MAA0B,OAAgD;CACxJ,MAAM,MAAM,IAAI,IAAa,IAAI;CACjC,IAAI;CACJ,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,IAAI,KAAK,KAAK,IAAI;EACvC,MAAM,OAAO,OAAO,WAAW,aAAa,OAAO,IAAI,IAAI;EAC3D,KAAK,MAAM,WAAW,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,OAAO;EACzD,KAAK,MAAM,WAAW,MAAM,WAAW,CAAC,GAAG,IAAI,OAAO,OAAO;EAC7D,MAAM,MAAM;CACd;CACA,KAAK,MAAM,WAAW,OAAO,CAAC,GAAG,IAAI,IAAI,OAAO;CAChD,OAAO;AACT"}
|
|
@@ -16,8 +16,8 @@ export type ChatStream = (messages: ChatMessage[], signal: AbortSignal) => Async
|
|
|
16
16
|
/** Body schema of the generate request (`POST /api/ai/generate`, `EditorBackend['ai'].stream('generate', …)`). */
|
|
17
17
|
export declare const generateBodySchema: z.ZodObject<{
|
|
18
18
|
task: z.ZodEnum<{
|
|
19
|
-
custom: "custom";
|
|
20
19
|
generate: "generate";
|
|
20
|
+
custom: "custom";
|
|
21
21
|
continue: "continue";
|
|
22
22
|
reformulate: "reformulate";
|
|
23
23
|
"transcript-summary": "transcript-summary";
|
|
@@ -129,6 +129,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
129
129
|
return [{
|
|
130
130
|
row,
|
|
131
131
|
anchor,
|
|
132
|
+
editable: editable.value && row.controls,
|
|
132
133
|
left,
|
|
133
134
|
top: (inStrip ? anchor.toggleTop + anchor.toggleSize / 2 : anchor.lastCenter) - ROW_HEIGHT / 2,
|
|
134
135
|
width: Math.max((frame.value?.right ?? anchor.right) - left, 120),
|
|
@@ -173,7 +174,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
173
174
|
content: withCtx(() => [createVNode(_sfc_main$2, {
|
|
174
175
|
editor: __props.editor,
|
|
175
176
|
row: item.row,
|
|
176
|
-
editable: editable
|
|
177
|
+
editable: item.editable
|
|
177
178
|
}, null, 8, [
|
|
178
179
|
"editor",
|
|
179
180
|
"row",
|
|
@@ -182,10 +183,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
182
183
|
default: withCtx(() => [createElementVNode("span", {
|
|
183
184
|
role: "button",
|
|
184
185
|
tabindex: "0",
|
|
185
|
-
draggable: editable
|
|
186
|
+
draggable: item.editable,
|
|
186
187
|
class: normalizeClass([
|
|
187
188
|
"absolute z-10 flex items-center justify-center rounded-sm bg-[var(--sk-bg)] text-[var(--sk-muted)] transition-[color,background-color,opacity] duration-120 motion-reduce:transition-none hover:bg-[var(--sk-bg-elevated)] hover:text-[var(--sk-fg-highlighted)] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--sk-primary)]",
|
|
188
|
-
editable
|
|
189
|
+
item.editable ? "cursor-grab active:cursor-grabbing" : "cursor-pointer",
|
|
189
190
|
iconOnHover.value && !hoveredKeys.value.has(item.row.key) && chipsHoveredKey.value !== item.row.key && "opacity-0 hover:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100"
|
|
190
191
|
]),
|
|
191
192
|
style: normalizeStyle({
|
|
@@ -195,7 +196,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
195
196
|
height: `${item.anchor.toggleSize}px`
|
|
196
197
|
}),
|
|
197
198
|
"aria-label": unref(t)("composites.chrome.details", { kind: item.row.kindLabel }),
|
|
198
|
-
title: editable
|
|
199
|
+
title: item.editable ? unref(t)("composites.chrome.detailsDrag", { kind: item.row.kindLabel }) : unref(t)("composites.chrome.details", { kind: item.row.kindLabel }),
|
|
199
200
|
onKeydown: _cache[0] || (_cache[0] = withKeys(withModifiers(($event) => $event.currentTarget.click(), ["prevent"]), ["enter", "space"])),
|
|
200
201
|
onDragstart: ($event) => onIconDragStart($event, item.row)
|
|
201
202
|
}, [(openBlock(), createBlock(unref(UIcon), {
|
|
@@ -204,7 +205,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
204
205
|
class: "size-3.5"
|
|
205
206
|
}, null, 8, ["name"]))], 46, _hoisted_2)]),
|
|
206
207
|
_: 2
|
|
207
|
-
}, 1024)) : createCommentVNode("v-if", true), editable
|
|
208
|
+
}, 1024)) : createCommentVNode("v-if", true), item.editable || item.basket || item.status || item.derived || item.row.tagsVisible && item.row.tags.length ? (openBlock(), createElementBlock("div", {
|
|
208
209
|
key: 1,
|
|
209
210
|
class: "absolute z-10 flex items-center gap-px overflow-hidden rounded-md bg-[var(--sk-bg)]",
|
|
210
211
|
style: normalizeStyle({
|
|
@@ -216,7 +217,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
216
217
|
onMouseenter: ($event) => chipsHoveredKey.value = item.row.key,
|
|
217
218
|
onMouseleave: _cache[1] || (_cache[1] = ($event) => chipsHoveredKey.value = null)
|
|
218
219
|
}, [
|
|
219
|
-
editable
|
|
220
|
+
item.editable ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createVNode(_sfc_main$1, {
|
|
220
221
|
editor: __props.editor,
|
|
221
222
|
row: item.row
|
|
222
223
|
}, {
|
|
@@ -277,7 +278,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
277
278
|
class: "size-3.5"
|
|
278
279
|
}), createTextVNode(toDisplayString(unref(t)("composites.derivation.stale")), 1)], 2)) : createCommentVNode("v-if", true)
|
|
279
280
|
], 64)) : createCommentVNode("v-if", true),
|
|
280
|
-
item.status ? (openBlock(), createElementBlock(Fragment, { key: 3 }, [editable
|
|
281
|
+
item.status ? (openBlock(), createElementBlock(Fragment, { key: 3 }, [item.editable ? (openBlock(), createBlock(_sfc_main$3, {
|
|
281
282
|
key: 0,
|
|
282
283
|
editor: __props.editor,
|
|
283
284
|
row: item.row
|
|
@@ -302,7 +303,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
302
303
|
return openBlock(), createElementBlock("span", {
|
|
303
304
|
key: tag,
|
|
304
305
|
class: normalizeClass([chip, "group max-w-40 gap-0"])
|
|
305
|
-
}, [createElementVNode("span", _hoisted_9, "#" + toDisplayString(tag), 1), editable
|
|
306
|
+
}, [createElementVNode("span", _hoisted_9, "#" + toDisplayString(tag), 1), item.editable ? (openBlock(), createElementBlock("button", {
|
|
306
307
|
key: 0,
|
|
307
308
|
type: "button",
|
|
308
309
|
class: "flex h-4 w-0 shrink-0 items-center justify-center overflow-hidden rounded-sm opacity-0 transition-all duration-150 group-hover:ml-1 group-hover:w-4 group-hover:opacity-100 hover:bg-[var(--sk-bg-elevated)] hover:text-[var(--sk-fg-highlighted)] focus-visible:ml-1 focus-visible:w-4 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-[var(--sk-primary)]",
|
|
@@ -313,7 +314,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
313
314
|
class: "size-3"
|
|
314
315
|
})], 8, _hoisted_10)) : createCommentVNode("v-if", true)], 2);
|
|
315
316
|
}), 128)),
|
|
316
|
-
editable
|
|
317
|
+
item.editable && item.row.tagsVisible ? (openBlock(), createBlock(unref(UPopover), {
|
|
317
318
|
key: 4,
|
|
318
319
|
content: {
|
|
319
320
|
side: "bottom",
|