Spaces:
Runtime error
Runtime error
| import { tool } from "ai"; | |
| import { z } from "zod"; | |
| import { buildTiptapTools } from "./tiptap-catalog.js"; | |
| /** | |
| * Tools exposed to the LLM. These return instructions that the frontend | |
| * will execute on the TipTap editor instance. The backend never touches | |
| * the document directly - it only produces "edit commands" that are | |
| * streamed back and applied client-side. | |
| * | |
| * All edits from a single agent turn are grouped into one Yjs undo step, | |
| * so the user can revert an entire agent action with a single Cmd+Z. | |
| * | |
| * The editor commands come from a static catalog (`tiptap-catalog.ts`) | |
| * that also drives the system prompt's "Selection State Machine" section. | |
| * The domain-specific tools (applyDiff, replaceSelection, insertAtCursor, | |
| * frontmatter) are defined inline here since they don't map 1:1 to a | |
| * native Tiptap command. | |
| */ | |
| const customEditorTools = { | |
| replaceSelection: tool({ | |
| description: | |
| "Replace the currently selected text in the editor with new content. " + | |
| "Use ONLY when text is selected and the user asks to rewrite, fix, or rephrase it.", | |
| inputSchema: z.object({ | |
| newText: z | |
| .string() | |
| .describe("The replacement text (plain text or markdown)"), | |
| }), | |
| }), | |
| insertAtCursor: tool({ | |
| description: | |
| "Insert text at the current cursor position (does not replace anything). " + | |
| "Use when the user asks to add or generate new content.", | |
| inputSchema: z.object({ | |
| text: z.string().describe("The text to insert (plain text or markdown)"), | |
| }), | |
| }), | |
| applyDiff: tool({ | |
| description: | |
| "The PRIMARY tool for editing prose. Locates a piece of text in the document using " + | |
| "context and replaces it. Provide surrounding context to disambiguate when the " + | |
| "same text appears multiple times. For multiple edits, call this tool multiple " + | |
| "times in a single response. Each call should be a minimal, surgical edit.", | |
| inputSchema: z.object({ | |
| contextBefore: z | |
| .string() | |
| .describe( | |
| "A few words of text that appear immediately BEFORE the content to delete. " + | |
| "Used to locate the correct position. Can be empty if at the start of the document.", | |
| ), | |
| contentToDelete: z | |
| .string() | |
| .describe( | |
| "The exact verbatim text to remove from the document. Must match perfectly.", | |
| ), | |
| contentToInsert: z | |
| .string() | |
| .describe( | |
| "The new text to insert in place of the deleted text. " + | |
| "Can be empty to just delete without replacing.", | |
| ), | |
| contextAfter: z | |
| .string() | |
| .describe( | |
| "A few words of text that appear immediately AFTER the content to delete. " + | |
| "Used to confirm the correct position. Can be empty if at the end.", | |
| ), | |
| }), | |
| }), | |
| }; | |
| const frontmatterTools = { | |
| updateFrontmatter: tool({ | |
| description: | |
| "Update article metadata (frontmatter). Use when the user asks to change the title, " + | |
| "subtitle, authors, affiliations, publication date, DOI, template, or any article setting. " + | |
| "Only include the fields you want to change - omitted fields are left unchanged.", | |
| inputSchema: z.object({ | |
| title: z.string().optional().describe("Article title"), | |
| subtitle: z.string().optional().describe("Subtitle"), | |
| description: z.string().optional().describe("Short SEO description"), | |
| published: z.string().optional().describe('Publication date, e.g. "Apr. 04, 2026"'), | |
| template: z.enum(["article", "paper"]).optional().describe("Layout template"), | |
| doi: z.string().optional().describe("DOI identifier"), | |
| banner: z.string().optional().describe("Banner embed filename"), | |
| showPdf: z.boolean().optional().describe("Show PDF download button"), | |
| tableOfContentsAutoCollapse: z.boolean().optional().describe("Auto-collapse TOC"), | |
| licence: z.string().optional().describe("Licence text (HTML allowed)"), | |
| pdfProOnly: z.boolean().optional().describe("Gate PDF behind HF Pro"), | |
| seoThumbImage: z.string().optional().describe("Custom OG image URL"), | |
| }), | |
| }), | |
| addAuthor: tool({ | |
| description: | |
| "Add an author to the article. Provide the author name and optionally a URL " + | |
| "and affiliation indices (1-based). If the affiliation doesn't exist yet, " + | |
| "provide newAffiliationName to create it.", | |
| inputSchema: z.object({ | |
| name: z.string().describe("Author name"), | |
| url: z.string().optional().describe("Author URL/homepage"), | |
| affiliations: z | |
| .array(z.number()) | |
| .optional() | |
| .describe("1-based affiliation indices"), | |
| newAffiliationName: z | |
| .string() | |
| .optional() | |
| .describe("Name of a new affiliation to create (if needed)"), | |
| newAffiliationUrl: z | |
| .string() | |
| .optional() | |
| .describe("URL of the new affiliation"), | |
| }), | |
| }), | |
| removeAuthor: tool({ | |
| description: "Remove an author by index (0-based).", | |
| inputSchema: z.object({ | |
| index: z.number().describe("0-based index of the author to remove"), | |
| }), | |
| }), | |
| }; | |
| export const editorTools = { | |
| ...buildTiptapTools(), | |
| ...customEditorTools, | |
| ...frontmatterTools, | |
| }; | |