Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
Some checks failed
CI / Lint, Unit Tests & Build (push) Successful in 5m37s
CI / Deploy production (on server) (push) Has been cancelled

Story 6-2 — Markdown roundtrip export/import:
- lib/editor/markdown-export.ts: tiptapHTMLToMarkdown, markdownToHTML, looksLikeMarkdown
- lib/editor/markdown-paste-extension.ts: TipTap extension paste Markdown → blocs
- note-editor-toolbar.tsx: export .md + import .md (file picker)
- rich-text-editor.tsx: intégration MarkdownPasteExtension
- 40 tests unitaires markdown-export.test.ts

Story 6-3 — Brainstorm PPTX + Canvas:
- lib/brainstorm/export-pptx.ts: génération PPTX 5 slides (pptxgenjs)
- app/api/brainstorm/[sessionId]/export-pptx/route.ts: route POST protégée
- brainstorm-page.tsx: bouton PPTX, auto-select session, fix emoji, fix router.replace
- wave-canvas.tsx: fitTrigger recentrage, légende bas-droite

Onboarding activation wizard (Story 6-1):
- components/onboarding/: wizard multi-étapes, hints éditeur
- app/api/onboarding/: route PATCH onboarding
- prisma/migrations: champs onboarding user

Locales: 15 langues mises à jour (brainstorm, markdown, onboarding keys)
Sprint: 6-1 done, 6-2 review, 6-3 review

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 11:24:56 +00:00
parent dae56187fc
commit 6b4ed8514f
49 changed files with 5215 additions and 66 deletions

View File

@@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '@/lib/prisma'
import { auth } from '@/auth'
import { generateBrainstormPptx } from '@/lib/brainstorm/export-pptx'
export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const { sessionId } = await params
const brainstormSession = await prisma.brainstormSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId: session.user.id },
{ participants: { some: { userId: session.user.id } } },
],
},
include: {
ideas: {
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
},
},
})
if (!brainstormSession) {
return NextResponse.json({ error: 'Session not found' }, { status: 404 })
}
const sessionData = {
seedIdea: brainstormSession.seedIdea,
createdAt: brainstormSession.createdAt,
ideas: brainstormSession.ideas.map(idea => ({
id: idea.id,
title: idea.title,
description: idea.description,
waveNumber: idea.waveNumber,
status: idea.status,
isStarred: idea.isStarred,
convertedToNoteId: idea.convertedToNoteId,
})),
}
const { buffer, filename } = await generateBrainstormPptx(sessionData)
return new NextResponse(new Uint8Array(buffer), {
status: 200,
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': String(buffer.length),
},
})
} catch (err) {
console.error('[export-pptx]', err)
return NextResponse.json({ error: 'Export failed' }, { status: 500 })
}
}

View File

