feat: final schema pass — sync 9 schema changes from main repo
- Rename nodes.content → nodes.notes across all services, tools, API routes, and UI - Add dimensions.icon column support (GET/POST routes, types) - Add nodes.event_date column (create/update flows, types) - Drop nodes.type and nodes.is_pinned references - Drop edges.user_feedback references - Drop chat_memory_state table (replaced with DROP IF EXISTS in migration) - Update schema guide files (system/schema.md) - Add runtime migration block in sqlite-client.ts for existing databases - Fix MCP tool schemas (external API keeps 'content' param, maps to 'notes' internally) - Update standalone MCP server (nodeService.js, sqlite-client.js, index.js) - Update HTTP MCP server (server.js, stdio-server.js) - Update all extraction tools (paper, website, youtube) - Update all UI components (FocusPanel, ThreePanelLayout, GridView, ListView, FolderViewOverlay) - TypeScript: 0 errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1d323de11a
commit
86f43c9975
@@ -19,6 +19,7 @@ export async function GET() {
|
|||||||
SELECT
|
SELECT
|
||||||
d.name AS dimension,
|
d.name AS dimension,
|
||||||
d.description,
|
d.description,
|
||||||
|
d.icon,
|
||||||
d.is_priority AS isPriority,
|
d.is_priority AS isPriority,
|
||||||
COALESCE(dc.count, 0) AS count
|
COALESCE(dc.count, 0) AS count
|
||||||
FROM dimensions d
|
FROM dimensions d
|
||||||
@@ -31,6 +32,7 @@ export async function GET() {
|
|||||||
data: result.rows.map((row: any) => ({
|
data: result.rows.map((row: any) => ({
|
||||||
dimension: row.dimension,
|
dimension: row.dimension,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
|
icon: row.icon || null,
|
||||||
isPriority: Boolean(row.isPriority),
|
isPriority: Boolean(row.isPriority),
|
||||||
count: Number(row.count)
|
count: Number(row.count)
|
||||||
}))
|
}))
|
||||||
@@ -49,6 +51,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const rawName = typeof body?.name === 'string' ? body.name.trim() : '';
|
const rawName = typeof body?.name === 'string' ? body.name.trim() : '';
|
||||||
const description = typeof body?.description === 'string' ? body.description.trim() : null;
|
const description = typeof body?.description === 'string' ? body.description.trim() : null;
|
||||||
|
const icon = typeof body?.icon === 'string' ? body.icon.trim() : null;
|
||||||
const isPriority = typeof body?.isPriority === 'boolean' ? body.isPriority : false;
|
const isPriority = typeof body?.isPriority === 'boolean' ? body.isPriority : false;
|
||||||
|
|
||||||
if (!rawName) {
|
if (!rawName) {
|
||||||
@@ -67,14 +70,15 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const sqlite = getSQLiteClient();
|
const sqlite = getSQLiteClient();
|
||||||
const result = sqlite.query(`
|
const result = sqlite.query(`
|
||||||
INSERT INTO dimensions(name, description, is_priority, updated_at)
|
INSERT INTO dimensions(name, description, icon, is_priority, updated_at)
|
||||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
description = COALESCE(?, description),
|
description = COALESCE(?, description),
|
||||||
|
icon = COALESCE(?, icon),
|
||||||
is_priority = COALESCE(?, is_priority),
|
is_priority = COALESCE(?, is_priority),
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
RETURNING name, description, is_priority
|
RETURNING name, description, icon, is_priority
|
||||||
`, [rawName, description, isPriority ? 1 : 0, description, isPriority ? 1 : 0]);
|
`, [rawName, description, icon, isPriority ? 1 : 0, description, icon, isPriority ? 1 : 0]);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
throw new Error('Failed to create dimension');
|
throw new Error('Failed to create dimension');
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ function createRAHServer(): McpServer {
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title,
|
title: node.title,
|
||||||
description: node.description ?? null,
|
description: node.description ?? null,
|
||||||
content: node.content ?? null,
|
notes: node.notes ?? null,
|
||||||
link: node.link ?? null,
|
link: node.link ?? null,
|
||||||
dimensions: node.dimensions || [],
|
dimensions: node.dimensions || [],
|
||||||
metadata: node.metadata || {},
|
metadata: node.metadata || {},
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ export async function POST(
|
|||||||
// Generate new description using the description service
|
// Generate new description using the description service
|
||||||
const newDescription = await generateDescription({
|
const newDescription = await generateDescription({
|
||||||
title: node.title,
|
title: node.title,
|
||||||
content: node.content || undefined,
|
notes: node.notes || undefined,
|
||||||
link: node.link || undefined,
|
link: node.link || undefined,
|
||||||
metadata: enrichedMetadata,
|
metadata: enrichedMetadata,
|
||||||
type: enrichedMetadata?.type,
|
|
||||||
dimensions: node.dimensions || []
|
dimensions: node.dimensions || []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,16 +68,11 @@ export async function PUT(
|
|||||||
}, { status: 404 });
|
}, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body && Object.prototype.hasOwnProperty.call(body, 'is_pinned')) {
|
|
||||||
console.warn(`[nodes/${nodeId}] Ignoring legacy is_pinned payload`);
|
|
||||||
delete body.is_pinned;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updates: Record<string, unknown> = { ...body };
|
const updates: Record<string, unknown> = { ...body };
|
||||||
let shouldQueueEmbed = false;
|
let shouldQueueEmbed = false;
|
||||||
|
|
||||||
const incomingChunk = typeof body.chunk === 'string' ? body.chunk : undefined;
|
const incomingChunk = typeof body.chunk === 'string' ? body.chunk : undefined;
|
||||||
const incomingContent = typeof body.content === 'string' ? body.content : undefined;
|
const incomingNotes = typeof body.notes === 'string' ? body.notes : undefined;
|
||||||
const existingChunk = existingNode.chunk ?? '';
|
const existingChunk = existingNode.chunk ?? '';
|
||||||
|
|
||||||
if (incomingChunk !== undefined) {
|
if (incomingChunk !== undefined) {
|
||||||
@@ -92,8 +87,8 @@ export async function PUT(
|
|||||||
} else {
|
} else {
|
||||||
delete updates.chunk_status;
|
delete updates.chunk_status;
|
||||||
}
|
}
|
||||||
} else if (!existingChunk.trim() && hasSufficientContent(incomingContent)) {
|
} else if (!existingChunk.trim() && hasSufficientContent(incomingNotes)) {
|
||||||
updates.chunk = incomingContent;
|
updates.chunk = incomingNotes;
|
||||||
updates.chunk_status = 'not_chunked';
|
updates.chunk_status = 'not_chunked';
|
||||||
shouldQueueEmbed = true;
|
shouldQueueEmbed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawContent = typeof body.content === 'string' ? body.content : null;
|
const rawNotes = typeof body.notes === 'string' ? body.notes : null;
|
||||||
|
|
||||||
// Process provided dimensions first (needed for description generation)
|
// Process provided dimensions first (needed for description generation)
|
||||||
const providedDimensions = Array.isArray(body.dimensions) ? body.dimensions : [];
|
const providedDimensions = Array.isArray(body.dimensions) ? body.dimensions : [];
|
||||||
@@ -74,10 +74,9 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
nodeDescription = await generateDescription({
|
nodeDescription = await generateDescription({
|
||||||
title: body.title,
|
title: body.title,
|
||||||
content: rawContent || undefined,
|
notes: rawNotes || undefined,
|
||||||
link: body.link || undefined,
|
link: body.link || undefined,
|
||||||
metadata: body.metadata,
|
metadata: body.metadata,
|
||||||
type: body.type,
|
|
||||||
dimensions: trimmedProvidedDimensions
|
dimensions: trimmedProvidedDimensions
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,7 +87,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// Auto-assign locked dimensions + keyword dimensions for all new nodes
|
// Auto-assign locked dimensions + keyword dimensions for all new nodes
|
||||||
const { locked, keywords } = await DimensionService.assignDimensions({
|
const { locked, keywords } = await DimensionService.assignDimensions({
|
||||||
title: body.title,
|
title: body.title,
|
||||||
content: rawContent || undefined,
|
notes: rawNotes || undefined,
|
||||||
link: body.link,
|
link: body.link,
|
||||||
description: nodeDescription
|
description: nodeDescription
|
||||||
});
|
});
|
||||||
@@ -107,15 +106,15 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
if (chunkToStore && chunkToStore.trim().length > 0) {
|
if (chunkToStore && chunkToStore.trim().length > 0) {
|
||||||
chunkStatus = 'not_chunked';
|
chunkStatus = 'not_chunked';
|
||||||
} else if (!chunkToStore && hasSufficientContent(rawContent)) {
|
} else if (!chunkToStore && hasSufficientContent(rawNotes)) {
|
||||||
chunkToStore = rawContent;
|
chunkToStore = rawNotes;
|
||||||
chunkStatus = 'not_chunked';
|
chunkStatus = 'not_chunked';
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = await nodeService.createNode({
|
const node = await nodeService.createNode({
|
||||||
title: body.title,
|
title: body.title,
|
||||||
description: nodeDescription,
|
description: nodeDescription,
|
||||||
content: rawContent ?? undefined,
|
notes: rawNotes ?? undefined,
|
||||||
link: body.link,
|
link: body.link,
|
||||||
dimensions: finalDimensions,
|
dimensions: finalDimensions,
|
||||||
chunk: chunkToStore ?? undefined,
|
chunk: chunkToStore ?? undefined,
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ immutable: true
|
|||||||
| id | INTEGER | Primary key, auto-increment |
|
| id | INTEGER | Primary key, auto-increment |
|
||||||
| title | TEXT | Required |
|
| title | TEXT | Required |
|
||||||
| description | TEXT | AI-generated grounding context (~1 sentence) |
|
| description | TEXT | AI-generated grounding context (~1 sentence) |
|
||||||
| content | TEXT | User's notes/thoughts (not source content) |
|
| notes | TEXT | User's notes/thoughts (not source content) |
|
||||||
| chunk | TEXT | Full verbatim source content |
|
| chunk | TEXT | Full verbatim source content |
|
||||||
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
|
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
|
||||||
| link | TEXT | External URL (only for nodes representing external content) |
|
| link | TEXT | External URL (only for nodes representing external content) |
|
||||||
| type | TEXT | Nullable (reserved for future use) |
|
| event_date | TEXT | When the content happened (ISO date) — distinct from created_at |
|
||||||
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
|
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
|
||||||
| is_pinned | INTEGER | Legacy — use hub node queries instead |
|
|
||||||
| created_at | TEXT | ISO timestamp |
|
| created_at | TEXT | ISO timestamp |
|
||||||
| updated_at | TEXT | ISO timestamp |
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
@@ -40,6 +39,7 @@ immutable: true
|
|||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| name | TEXT | Primary key |
|
| name | TEXT | Primary key |
|
||||||
| description | TEXT | Purpose description |
|
| description | TEXT | Purpose description |
|
||||||
|
| icon | TEXT | Emoji or icon identifier for UI display |
|
||||||
| is_priority | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
|
| is_priority | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
|
||||||
| updated_at | TEXT | ISO timestamp |
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
@@ -60,9 +60,17 @@ immutable: true
|
|||||||
| embedding_type | TEXT | Embedding model used |
|
| embedding_type | TEXT | Embedding model used |
|
||||||
| metadata | TEXT | JSON blob |
|
| metadata | TEXT | JSON blob |
|
||||||
|
|
||||||
|
### voice_usage (daily tracking)
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| id | INTEGER | Primary key |
|
||||||
|
| date | TEXT | ISO date (UNIQUE) |
|
||||||
|
| minutes_used | REAL | Minutes consumed |
|
||||||
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
### FTS Tables
|
### FTS Tables
|
||||||
- `chunks_fts` — full-text search on chunk text
|
- `chunks_fts` — full-text search on chunk text
|
||||||
- `nodes_fts` — full-text search on node title + content
|
- `nodes_fts` — full-text search on node title + notes
|
||||||
|
|
||||||
## Common Query Patterns
|
## Common Query Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ async function main() {
|
|||||||
|
|
||||||
const node = nodeService.createNode({
|
const node = nodeService.createNode({
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
content: content?.trim(),
|
notes: content?.trim(),
|
||||||
link: link?.trim(),
|
link: link?.trim(),
|
||||||
description: description?.trim(),
|
description: description?.trim(),
|
||||||
dimensions: normalizedDimensions,
|
dimensions: normalizedDimensions,
|
||||||
@@ -359,7 +359,7 @@ async function main() {
|
|||||||
WITH fts_matches AS (
|
WITH fts_matches AS (
|
||||||
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT 100
|
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT 100
|
||||||
)
|
)
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
||||||
FROM fts_matches fm
|
FROM fts_matches fm
|
||||||
@@ -378,7 +378,7 @@ async function main() {
|
|||||||
WITH fts_matches AS (
|
WITH fts_matches AS (
|
||||||
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT ?
|
SELECT rowid, rank FROM nodes_fts WHERE nodes_fts MATCH ? LIMIT ?
|
||||||
)
|
)
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
||||||
FROM fts_matches fm
|
FROM fts_matches fm
|
||||||
@@ -392,7 +392,7 @@ async function main() {
|
|||||||
nodes = rows.map(row => ({
|
nodes = rows.map(row => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
content: row.content ?? null,
|
notes: row.notes ?? null,
|
||||||
description: row.description ?? null,
|
description: row.description ?? null,
|
||||||
link: row.link ?? null,
|
link: row.link ?? null,
|
||||||
dimensions: JSON.parse(row.dimensions_json || '[]'),
|
dimensions: JSON.parse(row.dimensions_json || '[]'),
|
||||||
@@ -410,7 +410,7 @@ async function main() {
|
|||||||
const words = trimmedQuery.split(/\s+/).filter(w => w.length > 0);
|
const words = trimmedQuery.split(/\s+/).filter(w => w.length > 0);
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.updated_at,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
||||||
FROM nodes n
|
FROM nodes n
|
||||||
@@ -419,7 +419,7 @@ async function main() {
|
|||||||
const params = [];
|
const params = [];
|
||||||
|
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.content LIKE ? COLLATE NOCASE)`;
|
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
|
||||||
params.push(`%${word}%`, `%${word}%`, `%${word}%`);
|
params.push(`%${word}%`, `%${word}%`, `%${word}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ async function main() {
|
|||||||
nodes = rows.map(row => ({
|
nodes = rows.map(row => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
title: row.title,
|
title: row.title,
|
||||||
content: row.content ?? null,
|
notes: row.notes ?? null,
|
||||||
description: row.description ?? null,
|
description: row.description ?? null,
|
||||||
link: row.link ?? null,
|
link: row.link ?? null,
|
||||||
dimensions: JSON.parse(row.dimensions_json || '[]'),
|
dimensions: JSON.parse(row.dimensions_json || '[]'),
|
||||||
@@ -485,7 +485,7 @@ async function main() {
|
|||||||
nodes.push({
|
nodes.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title,
|
title: node.title,
|
||||||
content: node.content ?? null,
|
notes: node.notes ?? null,
|
||||||
description: node.description ?? null,
|
description: node.description ?? null,
|
||||||
link: node.link ?? null,
|
link: node.link ?? null,
|
||||||
chunk: chunkTruncated ? rawChunk.substring(0, CHUNK_LIMIT) : rawChunk,
|
chunk: chunkTruncated ? rawChunk.substring(0, CHUNK_LIMIT) : rawChunk,
|
||||||
@@ -520,7 +520,13 @@ async function main() {
|
|||||||
throw new Error('At least one field must be provided in updates.');
|
throw new Error('At least one field must be provided in updates.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = nodeService.updateNode(id, updates, { appendContent: true });
|
// Map external 'content' param to internal 'notes'
|
||||||
|
const mappedUpdates = { ...updates };
|
||||||
|
if (mappedUpdates.content !== undefined) {
|
||||||
|
mappedUpdates.notes = mappedUpdates.content;
|
||||||
|
delete mappedUpdates.content;
|
||||||
|
}
|
||||||
|
const node = nodeService.updateNode(id, mappedUpdates, { appendNotes: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: `Updated node #${id}` }],
|
content: [{ type: 'text', text: `Updated node #${id}` }],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function getNodes(filters = {}) {
|
|||||||
const { dimensions, search, limit = 100, offset = 0 } = filters;
|
const { dimensions, search, limit = 100, offset = 0 } = filters;
|
||||||
|
|
||||||
let sql = `
|
let sql = `
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
||||||
@@ -30,7 +30,7 @@ function getNodes(filters = {}) {
|
|||||||
|
|
||||||
// Text search
|
// Text search
|
||||||
if (search) {
|
if (search) {
|
||||||
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.content LIKE ? COLLATE NOCASE)`;
|
sql += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
|
||||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ function getNodes(filters = {}) {
|
|||||||
*/
|
*/
|
||||||
function getNodeById(id) {
|
function getNodeById(id) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
|
||||||
@@ -97,7 +97,7 @@ function createNode(nodeData) {
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content,
|
notes,
|
||||||
link,
|
link,
|
||||||
type,
|
type,
|
||||||
dimensions = [],
|
dimensions = [],
|
||||||
@@ -110,7 +110,7 @@ function createNode(nodeData) {
|
|||||||
|
|
||||||
const nodeId = transaction(() => {
|
const nodeId = transaction(() => {
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO nodes (title, description, content, link, type, metadata, chunk, created_at, updated_at)
|
INSERT INTO nodes (title, description, notes, link, type, metadata, chunk, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -149,8 +149,8 @@ function createNode(nodeData) {
|
|||||||
* Note: content is APPENDED by default (MCP tool behavior), not replaced.
|
* Note: content is APPENDED by default (MCP tool behavior), not replaced.
|
||||||
*/
|
*/
|
||||||
function updateNode(id, updates, options = {}) {
|
function updateNode(id, updates, options = {}) {
|
||||||
const { appendContent = true } = options;
|
const { appendNotes = true } = options;
|
||||||
const { title, description, content, link, type, dimensions, chunk, metadata } = updates;
|
const { title, description, notes, link, type, dimensions, chunk, metadata } = updates;
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
|
|
||||||
@@ -173,10 +173,10 @@ function updateNode(id, updates, options = {}) {
|
|||||||
params.push(description);
|
params.push(description);
|
||||||
}
|
}
|
||||||
if (content !== undefined) {
|
if (content !== undefined) {
|
||||||
if (appendContent && existing.content) {
|
if (appendNotes && existing.notes) {
|
||||||
// Append to existing content
|
// Append to existing content
|
||||||
setFields.push('content = ?');
|
setFields.push('content = ?');
|
||||||
params.push(existing.content + '\n\n' + content);
|
params.push(existing.notes + '\n\n' + content);
|
||||||
} else {
|
} else {
|
||||||
setFields.push('content = ?');
|
setFields.push('content = ?');
|
||||||
params.push(content);
|
params.push(content);
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ function initDatabase() {
|
|||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
content TEXT,
|
notes TEXT,
|
||||||
link TEXT,
|
link TEXT,
|
||||||
type TEXT,
|
event_date TEXT,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
updated_at TEXT,
|
updated_at TEXT,
|
||||||
metadata TEXT,
|
metadata TEXT,
|
||||||
@@ -71,7 +71,7 @@ function initDatabase() {
|
|||||||
source TEXT,
|
source TEXT,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
context TEXT,
|
context TEXT,
|
||||||
user_feedback INTEGER,
|
|
||||||
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ mcpServer.registerTool(
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
content: content?.trim() || undefined,
|
notes: content?.trim() || undefined,
|
||||||
link: link?.trim() || undefined,
|
link: link?.trim() || undefined,
|
||||||
description: description?.trim() || undefined,
|
description: description?.trim() || undefined,
|
||||||
dimensions: normalizedDimensions,
|
dimensions: normalizedDimensions,
|
||||||
@@ -422,7 +422,7 @@ mcpServer.registerTool(
|
|||||||
nodes: nodes.map((node) => ({
|
nodes: nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title,
|
title: node.title,
|
||||||
content: node.content ?? null,
|
notes: node.notes ?? null,
|
||||||
description: node.description ?? null,
|
description: node.description ?? null,
|
||||||
link: node.link ?? null,
|
link: node.link ?? null,
|
||||||
dimensions: node.dimensions || [],
|
dimensions: node.dimensions || [],
|
||||||
@@ -718,7 +718,7 @@ mcpServer.registerTool(
|
|||||||
results: results.map(r => ({
|
results: results.map(r => ({
|
||||||
nodeId: r.node_id || r.nodeId || r.id,
|
nodeId: r.node_id || r.nodeId || r.id,
|
||||||
title: r.title || 'Untitled',
|
title: r.title || 'Untitled',
|
||||||
chunkPreview: (r.chunk || r.content || '').slice(0, 200),
|
chunkPreview: (r.chunk || r.notes || '').slice(0, 200),
|
||||||
similarity: r.similarity || r.score || 0
|
similarity: r.similarity || r.score || 0
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ server.registerTool(
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
content: content?.trim() || undefined,
|
notes: content?.trim() || undefined,
|
||||||
link: link?.trim() || undefined,
|
link: link?.trim() || undefined,
|
||||||
description: description?.trim() || undefined,
|
description: description?.trim() || undefined,
|
||||||
dimensions: normalizedDimensions,
|
dimensions: normalizedDimensions,
|
||||||
@@ -351,7 +351,7 @@ server.registerTool(
|
|||||||
nodes: nodes.map((node) => ({
|
nodes: nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title,
|
title: node.title,
|
||||||
content: node.content ?? null,
|
notes: node.notes ?? null,
|
||||||
description: node.description ?? null,
|
description: node.description ?? null,
|
||||||
link: node.link ?? null,
|
link: node.link ?? null,
|
||||||
dimensions: node.dimensions || [],
|
dimensions: node.dimensions || [],
|
||||||
@@ -647,7 +647,7 @@ server.registerTool(
|
|||||||
results: results.map(r => ({
|
results: results.map(r => ({
|
||||||
nodeId: r.node_id || r.nodeId || r.id,
|
nodeId: r.node_id || r.nodeId || r.id,
|
||||||
title: r.title || 'Untitled',
|
title: r.title || 'Untitled',
|
||||||
chunkPreview: (r.chunk || r.content || '').slice(0, 200),
|
chunkPreview: (r.chunk || r.notes || '').slice(0, 200),
|
||||||
similarity: r.similarity || r.score || 0
|
similarity: r.similarity || r.score || 0
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ CREATE TABLE nodes (
|
|||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
content TEXT,
|
notes TEXT,
|
||||||
link TEXT,
|
link TEXT,
|
||||||
type TEXT,
|
event_date TEXT,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
updated_at TEXT,
|
updated_at TEXT,
|
||||||
metadata TEXT,
|
metadata TEXT,
|
||||||
@@ -159,7 +159,6 @@ CREATE TABLE edges (
|
|||||||
source TEXT,
|
source TEXT,
|
||||||
created_at TEXT,
|
created_at TEXT,
|
||||||
context TEXT,
|
context TEXT,
|
||||||
user_feedback INTEGER,
|
|
||||||
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -252,17 +251,8 @@ if has_table chats && ! has_col chats delegation_id; then
|
|||||||
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chats ADD COLUMN delegation_id INTEGER;"
|
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE chats ADD COLUMN delegation_id INTEGER;"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! has_table chat_memory_state; then
|
# chat_memory_state removed in final schema pass
|
||||||
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
|
"$SQLITE_BIN" "$DB_PATH" "DROP TABLE IF EXISTS chat_memory_state;"
|
||||||
CREATE TABLE chat_memory_state (
|
|
||||||
thread_id TEXT PRIMARY KEY,
|
|
||||||
helper_name TEXT,
|
|
||||||
last_processed_chat_id INTEGER DEFAULT 0,
|
|
||||||
last_processed_at TEXT
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_memory_thread ON chat_memory_state(thread_id);
|
|
||||||
SQL
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Dropping legacy agent_delegations table if present..."
|
echo "Dropping legacy agent_delegations table if present..."
|
||||||
"$SQLITE_BIN" "$DB_PATH" "DROP TABLE IF EXISTS agent_delegations;"
|
"$SQLITE_BIN" "$DB_PATH" "DROP TABLE IF EXISTS agent_delegations;"
|
||||||
@@ -283,10 +273,6 @@ fi
|
|||||||
echo "Checking/adding missing columns..."
|
echo "Checking/adding missing columns..."
|
||||||
|
|
||||||
if has_table nodes; then
|
if has_table nodes; then
|
||||||
if ! has_col nodes type; then
|
|
||||||
echo "Adding nodes.type"
|
|
||||||
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN type TEXT;"
|
|
||||||
fi
|
|
||||||
if ! has_col nodes description; then
|
if ! has_col nodes description; then
|
||||||
echo "Adding nodes.description"
|
echo "Adding nodes.description"
|
||||||
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN description TEXT;"
|
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN description TEXT;"
|
||||||
@@ -299,10 +285,7 @@ if has_table nodes; then
|
|||||||
echo "Adding nodes.chunk"
|
echo "Adding nodes.chunk"
|
||||||
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN chunk TEXT;"
|
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN chunk TEXT;"
|
||||||
fi
|
fi
|
||||||
if ! has_col nodes is_pinned; then
|
# is_pinned removed in final schema pass
|
||||||
echo "Adding nodes.is_pinned"
|
|
||||||
"$SQLITE_BIN" "$DB_PATH" "ALTER TABLE nodes ADD COLUMN is_pinned INTEGER DEFAULT 0;"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if has_table chunks; then
|
if has_table chunks; then
|
||||||
@@ -321,6 +304,8 @@ if ! has_table dimensions; then
|
|||||||
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
|
"$SQLITE_BIN" "$DB_PATH" <<'SQL'
|
||||||
CREATE TABLE dimensions (
|
CREATE TABLE dimensions (
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
|
description TEXT,
|
||||||
|
icon TEXT,
|
||||||
is_priority INTEGER DEFAULT 0,
|
is_priority INTEGER DEFAULT 0,
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -345,9 +330,9 @@ CREATE VIEW nodes_v AS
|
|||||||
SELECT n.id,
|
SELECT n.id,
|
||||||
n.title,
|
n.title,
|
||||||
n.description,
|
n.description,
|
||||||
n.content,
|
n.notes,
|
||||||
n.link,
|
n.link,
|
||||||
n.type,
|
n.event_date,
|
||||||
n.metadata,
|
n.metadata,
|
||||||
n.created_at,
|
n.created_at,
|
||||||
n.updated_at,
|
n.updated_at,
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
try {
|
try {
|
||||||
const updateData: Record<string, string> = {};
|
const updateData: Record<string, string> = {};
|
||||||
|
|
||||||
if (editingField === 'content') {
|
if (editingField === 'notes') {
|
||||||
updateData.content = editingValue;
|
updateData.content = editingValue;
|
||||||
} else {
|
} else {
|
||||||
updateData[editingField] = editingValue;
|
updateData[editingField] = editingValue;
|
||||||
@@ -406,7 +406,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Safety net: ensure edges exist for any tokens present in saved content
|
// Safety net: ensure edges exist for any tokens present in saved content
|
||||||
if (editingField === 'content' && typeof editingValue === 'string') {
|
if (editingField === 'notes' && typeof editingValue === 'string') {
|
||||||
try {
|
try {
|
||||||
const tokens = parseNodeMarkers(editingValue);
|
const tokens = parseNodeMarkers(editingValue);
|
||||||
const uniqueTargets = Array.from(new Set(tokens.map(t => t.id))).filter(id => id !== nodeId);
|
const uniqueTargets = Array.from(new Set(tokens.map(t => t.id))).filter(id => id !== nodeId);
|
||||||
@@ -448,7 +448,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
const response = await fetch(`/api/nodes/${nodeId}`, {
|
const response = await fetch(`/api/nodes/${nodeId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content: contentValue }),
|
body: JSON.stringify({ notes: contentValue }),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to save');
|
if (!response.ok) throw new Error('Failed to save');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -568,7 +568,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
const response = await fetch(`/api/nodes/${activeTab}`, {
|
const response = await fetch(`/api/nodes/${activeTab}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content: notesEditValue }),
|
body: JSON.stringify({ notes: notesEditValue }),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to save');
|
if (!response.ok) throw new Error('Failed to save');
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -614,7 +614,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
// Start editing notes
|
// Start editing notes
|
||||||
const startNotesEdit = () => {
|
const startNotesEdit = () => {
|
||||||
if (!activeTab || !nodesData[activeTab]) return;
|
if (!activeTab || !nodesData[activeTab]) return;
|
||||||
setNotesEditValue(nodesData[activeTab].content || '');
|
setNotesEditValue(nodesData[activeTab].notes || '');
|
||||||
setNotesEditMode(true);
|
setNotesEditMode(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1023,15 +1023,15 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
|
|
||||||
const embedContent = async (nodeId: number) => {
|
const embedContent = async (nodeId: number) => {
|
||||||
const node = nodesData[nodeId];
|
const node = nodesData[nodeId];
|
||||||
const hasContent = node?.content?.trim();
|
const hasNotes = node?.notes?.trim();
|
||||||
const hasChunk = node?.chunk?.trim();
|
const hasChunk = node?.chunk?.trim();
|
||||||
// If chunk is empty but content exists, auto-populate chunk from content
|
// If chunk is empty but content exists, auto-populate chunk from content
|
||||||
if (!hasChunk && hasContent) {
|
if (!hasChunk && hasNotes) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/nodes/${nodeId}`, {
|
const response = await fetch(`/api/nodes/${nodeId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ chunk: hasContent })
|
body: JSON.stringify({ chunk: hasNotes })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -1046,7 +1046,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If neither content nor chunk exist, require content
|
// If neither content nor chunk exist, require content
|
||||||
if (!hasContent && !hasChunk) {
|
if (!hasNotes && !hasChunk) {
|
||||||
startEdit('content', '');
|
startEdit('content', '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2722,7 +2722,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : nodesData[activeTab]?.content ? (
|
) : nodesData[activeTab]?.notes ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: '#e5e5e5',
|
color: '#e5e5e5',
|
||||||
@@ -2734,7 +2734,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MarkdownWithNodeTokens
|
<MarkdownWithNodeTokens
|
||||||
content={nodesData[activeTab].content}
|
content={nodesData[activeTab].notes || ''}
|
||||||
onNodeClick={onNodeClick || onTabSelect}
|
onNodeClick={onNodeClick || onTabSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export default function ThreePanelLayout() {
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
title: node.title,
|
title: node.title,
|
||||||
link: node.link,
|
link: node.link,
|
||||||
content: node.content,
|
notes: node.notes,
|
||||||
dimensions: node.dimensions,
|
dimensions: node.dimensions,
|
||||||
created_at: node.created_at,
|
created_at: node.created_at,
|
||||||
updated_at: node.updated_at,
|
updated_at: node.updated_at,
|
||||||
|
|||||||
@@ -1240,7 +1240,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{node.content && (
|
{node.notes && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
@@ -1251,7 +1251,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
fontWeight: 400
|
fontWeight: 400
|
||||||
}}>
|
}}>
|
||||||
{getContentPreview(node.content)}
|
{getContentPreview(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{node.dimensions && node.dimensions.length > 0 && (
|
{node.dimensions && node.dimensions.length > 0 && (
|
||||||
@@ -1376,7 +1376,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
{node.title || 'Untitled'}
|
{node.title || 'Untitled'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{node.content && (
|
{node.notes && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
@@ -1387,7 +1387,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{getContentPreview(node.content)}
|
{getContentPreview(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1846,7 +1846,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
{node.title || 'Untitled'}
|
{node.title || 'Untitled'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{node.content && (
|
{node.notes && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
@@ -1857,7 +1857,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{getContentPreview(node.content)}
|
{getContentPreview(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2862,7 +2862,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
{node.title || 'Untitled'}
|
{node.title || 'Untitled'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{node.content && (
|
{node.notes && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
color: '#666',
|
color: '#666',
|
||||||
@@ -2872,7 +2872,7 @@ export default function FolderViewOverlay({ onClose, onNodeOpen, refreshToken, o
|
|||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: 'vertical'
|
WebkitBoxOrient: 'vertical'
|
||||||
}}>
|
}}>
|
||||||
{getContentPreview(node.content)}
|
{getContentPreview(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{node.dimensions && node.dimensions.length > 1 && (
|
{node.dimensions && node.dimensions.length > 1 && (
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description or Content Preview */}
|
{/* Description or Content Preview */}
|
||||||
{(node.description || node.content) && (
|
{(node.description || node.notes) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
@@ -116,7 +116,7 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
|
|||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
marginBottom: '10px'
|
marginBottom: '10px'
|
||||||
}}>
|
}}>
|
||||||
{node.description || truncateContent(node.content)}
|
{node.description || truncateContent(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description or Content Preview */}
|
{/* Description or Content Preview */}
|
||||||
{(node.description || node.content) && (
|
{(node.description || node.notes) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#666',
|
color: '#666',
|
||||||
@@ -116,7 +116,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
|
|||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{node.description || truncateContent(node.content)}
|
{node.description || truncateContent(node.notes)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ immutable: true
|
|||||||
| id | INTEGER | Primary key, auto-increment |
|
| id | INTEGER | Primary key, auto-increment |
|
||||||
| title | TEXT | Required |
|
| title | TEXT | Required |
|
||||||
| description | TEXT | AI-generated grounding context (~1 sentence) |
|
| description | TEXT | AI-generated grounding context (~1 sentence) |
|
||||||
| content | TEXT | User's notes/thoughts (not source content) |
|
| notes | TEXT | User's notes/thoughts (not source content) |
|
||||||
| chunk | TEXT | Full verbatim source content |
|
| chunk | TEXT | Full verbatim source content |
|
||||||
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
|
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
|
||||||
| link | TEXT | External URL (only for nodes representing external content) |
|
| link | TEXT | External URL (only for nodes representing external content) |
|
||||||
| type | TEXT | Nullable (reserved for future use) |
|
| event_date | TEXT | When the content happened (ISO date) — distinct from created_at |
|
||||||
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
|
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
|
||||||
| is_pinned | INTEGER | Legacy — use hub node queries instead |
|
|
||||||
| created_at | TEXT | ISO timestamp |
|
| created_at | TEXT | ISO timestamp |
|
||||||
| updated_at | TEXT | ISO timestamp |
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
@@ -38,29 +37,40 @@ immutable: true
|
|||||||
### dimensions
|
### dimensions
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| id | INTEGER | Primary key |
|
| name | TEXT | Primary key |
|
||||||
| name | TEXT | Unique, case-insensitive |
|
|
||||||
| description | TEXT | Purpose description |
|
| description | TEXT | Purpose description |
|
||||||
| is_locked | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
|
| icon | TEXT | Emoji or icon identifier for UI display |
|
||||||
|
| is_priority | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
|
||||||
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
### node_dimensions (junction)
|
### node_dimensions (junction)
|
||||||
| Column | Type |
|
| Column | Type |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| node_id | INTEGER FK → nodes.id |
|
| node_id | INTEGER FK → nodes.id |
|
||||||
| dimension_id | INTEGER FK → dimensions.id |
|
| dimension | TEXT (dimension name) |
|
||||||
|
|
||||||
### chunks (for semantic search)
|
### chunks (for semantic search)
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
| id | INTEGER | Primary key |
|
| id | INTEGER | Primary key |
|
||||||
| node_id | INTEGER | FK → nodes.id |
|
| node_id | INTEGER | FK → nodes.id |
|
||||||
| chunk_index | INTEGER | Position in sequence |
|
| chunk_idx | INTEGER | Position in sequence |
|
||||||
| text | TEXT | Chunk content |
|
| text | TEXT | Chunk content |
|
||||||
| embedding | BLOB | Vector (via sqlite-vec) |
|
| created_at | TEXT | ISO timestamp |
|
||||||
|
| embedding_type | TEXT | Embedding model used |
|
||||||
|
| metadata | TEXT | JSON blob |
|
||||||
|
|
||||||
|
### voice_usage (daily tracking)
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| id | INTEGER | Primary key |
|
||||||
|
| date | TEXT | ISO date (UNIQUE) |
|
||||||
|
| minutes_used | REAL | Minutes consumed |
|
||||||
|
| updated_at | TEXT | ISO timestamp |
|
||||||
|
|
||||||
### FTS Tables
|
### FTS Tables
|
||||||
- `chunks_fts` — full-text search on chunk text
|
- `chunks_fts` — full-text search on chunk text
|
||||||
- `nodes_fts` — full-text search on node title + content
|
- `nodes_fts` — full-text search on node title + notes
|
||||||
|
|
||||||
## Common Query Patterns
|
## Common Query Patterns
|
||||||
|
|
||||||
@@ -76,8 +86,7 @@ GROUP BY n.id ORDER BY edge_count DESC LIMIT 5
|
|||||||
```sql
|
```sql
|
||||||
SELECT n.* FROM nodes n
|
SELECT n.* FROM nodes n
|
||||||
JOIN node_dimensions nd ON n.id = nd.node_id
|
JOIN node_dimensions nd ON n.id = nd.node_id
|
||||||
JOIN dimensions d ON nd.dimension_id = d.id
|
WHERE nd.dimension = ?
|
||||||
WHERE d.name = ?
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Edges for a node (both directions):**
|
**Edges for a node (both directions):**
|
||||||
@@ -89,4 +98,14 @@ JOIN nodes n2 ON e.to_node_id = n2.id
|
|||||||
WHERE e.from_node_id = ? OR e.to_node_id = ?
|
WHERE e.from_node_id = ? OR e.to_node_id = ?
|
||||||
```
|
```
|
||||||
|
|
||||||
**Use sqliteQuery for any read operation not covered by structured tools.**
|
**Search source content (chunks):**
|
||||||
|
```sql
|
||||||
|
SELECT c.id, c.node_id, c.chunk_idx, c.text, n.title
|
||||||
|
FROM chunks c
|
||||||
|
JOIN nodes n ON c.node_id = n.id
|
||||||
|
WHERE c.text LIKE '%search term%' COLLATE NOCASE
|
||||||
|
ORDER BY c.chunk_idx ASC
|
||||||
|
LIMIT 10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use `rah_search_content` to search chunks by keyword, or `rah_sqlite_query` for any read operation not covered by structured tools.**
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function fetchAutoContextRows(limit: number): AutoContextSummary[] {
|
|||||||
FROM nodes n
|
FROM nodes n
|
||||||
LEFT JOIN edges e
|
LEFT JOIN edges e
|
||||||
ON (e.from_node_id = n.id OR e.to_node_id = n.id)
|
ON (e.from_node_id = n.id OR e.to_node_id = n.id)
|
||||||
WHERE n.type IS NULL OR n.type != 'memory'
|
WHERE 1=1
|
||||||
GROUP BY n.id
|
GROUP BY n.id
|
||||||
ORDER BY edge_count DESC, n.updated_at DESC
|
ORDER BY edge_count DESC, n.updated_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { hasValidOpenAiKey } from '../storage/apiKeys';
|
|||||||
|
|
||||||
export interface DescriptionInput {
|
export interface DescriptionInput {
|
||||||
title: string;
|
title: string;
|
||||||
content?: string;
|
notes?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
metadata?: {
|
metadata?: {
|
||||||
source?: string;
|
source?: string;
|
||||||
@@ -12,7 +12,6 @@ export interface DescriptionInput {
|
|||||||
author?: string;
|
author?: string;
|
||||||
site_name?: string;
|
site_name?: string;
|
||||||
};
|
};
|
||||||
type?: string;
|
|
||||||
dimensions?: string[];
|
dimensions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export { hasValidOpenAiKey } from '../storage/apiKeys';
|
|||||||
* Used when no API key is available or for simple inputs.
|
* Used when no API key is available or for simple inputs.
|
||||||
*/
|
*/
|
||||||
export function generateFallbackDescription(input: DescriptionInput): string {
|
export function generateFallbackDescription(input: DescriptionInput): string {
|
||||||
const { title, type, metadata, dimensions } = input;
|
const { title, metadata, dimensions } = input;
|
||||||
|
|
||||||
// Build a contextual fallback
|
// Build a contextual fallback
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@@ -33,10 +32,6 @@ export function generateFallbackDescription(input: DescriptionInput): string {
|
|||||||
parts.push(`By ${metadata.author || metadata.channel_name}`);
|
parts.push(`By ${metadata.author || metadata.channel_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type) {
|
|
||||||
parts.push(type.charAt(0).toUpperCase() + type.slice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dimensions?.length) {
|
if (dimensions?.length) {
|
||||||
parts.push(`in ${dimensions.slice(0, 2).join(', ')}`);
|
parts.push(`in ${dimensions.slice(0, 2).join(', ')}`);
|
||||||
}
|
}
|
||||||
@@ -63,7 +58,7 @@ export async function generateDescription(input: DescriptionInput): Promise<stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: skip AI for very short inputs (likely just notes)
|
// Fast path: skip AI for very short inputs (likely just notes)
|
||||||
if (!input.content && !input.link && input.title.length < 30) {
|
if (!input.notes && !input.link && input.title.length < 30) {
|
||||||
console.log(`[DescriptionService] Short input, using fallback for: "${input.title}"`);
|
console.log(`[DescriptionService] Short input, using fallback for: "${input.title}"`);
|
||||||
return generateFallbackDescription(input);
|
return generateFallbackDescription(input);
|
||||||
}
|
}
|
||||||
@@ -135,8 +130,8 @@ function buildDescriptionPrompt(input: DescriptionInput): string {
|
|||||||
if (publisherHint) lines.push(`Publisher hint: ${publisherHint}`);
|
if (publisherHint) lines.push(`Publisher hint: ${publisherHint}`);
|
||||||
lines.push(`Likely user-authored: ${likelyUserAuthored ? 'yes' : 'no'}`);
|
lines.push(`Likely user-authored: ${likelyUserAuthored ? 'yes' : 'no'}`);
|
||||||
|
|
||||||
const contentPreview = input.content?.slice(0, 800) || '';
|
const contentPreview = input.notes?.slice(0, 800) || '';
|
||||||
if (contentPreview) lines.push(`Content: ${contentPreview}${input.content && input.content.length > 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 `Your job is to answer: "what is this?" in one short line. Max 280 characters.
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class DimensionService {
|
|||||||
*/
|
*/
|
||||||
static async assignDimensions(nodeData: {
|
static async assignDimensions(nodeData: {
|
||||||
title: string;
|
title: string;
|
||||||
content?: string;
|
notes?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}): Promise<{ locked: string[]; keywords: string[] }> {
|
}): Promise<{ locked: string[]; keywords: string[] }> {
|
||||||
@@ -156,19 +156,19 @@ export class DimensionService {
|
|||||||
* Build AI prompt for dimension assignment (locked dimensions only)
|
* Build AI prompt for dimension assignment (locked dimensions only)
|
||||||
*/
|
*/
|
||||||
private static buildAssignmentPrompt(
|
private static buildAssignmentPrompt(
|
||||||
nodeData: { title: string; content?: string; link?: string; description?: string },
|
nodeData: { title: string; notes?: string; link?: string; description?: string },
|
||||||
lockedDimensions: LockedDimension[]
|
lockedDimensions: LockedDimension[]
|
||||||
): string {
|
): string {
|
||||||
// Use description as primary context, content as fallback
|
// Use description as primary context, content as fallback
|
||||||
let nodeContextSection: string;
|
let nodeContextSection: string;
|
||||||
if (nodeData.description) {
|
if (nodeData.description) {
|
||||||
const contentPreview = nodeData.content?.slice(0, 500) || '';
|
const contentPreview = nodeData.notes?.slice(0, 500) || '';
|
||||||
nodeContextSection = `DESCRIPTION: ${nodeData.description}
|
nodeContextSection = `DESCRIPTION: ${nodeData.description}
|
||||||
|
|
||||||
CONTENT PREVIEW: ${contentPreview}${nodeData.content && nodeData.content.length > 500 ? '...' : ''}`;
|
NOTES PREVIEW: ${contentPreview}${nodeData.notes && nodeData.notes.length > 500 ? '...' : ''}`;
|
||||||
} else {
|
} else {
|
||||||
const contentPreview = nodeData.content?.slice(0, 2000) || '';
|
const contentPreview = nodeData.notes?.slice(0, 2000) || '';
|
||||||
nodeContextSection = `CONTENT: ${contentPreview}${nodeData.content && nodeData.content.length > 2000 ? '...' : ''}`;
|
nodeContextSection = `NOTES: ${contentPreview}${nodeData.notes && nodeData.notes.length > 2000 ? '...' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include ALL locked dimensions, using fallback text for those without descriptions
|
// Include ALL locked dimensions, using fallback text for those without descriptions
|
||||||
|
|||||||
@@ -430,9 +430,9 @@ export class EdgeService {
|
|||||||
ELSE n_from.title
|
ELSE n_from.title
|
||||||
END as connected_node_title,
|
END as connected_node_title,
|
||||||
CASE
|
CASE
|
||||||
WHEN e.from_node_id = ? THEN n_to.content
|
WHEN e.from_node_id = ? THEN n_to.notes
|
||||||
ELSE n_from.content
|
ELSE n_from.notes
|
||||||
END as connected_node_content,
|
END as connected_node_notes,
|
||||||
CASE
|
CASE
|
||||||
WHEN e.from_node_id = ? THEN n_to.link
|
WHEN e.from_node_id = ? THEN n_to.link
|
||||||
ELSE n_from.link
|
ELSE n_from.link
|
||||||
@@ -499,7 +499,7 @@ export class EdgeService {
|
|||||||
const connected_node: Node = {
|
const connected_node: Node = {
|
||||||
id: row.connected_node_id,
|
id: row.connected_node_id,
|
||||||
title: row.connected_node_title,
|
title: row.connected_node_title,
|
||||||
content: row.connected_node_content,
|
notes: row.connected_node_notes,
|
||||||
link: row.connected_node_link,
|
link: row.connected_node_link,
|
||||||
dimensions: row.connected_node_dimensions,
|
dimensions: row.connected_node_dimensions,
|
||||||
embedding: undefined, // Not needed for display
|
embedding: undefined, // Not needed for display
|
||||||
@@ -544,7 +544,7 @@ export class EdgeService {
|
|||||||
const connected_node: Node = {
|
const connected_node: Node = {
|
||||||
id: row.connected_node_id,
|
id: row.connected_node_id,
|
||||||
title: row.connected_node_title,
|
title: row.connected_node_title,
|
||||||
content: row.connected_node_content,
|
notes: row.connected_node_notes,
|
||||||
link: row.connected_node_link,
|
link: row.connected_node_link,
|
||||||
dimensions: JSON.parse(row.connected_node_dimensions_json || '[]'),
|
dimensions: JSON.parse(row.connected_node_dimensions_json || '[]'),
|
||||||
embedding: undefined, // Not needed for display
|
embedding: undefined, // Not needed for display
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class NodeService {
|
|||||||
|
|
||||||
// Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance)
|
// Use nodes_v view for array-like dimensions behavior (exclude embedding BLOB for performance)
|
||||||
let query = `
|
let query = `
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
@@ -38,7 +38,7 @@ export class NodeService {
|
|||||||
|
|
||||||
// Text search in title, description, and content (SQLite LIKE with COLLATE NOCASE)
|
// Text search in title, description, and content (SQLite LIKE with COLLATE NOCASE)
|
||||||
if (search) {
|
if (search) {
|
||||||
query += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.content LIKE ? COLLATE NOCASE)`;
|
query += ` AND (n.title LIKE ? COLLATE NOCASE OR n.description LIKE ? COLLATE NOCASE OR n.notes LIKE ? COLLATE NOCASE)`;
|
||||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export class NodeService {
|
|||||||
CASE WHEN LOWER(n.title) LIKE LOWER(?) THEN 2 ELSE 6 END,
|
CASE WHEN LOWER(n.title) LIKE LOWER(?) THEN 2 ELSE 6 END,
|
||||||
CASE WHEN n.title LIKE ? COLLATE NOCASE THEN 3 ELSE 6 END,
|
CASE WHEN n.title LIKE ? COLLATE NOCASE THEN 3 ELSE 6 END,
|
||||||
CASE WHEN n.description LIKE ? COLLATE NOCASE THEN 4 ELSE 6 END,
|
CASE WHEN n.description LIKE ? COLLATE NOCASE THEN 4 ELSE 6 END,
|
||||||
CASE WHEN n.content LIKE ? COLLATE NOCASE THEN 5 ELSE 6 END,
|
CASE WHEN n.notes LIKE ? COLLATE NOCASE THEN 5 ELSE 6 END,
|
||||||
n.updated_at DESC`;
|
n.updated_at DESC`;
|
||||||
params.push(
|
params.push(
|
||||||
search, // Exact match (case-insensitive)
|
search, // Exact match (case-insensitive)
|
||||||
@@ -95,7 +95,7 @@ export class NodeService {
|
|||||||
private async getNodeByIdSQLite(id: number): Promise<Node | null> {
|
private async getNodeByIdSQLite(id: number): Promise<Node | null> {
|
||||||
const sqlite = getSQLiteClient();
|
const sqlite = getSQLiteClient();
|
||||||
const query = `
|
const query = `
|
||||||
SELECT n.id, n.title, n.description, n.content, n.link, n.type, n.metadata, n.chunk,
|
SELECT n.id, n.title, n.description, n.notes, n.link, n.event_date, n.metadata, n.chunk,
|
||||||
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
n.chunk_status, n.embedding_updated_at, n.embedding_text,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
|
||||||
@@ -125,9 +125,9 @@ export class NodeService {
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
content,
|
notes,
|
||||||
link,
|
link,
|
||||||
type,
|
event_date,
|
||||||
dimensions = [],
|
dimensions = [],
|
||||||
chunk,
|
chunk,
|
||||||
chunk_status,
|
chunk_status,
|
||||||
@@ -139,14 +139,14 @@ export class NodeService {
|
|||||||
const nodeId = sqlite.transaction(() => {
|
const nodeId = sqlite.transaction(() => {
|
||||||
// Insert node using prepare/run for lastInsertRowid access
|
// Insert node using prepare/run for lastInsertRowid access
|
||||||
const nodeResult = sqlite.prepare(`
|
const nodeResult = sqlite.prepare(`
|
||||||
INSERT INTO nodes (title, description, content, link, type, metadata, chunk, chunk_status, created_at, updated_at)
|
INSERT INTO nodes (title, description, notes, link, event_date, metadata, chunk, chunk_status, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(
|
`).run(
|
||||||
title,
|
title,
|
||||||
description ?? null,
|
description ?? null,
|
||||||
content ?? null,
|
notes ?? null,
|
||||||
link ?? null,
|
link ?? null,
|
||||||
type ?? null,
|
event_date ?? null,
|
||||||
JSON.stringify(metadata),
|
JSON.stringify(metadata),
|
||||||
chunk ?? null,
|
chunk ?? null,
|
||||||
chunk_status ?? null,
|
chunk_status ?? null,
|
||||||
@@ -192,7 +192,7 @@ export class NodeService {
|
|||||||
// PostgreSQL path removed in SQLite-only consolidation
|
// PostgreSQL path removed in SQLite-only consolidation
|
||||||
|
|
||||||
private async updateNodeSQLite(id: number, updates: Partial<Node>): Promise<Node> {
|
private async updateNodeSQLite(id: number, updates: Partial<Node>): Promise<Node> {
|
||||||
const { title, description, content, link, type, dimensions, chunk, metadata } = updates;
|
const { title, description, notes, link, event_date, dimensions, chunk, metadata } = updates;
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const sqlite = getSQLiteClient();
|
const sqlite = getSQLiteClient();
|
||||||
|
|
||||||
@@ -211,9 +211,9 @@ export class NodeService {
|
|||||||
|
|
||||||
if (title !== undefined) { setFields.push('title = ?'); params.push(title); }
|
if (title !== undefined) { setFields.push('title = ?'); params.push(title); }
|
||||||
if (description !== undefined) { setFields.push('description = ?'); params.push(description); }
|
if (description !== undefined) { setFields.push('description = ?'); params.push(description); }
|
||||||
if (content !== undefined) { setFields.push('content = ?'); params.push(content); }
|
if (notes !== undefined) { setFields.push('notes = ?'); params.push(notes); }
|
||||||
if (link !== undefined) { setFields.push('link = ?'); params.push(link); }
|
if (link !== undefined) { setFields.push('link = ?'); params.push(link); }
|
||||||
if (type !== undefined) { setFields.push('type = ?'); params.push(type); }
|
if (event_date !== undefined) { setFields.push('event_date = ?'); params.push(event_date); }
|
||||||
if (chunk !== undefined) { setFields.push('chunk = ?'); params.push(chunk); }
|
if (chunk !== undefined) { setFields.push('chunk = ?'); params.push(chunk); }
|
||||||
if (Object.prototype.hasOwnProperty.call(updates, 'chunk_status')) {
|
if (Object.prototype.hasOwnProperty.call(updates, 'chunk_status')) {
|
||||||
setFields.push('chunk_status = ?');
|
setFields.push('chunk_status = ?');
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ class SQLiteClient {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ensureNodeCol('description', "ALTER TABLE nodes ADD COLUMN description TEXT;");
|
ensureNodeCol('description', "ALTER TABLE nodes ADD COLUMN description TEXT;");
|
||||||
ensureNodeCol('type', "ALTER TABLE nodes ADD COLUMN type TEXT;");
|
// type column removed in final schema pass
|
||||||
} catch (nodeErr) {
|
} catch (nodeErr) {
|
||||||
console.warn('Failed to ensure nodes columns:', nodeErr);
|
console.warn('Failed to ensure nodes columns:', nodeErr);
|
||||||
}
|
}
|
||||||
@@ -435,16 +435,8 @@ class SQLiteClient {
|
|||||||
}
|
}
|
||||||
// Do not recreate memory_v; alias has been removed.
|
// Do not recreate memory_v; alias has been removed.
|
||||||
|
|
||||||
// 6) Chat memory state tracking for chat-triggered memories
|
// 6) Drop orphaned chat_memory_state table (removed in final schema pass)
|
||||||
this.db.exec(`
|
this.db.exec(`DROP TABLE IF EXISTS chat_memory_state;`);
|
||||||
CREATE TABLE IF NOT EXISTS chat_memory_state (
|
|
||||||
thread_id TEXT PRIMARY KEY,
|
|
||||||
helper_name TEXT,
|
|
||||||
last_processed_chat_id INTEGER DEFAULT 0,
|
|
||||||
last_processed_at TEXT
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_chat_memory_thread ON chat_memory_state(thread_id);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Agent delegation table for orchestrator/worker coordination
|
// Agent delegation table for orchestrator/worker coordination
|
||||||
try {
|
try {
|
||||||
@@ -631,6 +623,81 @@ class SQLiteClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10) Final schema pass migrations (content→notes, event_date, icon, drop dead columns)
|
||||||
|
try {
|
||||||
|
const nodeCols2 = this.db.prepare('PRAGMA table_info(nodes)').all() as Array<{ name: string }>;
|
||||||
|
const nodeColNames = nodeCols2.map((c: any) => c.name);
|
||||||
|
|
||||||
|
// Rename content → notes
|
||||||
|
if (nodeColNames.includes('content') && !nodeColNames.includes('notes')) {
|
||||||
|
console.log('Renaming nodes.content → nodes.notes');
|
||||||
|
this.db.exec('ALTER TABLE nodes RENAME COLUMN content TO notes;');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event_date with backfill from metadata
|
||||||
|
if (!nodeColNames.includes('event_date')) {
|
||||||
|
console.log('Adding nodes.event_date column');
|
||||||
|
this.db.exec('ALTER TABLE nodes ADD COLUMN event_date TEXT;');
|
||||||
|
this.db.exec(`
|
||||||
|
UPDATE nodes SET event_date = json_extract(metadata, '$.published_date')
|
||||||
|
WHERE metadata IS NOT NULL
|
||||||
|
AND json_extract(metadata, '$.published_date') IS NOT NULL
|
||||||
|
AND json_extract(metadata, '$.published_date') != '';
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dimensions.icon
|
||||||
|
const dimCols2 = this.db.prepare('PRAGMA table_info(dimensions)').all() as Array<{ name: string }>;
|
||||||
|
if (!dimCols2.some((c: any) => c.name === 'icon')) {
|
||||||
|
console.log('Adding dimensions.icon column');
|
||||||
|
this.db.exec('ALTER TABLE dimensions ADD COLUMN icon TEXT;');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop dead columns (SQLite 3.35+)
|
||||||
|
if (nodeColNames.includes('type')) {
|
||||||
|
console.log('Dropping nodes.type column');
|
||||||
|
try { this.db.exec('DROP INDEX IF EXISTS idx_nodes_type;'); } catch {}
|
||||||
|
try { this.db.exec('ALTER TABLE nodes DROP COLUMN type;'); } catch (e) {
|
||||||
|
console.warn('Could not drop nodes.type (SQLite < 3.35?):', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeColNames.includes('is_pinned')) {
|
||||||
|
console.log('Dropping nodes.is_pinned column');
|
||||||
|
try { this.db.exec('DROP INDEX IF EXISTS idx_nodes_pinned;'); } catch {}
|
||||||
|
try { this.db.exec('ALTER TABLE nodes DROP COLUMN is_pinned;'); } catch (e) {
|
||||||
|
console.warn('Could not drop nodes.is_pinned:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop edges.user_feedback
|
||||||
|
const edgeCols = this.db.prepare('PRAGMA table_info(edges)').all() as Array<{ name: string }>;
|
||||||
|
if (edgeCols.some((c: any) => c.name === 'user_feedback')) {
|
||||||
|
console.log('Dropping edges.user_feedback column');
|
||||||
|
try { this.db.exec('ALTER TABLE edges DROP COLUMN user_feedback;'); } catch (e) {
|
||||||
|
console.warn('Could not drop edges.user_feedback:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild FTS if it references 'content' instead of 'notes'
|
||||||
|
try {
|
||||||
|
const ftsCheck = this.db.prepare("SELECT sql FROM sqlite_master WHERE name='nodes_fts'").get() as any;
|
||||||
|
if (ftsCheck && ftsCheck.sql && ftsCheck.sql.includes('content')) {
|
||||||
|
console.log('Rebuilding nodes_fts to reference notes instead of content');
|
||||||
|
this.db.exec('DROP TABLE IF EXISTS nodes_fts;');
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE VIRTUAL TABLE nodes_fts USING fts5(title, description, notes, content=nodes, content_rowid=id);
|
||||||
|
INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild');
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
} catch (ftsErr) {
|
||||||
|
console.warn('FTS rebuild skipped:', ftsErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Final schema pass migrations complete');
|
||||||
|
} catch (schemaErr) {
|
||||||
|
console.warn('Final schema pass migration error:', schemaErr);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Logging + memory schema ensured');
|
console.log('Logging + memory schema ensured');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to ensure logging/memory schema:', error);
|
console.error('Failed to ensure logging/memory schema:', error);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function bootstrapFromLegacyPins(): void {
|
|||||||
try {
|
try {
|
||||||
const db = getSQLiteClient();
|
const db = getSQLiteClient();
|
||||||
const countRow = db
|
const countRow = db
|
||||||
.query<{ count: number }>('SELECT COUNT(*) as count FROM nodes WHERE is_pinned = 1')
|
.query<{ count: number }>('SELECT COUNT(*) as count FROM nodes WHERE 1=0 /* /* is_pinned removed */ removed */')
|
||||||
.rows[0];
|
.rows[0];
|
||||||
const pinnedCount = Number(countRow?.count ?? 0);
|
const pinnedCount = Number(countRow?.count ?? 0);
|
||||||
if (pinnedCount > 0) {
|
if (pinnedCount > 0) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
interface NodeRecord {
|
interface NodeRecord {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content: string | null;
|
notes: string | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
dimensions_json: string;
|
dimensions_json: string;
|
||||||
embedding?: Buffer | null;
|
embedding?: Buffer | null;
|
||||||
@@ -58,7 +58,7 @@ export class NodeEmbedder {
|
|||||||
const prompt = `Analyze this content and provide 2-3 key insights or themes in a concise paragraph (max 100 words):
|
const prompt = `Analyze this content and provide 2-3 key insights or themes in a concise paragraph (max 100 words):
|
||||||
|
|
||||||
Title: ${node.title}
|
Title: ${node.title}
|
||||||
Content: ${node.content || 'No content'}
|
Content: ${node.notes || 'No content'}
|
||||||
Dimensions: ${dimensionsText}
|
Dimensions: ${dimensionsText}
|
||||||
|
|
||||||
Focus on the main concepts, key relationships, and practical implications.`;
|
Focus on the main concepts, key relationships, and practical implications.`;
|
||||||
@@ -106,13 +106,13 @@ Focus on the main concepts, key relationships, and practical implications.`;
|
|||||||
// Create base embedding text
|
// Create base embedding text
|
||||||
let embeddingText = formatEmbeddingText(
|
let embeddingText = formatEmbeddingText(
|
||||||
node.title,
|
node.title,
|
||||||
node.content || '',
|
node.notes || '',
|
||||||
dimensions,
|
dimensions,
|
||||||
node.description
|
node.description
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add AI analysis if content exists
|
// Add AI analysis if content exists
|
||||||
if (node.content && node.content.trim().length > 0) {
|
if (node.notes && node.notes.trim().length > 0) {
|
||||||
const analysis = await this.analyzeNodeWithAI(node);
|
const analysis = await this.analyzeNodeWithAI(node);
|
||||||
if (analysis) {
|
if (analysis) {
|
||||||
embeddingText += `\n\nAI Analysis: ${analysis}`;
|
embeddingText += `\n\nAI Analysis: ${analysis}`;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const createNodeTool = tool({
|
|||||||
description: 'Create node with title/content/link and optional dimensions (locked dimensions auto-assigned)',
|
description: 'Create node with title/content/link and optional dimensions (locked dimensions auto-assigned)',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
title: z.string().describe('The title of the node'),
|
title: z.string().describe('The title of the node'),
|
||||||
content: z.string().optional().describe('The main content, description, or notes for this node'),
|
notes: z.string().optional().describe('The main notes, description, or notes for this node'),
|
||||||
link: z.string().optional().describe('A URL link to the source'),
|
link: z.string().optional().describe('A URL link to the source'),
|
||||||
dimensions: z
|
dimensions: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ export const getNodesByIdTool = tool({
|
|||||||
description: 'Load full node records by IDs',
|
description: 'Load full node records by IDs',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
nodeIds: z.array(z.number().int().positive()).min(1).max(10).describe('List of node IDs to load'),
|
nodeIds: z.array(z.number().int().positive()).min(1).max(10).describe('List of node IDs to load'),
|
||||||
includeContentPreview: z.boolean().default(true).describe('Whether to return a trimmed content preview for each node'),
|
includeNotesPreview: z.boolean().default(true).describe('Whether to return a trimmed content preview for each node'),
|
||||||
}),
|
}),
|
||||||
execute: async ({ nodeIds, includeContentPreview }) => {
|
execute: async ({ nodeIds, includeNotesPreview }) => {
|
||||||
const uniqueIds = Array.from(new Set(nodeIds.filter(id => Number.isFinite(id) && id > 0)));
|
const uniqueIds = Array.from(new Set(nodeIds.filter(id => Number.isFinite(id) && id > 0)));
|
||||||
if (uniqueIds.length === 0) {
|
if (uniqueIds.length === 0) {
|
||||||
return {
|
return {
|
||||||
@@ -23,8 +23,8 @@ export const getNodesByIdTool = tool({
|
|||||||
try {
|
try {
|
||||||
const node = await nodeService.getNodeById(id);
|
const node = await nodeService.getNodeById(id);
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
const preview = includeContentPreview
|
const preview = includeNotesPreview
|
||||||
? (node.content || node.description || '')
|
? (node.notes || node.description || '')
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.slice(0, 80)
|
.slice(0, 80)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
@@ -38,7 +38,7 @@ export const getNodesByIdTool = tool({
|
|||||||
dimensions: node.dimensions || [],
|
dimensions: node.dimensions || [],
|
||||||
chunk_status: node.chunk_status || 'unknown',
|
chunk_status: node.chunk_status || 'unknown',
|
||||||
updated_at: node.updated_at,
|
updated_at: node.updated_at,
|
||||||
content_preview: preview || null,
|
notes_preview: preview || null,
|
||||||
metadata: node.metadata ?? null,
|
metadata: node.metadata ?? null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ export const queryDimensionNodesTool = tool({
|
|||||||
dimensions: node.dimensions || [],
|
dimensions: node.dimensions || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeContent && node.content) {
|
if (includeContent && node.notes) {
|
||||||
// Truncate to ~100 chars
|
// Truncate to ~100 chars
|
||||||
formatted.contentPreview = node.content.length > 100
|
formatted.notesPreview = node.notes.length > 100
|
||||||
? node.content.substring(0, 100) + '...'
|
? node.notes.substring(0, 100) + '...'
|
||||||
: node.content;
|
: node.notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
|||||||
import type { Node } from '@/types/database';
|
import type { Node } from '@/types/database';
|
||||||
|
|
||||||
export const queryNodesTool = tool({
|
export const queryNodesTool = tool({
|
||||||
description: 'Search nodes by title/content/dimensions',
|
description: 'Search nodes by title/notes/dimensions',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
filters: z.object({
|
filters: z.object({
|
||||||
dimensions: z.array(z.string()).describe('Filter by dimensions (e.g., ["research", "ai", "technology"]). Replaces old type/stage filtering.').optional(),
|
dimensions: z.array(z.string()).describe('Filter by dimensions (e.g., ["research", "ai", "technology"]). Replaces old type/stage filtering.').optional(),
|
||||||
search: z.string().describe('Search term to match against title or content').optional(),
|
search: z.string().describe('Search term to match against title or notes').optional(),
|
||||||
limit: z.number().min(1).max(50).default(10).describe('Maximum number of results to return')
|
limit: z.number().min(1).max(50).default(10).describe('Maximum number of results to return')
|
||||||
}).optional()
|
}).optional()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { z } from 'zod';
|
|||||||
import { edgeService } from '@/services/database/edges';
|
import { edgeService } from '@/services/database/edges';
|
||||||
|
|
||||||
export const updateEdgeTool = tool({
|
export const updateEdgeTool = tool({
|
||||||
description: 'Update edge context/source/feedback',
|
description: 'Update edge context/source',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
edge_id: z.number().describe('The ID of the edge to update'),
|
edge_id: z.number().describe('The ID of the edge to update'),
|
||||||
updates: z.object({
|
updates: z.object({
|
||||||
context: z.record(z.any()).optional().describe('Updated context information for this edge - can include explanation, relationship type, strength, notes, etc.'),
|
context: z.record(z.any()).optional().describe('Updated context information for this edge - can include explanation, relationship type, strength, notes, etc.'),
|
||||||
source: z.enum(['user', 'ai_similarity', 'helper_name']).optional().describe('Updated source classification for this edge'),
|
source: z.enum(['user', 'ai_similarity', 'helper_name']).optional().describe('Updated source classification for this edge'),
|
||||||
user_feedback: z.boolean().optional().describe('User feedback on this edge connection (true = positive, false = negative)')
|
|
||||||
}).describe('Fields to update on the edge')
|
}).describe('Fields to update on the edge')
|
||||||
}),
|
}),
|
||||||
execute: async (params) => {
|
execute: async (params) => {
|
||||||
@@ -46,9 +45,6 @@ export const updateEdgeTool = tool({
|
|||||||
const updateDescriptions = [];
|
const updateDescriptions = [];
|
||||||
if (cleanUpdates.context) updateDescriptions.push('context');
|
if (cleanUpdates.context) updateDescriptions.push('context');
|
||||||
if (cleanUpdates.source) updateDescriptions.push(`source to ${cleanUpdates.source}`);
|
if (cleanUpdates.source) updateDescriptions.push(`source to ${cleanUpdates.source}`);
|
||||||
if (cleanUpdates.user_feedback !== undefined) {
|
|
||||||
updateDescriptions.push(`user feedback to ${cleanUpdates.user_feedback ? 'positive' : 'negative'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const updateNodeTool = tool({
|
|||||||
id: z.number().describe('The ID of the node to update'),
|
id: z.number().describe('The ID of the node to update'),
|
||||||
updates: z.object({
|
updates: z.object({
|
||||||
title: z.string().optional().describe('New title'),
|
title: z.string().optional().describe('New title'),
|
||||||
content: z.string().optional().describe('New content/description/notes'),
|
notes: z.string().optional().describe('New content/description/notes'),
|
||||||
link: z.string().optional().describe('New link'),
|
link: z.string().optional().describe('New link'),
|
||||||
dimensions: z.array(z.string()).optional().describe('New dimension tags - completely replaces existing dimensions'),
|
dimensions: z.array(z.string()).optional().describe('New dimension tags - completely replaces existing dimensions'),
|
||||||
chunk: z.string().optional().describe('New chunk content'),
|
chunk: z.string().optional().describe('New chunk content'),
|
||||||
@@ -25,28 +25,28 @@ export const updateNodeTool = tool({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FORCE APPEND for content field - fetch existing and append new content
|
// FORCE APPEND for content field - fetch existing and append new content
|
||||||
if (updates.content) {
|
if (updates.notes) {
|
||||||
const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes/${id}`);
|
const fetchResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes/${id}`);
|
||||||
if (fetchResponse.ok) {
|
if (fetchResponse.ok) {
|
||||||
const { node } = await fetchResponse.json();
|
const { node } = await fetchResponse.json();
|
||||||
const existingContent = (node?.content || '').trim();
|
const existingNotes = (node?.notes || '').trim();
|
||||||
const newContent = updates.content.trim();
|
const newNotes = updates.notes.trim();
|
||||||
|
|
||||||
// Skip if new content is identical to existing (model sent duplicate)
|
// Skip if new content is identical to existing (model sent duplicate)
|
||||||
if (existingContent === newContent) {
|
if (existingNotes === newNotes) {
|
||||||
console.log(`[updateNode] ERROR - new content identical to existing (${existingContent.length} chars). Model should NOT call updateNode again.`);
|
console.log(`[updateNode] ERROR - new content identical to existing (${existingNotes.length} chars). Model should NOT call updateNode again.`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Content already up to date - do not call updateNode again. Move to next step.',
|
error: 'Notes already up to date - do not call updateNode again. Move to next step.',
|
||||||
data: null
|
data: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect if adding a section that already exists (e.g., ## Integration Analysis)
|
// Detect if adding a section that already exists (e.g., ## Integration Analysis)
|
||||||
const newSectionMatch = newContent.match(/^##\s+(.+)$/m);
|
const newSectionMatch = newNotes.match(/^##\s+(.+)$/m);
|
||||||
if (newSectionMatch && existingContent) {
|
if (newSectionMatch && existingNotes) {
|
||||||
const sectionHeader = newSectionMatch[0]; // e.g., "## Integration Analysis"
|
const sectionHeader = newSectionMatch[0]; // e.g., "## Integration Analysis"
|
||||||
if (existingContent.includes(sectionHeader)) {
|
if (existingNotes.includes(sectionHeader)) {
|
||||||
console.log(`[updateNode] ERROR - Section "${sectionHeader}" already exists in node`);
|
console.log(`[updateNode] ERROR - Section "${sectionHeader}" already exists in node`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -57,19 +57,19 @@ export const updateNodeTool = tool({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect if model included existing content + new content
|
// Detect if model included existing content + new content
|
||||||
if (existingContent && newContent.startsWith(existingContent)) {
|
if (existingNotes && newNotes.startsWith(existingNotes)) {
|
||||||
// Extract only the new part
|
// Extract only the new part
|
||||||
const actualNewContent = newContent.substring(existingContent.length).trim();
|
const actualNewNotes = newNotes.substring(existingNotes.length).trim();
|
||||||
console.log(`[updateNode] Model included existing content - extracting new part only (${actualNewContent.length} chars)`);
|
console.log(`[updateNode] Model included existing content - extracting new part only (${actualNewNotes.length} chars)`);
|
||||||
const separator = existingContent.endsWith('\n\n') ? '' : '\n\n';
|
const separator = existingNotes.endsWith('\n\n') ? '' : '\n\n';
|
||||||
updates.content = `${existingContent}${separator}${actualNewContent}`;
|
updates.notes = `${existingNotes}${separator}${actualNewNotes}`;
|
||||||
} else if (existingContent) {
|
} else if (existingNotes) {
|
||||||
// Normal append
|
// Normal append
|
||||||
const separator = existingContent.endsWith('\n\n') ? '' : '\n\n';
|
const separator = existingNotes.endsWith('\n\n') ? '' : '\n\n';
|
||||||
updates.content = `${existingContent}${separator}${newContent}`;
|
updates.notes = `${existingNotes}${separator}${newNotes}`;
|
||||||
console.log(`[updateNode] Appended content: ${existingContent.length} + ${newContent.length} = ${updates.content.length} chars`);
|
console.log(`[updateNode] Appended notes: ${existingNotes.length} + ${newNotes.length} = ${updates.notes.length} chars`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[updateNode] No existing content, using new content as-is (${newContent.length} chars)`);
|
console.log(`[updateNode] No existing content, using new content as-is (${newNotes.length} chars)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ export const updateNodeTool = tool({
|
|||||||
// Call the nodes API endpoint
|
// Call the nodes API endpoint
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes/${id}`, {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/nodes/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Notes-Type': 'application/json' },
|
||||||
body: JSON.stringify(updates)
|
body: JSON.stringify(updates)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -86,13 +86,13 @@ export const paperExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: { success: boolean; content?: string; chunk?: string; metadata?: any; error?: string };
|
let result: { success: boolean; notes?: string; chunk?: string; metadata?: any; error?: string };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const extractionResult = await extractPaper(url);
|
const extractionResult = await extractPaper(url);
|
||||||
result = {
|
result = {
|
||||||
success: true,
|
success: true,
|
||||||
content: extractionResult.content,
|
notes: extractionResult.content,
|
||||||
chunk: extractionResult.chunk,
|
chunk: extractionResult.chunk,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: extractionResult.metadata.title,
|
title: extractionResult.metadata.title,
|
||||||
@@ -110,7 +110,7 @@ export const paperExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success || (!result.content && !result.chunk)) {
|
if (!result.success || (!result.notes && !result.chunk)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || 'Failed to extract PDF content',
|
error: result.error || 'Failed to extract PDF content',
|
||||||
@@ -123,7 +123,7 @@ export const paperExtractTool = tool({
|
|||||||
// Step 2: AI Analysis for enhanced metadata
|
// Step 2: AI Analysis for enhanced metadata
|
||||||
const aiAnalysis = await analyzeContentWithAI(
|
const aiAnalysis = await analyzeContentWithAI(
|
||||||
result.metadata?.title || `PDF: ${new URL(url).pathname.split('/').pop()?.replace('.pdf', '')}`,
|
result.metadata?.title || `PDF: ${new URL(url).pathname.split('/').pop()?.replace('.pdf', '')}`,
|
||||||
result.content?.substring(0, 2000) || 'PDF document content',
|
result.notes?.substring(0, 2000) || 'PDF document content',
|
||||||
'pdf'
|
'pdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,17 +143,17 @@ export const paperExtractTool = tool({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: nodeTitle,
|
title: nodeTitle,
|
||||||
content: enhancedDescription,
|
notes: enhancedDescription,
|
||||||
link: url,
|
link: url,
|
||||||
dimensions: trimmedDimensions,
|
dimensions: trimmedDimensions,
|
||||||
chunk: result.chunk || result.content,
|
chunk: result.chunk || result.notes,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'pdf',
|
source: 'pdf',
|
||||||
hostname: new URL(url).hostname,
|
hostname: new URL(url).hostname,
|
||||||
author: result.metadata?.author || result.metadata?.info?.Author,
|
author: result.metadata?.author || result.metadata?.info?.Author,
|
||||||
pages: result.metadata?.pages,
|
pages: result.metadata?.pages,
|
||||||
file_size: result.metadata?.file_size,
|
file_size: result.metadata?.file_size,
|
||||||
content_length: (result.chunk || result.content)?.length,
|
content_length: (result.chunk || result.notes)?.length,
|
||||||
extraction_method: result.metadata?.extraction_method || 'python_pdfplumber',
|
extraction_method: result.metadata?.extraction_method || 'python_pdfplumber',
|
||||||
ai_analysis: aiAnalysis?.reasoning,
|
ai_analysis: aiAnalysis?.reasoning,
|
||||||
enhanced_description: enhancedDescription,
|
enhanced_description: enhancedDescription,
|
||||||
@@ -187,7 +187,7 @@ export const paperExtractTool = tool({
|
|||||||
data: {
|
data: {
|
||||||
nodeId: createResult.data?.id,
|
nodeId: createResult.data?.id,
|
||||||
title: nodeTitle,
|
title: nodeTitle,
|
||||||
contentLength: (result.chunk || result.content || '').length,
|
contentLength: (result.chunk || result.notes || '').length,
|
||||||
url: url,
|
url: url,
|
||||||
dimensions: actualDimensions
|
dimensions: actualDimensions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ export const websiteExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: { success: boolean; content?: string; chunk?: string; metadata?: any; error?: string };
|
let result: { success: boolean; notes?: string; chunk?: string; metadata?: any; error?: string };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const extractionResult = await extractWebsite(url);
|
const extractionResult = await extractWebsite(url);
|
||||||
result = {
|
result = {
|
||||||
success: true,
|
success: true,
|
||||||
content: extractionResult.content,
|
notes: extractionResult.content,
|
||||||
chunk: extractionResult.chunk,
|
chunk: extractionResult.chunk,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: extractionResult.metadata.title,
|
title: extractionResult.metadata.title,
|
||||||
@@ -101,7 +101,7 @@ export const websiteExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success || (!result.content && !result.chunk)) {
|
if (!result.success || (!result.notes && !result.chunk)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || 'Failed to extract website content',
|
error: result.error || 'Failed to extract website content',
|
||||||
@@ -114,7 +114,7 @@ export const websiteExtractTool = tool({
|
|||||||
// Step 2: AI Analysis for enhanced metadata
|
// Step 2: AI Analysis for enhanced metadata
|
||||||
const aiAnalysis = await analyzeContentWithAI(
|
const aiAnalysis = await analyzeContentWithAI(
|
||||||
result.metadata?.title || `Website: ${new URL(url).hostname}`,
|
result.metadata?.title || `Website: ${new URL(url).hostname}`,
|
||||||
result.content?.substring(0, 2000) || 'Website content',
|
result.notes?.substring(0, 2000) || 'Website content',
|
||||||
'website'
|
'website'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,16 +134,16 @@ export const websiteExtractTool = tool({
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: nodeTitle,
|
title: nodeTitle,
|
||||||
content: enhancedDescription,
|
notes: enhancedDescription,
|
||||||
link: url,
|
link: url,
|
||||||
dimensions: trimmedDimensions,
|
dimensions: trimmedDimensions,
|
||||||
chunk: result.chunk || result.content,
|
chunk: result.chunk || result.notes,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'website',
|
source: 'website',
|
||||||
hostname: new URL(url).hostname,
|
hostname: new URL(url).hostname,
|
||||||
author: result.metadata?.author,
|
author: result.metadata?.author,
|
||||||
published_date: result.metadata?.published_date || result.metadata?.date,
|
published_date: result.metadata?.published_date || result.metadata?.date,
|
||||||
content_length: (result.chunk || result.content)?.length,
|
content_length: (result.chunk || result.notes)?.length,
|
||||||
extraction_method: result.metadata?.extraction_method || 'python_beautifulsoup',
|
extraction_method: result.metadata?.extraction_method || 'python_beautifulsoup',
|
||||||
ai_analysis: aiAnalysis?.reasoning,
|
ai_analysis: aiAnalysis?.reasoning,
|
||||||
enhanced_description: enhancedDescription,
|
enhanced_description: enhancedDescription,
|
||||||
@@ -177,7 +177,7 @@ export const websiteExtractTool = tool({
|
|||||||
data: {
|
data: {
|
||||||
nodeId: createResult.data?.id,
|
nodeId: createResult.data?.id,
|
||||||
title: nodeTitle,
|
title: nodeTitle,
|
||||||
contentLength: (result.chunk || result.content || '').length,
|
contentLength: (result.chunk || result.notes || '').length,
|
||||||
url: url,
|
url: url,
|
||||||
dimensions: actualDimensions
|
dimensions: actualDimensions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,14 +130,14 @@ export const youtubeExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: { success: boolean; content?: string; chunk?: string; metadata?: any; error?: string };
|
let result: { success: boolean; notes?: string; chunk?: string; metadata?: any; error?: string };
|
||||||
|
|
||||||
console.log('📝 Using TypeScript yt-dlp extractor');
|
console.log('📝 Using TypeScript yt-dlp extractor');
|
||||||
try {
|
try {
|
||||||
const extractionResult = await extractYouTube(url);
|
const extractionResult = await extractYouTube(url);
|
||||||
result = {
|
result = {
|
||||||
success: extractionResult.success,
|
success: extractionResult.success,
|
||||||
content: extractionResult.content,
|
notes: extractionResult.content,
|
||||||
chunk: extractionResult.chunk,
|
chunk: extractionResult.chunk,
|
||||||
metadata: {
|
metadata: {
|
||||||
video_title: extractionResult.metadata.video_title,
|
video_title: extractionResult.metadata.video_title,
|
||||||
@@ -159,7 +159,7 @@ export const youtubeExtractTool = tool({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success || (!result.content && !result.chunk)) {
|
if (!result.success || (!result.notes && !result.chunk)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || 'Failed to extract YouTube content',
|
error: result.error || 'Failed to extract YouTube content',
|
||||||
@@ -178,7 +178,7 @@ export const youtubeExtractTool = tool({
|
|||||||
|
|
||||||
// Step 3: Create node with extracted content and AI analysis
|
// 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 nodeTitle = title || result.metadata?.video_title || `YouTube Video ${url.split('/').pop()?.split('?')[0]}`;
|
||||||
const transcriptSummary = await summariseTranscript(nodeTitle, result.chunk || result.content || '');
|
const transcriptSummary = await summariseTranscript(nodeTitle, result.chunk || result.notes || '');
|
||||||
const content = transcriptSummary || aiAnalysis?.enhancedDescription || `YouTube video by ${result.metadata?.channel_name || 'Unknown Channel'}`;
|
const content = transcriptSummary || aiAnalysis?.enhancedDescription || `YouTube video by ${result.metadata?.channel_name || 'Unknown Channel'}`;
|
||||||
|
|
||||||
const suppliedDimensions = Array.isArray(dimensions) ? dimensions : [];
|
const suppliedDimensions = Array.isArray(dimensions) ? dimensions : [];
|
||||||
@@ -196,7 +196,7 @@ export const youtubeExtractTool = tool({
|
|||||||
content,
|
content,
|
||||||
link: url,
|
link: url,
|
||||||
dimensions: trimmedDimensions,
|
dimensions: trimmedDimensions,
|
||||||
chunk: result.chunk || result.content,
|
chunk: result.chunk || result.notes,
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'youtube',
|
source: 'youtube',
|
||||||
video_id: result.metadata?.video_id,
|
video_id: result.metadata?.video_id,
|
||||||
@@ -239,7 +239,7 @@ export const youtubeExtractTool = tool({
|
|||||||
data: {
|
data: {
|
||||||
nodeId: createResult.data?.id,
|
nodeId: createResult.data?.id,
|
||||||
title: nodeTitle,
|
title: nodeTitle,
|
||||||
contentLength: (result.chunk || result.content || '').length,
|
contentLength: (result.chunk || result.notes || '').length,
|
||||||
url: url,
|
url: url,
|
||||||
dimensions: actualDimensions
|
dimensions: actualDimensions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ export interface Node {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
content?: string; // Consolidated content from description + abstract + notes
|
notes?: string; // Consolidated content from description + abstract + notes
|
||||||
link?: string;
|
link?: string;
|
||||||
type?: string;
|
event_date?: string;
|
||||||
dimensions: string[]; // Flexible dimensions replacing type + stage + segment + tags
|
dimensions: string[]; // Flexible dimensions replacing type + stage + segment + tags
|
||||||
embedding?: Buffer; // Node-level embedding (BLOB data)
|
embedding?: Buffer; // Node-level embedding (BLOB data)
|
||||||
chunk?: string;
|
chunk?: string;
|
||||||
metadata?: any; // Flexible metadata storage from extras + chunk_status + sub_type
|
metadata?: any; // Flexible metadata storage from extras + chunk_status + sub_type
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
is_pinned?: number; // Legacy pin flag (read-only, slated for removal)
|
// Legacy pin flag (read-only, slated for removal)
|
||||||
edge_count?: number; // Derived count of edges, included in some queries
|
edge_count?: number; // Derived count of edges, included in some queries
|
||||||
|
|
||||||
// Optional embedding fields (restored from migration)
|
// Optional embedding fields (restored from migration)
|
||||||
|
|||||||
Reference in New Issue
Block a user