sync: multiple features from private repo
- quick-add-loading: fire-and-forget with loading placeholders, auto-open feed, SSE events, QuickAddInput redesign - database-table-pane: TablePane + DatabaseTableView, countNodes(), event_date sort - ui-polish-fixes: focus tab order, dim name editing, tab title sync, cost chip removal, custom sort drag-reorder, refresh button, dimension filter removal (~2,200 lines) - node-creation-quality: description service prompt rewrite, title sanitization, extraction tool AI prompt rewrites - feed-pane-ux: stripped kanban/grid/saved views, sort dropdown, AND dimension filtering, description preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9954792b1d
commit
2f6518207d
@@ -6,8 +6,10 @@ export const createNodeTool = tool({
|
||||
description: 'Create node with title/content/link and optional dimensions (locked dimensions auto-assigned)',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('The title of the node'),
|
||||
notes: z.string().optional().describe('The main notes, description, or notes for this node'),
|
||||
description: z.string().max(280).optional().describe('WHAT this is + WHY it matters. Extremely concise. No "discusses/explores". Auto-generated if omitted.'),
|
||||
notes: z.string().optional().describe('The main notes or content for this node'),
|
||||
link: z.string().optional().describe('A URL link to the source'),
|
||||
event_date: z.string().optional().describe('ISO date string for time-anchored nodes (e.g. meetings, events)'),
|
||||
dimensions: z
|
||||
.array(z.string())
|
||||
.max(5)
|
||||
|
||||
@@ -7,8 +7,10 @@ export const updateNodeTool = tool({
|
||||
id: z.number().describe('The ID of the node to update'),
|
||||
updates: z.object({
|
||||
title: z.string().optional().describe('New title'),
|
||||
notes: z.string().optional().describe('New content/description/notes'),
|
||||
description: z.string().max(280).optional().describe('New description (overwrites existing). WHAT this is + WHY it matters. No "discusses/explores".'),
|
||||
notes: z.string().optional().describe('New notes (appended to existing)'),
|
||||
link: z.string().optional().describe('New link'),
|
||||
event_date: z.string().optional().describe('ISO date string for time-anchored nodes'),
|
||||
dimensions: z.array(z.string()).optional().describe('New dimension tags - completely replaces existing dimensions'),
|
||||
chunk: z.string().optional().describe('New chunk content'),
|
||||
metadata: z.record(z.any()).optional().describe('New metadata - completely replaces existing metadata')
|
||||
@@ -79,7 +81,7 @@ export const updateNodeTool = tool({
|
||||
// Call the nodes API endpoint
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Notes-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
|
||||
@@ -8,43 +8,51 @@ import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
// AI-powered content analysis
|
||||
async function analyzeContentWithAI(title: string, description: string, contentType: string) {
|
||||
try {
|
||||
const prompt = `Analyze this ${contentType} content and provide classification:
|
||||
const prompt = `Analyze this ${contentType} content and provide classification.
|
||||
|
||||
Title: "${title}"
|
||||
Description: "${description}"
|
||||
|
||||
CRITICAL — nodeDescription rules (max 280 chars):
|
||||
1. Say WHAT this literally is: "Paper by…", "Research from…", "Preprint introducing…"
|
||||
2. Name the authors if known from the metadata.
|
||||
3. State the actual finding, method, or contribution — not "a study on X" but what they actually found or built.
|
||||
4. End with why it matters — one concrete phrase about impact or implication.
|
||||
5. ABSOLUTELY FORBIDDEN: "discusses", "explores", "examines", "talks about", "delves into", "emphasizing the need for". State things directly.
|
||||
|
||||
Examples:
|
||||
- Title: "Attention Is All You Need" / Authors: Vaswani et al.
|
||||
GOOD: "Vaswani et al. introduce the Transformer architecture — replaces recurrence with self-attention for sequence modeling. Foundation of every modern LLM."
|
||||
BAD: "This paper discusses a new architecture called the Transformer and explores its applications."
|
||||
|
||||
- Title: "Scaling Laws for Neural Language Models" / Authors: Kaplan et al.
|
||||
GOOD: "Kaplan et al. show that LLM performance scales as a power law with compute, data, and parameters — and compute matters most. The paper that launched the scaling era."
|
||||
BAD: "A study examining how neural language models scale with different factors."
|
||||
|
||||
Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
{
|
||||
"enhancedDescription": "A comprehensive summary of what this content is about (can be several paragraphs, up to ~1500 characters)",
|
||||
"tags": ["relevant", "semantic", "tags", "like", "ai", "economics", "research"],
|
||||
"reasoning": "Brief explanation of why you chose these categories"
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- enhancedDescription should be thorough - cover key points, arguments, and takeaways
|
||||
- Aim for 3-6 paragraphs or 800-1500 characters - don't artificially truncate
|
||||
- Include 3-8 relevant semantic tags (not just generic ones)
|
||||
- For academic papers, include tags like: research, academic, paper, plus domain-specific tags
|
||||
- For AI/ML papers, include: ai, machine-learning, artificial-intelligence, deep-learning
|
||||
- For economics papers, include: economics, finance, markets, policy
|
||||
- Be specific and insightful
|
||||
- Return ONLY the JSON object, no other text`;
|
||||
"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"],
|
||||
"reasoning": "Brief explanation of classification choices"
|
||||
}`;
|
||||
|
||||
const response = await generateText({
|
||||
model: openai('gpt-5-mini'),
|
||||
model: openai('gpt-4o-mini'),
|
||||
prompt,
|
||||
maxOutputTokens: 800
|
||||
});
|
||||
|
||||
let content = response.text || '{}';
|
||||
|
||||
|
||||
// Clean up the response - remove markdown code blocks if present
|
||||
content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||
|
||||
|
||||
const result = JSON.parse(content);
|
||||
|
||||
return {
|
||||
enhancedDescription: result.enhancedDescription || description,
|
||||
nodeDescription: typeof result.nodeDescription === 'string' ? result.nodeDescription.slice(0, 280) : undefined,
|
||||
tags: Array.isArray(result.tags) ? result.tags : [],
|
||||
reasoning: result.reasoning || 'AI analysis completed'
|
||||
};
|
||||
@@ -53,6 +61,7 @@ Guidelines:
|
||||
console.warn('Paper analysis fallback (using default description):', message);
|
||||
return {
|
||||
enhancedDescription: description,
|
||||
nodeDescription: undefined,
|
||||
tags: [],
|
||||
reasoning: 'Fallback description used'
|
||||
};
|
||||
@@ -143,6 +152,7 @@ export const paperExtractTool = tool({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: nodeTitle,
|
||||
description: aiAnalysis?.nodeDescription,
|
||||
notes: enhancedDescription,
|
||||
link: url,
|
||||
dimensions: trimmedDimensions,
|
||||
|
||||
@@ -8,42 +8,51 @@ import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
// AI-powered content analysis
|
||||
async function analyzeContentWithAI(title: string, description: string, contentType: string) {
|
||||
try {
|
||||
const prompt = `Analyze this ${contentType} content and provide classification:
|
||||
const prompt = `Analyze this ${contentType} content and provide classification.
|
||||
|
||||
Title: "${title}"
|
||||
Description: "${description}"
|
||||
|
||||
CRITICAL — nodeDescription rules (max 280 chars):
|
||||
1. Say WHAT this literally is: "Blog post by…", "Article from…", "Essay arguing…", "Tutorial on…", "Thread by…"
|
||||
2. Name the author/site if known from the metadata.
|
||||
3. State the actual claim or thesis — don't paraphrase into vague abstractions.
|
||||
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.
|
||||
|
||||
Examples:
|
||||
- Title: "Software is eating the world — again" / Author: Andrej Karpathy
|
||||
GOOD: "Karpathy's blog post arguing AI agents make software fluid — they can rip functionality from repos instead of taking dependencies. Signals the end of monolithic libraries."
|
||||
BAD: "By Karpathy — discusses the importance of software becoming more fluid and malleable with agents."
|
||||
|
||||
- Title: "The case for slowing down AI" / Site: The Atlantic
|
||||
GOOD: "Atlantic article making the case that AI labs should voluntarily slow capability research until safety catches up. Notable because it cites internal lab disagreements."
|
||||
BAD: "This article explores ideas about slowing down AI development and its implications."
|
||||
|
||||
Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
{
|
||||
"enhancedDescription": "A comprehensive summary of what this content is about (can be several paragraphs, up to ~1500 characters)",
|
||||
"tags": ["relevant", "semantic", "tags", "like", "ai", "economics", "research"],
|
||||
"reasoning": "Brief explanation of why you chose these categories"
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- enhancedDescription should be thorough - cover key points, arguments, and takeaways
|
||||
- Aim for 3-6 paragraphs or 800-1500 characters - don't artificially truncate
|
||||
- Include 3-8 relevant semantic tags (not just generic ones)
|
||||
- For AI/ML content, include tags like: ai, machine-learning, artificial-intelligence, deep-learning
|
||||
- For economics content, include: economics, finance, markets, policy
|
||||
- Be specific and insightful
|
||||
- Return ONLY the JSON object, no other text`;
|
||||
"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"],
|
||||
"reasoning": "Brief explanation of classification choices"
|
||||
}`;
|
||||
|
||||
const response = await generateText({
|
||||
model: openai('gpt-5-mini'),
|
||||
model: openai('gpt-4o-mini'),
|
||||
prompt,
|
||||
maxOutputTokens: 800
|
||||
});
|
||||
|
||||
let content = response.text || '{}';
|
||||
|
||||
|
||||
// Clean up the response - remove markdown code blocks if present
|
||||
content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||
|
||||
|
||||
const result = JSON.parse(content);
|
||||
|
||||
return {
|
||||
enhancedDescription: result.enhancedDescription || description,
|
||||
nodeDescription: typeof result.nodeDescription === 'string' ? result.nodeDescription.slice(0, 280) : undefined,
|
||||
tags: Array.isArray(result.tags) ? result.tags : [],
|
||||
reasoning: result.reasoning || 'AI analysis completed'
|
||||
};
|
||||
@@ -52,6 +61,7 @@ Guidelines:
|
||||
console.warn('Website analysis fallback (using default description):', message);
|
||||
return {
|
||||
enhancedDescription: description,
|
||||
nodeDescription: undefined,
|
||||
tags: [],
|
||||
reasoning: 'Fallback description used'
|
||||
};
|
||||
@@ -134,6 +144,7 @@ export const websiteExtractTool = tool({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: nodeTitle,
|
||||
description: aiAnalysis?.nodeDescription,
|
||||
notes: enhancedDescription,
|
||||
link: url,
|
||||
dimensions: trimmedDimensions,
|
||||
|
||||
@@ -5,61 +5,55 @@ import { generateText } from 'ai';
|
||||
import { extractYouTube } from '@/services/typescript/extractors/youtube';
|
||||
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
|
||||
// Available segments for categorization - match database constraint
|
||||
const AVAILABLE_SEGMENTS = [
|
||||
'inbox', 'parking', 'in progress', 'archive'
|
||||
];
|
||||
|
||||
// AI-powered content analysis
|
||||
async function analyzeContentWithAI(title: string, description: string, contentType: string) {
|
||||
try {
|
||||
const prompt = `Analyze this ${contentType} content and provide classification:
|
||||
const prompt = `Analyze this ${contentType} content and provide classification.
|
||||
|
||||
Title: "${title}"
|
||||
Description: "${description}"
|
||||
|
||||
Available types: ${AVAILABLE_SEGMENTS.join(', ')}
|
||||
CRITICAL — nodeDescription rules (max 280 chars):
|
||||
1. Say WHAT this literally is: "Podcast episode where…", "Talk by…", "Interview with…", "Video essay on…"
|
||||
2. Name people by their role: the channel/host is the creator, anyone in the title is likely the guest or subject.
|
||||
3. State the actual claim or thesis from the title — don't paraphrase into vague abstractions.
|
||||
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.
|
||||
|
||||
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."
|
||||
BAD: "By Dario Amodei — discusses reaching the limits of exponential growth in AI, emphasizing the need for a critical perspective."
|
||||
|
||||
- Title: "The spell of language models" / Channel: Andrej Karpathy
|
||||
GOOD: "Karpathy talk on how LLMs work under the hood — tokenization, attention, and why they feel like magic but aren't. Essential mental model for anyone building with LLMs."
|
||||
BAD: "By Andrej Karpathy — explores the nature of language models and their capabilities."
|
||||
|
||||
Respond with ONLY valid JSON (no markdown, no code blocks):
|
||||
{
|
||||
"enhancedDescription": "A comprehensive summary of what this content is about (can be several paragraphs, up to ~1500 characters)",
|
||||
"tags": ["relevant", "semantic", "tags", "like", "ai", "economics", "research"],
|
||||
"segments": ["research"],
|
||||
"reasoning": "Brief explanation of why you chose these categories"
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
- Choose 1 segment that best fits the content type
|
||||
- enhancedDescription should be thorough - cover key points, arguments, and takeaways
|
||||
- Aim for 3-6 paragraphs or 800-1500 characters - don't artificially truncate
|
||||
- Include 3-8 relevant semantic tags (not just generic ones)
|
||||
- For AI/ML content, include tags like: ai, machine-learning, artificial-intelligence, deep-learning
|
||||
- For economics content, include: economics, finance, markets, policy
|
||||
- Be specific and insightful
|
||||
- Return ONLY the JSON object, no other text`;
|
||||
"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"],
|
||||
"reasoning": "Brief explanation of classification choices"
|
||||
}`;
|
||||
|
||||
const response = await generateText({
|
||||
model: openai('gpt-5-mini'),
|
||||
model: openai('gpt-4o-mini'),
|
||||
prompt,
|
||||
maxOutputTokens: 800
|
||||
});
|
||||
|
||||
let content = response.text || '{}';
|
||||
|
||||
|
||||
// Clean up the response - remove markdown code blocks if present
|
||||
content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||
|
||||
|
||||
const result = JSON.parse(content);
|
||||
|
||||
// Validate segments are from available list
|
||||
const validSegments = result.segments?.filter((seg: string) =>
|
||||
AVAILABLE_SEGMENTS.includes(seg)
|
||||
) || [];
|
||||
|
||||
return {
|
||||
enhancedDescription: result.enhancedDescription || description,
|
||||
nodeDescription: typeof result.nodeDescription === 'string' ? result.nodeDescription.slice(0, 280) : undefined,
|
||||
tags: Array.isArray(result.tags) ? result.tags : [],
|
||||
segments: validSegments,
|
||||
reasoning: result.reasoning || 'AI analysis completed'
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -67,8 +61,8 @@ Guidelines:
|
||||
console.warn('YouTube analysis fallback (using default description):', message);
|
||||
return {
|
||||
enhancedDescription: description,
|
||||
nodeDescription: undefined,
|
||||
tags: [],
|
||||
segments: [],
|
||||
reasoning: 'Fallback description used'
|
||||
};
|
||||
}
|
||||
@@ -79,7 +73,7 @@ async function summariseTranscript(title: string, transcript: string): Promise<s
|
||||
return null;
|
||||
}
|
||||
|
||||
// Limit transcript length to keep token costs manageable (approx ≤4k tokens for gpt-4o-mini)
|
||||
// Limit transcript length to keep token costs manageable
|
||||
const MAX_CHARS = 16000;
|
||||
let excerpt = transcript.trim();
|
||||
if (excerpt.length > MAX_CHARS) {
|
||||
@@ -179,8 +173,8 @@ export const youtubeExtractTool = tool({
|
||||
// Step 3: Create node with extracted content and AI analysis
|
||||
const nodeTitle = title || result.metadata?.video_title || `YouTube Video ${url.split('/').pop()?.split('?')[0]}`;
|
||||
const transcriptSummary = await summariseTranscript(nodeTitle, result.chunk || result.notes || '');
|
||||
const content = transcriptSummary || aiAnalysis?.enhancedDescription || `YouTube video by ${result.metadata?.channel_name || 'Unknown Channel'}`;
|
||||
|
||||
const nodeNotes = transcriptSummary || aiAnalysis?.enhancedDescription || `YouTube video by ${result.metadata?.channel_name || 'Unknown Channel'}`;
|
||||
|
||||
const suppliedDimensions = Array.isArray(dimensions) ? dimensions : [];
|
||||
let trimmedDimensions = suppliedDimensions
|
||||
.map(dim => (typeof dim === 'string' ? dim.trim() : ''))
|
||||
@@ -193,7 +187,8 @@ export const youtubeExtractTool = tool({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: nodeTitle,
|
||||
content,
|
||||
description: aiAnalysis?.nodeDescription,
|
||||
notes: nodeNotes,
|
||||
link: url,
|
||||
dimensions: trimmedDimensions,
|
||||
chunk: result.chunk || result.notes,
|
||||
|
||||
Reference in New Issue
Block a user