feat: redesign agents page (architectural-grid style), add image description, fix AI limits, remove dead code
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 53s

- Redesign agents page with architectural-grid (8) design system:
  rounded-2xl cards, serif headings, motion tabs, dashed templates section
- Replace agent form popup with full-page detail view (SettingsView style)
  with dark planning card, section tooltips, and help button
- Hide advanced mode for slide/excalidraw generators
- Add 'describe images' action to contextual AI assistant
- Add copy button to action/resource preview with HTTP fallback
- Add delete history button to agent run log panel
- Increase AI word limit from 2000 to 5000 (reformulate + transform-markdown)
- Increase max steps slider from 25 to 50
- Fix image description error with clear model compatibility message
- Fix doubled execution count display in agent detail view
- Remove dead files: notes-list-view.tsx, notes-view-toggle.tsx
- Remove 'list' view mode from NotesViewMode type
- Add missing i18n keys (back, configuration, options, copy, cleared)
This commit is contained in:
Antigravity
2026-05-09 17:18:47 +00:00
parent 79381a4cc9
commit 2fd435df6f
33 changed files with 1441 additions and 745 deletions

View File

@@ -1,17 +1,13 @@
'use client'
/**
* Agents Page Client
* Main client component for the agents page.
*/
import { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { Plus, Bot, LayoutTemplate, Search, HelpCircle } from 'lucide-react'
import { motion } from 'motion/react'
import { Plus, Bot, Search, LifeBuoy } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
import { AgentCard } from '@/components/agents/agent-card'
import { AgentForm } from '@/components/agents/agent-form'
import { AgentDetailView } from '@/components/agents/agent-detail-view'
import { AgentTemplates } from '@/components/agents/agent-templates'
import { AgentRunLog } from '@/components/agents/agent-run-log'
import { AgentHelp } from '@/components/agents/agent-help'
@@ -21,8 +17,6 @@ import {
getAgents,
} from '@/app/actions/agent-actions'
// --- Types ---
interface Notebook {
id: string
name: string
@@ -37,16 +31,23 @@ interface AgentItem {
role: string
sourceUrls?: string | null
sourceNotebookId?: string | null
sourceNoteIds?: string | null
targetNotebookId?: string | null
frequency: string
isEnabled: boolean
lastRun: string | Date | null
nextRun?: string | Date | null
createdAt: string | Date
updatedAt: string | Date
tools?: string | null
maxSteps?: number
notifyEmail?: boolean
includeImages?: boolean
scheduledTime?: string | null
scheduledDay?: number | null
timezone?: string | null
slideTheme?: string | null
slideStyle?: string | null
_count: { actions: number }
actions: { id: string; status: string; createdAt: string | Date }[]
notebook?: { id: string; name: string; icon?: string | null } | null
@@ -65,21 +66,18 @@ const typeFilterOptions = [
{ value: 'custom', labelKey: 'agents.types.custom' },
] as const
// --- Component ---
export function AgentsPageClient({
agents: initialAgents,
notebooks,
}: AgentsPageClientProps) {
const { t } = useLanguage()
const [agents, setAgents] = useState(initialAgents)
const [showForm, setShowForm] = useState(false)
const [editingAgent, setEditingAgent] = useState<AgentItem | null>(null)
const [selectedAgent, setSelectedAgent] = useState<AgentItem | null>(null)
const [isNewAgent, setIsNewAgent] = useState(false)
const [logAgent, setLogAgent] = useState<{ id: string; name: string } | null>(null)
const [showHelp, setShowHelp] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [typeFilter, setTypeFilter] = useState('')
const [activeTab, setActiveTab] = useState<'dashboard' | 'templates'>('dashboard')
const refreshAgents = useCallback(async () => {
try {
@@ -139,15 +137,15 @@ export function AgentsPageClient({
}, [])
const handleCreate = useCallback(() => {
setEditingAgent(null)
setShowForm(true)
setIsNewAgent(true)
setSelectedAgent(null)
}, [])
const handleEdit = useCallback((id: string) => {
const agent = agents.find(a => a.id === id)
if (agent) {
setEditingAgent(agent)
setShowForm(true)
setIsNewAgent(false)
setSelectedAgent(agent)
}
}, [agents])
@@ -172,17 +170,22 @@ export function AgentsPageClient({
slideTheme: (formData.get('slideTheme') as string) || undefined,
slideStyle: (formData.get('slideStyle') as string) || undefined,
}
if (editingAgent) {
await updateAgent(editingAgent.id, data)
if (selectedAgent && !isNewAgent) {
await updateAgent(selectedAgent.id, data)
toast.success(t('agents.toasts.updated'))
} else {
await createAgent(data)
toast.success(t('agents.toasts.created'))
}
setShowForm(false)
setEditingAgent(null)
setSelectedAgent(null)
setIsNewAgent(false)
await refreshAgents()
}, [editingAgent, refreshAgents, t])
}, [selectedAgent, isNewAgent, refreshAgents, t])
const handleBack = useCallback(() => {
setSelectedAgent(null)
setIsNewAgent(false)
}, [])
const filteredAgents = useMemo(() => {
return agents.filter(agent => {
@@ -198,110 +201,126 @@ export function AgentsPageClient({
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
const showDetail = selectedAgent !== null || isNewAgent
return (
/* Full-bleed layout */
<div className="flex flex-col h-full overflow-hidden">
{/* ── Top header bar — architectural grid style ── */}
<header className="flex items-center justify-between px-12 py-10 border-b border-border/40 flex-shrink-0">
<div>
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight">
{t('agents.myAgents')}
</h1>
<p className="text-[11px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-2">
{t('agents.subtitle')}
</p>
</div>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-6 py-3 text-[13px] font-medium uppercase tracking-[0.12em] border border-foreground text-foreground hover:bg-foreground hover:text-background transition-all"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</header>
{/* ── Scrollable content area ── */}
<main className="flex-1 overflow-y-auto px-12 py-10">
{/* Dashboard tab - agents + templates */}
{activeTab === 'dashboard' && (
<>
{agents.length > 0 && (
<>
{/* Filter pills + search */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
<div className="flex items-center gap-1 bg-muted/40 p-1 rounded-lg border border-border/40">
{typeFilterOptions.map(opt => (
<button
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={`px-3 py-1.5 text-[12px] font-semibold rounded-md transition-all ${
typeFilter === opt.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/60" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/10 transition-all placeholder:text-muted-foreground/40 w-56"
/>
</div>
</div>
{filteredAgents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 mb-10">
{filteredAgents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onEdit={handleEdit}
onRefresh={refreshAgents}
onToggle={handleToggle}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center mb-10">
<Search className="w-10 h-10 text-muted-foreground/20 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</div>
)}
</>
)}
{agents.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center bg-card rounded-xl border border-border/40 shadow-sm mb-10">
<Bot className="w-12 h-12 text-muted-foreground/20 mb-4" />
<h3 className="text-base font-semibold text-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-2">{t('agents.noAgentsDescription')}</p>
</div>
)}
{/* Templates always visible on dashboard */}
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
</>
)}
</main>
{/* Sliding panels */}
{showForm && (
<AgentForm
agent={editingAgent}
<>
{showDetail ? (
<AgentDetailView
agent={isNewAgent ? null : selectedAgent}
notebooks={notebooks}
onSave={handleSave}
onCancel={() => { setShowForm(false); setEditingAgent(null) }}
onBack={handleBack}
onOpenLogs={(id, name) => setLogAgent({ id, name })}
onOpenHelp={() => setShowHelp(true)}
isNew={isNewAgent}
/>
) : (
<>
<header className="px-12 pt-12 pb-8 flex flex-col gap-6 sticky top-0 bg-background/80 backdrop-blur-md z-30">
<div className="flex justify-between items-end">
<div className="space-y-1">
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground">
{t('agents.myAgents')}
</h1>
<p className="text-sm text-muted-foreground font-light">
{t('agents.subtitle')}
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowHelp(true)}
className="p-2.5 text-muted-foreground hover:text-foreground transition-colors rounded-lg hover:bg-muted"
title={t('agents.help.title')}
>
<LifeBuoy className="w-4 h-4" />
</button>
<button
onClick={handleCreate}
className="px-6 py-2.5 bg-foreground text-background text-sm font-medium rounded-xl hover:opacity-90 transition-all flex items-center gap-3 shadow-lg shadow-foreground/10"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</div>
</div>
<div className="flex items-center justify-between gap-8 border-b border-border pt-4">
<div className="flex items-center gap-8">
{typeFilterOptions.map((opt, i) => (
<button
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={`pb-4 text-xs font-bold uppercase tracking-widest transition-all relative ${
typeFilter === opt.value ? 'text-foreground' : 'text-muted-foreground hover:text-foreground/60'
}`}
>
{t(opt.labelKey)}
{typeFilter === opt.value && (
<motion.div
layoutId="activeAgentTag"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-foreground"
/>
)}
</button>
))}
</div>
<div className="relative pb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/60" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-foreground/20 transition-all placeholder:text-muted-foreground/40 w-48"
/>
</div>
</div>
</header>
<div className="px-12 flex-1 pb-20 space-y-12">
{agents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center bg-card rounded-2xl border border-border/40 shadow-sm">
<Bot className="w-12 h-12 text-muted-foreground/20 mb-4" />
<h3 className="text-base font-semibold text-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-2">{t('agents.noAgentsDescription')}</p>
</div>
) : (
<>
{filteredAgents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredAgents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onEdit={handleEdit}
onRefresh={refreshAgents}
onToggle={handleToggle}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-10 h-10 text-muted-foreground/20 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</div>
)}
</>
)}
<div className="space-y-8">
<div className="flex items-center gap-4">
<h5 className="text-[10px] font-bold uppercase tracking-[0.3em] text-muted-foreground whitespace-nowrap">
{t('agents.templates.title')}
</h5>
<div className="h-px w-full bg-border/40" />
</div>
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
</div>
</div>
</>
)}
{logAgent && (
<AgentRunLog
agentId={logAgent.id}
@@ -312,6 +331,6 @@ export function AgentsPageClient({
{showHelp && (
<AgentHelp onClose={() => setShowHelp(false)} />
)}
</div>
</>
)
}

View File

@@ -5,29 +5,23 @@ import { updateAISettings } from '@/app/actions/ai-settings'
import { updateUserSettings } from '@/app/actions/user-settings'
import { useLanguage } from '@/lib/i18n'
import { toast } from 'sonner'
import { Palette, Type, LayoutGrid, Maximize2 } from 'lucide-react'
import { Palette, Type } from 'lucide-react'
import { applyDocumentTheme, normalizeThemeId, type ThemeId } from '@/lib/apply-document-theme'
interface AppearanceSettingsClientProps {
initialFontSize: string
initialTheme: string
initialNotesViewMode: 'masonry' | 'tabs' | 'list'
initialCardSizeMode?: 'variable' | 'uniform'
initialFontFamily?: string
}
export function AppearanceSettingsClient({
initialFontSize,
initialTheme,
initialNotesViewMode,
initialCardSizeMode = 'variable',
initialFontFamily = 'inter',
}: AppearanceSettingsClientProps) {
const { t } = useLanguage()
const [theme, setTheme] = useState<ThemeId>(normalizeThemeId(initialTheme || 'light'))
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs' | 'list'>(initialNotesViewMode)
const [cardSizeMode, setCardSizeMode] = useState<'variable' | 'uniform'>(initialCardSizeMode)
const [fontFamily, setFontFamily] = useState(initialFontFamily)
const handleThemeChange = async (value: string) => {
@@ -47,21 +41,6 @@ export function AppearanceSettingsClient({
toast.success(t('settings.settingsSaved') || 'Saved')
}
const handleNotesViewChange = async (value: string) => {
const mode = value === 'tabs' ? 'tabs' : value === 'list' ? 'list' : 'masonry'
setNotesViewMode(mode)
await updateAISettings({ notesViewMode: mode })
toast.success(t('settings.settingsSaved') || 'Saved')
}
const handleCardSizeModeChange = async (value: string) => {
const mode = value === 'uniform' ? 'uniform' : 'variable'
setCardSizeMode(mode)
localStorage.setItem('card-size-mode', mode)
await updateUserSettings({ cardSizeMode: mode })
toast.success(t('settings.settingsSaved') || 'Saved')
}
const handleFontFamilyChange = async (value: string) => {
const font = value === 'system' ? 'system'
: value === 'playfair' ? 'playfair'
@@ -206,30 +185,7 @@ export function AppearanceSettingsClient({
onChange={handleFontFamilyChange}
/>
<SelectCard
icon={LayoutGrid}
title={t('appearance.notesViewLabel')}
description={t('appearance.notesViewDescription')}
value={notesViewMode}
options={[
{ value: 'masonry', label: t('appearance.notesViewMasonry') },
{ value: 'list', label: t('appearance.notesViewList') },
{ value: 'tabs', label: t('appearance.notesViewTabs') },
]}
onChange={handleNotesViewChange}
/>
<SelectCard
icon={Maximize2}
title={t('settings.cardSizeMode')}
description={t('settings.cardSizeModeDescription')}
value={cardSizeMode}
options={[
{ value: 'variable', label: t('settings.cardSizeVariable') },
{ value: 'uniform', label: t('settings.cardSizeUniform') },
]}
onChange={handleCardSizeModeChange}
/>
</div>
</div>
)

View File

@@ -19,14 +19,6 @@ export default async function AppearanceSettingsPage() {
<AppearanceSettingsClient
initialFontSize={aiSettings.fontSize}
initialTheme={userSettings.theme}
initialNotesViewMode={
aiSettings.notesViewMode === 'masonry'
? 'masonry'
: aiSettings.notesViewMode === 'list'
? 'list'
: 'tabs'
}
initialCardSizeMode={userSettings.cardSizeMode}
initialFontFamily={aiSettings.fontFamily || 'inter'}
/>
)

View File

@@ -287,6 +287,31 @@ export async function getAgentActions(agentId: string) {
}
}
export async function deleteAgentHistory(agentId: string) {
const session = await auth()
if (!session?.user?.id) {
throw new Error('Non autorise')
}
try {
const agent = await prisma.agent.findFirst({
where: { id: agentId, userId: session.user.id },
select: { id: true }
})
if (!agent) throw new Error('Agent non trouve')
await prisma.agentAction.deleteMany({
where: { agentId }
})
revalidatePath('/agents')
return { success: true }
} catch (error) {
console.error('Error deleting agent history:', error)
throw new Error('Impossible de supprimer l\'historique')
}
}
export async function toggleAgent(id: string, isEnabled: boolean) {
const session = await auth()
if (!session?.user?.id) {

View File

@@ -27,9 +27,9 @@ export async function POST(request: NextRequest) {
)
}
if (wordCount > 2000) {
if (wordCount > 5000) {
return NextResponse.json(
{ errorKey: 'ai.wordCountMax', params: { max: 2000, current: wordCount } },
{ errorKey: 'ai.wordCountMax', params: { max: 5000, current: wordCount } },
{ status: 400 }
)
}