feat: architectural grid editor fullPage + slash commands + doc info panel + AI title

This commit is contained in:
Antigravity
2026-05-07 22:29:02 +00:00
parent 0d8252aec0
commit e458b63115
126 changed files with 7652 additions and 1110 deletions

View File

@@ -159,6 +159,7 @@ export function AgentsPageClient({
role: formData.get('role') as string,
sourceUrls: formData.get('sourceUrls') ? JSON.parse(formData.get('sourceUrls') as string) : undefined,
sourceNotebookId: (formData.get('sourceNotebookId') as string) || undefined,
sourceNoteIds: formData.get('sourceNoteIds') ? JSON.parse(formData.get('sourceNoteIds') as string) : undefined,
targetNotebookId: (formData.get('targetNotebookId') as string) || undefined,
frequency: formData.get('frequency') as string,
tools: formData.get('tools') ? JSON.parse(formData.get('tools') as string) : undefined,
@@ -168,6 +169,8 @@ export function AgentsPageClient({
scheduledTime: (formData.get('scheduledTime') as string) || undefined,
scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined,
timezone: (formData.get('timezone') as string) || undefined,
slideTheme: (formData.get('slideTheme') as string) || undefined,
slideStyle: (formData.get('slideStyle') as string) || undefined,
}
if (editingAgent) {
await updateAgent(editingAgent.id, data)
@@ -196,70 +199,30 @@ export function AgentsPageClient({
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
return (
/* Full-bleed layout: -m-4 cancels the p-4 of the parent <main> */
<div className="flex -m-4 h-[calc(100vh-4rem)] overflow-hidden">
/* Full-bleed layout */
<div className="flex flex-col h-full 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>
{/* 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" />
{/* ── 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')}
</button>
</nav>
{/* 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>
</h1>
<p className="text-[11px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-2">
{t('agents.subtitle')}
</p>
</div>
</aside>
<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>
{/* ── 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>
{/* Scrollable content area */}
<main className="flex-1 overflow-y-auto p-8">
{/* ── Scrollable content area ── */}
<main className="flex-1 overflow-y-auto px-12 py-10">
{/* Dashboard tab - agents + templates */}
{activeTab === 'dashboard' && (
@@ -329,7 +292,6 @@ export function AgentsPageClient({
</>
)}
</main>
</div>
{/* Sliding panels */}
{showForm && (

View File

@@ -1,5 +1,4 @@
import { Suspense } from "react";
import { HeaderWrapper } from "@/components/header-wrapper";
import { Sidebar } from "@/components/sidebar";
import { ProvidersWrapper } from "@/components/providers-wrapper";
import { auth } from "@/auth";
@@ -7,28 +6,23 @@ import { headers } from "next/headers";
import { detectUserLanguage, parseAcceptLanguage } 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";
import { AIChatLayoutBridge } from "@/components/ai-chat-layout-bridge";
export default async function MainLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// Read browser language hint from Accept-Language header
const headersList = await headers();
const browserLang = parseAcceptLanguage(headersList.get("accept-language"));
// Run auth + language detection + translation loading in parallel
const [session, initialLanguage] = await Promise.all([
auth(),
detectUserLanguage(browserLang),
]);
// 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;
@@ -36,25 +30,17 @@ export default async function MainLayout({
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">
{/* Top Navigation - Style Keep */}
<HeaderWrapper user={session?.user} />
{/* No top-bar header — sidebar-only navigation (architectural-grid design) */}
<div className="flex h-screen overflow-hidden bg-[#E5E2D9]">
<Suspense fallback={<div className="hidden w-80 shrink-0 md:block" />}>
<Sidebar user={session?.user} />
</Suspense>
{/* Main Layout */}
<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} />
</Suspense>
<main className="memento-paper-texture flex min-h-0 flex-1 flex-col overflow-y-auto scroll-smooth">
{children}
</main>
{/* Main Content Area */}
<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>
{showAIAssistant && <AIChatLayoutBridge />}
</div>
</ProvidersWrapper>
);

View File

@@ -10,10 +10,12 @@ export default async function HomePage() {
const notesViewMode =
settings?.notesViewMode === 'masonry'
? 'masonry' as const
: settings?.notesViewMode === 'tabs' || settings?.notesViewMode === 'list'
? 'tabs' as const
: 'masonry' as const
? ('masonry' as const)
: settings?.notesViewMode === 'tabs'
? ('tabs' as const)
: settings?.notesViewMode === 'list'
? ('list' as const)
: ('masonry' as const)
return (
<HomeClient

View File

@@ -1,132 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { SettingsSection, SettingSelect } from '@/components/settings'
import { updateAISettings as updateAI } from '@/app/actions/ai-settings'
import { updateUserSettings as updateUser } from '@/app/actions/user-settings'
import { useLanguage } from '@/lib/i18n'
interface AppearanceSettingsFormProps {
initialTheme: string
initialFontSize: string
initialCardSizeMode?: string
}
export function AppearanceSettingsForm({ initialTheme, initialFontSize, initialCardSizeMode = 'variable' }: AppearanceSettingsFormProps) {
const router = useRouter()
const [theme, setTheme] = useState(initialTheme)
const [fontSize, setFontSize] = useState(initialFontSize)
const [cardSizeMode, setCardSizeMode] = useState(initialCardSizeMode)
const { t } = useLanguage()
const handleThemeChange = async (value: string) => {
setTheme(value)
localStorage.setItem('theme-preference', value)
// Instant visual update
const root = document.documentElement
root.removeAttribute('data-theme')
root.classList.remove('dark')
if (value === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark')
} else if (value === 'dark') {
root.classList.add('dark')
} else if (value === 'light') {
root.setAttribute('data-theme', 'light')
} else {
root.setAttribute('data-theme', value)
if (['midnight', 'blue', 'sepia'].includes(value)) root.classList.add('dark')
}
// Save to DB (no need for router.refresh - localStorage handles immediate visuals)
await updateUser({ theme: value as 'light' | 'dark' | 'auto' | 'sepia' | 'midnight' | 'blue' })
}
const handleFontSizeChange = async (value: string) => {
setFontSize(value)
// Instant visual update
const fontSizeMap: Record<string, string> = {
'small': '14px', 'medium': '16px', 'large': '18px', 'extra-large': '20px'
}
const root = document.documentElement
root.style.setProperty('--user-font-size', fontSizeMap[value] || '16px')
await updateAI({ fontSize: value as any })
}
const handleCardSizeModeChange = async (value: string) => {
setCardSizeMode(value)
localStorage.setItem('card-size-mode', value)
await updateUser({ cardSizeMode: value as 'variable' | 'uniform' })
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('appearance.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('appearance.description')}
</p>
</div>
<SettingsSection
title={t('settings.theme')}
icon={<span className="text-2xl">🎨</span>}
description={t('settings.themeLight') + ' / ' + t('settings.themeDark')}
>
<SettingSelect
label={t('settings.theme')}
description={t('settings.selectLanguage')}
value={theme}
options={[
{ value: 'slate', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'blue', label: 'Blue' },
{ value: 'auto', label: t('settings.themeSystem') },
]}
onChange={handleThemeChange}
/>
</SettingsSection>
<SettingsSection
title={t('profile.fontSize')}
icon={<span className="text-2xl">📝</span>}
description={t('profile.fontSizeDescription')}
>
<SettingSelect
label={t('profile.fontSize')}
description={t('profile.selectFontSize')}
value={fontSize}
options={[
{ value: 'small', label: t('profile.fontSizeSmall') },
{ value: 'medium', label: t('profile.fontSizeMedium') },
{ value: 'large', label: t('profile.fontSizeLarge') },
]}
onChange={handleFontSizeChange}
/>
</SettingsSection>
<SettingsSection
title={t('settings.cardSizeMode')}
icon={<span className="text-2xl">📐</span>}
description={t('settings.cardSizeModeDescription')}
>
<SettingSelect
label={t('settings.cardSizeMode')}
description={t('settings.selectCardSizeMode')}
value={cardSizeMode}
options={[
{ value: 'variable', label: t('settings.cardSizeVariable') },
{ value: 'uniform', label: t('settings.cardSizeUniform') },
]}
onChange={handleCardSizeModeChange}
/>
</SettingsSection>
</div>
)
}

View File

@@ -6,11 +6,12 @@ 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 { applyDocumentTheme, normalizeThemeId, type ThemeId } from '@/lib/apply-document-theme'
interface AppearanceSettingsClientProps {
initialFontSize: string
initialTheme: string
initialNotesViewMode: 'masonry' | 'tabs'
initialNotesViewMode: 'masonry' | 'tabs' | 'list'
initialCardSizeMode?: 'variable' | 'uniform'
initialFontFamily?: string
}
@@ -23,27 +24,18 @@ export function AppearanceSettingsClient({
initialFontFamily = 'inter',
}: AppearanceSettingsClientProps) {
const { t } = useLanguage()
const [theme, setTheme] = useState(initialTheme || 'light')
const [theme, setTheme] = useState<ThemeId>(normalizeThemeId(initialTheme || 'light'))
const [fontSize, setFontSize] = useState(initialFontSize || 'medium')
const [notesViewMode, setNotesViewMode] = useState<'masonry' | 'tabs'>(initialNotesViewMode)
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) => {
setTheme(value)
localStorage.setItem('theme-preference', value)
const root = document.documentElement
root.removeAttribute('data-theme')
root.classList.remove('dark')
if (value === 'auto') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.add('dark')
} else if (value === 'dark') {
root.classList.add('dark')
} else {
root.setAttribute('data-theme', value)
if (['midnight'].includes(value)) root.classList.add('dark')
}
await updateUserSettings({ theme: value as any })
const next = normalizeThemeId(value)
setTheme(next)
localStorage.setItem('theme-preference', next)
applyDocumentTheme(next)
await updateUserSettings({ theme: next })
toast.success(t('settings.settingsSaved') || 'Saved')
}
@@ -56,7 +48,7 @@ export function AppearanceSettingsClient({
}
const handleNotesViewChange = async (value: string) => {
const mode = value === 'tabs' ? 'tabs' : 'masonry'
const mode = value === 'tabs' ? 'tabs' : value === 'list' ? 'list' : 'masonry'
setNotesViewMode(mode)
await updateAISettings({ notesViewMode: mode })
toast.success(t('settings.settingsSaved') || 'Saved')
@@ -87,12 +79,14 @@ export function AppearanceSettingsClient({
value,
options,
onChange,
optionGroups,
}: {
icon: React.ElementType
title: string
description: string
value: string
options: { value: string; label: string }[]
options?: { value: string; label: string }[]
optionGroups?: { label: string; options: { value: string; label: string }[] }[]
onChange: (v: string) => void
}) => (
<div className="bg-card rounded-lg border border-border p-6 shadow-sm flex flex-col gap-4">
@@ -111,17 +105,56 @@ export function AppearanceSettingsClient({
onChange={(e) => onChange(e.target.value)}
className="w-full h-11 px-4 bg-muted border border-border rounded-lg text-foreground text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none appearance-none cursor-pointer transition-colors"
>
{options.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
{optionGroups
? optionGroups.map((g) => (
<optgroup key={g.label} label={g.label}>
{g.options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</optgroup>
))
: options?.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
</div>
)
const themeOptionGroups = [
{
label: t('settings.themeBaseGroup'),
options: [
{ value: 'light', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'auto', label: t('settings.themeSystem') },
],
},
{
label: t('settings.themePalettesGroup'),
options: [
{ value: 'sepia', label: t('settings.themeSepia') },
{ value: 'midnight', label: t('settings.themeMidnight') },
{ value: 'rose', label: t('settings.themeRose') },
{ value: 'green', label: t('settings.themeGreen') },
{ value: 'lavender', label: t('settings.themeLavender') },
{ value: 'sand', label: t('settings.themeSand') },
{ value: 'ocean', label: t('settings.themeOcean') },
{ value: 'sunset', label: t('settings.themeSunset') },
{ value: 'blue', label: t('settings.themeBlue') },
],
},
]
return (
<div className="space-y-8">
<div>
@@ -135,19 +168,7 @@ export function AppearanceSettingsClient({
title={t('settings.theme')}
description={t('appearance.selectTheme')}
value={theme}
options={[
{ value: 'light', label: t('settings.themeLight') },
{ value: 'dark', label: t('settings.themeDark') },
{ value: 'sepia', label: 'Sepia' },
{ value: 'midnight', label: 'Midnight' },
{ value: 'rose', label: 'Rose' },
{ value: 'green', label: 'Green' },
{ value: 'lavender', label: 'Lavender' },
{ value: 'sand', label: 'Sand' },
{ value: 'ocean', label: 'Ocean' },
{ value: 'sunset', label: 'Sunset' },
{ value: 'auto', label: t('settings.themeSystem') },
]}
optionGroups={themeOptionGroups}
onChange={handleThemeChange}
/>
@@ -168,7 +189,7 @@ export function AppearanceSettingsClient({
<SelectCard
icon={Type}
title={t('appearance.fontFamilyLabel') || 'Police'}
description={t('appearance.fontFamilyDescription') || 'Choisissez la police de l\'application'}
description={t('appearance.fontFamilyDescription') || "Choisissez la police de l'application"}
value={fontFamily}
options={[
{ value: 'inter', label: 'Inter' },
@@ -184,6 +205,7 @@ export function AppearanceSettingsClient({
value={notesViewMode}
options={[
{ value: 'masonry', label: t('appearance.notesViewMasonry') },
{ value: 'list', label: t('appearance.notesViewList') },
{ value: 'tabs', label: t('appearance.notesViewTabs') },
]}
onChange={handleNotesViewChange}

View File

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