Intelex / frontend /src /app /api.ts
yakub
feat: implement real-time source ingestion progress, chitchat intent classifier, and empty state fallback
fd1e711
Raw
History Blame Contribute Delete
20.1 kB
import { KnowledgeSource, RetrievedChunk, Citation, Language, LegalQueryResponse, ImageJob } from './types';
const DEFAULT_API_BASE = 'http://127.0.0.1:8000';
const API_BASE = (import.meta as any).env?.VITE_API_URL || DEFAULT_API_BASE;
// ── Types ────────────────────────────────────────────────────────────────────
type BackendSource = {
id: string;
type: 'pdf' | 'url' | 'youtube' | 'statute' | 'judgment' | 'constitution';
title: string;
origin?: string;
language?: string;
status?: KnowledgeSource['status'];
chunkCount?: number;
createdAt?: string;
dateAdded?: string;
doc_type?: string; // For legal sources
court?: string; // For legal sources
judgment_date?: string; // For legal sources
progress_percentage?: number;
error_message?: string;
};
export interface QueryResponse {
answer: string;
sources: Citation[];
retrievedChunks: RetrievedChunk[];
conversationId?: string; // Added for Phase 1
}
export interface Conversation {
id: string;
title: string;
conv_type?: 'general' | 'legal';
created_at: string;
updated_at: string;
message_count: number;
}
export interface ConversationMessage {
id: string;
question: string;
answer: string;
sourcesUsed: string[];
createdAt: string;
}
// ── Helper: Normalization ───────────────────────────────────────────────────
function normalizeSource(raw: BackendSource): KnowledgeSource {
// Map "url" β†’ "web" for frontend type consistency
let type: KnowledgeSource['type'] =
raw.type === 'url' ? 'web' :
(raw.type as KnowledgeSource['type']);
// If it's a legal type from Prompt 4, we might want to map it to 'pdf'
// or keep it if types.ts supports it. Assuming pdf for now if not 'web'/'youtube'.
if (['statute', 'judgment', 'constitution'].includes(raw.type)) {
type = 'pdf';
}
const language = ((raw.language || 'en').toUpperCase()) as Language;
const dateRaw = raw.createdAt || raw.dateAdded;
const dateAdded = dateRaw ? new Date(dateRaw) : new Date();
const metadata: KnowledgeSource['metadata'] = {};
if (raw.origin) {
if (type === 'web' || type === 'youtube') metadata.url = raw.origin;
if (raw.origin.match(/[?&]v=([^&]+)/)) {
const match = raw.origin.match(/[?&]v=([^&]+)/);
if (match) metadata.videoId = match[1];
}
}
// Add legal metadata if present
if (raw.doc_type) metadata.docType = raw.doc_type;
if (raw.court) metadata.court = raw.court;
if (raw.judgment_date) metadata.judgmentDate = raw.judgment_date;
return {
id: String(raw.id),
type: type || 'pdf',
title: raw.title,
language,
status: raw.status || 'completed',
chunkCount: raw.chunkCount ?? 0,
dateAdded,
progress_percentage: raw.progress_percentage ?? 0,
error_message: raw.error_message,
metadata,
};
}
// ── Sources API ──────────────────────────────────────────────────────────────
export async function fetchSources(): Promise<KnowledgeSource[]> {
const res = await fetch(`${API_BASE}/sources`);
if (!res.ok) throw new Error(`Failed to fetch sources: ${res.statusText}`);
const data = await res.json();
const raw: BackendSource[] = Array.isArray(data) ? data : (data.sources ?? []);
return raw.map(normalizeSource);
}
export async function fetchLegalSources(): Promise<KnowledgeSource[]> {
const res = await fetch(`${API_BASE}/legal/legal-sources`);
if (!res.ok) throw new Error(`Failed to fetch legal sources: ${res.statusText}`);
const data = await res.json();
const raw: BackendSource[] = data.sources ?? [];
return raw.map(normalizeSource);
}
export async function deleteSource(id: string): Promise<void> {
const res = await fetch(`${API_BASE}/sources/${encodeURIComponent(id)}`, {
method: 'DELETE',
});
if (!res.ok) {
const detail = await res.text().catch(() => '');
throw new Error(detail || `Delete failed with status ${res.status}`);
}
}
// ── Ingestion API ────────────────────────────────────────────────────────────
export async function uploadPdf(file: File): Promise<KnowledgeSource> {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${API_BASE}/upload-pdf`, {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(await res.text() || 'PDF Upload failed');
const data = await res.json();
return normalizeSource({
id: data.source_id,
type: 'pdf',
title: data.title,
chunkCount: data.chunk_count,
status: data.status || 'UPLOADING',
createdAt: new Date().toISOString(),
});
}
export async function fetchSourceProgress(sourceId: string): Promise<KnowledgeSource & { progress_percentage: number; error_message?: string }> {
const res = await fetch(`${API_BASE}/sources/${encodeURIComponent(sourceId)}`);
if (!res.ok) throw new Error(`Failed to fetch source progress: ${res.statusText}`);
const data = await res.json();
return {
...normalizeSource(data),
progress_percentage: data.progress_percentage ?? 0,
error_message: data.error_message,
};
}
export async function uploadLegal(file: File, docType: string = "judgment"): Promise<KnowledgeSource> {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${API_BASE}/legal/upload-legal?doc_type=${docType}`, {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(await res.text() || 'Legal Upload failed');
const data = await res.json();
return normalizeSource({
id: data.source_id,
type: data.doc_type as any || 'pdf',
title: data.title,
chunkCount: data.chunk_count,
status: 'completed',
createdAt: new Date().toISOString(),
doc_type: data.doc_type
});
}
export async function addWebsite(url: string, language: Language = 'EN'): Promise<KnowledgeSource> {
const res = await fetch(`${API_BASE}/add-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, language: language.toLowerCase() }),
});
if (!res.ok) throw new Error(await res.text() || 'URL Ingestion failed');
const data = await res.json();
return normalizeSource({
id: data.source_id,
type: 'url',
title: data.title,
chunkCount: data.chunk_count,
status: data.status || 'EXTRACTING',
origin: url,
});
}
export async function addYouTube(url: string, language: Language = 'EN'): Promise<KnowledgeSource> {
const res = await fetch(`${API_BASE}/add-youtube`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, language: language.toLowerCase() }),
});
if (!res.ok) throw new Error(await res.text() || 'YouTube Ingestion failed');
const data = await res.json();
return normalizeSource({
id: data.source_id,
type: 'youtube',
title: data.title,
chunkCount: data.chunk_count,
status: data.status || 'EXTRACTING',
origin: url,
});
}
// ── Image API ────────────────────────────────────────────────────────────────
export async function uploadImage(file: File, context: string = ""): Promise<{ image_id: string, status: string }> {
const formData = new FormData();
formData.append('file', file);
formData.append('context', context);
const res = await fetch(`${API_BASE}/images/upload-image`, {
method: 'POST',
body: formData,
});
if (!res.ok) throw new Error(await res.text() || 'Image Upload failed');
return await res.json();
}
export async function fetchImageJobs(): Promise<ImageJob[]> {
const res = await fetch(`${API_BASE}/images/image-jobs`);
if (!res.ok) throw new Error(`Failed to fetch image jobs: ${res.statusText}`);
const data = await res.json();
return data.jobs ?? [];
}
export async function fetchPendingImageCount(): Promise<number> {
const res = await fetch(`${API_BASE}/images/image-jobs/pending-count`);
if (!res.ok) throw new Error(`Failed to fetch pending count: ${res.statusText}`);
const data = await res.json();
return data.count ?? 0;
}
// ── Query API ───────────────────────────────────────────────────────────────
export async function queryRag(
question: string,
sourceIds?: string[],
imageId?: string,
includeImages?: boolean,
llmProvider: string = "groq"
): Promise<QueryResponse> {
const res = await fetch(`${API_BASE}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question,
source_ids: sourceIds && sourceIds.length > 0 ? sourceIds : null,
conversation_id: (window as any).currentConversationId || null,
image_id: imageId || null,
include_images: includeImages || false,
llm_provider: llmProvider,
}),
});
if (!res.ok) throw new Error(await res.text() || 'Query failed');
const data = await res.json();
return {
answer: data.answer,
conversationId: data.conversationId, // Added for Phase 1
sources: (data.citations || []).map((c: any) => ({
sourceTitle: c.sourceTitle || c.source_title || '',
sourceType: c.sourceType === 'url' ? 'web' : (c.sourceType || 'pdf'),
reference: c.reference || '',
snippet: c.snippet || '',
})),
retrievedChunks: (data.retrievedChunks || []).map((c: any) => ({
id: c.chunkId || c.id || '',
sourceId: c.sourceId || '',
sourceName: c.sourceTitle || c.sourceName || '',
sourceType: c.sourceType === 'url' ? 'web' : (c.sourceType || 'pdf'),
language: ((c.language || 'en').toUpperCase()) as Language,
text: c.text || '',
similarityScore: c.score ?? c.similarityScore ?? 0,
metadata: {
page: c.pageNumber ?? c.metadata?.page,
timestamp: c.timestampS != null
? `${Math.floor(c.timestampS / 60)}:${String(c.timestampS % 60).padStart(2, '0')}`
: c.metadata?.timestamp,
url: c.urlRef || c.metadata?.url,
ipcSections: c.metadata?.ipc_sections || c.ipc_sections || [],
},
})),
};
}
// ── Streaming API ───────────────────────────────────────────────────────────
export async function streamQueryRag(
question: string,
sourceIds: string[] | undefined,
history: Array<{ role: string; content: string }>,
onToken: (token: string) => void,
onMeta: (meta: { chatId: string; conversationId?: string; citations: Citation[]; retrievedChunks: RetrievedChunk[] }) => void,
onError: (err: Error) => void,
imageId?: string,
includeImages?: boolean,
llmProvider: string = "groq",
isLegalMode: boolean = false,
legalFilter: string | null = null,
agenticMode: boolean = false,
onAgentStatus?: (status: { stage: number; message: string }) => void
): Promise<void> {
let response: Response;
try {
response = await fetch(`${API_BASE}/query-stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question,
source_ids: sourceIds && sourceIds.length > 0 ? sourceIds : null,
history,
conversation_id: (window as any).currentConversationId || null,
image_id: imageId || null,
include_images: includeImages || false,
llm_provider: llmProvider,
is_legal_mode: isLegalMode,
legal_filter: legalFilter,
agentic_mode: agenticMode,
}),
});
} catch (err) {
if (err instanceof TypeError && err.message.includes('fetch')) {
onError(new Error("Cannot connect to server. Is the backend running on port 8000?"));
} else {
onError(err instanceof Error ? err : new Error(String(err)));
}
return;
}
if (!response.ok) {
onError(new Error(`Server returned ${response.status}`));
return;
}
if (!response.body) {
onError(new Error('No response body from server'));
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
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;
const jsonStr = line.slice(6).trim();
if (!jsonStr) continue;
try {
const parsed = JSON.parse(jsonStr);
if (parsed.type === 'error') {
onError(new Error(parsed.message || "Unknown streaming error"));
return;
}
if (parsed.type === 'agent_status') {
if (onAgentStatus) {
onAgentStatus({ stage: parsed.stage, message: parsed.message });
}
}
if (parsed.type === 'meta') {
const citations: Citation[] = (parsed.citations || []).map((c: any) => ({
sourceTitle: c.sourceTitle || c.source_title || '',
sourceType: (c.sourceType === 'url' ? 'web' : c.sourceType || 'pdf') as any,
reference: c.reference || '',
snippet: c.snippet || '',
}));
const retrievedChunks: RetrievedChunk[] = (parsed.retrievedChunks || []).map((c: any, i: number) => ({
id: c.chunkId || String(i),
sourceId: c.sourceId || '',
sourceName: c.sourceTitle || '',
sourceType: (c.sourceType === 'url' ? 'web' : c.sourceType || 'pdf') as any,
text: c.text || '',
similarityScore: c.score || 0,
metadata: { page: c.pageNumber, timestamp: c.timestampS, url: c.urlRef }
}));
onMeta({
chatId: parsed.chatId || '',
conversationId: parsed.conversationId,
citations,
retrievedChunks
});
}
if (parsed.type === 'token' && parsed.content) {
onToken(parsed.content);
}
if (parsed.type === 'done') {
return;
}
} catch (e) {
// Partial JSON or parse error β€” skip
}
}
}
} catch (err) {
onError(err instanceof Error ? err : new Error(String(err)));
}
}
// ── Evaluation API ───────────────────────────────────────────────────────────
export async function startEvaluation(nQuestions: number = 15, agenticMode: boolean = false): Promise<string> {
const response = await fetch(`${API_BASE}/eval/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
n_questions: nQuestions,
agentic_mode: agenticMode
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to start evaluation');
}
const data = await response.json();
return data.job_id;
}
export async function getEvaluationStatus(jobId: string): Promise<any> {
const response = await fetch(`${API_BASE}/eval/status/${jobId}`);
if (!response.ok) {
throw new Error('Failed to fetch evaluation status');
}
return response.json();
}
// ── History API ─────────────────────────────────────────────────────────────
export interface BackendChatEntry {
id: string;
question: string;
answer: string;
sourcesUsed: string[];
createdAt: string;
}
export async function fetchHistory(): Promise<BackendChatEntry[]> {
const res = await fetch(`${API_BASE}/history`);
if (!res.ok) throw new Error(`Failed to fetch history: ${res.statusText}`);
const data = await res.json();
return data.history ?? [];
}
export async function clearHistory(): Promise<void> {
const res = await fetch(`${API_BASE}/history`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed to clear history');
}
// ── Events ──────────────────────────────────────────────────────────────────
export function notifySidebarRefresh() {
window.dispatchEvent(new CustomEvent('sources-updated'));
}
export async function queryLegal(question: string, sourceFilter?: string, modelType: "finetuned" | "base" = "finetuned"): Promise<LegalQueryResponse> {
const res = await fetch(`${API_BASE}/legal-query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question,
source_filter: sourceFilter || null,
language: "en",
model_type: modelType
})
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to query legal search: ${text}`);
}
return res.json();
}
// ── Conversations API ────────────────────────────────────────────────────────
export async function createConversation(title = 'New Chat'): Promise<Conversation> {
const res = await fetch(`${API_BASE}/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (!res.ok) throw new Error('Failed to create conversation');
return res.json();
}
export async function fetchConversations(): Promise<Conversation[]> {
const res = await fetch(`${API_BASE}/conversations`);
if (!res.ok) throw new Error('Failed to fetch conversations');
const data = await res.json();
return data.conversations ?? [];
}
export async function fetchConversationMessages(convId: string): Promise<{
conversation: { id: string; title: string };
messages: ConversationMessage[];
}> {
const res = await fetch(`${API_BASE}/conversations/${convId}/messages`);
if (!res.ok) throw new Error('Failed to fetch messages');
return res.json();
}
export async function renameConversation(convId: string, title: string): Promise<void> {
await fetch(`${API_BASE}/conversations/${convId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
}
export async function deleteConversation(convId: string): Promise<void> {
await fetch(`${API_BASE}/conversations/${convId}`, { method: 'DELETE' });
}
// ── Export API ──────────────────────────────────────────────────────────────
export async function exportToPDF(title: string, content: string, citations: any[]): Promise<void> {
const response = await fetch(`${API_BASE}/export/pdf`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, citations }),
});
if (!response.ok) throw new Error('Failed to export PDF');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `InteleX_Research_${new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}