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:
@@ -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) }}
|
||||
|
||||
Reference in New Issue
Block a user