feat: Step 10 - Node Editor & AI-Powered Linking
Implemented the node editor page and AI-powered link suggestions: 1. Node Editor Page (/editor/[id]): - Form with title and body fields using Mantine - Pre-fill support from query params (for chat redirects) - "Find Related" button to discover similar nodes - "Publish Node" button to save to ATproto + SurrealDB - Display of suggested links with similarity scores - Mantine notifications for success/error feedback 2. Suggest Links API (/api/suggest-links): - Authenticates using SurrealDB JWT cookie - Generates embedding for draft text using Google AI - Performs vector similarity search using SurrealDB - Returns top 5 most similar nodes with cosine scores - Enforces row-level security (users only see their nodes) 3. Magnitude Tests: - Editor page rendering - Pre-filled form from query params - Publishing new nodes - Form validation The editor integrates with the existing /api/nodes write-through cache from Step 6, completing the node creation workflow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
74
app/api/suggest-links/route.ts
Normal file
74
app/api/suggest-links/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { connectToDB } from '@/lib/db';
|
||||
import { generateEmbedding } from '@/lib/ai';
|
||||
|
||||
/**
|
||||
* POST /api/suggest-links
|
||||
*
|
||||
* Uses vector similarity search to find related nodes.
|
||||
* Takes the body text of a draft node, generates an embedding,
|
||||
* and returns the top 5 most similar nodes using cosine similarity.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const cookieStore = await cookies();
|
||||
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
||||
|
||||
if (!surrealJwt) {
|
||||
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { body } = (await request.json()) as { body: string };
|
||||
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: 'Body text is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Generate embedding for the current draft
|
||||
const draftEmbedding = await generateEmbedding(body);
|
||||
|
||||
// 2. Connect to DB (as the user)
|
||||
// This enforces row-level security - user can only search their own nodes
|
||||
const db = await connectToDB(surrealJwt);
|
||||
|
||||
// 3. Run the vector similarity search query
|
||||
// This query finds the 5 closest nodes in the 'node' table
|
||||
// using cosine similarity on the 'embedding' field.
|
||||
// It only searches nodes WHERE user_did = $token.did,
|
||||
// which is enforced by the table's PERMISSIONS.
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
atp_uri,
|
||||
vector::similarity::cosine(embedding, $draft_embedding) AS score
|
||||
FROM node
|
||||
ORDER BY score DESC
|
||||
LIMIT 5;
|
||||
`;
|
||||
|
||||
const results = await db.query<[Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
atp_uri: string;
|
||||
score: number;
|
||||
}>]>(query, {
|
||||
draft_embedding: draftEmbedding,
|
||||
});
|
||||
|
||||
// The query returns an array of result sets. We want the first one.
|
||||
const suggestions = results[0] || [];
|
||||
|
||||
return NextResponse.json(suggestions);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Suggest Links] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to suggest links' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user