Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["@tiptap/*"]],
"fixed": [["@tiptap/*", "!@tiptap/server-ai-toolkit"]],
"linked": [],
"access": "public",
"baseBranch": "main",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/young-mugs-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/server-ai-toolkit': minor
---

Initial open-source release of the Server AI Toolkit package
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ jobs:
run: tar -xzf /tmp/build-output/build-output.tar.gz

- name: Test ${{ matrix.test-spec.name }}
uses: cypress-io/github-action@v7.1.8
uses: cypress-io/github-action@v7.3.0
with:
install: false
start: pnpm exec http-server ./demos/dist -s -p 3000
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
#### Patch Changes

- a4e5154: Fix `DragHandle` unmounts by rendering children into the plugin-managed drag handle element with a React portal.

This avoids React trying to remove a host node after the drag handle plugin has moved it into its own wrapper.

- @tiptap/extension-drag-handle@3.23.1
- @tiptap/pm@3.23.1
- @tiptap/react@3.23.1
Expand Down
18 changes: 18 additions & 0 deletions packages/server-ai-toolkit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# @tiptap/server-ai-toolkit

[![Version](https://img.shields.io/npm/v/@tiptap/server-ai-toolkit.svg?label=version)](https://www.npmjs.com/package/@tiptap/server-ai-toolkit)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/server-ai-toolkit.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/server-ai-toolkit.svg)](https://www.npmjs.com/package/@tiptap/server-ai-toolkit)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)

## Introduction

Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.

## Official Documentation

Documentation can be found on the [Tiptap website](https://tiptap.dev).

## License

Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
58 changes: 58 additions & 0 deletions packages/server-ai-toolkit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@tiptap/server-ai-toolkit",
"description": "Integrate AI capabilities into the Tiptap editor. This package contains the SDK for connecting to the Server AI Toolkit service.",
"version": "0.0.0",
"homepage": "https://tiptap.dev/docs/editor/extensions/functionality/server-ai-toolkit",
"keywords": [
"tiptap",
"tiptap ai",
"ai toolkit",
"server",
"server ai toolkit"
],
"license": "MIT",
"type": "module",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"exports": {
".": {
"types": {
"import": "./dist/index.d.ts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "workspace:*",
"@tiptap/pm": "workspace:*"
},
"devDependencies": {
"@tiptap/core": "workspace:^",
"@tiptap/pm": "workspace:^"
},
"dependencies": {
"es-toolkit": "^1.41.0",
"zod": "^4.1.8"
},
"repository": {
"type": "git",
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/server-ai-toolkit"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch --sourcemap",
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Extension } from '@tiptap/core'

/**
* The attribute name used to store the hash on nodes.
*/
const ATTRIBUTE_NAME = '_hash'

/**
* Checks whether the editor already includes the full {@link AiToolkit} extension.
*
* @param extensionNames - Extension names available in the current editor setup.
* @return `true` when the AI Toolkit extension is installed.
*/
function hasAiToolkitExtension(extensionNames: string[]): boolean {
return extensionNames.includes('aiToolkit')
}

/**
* A Tiptap extension that registers `_hash` on non-inline nodes.
*
* The server package only contributes the attribute definition when AI Toolkit
* itself is not already installed.
*/
export const ServerAiToolkitHashExtension = Extension.create({
name: 'serverAiToolkitHash',

/**
* Registers the hash attribute on all non-inline, non-text, non-doc node types.
*
* @return The global attribute configuration.
*/
addGlobalAttributes() {
if (hasAiToolkitExtension(this.extensions.map(extension => extension.name))) {
return []
}

const types = this.extensions
.filter(ext => {
if (ext.name === 'text' || ext.name === 'doc' || ext.name === 'tableHeader' || ext.name === 'tableCell') {
return false
}
if (typeof ext.config?.group === 'string' && ext.config.group.includes('inline')) {
return false
}
return true
})
.map(ext => ext.name)

return [
{
types,
attributes: {
[ATTRIBUTE_NAME]: {
default: null,
parseHTML: (element: HTMLElement) => element.getAttribute(ATTRIBUTE_NAME),
renderHTML: (attributes: Record<string, unknown>) => {
if (!attributes[ATTRIBUTE_NAME]) {
return {}
}

return {
[ATTRIBUTE_NAME]: attributes[ATTRIBUTE_NAME],
}
},
},
},
},
]
},
})
2 changes: 2 additions & 0 deletions packages/server-ai-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './schema-awareness/index.js'
export * from './server-ai-toolkit-extension.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { type Editor, getExtensionField, Mark } from '@tiptap/core'
import { z } from 'zod'

import type { EditorContext } from './types/editor-context.js'
import type { AddJsonSchemaAwareness, JsonItem } from './types/json-item.js'
import { defaultJsonItems } from './utils/default-json-items.js'
import { mergeJsonItems } from './utils/merge-json-items.js'
import { serializeSchema } from './utils/serialize-schema.js'

/**
* Options for {@link getEditorContext}.
*/
export interface GetEditorContextOptions {
/**
* Custom schema awareness items to include in addition to the default ones.
*
* This lets callers extend the serialized editor context with custom node
* definitions or overrides.
* @default []
*/
customNodes?: JsonItem[]
}

/**
* Returns editor context data including a serialized schema and merged custom nodes.
*
* This function collects schema information from extensions and converts it to a format
* suitable for AI model consumption. Zod schemas in attributes are converted to JSON schemas.
*
* @param editor - The Tiptap editor instance
* @param options - Configuration options for the editor context
* @return An object containing the serialized schema and merged custom nodes with JSON schema attributes
*/
export function getEditorContext(editor: Editor, options: GetEditorContextOptions = {}): EditorContext {
const extensionNamesSet = new Set(editor.extensionManager.extensions.map(ext => ext.name))

const customNodesFromExtensions = editor.extensionManager.extensions.flatMap(extension => {
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
editor,
}
const addJsonSchemaAwareness = getExtensionField<AddJsonSchemaAwareness>(
extension,
'addJsonSchemaAwareness',
context,
)
if (addJsonSchemaAwareness) {
return {
...addJsonSchemaAwareness(),
isMark: extension instanceof Mark,
extensionName: extension.name,
}
}
return []
})

const items = [defaultJsonItems, customNodesFromExtensions, options.customNodes ?? []]
.reduce(mergeJsonItems, [])
.filter((item: JsonItem) => extensionNamesSet.has(item.extensionName))

// Convert Zod schemas to JSON schemas
const mergedCustomNodes = items.map((item: JsonItem) => {
const { attributes, ...rest } = item
if (!attributes) {
return rest
}

const attributesSchema = z.object(attributes)
const jsonSchema = z.toJSONSchema(attributesSchema)

return {
...rest,
attributes: jsonSchema,
}
})

return {
serializedSchema: serializeSchema(editor.schema),
items: mergedCustomNodes,
}
}
3 changes: 3 additions & 0 deletions packages/server-ai-toolkit/src/schema-awareness/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './get-editor-context.js'
export * from './legacy-exports.js'
export * from './types/index.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { GetEditorContextOptions as GetSchemaAwarenessDataOptions } from './get-editor-context.js'
export { getEditorContext as getSchemaAwarenessData } from './get-editor-context.js'
export type { EditorContext as SchemaAwarenessData } from './types/editor-context.js'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { SerializedSchema } from '../utils/serialize-schema.js'
import type { SerializedJsonItem } from './json-item.js'

/**
* Result of {@link getEditorContext} containing information about the
* editor's extensions that the Server AI Toolkit needs to work.
*/
export interface EditorContext {
/**
* The serialized ProseMirror schema
*/
serializedSchema: SerializedSchema
/**
* The merged custom nodes with Zod schemas converted to JSON schemas
*/
items: SerializedJsonItem[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './editor-context.js'
export * from './json-item.js'
51 changes: 51 additions & 0 deletions packages/server-ai-toolkit/src/schema-awareness/types/json-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Editor } from '@tiptap/core'
import type { z } from 'zod'

/**
* Represents a JSON schema awareness item for AI model understanding
*
* This interface defines the structure of schema information that is provided
* to AI models to help them understand what JSON elements and attributes are
* available in the editor's schema.
*/
export interface JsonItem {
/**
* The name of the extension that provides this element
*/
extensionName: string
/**
* If `true`, the item is a mark instead of a node.
*/
isMark?: boolean
/**
* The human-readable name of the element in English
*/
name: string
/**
* Explanation of the element in English for the AI model
*/
description?: string | null
/**
* Possible attributes of the JSON element. If `undefined`, there are no attributes.
* The keys are attribute names and the values are Zod schemas that define the attribute structure.
*/
attributes?: Record<string, z.ZodTypeAny>
}

export type SerializedJsonItem = Omit<JsonItem, 'attributes'> & { attributes?: unknown }

export type AddJsonSchemaAwareness<Options = any, Storage = any> = (this: {
name: string
options: Options
storage: Storage
editor?: Editor
}) => Omit<JsonItem, 'extensionName' | 'isMark'>

declare module '@tiptap/core' {
interface NodeConfig<Options, Storage> {
addJsonSchemaAwareness?: AddJsonSchemaAwareness<Options, Storage>
}
interface MarkConfig<Options, Storage> {
addJsonSchemaAwareness?: AddJsonSchemaAwareness<Options, Storage>
}
}
Loading
Loading