Livre US-FLASHCARDS avec decks, session de révision, stats et migration Prisma. Finalise le Web Clipper (i18n 15 langues) et corrige les erreurs ESLint bloquant la CI. Co-authored-by: Cursor <cursoragent@cursor.com>
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
import { getSystemConfig } from '@/lib/config'
|
|
import { getChatProvider } from '@/lib/ai/factory'
|
|
|
|
export type FlashcardStyle = 'qa' | 'cloze' | 'concept'
|
|
|
|
export interface GeneratedFlashcard {
|
|
front: string
|
|
back: string
|
|
type: FlashcardStyle
|
|
}
|
|
|
|
const STYLE_HINTS: Record<FlashcardStyle, string> = {
|
|
qa: 'question/answer pairs — front is a clear question, back is a concise answer',
|
|
cloze: 'fill-in-the-blank — front uses ___ for the missing word(s), back is the complete sentence',
|
|
concept: 'term/definition — front is a term or concept name, back is its definition',
|
|
}
|
|
|
|
function parseFlashcardsJson(raw: string, style: FlashcardStyle): GeneratedFlashcard[] {
|
|
const trimmed = raw.trim()
|
|
const arrayMatch = trimmed.match(/\[[\s\S]*\]/)
|
|
if (!arrayMatch) return []
|
|
try {
|
|
const parsed = JSON.parse(arrayMatch[0]) as unknown
|
|
if (!Array.isArray(parsed)) return []
|
|
return parsed
|
|
.map((item) => {
|
|
if (!item || typeof item !== 'object') return null
|
|
const obj = item as Record<string, unknown>
|
|
const front = typeof obj.front === 'string' ? obj.front.trim() : ''
|
|
const back = typeof obj.back === 'string' ? obj.back.trim() : ''
|
|
const type = (typeof obj.type === 'string' ? obj.type : style) as FlashcardStyle
|
|
if (!front || !back) return null
|
|
return {
|
|
front: front.slice(0, 500),
|
|
back: back.slice(0, 800),
|
|
type: ['qa', 'cloze', 'concept'].includes(type) ? type : style,
|
|
}
|
|
})
|
|
.filter((c): c is GeneratedFlashcard => c !== null)
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export async function generateFlashcardsFromNote(params: {
|
|
title: string
|
|
textContent: string
|
|
count: number
|
|
style: FlashcardStyle
|
|
language?: string
|
|
}): Promise<GeneratedFlashcard[]> {
|
|
const count = Math.min(20, Math.max(5, params.count))
|
|
const excerpt = params.textContent.slice(0, 8000)
|
|
const lang = params.language && params.language !== 'auto' ? params.language : 'same as source'
|
|
|
|
const config = await getSystemConfig()
|
|
const provider = getChatProvider(config)
|
|
|
|
const prompt = `You create study flashcards from personal notes for spaced repetition.
|
|
|
|
Note title: ${params.title || 'Untitled'}
|
|
Language: ${lang}
|
|
Style: ${params.style} — ${STYLE_HINTS[params.style]}
|
|
|
|
Source content:
|
|
${excerpt}
|
|
|
|
Generate exactly ${count} flashcards. Use the same language as the source.
|
|
Respond with ONLY a JSON array (no markdown):
|
|
[
|
|
{ "front": "...", "back": "...", "type": "${params.style}" }
|
|
]
|
|
|
|
Rules:
|
|
- Each card tests one distinct fact from the note
|
|
- Front and back must be self-contained
|
|
- No duplicate cards
|
|
- Cloze cards must include ___ on the front`
|
|
|
|
const raw = await provider.generateText(prompt)
|
|
const cards = parseFlashcardsJson(raw, params.style)
|
|
if (cards.length === 0) {
|
|
throw new Error('Could not parse flashcards from AI response')
|
|
}
|
|
return cards.slice(0, count)
|
|
}
|