feat(agents): refonte complète slide-generator + excalidraw-generator
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
Slide generator (generate_pptx): - Pivot vers génération PowerPoint natif (pptxgenjs) au lieu de Reveal.js HTML - 4 nouveaux layouts diagramme : timeline, process, comparison, metrics - 2 nouveaux layouts image : image-content (texte + image), image-full (plein cadre) - Redesign visuel de tous les layouts (cover split, section full-bleed, header band) - Palettes corrigées : bg blanc sur toutes les palettes, contrastes réels - fit:shrink systématique sur tous les textes pour éviter les débordements - Extraction automatique des images des notes (Markdown/HTML) et injection dans le prompt IA - Prompt IA renforcé : impose "style" et "theme" explicitement dans le JSON, impose ≥2 layouts diagramme - Fix overlap timeline : zones de texte calculées sans collision avec les cercles - Notification agent mise à jour : bouton download .pptx au lieu d'ouvrir HTML Excalidraw generator: - Layout Dagre/ELK pour graphes auto-positionnés - Styles visuels : coloré, austère, sketch-plus (Virgil font) - Zones/containers pour architecture-cloud - Sanitisation du graphe et métriques de qualité de layout Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,15 +6,15 @@
|
||||
* Novice-friendly: hides system prompt and tools behind "Advanced mode".
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useRef } from 'react'
|
||||
import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon } from 'lucide-react'
|
||||
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
||||
import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon, Presentation, Pencil, Check } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom'
|
||||
type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom' | 'slide-generator' | 'excalidraw-generator'
|
||||
|
||||
/** Small "?" tooltip shown next to form labels */
|
||||
function FieldHelp({ tooltip }: { tooltip: string }) {
|
||||
@@ -41,6 +41,7 @@ interface AgentFormProps {
|
||||
role: string
|
||||
sourceUrls?: string | null
|
||||
sourceNotebookId?: string | null
|
||||
sourceNoteIds?: string | null
|
||||
targetNotebookId?: string | null
|
||||
frequency: string
|
||||
tools?: string | null
|
||||
@@ -50,6 +51,8 @@ interface AgentFormProps {
|
||||
scheduledTime?: string | null
|
||||
scheduledDay?: number | null
|
||||
timezone?: string | null
|
||||
slideTheme?: string | null
|
||||
slideStyle?: string | null
|
||||
} | null
|
||||
notebooks: { id: string; name: string; icon?: string | null }[]
|
||||
onSave: (data: FormData) => Promise<void>
|
||||
@@ -62,6 +65,8 @@ const TOOL_PRESETS: Record<string, string[]> = {
|
||||
researcher: ['web_search', 'web_scrape', 'note_search', 'note_create', 'memory_search'],
|
||||
monitor: ['note_search', 'note_read', 'note_create', 'memory_search'],
|
||||
custom: ['memory_search'],
|
||||
'slide-generator': ['generate_pptx'],
|
||||
'excalidraw-generator': ['generate_excalidraw'],
|
||||
}
|
||||
|
||||
// --- Shared class strings ---
|
||||
@@ -89,6 +94,13 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
return ['']
|
||||
})
|
||||
const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '')
|
||||
const [sourceNoteIds, setSourceNoteIds] = useState<string[]>(() => {
|
||||
if (agent?.sourceNoteIds) {
|
||||
try { return JSON.parse(agent.sourceNoteIds) } catch { return [] }
|
||||
}
|
||||
return []
|
||||
})
|
||||
const [noteOptions, setNoteOptions] = useState<{ id: string; title: string }[]>([])
|
||||
const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '')
|
||||
const [frequency, setFrequency] = useState(agent?.frequency || 'manual')
|
||||
const [scheduledTime, setScheduledTime] = useState(agent?.scheduledTime || '08:00')
|
||||
@@ -110,7 +122,36 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
const [maxSteps, setMaxSteps] = useState(agent?.maxSteps || 10)
|
||||
const [notifyEmail, setNotifyEmail] = useState(agent?.notifyEmail || false)
|
||||
const [includeImages, setIncludeImages] = useState(agent?.includeImages || false)
|
||||
const [slideTheme, setSlideTheme] = useState(agent?.slideTheme || '')
|
||||
const [slideStyle, setSlideStyle] = useState<'soft' | 'sharp' | 'rounded' | 'pill'>(
|
||||
(agent?.slideStyle as 'soft' | 'sharp' | 'rounded' | 'pill') || 'soft'
|
||||
)
|
||||
const [excalidrawStyle, setExcalidrawStyle] = useState<'default' | 'austere' | 'sketch-plus'>(() => {
|
||||
if (agent?.slideStyle === 'austere') return 'austere'
|
||||
if (agent?.slideStyle === 'sketch-plus') return 'sketch-plus'
|
||||
return 'default'
|
||||
})
|
||||
const [excalidrawType, setExcalidrawType] = useState<'auto' | 'architecture-cloud' | 'flowchart' | 'mindmap' | 'org-chart' | 'timeline' | 'process-map'>(() => {
|
||||
const value = (agent?.slideTheme || '').trim()
|
||||
if (value === 'auto' || value === 'architecture-cloud' || value === 'mindmap' || value === 'org-chart' || value === 'timeline' || value === 'process-map') return value
|
||||
return 'flowchart'
|
||||
})
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!sourceNotebookId || (type !== 'slide-generator' && type !== 'excalidraw-generator' && type !== 'monitor')) {
|
||||
setNoteOptions([])
|
||||
return
|
||||
}
|
||||
fetch(`/api/notes?notebookId=${sourceNotebookId}&limit=50`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const notes = Array.isArray(data.data) ? data.data : Array.isArray(data) ? data : []
|
||||
setNoteOptions(notes.map((n: any) => ({ id: n.id, title: n.title || 'Sans titre' })))
|
||||
})
|
||||
.catch(() => setNoteOptions([]))
|
||||
}, [sourceNotebookId, type])
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(() => {
|
||||
// Auto-open advanced if editing an agent with custom tools or custom prompt
|
||||
if (agent?.tools) {
|
||||
@@ -133,6 +174,9 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
{ id: 'note_create', icon: FilePlus, labelKey: 'agents.tools.noteCreate', external: false },
|
||||
{ id: 'url_fetch', icon: ExternalLink, labelKey: 'agents.tools.urlFetch', external: false },
|
||||
{ id: 'memory_search', icon: Brain, labelKey: 'agents.tools.memorySearch', external: false },
|
||||
{ id: 'generate_pptx', icon: Presentation, labelKey: 'agents.tools.generatePptx', external: false },
|
||||
{ id: 'generate_slides', icon: Presentation, labelKey: 'agents.tools.generateSlides', external: false },
|
||||
{ id: 'generate_excalidraw', icon: Pencil, labelKey: 'agents.tools.generateExcalidraw', external: false },
|
||||
], [])
|
||||
|
||||
// Track previous type to detect user-initiated type changes
|
||||
@@ -172,10 +216,14 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
formData.set('frequency', frequency)
|
||||
formData.set('targetNotebookId', targetNotebookId)
|
||||
|
||||
if (type === 'monitor') {
|
||||
if (type === 'monitor' || type === 'slide-generator' || type === 'excalidraw-generator') {
|
||||
formData.set('sourceNotebookId', sourceNotebookId)
|
||||
}
|
||||
|
||||
if (sourceNoteIds.length > 0) {
|
||||
formData.set('sourceNoteIds', JSON.stringify(sourceNoteIds))
|
||||
}
|
||||
|
||||
const validUrls = urls.filter(u => u.trim())
|
||||
if (validUrls.length > 0) {
|
||||
formData.set('sourceUrls', JSON.stringify(validUrls))
|
||||
@@ -189,6 +237,15 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
formData.set('scheduledDay', String(scheduledDay))
|
||||
formData.set('timezone', timezone)
|
||||
|
||||
if (type === 'slide-generator') {
|
||||
if (slideTheme) formData.set('slideTheme', slideTheme)
|
||||
formData.set('slideStyle', slideStyle)
|
||||
}
|
||||
if (type === 'excalidraw-generator') {
|
||||
formData.set('slideTheme', excalidrawType)
|
||||
formData.set('slideStyle', excalidrawStyle)
|
||||
}
|
||||
|
||||
await onSave(formData)
|
||||
} catch {
|
||||
toast.error(t('agents.toasts.saveError'))
|
||||
@@ -197,12 +254,14 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
}
|
||||
}
|
||||
|
||||
const showSourceNotebook = type === 'monitor'
|
||||
const showSourceNotebook = type === 'monitor' || type === 'slide-generator' || type === 'excalidraw-generator'
|
||||
|
||||
const agentTypes: { value: AgentType; labelKey: string; descKey: string }[] = [
|
||||
{ value: 'researcher', labelKey: 'agents.types.researcher', descKey: 'agents.typeDescriptions.researcher' },
|
||||
{ value: 'scraper', labelKey: 'agents.types.scraper', descKey: 'agents.typeDescriptions.scraper' },
|
||||
{ value: 'monitor', labelKey: 'agents.types.monitor', descKey: 'agents.typeDescriptions.monitor' },
|
||||
{ value: 'slide-generator', labelKey: 'agents.types.slideGenerator', descKey: 'agents.typeDescriptions.slideGenerator' },
|
||||
{ value: 'excalidraw-generator', labelKey: 'agents.types.excalidrawGenerator', descKey: 'agents.typeDescriptions.excalidrawGenerator' },
|
||||
{ value: 'custom', labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom' },
|
||||
]
|
||||
|
||||
@@ -318,13 +377,13 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Source Notebook (monitor only) */}
|
||||
{/* Source Notebook (monitor, slide, excalidraw) */}
|
||||
{showSourceNotebook && (
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.sourceNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.sourceNotebook')} /></label>
|
||||
<select
|
||||
value={sourceNotebookId}
|
||||
onChange={e => setSourceNotebookId(e.target.value)}
|
||||
onChange={e => { setSourceNotebookId(e.target.value); setSourceNoteIds([]) }}
|
||||
className={selectCls}
|
||||
>
|
||||
<option value="">{t('agents.form.selectNotebook')}</option>
|
||||
@@ -337,7 +396,149 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Target Notebook */}
|
||||
{/* Note multi-select (slide-generator, excalidraw-generator only) */}
|
||||
{(type === 'slide-generator' || type === 'excalidraw-generator') && sourceNotebookId && noteOptions.length > 0 && (
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.selectNotes')}<FieldHelp tooltip={t('agents.help.tooltips.selectNotes')} /></label>
|
||||
<div className="border border-input rounded-lg max-h-48 overflow-y-auto bg-card">
|
||||
{noteOptions.map(note => {
|
||||
const isSelected = sourceNoteIds.includes(note.id)
|
||||
return (
|
||||
<button
|
||||
key={note.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSourceNoteIds(prev =>
|
||||
isSelected ? prev.filter(id => id !== note.id) : [...prev, note.id]
|
||||
)
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-accent/50 transition-colors ${isSelected ? 'bg-primary/5' : ''}`}
|
||||
>
|
||||
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 ${isSelected ? 'border-primary bg-primary' : 'border-input'}`}>
|
||||
{isSelected && <Check className="w-3 h-3 text-primary-foreground" />}
|
||||
</div>
|
||||
<span className={isSelected ? 'text-primary font-medium' : 'text-foreground'}>{note.title}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{sourceNoteIds.length > 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{t('agents.form.notesSelected', { count: sourceNoteIds.length })}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Theme selector — slide-generator only */}
|
||||
{type === 'slide-generator' && (
|
||||
<>
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.slideTheme')}<FieldHelp tooltip={t('agents.help.tooltips.slideTheme')} /></label>
|
||||
<select
|
||||
value={slideTheme}
|
||||
onChange={e => setSlideTheme(e.target.value)}
|
||||
className={selectCls}
|
||||
>
|
||||
<option value="">{t('agents.form.slideThemeDefault')}</option>
|
||||
<option value="modern_wellness">Modern & Wellness</option>
|
||||
<option value="business_authority">Business & Authority</option>
|
||||
<option value="nature_outdoors">Nature & Outdoors</option>
|
||||
<option value="vintage_academic">Vintage & Academic</option>
|
||||
<option value="soft_creative">Soft & Creative</option>
|
||||
<option value="bohemian">Bohemian</option>
|
||||
<option value="vibrant_tech">Vibrant & Tech</option>
|
||||
<option value="craft_artisan">Craft & Artisan</option>
|
||||
<option value="tech_night">Tech & Night (dark)</option>
|
||||
<option value="education_charts">Education & Charts</option>
|
||||
<option value="forest_eco">Forest & Eco</option>
|
||||
<option value="elegant_fashion">Elegant & Fashion</option>
|
||||
<option value="art_food">Art & Food</option>
|
||||
<option value="luxury_mystery">Luxury & Mystery</option>
|
||||
<option value="pure_tech_blue">Pure Tech Blue</option>
|
||||
<option value="coastal_coral">Coastal Coral</option>
|
||||
<option value="vibrant_orange_mint">Vibrant Orange Mint</option>
|
||||
<option value="platinum_white_gold">Platinum White Gold</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.slideStyle')}<FieldHelp tooltip={t('agents.help.tooltips.slideStyle')} /></label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(['soft', 'sharp', 'rounded', 'pill'] as const).map(s => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => setSlideStyle(s)}
|
||||
className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${
|
||||
slideStyle === s
|
||||
? 'border-primary bg-primary/5 text-primary font-medium'
|
||||
: `${toggleOffBorder} text-muted-foreground`
|
||||
}`}
|
||||
>
|
||||
{t(`agents.form.slideStyle${s.charAt(0).toUpperCase() + s.slice(1)}` as any)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Visual style selector — excalidraw-generator only */}
|
||||
{type === 'excalidraw-generator' && (
|
||||
<>
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.excalidrawDiagramType')}</label>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{([
|
||||
{ id: 'auto', labelKey: 'agents.form.excalidrawDiagramTypeAuto' },
|
||||
{ id: 'flowchart', labelKey: 'agents.form.excalidrawDiagramTypeFlowchart' },
|
||||
{ id: 'mindmap', labelKey: 'agents.form.excalidrawDiagramTypeMindmap' },
|
||||
{ id: 'org-chart', labelKey: 'agents.form.excalidrawDiagramTypeOrgChart' },
|
||||
{ id: 'timeline', labelKey: 'agents.form.excalidrawDiagramTypeTimeline' },
|
||||
{ id: 'process-map', labelKey: 'agents.form.excalidrawDiagramTypeProcessMap' },
|
||||
{ id: 'architecture-cloud', labelKey: 'agents.form.excalidrawDiagramTypeArchitectureCloud' },
|
||||
] as const).map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => setExcalidrawType(opt.id)}
|
||||
className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${
|
||||
excalidrawType === opt.id
|
||||
? 'border-primary bg-primary/5 text-primary font-medium'
|
||||
: `${toggleOffBorder} text-muted-foreground`
|
||||
}`}
|
||||
>
|
||||
{t(opt.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.excalidrawDiagramStyle')}</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{([
|
||||
{ id: 'default', labelKey: 'agents.form.excalidrawDiagramStyleDefault' },
|
||||
{ id: 'sketch-plus', labelKey: 'agents.form.excalidrawDiagramStyleSketchPlus' },
|
||||
{ id: 'austere', labelKey: 'agents.form.excalidrawDiagramStyleAustere' },
|
||||
] as const).map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => setExcalidrawStyle(opt.id)}
|
||||
className={`px-3 py-2 rounded-lg border-2 text-sm transition-all text-left ${
|
||||
excalidrawStyle === opt.id
|
||||
? 'border-primary bg-primary/5 text-primary font-medium'
|
||||
: `${toggleOffBorder} text-muted-foreground`
|
||||
}`}
|
||||
>
|
||||
{t(opt.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Target Notebook — hidden for file generators (they never create notes) */}
|
||||
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
||||
<div>
|
||||
<label className={labelCls}>{t('agents.form.targetNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.targetNotebook')} /></label>
|
||||
<select
|
||||
@@ -353,6 +554,7 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frequency */}
|
||||
<div>
|
||||
@@ -423,7 +625,8 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Notification */}
|
||||
{/* Email Notification — hidden for file generators */}
|
||||
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
||||
<div
|
||||
onClick={() => setNotifyEmail(!notifyEmail)}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
@@ -441,8 +644,10 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${notifyEmail ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Include Images */}
|
||||
{/* Include Images — hidden for file generators */}
|
||||
{type !== 'slide-generator' && type !== 'excalidraw-generator' && (
|
||||
<div
|
||||
onClick={() => setIncludeImages(!includeImages)}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
@@ -460,6 +665,7 @@ export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps
|
||||
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${includeImages ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Advanced mode toggle */}
|
||||
<button
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2 } from 'lucide-react'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2, Download, Pencil, Presentation } from 'lucide-react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -196,26 +196,39 @@ export function NotificationPanel() {
|
||||
) : (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{/* App notifications (agents, system) */}
|
||||
{appNotifications.map((notif) => (
|
||||
{appNotifications.map((notif) => {
|
||||
const isSlides = notif.type === 'agent_slides_ready'
|
||||
const isCanvas = notif.type === 'agent_canvas_ready'
|
||||
const canvasId = notif.relatedId
|
||||
|
||||
return (
|
||||
<div
|
||||
key={notif.id}
|
||||
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (notif.actionUrl) {
|
||||
handleMarkNotifRead(notif.id)
|
||||
setOpen(false)
|
||||
router.push(notif.actionUrl)
|
||||
}
|
||||
}}
|
||||
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="flex items-start gap-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (notif.actionUrl) {
|
||||
handleMarkNotifRead(notif.id)
|
||||
setOpen(false)
|
||||
router.push(notif.actionUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
"mt-0.5 flex-none rounded-full p-1",
|
||||
notif.type === 'agent_success' && 'bg-green-100 dark:bg-green-900/30 text-green-600',
|
||||
notif.type === 'agent_slides_ready' && 'bg-purple-100 dark:bg-purple-900/30 text-purple-600',
|
||||
notif.type === 'agent_canvas_ready' && 'bg-blue-100 dark:bg-blue-900/30 text-blue-600',
|
||||
notif.type === 'agent_failure' && 'bg-red-100 dark:bg-red-900/30 text-red-600',
|
||||
notif.type === 'system' && 'bg-blue-100 dark:bg-blue-900/30 text-blue-600',
|
||||
)}>
|
||||
{notif.type.startsWith('agent') ? (
|
||||
{isSlides ? (
|
||||
<Presentation className="w-3.5 h-3.5" />
|
||||
) : isCanvas ? (
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
) : notif.type.startsWith('agent') ? (
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
@@ -226,9 +239,13 @@ export function NotificationPanel() {
|
||||
<span className={cn(
|
||||
"text-[10px] font-semibold uppercase tracking-wider",
|
||||
notif.type === 'agent_success' && 'text-green-600 dark:text-green-400',
|
||||
notif.type === 'agent_slides_ready' && 'text-purple-600 dark:text-purple-400',
|
||||
notif.type === 'agent_canvas_ready' && 'text-blue-600 dark:text-blue-400',
|
||||
notif.type === 'agent_failure' && 'text-red-600 dark:text-red-400',
|
||||
notif.type === 'system' && 'text-blue-600 dark:text-blue-400',
|
||||
)}>
|
||||
{notif.type === 'agent_slides_ready' && (t('notification.slidesReady') || 'Slides Ready')}
|
||||
{notif.type === 'agent_canvas_ready' && (t('notification.canvasReady') || 'Diagram Ready')}
|
||||
{notif.type === 'agent_success' && (t('notification.agentSuccess') || 'Agent completed')}
|
||||
{notif.type === 'agent_failure' && (t('notification.agentFailed') || 'Agent failed')}
|
||||
{notif.type === 'system' && 'System'}
|
||||
@@ -251,8 +268,23 @@ export function NotificationPanel() {
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{isSlides && canvasId && (
|
||||
<div className="mt-2 ml-8">
|
||||
<button
|
||||
onClick={async () => {
|
||||
handleMarkNotifRead(notif.id)
|
||||
window.open(`/api/canvas/download?id=${canvasId}`, '_blank')
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold rounded-md bg-purple-500 text-white hover:bg-purple-600 shadow-sm transition-all active:scale-95"
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
{t('notification.downloadPptx') || 'Download .pptx'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Overdue reminders */}
|
||||
{overdueReminders.map((note) => (
|
||||
|
||||
Reference in New Issue
Block a user