feat: add slides generation tool with multiple slide types
Some checks failed
CI / Lint, Test & Build (push) Failing after 17s
CI / Deploy production (on server) (push) Has been skipped

- Add slides.tool.ts with support for title, bullets, chart, stats, table, cards, timeline, quote, comparison, equation, image, summary slide types
- Chart types: bar, horizontal-bar, line, donut, radar
- Integrate with agent executor and canvas system
- Add multilingual support (en/fr)
- Various UI improvements and bug fixes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antigravity
2026-05-22 17:18:48 +00:00
parent 0f6b9509da
commit 5728452b4a
68 changed files with 6990 additions and 2584 deletions

View File

@@ -352,6 +352,63 @@ const toolDefinitions = [
inputSchema: { type: 'object', properties: {} },
},
// ═══ TRASH ═══
{
name: 'trash_note',
description: 'Move a note to trash (soft delete). Can be restored later.',
inputSchema: {
type: 'object',
properties: { id: { type: 'string', description: 'Note ID' } },
required: ['id'],
},
},
{
name: 'restore_note',
description: 'Restore a trashed note back to the active notes list.',
inputSchema: {
type: 'object',
properties: { id: { type: 'string', description: 'Note ID' } },
required: ['id'],
},
},
{
name: 'get_trash',
description: 'List all notes currently in trash.',
inputSchema: { type: 'object', properties: {} },
},
// ═══ ADVANCED NOTE OPERATIONS ═══
{
name: 'append_to_note',
description: 'Append text to the end of an existing note without replacing its current content.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Note ID' },
content: { type: 'string', description: 'Content to append' },
},
required: ['id', 'content'],
},
},
{
name: 'find_and_update_note',
description: 'Find a note by searching its title/content, then append, prepend, or replace information. Use when you need to update a note found by keyword (e.g. "find the note about bugs and add this info").',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query to find the note' },
newContent: { type: 'string', description: 'Content to add to the note' },
operation: {
type: 'string',
enum: ['append', 'prepend', 'replace'],
description: 'append: add to end, prepend: add to start, replace: overwrite',
default: 'append',
},
},
required: ['query', 'newContent'],
},
},
// ═══ LABELS ═══
{
name: 'create_label',
@@ -538,25 +595,32 @@ export function registerTools(server, prisma) {
}
case 'search_notes': {
const safeQuery = (args.query || '').replace(/'/g, "''");
const userClause = uid ? `AND "userId" = '${uid}'` : '';
const notebookClause = args.notebookId ? `AND "notebookId" = '${args.notebookId.replace(/'/g, "''")}'` : '';
const query = args.query || ''
const params = [query]
let paramIdx = 1
const ftsRows = await prisma.$queryRawUnsafe(`
SELECT id, title, content, color, type, "isPinned", "isArchived",
"isMarkdown", size, "createdAt", "updatedAt", "notebookId",
images, labels, "checkItems", reminder, "isReminderDone"
FROM "Note"
WHERE "tsv" @@ plainto_tsquery('simple', '${safeQuery}')
AND "trashedAt" IS NULL
AND "isArchived" = ${args.includeArchived ? 'true' : 'false'}
${userClause}
${notebookClause}
ORDER BY ts_rank("tsv", plainto_tsquery('simple', '${safeQuery}')) DESC
LIMIT ${DEFAULT_SEARCH_LIMIT}
`);
const userClause = uid ? `AND "userId" = $${++paramIdx}` : ''
if (uid) params.push(uid)
return textResult(ftsRows.map(parseNoteLightweight));
const notebookClause = args.notebookId ? `AND "notebookId" = $${++paramIdx}` : ''
if (args.notebookId) params.push(args.notebookId)
const ftsRows = await prisma.$queryRawUnsafe(
`SELECT id, title, content, color, type, "isPinned", "isArchived",
"isMarkdown", size, "createdAt", "updatedAt", "notebookId",
images, labels, "checkItems", reminder, "isReminderDone"
FROM "Note"
WHERE "tsv" @@ plainto_tsquery('simple', $1)
AND "trashedAt" IS NULL
AND "isArchived" = ${args.includeArchived ? 'true' : 'false'}
${userClause}
${notebookClause}
ORDER BY ts_rank("tsv", plainto_tsquery('simple', $1)) DESC
LIMIT ${DEFAULT_SEARCH_LIMIT}`,
...params
)
return textResult(ftsRows.map(parseNoteLightweight))
}
case 'move_note': {
@@ -950,6 +1014,127 @@ export function registerTools(server, prisma) {
return textResult({ count: reminders.length, reminders });
}
// ═══ TRASH ═══
case 'trash_note': {
const note = await prisma.note.update({
where: { id: args.id, ...(uid ? { userId: uid } : {}) },
data: { trashedAt: new Date() },
});
return textResult({ success: true, id: note.id, trashedAt: note.trashedAt });
}
case 'restore_note': {
const note = await prisma.note.findUnique({
where: { id: args.id, ...(uid ? { userId: uid } : {}) },
});
if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
if (!note.trashedAt) return textResult({ success: true, message: 'Note was not in trash' });
const restored = await prisma.note.update({
where: { id: args.id },
data: { trashedAt: null },
});
return textResult({ success: true, id: restored.id });
}
case 'get_trash': {
const where = { trashedAt: { not: null }, ...(uid ? { userId: uid } : {}) };
const trashed = await prisma.note.findMany({
where,
select: { id: true, title: true, content: true, color: true, trashedAt: true, notebookId: true },
orderBy: { trashedAt: 'desc' },
});
return textResult(trashed.map(n => ({
...n,
content: n.content?.slice(0, 100),
})));
}
// ═══ ADVANCED NOTE OPERATIONS ═══
case 'append_to_note': {
const note = await prisma.note.findUnique({
where: { id: args.id, ...(uid ? { userId: uid } : {}), trashedAt: null },
select: { content: true },
});
if (!note) throw new McpError(ErrorCode.InvalidRequest, 'Note not found');
const updated = await prisma.note.update({
where: { id: args.id },
data: {
content: note.content ? `${note.content}\n\n${args.content}` : args.content,
updatedAt: new Date(),
},
});
return textResult({ success: true, id: updated.id });
}
case 'find_and_update_note': {
const query = args.query || ''
const operation = args.operation || 'append'
const params = [query]
let paramIdx = 1
const userClause = uid ? `AND "userId" = $${++paramIdx}` : ''
if (uid) params.push(uid)
const results = await prisma.$queryRawUnsafe(
`SELECT id, title, content
FROM "Note"
WHERE "tsv" @@ plainto_tsquery('simple', $1)
AND "trashedAt" IS NULL
AND "isArchived" = false
${userClause}
ORDER BY ts_rank("tsv", plainto_tsquery('simple', $1)) DESC
LIMIT 1`,
...params
)
let note = results[0] || null
if (!note) {
// Fallback: ILIKE search
note = await prisma.note.findFirst({
where: {
trashedAt: null,
isArchived: false,
...(uid ? { userId: uid } : {}),
OR: [
{ title: { contains: query, mode: 'insensitive' } },
{ content: { contains: query, mode: 'insensitive' } },
],
},
select: { id: true, title: true, content: true },
})
}
if (!note) {
throw new McpError(ErrorCode.InvalidRequest, `No note found matching "${query}"`)
}
let updatedContent
switch (operation) {
case 'append':
updatedContent = note.content ? `${note.content}\n\n${args.newContent}` : args.newContent
break
case 'prepend':
updatedContent = note.content ? `${args.newContent}\n\n${note.content}` : args.newContent
break
case 'replace':
default:
updatedContent = args.newContent
}
await prisma.note.update({
where: { id: note.id },
data: { content: updatedContent, updatedAt: new Date() },
})
return textResult({
success: true,
noteId: note.id,
noteTitle: note.title || 'Untitled',
operation,
})
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}