87 lines
2.6 KiB
TypeScript
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,
|
|
}
|
|
}
|