import { z } from 'zod'; import { createPage } from '@/modules/pages/client-pages'; type LogFunction = (level: string, message: string, data?: any) => void; const defaultLog: LogFunction = (level, message, data) => console.log(`[${level}] ${message}`, data); /** * Creates a URL-friendly slug from a string. * @param text The text to slugify. * @returns A URL-friendly slug. */ export const slugify = (text: string): string => { return text .toString() .toLowerCase() .trim() .replace(/\s+/g, '-') // Replace spaces with - .replace(/[^\w\-]+/g, '') // Remove all non-word chars .replace(/\-\-+/g, '-') // Replace multiple - with single - .replace(/^-+/, '') // Trim - from start of text .replace(/-+$/, ''); // Trim - from end of text }; /** * Formats markdown content into the required page JSON structure. * @param pageId The unique identifier for the page. * @param pageName The name of the page. * @param markdownContent The markdown content for the page. * @returns The structured page content object. */ export const formatPageContent = (pageId: string, pageName: string, markdownContent: string | undefined | null) => { const now = Date.now(); const prefixedPageId = `page-${pageId}`; // Ensure there is always some content, to avoid creating an empty-looking page. const finalContent = (markdownContent && markdownContent.trim()) ? markdownContent : `# ${pageName}\n\n*This page was generated by AI. Edit this content to get started.*`; const pageStructure = { pages: { [prefixedPageId]: { id: prefixedPageId, name: pageName, createdAt: now, updatedAt: now, containers: [ { id: `container-${now}-${crypto.randomUUID().toString().slice(-12)}`, gap: 16, type: "container", order: 0, columns: 1, widgets: [ { id: `widget-${now}-${crypto.randomUUID().toString().slice(-12)}`, order: 0, props: { content: finalContent, templates: [], placeholder: "Enter your text here...", }, widgetId: "markdown-text", }, ], children: [], }, ], }, }, version: "1.0.0", lastUpdated: now, }; console.log('[PAGE-TOOLS] Generated page structure:', JSON.stringify(pageStructure, null, 2)); return pageStructure; }; /** * Shared logic to create a page via the API. * Slug generation and collision handling are done server-side. */ export const createPageInDb = async ( userId: string, args: { title: string; content: string; tags?: string[]; is_public?: boolean; visible?: boolean; parent?: string | null; }, addLog: LogFunction = defaultLog ) => { if (!userId) { throw new Error('User not authenticated.'); } const pageId = crypto.randomUUID(); const slug = slugify(args.title); const pageContent = formatPageContent(pageId, args.title, args.content); addLog('debug', '[PAGE-TOOLS] createPageInDb - Formatted page content', { pageContent: JSON.stringify(pageContent) }); // Sanitize parent: must be a valid UUID or omitted entirely const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const cleanParent = args.parent && UUID_RE.test(args.parent) ? args.parent : undefined; const newPage: Record = { id: pageId, owner: userId, title: args.title, slug, content: pageContent, tags: args.tags, is_public: args.is_public ?? false, visible: args.visible ?? true, type: 'article', }; if (cleanParent) newPage.parent = cleanParent; const data = await createPage(newPage); const pageUrl = `/user/${userId}/pages/${data.slug || slug}`; addLog('info', '[PAGE-TOOLS] Page created successfully', { pageId: data.id, url: pageUrl }); return { success: true, pageId: data.id, slug: data.slug || slug, url: pageUrl, message: `Successfully created page: "${args.title}". View it at: ${pageUrl}` }; }; /** * Tool: Create Page * Creates a new page with the given title, content, and metadata, and saves it to the database. * Uses explicit JSON schema (not zodToJsonSchema) for OpenAI compatibility. */ export const createPageTool = (userId: string, addLog: LogFunction = defaultLog) => ({ type: 'function' as const, function: { name: 'create_page', description: 'Creates a new page with the given title, markdown content, and optional metadata. The page is saved to the user\'s account.', parameters: { type: 'object', properties: { title: { type: 'string', description: 'The title of the page.' }, content: { type: 'string', description: 'The full markdown content for the page. This should be generated first.' }, tags: { type: 'array', items: { type: 'string' }, description: 'An array of relevant tags for the page.' }, is_public: { type: 'boolean', description: 'Whether the page should be publicly accessible. Defaults to false.' }, visible: { type: 'boolean', description: 'Whether the page should be visible in lists. Defaults to true.' }, }, required: ['title', 'content'], } as any, parse(input: string) { const obj = JSON.parse(input); return z.object({ title: z.string(), content: z.string(), tags: z.array(z.string()).optional(), is_public: z.boolean().optional().default(false), visible: z.boolean().optional().default(true), }).parse(obj); }, function: async (args: { title: string; content: string; tags?: string[]; is_public?: boolean; visible?: boolean }) => { try { addLog('info', '[PAGE-TOOLS] Tool::CreatePage called', { title: args.title, userId }); const result = await createPageInDb(userId, args as any, addLog); return result; } catch (error: any) { addLog('error', '[PAGE-TOOLS] Tool::CreatePage failed', error); return { success: false, error: error.message }; } }, }, });