feat: page interactive, démos Play/Step et simulateur Carnot

Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-07-24 17:51:43 +00:00
parent f385d43d5d
commit 69c99e4f4f
67 changed files with 12005 additions and 33 deletions

View File

@@ -19,10 +19,11 @@ import { Badge } from '@/components/ui/badge'
import {
X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles,
Maximize2, Copy, ArrowLeft, ChevronRight, PanelRight, Check, Loader2, Save, MoreHorizontal,
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap, FileDown, FileUp, Mic, MicOff, Printer, PenTool, Loader2 as Loader2Icon, Globe, ExternalLink, History
Trash2, LogOut, Wand2, Share2, Wind, Paperclip, GraduationCap, FileDown, FileUp, Mic, MicOff, Printer, PenTool, Loader2 as Loader2Icon, Globe, ExternalLink, History, Clapperboard
} from 'lucide-react'
import { FlashcardGenerateDialog } from '@/components/flashcards/flashcard-generate-dialog'
import { NoteShareDialog } from './note-share-dialog'
import { InteractivePagePublishDialog } from '@/components/interactive-page/interactive-page-publish-dialog'
import { deleteNote, leaveSharedNote } from '@/app/actions/notes'
import { emitNoteChange } from '@/lib/note-change-sync'
import { useLanguage } from '@/lib/i18n'
@@ -53,6 +54,8 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
const [flashcardsOpen, setFlashcardsOpen] = useState(false)
const [publishOpen, setPublishOpen] = useState(false)
const [publishLoading, setPublishLoading] = useState(false)
const [interactivePageOpen, setInteractivePageOpen] = useState(false)
const [interactivePageContent, setInteractivePageContent] = useState('')
const [publishMeta, setPublishMeta] = useState({
isPublic: Boolean(note.isPublic),
slug: note.publicSlug ?? null,
@@ -343,6 +346,28 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
magazine: t('richTextEditor.publishTemplateMagazine'),
brief: t('richTextEditor.publishTemplateBrief'),
essay: t('richTextEditor.publishTemplateEssay'),
'interactive-page': t('richTextEditor.publishTemplateInteractivePage') || 'Page interactive',
}
const classicPublishTemplates = PUBLISH_TEMPLATES.filter(
(tpl) => tpl !== 'interactive-page'
)
const handlePublishInteractivePage = async () => {
if (publishLoading) return
const consented = await requestAiConsent()
if (!consented) return
if (state.isDirty && !state.isSaving) {
await actions.handleSaveInPlace()
}
const html =
richTextEditorRef?.current?.getEditor()?.getHTML?.() ||
state.content ||
note.content ||
''
setInteractivePageContent(html)
setPublishOpen(false)
setInteractivePageOpen(true)
}
const handlePublishWithAi = async () => {
@@ -664,6 +689,25 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
{t('richTextEditor.publishSimpleHint')}
</p>
<button
type="button"
onClick={handlePublishInteractivePage}
disabled={publishLoading}
className="w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-brand-accent/40 bg-brand-accent/5 text-sm font-medium text-brand-accent hover:bg-brand-accent/10 disabled:opacity-40 transition-colors"
>
{publishLoading ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Clapperboard size={14} />
)}
{t('richTextEditor.publishInteractivePage') ||
'Page interactive'}
</button>
<p className="text-[10px] text-muted-foreground text-center -mt-1">
{t('richTextEditor.publishInteractivePageHint') ||
'Hero, sections, démos Play/Step — 20 crédits'}
</p>
<div className="border-t border-border/50 pt-3 space-y-2">
<div className="flex items-center gap-1.5">
<Sparkles size={13} className="text-brand-accent shrink-0" />
@@ -679,7 +723,7 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
</p>
{/* Sélection template */}
<div className="space-y-1">
{PUBLISH_TEMPLATES.map((tpl) => (
{classicPublishTemplates.map((tpl) => (
<label
key={tpl}
className={cn(
@@ -1009,6 +1053,21 @@ export function NoteEditorToolbar({ mode, onClose, onToggleAttachments, attachme
}}
/>
<InteractivePagePublishDialog
open={interactivePageOpen}
onOpenChange={setInteractivePageOpen}
noteId={note.id}
content={interactivePageContent}
language={language}
onPublished={(slug) => {
setPublishMeta({
isPublic: true,
slug,
template: 'interactive-page',
})
}}
/>
<button
aria-label={t('notes.documentInfoAria')}
onClick={() => { actions.setInfoOpen(!state.infoOpen); actions.setAiOpen(false) }}