feat: publication IA (magazine/brief/essay) + fixes critique
Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { getChatProvider } from '../factory'
|
||||
import { getSystemConfig } from '@/lib/config'
|
||||
|
||||
export interface NoteAnalysisResult {
|
||||
noteId: string
|
||||
include: boolean
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface NotebookAnalysis {
|
||||
notes: NoteAnalysisResult[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export class NotebookPublishAnalyzerService {
|
||||
async analyze(
|
||||
notebookName: string,
|
||||
notes: Array<{ id: string; title: string; wordCount: number; preview: string }>,
|
||||
language: string,
|
||||
): Promise<NotebookAnalysis> {
|
||||
const lang = language || 'fr'
|
||||
const langName = lang === 'fr' ? 'français' : lang === 'en' ? 'English' : lang === 'fa' ? 'فارسی' : lang === 'ar' ? 'العربية' : lang === 'de' ? 'Deutsch' : lang === 'es' ? 'Español' : lang
|
||||
|
||||
const notesList = notes.map((n, i) =>
|
||||
`${i + 1}. [${n.id}] "${n.title || 'Sans titre'}" — ${n.wordCount} mots — "${n.preview}"`
|
||||
).join('\n')
|
||||
|
||||
const prompt = `Tu es un éditeur web expert. Analyse ce carnet de notes "${notebookName}" et décide quelles notes méritent d'être publiées sur un site web professionnel.
|
||||
|
||||
NOTES DU CARNET :
|
||||
${notesList}
|
||||
|
||||
CRITÈRES D'INCLUSION :
|
||||
- Note complète (>150 mots), structurée, sans fautes évidentes
|
||||
- Contenu utile pour un lecteur externe (pas juste des notes personnelles)
|
||||
- Titre clair et représentatif du contenu
|
||||
|
||||
CRITÈRES D'EXCLUSION :
|
||||
- Brouillon ou note vide (<50 mots)
|
||||
- Contenu incomplet ("à compléter", "TODO", "...")
|
||||
- Note purement personnelle ou de travail interne
|
||||
|
||||
Réponds en JSON strict :
|
||||
{
|
||||
"description": "Résumé du carnet en 1-2 phrases percutantes en ${langName}",
|
||||
"notes": [
|
||||
{ "noteId": "id_ici", "include": true, "reason": "Courte justification en ${langName}" }
|
||||
]
|
||||
}
|
||||
|
||||
Inclus toutes les notes dans le tableau "notes", même celles exclues.`
|
||||
|
||||
const config = await getSystemConfig()
|
||||
const provider = getChatProvider(config)
|
||||
const raw = await provider.generateText(prompt)
|
||||
|
||||
return this.parseResponse(raw, notes)
|
||||
}
|
||||
|
||||
private parseResponse(
|
||||
raw: string,
|
||||
notes: Array<{ id: string }>,
|
||||
): NotebookAnalysis {
|
||||
const jsonMatch = raw.match(/```json\s*([\s\S]+?)\s*```/)
|
||||
let jsonStr = jsonMatch ? jsonMatch[1] : raw
|
||||
const s = jsonStr.indexOf('{')
|
||||
const e = jsonStr.lastIndexOf('}')
|
||||
if (s >= 0 && e > s) jsonStr = jsonStr.slice(s, e + 1)
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const noteIds = new Set(notes.map(n => n.id))
|
||||
const analysisNotes: NoteAnalysisResult[] = (parsed.notes || [])
|
||||
.filter((n: any) => noteIds.has(n.noteId))
|
||||
.map((n: any) => ({
|
||||
noteId: String(n.noteId),
|
||||
include: Boolean(n.include),
|
||||
reason: String(n.reason || ''),
|
||||
}))
|
||||
|
||||
// Include any notes missing from AI response with include=true as default
|
||||
const analyzedIds = new Set(analysisNotes.map(n => n.noteId))
|
||||
for (const note of notes) {
|
||||
if (!analyzedIds.has(note.id)) {
|
||||
analysisNotes.push({ noteId: note.id, include: true, reason: '' })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notes: analysisNotes,
|
||||
description: String(parsed.description || ''),
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
notes: notes.map(n => ({ noteId: n.id, include: true, reason: '' })),
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const notebookPublishAnalyzerService = new NotebookPublishAnalyzerService()
|
||||
Reference in New Issue
Block a user