@@ -0,0 +1,300 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import prisma from '@/lib/prisma'
import { embeddingService } from '@/lib/ai/services/embedding.service'
import { upsertNoteEmbedding } from '@/lib/embeddings'
type DemoNote = { title: string; content: string }
const DEMO_NOTES: Record<string, DemoNote[]> = {
fr: [
{
title: 'Réunion Q3 — Stratégie produit',
content: `<h2>Réunion Q3 — Stratégie produit</h2>
<p>Points clés abordés lors de la réunion trimestrielle :</p>
<ul>
<li><strong>Roadmap</strong> : priorité aux features de monétisation (Stripe, BYOK) avant les agents autonomes</li>
<li><strong>KPI Activation</strong> : objectif 40% d'utilisateurs actifs dans les 7 jours suivant l'inscription</li>
<li><strong>Acquisition PLG</strong> : brainstorm partageable = viral loop principale</li>
<li><strong>Pricing</strong> : Pro à 9,90 €/mois validé avec 3 personas cibles</li>
</ul>
<p>Prochaine étape : finaliser l'onboarding wizard pour atteindre le taux d'activation cible.</p>`,
},
{
title: 'Idées de projets secondaires',
content: `<h2>Idées de projets secondaires</h2>
<p>Liste de projets à explorer dans les prochains mois :</p>
<ul>
<li>📱 <strong>App de suivi d'habitudes</strong> — gamification, streaks, rappels intelligents</li>
<li>🎙️ <strong>Podcast "Tech Indépendant"</strong> — interviews de freelances tech, conseils pratiques</li>
<li>🤖 <strong>Bot Telegram de veille</strong> — scraping RSS + résumé IA chaque matin</li>
<li>📚 <strong>Newsletter hebdomadaire</strong> — curation de ressources pour développeurs</li>
<li>🎮 <strong>Jeu de stratégie minimaliste</strong> — WebGL, logique pure, pas de monétisation</li>
</ul>
<p>Critères de sélection : impact fort, temps minimal, apprentissage technique.</p>`,
},
{
title: 'Livres à lire — Recommandations',
content: `<h2>Livres à lire — Recommandations</h2>
<h3>Productivité & Pensée</h3>
<ul>
<li><strong>Building a Second Brain</strong> — Tiago Forte · Système de notes PKM, méthode CODE</li>
<li><strong>Deep Work</strong> — Cal Newport · Concentration profonde dans un monde de distractions</li>
<li><strong>Atomic Habits</strong> — James Clear · Les habitudes comme système de progression</li>
</ul>
<h3>Technique</h3>
<ul>
<li><strong>Designing Data-Intensive Applications</strong> — Martin Kleppmann · Architecture systèmes</li>
<li><strong>The Pragmatic Programmer</strong> — Hunt & Thomas · Bonnes pratiques développement</li>
</ul>
<p>Prochaine lecture : Building a Second Brain (lien avec Momento évident).</p>`,
},
{
title: 'Notes de formation React — Hooks avancés',
content: `<h2>Notes de formation React — Hooks avancés</h2>
<h3>useCallback vs useMemo</h3>
<ul>
<li><strong>useCallback</strong> : mémoïse une <em>fonction</em>, utile pour éviter re-renders des enfants</li>
<li><strong>useMemo</strong> : mémoïse une <em>valeur calculée</em>, utile pour calculs coûteux</li>
<li>⚠️ Ne pas abuser — le coût de la mémoïsation peut dépasser le gain</li>
</ul>
<h3>useReducer</h3>
<p>Préférer <code>useReducer</code> à <code>useState</code> quand :</p>
<ul>
<li>L'état a plusieurs sous-valeurs liées</li>
<li>La prochaine valeur dépend de la précédente</li>
<li>La logique de transition est complexe</li>
</ul>
<h3>Pattern : Context + useReducer</h3>
<p>Combine Context API avec useReducer pour un state management léger sans Redux.</p>`,
},
{
title: 'Objectifs personnels 2025',
content: `<h2>Objectifs personnels 2025</h2>
<h3>🎯 Professionnel</h3>
<ul>
<li>Lancer un produit SaaS avec les premiers utilisateurs payants</li>
<li>Contribuer à un projet open source significatif</li>
<li>Maîtriser l'architecture de systèmes distribués</li>
</ul>
<h3>📚 Apprentissage</h3>
<ul>
<li>Lire 12 livres techniques (1 par mois)</li>
<li>Compléter une formation en machine learning appliqué</li>
<li>Améliorer le niveau en algorithmique (LeetCode medium)</li>
</ul>
<h3>🌱 Personnel</h3>
<ul>
<li>Sport : 3x par semaine minimum</li>
<li>Méditation : 10 minutes par jour (habitude matinale)</li>
<li>Voyager dans 2 nouveaux pays</li>
</ul>
<p>Revue mensuelle le 1er de chaque mois pour ajuster les priorités.</p>`,
},
],
en: [
{
title: 'Q3 Meeting — Product Strategy',
content: `<h2>Q3 Meeting — Product Strategy</h2>
<p>Key points from the quarterly review:</p>
<ul>
<li><strong>Roadmap</strong>: monetization features first (Stripe, BYOK) before autonomous agents</li>
<li><strong>Activation KPI</strong>: target 40% active users within 7 days of signup</li>
<li><strong>PLG Acquisition</strong>: shareable brainstorm canvas = main viral loop</li>
<li><strong>Pricing</strong>: Pro at $9.90/month validated with 3 target personas</li>
</ul>
<p>Next step: finalize onboarding wizard to reach activation target.</p>`,
},
{
title: 'Side Project Ideas',
content: `<h2>Side Project Ideas</h2>
<p>Projects to explore in the coming months:</p>
<ul>
<li>📱 <strong>Habit tracking app</strong> — gamification, streaks, smart reminders</li>
<li>🎙️ <strong>Tech podcast</strong> — interviews with indie developers, practical advice</li>
<li>🤖 <strong>Telegram news bot</strong> — RSS scraping + AI summary every morning</li>
<li>📚 <strong>Weekly newsletter</strong> — curated resources for developers</li>
<li>🎮 <strong>Minimalist strategy game</strong> — WebGL, pure logic, no monetization</li>
</ul>
<p>Selection criteria: high impact, minimal time, technical learning.</p>`,
},
{
title: 'Books to Read — Recommendations',
content: `<h2>Books to Read — Recommendations</h2>
<h3>Productivity & Thinking</h3>
<ul>
<li><strong>Building a Second Brain</strong> — Tiago Forte · PKM note-taking system, CODE method</li>
<li><strong>Deep Work</strong> — Cal Newport · Deep focus in a distracted world</li>
<li><strong>Atomic Habits</strong> — James Clear · Habits as a system for progress</li>
</ul>
<h3>Technical</h3>
<ul>
<li><strong>Designing Data-Intensive Applications</strong> — Martin Kleppmann · Systems architecture</li>
<li><strong>The Pragmatic Programmer</strong> — Hunt & Thomas · Development best practices</li>
</ul>
<p>Next read: Building a Second Brain (obvious connection to Momento).</p>`,
},
{
title: 'React Training Notes — Advanced Hooks',
content: `<h2>React Training Notes — Advanced Hooks</h2>
<h3>useCallback vs useMemo</h3>
<ul>
<li><strong>useCallback</strong>: memoizes a <em>function</em>, useful to prevent child re-renders</li>
<li><strong>useMemo</strong>: memoizes a <em>computed value</em>, useful for expensive calculations</li>
<li>⚠️ Don't overuse — memoization cost can exceed the gain</li>
</ul>
<h3>useReducer</h3>
<p>Prefer <code>useReducer</code> over <code>useState</code> when:</p>
<ul>
<li>State has multiple related sub-values</li>
<li>Next state depends on previous state</li>
<li>Transition logic is complex</li>
</ul>`,
},
{
title: 'Personal Goals 2025',
content: `<h2>Personal Goals 2025</h2>
<h3>🎯 Professional</h3>
<ul>
<li>Launch a SaaS product with first paying users</li>
<li>Contribute to a significant open source project</li>
<li>Master distributed systems architecture</li>
</ul>
<h3>📚 Learning</h3>
<ul>
<li>Read 12 technical books (1 per month)</li>
<li>Complete an applied machine learning course</li>
<li>Improve algorithmic skills (LeetCode medium)</li>
</ul>
<h3>🌱 Personal</h3>
<ul>
<li>Exercise: 3x per week minimum</li>
<li>Meditation: 10 minutes per day (morning habit)</li>
<li>Travel to 2 new countries</li>
</ul>`,
},
],
fa: [
{
title: 'جلسه Q3 — استراتژی محصول',
content: `<h2>جلسه Q3 — استراتژی محصول</h2>
<p>نکات کلیدی جلسه فصلی:</p>
<ul>
<li><strong>نقشه راه</strong>: اول ویژگی‌های پولی‌سازی (Stripe، BYOK) و سپس عامل‌های خودمختار</li>
<li><strong>KPI فعال‌سازی</strong>: هدف ۴۰٪ کاربران فعال در ۷ روز اول</li>
<li><strong>رشد محصول‌محور</strong>: اشتراک‌گذاری بوم طوفان فکری = حلقه ویروسی اصلی</li>
</ul>
<p>قدم بعدی: نهایی کردن ویزارد آنبوردینگ برای رسیدن به هدف فعال‌سازی.</p>`,
},
{
title: 'ایده‌های پروژه‌های جانبی',
content: `<h2>ایده‌های پروژه‌های جانبی</h2>
<ul>
<li>📱 <strong>اپلیکیشن پیگیری عادت</strong> — گیمیفیکیشن، استریک، یادآوری هوشمند</li>
<li>🎙️ <strong>پادکست فناوری</strong> — مصاحبه با توسعه‌دهندگان مستقل</li>
<li>🤖 <strong>ربات تلگرام اخبار</strong> — خلاصه‌سازی روزانه با هوش مصنوعی</li>
</ul>`,
},
{
title: 'کتاب‌های پیشنهادی',
content: `<h2>کتاب‌های پیشنهادی</h2>
<ul>
<li><strong>ساختن مغز دوم</strong> — تیاگو فورتی · سیستم یادداشت‌برداری PKM</li>
<li><strong>کار عمیق</strong> — کال نیوپورت · تمرکز عمیق در دنیای پر از حواس‌پرتی</li>
<li><strong>عادت‌های اتمی</strong> — جیمز کلیر · عادت‌ها به عنوان سیستم پیشرفت</li>
</ul>`,
},
{
title: 'یادداشت‌های آموزش React',
content: `<h2>یادداشت‌های آموزش React</h2>
<h3>useCallback و useMemo</h3>
<ul>
<li><strong>useCallback</strong>: یک تابع را حفظ می‌کند</li>
<li><strong>useMemo</strong>: یک مقدار محاسبه‌شده را حفظ می‌کند</li>
</ul>`,
},
{
title: 'اهداف شخصی ۲۰۲۵',
content: `<h2>اهداف شخصی ۲۰۲۵</h2>
<h3>🎯 حرفه‌ای</h3>
<ul>
<li>راه‌اندازی یک محصول SaaS با اولین کاربران پرداخت‌کننده</li>
<li>مشارکت در یک پروژه متن‌باز مهم</li>
</ul>
<h3>📚 یادگیری</h3>
<ul>
<li>خواندن ۱۲ کتاب فناوری (یکی در ماه)</li>
<li>تکمیل یک دوره یادگیری ماشین کاربردی</li>
</ul>`,
},
],
}
function getNotesForLocale(locale: string): DemoNote[] {
const lang = locale.split('-')[0].toLowerCase()
return DEMO_NOTES[lang] ?? DEMO_NOTES.en
}
/** Wraps a promise with a timeout — rejects after `ms` milliseconds. */
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
)
return Promise.race([promise, timeout])
}
export async function POST(req: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
let locale = 'en'
try {
const body = await req.json().catch(() => ({}))
if (body?.locale) locale = body.locale
} catch { /* ignore */ }
// Idempotency check — if any demo notes already exist, return them (handles partial creation too)
const existing = await prisma.note.findMany({
where: { userId, isDemo: true, trashedAt: null },
select: { id: true, title: true },
})
if (existing.length > 0) {
return NextResponse.json({ created: false, notes: existing, message: 'Demo notes already exist' })
}
const demoNotes = getNotesForLocale(locale)
const created: { id: string; title: string | null }[] = []
for (const demo of demoNotes) {
const note = await prisma.note.create({
data: {
userId,
title: demo.title,
content: demo.content,
isMarkdown: true,
isDemo: true,
type: 'richtext',
color: 'default',
},
})
created.push({ id: note.id, title: note.title })
// Synchronous embedding generation so semantic search works immediately (6s timeout per note)
try {
const { embedding } = await withTimeout(
embeddingService.generateNoteEmbedding(demo.title, demo.content),
6000
)
if (embedding) {
await withTimeout(upsertNoteEmbedding(note.id, embedding), 3000)
}
} catch (e) {
console.error('[ONBOARDING] Embedding failed for demo note:', note.id, e)
}
}
return NextResponse.json({ created: true, notes: created })
}

