Epic 6: Stories 6-2 (Markdown roundtrip) + 6-3 (Brainstorm PPTX + Canvas)
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:
@@ -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 })
|
||||
}
|
||||
}
|
||||
300
memento-note/app/api/onboarding/seed-demo-notes/route.ts
Normal file
300
memento-note/app/api/onboarding/seed-demo-notes/route.ts
Normal 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 })
|
||||
}
|
||||
61
memento-note/app/api/user/me/route.ts
Normal file
61
memento-note/app/api/user/me/route.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user