fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Replaced ~100+ hardcoded French and English text strings across 30+ components with proper i18n t() calls. Added 57 new translation keys to all 15 locale files (ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh). Key changes: - contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders) - ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history) - note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist) - lab-skeleton.tsx: French loading text → t() - admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed - New AI chat component, agent cards, sidebar, settings panel i18n cleanup Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react'
|
||||
import { Plus, Bot, LifeBuoy, Search } from 'lucide-react'
|
||||
import { Plus, Bot, LayoutTemplate, Search, HelpCircle } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
@@ -79,6 +79,7 @@ export function AgentsPageClient({
|
||||
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 {
|
||||
@@ -90,20 +91,15 @@ export function AgentsPageClient({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Track latest action ID per agent to detect new executions
|
||||
const prevActionsRef = useRef<Record<string, string | null>>({})
|
||||
|
||||
// Check for new actions and show toast notifications
|
||||
const checkForNewActions = useCallback((updated: AgentItem[]) => {
|
||||
for (const agent of updated) {
|
||||
const lastAction = agent.actions[0]
|
||||
if (!lastAction) continue
|
||||
|
||||
const prevId = prevActionsRef.current[agent.id]
|
||||
if (prevId === undefined) continue // Not tracked
|
||||
|
||||
if (prevId === undefined) continue
|
||||
if (prevId !== lastAction.id) {
|
||||
// Only toast for recently created actions (within 5 min)
|
||||
const age = Date.now() - new Date(lastAction.createdAt).getTime()
|
||||
if (age < 5 * 60 * 1000) {
|
||||
if (lastAction.status === 'success') {
|
||||
@@ -118,18 +114,13 @@ export function AgentsPageClient({
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize tracking from initial data
|
||||
for (const agent of initialAgents) {
|
||||
prevActionsRef.current[agent.id] = agent.actions[0]?.id ?? null
|
||||
}
|
||||
|
||||
// Interval polling every 30s
|
||||
const interval = setInterval(async () => {
|
||||
const updated = await refreshAgents()
|
||||
if (updated) checkForNewActions(updated)
|
||||
}, 30000)
|
||||
|
||||
// Refresh immediately when user comes back to the tab
|
||||
const onVisible = async () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
const updated = await refreshAgents()
|
||||
@@ -137,7 +128,6 @@ export function AgentsPageClient({
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisible)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
document.removeEventListener('visibilitychange', onVisible)
|
||||
@@ -179,7 +169,6 @@ export function AgentsPageClient({
|
||||
scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined,
|
||||
timezone: (formData.get('timezone') as string) || undefined,
|
||||
}
|
||||
|
||||
if (editingAgent) {
|
||||
await updateAgent(editingAgent.id, data)
|
||||
toast.success(t('agents.toasts.updated'))
|
||||
@@ -187,7 +176,6 @@ export function AgentsPageClient({
|
||||
await createAgent(data)
|
||||
toast.success(t('agents.toasts.created'))
|
||||
}
|
||||
|
||||
setShowForm(false)
|
||||
setEditingAgent(null)
|
||||
await refreshAgents()
|
||||
@@ -208,110 +196,142 @@ export function AgentsPageClient({
|
||||
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-primary/10 rounded-2xl shadow-sm border border-primary/20">
|
||||
<Bot className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{t('agents.title')}</h1>
|
||||
<p className="text-muted-foreground text-sm">{t('agents.subtitle')}</p>
|
||||
/* Full-bleed layout: -m-4 cancels the p-4 of the parent <main> */
|
||||
<div className="flex -m-4 h-[calc(100vh-4rem)] overflow-hidden">
|
||||
|
||||
{/* ── LEFT SIDEBAR ── */}
|
||||
<aside className="w-60 flex-shrink-0 flex flex-col bg-muted/30 border-r border-border/40 h-full font-display">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-3 px-5 py-5 border-b border-border/40">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="font-bold text-base tracking-tight">{t('agents.title')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<button
|
||||
onClick={() => setShowHelp(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary bg-primary/10 border border-primary/20 rounded-lg hover:bg-primary/15 transition-colors"
|
||||
>
|
||||
<LifeBuoy className="w-4 h-4" />
|
||||
{t('agents.help.btnLabel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('agents.newAgent')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Agents grid */}
|
||||
{agents.length > 0 && (
|
||||
<div className="mb-10">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 p-3 space-y-0.5">
|
||||
<button
|
||||
onClick={() => setActiveTab('dashboard')}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium rounded-lg transition-all ${
|
||||
activeTab === 'dashboard'
|
||||
? 'bg-primary/10 text-primary border-r-2 border-primary'
|
||||
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
{t('agents.myAgents')}
|
||||
</h3>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Search and filter */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 mb-4">
|
||||
<div className="relative flex-1 w-full sm:max-w-xs">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
placeholder={t('agents.searchPlaceholder')}
|
||||
className="w-full pl-9 pr-3 py-2 text-sm bg-card border border-border rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{typeFilterOptions.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setTypeFilter(opt.value)}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
|
||||
typeFilter === opt.value
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-muted text-muted-foreground hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{t(opt.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Footer: Help */}
|
||||
<div className="p-3 border-t border-border/40">
|
||||
<button
|
||||
onClick={() => setShowHelp(true)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-background/70 hover:text-foreground rounded-lg transition-all"
|
||||
>
|
||||
<HelpCircle className="w-4 h-4" />
|
||||
{t('agents.help.btnLabel')}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ── MAIN CONTENT ── */}
|
||||
<div className="flex-1 flex flex-col min-w-0 bg-background overflow-hidden">
|
||||
|
||||
{/* Top header bar */}
|
||||
<header className="flex items-center justify-between px-8 py-4 border-b border-border/40 bg-background flex-shrink-0 font-display">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold tracking-tight">
|
||||
{t('agents.myAgents')}
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t('agents.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg shadow-sm hover:shadow-md hover:shadow-primary/20 transition-all"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('agents.newAgent')}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{filteredAgents.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{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/30 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
|
||||
</div>
|
||||
{/* Scrollable content area */}
|
||||
<main className="flex-1 overflow-y-auto p-8">
|
||||
|
||||
{/* 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} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{agents.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center mb-10">
|
||||
<Bot className="w-16 h-16 text-muted-foreground/30 mb-4" />
|
||||
<h3 className="text-lg font-medium text-muted-foreground mb-2">{t('agents.noAgents')}</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
{t('agents.noAgentsDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Templates */}
|
||||
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
|
||||
|
||||
{/* Form modal */}
|
||||
{/* Sliding panels */}
|
||||
{showForm && (
|
||||
<AgentForm
|
||||
agent={editingAgent}
|
||||
@@ -320,8 +340,6 @@ export function AgentsPageClient({
|
||||
onCancel={() => { setShowForm(false); setEditingAgent(null) }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Run log modal */}
|
||||
{logAgent && (
|
||||
<AgentRunLog
|
||||
agentId={logAgent.id}
|
||||
@@ -329,11 +347,9 @@ export function AgentsPageClient({
|
||||
onClose={() => setLogAgent(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Help modal */}
|
||||
{showHelp && (
|
||||
<AgentHelp onClose={() => setShowHelp(false)} />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,15 +19,9 @@ export default async function AgentsPage() {
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full bg-background">
|
||||
<div className="flex-1 p-8 overflow-y-auto">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<AgentsPageClient
|
||||
agents={agents}
|
||||
notebooks={notebooks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AgentsPageClient
|
||||
agents={agents}
|
||||
notebooks={notebooks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import { ProvidersWrapper } from "@/components/providers-wrapper";
|
||||
import { auth } from "@/auth";
|
||||
import { detectUserLanguage } from "@/lib/i18n/detect-user-language";
|
||||
import { loadTranslations } from "@/lib/i18n/load-translations";
|
||||
import { getAISettings } from "@/app/actions/ai-settings";
|
||||
|
||||
import { AIChat } from "@/components/ai-chat";
|
||||
|
||||
export default async function MainLayout({
|
||||
children,
|
||||
@@ -20,6 +23,12 @@ export default async function MainLayout({
|
||||
// Load initial translations server-side to prevent hydration mismatch
|
||||
const initialTranslations = await loadTranslations(initialLanguage);
|
||||
|
||||
// Load AI settings to conditionally render AI features
|
||||
const aiSettings = session?.user?.id
|
||||
? await getAISettings(session.user.id)
|
||||
: null;
|
||||
const showAIAssistant = aiSettings?.paragraphRefactor !== false;
|
||||
|
||||
return (
|
||||
<ProvidersWrapper initialLanguage={initialLanguage} initialTranslations={initialTranslations}>
|
||||
<div className="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-white overflow-hidden h-screen flex flex-col">
|
||||
@@ -27,7 +36,7 @@ export default async function MainLayout({
|
||||
<HeaderWrapper user={session?.user} />
|
||||
|
||||
{/* Main Layout */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
{/* Sidebar Navigation - Style Keep */}
|
||||
<Suspense fallback={<div className="w-64 flex-none hidden md:flex" />}>
|
||||
<Sidebar className="w-64 flex-none flex-col bg-white dark:bg-[#1e2128] border-e border-slate-200 dark:border-slate-800 overflow-y-auto hidden md:flex" user={session?.user} />
|
||||
@@ -37,8 +46,12 @@ export default async function MainLayout({
|
||||
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background-light dark:bg-background-dark p-4 scroll-smooth">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* AI Chat Drawer — only shown if user has Assistant IA enabled */}
|
||||
{showAIAssistant && <AIChat />}
|
||||
</div>
|
||||
</div>
|
||||
</ProvidersWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user