- 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>
131 lines
3.6 KiB
TypeScript
131 lines
3.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import prisma from '@/lib/prisma'
|
|
import { auth } from '@/auth'
|
|
|
|
const WIKILINK_RE = /\[\[([^\]|#]+?)(?:[|#][^\]]+)?\]\]/g
|
|
|
|
/**
|
|
* Extract [[wikilink]] targets from markdown/html content.
|
|
* Returns deduplicated list of linked note titles.
|
|
*/
|
|
function extractWikilinks(content: string): { title: string; snippet: string }[] {
|
|
// Strip HTML tags
|
|
const plain = content.replace(/<[^>]+>/g, ' ')
|
|
const results: { title: string; snippet: string }[] = []
|
|
const seen = new Set<string>()
|
|
let match: RegExpExecArray | null
|
|
|
|
WIKILINK_RE.lastIndex = 0
|
|
while ((match = WIKILINK_RE.exec(plain)) !== null) {
|
|
const title = match[1].trim()
|
|
if (!title || seen.has(title.toLowerCase())) continue
|
|
seen.add(title.toLowerCase())
|
|
|
|
// Extract snippet: 50 chars before + after the match
|
|
const start = Math.max(0, match.index - 50)
|
|
const end = Math.min(plain.length, match.index + match[0].length + 50)
|
|
const snippet = plain.slice(start, end).replace(/\s+/g, ' ').trim()
|
|
|
|
results.push({ title, snippet })
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// POST /api/notes/[id]/sync-links
|
|
// Parse [[wikilinks]] in note content and sync the NoteLink table.
|
|
// Called automatically after note save.
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const { id } = await params
|
|
const userId = session.user.id
|
|
|
|
const note = await prisma.note.findUnique({
|
|
where: { id },
|
|
select: { id: true, userId: true, content: true },
|
|
})
|
|
|
|
if (!note || note.userId !== userId) {
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
}
|
|
|
|
const wikilinks = extractWikilinks(note.content)
|
|
|
|
// For each wikilink, find or create the target note
|
|
const upsertedLinks: string[] = []
|
|
|
|
for (const { title, snippet } of wikilinks) {
|
|
// Find target note by title (case-insensitive) belonging to same user
|
|
let targetNote = await prisma.note.findFirst({
|
|
where: {
|
|
userId,
|
|
title: { equals: title, mode: 'insensitive' },
|
|
trashedAt: null,
|
|
},
|
|
select: { id: true },
|
|
})
|
|
|
|
if (!targetNote) {
|
|
// Create a stub note so the link resolves
|
|
targetNote = await prisma.note.create({
|
|
data: {
|
|
title,
|
|
content: '',
|
|
userId,
|
|
type: 'richtext',
|
|
color: 'default',
|
|
isMarkdown: true,
|
|
order: 0,
|
|
},
|
|
select: { id: true },
|
|
})
|
|
}
|
|
|
|
// Skip self-links
|
|
if (targetNote.id === id) continue
|
|
|
|
// Upsert the NoteLink
|
|
await (prisma as any).noteLink.upsert({
|
|
where: {
|
|
sourceNoteId_targetNoteId: {
|
|
sourceNoteId: id,
|
|
targetNoteId: targetNote.id,
|
|
},
|
|
},
|
|
update: { contextSnippet: snippet.slice(0, 200) },
|
|
create: {
|
|
id: crypto.randomUUID(),
|
|
sourceNoteId: id,
|
|
targetNoteId: targetNote.id,
|
|
contextSnippet: snippet.slice(0, 200),
|
|
},
|
|
})
|
|
|
|
upsertedLinks.push(targetNote.id)
|
|
}
|
|
|
|
// Delete obsolete links (links that existed before but wikilink was removed)
|
|
if (upsertedLinks.length > 0) {
|
|
await (prisma as any).noteLink.deleteMany({
|
|
where: {
|
|
sourceNoteId: id,
|
|
targetNoteId: { notIn: upsertedLinks },
|
|
},
|
|
})
|
|
} else {
|
|
// No wikilinks at all — remove all outgoing links from this note
|
|
await (prisma as any).noteLink.deleteMany({
|
|
where: { sourceNoteId: id },
|
|
})
|
|
}
|
|
|
|
return NextResponse.json({ synced: upsertedLinks.length })
|
|
}
|