@skaile/forge-editor 6.3.0 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/adapters/nitro.d.ts +11 -3
- package/dist/adapters/nitro.js +2 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/block-index.js +3 -2
- package/dist/core/block-index.js.map +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/index.d.ts +2 -2
- package/dist/features/ai/index.js +2 -2
- package/dist/features/ai/protocol/chat.d.ts +5 -0
- package/dist/features/ai/protocol/chat.js +7 -4
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +2 -2
- package/dist/features/ai/protocol/index.js +2 -2
- package/dist/markdown/MarkdownSurface.vue.d.ts +5 -5
- package/dist/server/chat-agent.d.ts +85 -0
- package/dist/server/chat-agent.js +61 -0
- package/dist/server/chat-agent.js.map +1 -0
- package/dist/server/document-tools.d.ts +22 -0
- package/dist/server/document-tools.js +174 -0
- package/dist/server/document-tools.js.map +1 -0
- package/dist/server/handlers/ai.d.ts +2 -1
- package/dist/server/handlers/ai.js +232 -121
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +4 -1
- package/dist/server/handlers/context.js.map +1 -1
- package/dist/server/index.d.ts +7 -3
- package/dist/server/index.js +5 -2
- package/dist/server/workspace-tools.d.ts +1 -0
- package/dist/server/workspace-tools.js +8 -3
- package/dist/server/workspace-tools.js.map +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.4.1 - 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `document_outline` titles a heading or Typed block exactly as the Block index does: a hard break in the title is a line break, no longer a space, and the title is no longer trimmed.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Tests of the chat agent seam and the document tools check behaviour instead of the wording of tool results; the glossary (`CONTEXT.md`) names the Chat agent and how `document_outline` relates to the Block outline.
|
|
12
|
+
|
|
13
|
+
## 6.4.0 - 2026-11-02
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Chat agent seam (`@skaile/forge-editor/server`): `EditorServerOptions.chatAgent` (also on `ProvidedEditorServer`, so `provideEditorServer` takes it) is resolved for the user of every `POST /api/ai/chat` request; a `ChatAgent` answers the turn in place of the built-in model loop, `null` keeps the loop. The route still checks the request and streams the same SSE events, so the client (`AiSidebar`) is unchanged: the agent's `ChatAgentPart`s map `text` → `delta`, `tool` → `tool`, all `edit` parts → one proposal (`change-set` for other Documents, `ops` numbered from `nextOp`), then `done`; a thrown error is `error`. `ChatAgentTurn` carries the messages, the document snapshot, the scope, the block model, the read-only tools with their `ToolContext`, the built-in system prompt, the `edit_document` tool and `checkEdit(input)`, which checks a call with `checkAgentOps` (together with the turn's earlier calls) and answers the numbered operations, the problems and the tool result for the model.
|
|
18
|
+
- `documentChatSystem(blockModel, tools)` (`@skaile/forge-editor/ai/protocol` and `/server`): the document chat's system prompt, which the built-in loop uses too. `documentEditTool({ workspace })` and `documentEditInputSchema({ workspace })` (`/server`): the `edit_document` tool and the JSON schema of its input as the built-in loop offers them.
|
|
19
|
+
- `createDocumentTools()` (`@skaile/forge-editor/server`): two read-only chat tools for either chat path. `document_outline(documentId?)` answers the block tree of the open (or another readable) Document: id, type (heading level, Typed block kind and title), child ids and character count of every block, from the live content when the Document is open in the collaboration server, else the stored content. `read_blocks(documentId?, blockIds)` answers the Markdown of up to 50 blocks with the limits of `read_block`. An unreadable Document and an unknown block answer the same sentence.
|
|
20
|
+
|
|
3
21
|
## 6.3.0 - 2026-10-31
|
|
4
22
|
|
|
5
23
|
### Added
|
package/dist/adapters/nitro.d.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { BlockType } from "../features/composites/block-types.js";
|
|
|
4
4
|
import "../features/ai/protocol/index.js";
|
|
5
5
|
import { EditorPorts } from "../server/ports.js";
|
|
6
6
|
import { AiModels, AiModelsConfig } from "../server/ai.js";
|
|
7
|
+
import { ToolProvider } from "../server/tools.js";
|
|
8
|
+
import { ChatAgent } from "../server/chat-agent.js";
|
|
7
9
|
import { CollabServer, CollabServerOptions } from "../server/collab.js";
|
|
8
10
|
import { DocumentMutator } from "../server/document-mutator.js";
|
|
9
11
|
import { RateLimiterOptions } from "../server/rate-limit.js";
|
|
10
|
-
import { ToolProvider } from "../server/tools.js";
|
|
11
12
|
import "../server/index.js";
|
|
12
13
|
import "../features/composites/schema.js";
|
|
13
14
|
import { RouteDef } from "../server/routes.js";
|
|
@@ -59,6 +60,13 @@ export interface EditorServerOptions {
|
|
|
59
60
|
* turn; the system prompt lists them. Needs `ai`; an unusable provider (duplicate name, `edit_document`) throws at install. Omitted: the chat has only `edit_document`.
|
|
60
61
|
*/
|
|
61
62
|
tools?: readonly ToolProvider[];
|
|
63
|
+
/**
|
|
64
|
+
* The host's agent for the document chat (`ChatAgent`, `@skaile/forge-editor/server`), resolved for the user of each
|
|
65
|
+
* `POST /api/ai/chat` request: an agent answers the turn in place of the built-in model loop (the route still checks the
|
|
66
|
+
* request and the proposed operations and streams the same events), `null` keeps the built-in loop, so a host setting
|
|
67
|
+
* takes effect with the next message. Needs `ai`. Omitted: always the built-in loop.
|
|
68
|
+
*/
|
|
69
|
+
chatAgent?: (user: object) => ChatAgent | null | Promise<ChatAgent | null>;
|
|
62
70
|
}
|
|
63
71
|
/** What `installEditorServer` returns: the installed server, one handle for host code (routes, scripts) to reach the instances the editor's own routes use. */
|
|
64
72
|
export interface EditorServer {
|
|
@@ -110,8 +118,8 @@ export interface EditorServer {
|
|
|
110
118
|
* client (`createHttpBackend`) calls.
|
|
111
119
|
*/
|
|
112
120
|
export declare function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer;
|
|
113
|
-
/** What a Nuxt host provides: its ports, collaboration schema, Block types and chat
|
|
114
|
-
export type ProvidedEditorServer = Pick<EditorServerOptions, 'ports' | 'schema' | 'blockTypes' | 'tools'>;
|
|
121
|
+
/** What a Nuxt host provides: its ports, collaboration schema, Block types, chat tools and chat agent; the module adds `ai`, `rateLimit` and `collab` from the runtime config. */
|
|
122
|
+
export type ProvidedEditorServer = Pick<EditorServerOptions, 'ports' | 'schema' | 'blockTypes' | 'tools' | 'chatAgent'>;
|
|
115
123
|
/** Nuxt hosts: registers the ports (and optional schema and Block types) the module's server plugin installs at the first request. Call it in a Nitro plugin. */
|
|
116
124
|
export declare function provideEditorServer(options: ProvidedEditorServer): void;
|
|
117
125
|
/**
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -103,7 +103,8 @@ function installEditorServer(nitroApp, options) {
|
|
|
103
103
|
...context,
|
|
104
104
|
ai: models,
|
|
105
105
|
limiter,
|
|
106
|
-
tools
|
|
106
|
+
tools,
|
|
107
|
+
...options.chatAgent && { chatAgent: options.chatAgent }
|
|
107
108
|
};
|
|
108
109
|
const mounted = serverRoutesOf(ports, { ai: aiContext !== void 0 });
|
|
109
110
|
for (const route of options.routes ? options.routes(mounted) : mounted) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension, JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { ModelRef } from '../features/ai/protocol'\nimport type { BlockType } from '../features/composites/schema'\nimport { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createDocumentMutator, createRateLimiter, type DocumentMutator, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig, type EditorServerConfig } from '../config'\nimport { createBlockMarkdown, withBlockIndex } from '../server/block-index'\nimport { routeOf } from '../server/h3'\nimport { HttpError } from '../server/errors'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { enforceRateLimit } from '../server/handlers/context'\nimport { type RouteDef, serverRoutesOf } from '../server/routes'\nimport { type ToolProvider, toolRegistry } from '../server/tools'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@skaile/forge-editor/nuxt`) from its server plugin. Both end in the same `@skaile/forge-editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\n/** The route type of the table `EditorServerOptions.routes` filters. */\nexport type { RouteDef }\n\nexport interface EditorServerOptions {\n /** Access control, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`AccessControl.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, model roles of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */\n routes?: (routes: readonly RouteDef[]) => readonly RouteDef[]\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n /**\n * The host's Block types (the list its editors get through the `blockTypes` option, e.g. `mergeBlockTypes(defaultBlockTypes(t), parseBlockTypes(file))`),\n * so the server resolves them without the client repeating them. The AI chat (`POST /api/ai/chat`) describes these to the model and ignores the\n * `blockTypes` in the request. Omitted: the chat takes the `blockTypes` the client sends. The collaboration schema does not need them (Block types are storage, not schema).\n */\n blockTypes?: readonly BlockType[]\n /**\n * The document chat's read-only tools besides `edit_document` (`ToolProvider`, `@skaile/forge-editor/server`), e.g. `createWorkspaceTools()` so the\n * model can find and read Blocks of the workspace's other Documents. The server runs a call itself and feeds the result back before the model's next\n * turn; the system prompt lists them. Needs `ai`; an unusable provider (duplicate name, `edit_document`) throws at install. Omitted: the chat has only `edit_document`.\n */\n tools?: readonly ToolProvider[]\n}\n\n/** What `installEditorServer` returns: the installed server, one handle for host code (routes, scripts) to reach the instances the editor's own routes use. */\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** Applies validated block operations to any Document, open or not (the one behind `POST /api/editor/documents/:id/ops`); its writes re-project the Block index. For host code that changes Documents on a user's behalf. */\n mutator: DocumentMutator\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Tell connected clients the document is gone, disconnect them and drop the Yjs state, and drop its rows from the Block index. Call before removing the JSON. The collaboration part is a no-op with `collab: false`. */\n forgetDocument(id: string): Promise<void>\n /** Re-projects the document into the Block index from the documents port (live content first). Call after a host route changed the stored content itself; a no-op without the `index` port. Editor stores and `DocumentAdmin` writes need no call. */\n reindexDocument(id: string): Promise<void>\n /** Current content of a document that is open in the collaboration server (unsaved edits included), `null` when nobody has it open or with `collab: false`. */\n liveContent(id: string): JSONContent | null\n /**\n * Per-user limit for host-defined AI routes: the same limiter as the editor's own AI endpoints. Call first in the route.\n * Throws 401 (`HttpError`) without a user, 429 when exceeded; returns the user the identity port authenticated, for `resolveModel`.\n * With a Node-backed request (`event.node.res`, as h3 events have) the 429's `Retry-After` header is set on the response too.\n * Throws a plain `Error` without `ai`.\n */\n enforceRateLimit(request: { headers: Headers, node?: { res: { setHeader(name: string, value: string): unknown } } }): Promise<object>\n /** AI SDK language model for `ref`, see `AiModels.resolveModel`. Throws a plain `Error` without `ai`. */\n resolveModel(ref: ModelRef, user: object | null): Promise<LanguageModel>\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/editor/blocks/:documentId/:blockId GET a Block of any document by address, with `ports.index` (kept current by the documents port's writes)\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The returned `EditorServer` is the one handle to what the routes use (`forgetDocument`, `liveContent`,\n * `enforceRateLimit`, `resolveModel`); a Nuxt host reaches it through `useEditorServer()`. The paths are the ones the editor's\n * client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { schema } = options\n const tools = toolRegistry(options.tools) // fail at install, not at the first chat message\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n // The index decorates the documents port before anything reads it, so the collaboration server's stores re-project too; it reads live content through the collab server created from the result.\n const indexed = options.ports.index && withBlockIndex({ ...options.ports, index: options.ports.index }, {\n liveContent: id => collab?.liveContent(id) ?? null,\n markdown: createBlockMarkdown({ ...(schema && { schema }), ...(options.blockTypes && { blockTypes: options.blockTypes }) }),\n })\n const ports = indexed?.ports ?? options.ports\n const collab: CollabServer | undefined = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const mutator = createDocumentMutator({ ports, ...(collab && { collab }), ...(schema && { schema }), ...(options.blockTypes && { blockTypes: options.blockTypes }), ...(indexed && { reindex: indexed.reindex }) })\n const context: HandlerContext = { ports, ...(collab && { collab }), mutator, ...(options.blockTypes && { blockTypes: options.blockTypes }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter, tools }\n const mounted = serverRoutesOf(ports, { ai: aiContext !== undefined })\n for (const route of options.routes ? options.routes(mounted) : mounted) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const server: EditorServer = {\n ports,\n collab,\n models,\n mutator,\n forgetDocument: async (id) => {\n await collab?.forgetDocument(id)\n await indexed?.forget(id)\n },\n reindexDocument: async id => void await indexed?.reindex(id),\n liveContent: id => collab?.liveContent(id) ?? null,\n async enforceRateLimit(request) {\n if (!aiContext) throw new Error('@skaile/forge-editor: AI is off (installEditorServer without ai)')\n try {\n return await enforceRateLimit(aiContext, { headers: request.headers })\n }\n catch (error) {\n if (error instanceof HttpError) for (const [name, value] of Object.entries(error.headers)) request.node?.res.setHeader(name, value)\n throw error\n }\n },\n async resolveModel(ref, user) {\n if (!models) throw new Error('@skaile/forge-editor: AI is off (installEditorServer without ai)')\n return models.resolveModel(ref, user)\n },\n close: async () => void await collab?.close(),\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n\n// A Nuxt host cannot call `installEditorServer` itself: its ports are registered in a Nitro plugin of its own, and the\n// module's server plugin installs at the first request, after every plugin has run, with the AI configuration, rate limit\n// and collaboration options of the runtime config. The host hands over its part with `provideEditorServer`; the module\n// installs it and routes reach the handle with `useEditorServer`. The registration is consumed by the install; nothing\n// here keeps a second copy of the ports.\n\n/** What a Nuxt host provides: its ports, collaboration schema, Block types and chat tools; the module adds `ai`, `rateLimit` and `collab` from the runtime config. */\nexport type ProvidedEditorServer = Pick<EditorServerOptions, 'ports' | 'schema' | 'blockTypes' | 'tools'>\n\nlet provided: ProvidedEditorServer | undefined\nlet installed: EditorServer | undefined\n\n/** Nuxt hosts: registers the ports (and optional schema and Block types) the module's server plugin installs at the first request. Call it in a Nitro plugin. */\nexport function provideEditorServer(options: ProvidedEditorServer): void {\n provided = options\n}\n\n/**\n * The Nuxt module's server plugin: installs what `provideEditorServer` registered over `config` (`serverConfig` of the module's\n * runtime config) and makes it the one `useEditorServer` returns until it closes. Without the `collabState` port there is\n * no collaboration websocket (documents still load and save over HTTP); a preset without key names its `NUXT_TIPTAP_EDITOR_AI_*` env variables.\n */\nexport function installProvidedEditorServer(nitroApp: NitroHost, config: EditorServerConfig): EditorServer {\n if (!provided) throw new Error('@skaile/forge-editor: provideEditorServer() was not called')\n const server = installEditorServer(nitroApp, {\n ...provided,\n collab: provided.ports.collabState ? config.collab : false,\n // Env names use the preset id in upper snake case (`flashLite` → `FLASH_LITE`), like Nuxt's runtime config mapping.\n // i18n-ignore: operator-facing server error text (the HttpError messages of the server are German), not UI\n ai: { ...config.ai, missingKeyHint: id => ` (NUXT_TIPTAP_EDITOR_AI_PRESETS_${id.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_API_KEY oder NUXT_TIPTAP_EDITOR_AI_API_KEY)` },\n rateLimit: config.rateLimit,\n })\n provided = undefined\n installed = server\n nitroApp.hooks.hook('close', () => {\n if (installed === server) installed = undefined\n })\n return server\n}\n\n/** Nuxt hosts: the installed editor server, for host routes (`forgetDocument`, `liveContent`, `enforceRateLimit`, `resolveModel`). The module installs it at the first request, so it is there in every route handler; throws before that and after the server closed. */\nexport function useEditorServer(): EditorServer {\n if (!installed) throw new Error('@skaile/forge-editor: the editor server is not installed (the Nuxt module installs it at the first request)')\n return installed\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension, JSONContent } from '@tiptap/core'\nimport type { LanguageModel } from 'ai'\nimport type { ModelRef } from '../features/ai/protocol'\nimport type { BlockType } from '../features/composites/schema'\nimport { type AiModels, type AiModelsConfig, type ChatAgent, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createDocumentMutator, createRateLimiter, type DocumentMutator, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig, type EditorServerConfig } from '../config'\nimport { createBlockMarkdown, withBlockIndex } from '../server/block-index'\nimport { routeOf } from '../server/h3'\nimport { HttpError } from '../server/errors'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { enforceRateLimit } from '../server/handlers/context'\nimport { type RouteDef, serverRoutesOf } from '../server/routes'\nimport { type ToolProvider, toolRegistry } from '../server/tools'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@skaile/forge-editor/nuxt`) from its server plugin. Both end in the same `@skaile/forge-editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\n/** The route type of the table `EditorServerOptions.routes` filters. */\nexport type { RouteDef }\n\nexport interface EditorServerOptions {\n /** Access control, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`AccessControl.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, model roles of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */\n routes?: (routes: readonly RouteDef[]) => readonly RouteDef[]\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n /**\n * The host's Block types (the list its editors get through the `blockTypes` option, e.g. `mergeBlockTypes(defaultBlockTypes(t), parseBlockTypes(file))`),\n * so the server resolves them without the client repeating them. The AI chat (`POST /api/ai/chat`) describes these to the model and ignores the\n * `blockTypes` in the request. Omitted: the chat takes the `blockTypes` the client sends. The collaboration schema does not need them (Block types are storage, not schema).\n */\n blockTypes?: readonly BlockType[]\n /**\n * The document chat's read-only tools besides `edit_document` (`ToolProvider`, `@skaile/forge-editor/server`), e.g. `createWorkspaceTools()` so the\n * model can find and read Blocks of the workspace's other Documents. The server runs a call itself and feeds the result back before the model's next\n * turn; the system prompt lists them. Needs `ai`; an unusable provider (duplicate name, `edit_document`) throws at install. Omitted: the chat has only `edit_document`.\n */\n tools?: readonly ToolProvider[]\n /**\n * The host's agent for the document chat (`ChatAgent`, `@skaile/forge-editor/server`), resolved for the user of each\n * `POST /api/ai/chat` request: an agent answers the turn in place of the built-in model loop (the route still checks the\n * request and the proposed operations and streams the same events), `null` keeps the built-in loop, so a host setting\n * takes effect with the next message. Needs `ai`. Omitted: always the built-in loop.\n */\n chatAgent?: (user: object) => ChatAgent | null | Promise<ChatAgent | null>\n}\n\n/** What `installEditorServer` returns: the installed server, one handle for host code (routes, scripts) to reach the instances the editor's own routes use. */\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** Applies validated block operations to any Document, open or not (the one behind `POST /api/editor/documents/:id/ops`); its writes re-project the Block index. For host code that changes Documents on a user's behalf. */\n mutator: DocumentMutator\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Tell connected clients the document is gone, disconnect them and drop the Yjs state, and drop its rows from the Block index. Call before removing the JSON. The collaboration part is a no-op with `collab: false`. */\n forgetDocument(id: string): Promise<void>\n /** Re-projects the document into the Block index from the documents port (live content first). Call after a host route changed the stored content itself; a no-op without the `index` port. Editor stores and `DocumentAdmin` writes need no call. */\n reindexDocument(id: string): Promise<void>\n /** Current content of a document that is open in the collaboration server (unsaved edits included), `null` when nobody has it open or with `collab: false`. */\n liveContent(id: string): JSONContent | null\n /**\n * Per-user limit for host-defined AI routes: the same limiter as the editor's own AI endpoints. Call first in the route.\n * Throws 401 (`HttpError`) without a user, 429 when exceeded; returns the user the identity port authenticated, for `resolveModel`.\n * With a Node-backed request (`event.node.res`, as h3 events have) the 429's `Retry-After` header is set on the response too.\n * Throws a plain `Error` without `ai`.\n */\n enforceRateLimit(request: { headers: Headers, node?: { res: { setHeader(name: string, value: string): unknown } } }): Promise<object>\n /** AI SDK language model for `ref`, see `AiModels.resolveModel`. Throws a plain `Error` without `ai`. */\n resolveModel(ref: ModelRef, user: object | null): Promise<LanguageModel>\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/editor/blocks/:documentId/:blockId GET a Block of any document by address, with `ports.index` (kept current by the documents port's writes)\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The returned `EditorServer` is the one handle to what the routes use (`forgetDocument`, `liveContent`,\n * `enforceRateLimit`, `resolveModel`); a Nuxt host reaches it through `useEditorServer()`. The paths are the ones the editor's\n * client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { schema } = options\n const tools = toolRegistry(options.tools) // fail at install, not at the first chat message\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n // The index decorates the documents port before anything reads it, so the collaboration server's stores re-project too; it reads live content through the collab server created from the result.\n const indexed = options.ports.index && withBlockIndex({ ...options.ports, index: options.ports.index }, {\n liveContent: id => collab?.liveContent(id) ?? null,\n markdown: createBlockMarkdown({ ...(schema && { schema }), ...(options.blockTypes && { blockTypes: options.blockTypes }) }),\n })\n const ports = indexed?.ports ?? options.ports\n const collab: CollabServer | undefined = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const mutator = createDocumentMutator({ ports, ...(collab && { collab }), ...(schema && { schema }), ...(options.blockTypes && { blockTypes: options.blockTypes }), ...(indexed && { reindex: indexed.reindex }) })\n const context: HandlerContext = { ports, ...(collab && { collab }), mutator, ...(options.blockTypes && { blockTypes: options.blockTypes }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter, tools, ...(options.chatAgent && { chatAgent: options.chatAgent }) }\n const mounted = serverRoutesOf(ports, { ai: aiContext !== undefined })\n for (const route of options.routes ? options.routes(mounted) : mounted) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const server: EditorServer = {\n ports,\n collab,\n models,\n mutator,\n forgetDocument: async (id) => {\n await collab?.forgetDocument(id)\n await indexed?.forget(id)\n },\n reindexDocument: async id => void await indexed?.reindex(id),\n liveContent: id => collab?.liveContent(id) ?? null,\n async enforceRateLimit(request) {\n if (!aiContext) throw new Error('@skaile/forge-editor: AI is off (installEditorServer without ai)')\n try {\n return await enforceRateLimit(aiContext, { headers: request.headers })\n }\n catch (error) {\n if (error instanceof HttpError) for (const [name, value] of Object.entries(error.headers)) request.node?.res.setHeader(name, value)\n throw error\n }\n },\n async resolveModel(ref, user) {\n if (!models) throw new Error('@skaile/forge-editor: AI is off (installEditorServer without ai)')\n return models.resolveModel(ref, user)\n },\n close: async () => void await collab?.close(),\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n\n// A Nuxt host cannot call `installEditorServer` itself: its ports are registered in a Nitro plugin of its own, and the\n// module's server plugin installs at the first request, after every plugin has run, with the AI configuration, rate limit\n// and collaboration options of the runtime config. The host hands over its part with `provideEditorServer`; the module\n// installs it and routes reach the handle with `useEditorServer`. The registration is consumed by the install; nothing\n// here keeps a second copy of the ports.\n\n/** What a Nuxt host provides: its ports, collaboration schema, Block types, chat tools and chat agent; the module adds `ai`, `rateLimit` and `collab` from the runtime config. */\nexport type ProvidedEditorServer = Pick<EditorServerOptions, 'ports' | 'schema' | 'blockTypes' | 'tools' | 'chatAgent'>\n\nlet provided: ProvidedEditorServer | undefined\nlet installed: EditorServer | undefined\n\n/** Nuxt hosts: registers the ports (and optional schema and Block types) the module's server plugin installs at the first request. Call it in a Nitro plugin. */\nexport function provideEditorServer(options: ProvidedEditorServer): void {\n provided = options\n}\n\n/**\n * The Nuxt module's server plugin: installs what `provideEditorServer` registered over `config` (`serverConfig` of the module's\n * runtime config) and makes it the one `useEditorServer` returns until it closes. Without the `collabState` port there is\n * no collaboration websocket (documents still load and save over HTTP); a preset without key names its `NUXT_TIPTAP_EDITOR_AI_*` env variables.\n */\nexport function installProvidedEditorServer(nitroApp: NitroHost, config: EditorServerConfig): EditorServer {\n if (!provided) throw new Error('@skaile/forge-editor: provideEditorServer() was not called')\n const server = installEditorServer(nitroApp, {\n ...provided,\n collab: provided.ports.collabState ? config.collab : false,\n // Env names use the preset id in upper snake case (`flashLite` → `FLASH_LITE`), like Nuxt's runtime config mapping.\n // i18n-ignore: operator-facing server error text (the HttpError messages of the server are German), not UI\n ai: { ...config.ai, missingKeyHint: id => ` (NUXT_TIPTAP_EDITOR_AI_PRESETS_${id.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_API_KEY oder NUXT_TIPTAP_EDITOR_AI_API_KEY)` },\n rateLimit: config.rateLimit,\n })\n provided = undefined\n installed = server\n nitroApp.hooks.hook('close', () => {\n if (installed === server) installed = undefined\n })\n return server\n}\n\n/** Nuxt hosts: the installed editor server, for host routes (`forgetDocument`, `liveContent`, `enforceRateLimit`, `resolveModel`). The module installs it at the first request, so it is there in every route handler; throws before that and after the server closed. */\nexport function useEditorServer(): EditorServer {\n if (!installed) throw new Error('@skaile/forge-editor: the editor server is not installed (the Nuxt module installs it at the first request)')\n return installed\n}\n"],"mappings":"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"}
|
|
@@ -12,16 +12,16 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
|
12
12
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
13
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
14
|
"update:modelValue": (text: string) => any;
|
|
15
|
-
ready: (view: EditorView) => any;
|
|
16
15
|
save: (text: string) => any;
|
|
16
|
+
ready: (view: EditorView) => any;
|
|
17
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
18
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
19
|
-
onReady?: ((view: EditorView) => any) | undefined;
|
|
20
19
|
onSave?: ((text: string) => any) | undefined;
|
|
20
|
+
onReady?: ((view: EditorView) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
22
|
editable: boolean;
|
|
23
|
-
modelValue: string;
|
|
24
23
|
wordWrap: boolean;
|
|
24
|
+
modelValue: string;
|
|
25
25
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
26
26
|
declare const _default: typeof __VLS_export;
|
|
27
27
|
//#endregion
|
package/dist/core/block-index.js
CHANGED
|
@@ -50,7 +50,7 @@ const INLINE = /* @__PURE__ */ new Set([
|
|
|
50
50
|
CITE_NODE
|
|
51
51
|
]);
|
|
52
52
|
const attrString = (node, key) => typeof node.attrs?.[key] === "string" ? node.attrs[key] : "";
|
|
53
|
-
/** The plain text of an inline node or a textblock's inline content. */
|
|
53
|
+
/** The plain text of an inline node or a textblock's inline content. Internal: `document_outline` counts and titles Blocks with it. */
|
|
54
54
|
function inlineText(node) {
|
|
55
55
|
switch (node.type) {
|
|
56
56
|
case TEXT_NODE: return node.text ?? "";
|
|
@@ -68,6 +68,7 @@ function plainText(node) {
|
|
|
68
68
|
if (holdsInline(node)) return inlineText(node);
|
|
69
69
|
return (node.content ?? []).filter((child) => !(child.type !== void 0 && NOT_TEXT.has(child.type))).map(plainText).filter(Boolean).join("\n");
|
|
70
70
|
}
|
|
71
|
+
/** The title of a Block: a Typed block's title row, a heading's text, `''` for any other Block. Internal, like `inlineText`. */
|
|
71
72
|
const titleOf = (node) => {
|
|
72
73
|
if (isTypedBlock(node)) return (node.content ?? []).filter((child) => child.type === TYPED_BLOCK_TITLE).map(inlineText).join("");
|
|
73
74
|
return node.type === "heading" ? inlineText(node) : "";
|
|
@@ -274,6 +275,6 @@ function findBlock(node, blockId) {
|
|
|
274
275
|
return null;
|
|
275
276
|
}
|
|
276
277
|
//#endregion
|
|
277
|
-
export { META_EDGES_KEY, REFERENCE_NODE, addressKey, authoredEdgesOf, blockContentOf, blockEdgesOf, edgesInto, isTagEdgeTarget, projectDocument, sameAddress };
|
|
278
|
+
export { META_EDGES_KEY, REFERENCE_NODE, addressKey, authoredEdgesOf, blockContentOf, blockEdgesOf, edgesInto, inlineText, isTagEdgeTarget, projectDocument, sameAddress, titleOf };
|
|
278
279
|
|
|
279
280
|
//# sourceMappingURL=block-index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-index.js","names":[],"sources":["../../src/core/block-index.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport { sha256Hex } from './hash'\nimport { DERIVATION_KEY, derivationOf, isTypedBlock, metaOf, TYPED_BLOCK_DESCRIPTION, TYPED_BLOCK_TAGS, TYPED_BLOCK_TITLE, typeIdOf } from './typed-block'\n\n// Framework-free and server-importable: a Document's blocks as rows of the workspace's Block index. `projectDocument` is pure\n// (JSON in, rows out); where the rows live is the `BlockIndex` port (`server/ports`) and its adapters.\n\n// Same names as `features/mentions` and `features/composites`; copied because core must not import features.\nconst TAG_NODE = 'tag'\nconst MENTION_NODE = 'mention'\n/** Node name of a reference chip; `features/composites` spells it the same. */\nexport const REFERENCE_NODE = 'reference'\nconst CITE_NODE = 'cite'\nconst HARD_BREAK_NODE = 'hardBreak'\nconst TEXT_NODE = 'text'\n\n/** Where a Block is: the Document and the block ID inside it. Block IDs are unique per Document only. */\nexport interface BlockAddress {\n documentId: string\n blockId: string\n}\n\n/** A string that names `address` and nothing else, for a `Map` or `Set` key; an address without a Document ID (`''`, or missing) is the same key. */\nexport const addressKey = (address: BlockAddress): string => `${address.documentId ?? ''}\\u0000${address.blockId}`\n\n/** Whether two addresses name the same Block; an address without a Document ID (`''`, or missing) equals another one without. */\nexport const sameAddress = (a: BlockAddress, b: BlockAddress): boolean => (a.documentId ?? '') === (b.documentId ?? '') && a.blockId === b.blockId\n\n/** A reference chip in a Block: the Block it points at (`documentId` resolved: a chip without one points into its own Document) and its locator. */\nexport interface BlockReferenceRow extends BlockAddress {\n /** Position inside the target's source (`S. 12`, `03:14`); `null` = the whole Block. */\n locator: string | null\n}\n\n/** What a Derived block was made from, as an address: another Block (`documentId` resolved like a chip's) or an uploaded asset by name. */\nexport type BlockDerivationRow = BlockAddress | { asset: string }\n\n/** The Block property that holds a Block's authored edges (`AuthoredEdge[]`); `projectDocument` turns them into `BlockEdgeRow`s of origin `meta`. */\nexport const META_EDGES_KEY = 'edges'\n\n// Same node name as `features/composites/embed`; copied because core must not import features.\nconst EMBED_NODE = 'blockEmbed'\n\n/** What an Edge says about its two ends. The four built-in kinds are projected from the content; any other string is an authored kind (`supports`, `contradicts`, …). */\nexport type EdgeKind = 'reference' | 'derivation' | 'embed' | 'tag' | (string & {})\n\n/** Where an Edge was written: in the Block's content (a chip, an embed, a tag, the Derivation) or by hand in its `meta.edges`. */\nexport type EdgeOrigin = 'content' | 'meta'\n\n/** One outgoing Edge of a Block as its row holds it: the Block it points at (`documentId` resolved), or a tag name for a `tag` Edge. */\nexport interface BlockEdgeRow {\n to: BlockAddress | { tag: string }\n kind: EdgeKind\n origin: EdgeOrigin\n}\n\n/** An Edge with both ends: what `BlockIndex.edges` answers. `from` is the Block the Edge is written in. */\nexport interface BlockEdge extends BlockEdgeRow {\n from: BlockAddress\n}\n\n/** An Edge a person added to a Typed block (`meta.edges`): the target Block (`documentId` absent = this Document) and its kind. */\nexport interface AuthoredEdge {\n to: { documentId?: string, blockId: string }\n kind: string\n}\n\n/** `true` for the Edge that points at a tag name instead of a Block. */\nexport const isTagEdgeTarget = (to: BlockEdgeRow['to']): to is { tag: string } => 'tag' in to\n\n/** The authored edges in a `meta.edges` value; entries without a block ID or a kind are dropped, anything but an array is `[]`. */\nexport function authoredEdgesOf(value: unknown): AuthoredEdge[] {\n if (!Array.isArray(value)) return []\n const edges: AuthoredEdge[] = []\n for (const entry of value as unknown[]) {\n const { to, kind } = (entry ?? {}) as { to?: { documentId?: unknown, blockId?: unknown }, kind?: unknown }\n if (typeof to?.blockId !== 'string' || !to.blockId || typeof kind !== 'string' || !kind) continue\n edges.push({ to: { ...(typeof to.documentId === 'string' && to.documentId && { documentId: to.documentId }), blockId: to.blockId }, kind })\n }\n return edges\n}\n\n/**\n * One Block of a Document as the index holds it. Every node that carries a block ID is a Block (the `id` attribute of the\n * stored JSON): Typed blocks, paragraphs, headings, lists, media. A Block without an ID has no address and no row.\n * The content fields (`text`, `tags`, `references`) cover the Block's whole content, nested Blocks included: a Typed block's row\n * answers for its body, and the nested Blocks have rows of their own.\n */\nexport interface BlockRow extends BlockAddress {\n /** Block type id (`note`, `paper`, …) of a Typed block, the node type (`paragraph`, `heading`, …) of any other Block. */\n type: string\n /** The title of a Typed block, the text of a heading; `''` for any other Block. */\n title: string\n /**\n * The Block properties of a Typed block: its `meta` without the derivation (which is `derivedFrom`); `{}` for any other Block.\n * A Block that holds an uploaded file (a media block, or a Typed block whose first body block is one) also has `sha256`,\n * the file's SHA-256 hex, which the import check compares dropped files with.\n */\n properties: Record<string, unknown>\n /** Tag names in the Block: its tags row (Typed block) and every `#` chip in its content, in document order, without repeats. */\n tags: string[]\n /** The reference chips in the Block's content, in document order, without repeats. */\n references: BlockReferenceRow[]\n /**\n * The Edges that leave the Block, without repeats: a `reference` per chip target, a `derivation` for a Derivation from another\n * Block, an `embed` per embedded Block, a `tag` per tag name, then the authored `meta.edges` (origin `meta`). Like the other\n * content fields it covers the Block's whole content, nested Blocks included.\n */\n edges: BlockEdgeRow[]\n /** What the Block was derived from; `null` for a block that carries no Derivation. */\n derivedFrom: BlockDerivationRow | null\n /** SHA-256 hex of the Block's content, the `pruefungHash` formula (see `ProjectOptions.markdown`). */\n hash: string\n /**\n * SHA-256 hex of what a Derivation made from this Block records as its `inputHash`: the Markdown of a Typed block's body\n * (without the title, unlike `hash`), the Markdown of any other Block (then equal to `hash`). A Derived block in another\n * Document compares it to tell whether its original changed.\n */\n sourceHash: string\n /** The plain text of the Block's content (a Typed block: description and body, the title is `title`); for search. */\n text: string\n}\n\n/** What of a Block `blockGet` can answer with besides the summary: the Block itself (`block`) or the Blocks nested in it (`children`). */\nexport type BlockContentMode = 'block' | 'children'\n\n/** The part of a row the block route answers with, and what the client's `index.blockGet` resolves to. */\nexport interface BlockSummary {\n title: string\n type: string\n hash: string\n /** The row's `sourceHash`: what a Derivation made from the Block records as `inputHash`. */\n sourceHash: string\n /** `true` when no such Block exists (a client turns the `404` into it); the server answers `false`. */\n gone: boolean\n /** The Block's nodes as TipTap JSON (`blockContentOf`), only when the request asked for a `BlockContentMode`; absent otherwise. */\n content?: JSONContent[]\n}\n\n/**\n * One hit of `BlockIndex.search`: the Block that matched, with what a result list shows of it. A Block without a title (a\n * paragraph, a list) has `title: ''`; `type` then says what it is.\n */\nexport interface BlockHit extends BlockAddress {\n title: string\n type: string\n /** How well the Block matched, higher is better. Comparable within one result only; the search route leaves it out. */\n score: number\n}\n\n/** A hit as the search route answers it and a client gets it: where the Block is, its title and type, in the order of the ranking. */\nexport type BlockSearchHit = Omit<BlockHit, 'score'>\n\n/** Markdown of `nodes` serialised as the content of one document. */\nexport type BlockMarkdown = (nodes: readonly JSONContent[]) => string\n\nexport interface ProjectOptions {\n /**\n * The Markdown serializer of the hash: `pruefungHash` is the SHA-256 of the title line, a blank line and the Markdown of the\n * body (of a Typed block), and the hash of any other Block is that of its Markdown. Core has no serializer (it needs the\n * schema's Markdown handlers), so a host passes one; without it the plain text takes its place (`title`, a blank line, the\n * body text) and the hash is only comparable with other hashes of the same projection.\n */\n markdown?: BlockMarkdown\n}\n\nconst CHROME: ReadonlySet<string> = new Set([TYPED_BLOCK_TITLE, TYPED_BLOCK_DESCRIPTION, TYPED_BLOCK_TAGS])\n/** The chrome of a Typed block that is not in its text: the title is its own field, the tags row is data. */\nconst NOT_TEXT: ReadonlySet<string> = new Set([TYPED_BLOCK_TITLE, TYPED_BLOCK_TAGS])\nconst INLINE: ReadonlySet<string> = new Set([TEXT_NODE, HARD_BREAK_NODE, MENTION_NODE, TAG_NODE, REFERENCE_NODE, CITE_NODE])\n\nconst attrString = (node: JSONContent, key: string): string => typeof node.attrs?.[key] === 'string' ? node.attrs[key] as string : ''\n\n/** The plain text of an inline node or a textblock's inline content. */\nfunction inlineText(node: JSONContent): string {\n switch (node.type) {\n case TEXT_NODE: return node.text ?? ''\n case HARD_BREAK_NODE: return '\\n'\n case MENTION_NODE: return `@${attrString(node, 'label') || attrString(node, 'id')}`\n case TAG_NODE: return `#${attrString(node, 'name')}`\n case REFERENCE_NODE: return attrString(node, 'label')\n case CITE_NODE: return `[cite:${attrString(node, 'key')}]`\n default: return (node.content ?? []).map(inlineText).join('')\n }\n}\n\nconst holdsInline = (node: JSONContent): boolean => node.content?.some(child => child.type !== undefined && INLINE.has(child.type)) === true\n\n/** The plain text of `node`'s content: one line per textblock, chrome of a Typed block left out. */\nfunction plainText(node: JSONContent): string {\n if (holdsInline(node)) return inlineText(node)\n return (node.content ?? []).filter(child => !(child.type !== undefined && NOT_TEXT.has(child.type))).map(plainText).filter(Boolean).join('\\n')\n}\n\nconst titleOf = (node: JSONContent): string => {\n if (isTypedBlock(node)) return (node.content ?? []).filter(child => child.type === TYPED_BLOCK_TITLE).map(inlineText).join('')\n return node.type === 'heading' ? inlineText(node) : ''\n}\n\n/** Collects the tag names, reference chips and embedded Blocks (`embeds`, by `documentId/blockId`) of `node`'s whole subtree. */\nfunction collect(node: JSONContent, documentId: string, tags: Set<string>, references: Map<string, BlockReferenceRow>, embeds: Map<string, BlockAddress>): void {\n if (node.type === TAG_NODE) {\n const name = attrString(node, 'name')\n if (name) tags.add(name)\n }\n else if (node.type === TYPED_BLOCK_TAGS && Array.isArray(node.attrs?.tags)) {\n for (const name of node.attrs.tags) if (typeof name === 'string' && name) tags.add(name)\n }\n else if (node.type === REFERENCE_NODE) {\n const blockId = attrString(node, 'target')\n if (blockId) {\n const target = attrString(node, 'documentId') || documentId\n const locator = attrString(node, 'locator') || null\n references.set(`${addressKey({ documentId: target, blockId })}\\u0000${locator ?? ''}`, { documentId: target, blockId, locator })\n }\n }\n else if (node.type === EMBED_NODE) {\n const blockId = attrString(node, 'blockId')\n if (blockId) {\n const target = attrString(node, 'documentId') || documentId\n embeds.set(addressKey({ documentId: target, blockId }), { documentId: target, blockId })\n }\n }\n for (const child of node.content ?? []) collect(child, documentId, tags, references, embeds)\n}\n\n/** The Edges of a Block (`BlockRow.edges`): content Edges in the order reference, derivation, embed, tag, then the authored ones; one per kind and target. */\nfunction edgesOf(documentId: string, references: Iterable<BlockReferenceRow>, derivedFrom: BlockDerivationRow | null, embeds: Iterable<BlockAddress>, tags: Iterable<string>, authored: readonly AuthoredEdge[]): BlockEdgeRow[] {\n const edges = new Map<string, BlockEdgeRow>()\n const add = (edge: BlockEdgeRow): void => {\n const target = isTagEdgeTarget(edge.to) ? `tag\\n${edge.to.tag}` : addressKey(edge.to)\n const key = `${edge.origin}\\n${edge.kind}\\n${target}`\n if (!edges.has(key)) edges.set(key, edge)\n }\n for (const { documentId: target, blockId } of references) add({ to: { documentId: target, blockId }, kind: 'reference', origin: 'content' })\n if (derivedFrom && 'blockId' in derivedFrom) add({ to: { documentId: derivedFrom.documentId, blockId: derivedFrom.blockId }, kind: 'derivation', origin: 'content' })\n for (const { documentId: target, blockId } of embeds) add({ to: { documentId: target, blockId }, kind: 'embed', origin: 'content' })\n for (const tag of tags) add({ to: { tag }, kind: 'tag', origin: 'content' })\n for (const { to, kind } of authored) add({ to: { documentId: to.documentId ?? documentId, blockId: to.blockId }, kind, origin: 'meta' })\n return [...edges.values()]\n}\n\n/** The `hash` and the `sourceHash` of `node`: for a Typed block the title line plus the body's Markdown, and the body's Markdown alone; for any other Block both the Block's Markdown. */\nfunction hashesOf(node: JSONContent, title: string, text: string, markdown: BlockMarkdown | undefined): { hash: string, sourceHash: string } {\n if (!isTypedBlock(node)) {\n const hash = sha256Hex(markdown ? markdown([node]) : text)\n return { hash, sourceHash: hash }\n }\n const body = (node.content ?? []).filter(child => !(child.type !== undefined && CHROME.has(child.type)))\n const bodyText = markdown ? markdown(body) : body.map(plainText).filter(Boolean).join('\\n')\n return { hash: sha256Hex(`${title}\\n\\n${bodyText}`), sourceHash: sha256Hex(bodyText) }\n}\n\nconst FILE_HASH = /^[0-9a-f]{64}$/i\n\n/** The SHA-256 of the uploaded file a Block holds: the `hash` of a media node (image, audio, video, file) itself, or of the first body block of a Typed block (its source file); `''` for a Block without one. */\nfunction fileHashOf(node: JSONContent): string {\n const source = isTypedBlock(node) ? node.content?.find(child => !(child.type !== undefined && CHROME.has(child.type))) : node\n const hash = source?.attrs?.hash\n return typeof hash === 'string' && FILE_HASH.test(hash) ? hash.toLowerCase() : ''\n}\n\nfunction rowOf(node: JSONContent, blockId: string, documentId: string, markdown: BlockMarkdown | undefined): BlockRow {\n const typed = isTypedBlock(node)\n const title = titleOf(node)\n const text = plainText(node)\n const tags = new Set<string>()\n const references = new Map<string, BlockReferenceRow>()\n const embeds = new Map<string, BlockAddress>()\n collect(node, documentId, tags, references, embeds)\n const derivation = derivationOf(node)\n const from = derivation?.from\n const { [DERIVATION_KEY]: _derivation, ...meta } = typed ? metaOf(node) : {}\n const sha256 = fileHashOf(node)\n // The file's SHA-256 is a property of the row, so a drop of the same file is found (`core/import-check`); an authored `sha256` property wins.\n const properties = sha256 && !('sha256' in meta) ? { ...meta, sha256 } : meta\n const derivedFrom: BlockDerivationRow | null = !from ? null : 'block' in from ? { documentId: from.document || documentId, blockId: from.block } : { asset: from.asset }\n return {\n documentId,\n blockId,\n type: typed ? typeIdOf(node) : node.type ?? '',\n title,\n properties,\n tags: [...tags],\n references: [...references.values()],\n edges: edgesOf(documentId, references.values(), derivedFrom, embeds.values(), tags, authoredEdgesOf(properties[META_EDGES_KEY])),\n derivedFrom,\n ...hashesOf(node, title, text, markdown),\n text,\n }\n}\n\n/**\n * The rows of a Document's content: one per node with a block ID, in document order (a Block before the Blocks nested in it).\n * `content` is the Document's TipTap JSON in the current shape; nodes without an ID, and the inline nodes inside a textblock\n * (a mention's `id` is a user's, not a block's), have no row. Pure: the same content and options give the same rows.\n */\nexport function projectDocument(content: JSONContent, documentId: string, options: ProjectOptions = {}): BlockRow[] {\n const rows: BlockRow[] = []\n const walk = (node: JSONContent): void => {\n if (holdsInline(node)) return\n for (const child of node.content ?? []) {\n const id = child.attrs?.id\n if (typeof id === 'string' && id) rows.push(rowOf(child, id, documentId, options.markdown))\n walk(child)\n }\n }\n walk(content)\n return rows\n}\n\n/** The Edges that leave `row`, each with the Block it is written in. */\nexport const blockEdgesOf = (row: Pick<BlockRow, 'documentId' | 'blockId' | 'edges'>): BlockEdge[] =>\n row.edges.map(edge => ({ from: { documentId: row.documentId, blockId: row.blockId }, ...structuredClone(edge) }))\n\n/** The Edges of `rows` that point at the Block `address` (`tag` Edges never do), in the order of the rows. */\nexport function edgesInto(rows: Iterable<Pick<BlockRow, 'documentId' | 'blockId' | 'edges'>>, address: BlockAddress): BlockEdge[] {\n const found: BlockEdge[] = []\n for (const row of rows) {\n for (const edge of row.edges) {\n if (isTagEdgeTarget(edge.to) || !sameAddress(edge.to, address)) continue\n found.push({ from: { documentId: row.documentId, blockId: row.blockId }, ...structuredClone(edge) })\n }\n }\n return found\n}\n\n/**\n * The nodes of Block `blockId` in a Document's content, as `blockContentOf` of the block route: `block` = the Block itself,\n * `children` = what it holds (a Typed block: its body without the title, description and tags rows; a list or a quote: its\n * items; a Block that holds only text: nothing). `null` when the content has no Block with that ID. Pure and a copy-free view:\n * the nodes are those of `content`.\n */\nexport function blockContentOf(content: JSONContent, blockId: string, mode: BlockContentMode): JSONContent[] | null {\n const found = findBlock(content, blockId)\n if (!found) return null\n if (mode === 'block') return [found]\n if (holdsInline(found)) return []\n return (found.content ?? []).filter(child => !(isTypedBlock(found) && child.type !== undefined && CHROME.has(child.type)))\n}\n\nfunction findBlock(node: JSONContent, blockId: string): JSONContent | null {\n if (holdsInline(node)) return null\n for (const child of node.content ?? []) {\n if (child.attrs?.id === blockId) return child\n const nested = findBlock(child, blockId)\n if (nested) return nested\n }\n return null\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"block-index.js","names":[],"sources":["../../src/core/block-index.ts"],"sourcesContent":["import type { JSONContent } from '@tiptap/core'\nimport { sha256Hex } from './hash'\nimport { DERIVATION_KEY, derivationOf, isTypedBlock, metaOf, TYPED_BLOCK_DESCRIPTION, TYPED_BLOCK_TAGS, TYPED_BLOCK_TITLE, typeIdOf } from './typed-block'\n\n// Framework-free and server-importable: a Document's blocks as rows of the workspace's Block index. `projectDocument` is pure\n// (JSON in, rows out); where the rows live is the `BlockIndex` port (`server/ports`) and its adapters.\n\n// Same names as `features/mentions` and `features/composites`; copied because core must not import features.\nconst TAG_NODE = 'tag'\nconst MENTION_NODE = 'mention'\n/** Node name of a reference chip; `features/composites` spells it the same. */\nexport const REFERENCE_NODE = 'reference'\nconst CITE_NODE = 'cite'\nconst HARD_BREAK_NODE = 'hardBreak'\nconst TEXT_NODE = 'text'\n\n/** Where a Block is: the Document and the block ID inside it. Block IDs are unique per Document only. */\nexport interface BlockAddress {\n documentId: string\n blockId: string\n}\n\n/** A string that names `address` and nothing else, for a `Map` or `Set` key; an address without a Document ID (`''`, or missing) is the same key. */\nexport const addressKey = (address: BlockAddress): string => `${address.documentId ?? ''}\\u0000${address.blockId}`\n\n/** Whether two addresses name the same Block; an address without a Document ID (`''`, or missing) equals another one without. */\nexport const sameAddress = (a: BlockAddress, b: BlockAddress): boolean => (a.documentId ?? '') === (b.documentId ?? '') && a.blockId === b.blockId\n\n/** A reference chip in a Block: the Block it points at (`documentId` resolved: a chip without one points into its own Document) and its locator. */\nexport interface BlockReferenceRow extends BlockAddress {\n /** Position inside the target's source (`S. 12`, `03:14`); `null` = the whole Block. */\n locator: string | null\n}\n\n/** What a Derived block was made from, as an address: another Block (`documentId` resolved like a chip's) or an uploaded asset by name. */\nexport type BlockDerivationRow = BlockAddress | { asset: string }\n\n/** The Block property that holds a Block's authored edges (`AuthoredEdge[]`); `projectDocument` turns them into `BlockEdgeRow`s of origin `meta`. */\nexport const META_EDGES_KEY = 'edges'\n\n// Same node name as `features/composites/embed`; copied because core must not import features.\nconst EMBED_NODE = 'blockEmbed'\n\n/** What an Edge says about its two ends. The four built-in kinds are projected from the content; any other string is an authored kind (`supports`, `contradicts`, …). */\nexport type EdgeKind = 'reference' | 'derivation' | 'embed' | 'tag' | (string & {})\n\n/** Where an Edge was written: in the Block's content (a chip, an embed, a tag, the Derivation) or by hand in its `meta.edges`. */\nexport type EdgeOrigin = 'content' | 'meta'\n\n/** One outgoing Edge of a Block as its row holds it: the Block it points at (`documentId` resolved), or a tag name for a `tag` Edge. */\nexport interface BlockEdgeRow {\n to: BlockAddress | { tag: string }\n kind: EdgeKind\n origin: EdgeOrigin\n}\n\n/** An Edge with both ends: what `BlockIndex.edges` answers. `from` is the Block the Edge is written in. */\nexport interface BlockEdge extends BlockEdgeRow {\n from: BlockAddress\n}\n\n/** An Edge a person added to a Typed block (`meta.edges`): the target Block (`documentId` absent = this Document) and its kind. */\nexport interface AuthoredEdge {\n to: { documentId?: string, blockId: string }\n kind: string\n}\n\n/** `true` for the Edge that points at a tag name instead of a Block. */\nexport const isTagEdgeTarget = (to: BlockEdgeRow['to']): to is { tag: string } => 'tag' in to\n\n/** The authored edges in a `meta.edges` value; entries without a block ID or a kind are dropped, anything but an array is `[]`. */\nexport function authoredEdgesOf(value: unknown): AuthoredEdge[] {\n if (!Array.isArray(value)) return []\n const edges: AuthoredEdge[] = []\n for (const entry of value as unknown[]) {\n const { to, kind } = (entry ?? {}) as { to?: { documentId?: unknown, blockId?: unknown }, kind?: unknown }\n if (typeof to?.blockId !== 'string' || !to.blockId || typeof kind !== 'string' || !kind) continue\n edges.push({ to: { ...(typeof to.documentId === 'string' && to.documentId && { documentId: to.documentId }), blockId: to.blockId }, kind })\n }\n return edges\n}\n\n/**\n * One Block of a Document as the index holds it. Every node that carries a block ID is a Block (the `id` attribute of the\n * stored JSON): Typed blocks, paragraphs, headings, lists, media. A Block without an ID has no address and no row.\n * The content fields (`text`, `tags`, `references`) cover the Block's whole content, nested Blocks included: a Typed block's row\n * answers for its body, and the nested Blocks have rows of their own.\n */\nexport interface BlockRow extends BlockAddress {\n /** Block type id (`note`, `paper`, …) of a Typed block, the node type (`paragraph`, `heading`, …) of any other Block. */\n type: string\n /** The title of a Typed block, the text of a heading; `''` for any other Block. */\n title: string\n /**\n * The Block properties of a Typed block: its `meta` without the derivation (which is `derivedFrom`); `{}` for any other Block.\n * A Block that holds an uploaded file (a media block, or a Typed block whose first body block is one) also has `sha256`,\n * the file's SHA-256 hex, which the import check compares dropped files with.\n */\n properties: Record<string, unknown>\n /** Tag names in the Block: its tags row (Typed block) and every `#` chip in its content, in document order, without repeats. */\n tags: string[]\n /** The reference chips in the Block's content, in document order, without repeats. */\n references: BlockReferenceRow[]\n /**\n * The Edges that leave the Block, without repeats: a `reference` per chip target, a `derivation` for a Derivation from another\n * Block, an `embed` per embedded Block, a `tag` per tag name, then the authored `meta.edges` (origin `meta`). Like the other\n * content fields it covers the Block's whole content, nested Blocks included.\n */\n edges: BlockEdgeRow[]\n /** What the Block was derived from; `null` for a block that carries no Derivation. */\n derivedFrom: BlockDerivationRow | null\n /** SHA-256 hex of the Block's content, the `pruefungHash` formula (see `ProjectOptions.markdown`). */\n hash: string\n /**\n * SHA-256 hex of what a Derivation made from this Block records as its `inputHash`: the Markdown of a Typed block's body\n * (without the title, unlike `hash`), the Markdown of any other Block (then equal to `hash`). A Derived block in another\n * Document compares it to tell whether its original changed.\n */\n sourceHash: string\n /** The plain text of the Block's content (a Typed block: description and body, the title is `title`); for search. */\n text: string\n}\n\n/** What of a Block `blockGet` can answer with besides the summary: the Block itself (`block`) or the Blocks nested in it (`children`). */\nexport type BlockContentMode = 'block' | 'children'\n\n/** The part of a row the block route answers with, and what the client's `index.blockGet` resolves to. */\nexport interface BlockSummary {\n title: string\n type: string\n hash: string\n /** The row's `sourceHash`: what a Derivation made from the Block records as `inputHash`. */\n sourceHash: string\n /** `true` when no such Block exists (a client turns the `404` into it); the server answers `false`. */\n gone: boolean\n /** The Block's nodes as TipTap JSON (`blockContentOf`), only when the request asked for a `BlockContentMode`; absent otherwise. */\n content?: JSONContent[]\n}\n\n/**\n * One hit of `BlockIndex.search`: the Block that matched, with what a result list shows of it. A Block without a title (a\n * paragraph, a list) has `title: ''`; `type` then says what it is.\n */\nexport interface BlockHit extends BlockAddress {\n title: string\n type: string\n /** How well the Block matched, higher is better. Comparable within one result only; the search route leaves it out. */\n score: number\n}\n\n/** A hit as the search route answers it and a client gets it: where the Block is, its title and type, in the order of the ranking. */\nexport type BlockSearchHit = Omit<BlockHit, 'score'>\n\n/** Markdown of `nodes` serialised as the content of one document. */\nexport type BlockMarkdown = (nodes: readonly JSONContent[]) => string\n\nexport interface ProjectOptions {\n /**\n * The Markdown serializer of the hash: `pruefungHash` is the SHA-256 of the title line, a blank line and the Markdown of the\n * body (of a Typed block), and the hash of any other Block is that of its Markdown. Core has no serializer (it needs the\n * schema's Markdown handlers), so a host passes one; without it the plain text takes its place (`title`, a blank line, the\n * body text) and the hash is only comparable with other hashes of the same projection.\n */\n markdown?: BlockMarkdown\n}\n\nconst CHROME: ReadonlySet<string> = new Set([TYPED_BLOCK_TITLE, TYPED_BLOCK_DESCRIPTION, TYPED_BLOCK_TAGS])\n/** The chrome of a Typed block that is not in its text: the title is its own field, the tags row is data. */\nconst NOT_TEXT: ReadonlySet<string> = new Set([TYPED_BLOCK_TITLE, TYPED_BLOCK_TAGS])\nconst INLINE: ReadonlySet<string> = new Set([TEXT_NODE, HARD_BREAK_NODE, MENTION_NODE, TAG_NODE, REFERENCE_NODE, CITE_NODE])\n\nconst attrString = (node: JSONContent, key: string): string => typeof node.attrs?.[key] === 'string' ? node.attrs[key] as string : ''\n\n/** The plain text of an inline node or a textblock's inline content. Internal: `document_outline` counts and titles Blocks with it. */\nexport function inlineText(node: JSONContent): string {\n switch (node.type) {\n case TEXT_NODE: return node.text ?? ''\n case HARD_BREAK_NODE: return '\\n'\n case MENTION_NODE: return `@${attrString(node, 'label') || attrString(node, 'id')}`\n case TAG_NODE: return `#${attrString(node, 'name')}`\n case REFERENCE_NODE: return attrString(node, 'label')\n case CITE_NODE: return `[cite:${attrString(node, 'key')}]`\n default: return (node.content ?? []).map(inlineText).join('')\n }\n}\n\nconst holdsInline = (node: JSONContent): boolean => node.content?.some(child => child.type !== undefined && INLINE.has(child.type)) === true\n\n/** The plain text of `node`'s content: one line per textblock, chrome of a Typed block left out. */\nfunction plainText(node: JSONContent): string {\n if (holdsInline(node)) return inlineText(node)\n return (node.content ?? []).filter(child => !(child.type !== undefined && NOT_TEXT.has(child.type))).map(plainText).filter(Boolean).join('\\n')\n}\n\n/** The title of a Block: a Typed block's title row, a heading's text, `''` for any other Block. Internal, like `inlineText`. */\nexport const titleOf = (node: JSONContent): string => {\n if (isTypedBlock(node)) return (node.content ?? []).filter(child => child.type === TYPED_BLOCK_TITLE).map(inlineText).join('')\n return node.type === 'heading' ? inlineText(node) : ''\n}\n\n/** Collects the tag names, reference chips and embedded Blocks (`embeds`, by `documentId/blockId`) of `node`'s whole subtree. */\nfunction collect(node: JSONContent, documentId: string, tags: Set<string>, references: Map<string, BlockReferenceRow>, embeds: Map<string, BlockAddress>): void {\n if (node.type === TAG_NODE) {\n const name = attrString(node, 'name')\n if (name) tags.add(name)\n }\n else if (node.type === TYPED_BLOCK_TAGS && Array.isArray(node.attrs?.tags)) {\n for (const name of node.attrs.tags) if (typeof name === 'string' && name) tags.add(name)\n }\n else if (node.type === REFERENCE_NODE) {\n const blockId = attrString(node, 'target')\n if (blockId) {\n const target = attrString(node, 'documentId') || documentId\n const locator = attrString(node, 'locator') || null\n references.set(`${addressKey({ documentId: target, blockId })}\\u0000${locator ?? ''}`, { documentId: target, blockId, locator })\n }\n }\n else if (node.type === EMBED_NODE) {\n const blockId = attrString(node, 'blockId')\n if (blockId) {\n const target = attrString(node, 'documentId') || documentId\n embeds.set(addressKey({ documentId: target, blockId }), { documentId: target, blockId })\n }\n }\n for (const child of node.content ?? []) collect(child, documentId, tags, references, embeds)\n}\n\n/** The Edges of a Block (`BlockRow.edges`): content Edges in the order reference, derivation, embed, tag, then the authored ones; one per kind and target. */\nfunction edgesOf(documentId: string, references: Iterable<BlockReferenceRow>, derivedFrom: BlockDerivationRow | null, embeds: Iterable<BlockAddress>, tags: Iterable<string>, authored: readonly AuthoredEdge[]): BlockEdgeRow[] {\n const edges = new Map<string, BlockEdgeRow>()\n const add = (edge: BlockEdgeRow): void => {\n const target = isTagEdgeTarget(edge.to) ? `tag\\n${edge.to.tag}` : addressKey(edge.to)\n const key = `${edge.origin}\\n${edge.kind}\\n${target}`\n if (!edges.has(key)) edges.set(key, edge)\n }\n for (const { documentId: target, blockId } of references) add({ to: { documentId: target, blockId }, kind: 'reference', origin: 'content' })\n if (derivedFrom && 'blockId' in derivedFrom) add({ to: { documentId: derivedFrom.documentId, blockId: derivedFrom.blockId }, kind: 'derivation', origin: 'content' })\n for (const { documentId: target, blockId } of embeds) add({ to: { documentId: target, blockId }, kind: 'embed', origin: 'content' })\n for (const tag of tags) add({ to: { tag }, kind: 'tag', origin: 'content' })\n for (const { to, kind } of authored) add({ to: { documentId: to.documentId ?? documentId, blockId: to.blockId }, kind, origin: 'meta' })\n return [...edges.values()]\n}\n\n/** The `hash` and the `sourceHash` of `node`: for a Typed block the title line plus the body's Markdown, and the body's Markdown alone; for any other Block both the Block's Markdown. */\nfunction hashesOf(node: JSONContent, title: string, text: string, markdown: BlockMarkdown | undefined): { hash: string, sourceHash: string } {\n if (!isTypedBlock(node)) {\n const hash = sha256Hex(markdown ? markdown([node]) : text)\n return { hash, sourceHash: hash }\n }\n const body = (node.content ?? []).filter(child => !(child.type !== undefined && CHROME.has(child.type)))\n const bodyText = markdown ? markdown(body) : body.map(plainText).filter(Boolean).join('\\n')\n return { hash: sha256Hex(`${title}\\n\\n${bodyText}`), sourceHash: sha256Hex(bodyText) }\n}\n\nconst FILE_HASH = /^[0-9a-f]{64}$/i\n\n/** The SHA-256 of the uploaded file a Block holds: the `hash` of a media node (image, audio, video, file) itself, or of the first body block of a Typed block (its source file); `''` for a Block without one. */\nfunction fileHashOf(node: JSONContent): string {\n const source = isTypedBlock(node) ? node.content?.find(child => !(child.type !== undefined && CHROME.has(child.type))) : node\n const hash = source?.attrs?.hash\n return typeof hash === 'string' && FILE_HASH.test(hash) ? hash.toLowerCase() : ''\n}\n\nfunction rowOf(node: JSONContent, blockId: string, documentId: string, markdown: BlockMarkdown | undefined): BlockRow {\n const typed = isTypedBlock(node)\n const title = titleOf(node)\n const text = plainText(node)\n const tags = new Set<string>()\n const references = new Map<string, BlockReferenceRow>()\n const embeds = new Map<string, BlockAddress>()\n collect(node, documentId, tags, references, embeds)\n const derivation = derivationOf(node)\n const from = derivation?.from\n const { [DERIVATION_KEY]: _derivation, ...meta } = typed ? metaOf(node) : {}\n const sha256 = fileHashOf(node)\n // The file's SHA-256 is a property of the row, so a drop of the same file is found (`core/import-check`); an authored `sha256` property wins.\n const properties = sha256 && !('sha256' in meta) ? { ...meta, sha256 } : meta\n const derivedFrom: BlockDerivationRow | null = !from ? null : 'block' in from ? { documentId: from.document || documentId, blockId: from.block } : { asset: from.asset }\n return {\n documentId,\n blockId,\n type: typed ? typeIdOf(node) : node.type ?? '',\n title,\n properties,\n tags: [...tags],\n references: [...references.values()],\n edges: edgesOf(documentId, references.values(), derivedFrom, embeds.values(), tags, authoredEdgesOf(properties[META_EDGES_KEY])),\n derivedFrom,\n ...hashesOf(node, title, text, markdown),\n text,\n }\n}\n\n/**\n * The rows of a Document's content: one per node with a block ID, in document order (a Block before the Blocks nested in it).\n * `content` is the Document's TipTap JSON in the current shape; nodes without an ID, and the inline nodes inside a textblock\n * (a mention's `id` is a user's, not a block's), have no row. Pure: the same content and options give the same rows.\n */\nexport function projectDocument(content: JSONContent, documentId: string, options: ProjectOptions = {}): BlockRow[] {\n const rows: BlockRow[] = []\n const walk = (node: JSONContent): void => {\n if (holdsInline(node)) return\n for (const child of node.content ?? []) {\n const id = child.attrs?.id\n if (typeof id === 'string' && id) rows.push(rowOf(child, id, documentId, options.markdown))\n walk(child)\n }\n }\n walk(content)\n return rows\n}\n\n/** The Edges that leave `row`, each with the Block it is written in. */\nexport const blockEdgesOf = (row: Pick<BlockRow, 'documentId' | 'blockId' | 'edges'>): BlockEdge[] =>\n row.edges.map(edge => ({ from: { documentId: row.documentId, blockId: row.blockId }, ...structuredClone(edge) }))\n\n/** The Edges of `rows` that point at the Block `address` (`tag` Edges never do), in the order of the rows. */\nexport function edgesInto(rows: Iterable<Pick<BlockRow, 'documentId' | 'blockId' | 'edges'>>, address: BlockAddress): BlockEdge[] {\n const found: BlockEdge[] = []\n for (const row of rows) {\n for (const edge of row.edges) {\n if (isTagEdgeTarget(edge.to) || !sameAddress(edge.to, address)) continue\n found.push({ from: { documentId: row.documentId, blockId: row.blockId }, ...structuredClone(edge) })\n }\n }\n return found\n}\n\n/**\n * The nodes of Block `blockId` in a Document's content, as `blockContentOf` of the block route: `block` = the Block itself,\n * `children` = what it holds (a Typed block: its body without the title, description and tags rows; a list or a quote: its\n * items; a Block that holds only text: nothing). `null` when the content has no Block with that ID. Pure and a copy-free view:\n * the nodes are those of `content`.\n */\nexport function blockContentOf(content: JSONContent, blockId: string, mode: BlockContentMode): JSONContent[] | null {\n const found = findBlock(content, blockId)\n if (!found) return null\n if (mode === 'block') return [found]\n if (holdsInline(found)) return []\n return (found.content ?? []).filter(child => !(isTypedBlock(found) && child.type !== undefined && CHROME.has(child.type)))\n}\n\nfunction findBlock(node: JSONContent, blockId: string): JSONContent | null {\n if (holdsInline(node)) return null\n for (const child of node.content ?? []) {\n if (child.attrs?.id === blockId) return child\n const nested = findBlock(child, blockId)\n if (nested) return nested\n }\n return null\n}\n"],"mappings":"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"}
|
|
@@ -33,16 +33,16 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
|
|
|
33
33
|
};
|
|
34
34
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
35
35
|
"update:modelValue": (text: string) => any;
|
|
36
|
-
ready: (target: DocumentSurfaceTarget) => any;
|
|
37
36
|
save: (text: string) => any;
|
|
37
|
+
ready: (target: DocumentSurfaceTarget) => any;
|
|
38
38
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
39
39
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
40
|
-
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
41
40
|
onSave?: ((text: string) => any) | undefined;
|
|
41
|
+
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
42
42
|
}>, {
|
|
43
43
|
editable: boolean;
|
|
44
|
-
modelValue: string;
|
|
45
44
|
wordWrap: boolean;
|
|
45
|
+
modelValue: string;
|
|
46
46
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
47
47
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
48
48
|
declare const _default: typeof __VLS_export;
|
|
@@ -2,7 +2,7 @@ import { AGENT_OP_OUTCOMES, AGENT_OP_TYPES, AgentOp, AgentOpFeedback, AgentOpOut
|
|
|
2
2
|
import { ModelProfile, ModelRef } from "../../core/backend-types.js";
|
|
3
3
|
import { BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, BlockLines, TRUNCATED_LINE, blockLine, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, listLinePrefix, nestedLinePrefix, quoteLinePrefix, readBlockLines, singleBlockLine, stripBlockLineEcho, stripListMarkerEcho } from "./protocol/block-lines.js";
|
|
4
4
|
import { AGENT_LIMITS, AGENT_OP_OUTCOME_TEXT, AGENT_TOOL, AgentOpContext, AgentOpProblem, AgentOpProblemReason, AgentTextOptions, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, checkAgentOps } from "./protocol/agent-ops.js";
|
|
5
|
-
import { CHAT_LIMITS, CITATION_HREF_PREFIX, ChatBlock, ChatBlockModel, ChatBlockType, ChatToolCall, ChatToolInfo, DocumentChatMessage, citationBlockId, documentChatMessages } from "./protocol/chat.js";
|
|
5
|
+
import { CHAT_LIMITS, CITATION_HREF_PREFIX, ChatBlock, ChatBlockModel, ChatBlockType, ChatToolCall, ChatToolInfo, DocumentChatMessage, citationBlockId, documentChatMessages, documentChatSystem } from "./protocol/chat.js";
|
|
6
6
|
import { AI_LIMITS, AI_TASKS, AI_TASK_IDS, AiApplyMode, AiBlockInput, AiInput, AiMessage, AiTask, AiTaskId, blockTasks, inBlockTasks, withHeading, withTaskHeading } from "./protocol/tasks.js";
|
|
7
7
|
import { ChatMessage, ChatStream, GenerateInput, generateBodySchema, generateProblem, runGenerate } from "./protocol/generate.js";
|
|
8
8
|
import { AI_COMPLETE_LIMITS, AiCompleteMessage, cleanCompletion, completeBodySchema, completeMessages, completionReady, runCompletion } from "./protocol/complete.js";
|
|
@@ -27,4 +27,4 @@ import { isRateLimited, notifyAiRateLimited } from "./rate-limit.js";
|
|
|
27
27
|
import { AiReviewAnchor, AiReviewBarModel, AiReviewBarOptions, AiReviewBarState, createAiReviewBar } from "./review-bar-model.js";
|
|
28
28
|
import { AiSidebarFlow, AiSidebarFlowOptions, createAiSidebarFlow } from "./sidebar-flow.js";
|
|
29
29
|
import { AI_SELECTION_MENU_KEY, AiSelectionFlow, AiSelectionFlowOptions, AiSelectionMode, AiSelectionRequest, AiSelectionState, createAiSelectionFlow } from "./selection-flow.js";
|
|
30
|
-
export { AGENT_LIMITS, AGENT_OP_OUTCOMES, AGENT_OP_OUTCOME_TEXT, AGENT_OP_TYPES, AGENT_TOOL, AI_COMPLETE_LIMITS, AI_ID_PATTERN, AI_LANGUAGE_KEYS, AI_LIMITS, AI_MENU_AT_CARET, AI_MENU_FOR_SELECTION, AI_SELECTION_MENU_KEY, AI_TASKS, AI_TASK_IDS, type AgentOp, AgentOpContext, type AgentOpFeedback, type AgentOpOutcome, AgentOpProblem, AgentOpProblemReason, type AgentOpReview, type AgentOpReviewOptions, type AgentOpReviewState, type AgentOpState, type AgentOpType, type AgentScope, AgentTextOptions, AiApplyMode, type AiApplyOutcome, type AiAssistant, type AiAssistantOptions, type AiAssistantState, AiBlockInput, AiCompleteMessage, AiInput, type AiMenuAction, AiMessage, type AiResultAction, AiReview, type AiReviewAnchor, type AiReviewBarModel, type AiReviewBarOptions, type AiReviewBarState, type AiReviewChange, type AiReviewChangeSummary, type AiReviewDecision, type AiReviewHandle, type AiReviewHunk, type AiReviewOptions, type AiReviewOutcome, type AiReviewSkipReason, type AiReviewState, type AiRunBody, type AiSelectionFlow, type AiSelectionFlowOptions, AiSelectionHighlight, type AiSelectionMode, type AiSelectionRequest, type AiSelectionState, type AiSidebarFlow, type AiSidebarFlowOptions, type AiSseOutcome, type AiStatus, type AiTarget, AiTask, AiTaskId, type AiUi, BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, BlockLines, CHAT_LIMITS, CITATION_HREF_PREFIX, ChatBlock, ChatBlockModel, ChatBlockType, ChatMessage, ChatStream, ChatToolCall, ChatToolInfo, type DocumentChat, type DocumentChatGeneration, DocumentChatMessage, type DocumentChatOptions, type DocumentChatState, type DocumentChatTurn, GenerateInput, type GhostAutocompleteOptions, MODEL_REF_LIMITS, MODEL_ROLE_KEYS, type ModelAgentOp, type ModelProfile, type ModelProfilesModel, type ModelRef, TRUNCATED_LINE, UserModelProfile, acceptAiReview, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, aiApplyFailure, aiBlockAction, aiBlockInputSchema, aiBlockSchema, aiCommand, aiFeatureCore, aiMenuActionsOf, aiReviewKey, aiSelectionHighlightKey, applyAiResult, blockLine, blockTasks, blockTextRange, captureTarget, checkAgentOps, citationBlockId, cleanCompletion, completeBodySchema, completeMessages, completionFetcher, completionReady, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, createAgentOpReview, createAiAssistant, createAiReviewBar, createAiSelectionFlow, createAiSidebarFlow, createDocumentChat, documentChatMessages, documentChatText, documentMarkdown, formatTaskResult, generateBodySchema, generateProblem, ghostAutocomplete, ghostAutocompleteKey, hideAiSelectionHighlight, inBlockTasks, insertAtCaretLine, insertBlocksAtTarget, insertBlocksBelow, isRateLimited, listLinePrefix, modelProfiles, modelRefSchema, nestedLinePrefix, notifyAiRateLimited, openAiMenuAtCaret, openAiMenuForSelection, postAiSse, quoteLinePrefix, rangeMarkdown, readBlockLines, rejectAiReview, resultBlocks, runCompletion, runGenerate, selectionMarkdown, selectionScope, setActiveAiReviewHunk, showAiSelectionHighlight, singleBlockLine, startAiReview, stripBlockLineEcho, stripListMarkerEcho, withHeading, withTaskHeading };
|
|
30
|
+
export { AGENT_LIMITS, AGENT_OP_OUTCOMES, AGENT_OP_OUTCOME_TEXT, AGENT_OP_TYPES, AGENT_TOOL, AI_COMPLETE_LIMITS, AI_ID_PATTERN, AI_LANGUAGE_KEYS, AI_LIMITS, AI_MENU_AT_CARET, AI_MENU_FOR_SELECTION, AI_SELECTION_MENU_KEY, AI_TASKS, AI_TASK_IDS, type AgentOp, AgentOpContext, type AgentOpFeedback, type AgentOpOutcome, AgentOpProblem, AgentOpProblemReason, type AgentOpReview, type AgentOpReviewOptions, type AgentOpReviewState, type AgentOpState, type AgentOpType, type AgentScope, AgentTextOptions, AiApplyMode, type AiApplyOutcome, type AiAssistant, type AiAssistantOptions, type AiAssistantState, AiBlockInput, AiCompleteMessage, AiInput, type AiMenuAction, AiMessage, type AiResultAction, AiReview, type AiReviewAnchor, type AiReviewBarModel, type AiReviewBarOptions, type AiReviewBarState, type AiReviewChange, type AiReviewChangeSummary, type AiReviewDecision, type AiReviewHandle, type AiReviewHunk, type AiReviewOptions, type AiReviewOutcome, type AiReviewSkipReason, type AiReviewState, type AiRunBody, type AiSelectionFlow, type AiSelectionFlowOptions, AiSelectionHighlight, type AiSelectionMode, type AiSelectionRequest, type AiSelectionState, type AiSidebarFlow, type AiSidebarFlowOptions, type AiSseOutcome, type AiStatus, type AiTarget, AiTask, AiTaskId, type AiUi, BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, BlockLines, CHAT_LIMITS, CITATION_HREF_PREFIX, ChatBlock, ChatBlockModel, ChatBlockType, ChatMessage, ChatStream, ChatToolCall, ChatToolInfo, type DocumentChat, type DocumentChatGeneration, DocumentChatMessage, type DocumentChatOptions, type DocumentChatState, type DocumentChatTurn, GenerateInput, type GhostAutocompleteOptions, MODEL_REF_LIMITS, MODEL_ROLE_KEYS, type ModelAgentOp, type ModelProfile, type ModelProfilesModel, type ModelRef, TRUNCATED_LINE, UserModelProfile, acceptAiReview, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, aiApplyFailure, aiBlockAction, aiBlockInputSchema, aiBlockSchema, aiCommand, aiFeatureCore, aiMenuActionsOf, aiReviewKey, aiSelectionHighlightKey, applyAiResult, blockLine, blockTasks, blockTextRange, captureTarget, checkAgentOps, citationBlockId, cleanCompletion, completeBodySchema, completeMessages, completionFetcher, completionReady, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, createAgentOpReview, createAiAssistant, createAiReviewBar, createAiSelectionFlow, createAiSidebarFlow, createDocumentChat, documentChatMessages, documentChatSystem, documentChatText, documentMarkdown, formatTaskResult, generateBodySchema, generateProblem, ghostAutocomplete, ghostAutocompleteKey, hideAiSelectionHighlight, inBlockTasks, insertAtCaretLine, insertBlocksAtTarget, insertBlocksBelow, isRateLimited, listLinePrefix, modelProfiles, modelRefSchema, nestedLinePrefix, notifyAiRateLimited, openAiMenuAtCaret, openAiMenuForSelection, postAiSse, quoteLinePrefix, rangeMarkdown, readBlockLines, rejectAiReview, resultBlocks, runCompletion, runGenerate, selectionMarkdown, selectionScope, setActiveAiReviewHunk, showAiSelectionHighlight, singleBlockLine, startAiReview, stripBlockLineEcho, stripListMarkerEcho, withHeading, withTaskHeading };
|
|
@@ -6,7 +6,7 @@ import { generateBodySchema, generateProblem, runGenerate } from "./protocol/gen
|
|
|
6
6
|
import { AGENT_OP_OUTCOMES, AGENT_OP_TYPES } from "../../core/agent-op-types.js";
|
|
7
7
|
import { BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, TRUNCATED_LINE, blockLine, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, listLinePrefix, nestedLinePrefix, quoteLinePrefix, readBlockLines, singleBlockLine, stripBlockLineEcho, stripListMarkerEcho } from "./protocol/block-lines.js";
|
|
8
8
|
import { AGENT_LIMITS, AGENT_OP_OUTCOME_TEXT, AGENT_TOOL, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, checkAgentOps } from "./protocol/agent-ops.js";
|
|
9
|
-
import { CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, documentChatMessages } from "./protocol/chat.js";
|
|
9
|
+
import { CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, documentChatMessages, documentChatSystem } from "./protocol/chat.js";
|
|
10
10
|
import { aiApplyFailure, applyAiResult, captureTarget, documentMarkdown, formatTaskResult, insertAtCaretLine, insertBlocksAtTarget, insertBlocksBelow, rangeMarkdown, resultBlocks, selectionMarkdown } from "./ai-apply.js";
|
|
11
11
|
import { AiReview, acceptAiReview, aiReviewKey, rejectAiReview, setActiveAiReviewHunk, startAiReview } from "./ai-review.js";
|
|
12
12
|
import { AiSelectionHighlight, aiSelectionHighlightKey, hideAiSelectionHighlight, showAiSelectionHighlight } from "./ai-selection-highlight.js";
|
|
@@ -25,4 +25,4 @@ import { createAiSidebarFlow } from "./sidebar-flow.js";
|
|
|
25
25
|
import { documentChatText } from "./document-chat.js";
|
|
26
26
|
import { createAgentOpReview } from "./agent-op-review.js";
|
|
27
27
|
import { createDocumentChat } from "./document-chat-model.js";
|
|
28
|
-
export { AGENT_LIMITS, AGENT_OP_OUTCOMES, AGENT_OP_OUTCOME_TEXT, AGENT_OP_TYPES, AGENT_TOOL, AI_COMPLETE_LIMITS, AI_ID_PATTERN, AI_LANGUAGE_KEYS, AI_LIMITS, AI_MENU_AT_CARET, AI_MENU_FOR_SELECTION, AI_SELECTION_MENU_KEY, AI_TASKS, AI_TASK_IDS, AiReview, AiSelectionHighlight, BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, CHAT_LIMITS, CITATION_HREF_PREFIX, MODEL_REF_LIMITS, MODEL_ROLE_KEYS, TRUNCATED_LINE, acceptAiReview, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, aiApplyFailure, aiBlockAction, aiBlockInputSchema, aiBlockSchema, aiCommand, aiFeatureCore, aiMenuActionsOf, aiReviewKey, aiSelectionHighlightKey, applyAiResult, blockLine, blockTasks, blockTextRange, captureTarget, checkAgentOps, citationBlockId, cleanCompletion, completeBodySchema, completeMessages, completionFetcher, completionReady, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, createAgentOpReview, createAiAssistant, createAiReviewBar, createAiSelectionFlow, createAiSidebarFlow, createDocumentChat, documentChatMessages, documentChatText, documentMarkdown, formatTaskResult, generateBodySchema, generateProblem, ghostAutocomplete, ghostAutocompleteKey, hideAiSelectionHighlight, inBlockTasks, insertAtCaretLine, insertBlocksAtTarget, insertBlocksBelow, isRateLimited, listLinePrefix, modelProfiles, modelRefSchema, nestedLinePrefix, notifyAiRateLimited, openAiMenuAtCaret, openAiMenuForSelection, postAiSse, quoteLinePrefix, rangeMarkdown, readBlockLines, rejectAiReview, resultBlocks, runCompletion, runGenerate, selectionMarkdown, selectionScope, setActiveAiReviewHunk, showAiSelectionHighlight, singleBlockLine, startAiReview, stripBlockLineEcho, stripListMarkerEcho, withHeading, withTaskHeading };
|
|
28
|
+
export { AGENT_LIMITS, AGENT_OP_OUTCOMES, AGENT_OP_OUTCOME_TEXT, AGENT_OP_TYPES, AGENT_TOOL, AI_COMPLETE_LIMITS, AI_ID_PATTERN, AI_LANGUAGE_KEYS, AI_LIMITS, AI_MENU_AT_CARET, AI_MENU_FOR_SELECTION, AI_SELECTION_MENU_KEY, AI_TASKS, AI_TASK_IDS, AiReview, AiSelectionHighlight, BLOCK_LINES_LABEL, BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_EXAMPLE_TEXT, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_IDS_HINT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, CHAT_LIMITS, CITATION_HREF_PREFIX, MODEL_REF_LIMITS, MODEL_ROLE_KEYS, TRUNCATED_LINE, acceptAiReview, agentFeedbackText, agentOpSummary, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, aiApplyFailure, aiBlockAction, aiBlockInputSchema, aiBlockSchema, aiCommand, aiFeatureCore, aiMenuActionsOf, aiReviewKey, aiSelectionHighlightKey, applyAiResult, blockLine, blockTasks, blockTextRange, captureTarget, checkAgentOps, citationBlockId, cleanCompletion, completeBodySchema, completeMessages, completionFetcher, completionReady, compositeChromePart, compositeLineClose, compositeLineHeader, continuedLinePrefix, createAgentOpReview, createAiAssistant, createAiReviewBar, createAiSelectionFlow, createAiSidebarFlow, createDocumentChat, documentChatMessages, documentChatSystem, documentChatText, documentMarkdown, formatTaskResult, generateBodySchema, generateProblem, ghostAutocomplete, ghostAutocompleteKey, hideAiSelectionHighlight, inBlockTasks, insertAtCaretLine, insertBlocksAtTarget, insertBlocksBelow, isRateLimited, listLinePrefix, modelProfiles, modelRefSchema, nestedLinePrefix, notifyAiRateLimited, openAiMenuAtCaret, openAiMenuForSelection, postAiSse, quoteLinePrefix, rangeMarkdown, readBlockLines, rejectAiReview, resultBlocks, runCompletion, runGenerate, selectionMarkdown, selectionScope, setActiveAiReviewHunk, showAiSelectionHighlight, singleBlockLine, startAiReview, stripBlockLineEcho, stripListMarkerEcho, withHeading, withTaskHeading };
|
|
@@ -63,6 +63,11 @@ export interface ChatBlockModel {
|
|
|
63
63
|
targets: readonly string[];
|
|
64
64
|
structure: StructureMode;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The system prompt of the document chat: answering with citations, editing through `edit_document`, reading the workspace
|
|
68
|
+
* with the registered tools, and the editor's block model. The built-in loop and a host's `ChatAgent` (`ChatAgentTurn.system`) use it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function documentChatSystem(blockModel: ChatBlockModel, tools: readonly ChatToolInfo[]): string;
|
|
66
71
|
/**
|
|
67
72
|
* Model messages for a chat turn: the history as sent (read-only tool calls and their results, proposed operations as
|
|
68
73
|
* `edit_document` tool calls), with the current document placed in the last (user) message next to the scope, the latest
|
|
@@ -75,8 +75,11 @@ function readToolsText(tools) {
|
|
|
75
75
|
`Blöcke aus anderen Dokumenten belegst du ohne Link, mit wörtlichem Zitat und Herkunft: „Zitat“ (Dokument <documentId>, Block <blockId>). Links mit „${CITATION_HREF_PREFIX}“ gelten nur für Blöcke des geöffneten Dokuments.`
|
|
76
76
|
];
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The system prompt of the document chat: answering with citations, editing through `edit_document`, reading the workspace
|
|
80
|
+
* with the registered tools, and the editor's block model. The built-in loop and a host's `ChatAgent` (`ChatAgentTurn.system`) use it.
|
|
81
|
+
*/
|
|
82
|
+
function documentChatSystem(blockModel, tools) {
|
|
80
83
|
return [
|
|
81
84
|
"Du beantwortest Fragen zu einem Dokument, meist dem Transkript eines Gesprächs, und bearbeitest es auf Wunsch. Das Dokument steht in der letzten Nachricht der Nutzerin bzw. des Nutzers und ist der aktuelle Stand; frühere Nachrichten bezogen sich womöglich auf einen älteren Stand.",
|
|
82
85
|
BLOCK_LINE_INTRO,
|
|
@@ -182,7 +185,7 @@ function documentChatMessages(messages, document, scope, blockModel, tools = [])
|
|
|
182
185
|
return [
|
|
183
186
|
{
|
|
184
187
|
role: "system",
|
|
185
|
-
content:
|
|
188
|
+
content: documentChatSystem(blockModel, tools)
|
|
186
189
|
},
|
|
187
190
|
...history,
|
|
188
191
|
{
|
|
@@ -197,6 +200,6 @@ function documentChatMessages(messages, document, scope, blockModel, tools = [])
|
|
|
197
200
|
];
|
|
198
201
|
}
|
|
199
202
|
//#endregion
|
|
200
|
-
export { CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, documentChatMessages };
|
|
203
|
+
export { CHAT_LIMITS, CITATION_HREF_PREFIX, citationBlockId, documentChatMessages, documentChatSystem };
|
|
201
204
|
|
|
202
205
|
//# sourceMappingURL=chat.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.js","names":[],"sources":["../../../../src/features/ai/protocol/chat.ts"],"sourcesContent":["// i18n-ignore-file: model-facing chat system prompt and messages (German prompts stay German whatever the editor's language)\nimport type { ModelMessage } from 'ai'\nimport type { AiBlock, AiContextBlockType } from '../../../core/feature'\nimport type { StructureMode } from '../../../settings'\nimport { AGENT_TOOL, type AgentOp, type AgentOpFeedback, agentFeedbackText, agentShownMessage } from './agent-ops'\nimport { BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, BLOCK_LINES_LABEL } from './block-lines'\n\n/**\n * One turn of the document chat; the conversation lives on the client and is sent in full with every message.\n * A user turn carries the decisions on earlier proposals made since the previous turn (`feedback`); an assistant turn\n * the edit operations it proposed (`ops`, possibly without any text), how many invalid ones were dropped, and the\n * read-only tool calls the server ran for it (`tools`, in call order).\n */\nexport type DocumentChatMessage =\n | { role: 'user', content: string, feedback?: AgentOpFeedback[] }\n | { role: 'assistant', content: string, ops?: AgentOp[], dropped?: number, tools?: ChatToolCall[] }\n\n/** A read-only tool call the server ran while answering (`find_blocks`, `read_block`, …): the `tool` event of the chat stream, and what the history replays. */\nexport interface ChatToolCall {\n name: string\n input: Record<string, unknown>\n /** The text the model read as the result (cut to `CHAT_LIMITS.toolOutput`). */\n output: string\n}\n\n/** A registered read-only tool as the system prompt lists it. */\nexport interface ChatToolInfo { name: string, description: string }\n\n/** Input limits (characters / messages) of `POST /api/ai/chat`; the client trims to fit before sending. `tools`: tool calls kept per assistant turn; `toolOutput`: characters of one tool result. */\nexport const CHAT_LIMITS = { document: 120000, message: 8000, messages: 40, tools: 40, toolOutput: 8000 } as const\n\n/** Link target prefix of a quote citation: `[„Zitat“](#block-<id>)`. */\nexport const CITATION_HREF_PREFIX = '#block-'\n\n/**\n * The block ID a citation link points to, or `null` for any other href. Also accepts a bare `#<id>`, which models\n * write now and then despite the instructions; whether the ID exists is up to the caller.\n */\nexport function citationBlockId(href: unknown): string | null {\n if (typeof href !== 'string' || !href.startsWith('#')) return null\n return (href.startsWith(CITATION_HREF_PREFIX) ? href.slice(CITATION_HREF_PREFIX.length) : href.slice(1)) || null\n}\n\n/** A Block type as the chat model learns it: the client sends the Block types of the editor's `aiContext` with every message. */\nexport type ChatBlockType = Pick<AiContextBlockType, 'id' | 'label' | 'fields' | 'purpose'>\ntype ChatField = ChatBlockType['fields'][number]\n\n/** A host-defined block as the chat model learns it: the editor's `aiContext.blocks`, sent with every message. */\nexport type ChatBlock = AiBlock\n\n/**\n * What the model may build and convert into: the editor's Block types and host blocks, the `convert` targets it\n * offers, and the user's preferred structure of new content (settings `blocks.structure`).\n */\nexport interface ChatBlockModel { blockTypes: readonly ChatBlockType[], blocks: readonly ChatBlock[], targets: readonly string[], structure: StructureMode }\n\nconst FIELD_TYPES: Record<NonNullable<ChatField['type']>, string> = { text: 'Text', textarea: 'Langtext', date: 'Datum JJJJ-MM-TT', number: 'Zahl', select: 'Auswahl', multiselect: 'Mehrfachauswahl', reference: 'Block-ID eines typisierten Blocks', bibliography: 'Literaturangabe als CSL-JSON-Objekt', derivation: 'Herkunft, nur lesen', checkbox: 'Ja/Nein als true oder false', url: 'Web-Adresse', status: 'Status', 'reference-list': 'Block-Verweise als JSON-Liste aus {documentId, blockId}', 'source-anchor': 'PDF-Stelle, nur lesen' }\n\n/** `status (Status: open = Offen, doing = In Arbeit, done = Erledigt)`, `due (Fällig, Datum JJJJ-MM-TT)`. */\nfunction fieldText(field: ChatField): string {\n const values = field.options?.length ? `: ${field.options.map(option => `${option.value} = ${option.label}`).join(', ')}${field.type === 'multiselect' ? `; ${FIELD_TYPES.multiselect}, mehrere Werte als JSON-Liste` : ''}` : `, ${FIELD_TYPES[field.type ?? 'text']}`\n return `${field.key} (${field.label}${values})`\n}\n\n/** „Blockmodell“: the document's line format for composites and host blocks holding blocks, the fence the model writes composites in, and `convert`. */\nfunction blockModelText({ blockTypes, blocks, targets, structure }: ChatBlockModel): string[] {\n const composites = structure === 'structured' && blockTypes.length > 0\n const example = blockTypes.find(blockType => blockType.fields.some(field => field.options?.length)) ?? blockTypes[0]\n const exampleField = example?.fields.find(field => field.options?.length)\n const exampleMeta = exampleField ? ` {\"${exampleField.key}\":\"${exampleField.options!.at(-1)!.value}\"}` : ''\n return [\n 'Blockmodell:',\n `Blocktypen: Absatz, Überschrift (#, ##, ###), ${BLOCK_LINE_MARKERS}, Codeblock, Redebeitrag („**Name:** Text“) und typisierte Blöcke. ${BLOCK_LINE_LIST_TEXT}`,\n ...(blocks.length\n ? [\n `Eigene Blöcke: ${blocks.map(block => `${block.id} (${block.label}): ${block.description} Markdown-Form: „${block.syntax.replace(/\\n/g, '\\\\n')}“. Beispiel: „${block.example.replace(/\\n/g, '\\\\n')}“`).join(' ')}`,\n `${BLOCK_LINE_HOST_TEXT} „update“ auf seine ID mit vollständigem Markdown ersetzt den ganzen Block; „add“ nach einem seiner Inhaltsblöcke fügt innerhalb des Blocks ein, ein Block derselben Art danach.`,\n ]\n : []),\n composites\n ? 'Bevorzugte Gliederung: typisierte Blöcke. Neue Inhalte gliederst du mit typisierten Blöcken statt mit Überschriften; Unterabschnitte sind verschachtelte typisierte Blöcke (Zaun im Zaun, jeder Zaun endet mit einer eigenen Zeile „:::“). Markdown-Überschriften schreibst du nur als Titelzeile eines typisierten Blocks.'\n : 'Bevorzugte Gliederung: Überschriften. Neue Inhalte gliederst du mit Überschriften (##, ###) statt mit typisierten Blöcken; einen typisierten Block schreibst du nur, wenn ausdrücklich ein Block (Abschnitt, Aufgabe, Notiz …) verlangt wird oder du einen bestehenden typisierten Block änderst.',\n ...(blockTypes.length\n ? [\n `Typisierte Blöcke sind Blöcke mit Titel, Beschreibung, Tags, Feldern und eigenen Inhaltsblöcken. ${BLOCK_LINE_COMPOSITE_TEXT}`,\n `Blocktypen der typisierten Blöcke: ${blockTypes.map(blockType => `${blockType.id} (${blockType.label}${blockType.purpose ? `: ${blockType.purpose}` : ''}; ${blockType.fields.length ? `Felder: ${blockType.fields.map(fieldText).join(', ')}` : 'keine Felder'})`).join('; ')}.`,\n `In „markdown“ schreibst du einen typisierten Block ohne IDs und ohne Einrückung als Zaun: die Zeile „::: Blocktyp {\"feld\":\"wert\"}“ (Felder als JSON, nur gesetzte), dann „## Titel“, „> Beschreibung“ und „Tags: #a #b“ (jeweils nur, wenn vorhanden), dann die Inhaltsblöcke als Markdown, zuletzt die Zeile „:::“. Beispiel: „::: ${example?.id ?? 'note'}${exampleMeta}\\\\n## Angebot prüfen\\\\n- Preise vergleichen\\\\n- Rückmeldung an Anna\\\\n:::“.`,\n '„update“ auf einen typisierten Block mit Markdown ohne :::-Zaun ersetzt nur seinen Inhalt (Kinder); mit Zaun den ganzen Block (Blocktyp, Felder, Titel, Beschreibung, Tags und Inhalt).',\n ]\n : []),\n `„convert“ wandelt den Block „id“ in den Blocktyp „to“ um und behält seinen Inhalt. Erlaubte Werte für „to“: ${targets.join(', ')}. Eine Überschrift, die zum typisierten Block wird, nimmt die folgenden Blöcke bis zur nächsten Überschrift gleicher oder höherer Ebene als Inhalt mit; ein typisierter Block, der zu einem anderen Blocktyp wird, behält Beschreibung, Tags und Felder als Textzeilen; wird er zur Überschrift, werden seine inneren typisierten Blöcke zu Unterüberschriften.`,\n 'Sollen sich Blocktyp und Inhalt oder Felder zugleich ändern (z. B. ein Absatz wird zu einer Aufgabe mit Status), nimm statt „convert“ ein „update“ mit dem vollständigen neuen Markdown samt :::-Zaun; jeder Block bekommt höchstens eine Operation.',\n ]\n}\n\n/**\n * „Werkzeuge zum Lesen“: the registered read-only tools and how to cite what they return. Citation links only reach blocks of the\n * open document (the chat resolves them there), so a block of another Document is cited by naming its Document and block.\n */\nfunction readToolsText(tools: readonly ChatToolInfo[]): string[] {\n if (!tools.length) return []\n return [\n '',\n 'Werkzeuge zum Lesen:',\n 'Du kannst den ganzen Arbeitsbereich lesen, nicht nur das geöffnete Dokument. Steht die Antwort nicht im geöffneten Dokument oder fragt die Nutzerin bzw. der Nutzer nach anderen Dokumenten, suche und lies zuerst mit diesen Werkzeugen, ohne vorher nachzufragen und bevor du sagst, dass du sie nicht kennst. Für Fragen, die das geöffnete Dokument beantwortet, brauchst du sie nicht. Die Ergebnisse siehst nur du.',\n ...tools.map(({ name, description }) => `- ${name}${description ? `: ${description}` : ''}`),\n `Blöcke aus anderen Dokumenten belegst du ohne Link, mit wörtlichem Zitat und Herkunft: „Zitat“ (Dokument <documentId>, Block <blockId>). Links mit „${CITATION_HREF_PREFIX}“ gelten nur für Blöcke des geöffneten Dokuments.`,\n ]\n}\n\n/** The system prompt: answering with citations, editing through `edit_document`, reading the workspace with the registered tools, and the editor's block model. */\nfunction chatSystem(blockModel: ChatBlockModel, tools: readonly ChatToolInfo[]): string {\n return [\n 'Du beantwortest Fragen zu einem Dokument, meist dem Transkript eines Gesprächs, und bearbeitest es auf Wunsch. Das Dokument steht in der letzten Nachricht der Nutzerin bzw. des Nutzers und ist der aktuelle Stand; frühere Nachrichten bezogen sich womöglich auf einen älteren Stand.',\n BLOCK_LINE_INTRO,\n '',\n 'Fragen:',\n `Belege deine Antworten mit wörtlichen Zitaten: ein zusammenhängender, unveränderter Ausschnitt aus einem einzigen Block. Jedes Zitat ist ein Markdown-Link, dessen Ziel „${CITATION_HREF_PREFIX}“ gefolgt von der vollständigen ID dieses Blocks ist: [„Zitat“](${CITATION_HREF_PREFIX}ID).`,\n `Beispiel zur Zeile oben: Anna kündigt den Start an: [„Wir starten im Mai.“](${CITATION_HREF_PREFIX}${BLOCK_LINE_EXAMPLE_ID})`,\n 'Verwende nur IDs, die im Dokument vorkommen, und nenne IDs nie außerhalb solcher Links.',\n 'Steht die Antwort nicht im Dokument, sag das ehrlich, statt etwas zu erfinden.',\n 'Antworte knapp in Markdown (Absätze, Aufzählungen, **fett**) und in der Sprache der Frage.',\n ...readToolsText(tools),\n '',\n 'Änderungen:',\n `Bittet die Nutzerin bzw. der Nutzer darum, das Dokument zu ändern (kürzen, umformulieren, ergänzen, löschen, umwandeln …), rufe das Werkzeug ${AGENT_TOOL} auf, statt den neuen Text im Chat zu schreiben. Schreib davor höchstens ein bis zwei Sätze dazu, was du änderst. Bei Fragen rufst du das Werkzeug nicht auf.`,\n 'Operationen: „update“ ersetzt den Inhalt des Blocks „id“ durch „markdown“; „delete“ löscht den Block „id“; „add“ fügt „markdown“ (ein oder mehrere Blöcke, durch Leerzeilen getrennt) nach dem Block „afterId“ ein („afterId“ gehört bei „add“ immer dazu, für den Dokumentanfang schreibst du ausdrücklich null; mehrere „add“ nach demselben Block werden in der Reihenfolge eingefügt, in der du sie schreibst); „convert“ wandelt den Block „id“ in den Blocktyp „to“ um.',\n `${BLOCK_LINE_ECHO_TEXT} Redebeiträge behalten den fett gesetzten Namen mit Doppelpunkt am Anfang; ein anderer Name wechselt die Sprecherin bzw. den Sprecher. Neue Redebeiträge schreibst du ebenso als „**Name:** Text“.`,\n 'Ändere nur die Blöcke, die die Bitte betrifft; lass unveränderte Blöcke weg. Jeder Block höchstens einmal; eine Operation auf einen Block innerhalb eines Blocks, den eine andere Operation ersetzt oder löscht, ist ungültig (schreibe die Änderung dann in das Markdown der äußeren Operation). Verwende nur IDs aus dem Dokument. Nennt die Nachricht einen Bearbeitungsbereich, beziehen sich alle IDs und afterIds ausschließlich auf Blöcke daraus.',\n 'Jede Änderung wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt und einzeln übernommen oder abgelehnt. Die Entscheidungen erhältst du mit einer späteren Nachricht („Entscheidungen zu deinen letzten Vorschlägen: …“). Geh auf abgelehnte Vorschläge ein und wiederhole sie nicht unverändert.',\n '',\n ...blockModelText(blockModel),\n ].join('\\n')\n}\n\n/** A user turn as the model reads it: decisions on earlier proposals first, then the message. */\nfunction userText(content: string, feedback: readonly AgentOpFeedback[] | undefined): string {\n const decisions = agentFeedbackText(feedback ?? [])\n return decisions ? `${decisions}\\n\\nNachricht: ${content}` : content\n}\n\n/** The model messages that replay the read-only tool calls of assistant turn `index`: the calls, then their results. Empty without calls. */\nfunction toolReplay(tools: readonly ChatToolCall[] | undefined, index: number): ModelMessage[] {\n if (!tools?.length) return []\n const id = (i: number) => `read_${index}_${i}`\n return [\n { role: 'assistant', content: tools.map((call, i) => ({ type: 'tool-call' as const, toolCallId: id(i), toolName: call.name, input: call.input })) },\n { role: 'tool', content: tools.map((call, i) => ({ type: 'tool-result' as const, toolCallId: id(i), toolName: call.name, output: { type: 'text' as const, value: call.output } })) },\n ]\n}\n\n/**\n * Model messages for a chat turn: the history as sent (read-only tool calls and their results, proposed operations as\n * `edit_document` tool calls), with the current document placed in the last (user) message next to the scope, the latest\n * decisions and the message — document content never goes into the system prompt (the editor's block model does).\n * `scope`: IDs the edits are limited to (null: whole document). `tools`: the registered read-only tools the system prompt lists.\n */\nexport function documentChatMessages(messages: readonly DocumentChatMessage[], document: string, scope: readonly string[] | null, blockModel: ChatBlockModel, tools: readonly ChatToolInfo[] = []): ModelMessage[] {\n const history = messages.slice(0, -1).flatMap((message, index): ModelMessage[] => {\n if (message.role === 'user') return [{ role: 'user', content: userText(message.content, message.feedback) }]\n const reads = toolReplay(message.tools, index)\n if (!message.ops?.length && !message.dropped) return [...reads, ...(message.content ? [{ role: 'assistant' as const, content: message.content }] : [])]\n const ops = message.ops ?? []\n const toolCallId = `edit_${index}`\n return [\n ...reads,\n {\n role: 'assistant',\n content: [\n ...(message.content ? [{ type: 'text' as const, text: message.content }] : []),\n { type: 'tool-call', toolCallId, toolName: AGENT_TOOL, input: { ops: ops.map(({ op: _op, ...rest }) => rest) } },\n ],\n },\n {\n role: 'tool',\n content: [{ type: 'tool-result', toolCallId, toolName: AGENT_TOOL, output: { type: 'text', value: agentShownMessage(ops, message.dropped ?? 0) } }],\n },\n ]\n })\n const last = messages.at(-1)! as Extract<DocumentChatMessage, { role: 'user' }>\n const range = scope\n ? `Bearbeitungsbereich: die Auswahl der Nutzerin bzw. des Nutzers. Änderungen (id und afterId) nur an diesen Blöcken: ${scope.join(', ')}`\n : 'Bearbeitungsbereich: das ganze Dokument.'\n const decisions = agentFeedbackText(last.feedback ?? [])\n return [\n { role: 'system', content: chatSystem(blockModel, tools) },\n ...history,\n {\n role: 'user',\n content: [\n `${BLOCK_LINES_LABEL}:\\n\"\"\"\\n${document}\\n\"\"\"`,\n range,\n ...(decisions ? [decisions] : []),\n `Nachricht: ${last.content}`,\n ].join('\\n\\n'),\n },\n ]\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat.js","names":[],"sources":["../../../../src/features/ai/protocol/chat.ts"],"sourcesContent":["// i18n-ignore-file: model-facing chat system prompt and messages (German prompts stay German whatever the editor's language)\nimport type { ModelMessage } from 'ai'\nimport type { AiBlock, AiContextBlockType } from '../../../core/feature'\nimport type { StructureMode } from '../../../settings'\nimport { AGENT_TOOL, type AgentOp, type AgentOpFeedback, agentFeedbackText, agentShownMessage } from './agent-ops'\nimport { BLOCK_LINE_COMPOSITE_TEXT, BLOCK_LINE_ECHO_TEXT, BLOCK_LINE_EXAMPLE_ID, BLOCK_LINE_HOST_TEXT, BLOCK_LINE_INTRO, BLOCK_LINE_LIST_TEXT, BLOCK_LINE_MARKERS, BLOCK_LINES_LABEL } from './block-lines'\n\n/**\n * One turn of the document chat; the conversation lives on the client and is sent in full with every message.\n * A user turn carries the decisions on earlier proposals made since the previous turn (`feedback`); an assistant turn\n * the edit operations it proposed (`ops`, possibly without any text), how many invalid ones were dropped, and the\n * read-only tool calls the server ran for it (`tools`, in call order).\n */\nexport type DocumentChatMessage =\n | { role: 'user', content: string, feedback?: AgentOpFeedback[] }\n | { role: 'assistant', content: string, ops?: AgentOp[], dropped?: number, tools?: ChatToolCall[] }\n\n/** A read-only tool call the server ran while answering (`find_blocks`, `read_block`, …): the `tool` event of the chat stream, and what the history replays. */\nexport interface ChatToolCall {\n name: string\n input: Record<string, unknown>\n /** The text the model read as the result (cut to `CHAT_LIMITS.toolOutput`). */\n output: string\n}\n\n/** A registered read-only tool as the system prompt lists it. */\nexport interface ChatToolInfo { name: string, description: string }\n\n/** Input limits (characters / messages) of `POST /api/ai/chat`; the client trims to fit before sending. `tools`: tool calls kept per assistant turn; `toolOutput`: characters of one tool result. */\nexport const CHAT_LIMITS = { document: 120000, message: 8000, messages: 40, tools: 40, toolOutput: 8000 } as const\n\n/** Link target prefix of a quote citation: `[„Zitat“](#block-<id>)`. */\nexport const CITATION_HREF_PREFIX = '#block-'\n\n/**\n * The block ID a citation link points to, or `null` for any other href. Also accepts a bare `#<id>`, which models\n * write now and then despite the instructions; whether the ID exists is up to the caller.\n */\nexport function citationBlockId(href: unknown): string | null {\n if (typeof href !== 'string' || !href.startsWith('#')) return null\n return (href.startsWith(CITATION_HREF_PREFIX) ? href.slice(CITATION_HREF_PREFIX.length) : href.slice(1)) || null\n}\n\n/** A Block type as the chat model learns it: the client sends the Block types of the editor's `aiContext` with every message. */\nexport type ChatBlockType = Pick<AiContextBlockType, 'id' | 'label' | 'fields' | 'purpose'>\ntype ChatField = ChatBlockType['fields'][number]\n\n/** A host-defined block as the chat model learns it: the editor's `aiContext.blocks`, sent with every message. */\nexport type ChatBlock = AiBlock\n\n/**\n * What the model may build and convert into: the editor's Block types and host blocks, the `convert` targets it\n * offers, and the user's preferred structure of new content (settings `blocks.structure`).\n */\nexport interface ChatBlockModel { blockTypes: readonly ChatBlockType[], blocks: readonly ChatBlock[], targets: readonly string[], structure: StructureMode }\n\nconst FIELD_TYPES: Record<NonNullable<ChatField['type']>, string> = { text: 'Text', textarea: 'Langtext', date: 'Datum JJJJ-MM-TT', number: 'Zahl', select: 'Auswahl', multiselect: 'Mehrfachauswahl', reference: 'Block-ID eines typisierten Blocks', bibliography: 'Literaturangabe als CSL-JSON-Objekt', derivation: 'Herkunft, nur lesen', checkbox: 'Ja/Nein als true oder false', url: 'Web-Adresse', status: 'Status', 'reference-list': 'Block-Verweise als JSON-Liste aus {documentId, blockId}', 'source-anchor': 'PDF-Stelle, nur lesen' }\n\n/** `status (Status: open = Offen, doing = In Arbeit, done = Erledigt)`, `due (Fällig, Datum JJJJ-MM-TT)`. */\nfunction fieldText(field: ChatField): string {\n const values = field.options?.length ? `: ${field.options.map(option => `${option.value} = ${option.label}`).join(', ')}${field.type === 'multiselect' ? `; ${FIELD_TYPES.multiselect}, mehrere Werte als JSON-Liste` : ''}` : `, ${FIELD_TYPES[field.type ?? 'text']}`\n return `${field.key} (${field.label}${values})`\n}\n\n/** „Blockmodell“: the document's line format for composites and host blocks holding blocks, the fence the model writes composites in, and `convert`. */\nfunction blockModelText({ blockTypes, blocks, targets, structure }: ChatBlockModel): string[] {\n const composites = structure === 'structured' && blockTypes.length > 0\n const example = blockTypes.find(blockType => blockType.fields.some(field => field.options?.length)) ?? blockTypes[0]\n const exampleField = example?.fields.find(field => field.options?.length)\n const exampleMeta = exampleField ? ` {\"${exampleField.key}\":\"${exampleField.options!.at(-1)!.value}\"}` : ''\n return [\n 'Blockmodell:',\n `Blocktypen: Absatz, Überschrift (#, ##, ###), ${BLOCK_LINE_MARKERS}, Codeblock, Redebeitrag („**Name:** Text“) und typisierte Blöcke. ${BLOCK_LINE_LIST_TEXT}`,\n ...(blocks.length\n ? [\n `Eigene Blöcke: ${blocks.map(block => `${block.id} (${block.label}): ${block.description} Markdown-Form: „${block.syntax.replace(/\\n/g, '\\\\n')}“. Beispiel: „${block.example.replace(/\\n/g, '\\\\n')}“`).join(' ')}`,\n `${BLOCK_LINE_HOST_TEXT} „update“ auf seine ID mit vollständigem Markdown ersetzt den ganzen Block; „add“ nach einem seiner Inhaltsblöcke fügt innerhalb des Blocks ein, ein Block derselben Art danach.`,\n ]\n : []),\n composites\n ? 'Bevorzugte Gliederung: typisierte Blöcke. Neue Inhalte gliederst du mit typisierten Blöcken statt mit Überschriften; Unterabschnitte sind verschachtelte typisierte Blöcke (Zaun im Zaun, jeder Zaun endet mit einer eigenen Zeile „:::“). Markdown-Überschriften schreibst du nur als Titelzeile eines typisierten Blocks.'\n : 'Bevorzugte Gliederung: Überschriften. Neue Inhalte gliederst du mit Überschriften (##, ###) statt mit typisierten Blöcken; einen typisierten Block schreibst du nur, wenn ausdrücklich ein Block (Abschnitt, Aufgabe, Notiz …) verlangt wird oder du einen bestehenden typisierten Block änderst.',\n ...(blockTypes.length\n ? [\n `Typisierte Blöcke sind Blöcke mit Titel, Beschreibung, Tags, Feldern und eigenen Inhaltsblöcken. ${BLOCK_LINE_COMPOSITE_TEXT}`,\n `Blocktypen der typisierten Blöcke: ${blockTypes.map(blockType => `${blockType.id} (${blockType.label}${blockType.purpose ? `: ${blockType.purpose}` : ''}; ${blockType.fields.length ? `Felder: ${blockType.fields.map(fieldText).join(', ')}` : 'keine Felder'})`).join('; ')}.`,\n `In „markdown“ schreibst du einen typisierten Block ohne IDs und ohne Einrückung als Zaun: die Zeile „::: Blocktyp {\"feld\":\"wert\"}“ (Felder als JSON, nur gesetzte), dann „## Titel“, „> Beschreibung“ und „Tags: #a #b“ (jeweils nur, wenn vorhanden), dann die Inhaltsblöcke als Markdown, zuletzt die Zeile „:::“. Beispiel: „::: ${example?.id ?? 'note'}${exampleMeta}\\\\n## Angebot prüfen\\\\n- Preise vergleichen\\\\n- Rückmeldung an Anna\\\\n:::“.`,\n '„update“ auf einen typisierten Block mit Markdown ohne :::-Zaun ersetzt nur seinen Inhalt (Kinder); mit Zaun den ganzen Block (Blocktyp, Felder, Titel, Beschreibung, Tags und Inhalt).',\n ]\n : []),\n `„convert“ wandelt den Block „id“ in den Blocktyp „to“ um und behält seinen Inhalt. Erlaubte Werte für „to“: ${targets.join(', ')}. Eine Überschrift, die zum typisierten Block wird, nimmt die folgenden Blöcke bis zur nächsten Überschrift gleicher oder höherer Ebene als Inhalt mit; ein typisierter Block, der zu einem anderen Blocktyp wird, behält Beschreibung, Tags und Felder als Textzeilen; wird er zur Überschrift, werden seine inneren typisierten Blöcke zu Unterüberschriften.`,\n 'Sollen sich Blocktyp und Inhalt oder Felder zugleich ändern (z. B. ein Absatz wird zu einer Aufgabe mit Status), nimm statt „convert“ ein „update“ mit dem vollständigen neuen Markdown samt :::-Zaun; jeder Block bekommt höchstens eine Operation.',\n ]\n}\n\n/**\n * „Werkzeuge zum Lesen“: the registered read-only tools and how to cite what they return. Citation links only reach blocks of the\n * open document (the chat resolves them there), so a block of another Document is cited by naming its Document and block.\n */\nfunction readToolsText(tools: readonly ChatToolInfo[]): string[] {\n if (!tools.length) return []\n return [\n '',\n 'Werkzeuge zum Lesen:',\n 'Du kannst den ganzen Arbeitsbereich lesen, nicht nur das geöffnete Dokument. Steht die Antwort nicht im geöffneten Dokument oder fragt die Nutzerin bzw. der Nutzer nach anderen Dokumenten, suche und lies zuerst mit diesen Werkzeugen, ohne vorher nachzufragen und bevor du sagst, dass du sie nicht kennst. Für Fragen, die das geöffnete Dokument beantwortet, brauchst du sie nicht. Die Ergebnisse siehst nur du.',\n ...tools.map(({ name, description }) => `- ${name}${description ? `: ${description}` : ''}`),\n `Blöcke aus anderen Dokumenten belegst du ohne Link, mit wörtlichem Zitat und Herkunft: „Zitat“ (Dokument <documentId>, Block <blockId>). Links mit „${CITATION_HREF_PREFIX}“ gelten nur für Blöcke des geöffneten Dokuments.`,\n ]\n}\n\n/**\n * The system prompt of the document chat: answering with citations, editing through `edit_document`, reading the workspace\n * with the registered tools, and the editor's block model. The built-in loop and a host's `ChatAgent` (`ChatAgentTurn.system`) use it.\n */\nexport function documentChatSystem(blockModel: ChatBlockModel, tools: readonly ChatToolInfo[]): string {\n return [\n 'Du beantwortest Fragen zu einem Dokument, meist dem Transkript eines Gesprächs, und bearbeitest es auf Wunsch. Das Dokument steht in der letzten Nachricht der Nutzerin bzw. des Nutzers und ist der aktuelle Stand; frühere Nachrichten bezogen sich womöglich auf einen älteren Stand.',\n BLOCK_LINE_INTRO,\n '',\n 'Fragen:',\n `Belege deine Antworten mit wörtlichen Zitaten: ein zusammenhängender, unveränderter Ausschnitt aus einem einzigen Block. Jedes Zitat ist ein Markdown-Link, dessen Ziel „${CITATION_HREF_PREFIX}“ gefolgt von der vollständigen ID dieses Blocks ist: [„Zitat“](${CITATION_HREF_PREFIX}ID).`,\n `Beispiel zur Zeile oben: Anna kündigt den Start an: [„Wir starten im Mai.“](${CITATION_HREF_PREFIX}${BLOCK_LINE_EXAMPLE_ID})`,\n 'Verwende nur IDs, die im Dokument vorkommen, und nenne IDs nie außerhalb solcher Links.',\n 'Steht die Antwort nicht im Dokument, sag das ehrlich, statt etwas zu erfinden.',\n 'Antworte knapp in Markdown (Absätze, Aufzählungen, **fett**) und in der Sprache der Frage.',\n ...readToolsText(tools),\n '',\n 'Änderungen:',\n `Bittet die Nutzerin bzw. der Nutzer darum, das Dokument zu ändern (kürzen, umformulieren, ergänzen, löschen, umwandeln …), rufe das Werkzeug ${AGENT_TOOL} auf, statt den neuen Text im Chat zu schreiben. Schreib davor höchstens ein bis zwei Sätze dazu, was du änderst. Bei Fragen rufst du das Werkzeug nicht auf.`,\n 'Operationen: „update“ ersetzt den Inhalt des Blocks „id“ durch „markdown“; „delete“ löscht den Block „id“; „add“ fügt „markdown“ (ein oder mehrere Blöcke, durch Leerzeilen getrennt) nach dem Block „afterId“ ein („afterId“ gehört bei „add“ immer dazu, für den Dokumentanfang schreibst du ausdrücklich null; mehrere „add“ nach demselben Block werden in der Reihenfolge eingefügt, in der du sie schreibst); „convert“ wandelt den Block „id“ in den Blocktyp „to“ um.',\n `${BLOCK_LINE_ECHO_TEXT} Redebeiträge behalten den fett gesetzten Namen mit Doppelpunkt am Anfang; ein anderer Name wechselt die Sprecherin bzw. den Sprecher. Neue Redebeiträge schreibst du ebenso als „**Name:** Text“.`,\n 'Ändere nur die Blöcke, die die Bitte betrifft; lass unveränderte Blöcke weg. Jeder Block höchstens einmal; eine Operation auf einen Block innerhalb eines Blocks, den eine andere Operation ersetzt oder löscht, ist ungültig (schreibe die Änderung dann in das Markdown der äußeren Operation). Verwende nur IDs aus dem Dokument. Nennt die Nachricht einen Bearbeitungsbereich, beziehen sich alle IDs und afterIds ausschließlich auf Blöcke daraus.',\n 'Jede Änderung wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt und einzeln übernommen oder abgelehnt. Die Entscheidungen erhältst du mit einer späteren Nachricht („Entscheidungen zu deinen letzten Vorschlägen: …“). Geh auf abgelehnte Vorschläge ein und wiederhole sie nicht unverändert.',\n '',\n ...blockModelText(blockModel),\n ].join('\\n')\n}\n\n/** A user turn as the model reads it: decisions on earlier proposals first, then the message. */\nfunction userText(content: string, feedback: readonly AgentOpFeedback[] | undefined): string {\n const decisions = agentFeedbackText(feedback ?? [])\n return decisions ? `${decisions}\\n\\nNachricht: ${content}` : content\n}\n\n/** The model messages that replay the read-only tool calls of assistant turn `index`: the calls, then their results. Empty without calls. */\nfunction toolReplay(tools: readonly ChatToolCall[] | undefined, index: number): ModelMessage[] {\n if (!tools?.length) return []\n const id = (i: number) => `read_${index}_${i}`\n return [\n { role: 'assistant', content: tools.map((call, i) => ({ type: 'tool-call' as const, toolCallId: id(i), toolName: call.name, input: call.input })) },\n { role: 'tool', content: tools.map((call, i) => ({ type: 'tool-result' as const, toolCallId: id(i), toolName: call.name, output: { type: 'text' as const, value: call.output } })) },\n ]\n}\n\n/**\n * Model messages for a chat turn: the history as sent (read-only tool calls and their results, proposed operations as\n * `edit_document` tool calls), with the current document placed in the last (user) message next to the scope, the latest\n * decisions and the message — document content never goes into the system prompt (the editor's block model does).\n * `scope`: IDs the edits are limited to (null: whole document). `tools`: the registered read-only tools the system prompt lists.\n */\nexport function documentChatMessages(messages: readonly DocumentChatMessage[], document: string, scope: readonly string[] | null, blockModel: ChatBlockModel, tools: readonly ChatToolInfo[] = []): ModelMessage[] {\n const history = messages.slice(0, -1).flatMap((message, index): ModelMessage[] => {\n if (message.role === 'user') return [{ role: 'user', content: userText(message.content, message.feedback) }]\n const reads = toolReplay(message.tools, index)\n if (!message.ops?.length && !message.dropped) return [...reads, ...(message.content ? [{ role: 'assistant' as const, content: message.content }] : [])]\n const ops = message.ops ?? []\n const toolCallId = `edit_${index}`\n return [\n ...reads,\n {\n role: 'assistant',\n content: [\n ...(message.content ? [{ type: 'text' as const, text: message.content }] : []),\n { type: 'tool-call', toolCallId, toolName: AGENT_TOOL, input: { ops: ops.map(({ op: _op, ...rest }) => rest) } },\n ],\n },\n {\n role: 'tool',\n content: [{ type: 'tool-result', toolCallId, toolName: AGENT_TOOL, output: { type: 'text', value: agentShownMessage(ops, message.dropped ?? 0) } }],\n },\n ]\n })\n const last = messages.at(-1)! as Extract<DocumentChatMessage, { role: 'user' }>\n const range = scope\n ? `Bearbeitungsbereich: die Auswahl der Nutzerin bzw. des Nutzers. Änderungen (id und afterId) nur an diesen Blöcken: ${scope.join(', ')}`\n : 'Bearbeitungsbereich: das ganze Dokument.'\n const decisions = agentFeedbackText(last.feedback ?? [])\n return [\n { role: 'system', content: documentChatSystem(blockModel, tools) },\n ...history,\n {\n role: 'user',\n content: [\n `${BLOCK_LINES_LABEL}:\\n\"\"\"\\n${document}\\n\"\"\"`,\n range,\n ...(decisions ? [decisions] : []),\n `Nachricht: ${last.content}`,\n ].join('\\n\\n'),\n },\n ]\n}\n"],"mappings":"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"}
|