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
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();
|
|
}
|