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
@@ -51,16 +51,6 @@ function buildTaskPrompt(type: QuickAddInputType, input: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function buildExpectedOutcome(type: QuickAddInputType): string {
|
||||
if (type === 'note') {
|
||||
return 'Call createNode with the user input as content. Use a descriptive title extracted from the first few words. Set dimensions to an empty array. Return a brief confirmation like "Created note [NODE:id:title]"';
|
||||
}
|
||||
if (type === 'chat') {
|
||||
return 'Store the entire transcript in chunk_content, summarize the conversation into content, and return a confirmation like "Created chat transcript node [NODE:id:title]"';
|
||||
}
|
||||
return 'Use the appropriate extraction tool (youtubeExtract, websiteExtract, or paperExtract) to create the node. Return the standard extraction summary.';
|
||||
}
|
||||
|
||||
type ExtractionQuickAddType = Extract<QuickAddInputType, 'youtube' | 'website' | 'pdf'>;
|
||||
|
||||
const EXTRACTION_TOOL_MAP = {
|
||||
@@ -207,7 +197,7 @@ async function handleNoteQuickAdd(rawInput: string, task: string, userDescriptio
|
||||
const title = deriveNoteTitle(content);
|
||||
const nodePayload: Record<string, unknown> = {
|
||||
title,
|
||||
content,
|
||||
notes: content,
|
||||
metadata: {
|
||||
source: 'quick-add-note',
|
||||
refined_at: new Date().toISOString(),
|
||||
@@ -312,7 +302,7 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string): Pro
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
content,
|
||||
notes: content,
|
||||
chunk: transcript,
|
||||
metadata,
|
||||
}),
|
||||
@@ -341,41 +331,58 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string): Pro
|
||||
}
|
||||
|
||||
export interface QuickAddResult {
|
||||
success: boolean;
|
||||
id: string;
|
||||
task: string;
|
||||
inputType: QuickAddInputType;
|
||||
status: 'queued' | 'completed' | 'failed';
|
||||
summary?: string;
|
||||
error?: string;
|
||||
nodeId?: number;
|
||||
}
|
||||
|
||||
export async function enqueueQuickAdd({ rawInput, mode, description }: QuickAddInput): Promise<QuickAddResult> {
|
||||
const inputType = detectInputType(rawInput, mode);
|
||||
const task = buildTaskPrompt(inputType, rawInput);
|
||||
const id = `qa_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
try {
|
||||
let summary: string;
|
||||
if (inputType === 'note') {
|
||||
summary = await handleNoteQuickAdd(rawInput, task, description);
|
||||
} else if (inputType === 'chat') {
|
||||
summary = await handleChatTranscriptQuickAdd(rawInput, task);
|
||||
} else {
|
||||
summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task);
|
||||
}
|
||||
const result: QuickAddResult = {
|
||||
id,
|
||||
task,
|
||||
inputType,
|
||||
status: 'queued',
|
||||
};
|
||||
|
||||
// Extract node ID from summary if present
|
||||
const nodeIdMatch = summary.match(/\[NODE:(\d+)/);
|
||||
const nodeId = nodeIdMatch ? parseInt(nodeIdMatch[1], 10) : undefined;
|
||||
// Run async - fire and forget
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
let summary: string;
|
||||
if (inputType === 'note') {
|
||||
summary = await handleNoteQuickAdd(rawInput, task, description);
|
||||
} else if (inputType === 'chat') {
|
||||
summary = await handleChatTranscriptQuickAdd(rawInput, task);
|
||||
} else {
|
||||
summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task);
|
||||
}
|
||||
|
||||
// Broadcast node created event
|
||||
if (nodeId) {
|
||||
console.log(`[QuickAdd] Completed: ${task}`);
|
||||
// Broadcast completion so ThreePanelLayout can remove the pending placeholder
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'QUICK_ADD_COMPLETED',
|
||||
data: { quickAddId: id, source: 'quick-add' }
|
||||
});
|
||||
// Also broadcast NODE_CREATED to refresh the feed
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'NODE_CREATED',
|
||||
data: { node: { id: nodeId, title: 'Quick Add' } }
|
||||
data: { node: { title: task }, source: 'quick-add' }
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'unknown error';
|
||||
console.error(`[QuickAdd] Failed: ${task} - ${message}`);
|
||||
eventBroadcaster.broadcast({
|
||||
type: 'QUICK_ADD_FAILED',
|
||||
data: { quickAddId: id, error: message, source: 'quick-add' }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, summary, nodeId };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'unknown error';
|
||||
return { success: false, error: `Quick Add failed: ${message}` };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function generateFallbackDescription(input: DescriptionInput): string {
|
||||
return `${parts.join(' — ')}: ${title.slice(0, 200)}`;
|
||||
}
|
||||
|
||||
return `Knowledge item: ${title.slice(0, 250)}`;
|
||||
return title.slice(0, 280);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,20 +133,24 @@ function buildDescriptionPrompt(input: DescriptionInput): string {
|
||||
const contentPreview = input.notes?.slice(0, 800) || '';
|
||||
if (contentPreview) lines.push(`Notes: ${contentPreview}${input.notes && input.notes.length > 800 ? '...' : ''}`);
|
||||
|
||||
return `Your job is to answer: "what is this?" in one short line. Max 280 characters.
|
||||
return `Write a description for this knowledge node. Max 280 characters.
|
||||
|
||||
GOAL: Include "who created it" when possible.
|
||||
Say WHAT this literally is and WHY it matters. Be concrete and specific — like you're telling a friend what this thing is in one breath.
|
||||
|
||||
RULES (in priority order):
|
||||
1) ONLY use a creator if you have a creator hint (Author/Channel) or the content explicitly says "by <X>" / "hosted by <X>".
|
||||
- Do NOT treat prominent people in the title/transcript (e.g. a guest) as the creator.
|
||||
- If a creator hint is provided, prefer it.
|
||||
2) If you can identify a creator/author/channel/person/org from a creator hint, start with: "By <creator> — ..."
|
||||
3) If it's likely user-authored, start with: "Your <thing> — ..." (don't invent a creator name).
|
||||
4) If creator is unknown, do NOT guess; omit the byline.
|
||||
RULES:
|
||||
1) Name the format: "Podcast episode where…", "Blog post arguing…", "Your note on…", "Research paper showing…", "Idea that…"
|
||||
2) Name people by role — channel/host is the creator, title figures are guests/subjects. Use the Creator hint if available.
|
||||
3) State the actual claim, finding, or insight from the content — not a vague summary of the topic.
|
||||
4) End with why it's interesting or important — one concrete phrase.
|
||||
5) ABSOLUTELY FORBIDDEN — these words will be rejected: "discusses", "explores", "examines", "talks about", "is about", "delves into", "emphasizing the need for". State things directly instead.
|
||||
|
||||
Then, in the remainder, state what it is (video/paper/article/note/idea/etc) + what it's about (high-signal).
|
||||
If unsure, say so briefly.
|
||||
GOOD: "Karpathy blog post — AI agents make software fluid, ripping functionality from repos instead of taking dependencies. Signals the end of monolithic libraries."
|
||||
GOOD: "Dwarkesh Patel interview with Anthropic CEO Dario Amodei — argues we're nearing the end of exponential AI scaling. Key signal for what comes next."
|
||||
GOOD: "Your note — morning optimism consistently reverses to evening pessimism. Not energy — the belief itself flips. Pattern worth tracking."
|
||||
BAD: "By Dario Amodei — discusses reaching the limits of exponential growth in AI, emphasizing the need for a critical perspective on future advancements."
|
||||
BAD: "This article explores ideas about how software is changing."
|
||||
|
||||
Return ONLY the description text. Nothing else.
|
||||
|
||||
${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,46 @@ export class NodeService {
|
||||
return this.getNodesSQLite(filters);
|
||||
}
|
||||
|
||||
async countNodes(filters: NodeFilters = {}): Promise<number> {
|
||||
const { dimensions, search, dimensionsMatch = 'any',
|
||||
createdAfter, createdBefore, eventAfter, eventBefore } = filters;
|
||||
const sqlite = getSQLiteClient();
|
||||
|
||||
let query = `SELECT COUNT(*) as total FROM nodes n WHERE 1=1`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (dimensions && dimensions.length > 0) {
|
||||
if (dimensionsMatch === 'all' && dimensions.length > 1) {
|
||||
query += ` AND (
|
||||
SELECT COUNT(DISTINCT nd.dimension) FROM node_dimensions nd
|
||||
WHERE nd.node_id = n.id
|
||||
AND nd.dimension IN (${dimensions.map(() => '?').join(',')})
|
||||
) = ?`;
|
||||
params.push(...dimensions, dimensions.length);
|
||||
} else {
|
||||
query += ` AND EXISTS (
|
||||
SELECT 1 FROM node_dimensions nd
|
||||
WHERE nd.node_id = n.id
|
||||
AND nd.dimension IN (${dimensions.map(() => '?').join(',')})
|
||||
)`;
|
||||
params.push(...dimensions);
|
||||
}
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
|
||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
|
||||
if (createdAfter) { query += ` AND n.created_at >= ?`; params.push(createdAfter); }
|
||||
if (createdBefore) { query += ` AND n.created_at < ?`; params.push(createdBefore); }
|
||||
if (eventAfter) { query += ` AND n.event_date >= ?`; params.push(eventAfter); }
|
||||
if (eventBefore) { query += ` AND n.event_date < ?`; params.push(eventBefore); }
|
||||
|
||||
const result = sqlite.query<{ total: number }>(query, params);
|
||||
return result.rows[0]?.total ?? 0;
|
||||
}
|
||||
|
||||
// PostgreSQL path removed in SQLite-only consolidation
|
||||
|
||||
private async getNodesSQLite(filters: NodeFilters = {}): Promise<Node[]> {
|
||||
@@ -94,6 +134,8 @@ export class NodeService {
|
||||
query += ' ORDER BY edge_count DESC, n.updated_at DESC';
|
||||
} else if (sortBy === 'created') {
|
||||
query += ' ORDER BY n.created_at DESC';
|
||||
} else if (sortBy === 'event_date') {
|
||||
query += ' ORDER BY n.event_date IS NULL, n.event_date DESC, n.updated_at DESC';
|
||||
} else {
|
||||
query += ' ORDER BY n.updated_at DESC';
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface DatabaseEvent {
|
||||
| 'AGENT_DELEGATION_CREATED'
|
||||
| 'AGENT_DELEGATION_UPDATED'
|
||||
| 'GUIDE_UPDATED'
|
||||
| 'QUICK_ADD_COMPLETED'
|
||||
| 'QUICK_ADD_FAILED'
|
||||
| 'CONNECTION_ESTABLISHED';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any;
|
||||
|
||||
Reference in New Issue
Block a user