feat(ai-note): ajouter boutons Générer slides/diagramme dans le panneau IA
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 3s
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 3s
- Nouveau endpoint POST /api/agents/run-for-note : crée un agent one-shot (slide-generator ou excalidraw-generator) avec la note courante comme source et l'exécute immédiatement - ContextualAIChat : prop noteId + section "Générer depuis cette note" avec deux boutons gradient (violet=slides, cyan=diagramme), spinner pendant la génération, bouton de téléchargement .pptx ou lien "Ouvrir dans le Lab" après succès - note-editor.tsx : passage de note.id à ContextualAIChat - i18n fr/en : nouvelles clés ai.generate.* Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
Globe, BookOpen, FileText, RotateCcw, Check,
|
||||
Maximize2, ImageIcon, Link2, Download, ArrowDownToLine,
|
||||
GitMerge, PlusCircle, Eye, Code, Languages,
|
||||
Presentation, PenTool, ExternalLink,
|
||||
} from 'lucide-react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { MarkdownContent } from '@/components/markdown-content'
|
||||
@@ -71,11 +72,18 @@ const ACTION_IDS = [
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface GenerateResult {
|
||||
type: 'slides' | 'diagram'
|
||||
canvasId?: string
|
||||
noteId?: string
|
||||
}
|
||||
|
||||
interface ContextualAIChatProps {
|
||||
onClose: () => void
|
||||
noteTitle?: string
|
||||
noteContent?: string
|
||||
noteImages?: string[]
|
||||
noteId?: string
|
||||
/** Called when an action result should be injected into the note */
|
||||
onApplyToNote?: (newContent: string) => void
|
||||
/** Called when the user wants to undo the last injected action */
|
||||
@@ -95,6 +103,7 @@ export function ContextualAIChat({
|
||||
noteTitle,
|
||||
noteContent,
|
||||
noteImages,
|
||||
noteId,
|
||||
onApplyToNote,
|
||||
onUndoLastAction,
|
||||
lastActionApplied = false,
|
||||
@@ -116,6 +125,10 @@ export function ContextualAIChat({
|
||||
const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null)
|
||||
const [showLangPicker, setShowLangPicker] = useState(false)
|
||||
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('')
|
||||
|
||||
// Resource tab state
|
||||
@@ -249,6 +262,41 @@ export function ContextualAIChat({
|
||||
|
||||
const handleDiscardPreview = () => setActionPreview(null)
|
||||
|
||||
// ── Generate slides / diagram ────────────────────────────────────────────────
|
||||
|
||||
const handleGenerate = async (type: 'slides' | 'diagram') => {
|
||||
if (!noteId) {
|
||||
toast.error(t('ai.generate.noNoteId') || 'Note non sauvegardée')
|
||||
return
|
||||
}
|
||||
setGenerateLoading(type)
|
||||
setGenerateResult(null)
|
||||
try {
|
||||
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',
|
||||
}),
|
||||
})
|
||||
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')
|
||||
return
|
||||
}
|
||||
setGenerateResult({ type, canvasId: data.canvasId, noteId: data.noteId })
|
||||
toast.success(type === 'slides'
|
||||
? (t('ai.generate.slidesReady') || 'Présentation générée !')
|
||||
: (t('ai.generate.diagramReady') || 'Diagramme généré !')
|
||||
)
|
||||
} catch {
|
||||
toast.error(t('ai.generate.error') || 'Erreur lors de la génération')
|
||||
} finally {
|
||||
setGenerateLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resource tab handlers ────────────────────────────────────────────────────
|
||||
|
||||
const handleScrapeUrl = async () => {
|
||||
@@ -883,6 +931,76 @@ 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 button */}
|
||||
<button
|
||||
onClick={() => handleGenerate('slides')}
|
||||
disabled={!!generateLoading || !!actionLoading}
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
|
||||
{/* 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 button */}
|
||||
<button
|
||||
onClick={() => handleGenerate('diagram')}
|
||||
disabled={!!generateLoading || !!actionLoading}
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
|
||||
{/* Diagram result */}
|
||||
{generateResult?.type === 'diagram' && generateResult.canvasId && (
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 shrink-0" />
|
||||
{t('ai.generate.openDiagram') || 'Ouvrir dans le Lab'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Undo last action shortcut */}
|
||||
{lastActionApplied && onUndoLastAction && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user