36 KiB
🚀 Backend API Documentation - Complete Reference
Project: Enterprise AI Hub "Nexus" Backend Status: ✅ COMPLETE Frontend Target: Next.js 14+ (App Router) Date: 2026-02-12
📋 Table of Contents
- Overview
- Getting Started
- Authentication
- API Endpoints
- Data Models
- Server-Sent Events (SSE)
- Error Handling
- Code Examples
🎯 Overview
Architecture
┌─────────────────────┐
│ Next.js Frontend │
│ (localhost:3000) │
└──────────┬──────────┘
│ HTTP/SSE
▼
┌─────────────────────────────────┐
│ FastAPI Backend │
│ (localhost:8000/api/v1) │
│ │
│ ┌─────────────────────────┐ │
│ │ Authentication │ │
│ │ - Microsoft Entra ID │ │
│ │ - JWT (15min) + Refresh │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ Three Modes │ │
│ │ - RAG (SharePoint docs) │ │
│ │ - Assistant (Tools) │ │
│ │ - Notebook (Analysis) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
Base URL
http://localhost:8000/api/v1
Production: Replace with your production domain
Three Operating Modes
- RAG Mode: Query corporate knowledge base (SharePoint documents)
- Assistant Mode: AI productivity tools (summarization, transcription, translation)
- Notebook Mode: Isolated document analysis via NotebookLlama integration
🚀 Getting Started
Frontend Dependencies
npm install axios
# or
npm install @tanstack/react-query
Environment Variables
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
NEXT_PUBLIC_ENTRA_CLIENT_ID=your_client_id
NEXT_PUBLIC_ENTRA_TENANT_ID=your_tenant_id
API Client Setup
// lib/api-client.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor: Add JWT token
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor: Handle token refresh
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
try {
const { data } = await axios.post(
`${apiClient.defaults.baseURL}/auth/refresh`,
{ refresh_token: refreshToken }
);
localStorage.setItem('access_token', data.access_token);
// Retry original request
error.config.headers.Authorization = `Bearer ${data.access_token}`;
return apiClient.request(error.config);
} catch {
// Refresh failed, redirect to login
localStorage.clear();
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
export default apiClient;
🔐 Authentication
Overview
Flow: Microsoft Entra ID (OAuth) → Backend JWT
- Frontend redirects to Microsoft login
- Microsoft returns authorization code
- Frontend sends code to backend
- Backend exchanges code for user profile
- Backend returns JWT tokens (access + refresh)
- Frontend stores tokens (localStorage + httpOnly cookie)
Token Strategy
| Token Type | Storage | Expiry | Purpose |
|---|---|---|---|
| Access Token | localStorage |
15 minutes | API authentication |
| Refresh Token | httpOnly Cookie or localStorage |
7 days | Token renewal |
📡 API Endpoints
Auth Endpoints
1. POST /auth/login
Exchange Microsoft authorization code for JWT tokens.
Request:
POST /api/v1/auth/login
Content-Type: application/json
{
"code": "authorization_code_from_microsoft"
}
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@company.com",
"display_name": "John Doe",
"role": "user",
"department_id": "123e4567-e89b-12d3-a456-426614174000"
}
}
Frontend Implementation:
// lib/auth.ts
import apiClient from './api-client';
export async function loginWithMicrosoft(code: string) {
const { data } = await apiClient.post('/auth/login', { code });
// Store tokens
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
localStorage.setItem('user', JSON.stringify(data.user));
return data.user;
}
2. POST /auth/refresh
Refresh expired access token using refresh token.
Request:
POST /api/v1/auth/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Errors:
401 Unauthorized: Invalid or expired refresh token (redirect to login)
3. GET /auth/me
Get current user profile (requires authentication).
Request:
GET /api/v1/auth/me
Authorization: Bearer {access_token}
Response (200 OK):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@company.com",
"display_name": "John Doe",
"role": "user",
"department_id": "123e4567-e89b-12d3-a456-426614174000",
"created_at": "2024-01-15T10:30:00Z",
"last_login_at": "2024-02-12T14:22:00Z"
}
RAG Mode Endpoints
Query corporate knowledge base (SharePoint documents) with citations.
1. POST /chat/chat
Stream RAG response with citations (Server-Sent Events).
Request:
POST /api/v1/chat/chat
Authorization: Bearer {access_token}
Content-Type: application/json
{
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
"message": "What is our vacation policy?",
"mode": "rag"
}
Response (SSE Stream):
data: {"token": "According"}
data: {"token": " to"}
data: {"token": " company"}
data: {"token": " policy"}
data: {"token": " [1]"}
data: {"token": ","}
data: {"token": " employees"}
data: {"token": " receive"}
data: {"token": " 20"}
data: {"token": " days"}
data: {"token": " of"}
data: {"token": " annual"}
data: {"token": " leave"}
data: {"token": " [2]"}
data: {"token": "."}
data: {"sources": [{"title": "HR Policy 2024", "url": "https://sharepoint.com/...", "score": 0.92}, {"title": "Benefits Guide", "url": "https://sharepoint.com/...", "score": 0.87}]}
data: {"done": true, "message_id": "abc123"}
Frontend Implementation:
// hooks/useRAGChat.ts
import { useState, useEffect } from 'react';
export function useRAGChat(conversationId: string, message: string) {
const [response, setResponse] = useState('');
const [sources, setSources] = useState([]);
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
const token = localStorage.getItem('access_token');
const eventSource = new EventSource(
`${process.env.NEXT_PUBLIC_API_URL}/chat/chat?` +
`conversation_id=${conversationId}&message=${encodeURIComponent(message)}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
setIsStreaming(true);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.token) {
setResponse((prev) => prev + data.token);
} else if (data.sources) {
setSources(data.sources);
} else if (data.done) {
setIsStreaming(false);
eventSource.close();
} else if (data.error) {
console.error('Stream error:', data.error);
setIsStreaming(false);
eventSource.close();
}
};
eventSource.onerror = () => {
setIsStreaming(false);
eventSource.close();
};
return () => {
eventSource.close();
};
}, [conversationId, message]);
return { response, sources, isStreaming };
}
2. GET /chat/conversations
List user's conversations.
Request:
GET /api/v1/chat/conversations?mode=rag&limit=20
Authorization: Bearer {access_token}
Query Parameters:
mode(optional): Filter by mode (rag,assistant,notebook)limit(optional): Max results (default: 20)
Response (200 OK):
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mode": "rag",
"title": "Vacation Policy Question",
"created_at": "2024-02-12T10:00:00Z",
"updated_at": "2024-02-12T10:05:00Z",
"message_count": 4
},
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"mode": "rag",
"title": "Benefits Information",
"created_at": "2024-02-11T14:30:00Z",
"updated_at": "2024-02-11T14:35:00Z",
"message_count": 6
}
]
3. GET /chat/conversations/{conversation_id}/messages
Get message history for a conversation.
Request:
GET /api/v1/chat/conversations/{conversation_id}/messages
Authorization: Bearer {access_token}
Response (200 OK):
{
"conversation_id": "550e8400-e29b-41d4-a716-446655440000",
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "What is our vacation policy?",
"created_at": "2024-02-12T10:00:00Z"
},
{
"id": "msg-002",
"role": "assistant",
"content": "According to company policy [1], employees receive 20 days of annual leave [2].",
"sources": [
{
"title": "HR Policy 2024",
"url": "https://sharepoint.com/sites/hr/HR_Policy_2024.pdf",
"score": 0.92
},
{
"title": "Benefits Guide",
"url": "https://sharepoint.com/sites/hr/Benefits_Guide.docx",
"score": 0.87
}
],
"input_tokens": 156,
"output_tokens": 234,
"llm_model": "gpt-5.1",
"created_at": "2024-02-12T10:00:15Z"
}
]
}
4. POST /chat/conversations
Create a new conversation.
Request:
POST /api/v1/chat/conversations
Authorization: Bearer {access_token}
Content-Type: application/json
{
"mode": "rag",
"title": "Benefits Questions"
}
Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mode": "rag",
"title": "Benefits Questions",
"created_at": "2024-02-12T10:00:00Z",
"updated_at": "2024-02-12T10:00:00Z"
}
5. DELETE /chat/conversations/{conversation_id}
Delete a conversation and all its messages.
Request:
DELETE /api/v1/chat/conversations/{conversation_id}
Authorization: Bearer {access_token}
Response (200 OK):
{
"status": "deleted",
"conversation_id": "550e8400-e29b-41d4-a716-446655440000"
}
Notebook Mode Endpoints
Isolated document analysis with file uploads and chat.
1. POST /notebook/create
Create a new notebook session.
Request:
POST /api/v1/notebook/create
Authorization: Bearer {access_token}
Content-Type: application/json
{
"title": "Q4 Financial Analysis"
}
Response (201 Created):
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"conversation_id": "660e8400-e29b-41d4-a716-446655440001",
"notebookllama_notebook_id": 123,
"title": "Q4 Financial Analysis",
"is_pinned": false,
"expires_at": "2024-02-13T10:00:00Z",
"created_at": "2024-02-12T10:00:00Z"
}
Notes:
- Sessions expire after 24 hours by default
- Use pin/unpin endpoints to prevent expiration
notebookllama_notebook_idis the external notebook ID
2. POST /notebook/{session_id}/upload
Upload a file to a notebook session.
Request:
POST /api/v1/notebook/{session_id}/upload?wait_for_completion=false
Authorization: Bearer {access_token}
Content-Type: multipart/form-data
file: <binary data>
Query Parameters:
wait_for_completion(optional, default: false): If true, wait for processing to complete
Response (200 OK - Immediate):
{
"file_id": "abc123-def456-ghi789",
"file_name": "Q4_Report.pdf",
"file_size": 2048576,
"task_id": 456,
"processing_status": "queued",
"message": "File uploaded, processing in background"
}
Response (200 OK - Wait for completion):
{
"file_id": "abc123-def456-ghi789",
"file_name": "Q4_Report.pdf",
"file_size": 2048576,
"task_id": 456,
"processing_status": "completed",
"message": "File uploaded and processed successfully"
}
Supported File Types:
- Documents:
.pdf,.docx,.doc,.pptx,.ppt,.txt,.md,.rtf,.epub - Spreadsheets:
.xlsx,.xls,.csv,.tsv - Images (OCR):
.jpg,.jpeg,.png,.gif,.bmp,.tiff - Web:
.html,.htm - Audio:
.mp3,.m4a,.wav(20MB limit) - Video:
.mp4,.mov,.avi(2GB/55min limits)
Frontend Implementation:
// components/FileUpload.tsx
async function uploadFile(sessionId: string, file: File) {
const formData = new FormData();
formData.append('file', file);
const token = localStorage.getItem('access_token');
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/notebook/${sessionId}/upload`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
}
);
return response.json();
}
3. GET /notebook/{session_id}/files/{file_id}/status
Check file processing status.
Request:
GET /api/v1/notebook/{session_id}/files/{file_id}/status
Authorization: Bearer {access_token}
Response (200 OK):
{
"file_id": "abc123-def456-ghi789",
"file_name": "Q4_Report.pdf",
"processing_status": "completed",
"task_id": 456,
"created_at": "2024-02-12T10:00:00Z",
"completed_at": "2024-02-12T10:00:45Z",
"error_message": null
}
Processing Status Values:
queued: File uploaded, waiting for processingprocessing: Currently being processedcompleted: Processing finished successfullyfailed: Processing failed (seeerror_message)
4. POST /notebook/{session_id}/chat
Stream chat response about uploaded documents (SSE).
Request:
POST /api/v1/notebook/{session_id}/chat?message=Summarize the Q4 report
Authorization: Bearer {access_token}
Query Parameters:
message(required): User's question
Response (SSE Stream):
data: {"token": "The"}
data: {"token": " Q4"}
data: {"token": " report"}
data: {"token": " shows"}
data: {"token": " revenue"}
data: {"token": " of"}
data: {"token": " $2.5M"}
data: {"token": "..."}
data: {"sources": "## Sources\n- Q4_Report.pdf (page 3)\n- Budget_Analysis.xlsx (Sheet 2)"}
data: {"done": true}
Frontend Implementation:
// hooks/useNotebookChat.ts
export function useNotebookChat(sessionId: string, message: string) {
const [response, setResponse] = useState('');
const [sources, setSources] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
useEffect(() => {
const token = localStorage.getItem('access_token');
const eventSource = new EventSource(
`${process.env.NEXT_PUBLIC_API_URL}/notebook/${sessionId}/chat?` +
`message=${encodeURIComponent(message)}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
setIsStreaming(true);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.token) {
setResponse((prev) => prev + data.token);
} else if (data.sources) {
setSources(data.sources);
} else if (data.done) {
setIsStreaming(false);
eventSource.close();
}
};
return () => eventSource.close();
}, [sessionId, message]);
return { response, sources, isStreaming };
}
5. GET /notebook/{session_id}
Get notebook session details with uploaded files.
Request:
GET /api/v1/notebook/{session_id}
Authorization: Bearer {access_token}
Response (200 OK):
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"conversation_id": "660e8400-e29b-41d4-a716-446655440001",
"notebookllama_notebook_id": 123,
"title": "Q4 Financial Analysis",
"is_pinned": false,
"total_file_size": 4096000,
"expires_at": "2024-02-13T10:00:00Z",
"created_at": "2024-02-12T10:00:00Z",
"files": [
{
"file_id": "abc123-def456-ghi789",
"file_name": "Q4_Report.pdf",
"file_size": 2048576,
"file_type": "pdf",
"processing_status": "completed",
"processing_error": null,
"uploaded_at": "2024-02-12T10:05:00Z",
"processed_at": "2024-02-12T10:05:45Z"
},
{
"file_id": "xyz789-uvw456-rst123",
"file_name": "Budget.xlsx",
"file_size": 1048576,
"file_type": "xlsx",
"processing_status": "completed",
"processing_error": null,
"uploaded_at": "2024-02-12T10:08:00Z",
"processed_at": "2024-02-12T10:08:30Z"
}
]
}
6. POST /notebook/{session_id}/pin
Pin session to prevent expiration.
Request:
POST /api/v1/notebook/{session_id}/pin
Authorization: Bearer {access_token}
Response (200 OK):
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"is_pinned": true,
"expires_at": null
}
7. POST /notebook/{session_id}/unpin
Unpin session and reset 24h expiration.
Request:
POST /api/v1/notebook/{session_id}/unpin
Authorization: Bearer {access_token}
Response (200 OK):
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"is_pinned": false,
"expires_at": "2024-02-13T14:00:00Z"
}
8. DELETE /notebook/{session_id}
Delete notebook session and all files.
Request:
DELETE /api/v1/notebook/{session_id}
Authorization: Bearer {access_token}
Response (200 OK):
{
"status": "deleted",
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
Cleanup Performed:
- External notebook deleted from NotebookLlama
- Local files removed from server
- Database records deleted (cascades)
📦 Data Models
User
interface User {
id: string; // UUID
email: string;
display_name: string;
role: 'user' | 'content_manager' | 'super_admin';
department_id: string | null; // UUID
created_at: string; // ISO 8601
last_login_at: string; // ISO 8601
}
Conversation
interface Conversation {
id: string; // UUID
mode: 'rag' | 'assistant' | 'notebook';
title: string;
created_at: string; // ISO 8601
updated_at: string; // ISO 8601
message_count?: number;
}
Message
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
sources?: Source[]; // Only for RAG mode
input_tokens?: number;
output_tokens?: number;
llm_model?: string;
created_at: string; // ISO 8601
}
Source (RAG Citations)
interface Source {
title: string; // Document title
url: string; // SharePoint URL
score: number; // Relevance score (0-1)
}
Notebook Session
interface NotebookSession {
session_id: string; // UUID
conversation_id: string; // UUID
notebookllama_notebook_id: number; // External notebook ID
title: string;
is_pinned: boolean;
total_file_size: number; // Bytes
expires_at: string | null; // ISO 8601, null if pinned
created_at: string; // ISO 8601
files: UploadedFile[];
}
Uploaded File
interface UploadedFile {
file_id: string; // UUID
file_name: string;
file_size: number; // Bytes
file_type: string; // Extension without dot
processing_status: 'queued' | 'processing' | 'completed' | 'failed';
processing_error: string | null;
uploaded_at: string; // ISO 8601
processed_at: string | null; // ISO 8601
}
🌊 Server-Sent Events (SSE)
Both RAG and Notebook chat endpoints use Server-Sent Events for streaming responses.
SSE Event Format
All events follow this format:
data: <JSON object>\n\n
Event Types
Token Event (Text Streaming)
{"token": "Hello"}
Append this token to the response text.
Sources Event (RAG Citations)
{
"sources": [
{"title": "Document.pdf", "url": "https://...", "score": 0.92}
]
}
Display citations after the response.
Sources Event (Notebook - Markdown)
{"sources": "## Sources\n- Document.pdf (page 3)\n- Spreadsheet.xlsx (Sheet 2)"}
Display as formatted markdown.
Completion Event
{"done": true, "message_id": "abc123"}
Stream has finished, close connection.
Error Event
{"error": "Error message here"}
Display error, close connection.
Frontend SSE Example
// hooks/useStreamingChat.ts
import { useState, useEffect } from 'react';
interface StreamingResponse {
response: string;
sources: any;
isStreaming: boolean;
error: string | null;
}
export function useStreamingChat(
endpoint: string,
params: Record<string, string>
): StreamingResponse {
const [response, setResponse] = useState('');
const [sources, setSources] = useState<any>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const token = localStorage.getItem('access_token');
const queryString = new URLSearchParams(params).toString();
const eventSource = new EventSource(
`${process.env.NEXT_PUBLIC_API_URL}${endpoint}?${queryString}`,
{
headers: { Authorization: `Bearer ${token}` }
}
);
setIsStreaming(true);
setResponse('');
setSources(null);
setError(null);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.token) {
setResponse((prev) => prev + data.token);
} else if (data.sources) {
setSources(data.sources);
} else if (data.done) {
setIsStreaming(false);
eventSource.close();
} else if (data.error) {
setError(data.error);
setIsStreaming(false);
eventSource.close();
}
} catch (err) {
console.error('Failed to parse SSE event:', err);
}
};
eventSource.onerror = (err) => {
console.error('SSE connection error:', err);
setError('Connection lost');
setIsStreaming(false);
eventSource.close();
};
return () => {
eventSource.close();
};
}, [endpoint, JSON.stringify(params)]);
return { response, sources, isStreaming, error };
}
⚠️ Error Handling
HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Resource created |
| 400 | Bad Request | Invalid input, show error message |
| 401 | Unauthorized | Token expired, refresh or redirect to login |
| 403 | Forbidden | Insufficient permissions, show access denied |
| 404 | Not Found | Resource doesn't exist |
| 410 | Gone | Session expired (notebook mode) |
| 413 | Payload Too Large | File size limit exceeded |
| 500 | Internal Server Error | Backend error, show retry option |
| 502 | Bad Gateway | NotebookLlama unavailable (notebook mode) |
| 503 | Service Unavailable | External service down |
Error Response Format
{
"detail": "Human-readable error message"
}
Example:
{
"detail": "Session has expired"
}
Frontend Error Handler
// lib/error-handler.ts
export function handleAPIError(error: any): string {
if (error.response) {
const status = error.response.status;
const detail = error.response.data?.detail;
switch (status) {
case 401:
// Token expired, handled by interceptor
return 'Authentication expired, please log in again';
case 403:
return 'You do not have permission to perform this action';
case 404:
return 'Resource not found';
case 410:
return 'This session has expired';
case 413:
return 'File size exceeds the maximum allowed (100MB)';
case 502:
return 'External service temporarily unavailable';
default:
return detail || 'An unexpected error occurred';
}
} else if (error.request) {
return 'Unable to reach the server. Please check your connection.';
} else {
return error.message || 'An error occurred';
}
}
💻 Code Examples
Complete Chat Component (RAG Mode)
// app/chat/page.tsx
'use client';
import { useState } from 'react';
import { useStreamingChat } from '@/hooks/useStreamingChat';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
export default function ChatPage() {
const [message, setMessage] = useState('');
const [conversationId, setConversationId] = useState<string | null>(null);
const [shouldStream, setShouldStream] = useState(false);
const { response, sources, isStreaming } = useStreamingChat(
shouldStream ? '/chat/chat' : null,
{
conversation_id: conversationId || '',
message: message,
mode: 'rag'
}
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim()) return;
// Create conversation if needed
if (!conversationId) {
// TODO: Call POST /chat/conversations
// For now, use existing conversation ID
}
setShouldStream(true);
};
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
<div className="max-w-3xl mx-auto space-y-4">
{/* User message */}
<div className="text-right">
<div className="inline-block bg-blue-500 text-white rounded-lg px-4 py-2">
{message}
</div>
</div>
{/* Assistant response */}
{(response || isStreaming) && (
<div className="text-left">
<div className="inline-block bg-gray-100 rounded-lg px-4 py-2">
<div className="prose">
{response}
{isStreaming && <span className="animate-pulse">▊</span>}
</div>
{/* Sources */}
{sources && sources.length > 0 && (
<div className="mt-4 pt-4 border-t">
<div className="text-sm font-semibold mb-2">Sources:</div>
<ul className="text-sm space-y-1">
{sources.map((source: any, i: number) => (
<li key={i}>
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
[{i + 1}] {source.title}
</a>
<span className="text-gray-500 ml-2">
(Score: {source.score.toFixed(2)})
</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
</div>
</div>
{/* Input */}
<div className="border-t p-4">
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto flex gap-2">
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Ask about company policies, benefits, procedures..."
disabled={isStreaming}
className="flex-1"
/>
<Button type="submit" disabled={isStreaming || !message.trim()}>
{isStreaming ? 'Streaming...' : 'Send'}
</Button>
</form>
</div>
</div>
);
}
Complete File Upload Component (Notebook Mode)
// components/FileUpload.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import apiClient from '@/lib/api-client';
interface Props {
sessionId: string;
onUploadComplete: () => void;
}
export function FileUpload({ sessionId, onUploadComplete }: Props) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate file size (100MB)
if (file.size > 100 * 1024 * 1024) {
setError('File size exceeds 100MB limit');
return;
}
setUploading(true);
setError(null);
setProgress(0);
try {
const formData = new FormData();
formData.append('file', file);
const token = localStorage.getItem('access_token');
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/notebook/${sessionId}/upload`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Upload failed');
}
const data = await response.json();
console.log('Upload result:', data);
// Poll for completion
if (data.processing_status === 'queued' || data.processing_status === 'processing') {
await pollProcessingStatus(data.file_id);
}
onUploadComplete();
} catch (err: any) {
setError(err.message);
} finally {
setUploading(false);
setProgress(0);
}
};
const pollProcessingStatus = async (fileId: string) => {
const maxAttempts = 60; // 2 minutes max
let attempts = 0;
while (attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 second delay
try {
const { data } = await apiClient.get(
`/notebook/${sessionId}/files/${fileId}/status`
);
setProgress(((attempts + 1) / maxAttempts) * 100);
if (data.processing_status === 'completed') {
setProgress(100);
return;
} else if (data.processing_status === 'failed') {
throw new Error(data.processing_error || 'Processing failed');
}
} catch (err: any) {
console.error('Polling error:', err);
}
attempts++;
}
throw new Error('Processing timeout');
};
return (
<div className="space-y-4">
<div>
<input
type="file"
onChange={handleFileChange}
disabled={uploading}
className="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100
disabled:opacity-50"
/>
</div>
{uploading && (
<div className="space-y-2">
<div className="text-sm text-gray-600">
{progress < 100 ? 'Processing...' : 'Complete!'}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
{error && (
<div className="text-sm text-red-600">
Error: {error}
</div>
)}
</div>
);
}
🔒 Security Best Practices
1. Token Storage
DO:
- Store access token in
localStorage(short-lived, 15min) - Store refresh token in httpOnly cookie (if supported by backend)
DON'T:
- Store tokens in
sessionStorage(lost on tab close) - Store sensitive data in cookies without httpOnly flag
2. API Calls
DO:
- Always use HTTPS in production
- Include CSRF tokens if using cookies
- Validate all user input before sending
- Implement rate limiting on frontend
DON'T:
- Expose API keys in client-side code
- Trust client-side validation alone
3. Error Messages
DO:
- Show user-friendly error messages
- Log detailed errors to console (dev only)
- Hide sensitive information from users
DON'T:
- Expose internal error details to users
- Show database errors or stack traces
🎨 UI/UX Recommendations
RAG Mode
- Show sources prominently - Users trust responses with citations
- Link to original documents - Enable verification
- Highlight relevance scores - Help users assess quality
Notebook Mode
- Show upload progress - Large files take time
- Display processing status - Documents need processing before chat
- List uploaded files - Let users see what's in the session
- Pin/unpin toggle - Make expiration management obvious
- Show expiration countdown - Remind users sessions expire
General
- Streaming indicators - Animated cursor during streaming
- Error recovery - Retry buttons for failed requests
- Offline detection - Warn users when connection is lost
- Loading states - Skeleton loaders for better UX
🧪 Testing Endpoints
Using cURL
# Login (requires Microsoft auth code)
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"code": "auth_code_here"}'
# Get current user
curl -X GET http://localhost:8000/api/v1/auth/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Create notebook session
curl -X POST http://localhost:8000/api/v1/notebook/create \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Test Notebook"}'
# Upload file
curl -X POST http://localhost:8000/api/v1/notebook/SESSION_ID/upload \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-F "file=@document.pdf"
Using Swagger UI
Visit: http://localhost:8000/docs
Interactive API documentation with:
- Try-it-out functionality
- Request/response examples
- Schema definitions
📞 Support & Contact
Backend Issues
- Check backend logs:
docker-compose logs backend - Database issues:
docker-compose logs postgres - Redis issues:
docker-compose logs redis
Frontend Integration Help
- Review this document thoroughly
- Check Swagger docs at
/docs - Test endpoints with cURL first
✅ Implementation Checklist
Authentication
- Implement Microsoft Entra ID OAuth flow
- Store access token in localStorage
- Store refresh token (cookie or localStorage)
- Implement automatic token refresh
- Handle 401 errors (redirect to login)
- Clear tokens on logout
RAG Mode
- Create conversation endpoint integration
- Implement SSE streaming for chat
- Display citations with links
- Show relevance scores
- List conversation history
- Delete conversations
Notebook Mode
- Create session endpoint integration
- File upload with drag-and-drop
- Show upload progress bar
- Poll processing status
- Implement SSE streaming for chat
- Display uploaded files list
- Pin/unpin sessions
- Show expiration countdown
- Delete sessions
General
- Error handling for all API calls
- Loading states for async operations
- Responsive design (mobile-friendly)
- Accessibility (ARIA labels, keyboard nav)
- Dark mode support
- Internationalization (i18n) if needed
🎉 You're Ready!
The backend is 100% complete and ready for frontend integration. All endpoints are tested and documented. Good luck with the Next.js implementation! 🚀
Last Updated: 2026-02-12 Backend Version: 1.0.0 API Version: v1