Files
Momento/memento-note/lib/clip/analyze-clip.ts
Antigravity e881004c77
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m7s
CI / Deploy production (on server) (push) Has been skipped
feat(insights): fix DBSCAN, Persian embeddings crash, D3 physics layouts, and D3 node not found runtime error
2026-05-24 18:57:33 +00:00

87 lines
2.6 KiB
TypeScript

import { getSystemConfig } from '@/lib/config'
import { getChatProvider } from '@/lib/ai/factory'
export interface ClipAnalysis {
title: string
summary: string
tags: string[]
readingTimeMinutes: number
}
function parseAnalysisJson(raw: string): ClipAnalysis | null {
const trimmed = raw.trim()
const jsonMatch = trimmed.match(/\{[\s\S]*\}/)
if (!jsonMatch) return null
try {
const parsed = JSON.parse(jsonMatch[0]) as Partial<ClipAnalysis>
const tags = Array.isArray(parsed.tags)
? parsed.tags.filter((t): t is string => typeof t === 'string').slice(0, 5)
: []
const readingTime = typeof parsed.readingTimeMinutes === 'number'
? Math.max(1, Math.min(120, Math.round(parsed.readingTimeMinutes)))
: 5
return {
title: typeof parsed.title === 'string' && parsed.title.trim() ? parsed.title.trim().slice(0, 200) : 'Web clip',
summary: typeof parsed.summary === 'string' ? parsed.summary.trim().slice(0, 800) : '',
tags,
readingTimeMinutes: readingTime,
}
} catch {
return null
}
}
function estimateReadingMinutes(text: string): number {
const words = text.split(/\s+/).filter(Boolean).length
return Math.max(1, Math.round(words / 200))
}
export async function analyzeClipContent(params: {
url: string
title: string
textContent: string
}): Promise<ClipAnalysis> {
const excerpt = params.textContent.slice(0, 6000)
const fallbackReading = estimateReadingMinutes(params.textContent)
try {
const config = await getSystemConfig()
const provider = getChatProvider(config)
const prompt = `You analyze web articles for a personal knowledge base. URL: ${params.url}
Page title: ${params.title}
Content excerpt:
${excerpt}
Respond with ONLY valid JSON (no markdown):
{
"title": "concise improved title",
"summary": "max 3 sentences in the same language as the content",
"tags": ["tag1", "tag2"],
"readingTimeMinutes": ${fallbackReading}
}
Rules: tags max 5, short lowercase labels, summary factual.`
const raw = await provider.generateText(prompt)
const parsed = parseAnalysisJson(raw)
if (parsed) {
if (!parsed.title) parsed.title = params.title || 'Web clip'
if (!parsed.summary && params.textContent) {
parsed.summary = params.textContent.slice(0, 400)
}
if (parsed.tags.length === 0) parsed.tags = []
return parsed
}
} catch (error) {
console.error('[ClipAnalyze] AI failed:', error)
}
return {
title: params.title || 'Web clip',
summary: params.textContent.slice(0, 400),
tags: [],
readingTimeMinutes: fallbackReading,
}
}