- Increase logo size (48x48 desktop, 56x56 mobile) for better visibility - Add logo as favicon - Add logo to mobile header - Move user menu to navigation bars (sidebar on desktop, bottom bar on mobile) - Fix desktop chat layout - container structure prevents voice controls cutoff - Fix mobile bottom bar - use icon-only ActionIcons instead of truncated text buttons - Hide Create Node/New Conversation buttons on mobile to save header space - Make fixed header and voice controls work properly with containers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
84 lines
2.3 KiB
TypeScript
84 lines
2.3 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { cookies } from 'next/headers';
|
|
import { connectToDB } from '@/lib/db';
|
|
import { generateEmbedding } from '@/lib/ai';
|
|
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
|
|
|
/**
|
|
* 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 });
|
|
}
|
|
|
|
// Verify JWT to get user's DID
|
|
const userSession = verifySurrealJwt(surrealJwt);
|
|
if (!userSession) {
|
|
return NextResponse.json({ error: 'Invalid auth token' }, { status: 401 });
|
|
}
|
|
|
|
const { did: userDid } = userSession;
|
|
|
|
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 with root credentials
|
|
const db = await connectToDB();
|
|
|
|
// 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.
|
|
// We filter by user_did to ensure users only see their own nodes.
|
|
const query = `
|
|
SELECT
|
|
id,
|
|
title,
|
|
body,
|
|
atp_uri,
|
|
vector::similarity::cosine(embedding, $draft_embedding) AS score
|
|
FROM node
|
|
WHERE user_did = $user_did
|
|
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,
|
|
user_did: userDid,
|
|
});
|
|
|
|
// 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 }
|
|
);
|
|
}
|
|
}
|