# **File: COMMIT\_10\_LINKING.md** ## **Commit 10: Node Editor & AI-Powered Linking** ### **Objective** Build the node editor UI and the AI-powered "Find related" feature. This commit will: 1. Create the editor page (/editor/\[id\]) that is pre-filled by the chat (Commit 07\) or loaded from the DB. 2. Implement the "Publish" button, which calls the /api/nodes route (from Commit 06). 3. Implement the "Find related" button, which calls a *new* /api/suggest-links route. 4. Implement the /api/suggest-links route, which generates an embedding for the current draft and uses SurrealDB's vector search to find similar nodes.15 ### **Implementation Specification** **1\. Create Editor Page (app/editor/\[id\]/page.tsx)** Create a file at /app/editor/\[id\]/page.tsx: TypeScript 'use client'; import { Container, Title, TextInput, Textarea, Button, Stack, Paper, Text, LoadingOverlay, Group, } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useSearchParams, useRouter, useParams } from 'next/navigation'; import { useEffect, useState } from 'react'; // Define the shape of a suggested link interface SuggestedNode { id: string; title: string; body: string; score: number; } export default function EditorPage() { const router \= useRouter(); const params \= useParams(); const searchParams \= useSearchParams(); const \[isPublishing, setIsPublishing\] \= useState(false); const \[isFinding, setIsFinding\] \= useState(false); const \= useState\(); const form \= useForm({ initialValues: { title: '', body: '', links: as string, // Array of at-uri strings }, }); // Pre-fill form from search params (from AI chat redirect) useEffect(() \=\> { if (params.id \=== 'new') { const title \= searchParams.get('title') | | ''; const body \= searchParams.get('body') | | ''; form.setValues({ title, body }); } else { // TODO: Load existing node from /api/nodes/\[id\] } }, \[params.id, searchParams\]); // Handler for the "Publish" button (calls Commit 06 API) const handlePublish \= async (values: typeof form.values) \=\> { setIsPublishing(true); try { const response \= await fetch('/api/nodes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values), }); if (\!response.ok) { throw new Error('Failed to publish node'); } const newNode \= await response.json(); // On success, go to the graph router.push('/galaxy'); } catch (error) { console.error(error); // TODO: Show notification } finally { setIsPublishing(false); } }; // Handler for the "Find related" button const handleFindRelated \= async () \=\> { setIsFinding(true); setSuggestions(); try { const response \= await fetch('/api/suggest-links', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body: form.values.body }), }); if (\!response.ok) { throw new Error('Failed to find links'); } const relatedNodes \= await response.json(); setSuggestions(relatedNodes); } catch (error) { console.error(error); // TODO: Show notification } finally { setIsFinding(false); } }; return ( \ \
\ \ {params.id \=== 'new'? 'Create New Node' : 'Edit Node'} \</Title\> \<TextInput label="Title" placeholder="Your node title" required {...form.getInputProps('title')} /\> \<Textarea label="Body" placeholder="Your node content..." required minRows={10} autosize {...form.getInputProps('body')} /\> \<Group\> \<Button type="button" variant="outline" onClick={handleFindRelated} loading={isFinding} \> Find Related \</Button\> \<Button type="submit" loading={isPublishing}\> Publish Node \</Button\> \</Group\> {/\* Related Links Section \*/} \<Stack\> {isFinding && \<LoadingOverlay visible /\>} {suggestions.length \> 0 && \<Title order={4}\>Suggested Links\</Title\>} {suggestions.map((node) \=\> ( \<Paper key={node.id} withBorder p="sm"\> \<Text fw={700}\>{node.title}\</Text\> \<Text size="sm" lineClamp={2}\>{node.body}\</Text\> \<Text size="xs" c="dimmed"\>Similarity: {(node.score \* 100).toFixed(0)}%\</Text\> \</Paper\> ))} {\!isFinding && suggestions.length \=== 0 && ( \<Text size="sm" c="dimmed"\> {/\* Placeholder text \*/} \</Text\> )} \</Stack\> \</Stack\> \</form\> \</Container\> ); } **2\. Create Link Suggestion API (app/api/suggest-links/route.ts)** Create a file at /app/api/suggest-links/route.ts: TypeScript import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { connectToDB } from '@/lib/db'; import { generateEmbedding } from '@/lib/ai'; export async function POST(request: NextRequest) { const surrealJwt \= cookies().get('ponderants-auth')?.value; if (\!surrealJwt) { return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); } const { body } \= await request.json(); 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) 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(query, { draft\_embedding: draftEmbedding, }); // The query returns an array of result sets. We want the first one. return NextResponse.json(results.result); } catch (error) { console.error('Link suggestion error:', error); return NextResponse.json( { error: 'Failed to suggest links' }, { status: 500 } ); } } ### **Test Specification** **1\. Create Test File (tests/magnitude/10-linking.mag.ts)** Create a file at /tests/magnitude/10-linking.mag.ts: TypeScript import { test } from 'magnitude-test'; // Helper function to seed the database for this test async function seedDatabase(agent) { // This would use a custom magnitude.run command or API // to pre-populate the SurrealDB instance with mock nodes. await agent.act('Seed the database with 3 nodes: "Node A", "Node B", "Node C"'); // "Node A" is about "dogs and cats" // "Node B" is about "vector databases" // "Node C" is about "ATproto" } test('\[Happy Path\] User can find related links for a draft', async (agent) \=\> { // Setup: Seed the DB await seedDatabase(agent); // Act: Navigate to the editor await agent.act('Navigate to /editor/new'); // Act: Fill out the form with a related idea await agent.act( 'Enter "My New Post" into the "Title" input' ); await agent.act( 'Enter "This idea is about vectors and databases, and how they work." into the "Body" textarea' ); // Act: Click the find related button // (Mock the /api/suggest-links route to return "Node B") await agent.act('Click the "Find Related" button'); // Check: The related node appears in the suggestions await agent.check('A list of suggested links appears'); await agent.check('The suggested node "Node B" is visible in the list'); await agent.check('The suggested node "Node A" is NOT visible in the list'); }); test('\[Unhappy Path\] User sees empty state when no links found', async (agent) \=\> { // Setup: Seed the DB await seedDatabase(agent); // Act: Navigate to the editor await agent.act('Navigate to /editor/new'); // Act: Fill out the form with an unrelated idea await agent.act( 'Enter "Zebras" into the "Title" input' ); await agent.act( 'Enter "Zebras are striped equines." into the "Body" textarea' ); // Act: Click the find related button // (Mock the /api/suggest-links route to return an empty array) await agent.act('Click the "Find Related" button'); // Check: An empty state is shown await agent.check('The text "No related nodes found" is visible'); }); test('\[Happy Path\] User can publish a new node', async (agent) \=\> { // Act: Navigate to the editor await agent.act('Navigate to /editor/new'); // Act: Fill out the form await agent.act( 'Enter "My First Published Node" into the "Title" input' ); await agent.act( 'Enter "This is the body of my first node." into the "Body" textarea' ); // Act: Click Publish // (Mock the /api/nodes route (Commit 06\) to return success) await agent.act('Click the "Publish Node" button'); // Check: User is redirected to the galaxy await agent.check( 'The browser URL is now "http://localhost:3000/galaxy"' ); });