feat: sync quick add ingestion updates
- simplify quick add source selection and fallback saving - improve description context and manual edit behavior - add extractor dimension filtering and edge explanation migration
This commit is contained in:
@@ -5,9 +5,69 @@ import { generateText } from 'ai';
|
||||
import { extractYouTube } from '@/services/typescript/extractors/youtube';
|
||||
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
|
||||
// AI-powered content analysis
|
||||
async function analyzeContentWithAI(title: string, description: string, contentType: string) {
|
||||
interface ExistingDimension {
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
async function fetchExistingDimensions(): Promise<ExistingDimension[]> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/dimensions/popular`);
|
||||
if (!response.ok) return [];
|
||||
|
||||
const result = await response.json();
|
||||
if (!Array.isArray(result.data)) return [];
|
||||
|
||||
return result.data
|
||||
.map((dimension: { dimension?: unknown; description?: unknown }) => ({
|
||||
name: typeof dimension.dimension === 'string' ? dimension.dimension.trim() : '',
|
||||
description: typeof dimension.description === 'string' ? dimension.description.trim() : null
|
||||
}))
|
||||
.filter((dimension: ExistingDimension) => dimension.name.length > 0);
|
||||
} catch (error) {
|
||||
console.warn('YouTube dimension fetch fallback (no dimension context):', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectExistingDimensions(
|
||||
selected: unknown,
|
||||
existingDimensions: ExistingDimension[],
|
||||
max = 5
|
||||
): string[] {
|
||||
if (!Array.isArray(selected) || existingDimensions.length === 0) return [];
|
||||
|
||||
const byLowerName = new Map(existingDimensions.map((dimension) => [dimension.name.toLowerCase(), dimension.name]));
|
||||
const normalized: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const value of selected) {
|
||||
if (typeof value !== 'string') continue;
|
||||
const matched = byLowerName.get(value.trim().toLowerCase());
|
||||
if (!matched) continue;
|
||||
const key = matched.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
normalized.push(matched);
|
||||
if (normalized.length >= max) break;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// AI-powered content analysis
|
||||
async function analyzeContentWithAI(
|
||||
title: string,
|
||||
description: string,
|
||||
contentType: string,
|
||||
existingDimensions: ExistingDimension[]
|
||||
) {
|
||||
try {
|
||||
const availableDimensionsBlock = existingDimensions.length > 0
|
||||
? existingDimensions
|
||||
.map((dimension) => `- ${dimension.name}${dimension.description ? `: ${dimension.description}` : ''}`)
|
||||
.join('\n')
|
||||
: '- No existing dimensions available. Return an empty dimensions array.';
|
||||
const prompt = `Analyze this ${contentType} content and provide classification.
|
||||
|
||||
Title: "${title}"
|
||||
@@ -20,6 +80,15 @@ CRITICAL — nodeDescription rules (max 280 chars):
|
||||
4. End with why it's interesting or important — one concrete phrase.
|
||||
5. ABSOLUTELY FORBIDDEN: "discusses", "explores", "examines", "talks about", "delves into", "emphasizing the need for". State things directly.
|
||||
|
||||
DIMENSION SELECTION (critical):
|
||||
You must select 0-3 dimensions from the list below.
|
||||
Do NOT invent new dimension names.
|
||||
Pick only dimensions that genuinely fit this content.
|
||||
If nothing fits, return an empty array.
|
||||
|
||||
Available dimensions:
|
||||
${availableDimensionsBlock}
|
||||
|
||||
Examples:
|
||||
- Title: "Dario Amodei — We are near the end of the exponential" / Channel: Dwarkesh Patel
|
||||
GOOD: "Dwarkesh Patel interview with Anthropic CEO Dario Amodei — argues we're nearing the end of exponential AI scaling. Key signal for what the next phase of AI development looks like."
|
||||
@@ -33,7 +102,7 @@ Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
{
|
||||
"enhancedDescription": "A comprehensive summary (3-6 paragraphs, 800-1500 chars). Cover key points, arguments, takeaways.",
|
||||
"nodeDescription": "<your 280-char description following the rules above>",
|
||||
"tags": ["relevant", "semantic", "tags"],
|
||||
"dimensions": ["existing-dimension-1", "existing-dimension-2"],
|
||||
"reasoning": "Brief explanation of classification choices"
|
||||
}`;
|
||||
|
||||
@@ -53,7 +122,7 @@ Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
return {
|
||||
enhancedDescription: result.enhancedDescription || description,
|
||||
nodeDescription: typeof result.nodeDescription === 'string' ? result.nodeDescription.slice(0, 280) : undefined,
|
||||
tags: Array.isArray(result.tags) ? result.tags : [],
|
||||
dimensions: selectExistingDimensions(result.dimensions, existingDimensions, 5),
|
||||
reasoning: result.reasoning || 'AI analysis completed'
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -62,7 +131,7 @@ Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
return {
|
||||
enhancedDescription: description,
|
||||
nodeDescription: undefined,
|
||||
tags: [],
|
||||
dimensions: [],
|
||||
reasoning: 'Fallback description used'
|
||||
};
|
||||
}
|
||||
@@ -164,10 +233,12 @@ export const youtubeExtractTool = tool({
|
||||
console.log('🎯 YouTube extraction successful, analyzing with AI...');
|
||||
|
||||
// Step 2: AI Analysis for enhanced metadata
|
||||
const existingDimensions = await fetchExistingDimensions();
|
||||
const aiAnalysis = await analyzeContentWithAI(
|
||||
result.metadata?.video_title || 'YouTube Video',
|
||||
`Video by ${result.metadata?.channel_name || 'Unknown Channel'}`,
|
||||
'youtube'
|
||||
'youtube',
|
||||
existingDimensions
|
||||
);
|
||||
|
||||
// Step 3: Create node with extracted content and AI analysis
|
||||
@@ -181,6 +252,9 @@ export const youtubeExtractTool = tool({
|
||||
.filter(Boolean);
|
||||
|
||||
trimmedDimensions = trimmedDimensions.slice(0, 5);
|
||||
const finalDimensions = trimmedDimensions.length > 0
|
||||
? trimmedDimensions
|
||||
: (aiAnalysis?.dimensions || []).slice(0, 5);
|
||||
|
||||
const createResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes`, {
|
||||
method: 'POST',
|
||||
@@ -190,7 +264,7 @@ export const youtubeExtractTool = tool({
|
||||
description: aiAnalysis?.nodeDescription,
|
||||
notes: nodeNotes,
|
||||
link: url,
|
||||
dimensions: trimmedDimensions,
|
||||
dimensions: finalDimensions,
|
||||
chunk: result.chunk || result.notes,
|
||||
metadata: {
|
||||
source: 'youtube',
|
||||
@@ -222,7 +296,7 @@ export const youtubeExtractTool = tool({
|
||||
console.log('🎯 YouTubeExtract completed successfully');
|
||||
|
||||
// Use actual assigned dimensions from API response (includes auto-assigned locked + keywords)
|
||||
const actualDimensions: string[] = createResult.data?.dimensions || trimmedDimensions || [];
|
||||
const actualDimensions: string[] = createResult.data?.dimensions || finalDimensions || [];
|
||||
const formattedNode = createResult.data?.id
|
||||
? formatNodeForChat({ id: createResult.data.id, title: nodeTitle, dimensions: actualDimensions })
|
||||
: nodeTitle;
|
||||
|
||||
Reference in New Issue
Block a user