You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

139 lines
3.8 KiB

/**
* lib/api.ts — API client for the RAG backend.
*/
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
export interface SearchResult {
document_id: string;
chunk_id: string;
title: string;
path: string;
content: string;
score: number;
tags: string[];
highlight?: string;
}
export interface SearchResponse {
results: SearchResult[];
total: number;
query_time_ms: number;
}
export interface Document {
id: string;
path: string;
title: string;
content: string;
frontmatter: Record<string, unknown>;
tags: string[];
aliases: string[];
word_count: number | null;
created_at: string;
updated_at: string;
indexed_at: string | null;
}
export interface StatsResponse {
total_documents: number;
total_chunks: number;
total_relations: number;
total_tags: number;
last_indexed: string | null;
embedding_model: string;
chat_model: string;
}
export interface TagCount {
tag: string;
count: number;
}
export interface GraphData {
nodes: { id: string; title: string; path: string; tags: string[]; word_count: number | null }[];
edges: { source: string; target: string; relation_type: string; label: string | null }[];
}
export async function search(query: string, tags?: string[], limit = 10): Promise<SearchResponse> {
const res = await fetch(`${API_BASE}/api/v1/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, tags, limit, hybrid: true }),
});
if (!res.ok) throw new Error(`Search failed: ${res.status}`);
return res.json();
}
export async function getDocument(id: string): Promise<Document> {
const res = await fetch(`${API_BASE}/api/v1/document/${id}`);
if (!res.ok) throw new Error(`Document not found: ${res.status}`);
return res.json();
}
export async function getStats(): Promise<StatsResponse> {
const res = await fetch(`${API_BASE}/api/v1/stats`);
if (!res.ok) throw new Error('Stats fetch failed');
return res.json();
}
export async function getTags(): Promise<TagCount[]> {
const res = await fetch(`${API_BASE}/api/v1/tags`);
if (!res.ok) throw new Error('Tags fetch failed');
return res.json();
}
export async function getGraph(limit = 150): Promise<GraphData> {
const res = await fetch(`${API_BASE}/api/v1/graph?limit=${limit}`);
if (!res.ok) throw new Error('Graph fetch failed');
return res.json();
}
export function streamChat(
message: string,
contextLimit = 5,
onToken: (token: string) => void,
onSources: (sources: { title: string; path: string; score: number }[]) => void,
onDone: () => void,
): () => void {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, context_limit: contextLimit, stream: true }),
signal: controller.signal,
});
if (!res.ok || !res.body) return;
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'token') onToken(data.token);
else if (data.type === 'sources') onSources(data.sources);
else if (data.type === 'done') onDone();
} catch {}
}
}
} catch (err: unknown) {
if ((err as Error)?.name !== 'AbortError') console.error('Stream error:', err);
}
})();
return () => controller.abort();
}

Powered by TurnKey Linux.