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

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

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

View File

@@ -50,6 +50,7 @@ import { getNotebookIcon } from '@/lib/notebook-icon'
import { HierarchicalNotebookSelector } from '@/components/hierarchical-notebook-selector'
import { AutoLabelSuggestionDialog } from '@/components/auto-label-suggestion-dialog'
import { scrapePageText } from '@/app/actions/scrape'
import { PersonasPanel } from '@/components/personas-panel'
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -210,11 +211,13 @@ export function ContextualAIChat({
// Generate slides / diagram state
const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null)
const [generateProgress, setGenerateProgress] = useState(0)
const [generateResult, setGenerateResult] = useState<GenerateResult | null>(null)
const [customLangInput, setCustomLangInput] = useState('')
// Generation options
const [slideTheme, setSlideTheme] = useState('architectural_mono')
const [slideTheme, setSlideTheme] = useState('auto')
const [slideStyle, setSlideStyle] = useState('professional')
const [slideTemplate, setSlideTemplate] = useState('auto')
const [diagramType, setDiagramType] = useState('logic_flow')
const [diagramStyle, setDiagramStyle] = useState('polished')
const [diagramEmbedLoading, setDiagramEmbedLoading] = useState(false)
@@ -358,6 +361,24 @@ export function ContextualAIChat({
// ── Generate slides / diagram ────────────────────────────────────────────────
const generatePollRef = useRef<ReturnType<typeof setInterval> | null>(null)
const generateProgressRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Fake progress bar: fast up to 30%, then slows, caps at 90% until done
const startProgressBar = () => {
setGenerateProgress(0)
let current = 0
if (generateProgressRef.current) clearInterval(generateProgressRef.current)
generateProgressRef.current = setInterval(() => {
current += current < 30 ? 3 : current < 60 ? 1.2 : current < 80 ? 0.4 : 0.1
if (current >= 90) { clearInterval(generateProgressRef.current!); current = 90 }
setGenerateProgress(Math.min(current, 90))
}, 300)
}
const finishProgressBar = () => {
if (generateProgressRef.current) clearInterval(generateProgressRef.current)
setGenerateProgress(100)
setTimeout(() => setGenerateProgress(0), 600)
}
const handleGenerate = async (type: 'slides' | 'diagram') => {
if (!noteId) {
@@ -366,6 +387,7 @@ export function ContextualAIChat({
}
setGenerateLoading(type)
setGenerateResult(null)
startProgressBar()
const toastId = mToast.loading(
type === 'slides' ? t('ai.generateSlidesLoading') : t('ai.generateDiagramLoading'),
@@ -382,6 +404,7 @@ export function ContextualAIChat({
theme: type === 'slides' ? slideTheme : diagramType,
style: type === 'slides' ? slideStyle : diagramStyle,
language: language === 'fr' ? 'French' : 'English',
template: type === 'slides' ? slideTemplate : undefined,
}),
})
const data = await res.json()
@@ -394,7 +417,18 @@ export function ContextualAIChat({
const { agentId } = data as { agentId: string }
if (generatePollRef.current) clearInterval(generatePollRef.current)
let pollCount = 0
const MAX_POLLS = 200 // 200 × 3s = 10 min safety timeout
generatePollRef.current = setInterval(async () => {
pollCount++
if (pollCount > MAX_POLLS) {
clearInterval(generatePollRef.current!)
generatePollRef.current = null
finishProgressBar()
setGenerateLoading(null)
mToast.error(t('ai.errorShort'), { id: toastId })
return
}
try {
const pollRes = await fetch(`/api/agents/run-for-note?agentId=${agentId}`)
const poll = await pollRes.json()
@@ -402,12 +436,14 @@ export function ContextualAIChat({
if (poll.status === 'success') {
clearInterval(generatePollRef.current!)
generatePollRef.current = null
finishProgressBar()
setGenerateLoading(null)
setGenerateResult({ type, canvasId: poll.canvasId, noteId: poll.noteId })
mToast.success(t('ai.readyToast'), { id: toastId })
} else if (poll.status === 'failure') {
clearInterval(generatePollRef.current!)
generatePollRef.current = null
finishProgressBar()
setGenerateLoading(null)
mToast.error(poll.error || t('ai.errorShort'), { id: toastId })
}
@@ -415,6 +451,7 @@ export function ContextualAIChat({
}, 3000)
} catch {
mToast.error(t('ai.errorShort'), { id: toastId })
finishProgressBar()
setGenerateLoading(null)
}
}
@@ -731,7 +768,7 @@ export function ContextualAIChat({
<div className="w-12 h-12 rounded-full border border-dashed border-concrete/10 flex items-center justify-center">
<MessageSquare size={18} />
</div>
<p className="text-[11px] italic leading-relaxed px-12">{t('ai.welcomeMsg')}</p>
<p className="text-[11px] italic leading-relaxed px-6">{t('ai.welcomeMsg')}</p>
</div>
)}
@@ -980,9 +1017,20 @@ export function ContextualAIChat({
<div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.theme')}</span>
<select value={slideTheme} onChange={e => setSlideTheme(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
<option value="architectural_mono">{t('ai.generate.themeArchitecturalMono')}</option>
<option value="vibrant_tech">{t('ai.generate.themeVibrantTech')}</option>
<option value="minimal_silk">{t('ai.generate.themeMinimalSilk')}</option>
<option value="auto">{t('ai.generate.themeAuto')}</option>
<option value="Architectural SaaS">Architectural SaaS SaaS, Produit</option>
<option value="Midnight Cathedral">Midnight Cathedral Finance, Luxe</option>
<option value="Aurora Borealis">Aurora Borealis Tech, IA</option>
<option value="Tokyo Neon">Tokyo Neon Gaming</option>
<option value="Sunlit Gallery">Sunlit Gallery Art, Culture</option>
<option value="Clinical Precision">Clinical Precision Santé, Science</option>
<option value="Venture Pitch">Venture Pitch Startup</option>
<option value="Forest Floor">Forest Floor ESG, Nature</option>
<option value="Steel & Glass">Steel &amp; Glass Architecture</option>
<option value="Cyberpunk Terminal">Cyberpunk Terminal Dev, Cyber</option>
<option value="Editorial Ink">Editorial Ink Journalisme</option>
<option value="Coastal Morning">Coastal Morning Éducation</option>
<option value="Paper Studio">Paper Studio Research</option>
</select>
</div>
<div className="space-y-1.5">
@@ -994,10 +1042,36 @@ export function ContextualAIChat({
</select>
</div>
</div>
<div className="space-y-1.5">
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.template')}</span>
<select value={slideTemplate} onChange={e => setSlideTemplate(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
<option value="auto">{t('ai.generate.templateAuto')}</option>
<option value="board-update">{t('ai.generate.templateBoard')}</option>
<option value="project-status">{t('ai.generate.templateProject')}</option>
<option value="strategy-review">{t('ai.generate.templateStrategy')}</option>
<option value="quarterly-results">{t('ai.generate.templateQuarterly')}</option>
</select>
</div>
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 uppercase tracking-[0.2em] disabled:opacity-50">
{generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>}
</button>
{/* Progress bar — visible only during slides generation */}
{generateLoading === 'slides' && (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-[9px] text-foreground/40">
<span className="uppercase tracking-widest"> Génération en cours</span>
<span className="font-mono tabular-nums">{Math.round(generateProgress)}%</span>
</div>
<div className="w-full h-1.5 rounded-full bg-foreground/10 overflow-hidden">
<div
className="h-full rounded-full bg-brand-accent transition-all duration-300 ease-out"
style={{ width: `${generateProgress}%` }}
/>
</div>
</div>
)}
{generateResult?.type === 'slides' && generateResult.canvasId && (
<motion.div
initial={{ opacity: 0, y: 10 }}
@@ -1018,35 +1092,15 @@ export function ContextualAIChat({
<ExternalLink size={12} />
</a>
</div>
<button
onClick={async () => {
try {
const res = await fetch(`/api/canvas?id=${generateResult.canvasId}`)
const data = await res.json()
if (!data.canvas?.data) throw new Error('No data')
const parsed = JSON.parse(data.canvas.data)
if (!parsed.base64) throw new Error('No base64')
const byteChars = atob(parsed.base64)
const bytes = new Uint8Array(byteChars.length)
for (let i = 0; i < byteChars.length; i++) bytes[i] = byteChars.charCodeAt(i)
const blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = parsed.filename || `${data.canvas.name || 'presentation'}.pptx`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch {
mToast.error(t('ai.downloadFailedToast'))
}
}}
<a
href={`/lab?id=${generateResult.canvasId}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full py-2.5 bg-brand-accent text-white rounded-lg text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 transition-opacity shadow-sm"
>
<Download size={13} />
{t('ai.pptxDownloadButton')}
</button>
<Presentation size={13} />
{t('ai.viewSlidesButton')}
</a>
</motion.div>
)}
@@ -1129,6 +1183,8 @@ export function ContextualAIChat({
</div>
</div>
{/* ── Personas IA ── */}
<PersonasPanel noteTitle={noteTitle} noteContent={noteContent} />
</motion.div>
)}