| import { tool, type Tool } from "ai"; |
| import { z } from "zod"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| type Category = "query" | "selection" | "format" | "content"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| type SelectionEffect = "require" | "preserve" | "destroy" | "none"; |
|
|
| |
| |
| type AnyTool = Tool<any, any>; |
|
|
| export interface TiptapCommandEntry { |
| name: string; |
| category: Category; |
| selectionEffect: SelectionEffect; |
| destructive: boolean; |
| tool: AnyTool; |
| } |
|
|
| |
| |
| |
|
|
| export const TIPTAP_COMMANDS: TiptapCommandEntry[] = [ |
| |
| { |
| name: "selectText", |
| category: "selection", |
| selectionEffect: "preserve", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Find a text substring in the document and select it. Use this BEFORE applying " + |
| "mark-level formatting commands (toggleBold, setLink, etc.). Returns { found, from, to } " + |
| "so you can verify the match. When the same text appears multiple times, use `occurrence` " + |
| "(1-based) or provide `contextBefore` / `contextAfter` to disambiguate.", |
| inputSchema: z.object({ |
| text: z.string().describe("The exact text to select (substring match)"), |
| occurrence: z |
| .number() |
| .int() |
| .min(1) |
| .optional() |
| .describe( |
| "Which occurrence to select (1-based). Defaults to 1. Ignored if contextBefore/After are provided.", |
| ), |
| contextBefore: z |
| .string() |
| .optional() |
| .describe("A few words appearing just before the target text, used to disambiguate."), |
| contextAfter: z |
| .string() |
| .optional() |
| .describe("A few words appearing just after the target text, used to disambiguate."), |
| }), |
| }), |
| }, |
|
|
| |
| { |
| name: "toggleBold", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Toggle bold on the current selection. MUST be called after selectText - has no effect without an active text range.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleItalic", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Toggle italic on the current selection. MUST be called after selectText - has no effect without an active text range.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleStrike", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Toggle strikethrough on the current selection. MUST be called after selectText.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleCode", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Toggle inline code formatting on the current selection. MUST be called after selectText.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "setLink", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Turn the current selection into a hyperlink. MUST be called after selectText. " + |
| "Only http(s)/mailto/tel/relative URLs are accepted.", |
| inputSchema: z.object({ |
| href: z.string().describe("Target URL (http/https/mailto/tel or relative)"), |
| }), |
| }), |
| }, |
| { |
| name: "unsetLink", |
| category: "format", |
| selectionEffect: "require", |
| destructive: false, |
| tool: tool({ |
| description: "Remove hyperlink formatting from the current selection.", |
| inputSchema: z.object({}), |
| }), |
| }, |
|
|
| |
| { |
| name: "toggleHeading", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Convert the block at the cursor to a heading (or back to a paragraph if already that level). " + |
| "Operates on the containing block - no text selection needed.", |
| inputSchema: z.object({ |
| level: z.number().int().min(1).max(3).describe("Heading level (1, 2 or 3)"), |
| }), |
| }), |
| }, |
| { |
| name: "setParagraph", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: "Convert the block at the cursor to a plain paragraph.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleBulletList", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: "Toggle the block at the cursor between a bullet list item and a paragraph.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleOrderedList", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: "Toggle the block at the cursor between an ordered (numbered) list item and a paragraph.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleBlockquote", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: "Toggle blockquote on the block at the cursor.", |
| inputSchema: z.object({}), |
| }), |
| }, |
| { |
| name: "toggleCodeBlock", |
| category: "format", |
| selectionEffect: "none", |
| destructive: false, |
| tool: tool({ |
| description: |
| "Toggle code block on the block at the cursor. Use for multi-line code snippets.", |
| inputSchema: z.object({ |
| language: z |
| .string() |
| .optional() |
| .describe("Language hint for syntax highlighting (e.g. 'python', 'typescript')"), |
| }), |
| }), |
| }, |
| ]; |
|
|
| |
| |
| |
|
|
| |
| export function buildTiptapTools(): Record<string, AnyTool> { |
| const out: Record<string, AnyTool> = {}; |
| for (const entry of TIPTAP_COMMANDS) { |
| out[entry.name] = entry.tool; |
| } |
| return out; |
| } |
|
|
| |
| export function classifyBySelectionEffect(): Record<SelectionEffect, string[]> { |
| const groups: Record<SelectionEffect, string[]> = { |
| require: [], |
| preserve: [], |
| destroy: [], |
| none: [], |
| }; |
| for (const entry of TIPTAP_COMMANDS) { |
| groups[entry.selectionEffect].push(entry.name); |
| } |
| return groups; |
| } |
|
|
| |
| export function groupByCategory(): Record<Category, string[]> { |
| const groups: Record<Category, string[]> = { |
| query: [], |
| selection: [], |
| format: [], |
| content: [], |
| }; |
| for (const entry of TIPTAP_COMMANDS) { |
| groups[entry.category].push(entry.name); |
| } |
| return groups; |
| } |
|
|