179 lines
6.3 KiB
TypeScript
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 };
|
|
}
|
|
},
|
|
},
|
|
});
|