Compare commits
18 Commits
129d5541e6
...
ancien-ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d8252aec0 | ||
|
|
33ad874e5d | ||
|
|
3b72e4afbb | ||
|
|
ccc94a4b35 | ||
|
|
fb6823e25e | ||
|
|
7326cfc98f | ||
|
|
4f950740eb | ||
|
|
db200bbc9f | ||
|
|
08a49a04ce | ||
|
|
ab914f0587 | ||
|
|
b55f558a62 | ||
|
|
19d0b2759a | ||
|
|
34a977b5c4 | ||
|
|
75b08ef53b | ||
|
|
98e246e257 | ||
|
|
ff0b56f805 | ||
|
|
d1e08f64c8 | ||
|
|
e7f28abccc |
@@ -121,7 +121,8 @@ jobs:
|
|||||||
|
|
||||||
echo "=== Git pull ==="
|
echo "=== Git pull ==="
|
||||||
git config --global --add safe.directory /opt/memento
|
git config --global --add safe.directory /opt/memento
|
||||||
git pull origin main
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
|
||||||
echo "=== Building ==="
|
echo "=== Building ==="
|
||||||
docker compose build memento-note
|
docker compose build memento-note
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ export const dynamic = 'force-dynamic'
|
|||||||
export const revalidate = 0
|
export const revalidate = 0
|
||||||
|
|
||||||
export default async function LabPage(props: {
|
export default async function LabPage(props: {
|
||||||
searchParams: Promise<{ id?: string }>
|
searchParams: Promise<{ id?: string; canvas?: string }>
|
||||||
}) {
|
}) {
|
||||||
const searchParams = await props.searchParams
|
const searchParams = await props.searchParams
|
||||||
const id = searchParams.id
|
/** Canonical param is id; notifications / older UI used canvas= */
|
||||||
|
const requestedId = searchParams.id ?? searchParams.canvas
|
||||||
|
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
if (!session?.user?.id) redirect('/login')
|
if (!session?.user?.id) redirect('/login')
|
||||||
|
|
||||||
const canvases = await getCanvases()
|
const canvases = await getCanvases()
|
||||||
|
|
||||||
// Resolve current canvas correctly
|
const currentCanvasId = requestedId || (canvases.length > 0 ? canvases[0].id : undefined)
|
||||||
const currentCanvasId = searchParams.id || (canvases.length > 0 ? canvases[0].id : undefined)
|
|
||||||
const currentCanvas = currentCanvasId ? canvases.find(c => c.id === currentCanvasId) : undefined
|
const currentCanvas = currentCanvasId ? canvases.find(c => c.id === currentCanvasId) : undefined
|
||||||
|
|
||||||
// Wrapper for server action creation
|
// Wrapper for server action creation
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export async function createAgent(data: {
|
|||||||
role: string
|
role: string
|
||||||
sourceUrls?: string[]
|
sourceUrls?: string[]
|
||||||
sourceNotebookId?: string
|
sourceNotebookId?: string
|
||||||
|
sourceNoteIds?: string[]
|
||||||
targetNotebookId?: string
|
targetNotebookId?: string
|
||||||
frequency?: string
|
frequency?: string
|
||||||
tools?: string[]
|
tools?: string[]
|
||||||
@@ -28,6 +29,8 @@ export async function createAgent(data: {
|
|||||||
scheduledTime?: string
|
scheduledTime?: string
|
||||||
scheduledDay?: number
|
scheduledDay?: number
|
||||||
timezone?: string
|
timezone?: string
|
||||||
|
slideTheme?: string
|
||||||
|
slideStyle?: string
|
||||||
}) {
|
}) {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -43,6 +46,7 @@ export async function createAgent(data: {
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
sourceUrls: data.sourceUrls ? JSON.stringify(data.sourceUrls) : null,
|
sourceUrls: data.sourceUrls ? JSON.stringify(data.sourceUrls) : null,
|
||||||
sourceNotebookId: data.sourceNotebookId || null,
|
sourceNotebookId: data.sourceNotebookId || null,
|
||||||
|
sourceNoteIds: data.sourceNoteIds ? JSON.stringify(data.sourceNoteIds) : null,
|
||||||
targetNotebookId: data.targetNotebookId || null,
|
targetNotebookId: data.targetNotebookId || null,
|
||||||
frequency: data.frequency || 'manual',
|
frequency: data.frequency || 'manual',
|
||||||
tools: data.tools ? JSON.stringify(data.tools) : '[]',
|
tools: data.tools ? JSON.stringify(data.tools) : '[]',
|
||||||
@@ -52,6 +56,8 @@ export async function createAgent(data: {
|
|||||||
scheduledTime: data.scheduledTime || '08:00',
|
scheduledTime: data.scheduledTime || '08:00',
|
||||||
scheduledDay: data.scheduledDay ?? null,
|
scheduledDay: data.scheduledDay ?? null,
|
||||||
timezone: data.timezone || null,
|
timezone: data.timezone || null,
|
||||||
|
slideTheme: data.slideTheme || null,
|
||||||
|
slideStyle: data.slideStyle || null,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -88,6 +94,7 @@ export async function updateAgent(id: string, data: {
|
|||||||
role?: string
|
role?: string
|
||||||
sourceUrls?: string[]
|
sourceUrls?: string[]
|
||||||
sourceNotebookId?: string | null
|
sourceNotebookId?: string | null
|
||||||
|
sourceNoteIds?: string[] | null
|
||||||
targetNotebookId?: string | null
|
targetNotebookId?: string | null
|
||||||
frequency?: string
|
frequency?: string
|
||||||
isEnabled?: boolean
|
isEnabled?: boolean
|
||||||
@@ -98,6 +105,8 @@ export async function updateAgent(id: string, data: {
|
|||||||
scheduledTime?: string
|
scheduledTime?: string
|
||||||
scheduledDay?: number | null
|
scheduledDay?: number | null
|
||||||
timezone?: string
|
timezone?: string
|
||||||
|
slideTheme?: string | null
|
||||||
|
slideStyle?: string | null
|
||||||
}) {
|
}) {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -117,6 +126,7 @@ export async function updateAgent(id: string, data: {
|
|||||||
if (data.role !== undefined) updateData.role = data.role
|
if (data.role !== undefined) updateData.role = data.role
|
||||||
if (data.sourceUrls !== undefined) updateData.sourceUrls = JSON.stringify(data.sourceUrls)
|
if (data.sourceUrls !== undefined) updateData.sourceUrls = JSON.stringify(data.sourceUrls)
|
||||||
if (data.sourceNotebookId !== undefined) updateData.sourceNotebookId = data.sourceNotebookId
|
if (data.sourceNotebookId !== undefined) updateData.sourceNotebookId = data.sourceNotebookId
|
||||||
|
if (data.sourceNoteIds !== undefined) updateData.sourceNoteIds = data.sourceNoteIds ? JSON.stringify(data.sourceNoteIds) : null
|
||||||
if (data.targetNotebookId !== undefined) updateData.targetNotebookId = data.targetNotebookId
|
if (data.targetNotebookId !== undefined) updateData.targetNotebookId = data.targetNotebookId
|
||||||
if (data.frequency !== undefined) updateData.frequency = data.frequency
|
if (data.frequency !== undefined) updateData.frequency = data.frequency
|
||||||
if (data.isEnabled !== undefined) updateData.isEnabled = data.isEnabled
|
if (data.isEnabled !== undefined) updateData.isEnabled = data.isEnabled
|
||||||
@@ -127,6 +137,8 @@ export async function updateAgent(id: string, data: {
|
|||||||
if (data.scheduledTime !== undefined) updateData.scheduledTime = data.scheduledTime
|
if (data.scheduledTime !== undefined) updateData.scheduledTime = data.scheduledTime
|
||||||
if (data.scheduledDay !== undefined) updateData.scheduledDay = data.scheduledDay
|
if (data.scheduledDay !== undefined) updateData.scheduledDay = data.scheduledDay
|
||||||
if (data.timezone !== undefined) updateData.timezone = data.timezone
|
if (data.timezone !== undefined) updateData.timezone = data.timezone
|
||||||
|
if (data.slideTheme !== undefined) updateData.slideTheme = data.slideTheme
|
||||||
|
if (data.slideStyle !== undefined) updateData.slideStyle = data.slideStyle
|
||||||
|
|
||||||
// Recalculate nextRun when scheduling fields change
|
// Recalculate nextRun when scheduling fields change
|
||||||
const shouldRecalcNextRun =
|
const shouldRecalcNextRun =
|
||||||
@@ -192,7 +204,7 @@ export async function getAgents() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const agents = await prisma.agent.findMany({
|
const agents = await prisma.agent.findMany({
|
||||||
where: { userId: session.user.id },
|
where: { userId: session.user.id, NOT: { frequency: 'one-shot' } },
|
||||||
include: {
|
include: {
|
||||||
_count: { select: { actions: true } },
|
_count: { select: { actions: true } },
|
||||||
actions: {
|
actions: {
|
||||||
@@ -220,21 +232,21 @@ export async function runAgent(id: string) {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
throw new Error('Non autorise')
|
throw new Error('Non autorise')
|
||||||
}
|
}
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
try {
|
// Verify ownership
|
||||||
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
|
const agent = await prisma.agent.findUnique({ where: { id }, select: { id: true, userId: true } })
|
||||||
const result = await executeAgent(id, session.user.id)
|
if (!agent || agent.userId !== userId) {
|
||||||
revalidatePath('/agents')
|
return { success: false, agentId: id, error: 'Agent introuvable' }
|
||||||
revalidatePath('/')
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error running agent:', error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionId: '',
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire and forget — return immediately so the UI doesn't block
|
||||||
|
import('@/lib/ai/services/agent-executor.service')
|
||||||
|
.then(({ executeAgent }) => executeAgent(id, userId))
|
||||||
|
.then(() => { /* revalidation is handled client-side via polling */ })
|
||||||
|
.catch(err => console.error('[runAgent] Background error:', err))
|
||||||
|
|
||||||
|
return { success: true, agentId: id, status: 'running' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- History ---
|
// --- History ---
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ export async function getCanvases() {
|
|||||||
|
|
||||||
return prisma.canvas.findMany({
|
return prisma.canvas.findMany({
|
||||||
where: { userId: session.user.id },
|
where: { userId: session.user.id },
|
||||||
orderBy: { createdAt: 'asc' }
|
/** Most recently updated first — default canvas matches latest work (agent output, edits) */
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
140
memento-note/app/api/agents/run-for-note/route.ts
Normal file
140
memento-note/app/api/agents/run-for-note/route.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { auth } from '@/auth'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
|
||||||
|
type GenerateType = 'slide-generator' | 'excalidraw-generator'
|
||||||
|
|
||||||
|
const TYPE_DEFAULTS: Record<GenerateType, {
|
||||||
|
role: string
|
||||||
|
tools: string[]
|
||||||
|
maxSteps: number
|
||||||
|
}> = {
|
||||||
|
'slide-generator': {
|
||||||
|
role: 'Crée une présentation PowerPoint professionnelle et visuelle à partir du contenu de la note fournie.',
|
||||||
|
tools: ['note_search', 'note_read', 'generate_pptx'],
|
||||||
|
maxSteps: 8,
|
||||||
|
},
|
||||||
|
'excalidraw-generator': {
|
||||||
|
role: 'Génère un diagramme Excalidraw clair et professionnel à partir du contenu de la note fournie.',
|
||||||
|
tools: ['note_search', 'note_read', 'generate_excalidraw'],
|
||||||
|
maxSteps: 6,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── POST : kick off generation (fire-and-forget) ──────────────────────────
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
|
||||||
|
}
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
const body = await req.json()
|
||||||
|
const { noteId, type, theme, style } = body as {
|
||||||
|
noteId: string
|
||||||
|
type: GenerateType
|
||||||
|
theme?: string
|
||||||
|
style?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noteId || !type || !TYPE_DEFAULTS[type]) {
|
||||||
|
return NextResponse.json({ error: 'Paramètres invalides' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await prisma.note.findFirst({
|
||||||
|
where: { id: noteId, userId },
|
||||||
|
select: { id: true, title: true, notebookId: true },
|
||||||
|
})
|
||||||
|
if (!note) {
|
||||||
|
return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = TYPE_DEFAULTS[type]
|
||||||
|
const agentName = type === 'slide-generator'
|
||||||
|
? `Slides — ${(note.title || 'Note').substring(0, 40)}`
|
||||||
|
: `Diagramme — ${(note.title || 'Note').substring(0, 40)}`
|
||||||
|
|
||||||
|
const agent = await prisma.agent.create({
|
||||||
|
data: {
|
||||||
|
name: agentName,
|
||||||
|
type,
|
||||||
|
role: defaults.role,
|
||||||
|
tools: JSON.stringify(defaults.tools),
|
||||||
|
maxSteps: defaults.maxSteps,
|
||||||
|
frequency: 'one-shot',
|
||||||
|
isEnabled: true,
|
||||||
|
sourceNoteIds: JSON.stringify([noteId]),
|
||||||
|
targetNotebookId: note.notebookId ?? undefined,
|
||||||
|
slideTheme: theme ?? 'vibrant_tech',
|
||||||
|
slideStyle: style ?? 'soft',
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Fire and forget — do NOT await so the HTTP response returns immediately ──
|
||||||
|
// In Node.js / Docker self-hosted, the process keeps running after response.
|
||||||
|
import('@/lib/ai/services/agent-executor.service')
|
||||||
|
.then(({ executeAgent }) => executeAgent(agent.id, userId))
|
||||||
|
.catch(err => console.error('[run-for-note] Background agent error:', err))
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, agentId: agent.id, status: 'running' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── GET : poll current agent status ──────────────────────────────────────
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
|
||||||
|
}
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
const agentId = req.nextUrl.searchParams.get('agentId')
|
||||||
|
if (!agentId) {
|
||||||
|
return NextResponse.json({ error: 'agentId manquant' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ownership
|
||||||
|
const agent = await prisma.agent.findFirst({
|
||||||
|
where: { id: agentId, userId },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
if (!agent) {
|
||||||
|
return NextResponse.json({ error: 'Agent introuvable' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return latest action for this agent
|
||||||
|
const action = await prisma.agentAction.findFirst({
|
||||||
|
where: { agentId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
select: { id: true, status: true, result: true, log: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
// Action not created yet (race condition in first poll) — still running
|
||||||
|
return NextResponse.json({ status: 'running' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If success, find canvasId from the related canvas (result stores canvas id)
|
||||||
|
let canvasId: string | null = null
|
||||||
|
let noteId: string | null = null
|
||||||
|
if (action.status === 'success' && action.result) {
|
||||||
|
// result field: the executor stores canvas.id or note.id
|
||||||
|
const canvas = await prisma.canvas.findFirst({
|
||||||
|
where: { id: action.result },
|
||||||
|
select: { id: true },
|
||||||
|
})
|
||||||
|
if (canvas) {
|
||||||
|
canvasId = canvas.id
|
||||||
|
} else {
|
||||||
|
noteId = action.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
status: action.status, // 'running' | 'success' | 'failure'
|
||||||
|
actionId: action.id,
|
||||||
|
canvasId,
|
||||||
|
noteId,
|
||||||
|
error: action.status === 'failure' ? (action.log || 'Erreur inconnue') : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
* Compact card matching the reference design — with a "Next Run / Status" footer.
|
* Compact card matching the reference design — with a "Next Run / Status" footer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { fr } from 'date-fns/locale/fr'
|
import { fr } from 'date-fns/locale/fr'
|
||||||
import { enUS } from 'date-fns/locale/en-US'
|
import { enUS } from 'date-fns/locale/en-US'
|
||||||
@@ -83,21 +83,51 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps
|
|||||||
const dateLocale = language === 'fr' ? fr : enUS
|
const dateLocale = language === 'fr' ? fr : enUS
|
||||||
const isNew = Date.now() - new Date(agent.createdAt).getTime() < 5 * 60 * 1000
|
const isNew = Date.now() - new Date(agent.createdAt).getTime() < 5 * 60 * 1000
|
||||||
|
|
||||||
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
// Cleanup polling on unmount
|
||||||
|
useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current) }, [])
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
|
const toastId = toast.loading(
|
||||||
|
t('agents.toasts.running') || `Agent "${agent.name}" en cours…`,
|
||||||
|
{ description: t('agents.toasts.runningDesc') || 'La génération peut prendre quelques minutes.', duration: Infinity }
|
||||||
|
)
|
||||||
try {
|
try {
|
||||||
const { runAgent } = await import('@/app/actions/agent-actions')
|
const { runAgent } = await import('@/app/actions/agent-actions')
|
||||||
const result = await runAgent(agent.id)
|
const result = await runAgent(agent.id)
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
toast.success(t('agents.toasts.runSuccess', { name: agent.name }))
|
toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }), { id: toastId })
|
||||||
} else {
|
setIsRunning(false)
|
||||||
toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }))
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll status every 3 s until terminal state
|
||||||
|
if (pollRef.current) clearInterval(pollRef.current)
|
||||||
|
pollRef.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/agents/run-for-note?agentId=${agent.id}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.status === 'success') {
|
||||||
|
clearInterval(pollRef.current!)
|
||||||
|
pollRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
toast.success(t('agents.toasts.runSuccess', { name: agent.name }), { id: toastId, duration: 6000 })
|
||||||
|
onRefresh()
|
||||||
|
} else if (data.status === 'failure') {
|
||||||
|
clearInterval(pollRef.current!)
|
||||||
|
pollRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { id: toastId })
|
||||||
|
onRefresh()
|
||||||
|
}
|
||||||
|
} catch { /* network error — keep polling */ }
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('agents.toasts.runGenericError'))
|
toast.error(t('agents.toasts.runGenericError'), { id: toastId })
|
||||||
} finally {
|
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
onRefresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Globe, BookOpen, FileText, RotateCcw, Check,
|
Globe, BookOpen, FileText, RotateCcw, Check,
|
||||||
Maximize2, ImageIcon, Link2, Download, ArrowDownToLine,
|
Maximize2, ImageIcon, Link2, Download, ArrowDownToLine,
|
||||||
GitMerge, PlusCircle, Eye, Code, Languages,
|
GitMerge, PlusCircle, Eye, Code, Languages,
|
||||||
|
Presentation, PenTool, ExternalLink,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useLanguage } from '@/lib/i18n'
|
import { useLanguage } from '@/lib/i18n'
|
||||||
import { MarkdownContent } from '@/components/markdown-content'
|
import { MarkdownContent } from '@/components/markdown-content'
|
||||||
@@ -71,11 +72,18 @@ const ACTION_IDS = [
|
|||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface GenerateResult {
|
||||||
|
type: 'slides' | 'diagram'
|
||||||
|
canvasId?: string
|
||||||
|
noteId?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ContextualAIChatProps {
|
interface ContextualAIChatProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
noteTitle?: string
|
noteTitle?: string
|
||||||
noteContent?: string
|
noteContent?: string
|
||||||
noteImages?: string[]
|
noteImages?: string[]
|
||||||
|
noteId?: string
|
||||||
/** Called when an action result should be injected into the note */
|
/** Called when an action result should be injected into the note */
|
||||||
onApplyToNote?: (newContent: string) => void
|
onApplyToNote?: (newContent: string) => void
|
||||||
/** Called when the user wants to undo the last injected action */
|
/** Called when the user wants to undo the last injected action */
|
||||||
@@ -95,6 +103,7 @@ export function ContextualAIChat({
|
|||||||
noteTitle,
|
noteTitle,
|
||||||
noteContent,
|
noteContent,
|
||||||
noteImages,
|
noteImages,
|
||||||
|
noteId,
|
||||||
onApplyToNote,
|
onApplyToNote,
|
||||||
onUndoLastAction,
|
onUndoLastAction,
|
||||||
lastActionApplied = false,
|
lastActionApplied = false,
|
||||||
@@ -116,7 +125,16 @@ export function ContextualAIChat({
|
|||||||
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
||||||
const [showLangPicker, setShowLangPicker] = useState(false)
|
const [showLangPicker, setShowLangPicker] = useState(false)
|
||||||
const [translateTarget, setTranslateTarget] = useState('')
|
const [translateTarget, setTranslateTarget] = useState('')
|
||||||
|
|
||||||
|
// Generate slides / diagram state
|
||||||
|
const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null)
|
||||||
|
const [generateResult, setGenerateResult] = useState<GenerateResult | null>(null)
|
||||||
const [customLangInput, setCustomLangInput] = useState('')
|
const [customLangInput, setCustomLangInput] = useState('')
|
||||||
|
// Generation options
|
||||||
|
const [slideTheme, setSlideTheme] = useState('vibrant_tech')
|
||||||
|
const [slideStyle, setSlideStyle] = useState('soft')
|
||||||
|
const [diagramType, setDiagramType] = useState('auto')
|
||||||
|
const [diagramStyle, setDiagramStyle] = useState('default')
|
||||||
|
|
||||||
// Resource tab state
|
// Resource tab state
|
||||||
const [resourceUrl, setResourceUrl] = useState('')
|
const [resourceUrl, setResourceUrl] = useState('')
|
||||||
@@ -249,6 +267,104 @@ export function ContextualAIChat({
|
|||||||
|
|
||||||
const handleDiscardPreview = () => setActionPreview(null)
|
const handleDiscardPreview = () => setActionPreview(null)
|
||||||
|
|
||||||
|
// ── Generate slides / diagram ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const generatePollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
const handleGenerate = async (type: 'slides' | 'diagram') => {
|
||||||
|
if (!noteId) {
|
||||||
|
toast.error(t('ai.generate.noNoteId') || 'Note non sauvegardée')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setGenerateLoading(type)
|
||||||
|
setGenerateResult(null)
|
||||||
|
|
||||||
|
// Persistent loading toast — layout-level (Sonner), survives navigation
|
||||||
|
const toastId = toast.loading(
|
||||||
|
type === 'slides'
|
||||||
|
? (t('ai.generate.toastLoading.slides') || '⏳ Génération de la présentation en cours…')
|
||||||
|
: (t('ai.generate.toastLoading.diagram') || '⏳ Génération du diagramme en cours…'),
|
||||||
|
{ duration: Infinity, description: t('ai.generate.toastLoadingDesc') || 'Vous pouvez naviguer librement, une notification apparaîtra.' }
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// POST starts the agent immediately and returns agentId (fire-and-forget on server)
|
||||||
|
const res = await fetch('/api/agents/run-for-note', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
noteId,
|
||||||
|
type: type === 'slides' ? 'slide-generator' : 'excalidraw-generator',
|
||||||
|
theme: type === 'slides' ? slideTheme : diagramType,
|
||||||
|
style: type === 'slides' ? slideStyle : diagramStyle,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok || !data.success) {
|
||||||
|
toast.error(data.error || t('ai.generate.error') || 'Erreur lors de la génération', { id: toastId })
|
||||||
|
setGenerateLoading(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { agentId } = data as { agentId: string }
|
||||||
|
|
||||||
|
// Poll status every 3 s until terminal state
|
||||||
|
if (generatePollRef.current) clearInterval(generatePollRef.current)
|
||||||
|
generatePollRef.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const pollRes = await fetch(`/api/agents/run-for-note?agentId=${agentId}`)
|
||||||
|
const poll = await pollRes.json()
|
||||||
|
|
||||||
|
if (poll.status === 'success') {
|
||||||
|
clearInterval(generatePollRef.current!)
|
||||||
|
generatePollRef.current = null
|
||||||
|
setGenerateLoading(null)
|
||||||
|
setGenerateResult({ type, canvasId: poll.canvasId, noteId: poll.noteId })
|
||||||
|
|
||||||
|
if (type === 'slides' && poll.canvasId) {
|
||||||
|
toast.success(t('ai.generate.slidesReady') || 'Présentation générée !', {
|
||||||
|
id: toastId, duration: 10000,
|
||||||
|
description: t('ai.generate.toastSuccessSlides') || 'Cliquez sur Télécharger dans le panneau IA.',
|
||||||
|
action: { label: t('ai.generate.downloadPptx') || 'Télécharger', onClick: () => window.open(`/api/canvas/download?id=${poll.canvasId}`, '_blank') },
|
||||||
|
})
|
||||||
|
} else if (type === 'diagram' && poll.canvasId) {
|
||||||
|
toast.success(t('ai.generate.diagramReady') || 'Diagramme généré !', {
|
||||||
|
id: toastId, duration: 10000,
|
||||||
|
description: t('ai.generate.toastSuccessDiagram') || 'Votre diagramme est disponible dans le Lab.',
|
||||||
|
action: { label: t('ai.generate.openDiagram') || 'Ouvrir', onClick: () => { window.location.href = `/lab?id=${poll.canvasId}` } },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.success(type === 'slides'
|
||||||
|
? (t('ai.generate.slidesReady') || 'Présentation générée !')
|
||||||
|
: (t('ai.generate.diagramReady') || 'Diagramme généré !'),
|
||||||
|
{ id: toastId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (poll.status === 'failure') {
|
||||||
|
clearInterval(generatePollRef.current!)
|
||||||
|
generatePollRef.current = null
|
||||||
|
setGenerateLoading(null)
|
||||||
|
toast.error(poll.error || t('ai.generate.error') || 'Erreur lors de la génération', { id: toastId })
|
||||||
|
}
|
||||||
|
// If still 'running', do nothing — next poll will check again
|
||||||
|
} catch {
|
||||||
|
// Network error during poll — keep polling
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
toast.error(t('ai.generate.error') || 'Erreur lors de la génération', { id: toastId })
|
||||||
|
setGenerateLoading(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup polling on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (generatePollRef.current) clearInterval(generatePollRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// ── Resource tab handlers ────────────────────────────────────────────────────
|
// ── Resource tab handlers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleScrapeUrl = async () => {
|
const handleScrapeUrl = async () => {
|
||||||
@@ -883,6 +999,105 @@ export function ContextualAIChat({
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Generate slides / diagram ─────────────────────── */}
|
||||||
|
{noteId && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground mb-2.5 mt-4">
|
||||||
|
{t('ai.generate.sectionLabel') || 'Générer depuis cette note'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* ── Slides ── */}
|
||||||
|
<div className="mb-3 rounded-xl border-2 border-purple-200 dark:border-purple-800 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/40 dark:to-pink-950/40 overflow-hidden">
|
||||||
|
{/* Options */}
|
||||||
|
<div className="grid grid-cols-2 gap-1.5 px-3 pt-2.5 pb-1">
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-semibold uppercase tracking-wider text-purple-500 dark:text-purple-400 block mb-0.5">{t('ai.generate.theme') || 'Thème'}</label>
|
||||||
|
<select value={slideTheme} onChange={e => setSlideTheme(e.target.value)} disabled={generateLoading === 'slides'}
|
||||||
|
className="w-full text-[11px] rounded-md border border-purple-200 dark:border-purple-700 bg-white/70 dark:bg-purple-950/60 text-purple-800 dark:text-purple-200 px-1.5 py-1 focus:outline-none">
|
||||||
|
{[['vibrant_tech','Vibrant Tech'],['business_authority','Business'],['soft_creative','Créatif'],['modern_wellness','Wellness'],['tech_night','Dark Tech'],['education_charts','Éducation'],['elegant_fashion','Élégant'],['art_food','Art & Food'],['nature_outdoors','Nature'],['vintage_academic','Académique'],['platinum_white_gold','Premium']].map(([v,l]) =>
|
||||||
|
<option key={v} value={v}>{l}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-semibold uppercase tracking-wider text-purple-500 dark:text-purple-400 block mb-0.5">{t('ai.generate.style') || 'Style'}</label>
|
||||||
|
<select value={slideStyle} onChange={e => setSlideStyle(e.target.value)} disabled={generateLoading === 'slides'}
|
||||||
|
className="w-full text-[11px] rounded-md border border-purple-200 dark:border-purple-700 bg-white/70 dark:bg-purple-950/60 text-purple-800 dark:text-purple-200 px-1.5 py-1 focus:outline-none">
|
||||||
|
<option value="soft">{t('ai.generate.styleSoft') || 'Soft'}</option>
|
||||||
|
<option value="sharp">{t('ai.generate.styleSharp') || 'Sharp'}</option>
|
||||||
|
<option value="rounded">{t('ai.generate.styleRounded') || 'Arrondi'}</option>
|
||||||
|
<option value="pill">{t('ai.generate.stylePill') || 'Pill'}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Button */}
|
||||||
|
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading || !!actionLoading}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium text-purple-700 dark:text-purple-300 hover:bg-purple-100/60 dark:hover:bg-purple-900/30 transition-all text-left disabled:opacity-60">
|
||||||
|
{generateLoading === 'slides' ? <Loader2 className="h-4 w-4 shrink-0 animate-spin" /> : <Presentation className="h-4 w-4 shrink-0" />}
|
||||||
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<span>{t('ai.generate.slides') || 'Générer une présentation'}</span>
|
||||||
|
{generateLoading === 'slides' && <span className="text-[10px] text-purple-500">{t('ai.generate.loading') || 'Génération en cours…'}</span>}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slides result */}
|
||||||
|
{generateResult?.type === 'slides' && generateResult.canvasId && (
|
||||||
|
<a href={`/api/canvas/download?id=${generateResult.canvasId}`} target="_blank" rel="noopener noreferrer"
|
||||||
|
className="w-full flex items-center gap-2 rounded-xl border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30 px-4 py-2.5 text-sm font-medium text-green-700 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/40 transition-all mb-2">
|
||||||
|
<Download className="h-4 w-4 shrink-0" />
|
||||||
|
{t('ai.generate.downloadPptx') || 'Télécharger le .pptx'}
|
||||||
|
<ExternalLink className="h-3 w-3 ml-auto opacity-60" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Diagram ── */}
|
||||||
|
<div className="rounded-xl border-2 border-cyan-200 dark:border-cyan-800 bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-950/40 dark:to-blue-950/40 overflow-hidden">
|
||||||
|
<div className="grid grid-cols-2 gap-1.5 px-3 pt-2.5 pb-1">
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-semibold uppercase tracking-wider text-cyan-500 dark:text-cyan-400 block mb-0.5">{t('ai.generate.diagramType') || 'Type'}</label>
|
||||||
|
<select value={diagramType} onChange={e => setDiagramType(e.target.value)} disabled={generateLoading === 'diagram'}
|
||||||
|
className="w-full text-[11px] rounded-md border border-cyan-200 dark:border-cyan-700 bg-white/70 dark:bg-cyan-950/60 text-cyan-800 dark:text-cyan-200 px-1.5 py-1 focus:outline-none">
|
||||||
|
<option value="auto">{t('ai.generate.typeAuto') || 'Auto'}</option>
|
||||||
|
<option value="flowchart">Flowchart</option>
|
||||||
|
<option value="mindmap">Mind map</option>
|
||||||
|
<option value="timeline">Timeline</option>
|
||||||
|
<option value="org-chart">Org chart</option>
|
||||||
|
<option value="architecture-cloud">Architecture</option>
|
||||||
|
<option value="process-map">Process map</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[9px] font-semibold uppercase tracking-wider text-cyan-500 dark:text-cyan-400 block mb-0.5">{t('ai.generate.style') || 'Style'}</label>
|
||||||
|
<select value={diagramStyle} onChange={e => setDiagramStyle(e.target.value)} disabled={generateLoading === 'diagram'}
|
||||||
|
className="w-full text-[11px] rounded-md border border-cyan-200 dark:border-cyan-700 bg-white/70 dark:bg-cyan-950/60 text-cyan-800 dark:text-cyan-200 px-1.5 py-1 focus:outline-none">
|
||||||
|
<option value="default">{t('ai.generate.styleSketchy') || 'Sketchy'}</option>
|
||||||
|
<option value="austere">{t('ai.generate.styleAustere') || 'Austère'}</option>
|
||||||
|
<option value="sketch-plus">Sketch+</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => handleGenerate('diagram')} disabled={!!generateLoading || !!actionLoading}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2.5 text-sm font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100/60 dark:hover:bg-cyan-900/30 transition-all text-left disabled:opacity-60">
|
||||||
|
{generateLoading === 'diagram' ? <Loader2 className="h-4 w-4 shrink-0 animate-spin" /> : <PenTool className="h-4 w-4 shrink-0" />}
|
||||||
|
<div className="flex flex-col flex-1 min-w-0">
|
||||||
|
<span>{t('ai.generate.diagram') || 'Générer un diagramme'}</span>
|
||||||
|
{generateLoading === 'diagram' && <span className="text-[10px] text-cyan-500">{t('ai.generate.loading') || 'Génération en cours…'}</span>}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diagram result */}
|
||||||
|
{generateResult?.type === 'diagram' && generateResult.canvasId && (
|
||||||
|
<a href={`/lab?id=${generateResult.canvasId}`}
|
||||||
|
className="mt-2 w-full flex items-center gap-2 rounded-xl border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30 px-4 py-2.5 text-sm font-medium text-green-700 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/40 transition-all">
|
||||||
|
<ExternalLink className="h-4 w-4 shrink-0" />
|
||||||
|
{t('ai.generate.openDiagram') || 'Ouvrir dans le Lab'}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Undo last action shortcut */}
|
{/* Undo last action shortcut */}
|
||||||
{lastActionApplied && onUndoLastAction && (
|
{lastActionApplied && onUndoLastAction && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { Download, Presentation } from 'lucide-react'
|
||||||
import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'
|
import type { ExcalidrawElement } from '@excalidraw/excalidraw/element/types'
|
||||||
import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types'
|
import type { AppState, BinaryFiles } from '@excalidraw/excalidraw/types'
|
||||||
|
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types'
|
||||||
import '@excalidraw/excalidraw/index.css'
|
import '@excalidraw/excalidraw/index.css'
|
||||||
|
|
||||||
// Dynamic import with SSR disabled is REQUIRED for Excalidraw due to window dependencies
|
|
||||||
const Excalidraw = dynamic(
|
const Excalidraw = dynamic(
|
||||||
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
|
async () => (await import('@excalidraw/excalidraw')).Excalidraw,
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
@@ -19,34 +20,106 @@ interface CanvasBoardProps {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PptxPayload = { type: string; filename: string; base64: string; slideCount?: number; theme?: string }
|
||||||
|
|
||||||
|
function parseCanvasScene(initialData?: string): {
|
||||||
|
pptx: PptxPayload | null
|
||||||
|
elements: readonly ExcalidrawElement[]
|
||||||
|
files: BinaryFiles
|
||||||
|
} {
|
||||||
|
if (!initialData) {
|
||||||
|
return { pptx: null, elements: [], files: {} }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(initialData)
|
||||||
|
if (parsed && parsed.type === 'pptx' && parsed.base64) {
|
||||||
|
return { pptx: parsed as PptxPayload, elements: [], files: {} }
|
||||||
|
}
|
||||||
|
if (parsed && Array.isArray(parsed)) {
|
||||||
|
return { pptx: null, elements: parsed as ExcalidrawElement[], files: {} }
|
||||||
|
}
|
||||||
|
if (parsed && parsed.elements) {
|
||||||
|
const files: BinaryFiles =
|
||||||
|
parsed.files && typeof parsed.files === 'object' ? parsed.files : {}
|
||||||
|
return { pptx: null, elements: parsed.elements as readonly ExcalidrawElement[], files }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[CanvasBoard] Failed to parse canvas data:', e)
|
||||||
|
}
|
||||||
|
return { pptx: null, elements: [], files: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
function PptxViewer({ data, name }: { data: PptxPayload; name: string }) {
|
||||||
|
const handleDownload = () => {
|
||||||
|
try {
|
||||||
|
const byteCharacters = atob(data.base64)
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
const blob = new Blob([byteArray], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = data.filename || `${name}.pptx`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[PptxViewer] Download failed:', e)
|
||||||
|
toast.error('Failed to download presentation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 h-full w-full flex flex-col items-center justify-center bg-white dark:bg-zinc-900">
|
||||||
|
<div className="flex flex-col items-center gap-6 p-8">
|
||||||
|
<div className="p-4 rounded-2xl bg-primary/10">
|
||||||
|
<Presentation className="w-16 h-16 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold text-foreground mb-1">{name}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
PowerPoint Presentation
|
||||||
|
{data.slideCount ? ` • ${data.slideCount} slides` : ''}
|
||||||
|
{data.theme ? ` • ${data.theme} theme` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-colors font-medium shadow-sm"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
Download .pptx
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-muted-foreground max-w-sm text-center">
|
||||||
|
This file can be opened in Microsoft PowerPoint, Google Slides, or Keynote.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved')
|
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved')
|
||||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const excalidrawAPIRef = useRef<ExcalidrawImperativeAPI | null>(null)
|
||||||
const filesRef = useRef<BinaryFiles>({})
|
const filesRef = useRef<BinaryFiles>({})
|
||||||
|
|
||||||
// Parse initial state safely (ONLY ON MOUNT to prevent Next.js revalidation infinite loops)
|
const scene = useMemo(
|
||||||
const [elements] = useState<readonly ExcalidrawElement[]>(() => {
|
() => parseCanvasScene(initialData),
|
||||||
if (initialData) {
|
[canvasId, initialData]
|
||||||
try {
|
)
|
||||||
const parsed = JSON.parse(initialData)
|
|
||||||
if (parsed && Array.isArray(parsed)) {
|
useEffect(() => {
|
||||||
return parsed
|
filesRef.current = scene.files
|
||||||
} else if (parsed && parsed.elements) {
|
}, [scene])
|
||||||
// Restore binary files if present
|
|
||||||
if (parsed.files && typeof parsed.files === 'object') {
|
|
||||||
filesRef.current = parsed.files
|
|
||||||
}
|
|
||||||
return parsed.elements
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[CanvasBoard] Failed to parse initial Excalidraw data:", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
// Detect dark mode from html class
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkDarkMode = () => {
|
const checkDarkMode = () => {
|
||||||
const isDark = document.documentElement.classList.contains('dark')
|
const isDark = document.documentElement.classList.contains('dark')
|
||||||
@@ -55,7 +128,6 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
|||||||
|
|
||||||
checkDarkMode()
|
checkDarkMode()
|
||||||
|
|
||||||
// Observer for theme changes
|
|
||||||
const observer = new MutationObserver(checkDarkMode)
|
const observer = new MutationObserver(checkDarkMode)
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||||
|
|
||||||
@@ -64,10 +136,9 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
|||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
excalidrawElements: readonly ExcalidrawElement[],
|
excalidrawElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
_appState: AppState,
|
||||||
files: BinaryFiles
|
files: BinaryFiles
|
||||||
) => {
|
) => {
|
||||||
// Keep files ref up to date so we can include them in the save payload
|
|
||||||
if (files) filesRef.current = files
|
if (files) filesRef.current = files
|
||||||
|
|
||||||
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
|
||||||
@@ -75,7 +146,6 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
|||||||
setSaveStatus('saving')
|
setSaveStatus('saving')
|
||||||
saveTimeoutRef.current = setTimeout(async () => {
|
saveTimeoutRef.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
// Save both elements and binary files so images persist across page changes
|
|
||||||
const snapshot = JSON.stringify({ elements: excalidrawElements, files: filesRef.current })
|
const snapshot = JSON.stringify({ elements: excalidrawElements, files: filesRef.current })
|
||||||
await fetch('/api/canvas', {
|
await fetch('/api/canvas', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -84,26 +154,35 @@ export function CanvasBoard({ initialData, canvasId, name }: CanvasBoardProps) {
|
|||||||
})
|
})
|
||||||
setSaveStatus('saved')
|
setSaveStatus('saved')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[CanvasBoard] Save failure:", e)
|
console.error('[CanvasBoard] Save failure:', e)
|
||||||
setSaveStatus('error')
|
setSaveStatus('error')
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scene.pptx) {
|
||||||
|
return <PptxViewer data={scene.pptx} name={name} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const excalKey = canvasId ? `excal-${canvasId}` : 'excal-new'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 h-full w-full bg-slate-50 dark:bg-[#121212]" dir="ltr">
|
<div className="absolute inset-0 h-full w-full bg-slate-50 dark:bg-[#121212]" dir="ltr">
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
initialData={{ elements, files: filesRef.current }}
|
key={excalKey}
|
||||||
theme={isDarkMode ? "dark" : "light"}
|
excalidrawAPI={(api) => { excalidrawAPIRef.current = api }}
|
||||||
|
initialData={{ elements: scene.elements, files: scene.files }}
|
||||||
|
theme={isDarkMode ? 'dark' : 'light'}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
libraryReturnUrl={typeof window !== 'undefined' ? window.location.origin + window.location.pathname + window.location.search : undefined}
|
|
||||||
UIOptions={{
|
UIOptions={{
|
||||||
canvasActions: {
|
canvasActions: {
|
||||||
loadScene: false,
|
loadScene: true,
|
||||||
saveToActiveFile: false,
|
saveToActiveFile: false,
|
||||||
clearCanvas: true
|
clearCanvas: true,
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
validateEmbeddable={false}
|
||||||
|
renderTopRightUI={() => null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
<Dialog open={true} onOpenChange={onClose}>
|
<Dialog open={true} onOpenChange={onClose}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-hidden p-0 flex flex-row items-stretch',
|
'!max-w-[min(95vw,1600px)] max-h-[90vh] overflow-hidden p-0 flex flex-row items-stretch rounded-lg',
|
||||||
colorClasses.bg
|
colorClasses.bg
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -875,18 +875,18 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
{/* Reminder */}
|
{/* Reminder */}
|
||||||
<Button variant="ghost" size="icon" className={cn('h-8 w-8', currentReminder && 'text-primary')}
|
<Button variant="ghost" size="icon" className={cn('h-8 w-8 rounded-md', currentReminder && 'text-primary')}
|
||||||
onClick={() => setShowReminderDialog(true)} title={t('notes.setReminder')}>
|
onClick={() => setShowReminderDialog(true)} title={t('notes.setReminder')}>
|
||||||
<Bell className="h-4 w-4" />
|
<Bell className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/* Add Image */}
|
{/* Add Image */}
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||||
onClick={() => fileInputRef.current?.click()} title={t('notes.addImage')}>
|
onClick={() => fileInputRef.current?.click()} title={t('notes.addImage')}>
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Add Link */}
|
{/* Add Link */}
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||||
onClick={() => setShowLinkDialog(true)} title={t('notes.addLink')}>
|
onClick={() => setShowLinkDialog(true)} title={t('notes.addLink')}>
|
||||||
<LinkIcon className="h-4 w-4" />
|
<LinkIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -894,7 +894,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
<NoteTypeSelector value={noteType} onChange={(newType) => { setNoteType(newType); if (newType !== 'markdown') setShowMarkdownPreview(false) }} />
|
<NoteTypeSelector value={noteType} onChange={(newType) => { setNoteType(newType); if (newType !== 'markdown') setShowMarkdownPreview(false) }} />
|
||||||
|
|
||||||
{noteType === 'markdown' && (
|
{noteType === 'markdown' && (
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8"
|
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md"
|
||||||
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
onClick={() => setShowMarkdownPreview(!showMarkdownPreview)}
|
||||||
title={showMarkdownPreview ? t('general.edit') : t('notes.preview')}>
|
title={showMarkdownPreview ? t('general.edit') : t('notes.preview')}>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
@@ -904,7 +904,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
{/* AI Copilot */}
|
{/* AI Copilot */}
|
||||||
{noteType !== 'checklist' && aiAssistantEnabled && (
|
{noteType !== 'checklist' && aiAssistantEnabled && (
|
||||||
<Button variant="ghost" size="sm"
|
<Button variant="ghost" size="sm"
|
||||||
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-colors', aiOpen && 'bg-primary/10 text-primary')}
|
className={cn('h-8 gap-1.5 px-2 text-xs font-medium transition-all duration-200 rounded-md', aiOpen && 'bg-primary/10 text-primary')}
|
||||||
onClick={() => setAiOpen(!aiOpen)} title="IA Note">
|
onClick={() => setAiOpen(!aiOpen)} title="IA Note">
|
||||||
<Sparkles className="h-3.5 w-3.5" />
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
<span className="hidden sm:inline">IA Note</span>
|
<span className="hidden sm:inline">IA Note</span>
|
||||||
@@ -914,7 +914,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
{/* Size Selector */}
|
{/* Size Selector */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" title={t('notes.changeSize')}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md" title={t('notes.changeSize')}>
|
||||||
<Maximize2 className="h-4 w-4" />
|
<Maximize2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -934,7 +934,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
{/* Color Picker */}
|
{/* Color Picker */}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" title={t('notes.changeColor')}>
|
<Button variant="ghost" size="icon" className="h-8 w-8 rounded-md" title={t('notes.changeColor')}>
|
||||||
<Palette className="h-4 w-4" />
|
<Palette className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -1023,6 +1023,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
|
|||||||
noteTitle={title}
|
noteTitle={title}
|
||||||
noteContent={content}
|
noteContent={content}
|
||||||
noteImages={allImages}
|
noteImages={allImages}
|
||||||
|
noteId={note.id}
|
||||||
onApplyToNote={(newContent) => {
|
onApplyToNote={(newContent) => {
|
||||||
setPreviousContentForCopilot(content)
|
setPreviousContentForCopilot(content)
|
||||||
setContent(newContent)
|
setContent(newContent)
|
||||||
|
|||||||
@@ -919,6 +919,7 @@ export function NoteInlineEditor({
|
|||||||
noteTitle={title}
|
noteTitle={title}
|
||||||
noteContent={content}
|
noteContent={content}
|
||||||
noteImages={allImages}
|
noteImages={allImages}
|
||||||
|
noteId={note.id}
|
||||||
onApplyToNote={(newContent) => {
|
onApplyToNote={(newContent) => {
|
||||||
setPreviousContent(content)
|
setPreviousContent(content)
|
||||||
changeContent(newContent)
|
changeContent(newContent)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const toast = sonnerToast
|
|||||||
export function Toaster() {
|
export function Toaster() {
|
||||||
return (
|
return (
|
||||||
<SonnerToaster
|
<SonnerToaster
|
||||||
position="top-right"
|
position="bottom-right"
|
||||||
expand={false}
|
expand={false}
|
||||||
richColors
|
richColors
|
||||||
closeButton
|
closeButton
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ export class CustomOpenAIProvider implements AIProvider {
|
|||||||
const headers = new Headers(options?.headers);
|
const headers = new Headers(options?.headers);
|
||||||
headers.set('HTTP-Referer', 'https://localhost:3000');
|
headers.set('HTTP-Referer', 'https://localhost:3000');
|
||||||
headers.set('X-Title', 'Memento AI');
|
headers.set('X-Title', 'Memento AI');
|
||||||
|
// Disable DeepSeek extended thinking for reliable tool/function calling
|
||||||
|
if (options?.body) {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(options.body as string)
|
||||||
|
if (
|
||||||
|
typeof body.model === 'string' &&
|
||||||
|
(body.model.includes('deepseek') || body.model.includes('thinking') || body.model.includes('reasoner'))
|
||||||
|
) {
|
||||||
|
body.thinking = { type: 'disabled' }
|
||||||
|
}
|
||||||
|
return fetch(url, { ...options, headers, body: JSON.stringify(body) })
|
||||||
|
} catch { /* ignore parse errors */ }
|
||||||
|
}
|
||||||
return fetch(url, { ...options, headers });
|
return fetch(url, { ...options, headers });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -119,25 +132,40 @@ export class CustomOpenAIProvider implements AIProvider {
|
|||||||
|
|
||||||
async generateWithTools(options: ToolUseOptions): Promise<ToolCallResult> {
|
async generateWithTools(options: ToolUseOptions): Promise<ToolCallResult> {
|
||||||
const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options
|
const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options
|
||||||
const opts: Record<string, any> = {
|
|
||||||
model: this.model,
|
|
||||||
tools,
|
|
||||||
stopWhen: stepCountIs(maxSteps),
|
|
||||||
}
|
|
||||||
if (systemPrompt) opts.system = systemPrompt
|
|
||||||
if (messages) opts.messages = messages
|
|
||||||
else if (prompt) opts.prompt = prompt
|
|
||||||
|
|
||||||
const result = await aiGenerateText(opts as any)
|
const buildOpts = (steps: number): Record<string, any> => {
|
||||||
return {
|
const opts: Record<string, any> = { model: this.model, tools, stopWhen: stepCountIs(steps) }
|
||||||
toolCalls: result.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
if (systemPrompt) opts.system = systemPrompt
|
||||||
toolResults: result.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
if (messages) opts.messages = messages
|
||||||
text: result.text,
|
else if (prompt) opts.prompt = prompt
|
||||||
steps: result.steps?.map((step: any) => ({
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
const toResult = (r: any): ToolCallResult => ({
|
||||||
|
toolCalls: r.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||||
|
toolResults: r.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||||
|
text: r.text,
|
||||||
|
steps: r.steps?.map((step: any) => ({
|
||||||
text: step.text,
|
text: step.text,
|
||||||
toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||||
toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || []
|
toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||||
})) || []
|
})) || [],
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await aiGenerateText(buildOpts(maxSteps) as any)
|
||||||
|
return toResult(result)
|
||||||
|
} catch (err: any) {
|
||||||
|
// DeepSeek reasoning/thinking models require reasoning_content to be passed back
|
||||||
|
// between multi-step calls, which the AI SDK doesn't handle via the OpenAI-compat layer.
|
||||||
|
// Retry with a single step so the model calls the tool directly.
|
||||||
|
const msg: string = err?.message || String(err)
|
||||||
|
if (msg.includes('reasoning_content') || msg.includes('thinking mode')) {
|
||||||
|
console.warn('[CustomOpenAI] Reasoning model detected — retrying with maxSteps=1')
|
||||||
|
const result = await aiGenerateText(buildOpts(1) as any)
|
||||||
|
return toResult(result)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,21 @@ export class DeepSeekProvider implements AIProvider {
|
|||||||
|
|
||||||
constructor(apiKey: string, modelName: string = 'deepseek-chat', embeddingModelName: string = 'deepseek-embedding') {
|
constructor(apiKey: string, modelName: string = 'deepseek-chat', embeddingModelName: string = 'deepseek-embedding') {
|
||||||
// Create OpenAI-compatible client for DeepSeek
|
// Create OpenAI-compatible client for DeepSeek
|
||||||
|
// Disable extended thinking to ensure reliable tool/function calling
|
||||||
const deepseek = createOpenAI({
|
const deepseek = createOpenAI({
|
||||||
baseURL: 'https://api.deepseek.com/v1',
|
baseURL: 'https://api.deepseek.com/v1',
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
fetch: async (url, options) => {
|
||||||
|
if (options?.body) {
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(options.body as string)
|
||||||
|
// Disable thinking mode — tool calling is unreliable with it enabled
|
||||||
|
body.thinking = { type: 'disabled' }
|
||||||
|
return fetch(url, { ...options, body: JSON.stringify(body) })
|
||||||
|
} catch { /* ignore parse errors */ }
|
||||||
|
}
|
||||||
|
return fetch(url, options)
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.model = deepseek.chat(modelName);
|
this.model = deepseek.chat(modelName);
|
||||||
@@ -103,25 +115,40 @@ export class DeepSeekProvider implements AIProvider {
|
|||||||
|
|
||||||
async generateWithTools(options: ToolUseOptions): Promise<ToolCallResult> {
|
async generateWithTools(options: ToolUseOptions): Promise<ToolCallResult> {
|
||||||
const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options
|
const { tools, maxSteps = 10, systemPrompt, messages, prompt } = options
|
||||||
const opts: Record<string, any> = {
|
|
||||||
model: this.model,
|
|
||||||
tools,
|
|
||||||
stopWhen: stepCountIs(maxSteps),
|
|
||||||
}
|
|
||||||
if (systemPrompt) opts.system = systemPrompt
|
|
||||||
if (messages) opts.messages = messages
|
|
||||||
else if (prompt) opts.prompt = prompt
|
|
||||||
|
|
||||||
const result = await aiGenerateText(opts as any)
|
const buildOpts = (steps: number): Record<string, any> => {
|
||||||
return {
|
const opts: Record<string, any> = { model: this.model, tools, stopWhen: stepCountIs(steps) }
|
||||||
toolCalls: result.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
if (systemPrompt) opts.system = systemPrompt
|
||||||
toolResults: result.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
if (messages) opts.messages = messages
|
||||||
text: result.text,
|
else if (prompt) opts.prompt = prompt
|
||||||
steps: result.steps?.map((step: any) => ({
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
const toResult = (r: any): ToolCallResult => ({
|
||||||
|
toolCalls: r.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||||
|
toolResults: r.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||||
|
text: r.text,
|
||||||
|
steps: r.steps?.map((step: any) => ({
|
||||||
text: step.text,
|
text: step.text,
|
||||||
toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
toolCalls: step.toolCalls?.map((tc: any) => ({ toolName: tc.toolName, input: tc.input })) || [],
|
||||||
toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || []
|
toolResults: step.toolResults?.map((tr: any) => ({ toolName: tr.toolName, input: tr.input, output: tr.output })) || [],
|
||||||
})) || []
|
})) || [],
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await aiGenerateText(buildOpts(maxSteps) as any)
|
||||||
|
return toResult(result)
|
||||||
|
} catch (err: any) {
|
||||||
|
// DeepSeek reasoning/thinking models require reasoning_content to be passed back
|
||||||
|
// between multi-step calls, which the AI SDK doesn't handle automatically.
|
||||||
|
// Retry with a single step so the model calls the tool directly without multi-turn.
|
||||||
|
const msg: string = err?.message || String(err)
|
||||||
|
if (msg.includes('reasoning_content') || msg.includes('thinking mode')) {
|
||||||
|
console.warn('[DeepSeek] Reasoning model detected — retrying with maxSteps=1')
|
||||||
|
const result = await aiGenerateText(buildOpts(1) as any)
|
||||||
|
return toResult(result)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import { tool } from 'ai'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { toolRegistry } from './registry'
|
import { toolRegistry } from './registry'
|
||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import dagre from 'dagre'
|
// import type is erased at build time — Turbopack won't try to resolve the module
|
||||||
|
import type dagreType from 'dagre'
|
||||||
|
let _dagre: typeof dagreType | null = null
|
||||||
|
async function getDagre(): Promise<typeof dagreType> {
|
||||||
|
if (!_dagre) {
|
||||||
|
const mod = await import('dagre')
|
||||||
|
_dagre = (mod.default ?? mod) as typeof dagreType
|
||||||
|
}
|
||||||
|
return _dagre
|
||||||
|
}
|
||||||
|
|
||||||
interface SimplifiedNode {
|
interface SimplifiedNode {
|
||||||
id: string
|
id: string
|
||||||
@@ -504,12 +513,13 @@ function getNodeRenderSpec(node: SimplifiedNode, isCenter: boolean): NodeRenderS
|
|||||||
return { text: wrapped, fontSize, width, height }
|
return { text: wrapped, fontSize, width, height }
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeNodeLayoutForRankdir(
|
async function computeNodeLayoutForRankdir(
|
||||||
nodes: SimplifiedNode[],
|
nodes: SimplifiedNode[],
|
||||||
edges: SimplifiedEdge[],
|
edges: SimplifiedEdge[],
|
||||||
rankdir: 'LR' | 'TB',
|
rankdir: 'LR' | 'TB',
|
||||||
renderSpecs: Map<string, NodeRenderSpec>,
|
renderSpecs: Map<string, NodeRenderSpec>,
|
||||||
): Map<string, NodeLayoutBox> {
|
): Promise<Map<string, NodeLayoutBox>> {
|
||||||
|
const dagre = await getDagre()
|
||||||
const graph = new dagre.graphlib.Graph()
|
const graph = new dagre.graphlib.Graph()
|
||||||
graph.setGraph({
|
graph.setGraph({
|
||||||
rankdir,
|
rankdir,
|
||||||
@@ -738,11 +748,11 @@ async function computeNodeLayout(
|
|||||||
const firstRankdir: 'LR' | 'TB' = (diagramType === 'org-chart') ? 'TB' : 'LR'
|
const firstRankdir: 'LR' | 'TB' = (diagramType === 'org-chart') ? 'TB' : 'LR'
|
||||||
const secondRankdir: 'LR' | 'TB' = firstRankdir === 'LR' ? 'TB' : 'LR'
|
const secondRankdir: 'LR' | 'TB' = firstRankdir === 'LR' ? 'TB' : 'LR'
|
||||||
|
|
||||||
const lrLayout = computeNodeLayoutForRankdir(nodes, edges, firstRankdir, renderSpecs)
|
const lrLayout = await computeNodeLayoutForRankdir(nodes, edges, firstRankdir, renderSpecs)
|
||||||
const lrQuality = computeLayoutQuality(nodes, edges, lrLayout)
|
const lrQuality = computeLayoutQuality(nodes, edges, lrLayout)
|
||||||
const lrBounds = getLayoutBounds(lrLayout)
|
const lrBounds = getLayoutBounds(lrLayout)
|
||||||
|
|
||||||
const tbLayout = computeNodeLayoutForRankdir(nodes, edges, secondRankdir, renderSpecs)
|
const tbLayout = await computeNodeLayoutForRankdir(nodes, edges, secondRankdir, renderSpecs)
|
||||||
const tbQuality = computeLayoutQuality(nodes, edges, tbLayout)
|
const tbQuality = computeLayoutQuality(nodes, edges, tbLayout)
|
||||||
const tbBounds = getLayoutBounds(tbLayout)
|
const tbBounds = getLayoutBounds(tbLayout)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
|
||||||
import PptxGenJS from 'pptxgenjs'
|
// import type is erased at build time — Turbopack won't try to resolve the module
|
||||||
|
import type PptxGenJSModule from 'pptxgenjs'
|
||||||
|
// Lazy singleton — actual module loaded at runtime only
|
||||||
|
let _PptxGenJS: (new () => PptxGenJSModule) | null = null
|
||||||
|
async function getPptxGenClass(): Promise<new () => PptxGenJSModule> {
|
||||||
|
if (!_PptxGenJS) {
|
||||||
|
const mod = await import('pptxgenjs')
|
||||||
|
_PptxGenJS = (mod.default ?? mod) as unknown as new () => PptxGenJSModule
|
||||||
|
}
|
||||||
|
return _PptxGenJS
|
||||||
|
}
|
||||||
import { tool } from 'ai'
|
import { tool } from 'ai'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { toolRegistry } from './registry'
|
import { toolRegistry } from './registry'
|
||||||
@@ -105,7 +115,7 @@ function addBadge(s: any, num: number, accent: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Modern split-layout cover: white left panel with big title, colored right panel */
|
/** Modern split-layout cover: white left panel with big title, colored right panel */
|
||||||
function addCoverSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg) {
|
function addCoverSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -167,7 +177,7 @@ function addCoverSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Style
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTocSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addTocSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -216,7 +226,7 @@ function addTocSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCf
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Section divider: full-bleed colored left panel + large decorative number on right */
|
/** Section divider: full-bleed colored left panel + large decorative number on right */
|
||||||
function addSectionSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addSectionSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -256,7 +266,7 @@ function addSectionSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Sty
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addContentSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addContentSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -273,7 +283,7 @@ function addContentSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Sty
|
|||||||
|
|
||||||
const bullets = slide.content.map((line, i) => ({
|
const bullets = slide.content.map((line, i) => ({
|
||||||
text: line,
|
text: line,
|
||||||
options: { fontSize: 15, fontFace: 'Arial', color: textColor, bullet: { type: 'bullet', indent: 10 }, breakLine: true, paraSpaceAfter: 10, paraSpaceBefore: i === 0 ? 0 : 2 },
|
options: { fontSize: 15, fontFace: 'Arial', color: textColor, bullet: { type: 'bullet' as const, indent: 10 }, breakLine: true, paraSpaceAfter: 10, paraSpaceBefore: i === 0 ? 0 : 2 },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
s.addText(bullets, {
|
s.addText(bullets, {
|
||||||
@@ -285,7 +295,7 @@ function addContentSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Sty
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTwoColumnSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addTwoColumnSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -324,7 +334,7 @@ function addTwoColumnSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: S
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addCardsSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -403,7 +413,7 @@ function addCardsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Style
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStatsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addStatsSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -449,7 +459,7 @@ function addStatsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Style
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addQuoteSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addQuoteSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.primary }
|
s.background = { color: t.primary }
|
||||||
const textColor = textOnBg(t.primary)
|
const textColor = textOnBg(t.primary)
|
||||||
@@ -479,7 +489,7 @@ function addQuoteSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Style
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSummarySlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addSummarySlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.light }
|
s.background = { color: t.light }
|
||||||
const textColor = textOnBg(t.light)
|
const textColor = textOnBg(t.light)
|
||||||
@@ -525,7 +535,7 @@ function resolveImageProps(imageUrl: string): Record<string, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Image on the right (40%), bullets on the left (55%) */
|
/** Image on the right (40%), bullets on the left (55%) */
|
||||||
function addImageContentSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addImageContentSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -589,7 +599,7 @@ function addImageContentSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Full-bleed image slide with title + subtitle overlay */
|
/** Full-bleed image slide with title + subtitle overlay */
|
||||||
function addImageFullSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addImageFullSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.primary }
|
s.background = { color: t.primary }
|
||||||
|
|
||||||
@@ -600,7 +610,7 @@ function addImageFullSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dark overlay at bottom for text legibility
|
// Dark overlay at bottom for text legibility
|
||||||
s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 10, h: 2.33, fill: { color: '000000' }, transparency: 40 })
|
s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 10, h: 2.33, fill: { color: '000000', transparency: 40 } })
|
||||||
s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 0.12, h: 2.33, fill: { color: t.accent } })
|
s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 0.12, h: 2.33, fill: { color: t.accent } })
|
||||||
|
|
||||||
s.addText(slide.title, {
|
s.addText(slide.title, {
|
||||||
@@ -619,7 +629,7 @@ function addImageFullSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: S
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Horizontal timeline — content items: "Step title: description" */
|
/** Horizontal timeline — content items: "Step title: description" */
|
||||||
function addTimelineSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addTimelineSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -726,7 +736,7 @@ function addTimelineSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: St
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Vertical numbered steps with connecting line — content items: "Step title: description" */
|
/** Vertical numbered steps with connecting line — content items: "Step title: description" */
|
||||||
function addProcessSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addProcessSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
const textColor = textOnBg(t.bg)
|
const textColor = textOnBg(t.bg)
|
||||||
@@ -796,7 +806,7 @@ function addProcessSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Sty
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Two-panel comparison — subtitle: "Left Label | Right Label", content split 50/50 */
|
/** Two-panel comparison — subtitle: "Left Label | Right Label", content split 50/50 */
|
||||||
function addComparisonSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addComparisonSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -864,7 +874,7 @@ function addComparisonSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Visual KPI blocks — content items: "VALUE: label" or "VALUE - label" */
|
/** Visual KPI blocks — content items: "VALUE: label" or "VALUE - label" */
|
||||||
function addMetricsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
function addMetricsSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -922,7 +932,7 @@ function addMetricsSlide(pres: PptxGenJS, slide: SlideSpec, t: Theme, style: Sty
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function addClosingSlide(pres: PptxGenJS, spec: PresentationSpec, t: Theme, style: StyleCfg) {
|
function addClosingSlide(pres: PptxGenJSModule, spec: PresentationSpec, t: Theme, style: StyleCfg) {
|
||||||
const s = pres.addSlide()
|
const s = pres.addSlide()
|
||||||
s.background = { color: t.bg }
|
s.background = { color: t.bg }
|
||||||
|
|
||||||
@@ -960,10 +970,11 @@ function addClosingSlide(pres: PptxGenJS, spec: PresentationSpec, t: Theme, styl
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPresentation(spec: PresentationSpec): PptxGenJS {
|
async function buildPresentation(spec: PresentationSpec): Promise<PptxGenJSModule> {
|
||||||
const { theme } = resolveTheme(spec)
|
const { theme } = resolveTheme(spec)
|
||||||
const style = STYLES[spec.style || 'soft'] || STYLES.soft!
|
const style = STYLES[spec.style || 'soft'] || STYLES.soft!
|
||||||
|
|
||||||
|
const PptxGenJS = await getPptxGenClass()
|
||||||
const pres = new PptxGenJS()
|
const pres = new PptxGenJS()
|
||||||
pres.title = spec.title
|
pres.title = spec.title
|
||||||
pres.author = 'Momento'
|
pres.author = 'Momento'
|
||||||
@@ -1111,7 +1122,27 @@ RULES:
|
|||||||
|
|
||||||
if (spec.slides.length === 0) return { success: false, error: 'No slides provided' }
|
if (spec.slides.length === 0) return { success: false, error: 'No slides provided' }
|
||||||
|
|
||||||
const pptx = buildPresentation(spec)
|
// Pre-fetch all image URLs server-side and convert to base64 data URIs.
|
||||||
|
// pptxgenjs on Node.js cannot reliably fetch authenticated or relative URLs,
|
||||||
|
// so we do it here where we have full server context.
|
||||||
|
for (const slide of spec.slides) {
|
||||||
|
if (!slide.imageUrl || slide.imageUrl.startsWith('data:')) continue
|
||||||
|
try {
|
||||||
|
const res = await fetch(slide.imageUrl, { signal: AbortSignal.timeout(8000) })
|
||||||
|
if (res.ok) {
|
||||||
|
const buf = await res.arrayBuffer()
|
||||||
|
const mime = res.headers.get('content-type') || 'image/png'
|
||||||
|
const b64 = Buffer.from(buf).toString('base64')
|
||||||
|
slide.imageUrl = `data:${mime};base64,${b64}`
|
||||||
|
} else {
|
||||||
|
slide.imageUrl = undefined // failed: show placeholder text
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
slide.imageUrl = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pptx = await buildPresentation(spec)
|
||||||
const base64 = await pptx.write({ outputType: 'base64' }) as string
|
const base64 = await pptx.write({ outputType: 'base64' }) as string
|
||||||
|
|
||||||
const canvas = await prisma.canvas.create({
|
const canvas = await prisma.canvas.create({
|
||||||
|
|||||||
@@ -436,6 +436,36 @@
|
|||||||
"translate": "Translate",
|
"translate": "Translate",
|
||||||
"explain": "Explain"
|
"explain": "Explain"
|
||||||
},
|
},
|
||||||
|
"generate": {
|
||||||
|
"sectionLabel": "Generate from this note",
|
||||||
|
"slides": "Generate a presentation",
|
||||||
|
"diagram": "Generate a diagram",
|
||||||
|
"loading": "Generating…",
|
||||||
|
"slidesReady": "Presentation generated!",
|
||||||
|
"diagramReady": "Diagram generated!",
|
||||||
|
"downloadPptx": "Download .pptx",
|
||||||
|
"openDiagram": "Open in Lab",
|
||||||
|
"error": "Error during generation",
|
||||||
|
"noNoteId": "Save the note first",
|
||||||
|
"theme": "Theme",
|
||||||
|
"style": "Style",
|
||||||
|
"diagramType": "Type",
|
||||||
|
"typeAuto": "Auto",
|
||||||
|
"styleSoft": "Soft",
|
||||||
|
"styleSharp": "Sharp",
|
||||||
|
"styleRounded": "Rounded",
|
||||||
|
"stylePill": "Pill",
|
||||||
|
"styleSketchy": "Sketchy",
|
||||||
|
"styleAustere": "Austere",
|
||||||
|
"styleSketchPlus": "Sketch+",
|
||||||
|
"toastLoading": {
|
||||||
|
"slides": "⏳ Generating presentation…",
|
||||||
|
"diagram": "⏳ Generating diagram…"
|
||||||
|
},
|
||||||
|
"toastLoadingDesc": "You can navigate freely, a notification will appear.",
|
||||||
|
"toastSuccessSlides": "Click Download in the AI panel.",
|
||||||
|
"toastSuccessDiagram": "Your diagram is available in the Lab."
|
||||||
|
},
|
||||||
"openAssistant": "Open AI Assistant",
|
"openAssistant": "Open AI Assistant",
|
||||||
"poweredByMomento": "Powered by Momento AI",
|
"poweredByMomento": "Powered by Momento AI",
|
||||||
"welcomeMsg": "Hello! I'm your AI assistant. How can I help you with your notes today? I can help refine tone, expand messaging, or summarize content.",
|
"welcomeMsg": "Hello! I'm your AI assistant. How can I help you with your notes today? I can help refine tone, expand messaging, or summarize content.",
|
||||||
@@ -1560,6 +1590,8 @@
|
|||||||
"updated": "Agent updated",
|
"updated": "Agent updated",
|
||||||
"deleted": "\"{name}\" deleted",
|
"deleted": "\"{name}\" deleted",
|
||||||
"deleteError": "Error deleting",
|
"deleteError": "Error deleting",
|
||||||
|
"running": "Generation in progress…",
|
||||||
|
"runningDesc": "Generation may take a few minutes. You can navigate freely.",
|
||||||
"runSuccess": "\"{name}\" executed successfully",
|
"runSuccess": "\"{name}\" executed successfully",
|
||||||
"runError": "Error: {error}",
|
"runError": "Error: {error}",
|
||||||
"runFailed": "Execution failed",
|
"runFailed": "Execution failed",
|
||||||
|
|||||||
@@ -436,6 +436,36 @@
|
|||||||
"translate": "Traduire",
|
"translate": "Traduire",
|
||||||
"explain": "Expliquer"
|
"explain": "Expliquer"
|
||||||
},
|
},
|
||||||
|
"generate": {
|
||||||
|
"sectionLabel": "Générer depuis cette note",
|
||||||
|
"slides": "Générer une présentation",
|
||||||
|
"diagram": "Générer un diagramme",
|
||||||
|
"loading": "Génération en cours…",
|
||||||
|
"slidesReady": "Présentation générée !",
|
||||||
|
"diagramReady": "Diagramme généré !",
|
||||||
|
"downloadPptx": "Télécharger le .pptx",
|
||||||
|
"openDiagram": "Ouvrir dans le Lab",
|
||||||
|
"error": "Erreur lors de la génération",
|
||||||
|
"noNoteId": "Enregistrez d'abord la note",
|
||||||
|
"theme": "Thème",
|
||||||
|
"style": "Style",
|
||||||
|
"diagramType": "Type",
|
||||||
|
"typeAuto": "Auto",
|
||||||
|
"styleSoft": "Soft",
|
||||||
|
"styleSharp": "Sharp",
|
||||||
|
"styleRounded": "Arrondi",
|
||||||
|
"stylePill": "Pill",
|
||||||
|
"styleSketchy": "Sketchy",
|
||||||
|
"styleAustere": "Austère",
|
||||||
|
"styleSketchPlus": "Sketch+",
|
||||||
|
"toastLoading": {
|
||||||
|
"slides": "⏳ Génération de la présentation en cours…",
|
||||||
|
"diagram": "⏳ Génération du diagramme en cours…"
|
||||||
|
},
|
||||||
|
"toastLoadingDesc": "Vous pouvez naviguer librement, une notification apparaîtra.",
|
||||||
|
"toastSuccessSlides": "Cliquez sur Télécharger dans le panneau IA.",
|
||||||
|
"toastSuccessDiagram": "Votre diagramme est disponible dans le Lab."
|
||||||
|
},
|
||||||
"openAssistant": "Ouvrir IA Note",
|
"openAssistant": "Ouvrir IA Note",
|
||||||
"poweredByMomento": "Propulsé par Momento AI",
|
"poweredByMomento": "Propulsé par Momento AI",
|
||||||
"welcomeMsg": "Bonjour ! Je suis votre assistant IA. Comment puis-je vous aider avec vos notes ? Je peux affiner le ton, développer un message ou résumer le contenu.",
|
"welcomeMsg": "Bonjour ! Je suis votre assistant IA. Comment puis-je vous aider avec vos notes ? Je peux affiner le ton, développer un message ou résumer le contenu.",
|
||||||
@@ -1638,6 +1668,8 @@
|
|||||||
"updated": "Agent mis à jour",
|
"updated": "Agent mis à jour",
|
||||||
"deleted": "\"{name}\" supprimé",
|
"deleted": "\"{name}\" supprimé",
|
||||||
"deleteError": "Erreur lors de la suppression",
|
"deleteError": "Erreur lors de la suppression",
|
||||||
|
"running": "Génération en cours…",
|
||||||
|
"runningDesc": "La génération peut prendre quelques minutes. Vous pouvez naviguer.",
|
||||||
"runSuccess": "\"{name}\" exécuté avec succès",
|
"runSuccess": "\"{name}\" exécuté avec succès",
|
||||||
"runError": "Erreur : {error}",
|
"runError": "Erreur : {error}",
|
||||||
"runFailed": "Exécution échouée",
|
"runFailed": "Exécution échouée",
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ const nextConfig: NextConfig = {
|
|||||||
// Enable standalone output for Docker
|
// Enable standalone output for Docker
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
|
||||||
|
// These server-side packages use Node.js internals (buffers, native modules, etc.)
|
||||||
|
// and must not be bundled by Turbopack — they are required directly by Node.js at runtime.
|
||||||
|
serverExternalPackages: ['pptxgenjs', 'dagre', 'elkjs'],
|
||||||
|
|
||||||
// Serve dynamically uploaded files via API route (public/ is read-only in production)
|
// Serve dynamically uploaded files via API route (public/ is read-only in production)
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
187
memento-note/package-lock.json
generated
187
memento-note/package-lock.json
generated
@@ -52,9 +52,11 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"diff": "^9.0.0",
|
"diff": "^9.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"elkjs": "^0.11.1",
|
||||||
"isomorphic-dompurify": "^3.12.0",
|
"isomorphic-dompurify": "^3.12.0",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
"katex": "^0.16.27",
|
"katex": "^0.16.27",
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
"nodemailer": "^8.0.4",
|
"nodemailer": "^8.0.4",
|
||||||
"novel": "^1.0.2",
|
"novel": "^1.0.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"pptxgenjs": "^4.0.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -86,6 +89,7 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
@@ -7113,6 +7117,13 @@
|
|||||||
"@types/d3-selection": "*"
|
"@types/d3-selection": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.54.tgz",
|
||||||
|
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||||
@@ -7956,6 +7967,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cose-base": {
|
"node_modules/cose-base": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
||||||
@@ -8589,6 +8606,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dagre": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dagre-d3-es": {
|
"node_modules/dagre-d3-es": {
|
||||||
"version": "7.0.14",
|
"version": "7.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
|
||||||
@@ -8810,6 +8837,12 @@
|
|||||||
"integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
|
"integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/elkjs": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
|
||||||
|
"license": "EPL-2.0"
|
||||||
|
},
|
||||||
"node_modules/encoding-sniffer": {
|
"node_modules/encoding-sniffer": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
|
||||||
@@ -9113,6 +9146,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/graphlib": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hachure-fill": {
|
"node_modules/hachure-fill": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
|
||||||
@@ -9367,6 +9409,12 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/https": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -9408,6 +9456,27 @@
|
|||||||
"pica": "^7.1.0"
|
"pica": "^7.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/image-size": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"queue": "6.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"image-size": "bin/image-size.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "4.3.8",
|
"version": "4.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz",
|
||||||
@@ -9539,6 +9608,12 @@
|
|||||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isexe": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -9733,6 +9808,24 @@
|
|||||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||||
},
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jszip/node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/katex": {
|
"node_modules/katex": {
|
||||||
"version": "0.16.45",
|
"version": "0.16.45",
|
||||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
|
||||||
@@ -9794,6 +9887,15 @@
|
|||||||
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
|
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
@@ -10070,6 +10172,12 @@
|
|||||||
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.18.1",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||||
@@ -12431,6 +12539,33 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pptxgenjs": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^22.8.1",
|
||||||
|
"https": "^1.0.0",
|
||||||
|
"image-size": "^1.2.1",
|
||||||
|
"jszip": "^3.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pptxgenjs/node_modules/@types/node": {
|
||||||
|
"version": "22.19.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
|
||||||
|
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pptxgenjs/node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.24.3",
|
"version": "10.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
|
||||||
@@ -12485,6 +12620,12 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/property-information": {
|
"node_modules/property-information": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||||
@@ -12726,6 +12867,15 @@
|
|||||||
"integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==",
|
"integrity": "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/queue": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "~2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/radix-ui": {
|
"node_modules/radix-ui": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
|
||||||
@@ -13048,6 +13198,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -13290,6 +13455,12 @@
|
|||||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -13370,6 +13541,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sharp": {
|
"node_modules/sharp": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
@@ -13501,6 +13678,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stringify-entities": {
|
"node_modules/stringify-entities": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||||
@@ -14150,7 +14336,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "NODE_OPTIONS='--max-old-space-size=4096' next dev --turbopack",
|
||||||
"build": "prisma generate && next build",
|
"build": "prisma generate && next build --webpack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "node scripts/safe-migrate.js",
|
"db:migrate": "node scripts/safe-migrate.js",
|
||||||
@@ -69,9 +69,11 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"diff": "^9.0.0",
|
"diff": "^9.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"elkjs": "^0.11.1",
|
||||||
"isomorphic-dompurify": "^3.12.0",
|
"isomorphic-dompurify": "^3.12.0",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
"katex": "^0.16.27",
|
"katex": "^0.16.27",
|
||||||
@@ -81,6 +83,7 @@
|
|||||||
"nodemailer": "^8.0.4",
|
"nodemailer": "^8.0.4",
|
||||||
"novel": "^1.0.2",
|
"novel": "^1.0.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"pptxgenjs": "^4.0.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -103,6 +106,7 @@
|
|||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@types/diff": "^7.0.2",
|
"@types/diff": "^7.0.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/nodemailer": "^7.0.4",
|
"@types/nodemailer": "^7.0.4",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- AlterTable: Agent — notes sources explicites + options slides (aligné schema.prisma)
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Agent" ADD COLUMN "sourceNoteIds" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Agent" ADD COLUMN "slideTheme" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Agent" ADD COLUMN "slideStyle" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL;
|
||||||
|
END $$;
|
||||||
@@ -311,6 +311,9 @@ model Agent {
|
|||||||
isEnabled Boolean @default(true)
|
isEnabled Boolean @default(true)
|
||||||
targetNotebookId String?
|
targetNotebookId String?
|
||||||
sourceNotebookId String?
|
sourceNotebookId String?
|
||||||
|
sourceNoteIds String?
|
||||||
|
slideTheme String?
|
||||||
|
slideStyle String?
|
||||||
tools String? @default("[]")
|
tools String? @default("[]")
|
||||||
maxSteps Int @default(10)
|
maxSteps Int @default(10)
|
||||||
notifyEmail Boolean @default(false)
|
notifyEmail Boolean @default(false)
|
||||||
|
|||||||
Reference in New Issue
Block a user