feat: options de génération (thème/style/type) + masquer agents one-shot
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 23s

- Les agents créés par run-for-note ont frequency:'one-shot' et sont
  filtrés de la page /agents (NOT frequency:'one-shot' dans getAgents)
- Panneau slides : sélecteurs Thème (11 palettes) + Style (sharp/soft/rounded/pill)
- Panneau diagramme : sélecteurs Type (auto/flowchart/mindmap/timeline/
  org-chart/architecture/process-map) + Style (sketchy/austere/sketch+)
- Les valeurs sélectionnées sont transmises à l'API et injectées dans le prompt

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-05-05 21:45:57 +00:00
parent db200bbc9f
commit 4f950740eb
5 changed files with 102 additions and 44 deletions

View File

@@ -204,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: {

View File

@@ -61,7 +61,7 @@ export async function POST(req: NextRequest) {
role: defaults.role, role: defaults.role,
tools: defaults.tools, tools: defaults.tools,
maxSteps: defaults.maxSteps, maxSteps: defaults.maxSteps,
frequency: 'manual', frequency: 'one-shot',
isEnabled: true, isEnabled: true,
sourceNoteIds: JSON.stringify([noteId]), sourceNoteIds: JSON.stringify([noteId]),
targetNotebookId: note.notebookId ?? undefined, targetNotebookId: note.notebookId ?? undefined,

View File

@@ -130,6 +130,11 @@ export function ContextualAIChat({
const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null) const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null)
const [generateResult, setGenerateResult] = useState<GenerateResult | 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('')
@@ -290,6 +295,8 @@ export function ContextualAIChat({
body: JSON.stringify({ body: JSON.stringify({
noteId, noteId,
type: type === 'slides' ? 'slide-generator' : 'excalidraw-generator', type: type === 'slides' ? 'slide-generator' : 'excalidraw-generator',
theme: type === 'slides' ? slideTheme : diagramType,
style: type === 'slides' ? slideStyle : diagramStyle,
}), }),
}) })
const data = await res.json() const data = await res.json()
@@ -999,62 +1006,91 @@ export function ContextualAIChat({
{t('ai.generate.sectionLabel') || 'Générer depuis cette note'} {t('ai.generate.sectionLabel') || 'Générer depuis cette note'}
</p> </p>
{/* Slides button */} {/* ── Slides ── */}
<button <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">
onClick={() => handleGenerate('slides')} {/* Options */}
disabled={!!generateLoading || !!actionLoading} <div className="grid grid-cols-2 gap-1.5 px-3 pt-2.5 pb-1">
className="w-full flex items-center gap-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 px-4 py-3 text-sm font-medium text-purple-700 dark:text-purple-300 hover:from-purple-100 hover:to-pink-100 dark:hover:from-purple-900/50 dark:hover:to-pink-900/50 transition-all text-left disabled:opacity-60 mb-2" <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>
{generateLoading === 'slides' <select value={slideTheme} onChange={e => setSlideTheme(e.target.value)} disabled={generateLoading === 'slides'}
? <Loader2 className="h-4 w-4 shrink-0 animate-spin" /> 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">
: <Presentation className="h-4 w-4 shrink-0" /> {[['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>
<div className="flex flex-col flex-1 min-w-0"> )}
<span>{t('ai.generate.slides') || 'Générer une présentation'}</span> </select>
{generateLoading === 'slides' && ( </div>
<span className="text-[10px] text-purple-500">{t('ai.generate.loading') || 'Génération en cours…'}</span> <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> </div>
</button> {/* 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 */} {/* Slides result */}
{generateResult?.type === 'slides' && generateResult.canvasId && ( {generateResult?.type === 'slides' && generateResult.canvasId && (
<a <a href={`/api/canvas/download?id=${generateResult.canvasId}`} target="_blank" rel="noopener noreferrer"
href={`/api/canvas/download?id=${generateResult.canvasId}`} 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">
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" /> <Download className="h-4 w-4 shrink-0" />
{t('ai.generate.downloadPptx') || 'Télécharger le .pptx'} {t('ai.generate.downloadPptx') || 'Télécharger le .pptx'}
<ExternalLink className="h-3 w-3 ml-auto opacity-60" /> <ExternalLink className="h-3 w-3 ml-auto opacity-60" />
</a> </a>
)} )}
{/* Diagram button */} {/* ── Diagram ── */}
<button <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">
onClick={() => handleGenerate('diagram')} <div className="grid grid-cols-2 gap-1.5 px-3 pt-2.5 pb-1">
disabled={!!generateLoading || !!actionLoading} <div>
className="w-full flex items-center gap-3 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 px-4 py-3 text-sm font-medium text-cyan-700 dark:text-cyan-300 hover:from-cyan-100 hover:to-blue-100 dark:hover:from-cyan-900/50 dark:hover:to-blue-900/50 transition-all text-left disabled:opacity-60" <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'}
{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">
? <Loader2 className="h-4 w-4 shrink-0 animate-spin" /> <option value="auto">{t('ai.generate.typeAuto') || 'Auto'}</option>
: <PenTool className="h-4 w-4 shrink-0" /> <option value="flowchart">Flowchart</option>
} <option value="mindmap">Mind map</option>
<div className="flex flex-col flex-1 min-w-0"> <option value="timeline">Timeline</option>
<span>{t('ai.generate.diagram') || 'Générer un diagramme'}</span> <option value="org-chart">Org chart</option>
{generateLoading === 'diagram' && ( <option value="architecture-cloud">Architecture</option>
<span className="text-[10px] text-cyan-500">{t('ai.generate.loading') || 'Génération en cours…'}</span> <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> </div>
</button> <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 */} {/* Diagram result */}
{generateResult?.type === 'diagram' && generateResult.canvasId && ( {generateResult?.type === 'diagram' && generateResult.canvasId && (
<a <a href={`/lab?canvas=${generateResult.canvasId}`}
href={`/lab?canvas=${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">
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" /> <ExternalLink className="h-4 w-4 shrink-0" />
{t('ai.generate.openDiagram') || 'Ouvrir dans le Lab'} {t('ai.generate.openDiagram') || 'Ouvrir dans le Lab'}
</a> </a>

View File

@@ -447,6 +447,17 @@
"openDiagram": "Open in Lab", "openDiagram": "Open in Lab",
"error": "Error during generation", "error": "Error during generation",
"noNoteId": "Save the note first", "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": { "toastLoading": {
"slides": "⏳ Generating presentation…", "slides": "⏳ Generating presentation…",
"diagram": "⏳ Generating diagram…" "diagram": "⏳ Generating diagram…"

View File

@@ -447,6 +447,17 @@
"openDiagram": "Ouvrir dans le Lab", "openDiagram": "Ouvrir dans le Lab",
"error": "Erreur lors de la génération", "error": "Erreur lors de la génération",
"noNoteId": "Enregistrez d'abord la note", "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": { "toastLoading": {
"slides": "⏳ Génération de la présentation en cours…", "slides": "⏳ Génération de la présentation en cours…",
"diagram": "⏳ Génération du diagramme en cours…" "diagram": "⏳ Génération du diagramme en cours…"