feat: publication IA (magazine/brief/essay) + fixes critique
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user