feat: publication IA (magazine/brief/essay) + fixes critique
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped

Publication IA:
- 4 templates (magazine, brief, essay, simple) avec CSS riche
- Rewrite IA (article/exercises/tutorial/reference/mixed)
- Modération avec timeout 12s + fallback safe
- Quotas publish_enhance par tier (basic=2, pro=15, business=100)
- Détection contenu stale (hash)
- Migration DB publishedContent/publishedTemplate/publishedSourceHash

Fixes:
- cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast
- _isShared ajouté au type Note (champ virtuel serveur)
- callout colors PDF export: extraction fonction pure testable
- admin/published: guard note.userId null
- Cmd+S fonctionne en mode dialog (pas seulement fullPage)

i18n:
- 23 clés publish* traduites dans les 15 locales
- Extension Web Clipper: 13 locales mise à jour

Tests:
- callout-colors.test.ts (6 tests)
- note-visible-in-view.test.ts (5 tests)
- entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs
- 199/199 tests passent

Tracker: user-stories.md sync avec sprint-status.yaml
This commit is contained in:
Antigravity
2026-06-28 07:32:57 +00:00
parent 902fe95a69
commit 96e7902f01
169 changed files with 5382 additions and 1527 deletions

View File

@@ -20,7 +20,7 @@ import {
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
import { Button } from '@/components/ui/button'
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload } from 'lucide-react'
import { Plus, ArrowUpDown, Search, Sparkles, FileText, FolderOpen, ChevronRight, ChevronDown, Tag as TagIcon, X, Menu, LayoutGrid, List, Table, Columns3, CalendarDays, Wand2, Download, Upload, Globe } from 'lucide-react'
import { emitNoteChange } from '@/lib/note-change-sync'
import { useReminderCheck } from '@/hooks/use-reminder-check'
import { useAutoLabelSuggestion } from '@/hooks/use-auto-label-suggestion'
@@ -58,10 +58,18 @@ const OrganizeNotebookDialog = dynamic(
() => import('@/components/organize-notebook-dialog').then(m => ({ default: m.OrganizeNotebookDialog })),
{ ssr: false }
)
const NotebookSiteDialog = dynamic(
() => import('@/components/wizard/notebook-site-dialog').then(m => ({ default: m.NotebookSiteDialog })),
{ ssr: false }
)
const StructuredViewsIntro = dynamic(
() => import('@/components/structured-views/structured-views-intro').then(m => ({ default: m.StructuredViewsIntro })),
{ ssr: false }
)
const StructuredViewsWizard = dynamic(
() => import('@/components/structured-views/structured-views-wizard').then(m => ({ default: m.StructuredViewsWizard })),
{ ssr: false }
)
const StructuredViewsHelpBanner = dynamic(
() => import('@/components/structured-views/structured-views-help-banner').then(m => ({ default: m.StructuredViewsHelpBanner })),
{ ssr: false }
@@ -95,7 +103,7 @@ export function HomeClient({
}: HomeClientProps) {
const searchParams = useSearchParams()
const router = useRouter()
const { t } = useLanguage()
const { t, language } = useLanguage()
const [notes, setNotes] = useState<Note[]>(initialNotes)
const [pinnedNotes, setPinnedNotes] = useState<Note[]>(
@@ -136,6 +144,10 @@ export function HomeClient({
const [layoutMode, setLayoutMode] = useState<NotesLayoutMode>(initialLayoutMode)
const [addPropertyOpen, setAddPropertyOpen] = useState(false)
const [isEnablingStructured, setIsEnablingStructured] = useState(false)
const [showStructuredWizard, setShowStructuredWizard] = useState(false)
const [showNotebookSite, setShowNotebookSite] = useState(false)
const [aiMenuOpen, setAiMenuOpen] = useState(false)
const aiMenuRef = useRef<HTMLDivElement>(null)
const [showStudyPlanner, setShowStudyPlanner] = useState(false)
const [showOrganizer, setShowOrganizer] = useState(false)
@@ -237,7 +249,7 @@ export function HomeClient({
}
let allNotes = search
? await searchNotes(search, true, notebook || undefined)
? await searchNotes(search, true, notebook || undefined).then(r => { window.dispatchEvent(new Event('ai-usage-changed')); return r })
: await getAllNotes(false, notebook || undefined)
if (sharedOnly) {
@@ -1045,31 +1057,65 @@ export function HomeClient({
)}
{searchParams.get('notebook') && initialSettings.aiAssistantEnabled && (
<div className="flex items-center gap-3">
<div className="relative" ref={aiMenuRef}>
<button
onClick={() => setSummaryDialogOpen(true)}
className="flex items-center gap-1.5 text-[12px] text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setAiMenuOpen(o => !o)}
className={cn(
'flex items-center gap-1.5 text-[12px] font-medium transition-colors px-2.5 py-1.5 rounded-lg border',
aiMenuOpen
? 'bg-brand-accent/10 border-brand-accent/30 text-brand-accent'
: 'border-border/50 text-muted-foreground hover:text-foreground hover:border-border',
)}
>
<FileText size={14} />
<span>{t('notebook.summary')}</span>
</button>
<span className="w-px h-3.5 bg-border/40" />
<button
onClick={() => setShowStudyPlanner(true)}
className="flex items-center gap-1.5 text-[12px] text-muted-foreground hover:text-brand-accent transition-colors"
>
<CalendarDays size={14} />
<span>{t('wizard.studyPlanner') || 'Planning'}</span>
</button>
<span className="w-px h-3.5 bg-border/40" />
<button
onClick={() => setOrganizeNotebookOpen(true)}
className="flex items-center gap-1.5 text-[12px] text-muted-foreground hover:text-brand-accent transition-colors"
title={t('notebook.organizeNotebookWithAITooltip')}
>
<Sparkles size={14} />
<span>{t('batch.organize')}</span>
<Sparkles size={13} />
<span>IA</span>
<ChevronDown size={11} className={cn('transition-transform', aiMenuOpen && 'rotate-180')} />
</button>
{aiMenuOpen && (
<>
{/* overlay invisible pour fermer au clic extérieur */}
<div className="fixed inset-0 z-10" onClick={() => setAiMenuOpen(false)} />
<div className="absolute right-0 top-full mt-1.5 z-20 w-52 bg-popover border border-border rounded-xl shadow-lg overflow-hidden">
{[
{
icon: <FileText size={14} />,
label: t('notebook.summary') || 'Résumé du carnet',
action: () => { setSummaryDialogOpen(true); setAiMenuOpen(false) },
},
{
icon: <CalendarDays size={14} />,
label: t('wizard.studyPlanner') || 'Planning de révision',
action: () => { setShowStudyPlanner(true); setAiMenuOpen(false) },
},
{
icon: <Sparkles size={14} />,
label: t('batch.organize') || 'Organiser avec l\'IA',
action: () => { setOrganizeNotebookOpen(true); setAiMenuOpen(false) },
},
{
icon: <TagIcon size={14} />,
label: t('wizard.autoTags') || 'Tags automatiques',
action: () => { setShowOrganizer(true); setAiMenuOpen(false) },
},
{
icon: <Globe size={14} />,
label: t('notebookSite.shortTitle') || 'Site web',
action: () => { setShowNotebookSite(true); setAiMenuOpen(false) },
},
].map((item, i) => (
<button
key={i}
onClick={item.action}
className="w-full flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-foreground hover:bg-muted/60 transition-colors text-left"
>
<span className="text-muted-foreground">{item.icon}</span>
{item.label}
</button>
))}
</div>
</>
)}
</div>
)}
{searchParams.get('notebook') && (
@@ -1185,11 +1231,29 @@ export function HomeClient({
) : showStructuredLoading ? (
<div className="text-center py-8 text-muted-foreground">{t('general.loading')}</div>
) : showStructuredIntro ? (
<StructuredViewsIntro
target={structuredViewMode}
enabling={isEnablingStructured}
onEnable={() => void handleEnableStructured(structuredViewMode)}
/>
showStructuredWizard ? (
<StructuredViewsWizard
open={showStructuredWizard}
onClose={() => setShowStructuredWizard(false)}
onComplete={(view) => {
setShowStructuredWizard(false)
if (view !== 'gallery') setLayoutMode(view)
void schemaHook.reload()
}}
structuredModeActive={structuredModeActive}
enableStructuredMode={schemaHook.enableStructuredMode}
addProperty={schemaHook.addProperty}
setKanbanGroupProperty={schemaHook.setKanbanGroupProperty}
initialGoal={structuredViewMode === 'kanban' ? 'tasks' : undefined}
/>
) : (
<StructuredViewsIntro
target={structuredViewMode}
enabling={isEnablingStructured}
onEnable={() => void handleEnableStructured(structuredViewMode)}
onOpenWizard={() => setShowStructuredWizard(true)}
/>
)
) : showStructuredDataView && schemaHook.schema && notebookFilter ? (
<>
<StructuredViewsHelpBanner notebookId={notebookFilter} mode={structuredViewMode} />
@@ -1347,6 +1411,14 @@ export function HomeClient({
onClose={() => setShowOrganizer(false)}
/>
)}
{showNotebookSite && currentNotebook && (
<NotebookSiteDialog
notebookId={currentNotebook.id}
notebookName={currentNotebook.name}
onClose={() => setShowNotebookSite(false)}
/>
)}
</div>
)
}