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
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:
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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'}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user