View File

@@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
import prisma from '@/lib/prisma'
/**
* GET /api/user/me
* Returns lightweight user profile including onboardingCompleted flag.
*/
export async function GET() {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { id: true, name: true, email: true, onboardingCompleted: true, onboardingStep: true },
})
if (!user) return NextResponse.json({ error: 'User not found' }, { status: 404 })
return NextResponse.json(user)
}
/**
* PATCH /api/user/me
* Partial update of user profile. Supported fields: onboardingCompleted, onboardingStep.
*/
export async function PATCH(req: NextRequest) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
let body: Record<string, unknown>
try {
body = await req.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
}
const allowedFields = ['onboardingCompleted', 'onboardingStep'] as const
type AllowedField = typeof allowedFields[number]
const data: Partial<Record<AllowedField, unknown>> = {}
if ('onboardingCompleted' in body) {
data.onboardingCompleted = Boolean(body.onboardingCompleted)
}
if ('onboardingStep' in body) {
const val = parseInt(String(body.onboardingStep), 10)
if (Number.isInteger(val) && val >= 0 && val <= 10) {
data.onboardingStep = val
}
}
if (Object.keys(data).length === 0) {
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 })
}
const updated = await prisma.user.update({
where: { id: session.user.id },
data: data as any,
select: { id: true, onboardingCompleted: true, onboardingStep: true },
})
return NextResponse.json(updated)
}