refactor: align source-first schema surfaces
This commit is contained in:
@@ -291,7 +291,7 @@ export default function FocusPanel({
|
||||
};
|
||||
|
||||
const startSourceEdit = () => {
|
||||
setSourceEditValue(currentNode?.source || currentNode?.chunk || '');
|
||||
setSourceEditValue(currentNode?.source || '');
|
||||
setSourceEditMode(true);
|
||||
};
|
||||
|
||||
@@ -598,7 +598,7 @@ export default function FocusPanel({
|
||||
};
|
||||
|
||||
const renderSourceSection = () => {
|
||||
const sourceContent = currentNode?.source || currentNode?.chunk || '';
|
||||
const sourceContent = currentNode?.source || '';
|
||||
|
||||
return (
|
||||
<section
|
||||
|
||||
@@ -235,7 +235,6 @@ export default function ThreePanelLayout() {
|
||||
title: node.title,
|
||||
link: node.link,
|
||||
source: node.source,
|
||||
notes: node.notes,
|
||||
dimensions: node.dimensions,
|
||||
created_at: node.created_at,
|
||||
updated_at: node.updated_at,
|
||||
|
||||
@@ -483,11 +483,11 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Notes */}
|
||||
{/* Source */}
|
||||
<td style={tdStyle()}>
|
||||
<div style={truncCell}>
|
||||
<span style={{ fontSize: '11px', color: 'var(--rah-text-soft)' }}>
|
||||
{(node.source || node.notes) ? (node.source || node.notes || '').slice(0, 120) : '\u2014'}
|
||||
{node.source ? node.source.slice(0, 120) : '\u2014'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -576,7 +576,7 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
|
||||
<td style={tdStyle()}>
|
||||
<div style={truncCell}>
|
||||
<span style={{ fontSize: '10px', color: 'var(--rah-text-muted)' }}>
|
||||
{(node.source || node.chunk) ? (node.source || node.chunk || '').slice(0, 100) : '\u2014'}
|
||||
{node.source ? node.source.slice(0, 100) : '\u2014'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
|
||||
</div>
|
||||
|
||||
{/* Description or Content Preview */}
|
||||
{(node.description || node.source || node.notes) && (
|
||||
{(node.description || node.source) && (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
fontSize: '11px',
|
||||
@@ -116,7 +116,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
|
||||
WebkitBoxOrient: 'vertical',
|
||||
marginBottom: '10px'
|
||||
}}>
|
||||
{node.description || truncateContent(node.source || node.notes)}
|
||||
{node.description || truncateContent(node.source)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
|
||||
</div>
|
||||
|
||||
{/* Description or Content Preview */}
|
||||
{(node.description || node.source || node.notes) && (
|
||||
{(node.description || node.source) && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
@@ -116,7 +116,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{node.description || truncateContent(node.source || node.notes)}
|
||||
{node.description || truncateContent(node.source)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { hasValidOpenAiKey } from '../storage/apiKeys';
|
||||
|
||||
export interface DescriptionInput {
|
||||
title: string;
|
||||
notes?: string;
|
||||
source?: string;
|
||||
link?: string;
|
||||
metadata?: {
|
||||
source?: string;
|
||||
@@ -61,7 +61,7 @@ export async function generateDescription(input: DescriptionInput): Promise<stri
|
||||
}
|
||||
|
||||
// Fast path: skip AI for very short inputs (likely just notes)
|
||||
if (!input.notes && !input.link && input.title.length < 30) {
|
||||
if (!input.source && !input.link && input.title.length < 30) {
|
||||
console.log(`[DescriptionService] Short input, using fallback for: "${input.title}"`);
|
||||
return generateFallbackDescription(input);
|
||||
}
|
||||
@@ -134,8 +134,8 @@ function buildDescriptionPrompt(input: DescriptionInput): string {
|
||||
if (publisherHint) lines.push(`Publisher hint: ${publisherHint}`);
|
||||
lines.push(`Likely user-authored: ${likelyUserAuthored ? 'yes' : 'no'}`);
|
||||
|
||||
const contentPreview = input.notes?.slice(0, 800) || '';
|
||||
if (contentPreview) lines.push(`Notes: ${contentPreview}${input.notes && input.notes.length > 800 ? '...' : ''}`);
|
||||
const contentPreview = input.source?.slice(0, 800) || '';
|
||||
if (contentPreview) lines.push(`Source excerpt: ${contentPreview}${input.source && input.source.length > 800 ? '...' : ''}`);
|
||||
|
||||
return `Write a description for this knowledge node. Max 280 characters.
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export class NodeService {
|
||||
|
||||
// Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance)
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
@@ -252,7 +252,7 @@ export class NodeService {
|
||||
private async getNodeByIdSQLite(id: number): Promise<Node | null> {
|
||||
const sqlite = getSQLiteClient();
|
||||
const query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
@@ -589,7 +589,7 @@ export class NodeService {
|
||||
WHERE nodes_fts MATCH ?
|
||||
LIMIT ?
|
||||
)
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
@@ -618,7 +618,7 @@ export class NodeService {
|
||||
const words = search.split(/\s+/).filter(Boolean);
|
||||
const { clauses, params } = this.buildNodeFilterClauses(filters);
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
@@ -663,7 +663,7 @@ export class NodeService {
|
||||
|
||||
const { clauses, params } = this.buildNodeFilterClauses(filters);
|
||||
let query = `
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
@@ -737,7 +737,7 @@ export class NodeService {
|
||||
ORDER BY distance
|
||||
LIMIT ?
|
||||
)
|
||||
SELECT n.id, n.title, n.description, n.source, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||
SELECT n.id, n.title, n.description, n.source, n.link, n.event_date, n.metadata,
|
||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||
n.created_at, n.updated_at,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
|
||||
@@ -616,19 +616,11 @@ class SQLiteClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 10) Final schema pass migrations (content→notes, event_date, dimensions.icon, drop dead columns)
|
||||
// 10) Final schema pass migrations (source canonicalization, event_date, dimensions.icon, drop dead columns)
|
||||
try {
|
||||
let nodeCols2 = this.db.prepare('PRAGMA table_info(nodes)').all() as Array<{ name: string }>;
|
||||
let nodeColNames = nodeCols2.map(c => c.name);
|
||||
|
||||
// Rename content → notes (additive first)
|
||||
if (nodeColNames.includes('content') && !nodeColNames.includes('notes')) {
|
||||
console.log('Migrating nodes.content → nodes.notes...');
|
||||
this.db.exec('ALTER TABLE nodes RENAME COLUMN content TO notes;');
|
||||
nodeCols2 = this.db.prepare('PRAGMA table_info(nodes)').all() as Array<{ name: string }>;
|
||||
nodeColNames = nodeCols2.map(c => c.name);
|
||||
}
|
||||
|
||||
if (!nodeColNames.includes('source')) {
|
||||
console.log('Adding nodes.source column...');
|
||||
this.db.exec('ALTER TABLE nodes ADD COLUMN source TEXT;');
|
||||
@@ -637,22 +629,38 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
if (nodeColNames.includes('source')) {
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
SET source = chunk
|
||||
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
|
||||
AND chunk IS NOT NULL
|
||||
AND LENGTH(TRIM(chunk)) > 0;
|
||||
`);
|
||||
if (nodeColNames.includes('content')) {
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
SET source = content,
|
||||
chunk_status = 'not_chunked'
|
||||
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
|
||||
AND content IS NOT NULL
|
||||
AND LENGTH(TRIM(content)) > 0;
|
||||
`);
|
||||
}
|
||||
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
SET source = notes,
|
||||
chunk_status = 'not_chunked'
|
||||
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
|
||||
AND notes IS NOT NULL
|
||||
AND LENGTH(TRIM(notes)) > 0;
|
||||
`);
|
||||
if (nodeColNames.includes('notes')) {
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
SET source = notes,
|
||||
chunk_status = 'not_chunked'
|
||||
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
|
||||
AND notes IS NOT NULL
|
||||
AND LENGTH(TRIM(notes)) > 0;
|
||||
`);
|
||||
}
|
||||
|
||||
if (nodeColNames.includes('chunk')) {
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
SET source = chunk,
|
||||
chunk_status = 'not_chunked'
|
||||
WHERE (source IS NULL OR LENGTH(TRIM(source)) = 0)
|
||||
AND chunk IS NOT NULL
|
||||
AND LENGTH(TRIM(chunk)) > 0;
|
||||
`);
|
||||
}
|
||||
|
||||
this.db.exec(`
|
||||
UPDATE nodes
|
||||
|
||||
@@ -19,7 +19,6 @@ interface NodeRecord {
|
||||
id: number;
|
||||
title: string;
|
||||
source: string | null;
|
||||
notes: string | null;
|
||||
description: string | null;
|
||||
dimensions_json: string;
|
||||
embedding?: Buffer | null;
|
||||
@@ -63,7 +62,7 @@ export class NodeEmbedder {
|
||||
const prompt = `Analyze this content and provide 2-3 key insights or themes in a concise paragraph (max 100 words):
|
||||
|
||||
Title: ${node.title}
|
||||
Source: ${node.source || node.notes || 'No source'}
|
||||
Source: ${node.source || 'No source'}
|
||||
Dimensions: ${dimensionsText}
|
||||
|
||||
Focus on the main concepts, key relationships, and practical implications.`;
|
||||
@@ -111,13 +110,13 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
// Create base embedding text
|
||||
let embeddingText = formatEmbeddingText(
|
||||
node.title,
|
||||
node.source || node.notes || '',
|
||||
node.source || '',
|
||||
dimensions,
|
||||
node.description
|
||||
);
|
||||
|
||||
// Add AI analysis if source exists
|
||||
const sourceText = node.source || node.notes || '';
|
||||
const sourceText = node.source || '';
|
||||
if (sourceText.trim().length > 0) {
|
||||
const analysis = await this.analyzeNodeWithAI(node);
|
||||
if (analysis) {
|
||||
@@ -178,7 +177,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
if (nodeId) {
|
||||
// Single node
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.notes, n.description,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
|
||||
n.embedding, n.embedding_updated_at
|
||||
@@ -189,7 +188,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
} else if (forceReEmbed) {
|
||||
// All nodes
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.notes, n.description,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
|
||||
n.embedding, n.embedding_updated_at
|
||||
@@ -199,7 +198,7 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
||||
} else {
|
||||
// Only nodes without embeddings
|
||||
query = `
|
||||
SELECT n.id, n.title, n.source, n.notes, n.description,
|
||||
SELECT n.id, n.title, n.source, n.description,
|
||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json,
|
||||
n.embedding, n.embedding_updated_at
|
||||
|
||||
@@ -48,8 +48,8 @@ export const queryDimensionNodesTool = tool({
|
||||
event_date: node.event_date ?? null,
|
||||
};
|
||||
|
||||
if (includeContent && (node.source || node.notes)) {
|
||||
const previewSource = node.source || node.notes || '';
|
||||
if (includeContent && node.source) {
|
||||
const previewSource = node.source;
|
||||
// Truncate to ~100 chars
|
||||
formatted.sourcePreview = previewSource.length > 100
|
||||
? previewSource.substring(0, 100) + '...'
|
||||
|
||||
Reference in New Issue
Block a user