mono/packages/ui/src/lib/pageTools.ts
2026-03-21 20:18:25 +01:00

179 lines
6.3 KiB
TypeScript

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<string, any> = {
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 };
}
},
},
});