diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..7dbf7eb
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 0000000..1c6facd
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/index.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
\ No newline at end of file
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0fca6f0
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..4268a30
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "axios": "^1.14.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.7.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-router-dom": "^7.14.0",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zustand": "^5.0.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@tailwindcss/postcss": "^4.2.2",
+ "@types/node": "^24.12.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "postcss": "^8.5.8",
+ "tailwindcss": "^3.4.19",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.58.0",
+ "vite": "^8.0.4"
+ }
+}
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
new file mode 100644
index 0000000..e99ebc2
--- /dev/null
+++ b/frontend/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..f90339d
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,184 @@
+.counter {
+ font-size: 16px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ color: var(--accent);
+ background: var(--accent-bg);
+ border: 2px solid transparent;
+ transition: border-color 0.3s;
+ margin-bottom: 24px;
+
+ &:hover {
+ border-color: var(--accent-border);
+ }
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+.hero {
+ position: relative;
+
+ .base,
+ .framework,
+ .vite {
+ inset-inline: 0;
+ margin: 0 auto;
+ }
+
+ .base {
+ width: 170px;
+ position: relative;
+ z-index: 0;
+ }
+
+ .framework,
+ .vite {
+ position: absolute;
+ }
+
+ .framework {
+ z-index: 1;
+ top: 34px;
+ height: 28px;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+ scale(1.4);
+ }
+
+ .vite {
+ z-index: 0;
+ top: 107px;
+ height: 26px;
+ width: auto;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+ scale(0.8);
+ }
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ margin-bottom: 16px;
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#docs {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ height: 18px;
+ }
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..f96ebbf
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,147 @@
+import { useState, useEffect } from 'react';
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import { Layout } from '@/components/layout/Layout';
+import { Login } from '@/pages/Login';
+import { Dashboard } from '@/pages/Dashboard';
+import { Documents } from '@/pages/Documents';
+import { Chat } from '@/pages/Chat';
+import { Settings } from '@/pages/Settings';
+import { useAuthStore } from '@/stores/authStore';
+import { useSettingsStore } from '@/stores/settingsStore';
+import { Loader2 } from 'lucide-react';
+
+// Protected Route Component
+function ProtectedRoute({ children }: { children: React.ReactNode }) {
+ const { isAuthenticated } = useAuthStore();
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+// Public Route Component (redirects to home if authenticated)
+function PublicRoute({ children }: { children: React.ReactNode }) {
+ const { isAuthenticated } = useAuthStore();
+
+ if (isAuthenticated) {
+ return ;
+ }
+
+ return <>{children}>;
+}
+
+// App Content Component
+function AppContent() {
+ const { theme } = useSettingsStore();
+
+ // Apply theme on mount
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove('light', 'dark');
+
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light';
+ root.classList.add(systemTheme);
+ } else {
+ root.classList.add(theme);
+ }
+ }, [theme]);
+
+ return (
+
+
+ {/* Public Routes */}
+
+
+
+ }
+ />
+
+ {/* Protected Routes */}
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ {/* Catch all - redirect based on auth status */}
+ } />
+
+
+ );
+}
+
+// Navigate to home based on auth
+function NavigateToHome() {
+ const { isAuthenticated } = useAuthStore();
+ return ;
+}
+
+// Main App Component
+function App() {
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ // Check for stored auth on mount
+ const checkAuth = async () => {
+ // Small delay to prevent flash
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ setIsLoading(false);
+ };
+
+ checkAuth();
+ }, []);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts
new file mode 100644
index 0000000..4168b23
--- /dev/null
+++ b/frontend/src/api/client.ts
@@ -0,0 +1,126 @@
+import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
+import { useAuthStore } from '@/stores/authStore';
+
+const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
+
+class ApiClient {
+ private client: AxiosInstance;
+
+ constructor() {
+ this.client = axios.create({
+ baseURL: `${API_BASE_URL}/api/v1`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Request interceptor to add auth headers
+ this.client.interceptors.request.use(
+ (config) => {
+ const { apiKey, token } = useAuthStore.getState();
+
+ if (apiKey) {
+ config.headers['X-API-Key'] = apiKey;
+ } else if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ return config;
+ },
+ (error) => Promise.reject(error)
+ );
+
+ // Response interceptor for error handling
+ this.client.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response?.status === 401) {
+ // Unauthorized - clear auth
+ useAuthStore.getState().logout();
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+ );
+ }
+
+ // Generic request method
+ async get(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await this.client.get(url, config);
+ return response.data;
+ }
+
+ async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise {
+ const response = await this.client.post(url, data, config);
+ return response.data;
+ }
+
+ async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise {
+ const response = await this.client.put(url, data, config);
+ return response.data;
+ }
+
+ async delete(url: string, config?: AxiosRequestConfig): Promise {
+ const response = await this.client.delete(url, config);
+ return response.data;
+ }
+
+ // Documents API
+ async uploadDocument(file: File): Promise<{ document_id: string; filename: string; status: string }> {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await this.client.post('/documents', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+ return response.data;
+ }
+
+ async getDocuments(): Promise {
+ return this.get('/documents');
+ }
+
+ async deleteDocument(id: string): Promise {
+ await this.delete(`/documents/${id}`);
+ }
+
+ // Chat/Query API
+ async query(prompt: string, provider?: string, model?: string): Promise<{ response: string; sources: Source[] }> {
+ return this.post('/query', { prompt, provider, model });
+ }
+
+ async chat(message: string, conversationId?: string): Promise<{ response: string; sources: Source[] }> {
+ return this.post('/chat', { message, conversation_id: conversationId });
+ }
+
+ // Providers API
+ async getProviders(): Promise {
+ return this.get('/providers');
+ }
+
+ async getProviderModels(providerId: string): Promise<{ provider: string; models: Model[] }> {
+ return this.get(`/providers/${providerId}/models`);
+ }
+
+ async getConfig(): Promise {
+ return this.get('/config');
+ }
+
+ async updateDefaultProvider(provider: string, model: string): Promise<{ success: boolean; message: string }> {
+ return this.put('/config/provider', { provider, model });
+ }
+
+ // Health check
+ async healthCheck(): Promise<{ status: string; version: string }> {
+ const response = await axios.get(`${API_BASE_URL}/api/health`);
+ return response.data;
+ }
+}
+
+// Import types
+import type { Document, Provider, Model, Source, SystemConfig } from '@/types';
+
+export const apiClient = new ApiClient();
+export default apiClient;
\ No newline at end of file
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 0000000..5101b67
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/frontend/src/components/layout/Layout.tsx b/frontend/src/components/layout/Layout.tsx
new file mode 100644
index 0000000..5fee257
--- /dev/null
+++ b/frontend/src/components/layout/Layout.tsx
@@ -0,0 +1,163 @@
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { useAuthStore } from '@/stores/authStore';
+import { useSettingsStore } from '@/stores/settingsStore';
+import { Button } from '@/components/ui/button';
+import {
+ LayoutDashboard,
+ FileText,
+ MessageSquare,
+ Settings,
+ Menu,
+ X,
+ Brain,
+ LogOut,
+ ChevronLeft,
+ ChevronRight
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface LayoutProps {
+ children: React.ReactNode;
+}
+
+const navigation = [
+ { name: 'Dashboard', href: '/', icon: LayoutDashboard },
+ { name: 'Documents', href: '/documents', icon: FileText },
+ { name: 'Chat', href: '/chat', icon: MessageSquare },
+ { name: 'Settings', href: '/settings', icon: Settings },
+];
+
+export function Layout({ children }: LayoutProps) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { logout, isAuthenticated } = useAuthStore();
+ const { sidebarOpen, toggleSidebar, setSidebarOpen } = useSettingsStore();
+
+ const handleLogout = () => {
+ logout();
+ navigate('/login');
+ };
+
+ if (!isAuthenticated) {
+ return <>{children}>;
+ }
+
+ return (
+
+ {/* Sidebar */}
+
+ {/* Logo */}
+
+
+
+
+
+
AgenticRAG
+
+
setSidebarOpen(false)}
+ >
+
+
+
+
+ {/* Navigation */}
+
+ {navigation.map((item) => {
+ const isActive = location.pathname === item.href;
+ const Icon = item.icon;
+
+ return (
+
+
+ {item.name}
+
+ );
+ })}
+
+
+ {/* Bottom Actions */}
+
+
+
+ Sign Out
+
+
+
+
+ {/* Main Content */}
+
+ {/* Top Bar */}
+
+ setSidebarOpen(true)}
+ >
+
+
+
+ {/* Toggle Sidebar Button (Desktop) */}
+
+ {sidebarOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* User Menu */}
+
+
+ Welcome back
+
+
+
+
+ {/* Page Content */}
+
+
+ {children}
+
+
+
+
+ {/* Mobile Overlay */}
+ {sidebarOpen && (
+
setSidebarOpen(false)}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx
new file mode 100644
index 0000000..c9c71de
--- /dev/null
+++ b/frontend/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes
,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
\ No newline at end of file
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
new file mode 100644
index 0000000..938aa22
--- /dev/null
+++ b/frontend/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
\ No newline at end of file
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
new file mode 100644
index 0000000..522915b
--- /dev/null
+++ b/frontend/src/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
\ No newline at end of file
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
new file mode 100644
index 0000000..a7cafcd
--- /dev/null
+++ b/frontend/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
\ No newline at end of file
diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx
new file mode 100644
index 0000000..a4e95fb
--- /dev/null
+++ b/frontend/src/components/ui/separator.tsx
@@ -0,0 +1,30 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+interface SeparatorProps extends React.HTMLAttributes {
+ orientation?: "horizontal" | "vertical"
+ decorative?: boolean
+}
+
+const Separator = React.forwardRef(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = "Separator"
+
+export { Separator }
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..00b08e3
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,59 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 221.2 83.2% 53.3%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 224.3 76.3% 48%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..bef5202
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/frontend/src/pages/Chat.tsx b/frontend/src/pages/Chat.tsx
new file mode 100644
index 0000000..e597fd3
--- /dev/null
+++ b/frontend/src/pages/Chat.tsx
@@ -0,0 +1,235 @@
+import { useState, useRef, useEffect } from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { apiClient } from '@/api/client';
+import { useChatStore } from '@/stores/chatStore';
+import { useSettingsStore } from '@/stores/settingsStore';
+import {
+ Send,
+ Loader2,
+ Bot,
+ User,
+ FileText,
+ Plus
+} from 'lucide-react';
+import type { Message, Source } from '@/types';
+
+export function Chat() {
+ const [input, setInput] = useState('');
+ const messagesEndRef = useRef(null);
+
+ const {
+ messages,
+ addMessage,
+ isLoading,
+ setLoading,
+ startNewConversation
+ } = useChatStore();
+
+ const { defaultProvider, defaultModel } = useSettingsStore();
+
+ // Auto-scroll to bottom
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!input.trim() || isLoading) return;
+
+ const userMessage: Message = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: input.trim(),
+ timestamp: new Date().toISOString(),
+ };
+
+ addMessage(userMessage);
+ setInput('');
+ setLoading(true);
+
+ try {
+ const response = await apiClient.query(
+ userMessage.content,
+ defaultProvider,
+ defaultModel
+ );
+
+ const assistantMessage: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: response.response,
+ sources: response.sources,
+ timestamp: new Date().toISOString(),
+ };
+
+ addMessage(assistantMessage);
+ } catch (error) {
+ console.error('Error sending message:', error);
+
+ const errorMessage: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: 'Sorry, I encountered an error while processing your request. Please try again.',
+ timestamp: new Date().toISOString(),
+ };
+
+ addMessage(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleNewChat = () => {
+ if (messages.length > 0 && confirm('Start a new conversation? Current messages will be cleared.')) {
+ startNewConversation();
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Chat
+
+ Ask questions about your documents
+
+
+
+
+ New Chat
+
+
+
+ {/* Chat Container */}
+
+ {/* Messages */}
+
+ {messages.length === 0 ? (
+
+
+
Start a conversation
+
Ask questions about your uploaded documents
+
+ {[
+ 'What are the main topics in my documents?',
+ 'Summarize the key points',
+ 'Find information about...',
+ 'Explain the concept of...',
+ ].map((suggestion) => (
+ setInput(suggestion)}
+ >
+ {suggestion}
+
+ ))}
+
+
+ ) : (
+ <>
+ {messages.map((message) => (
+
+ ))}
+ {isLoading && (
+
+
+ Thinking...
+
+ )}
+
+ >
+ )}
+
+
+ {/* Input */}
+
+
+
+ Using {defaultProvider} • {defaultModel}
+
+
+
+
+ );
+}
+
+// Chat Message Component
+function ChatMessage({ message }: { message: Message }) {
+ const isUser = message.role === 'user';
+
+ return (
+
+
+ {/* Avatar */}
+
+ {isUser ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Message Content */}
+
+
+
+ {/* Sources */}
+ {!isUser && message.sources && message.sources.length > 0 && (
+
+
Sources:
+
+ {message.sources.map((source, index) => (
+
+ ))}
+
+
+ )}
+
+ {/* Timestamp */}
+
+ {new Date(message.timestamp).toLocaleTimeString()}
+
+
+
+
+ );
+}
+
+// Source Badge Component
+function SourceBadge({ source }: { source: Source }) {
+ return (
+
+
+ {source.document_name}
+ ({(source.score * 100).toFixed(0)}%)
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..35938f1
--- /dev/null
+++ b/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,196 @@
+import { useEffect, useState } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { apiClient } from '@/api/client';
+import type { Document, SystemConfig } from '@/types';
+import {
+ FileText,
+ Brain,
+ TrendingUp,
+ Upload,
+ MessageCircle,
+ ChevronRight,
+ Loader2
+} from 'lucide-react';
+import { useNavigate } from 'react-router-dom';
+
+export function Dashboard() {
+ const [documents, setDocuments] = useState([]);
+ const [config, setConfig] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [docs, cfg] = await Promise.all([
+ apiClient.getDocuments(),
+ apiClient.getConfig(),
+ ]);
+ setDocuments(docs.slice(0, 5)); // Last 5 documents
+ setConfig(cfg);
+ } catch (error) {
+ console.error('Error fetching dashboard data:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
Dashboard
+
+ Overview of your AgenticRAG system
+
+
+
+ {/* Stats Cards */}
+
+
+
+ Total Documents
+
+
+
+ {documents.length}
+
+ Documents in your knowledge base
+
+
+
+
+
+
+ Active Provider
+
+
+
+
+ {config?.default_llm_provider || 'N/A'}
+
+
+ {config?.default_llm_model || ''}
+
+
+
+
+
+
+ System Status
+
+
+
+ Active
+
+ All systems operational
+
+
+
+
+
+ {/* Quick Actions */}
+
+
navigate('/documents')}>
+
+
+ Upload Documents
+
+
+
+ Add new documents to your knowledge base
+
+
+
+
+ Go to Upload
+
+
+
+
+
+
navigate('/chat')}>
+
+
+ Start Chat
+
+
+
+ Ask questions about your documents
+
+
+
+
+ Open Chat
+
+
+
+
+
+
+ {/* Recent Documents */}
+
+
+
+
+ Recent Documents
+ Recently uploaded documents
+
+
navigate('/documents')}>
+ View All
+
+
+
+
+ {documents.length === 0 ? (
+
+
+
No documents yet
+
Upload your first document to get started
+
+ ) : (
+
+ {documents.map((doc) => (
+
+
+
+
+
{doc.filename}
+
+ {new Date(doc.created_at).toLocaleDateString()} • {(doc.size / 1024).toFixed(1)} KB
+
+
+
+
+ {doc.status}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Documents.tsx b/frontend/src/pages/Documents.tsx
new file mode 100644
index 0000000..177b961
--- /dev/null
+++ b/frontend/src/pages/Documents.tsx
@@ -0,0 +1,250 @@
+import { useEffect, useState, useCallback } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { apiClient } from '@/api/client';
+import type { Document } from '@/types';
+import {
+ FileText,
+ Upload,
+ Trash2,
+ Search,
+ Loader2,
+ X,
+ CheckCircle,
+ AlertCircle
+} from 'lucide-react';
+
+export function Documents() {
+ const [documents, setDocuments] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isUploading, setIsUploading] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [dragActive, setDragActive] = useState(false);
+
+ const fetchDocuments = useCallback(async () => {
+ try {
+ const docs = await apiClient.getDocuments();
+ setDocuments(docs);
+ } catch (error) {
+ console.error('Error fetching documents:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchDocuments();
+ }, [fetchDocuments]);
+
+ const handleUpload = async (file: File) => {
+ setIsUploading(true);
+ try {
+ await apiClient.uploadDocument(file);
+ await fetchDocuments();
+ } catch (error) {
+ console.error('Error uploading document:', error);
+ alert('Failed to upload document. Please try again.');
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
+ if (!confirm('Are you sure you want to delete this document?')) return;
+
+ try {
+ await apiClient.deleteDocument(id);
+ await fetchDocuments();
+ } catch (error) {
+ console.error('Error deleting document:', error);
+ alert('Failed to delete document. Please try again.');
+ }
+ };
+
+ const handleDrag = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.type === 'dragenter' || e.type === 'dragover') {
+ setDragActive(true);
+ } else if (e.type === 'dragleave') {
+ setDragActive(false);
+ }
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDragActive(false);
+
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
+ handleUpload(e.dataTransfer.files[0]);
+ }
+ };
+
+ const handleFileInput = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files[0]) {
+ handleUpload(e.target.files[0]);
+ }
+ };
+
+ const filteredDocuments = documents.filter((doc) =>
+ doc.filename.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ const getStatusIcon = (status: string) => {
+ switch (status) {
+ case 'ready':
+ return ;
+ case 'processing':
+ return ;
+ case 'error':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
Documents
+
+ Manage your knowledge base documents
+
+
+
+ {/* Upload Zone */}
+
+
+
+
+ {isUploading ? 'Uploading...' : 'Drag & drop your documents here'}
+
+
+ Supports PDF, DOCX, TXT, and MD files
+
+
+ document.getElementById('file-upload')?.click()}
+ >
+ {isUploading ? (
+ <>
+
+ Uploading...
+ >
+ ) : (
+ <>
+
+ Browse Files
+ >
+ )}
+
+
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+ {searchQuery && (
+ setSearchQuery('')}
+ >
+
+
+ )}
+
+
+ {/* Documents List */}
+
+
+ Your Documents ({filteredDocuments.length})
+
+ {documents.length === 0
+ ? 'No documents uploaded yet'
+ : 'Click the trash icon to delete a document'}
+
+
+
+ {isLoading ? (
+
+
+
+ ) : filteredDocuments.length === 0 ? (
+
+
+ {searchQuery ? (
+
No documents match your search
+ ) : (
+ <>
+
No documents yet
+
Upload documents to get started
+ >
+ )}
+
+ ) : (
+
+ {filteredDocuments.map((doc) => (
+
+
+
+
+
{doc.filename}
+
+ {new Date(doc.created_at).toLocaleDateString()} • {(doc.size / 1024).toFixed(1)} KB
+
+
+
+
+
+ {getStatusIcon(doc.status)}
+ {doc.status}
+
+
handleDelete(doc.id)}
+ >
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
new file mode 100644
index 0000000..9974794
--- /dev/null
+++ b/frontend/src/pages/Login.tsx
@@ -0,0 +1,93 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { useAuthStore } from '@/stores/authStore';
+import { apiClient } from '@/api/client';
+import { Brain, Key, Loader2 } from 'lucide-react';
+
+export function Login() {
+ const [apiKey, setApiKey] = useState('');
+ const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const navigate = useNavigate();
+ const loginWithApiKey = useAuthStore((state) => state.loginWithApiKey);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setIsLoading(true);
+
+ try {
+ // Test the API key by making a health check
+ loginWithApiKey(apiKey);
+ await apiClient.healthCheck();
+ navigate('/');
+ } catch (err) {
+ setError('Invalid API key or connection error. Please try again.');
+ console.error('Login error:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Welcome to AgenticRAG
+
+ Enter your API key to access the system
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
new file mode 100644
index 0000000..19b6dbf
--- /dev/null
+++ b/frontend/src/pages/Settings.tsx
@@ -0,0 +1,296 @@
+import { useEffect, useState } from 'react';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { Separator } from '@/components/ui/separator';
+import { apiClient } from '@/api/client';
+import { useSettingsStore } from '@/stores/settingsStore';
+import { useAuthStore } from '@/stores/authStore';
+import type { Provider, Model, SystemConfig } from '@/types';
+import {
+ Loader2,
+ CheckCircle,
+ XCircle,
+ Sun,
+ Moon,
+ Monitor,
+ LogOut,
+ Brain,
+ Key
+} from 'lucide-react';
+
+export function Settings() {
+ const [providers, setProviders] = useState([]);
+ const [models, setModels] = useState([]);
+ const [config, setConfig] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const {
+ theme,
+ setTheme,
+ defaultProvider,
+ defaultModel,
+ setDefaultProvider,
+ setDefaultModel
+ } = useSettingsStore();
+
+ const { logout, apiKey } = useAuthStore();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [providersData, configData] = await Promise.all([
+ apiClient.getProviders(),
+ apiClient.getConfig(),
+ ]);
+ setProviders(providersData);
+ setConfig(configData);
+
+ // Fetch models for current provider
+ if (defaultProvider) {
+ const modelsData = await apiClient.getProviderModels(defaultProvider);
+ setModels(modelsData.models);
+ }
+ } catch (error) {
+ console.error('Error fetching settings:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [defaultProvider]);
+
+ const handleProviderChange = async (providerId: string) => {
+ setDefaultProvider(providerId);
+ setDefaultModel(''); // Reset model when provider changes
+
+ try {
+ const modelsData = await apiClient.getProviderModels(providerId);
+ setModels(modelsData.models);
+ if (modelsData.models.length > 0) {
+ setDefaultModel(modelsData.models[0].id);
+ }
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ }
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ try {
+ await apiClient.updateDefaultProvider(defaultProvider, defaultModel);
+ alert('Settings saved successfully!');
+ } catch (error) {
+ console.error('Error saving settings:', error);
+ alert('Failed to save settings. Please try again.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleLogout = () => {
+ logout();
+ window.location.href = '/login';
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // Filter configured providers for display
+ providers.filter((p) => p.configured);
+
+ return (
+
+ {/* Header */}
+
+
Settings
+
+ Configure your AgenticRAG preferences
+
+
+
+ {/* Appearance */}
+
+
+
+
+ Appearance
+
+
+ Customize the look and feel of the application
+
+
+
+
+
Theme
+
+ setTheme('light')}
+ className="flex-1"
+ >
+
+ Light
+
+ setTheme('dark')}
+ className="flex-1"
+ >
+
+ Dark
+
+ setTheme('system')}
+ className="flex-1"
+ >
+
+ System
+
+
+
+
+
+
+ {/* LLM Provider */}
+
+
+
+
+ LLM Provider
+
+
+ Select your preferred language model provider
+
+
+
+ {/* Provider Selection */}
+
+
Provider
+
+ {providers.map((provider) => (
+
handleProviderChange(provider.id)}
+ disabled={!provider.configured}
+ className="justify-start"
+ >
+
+ {provider.configured ? (
+
+ ) : (
+
+ )}
+ {provider.name}
+
+
+ ))}
+
+
+
+ {/* Model Selection */}
+ {models.length > 0 && (
+
+
Model
+
+ {models.map((model) => (
+ setDefaultModel(model.id)}
+ className="justify-start"
+ >
+ {model.name}
+
+ ))}
+
+
+ )}
+
+
+ {isSaving ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ 'Save Settings'
+ )}
+
+
+
+
+ {/* System Info */}
+ {config && (
+
+
+ System Information
+
+ Current system configuration
+
+
+
+
+ Embedding Provider
+ {config.embedding_provider}
+
+
+ Embedding Model
+ {config.embedding_model}
+
+
+ Vector Store
+ Qdrant ({config.qdrant_host}:{config.qdrant_port})
+
+
+ Configured Providers
+ {config.configured_providers.length}
+
+
+
+ )}
+
+
+
+ {/* Account */}
+
+
+
+
+ Account
+
+
+ Manage your account settings
+
+
+
+
+
+
API Key
+
+ {apiKey ? `${apiKey.substring(0, 8)}...` : 'Not set'}
+
+
+
+
+ Sign Out
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts
new file mode 100644
index 0000000..d3839c2
--- /dev/null
+++ b/frontend/src/stores/authStore.ts
@@ -0,0 +1,68 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { User } from '@/types';
+
+interface AuthState {
+ // State
+ user: User | null;
+ apiKey: string | null;
+ token: string | null;
+ isAuthenticated: boolean;
+
+ // Actions
+ loginWithApiKey: (apiKey: string) => void;
+ loginWithToken: (token: string, user: User) => void;
+ logout: () => void;
+ setUser: (user: User) => void;
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set) => ({
+ user: null,
+ apiKey: null,
+ token: null,
+ isAuthenticated: false,
+
+ loginWithApiKey: (apiKey: string) => {
+ set({
+ apiKey,
+ token: null,
+ isAuthenticated: true,
+ user: { id: 'admin', auth_method: 'api_key' },
+ });
+ },
+
+ loginWithToken: (token: string, user: User) => {
+ set({
+ token,
+ apiKey: null,
+ isAuthenticated: true,
+ user,
+ });
+ },
+
+ logout: () => {
+ set({
+ user: null,
+ apiKey: null,
+ token: null,
+ isAuthenticated: false,
+ });
+ },
+
+ setUser: (user: User) => {
+ set({ user });
+ },
+ }),
+ {
+ name: 'auth-storage',
+ partialize: (state) => ({
+ apiKey: state.apiKey,
+ token: state.token,
+ isAuthenticated: state.isAuthenticated,
+ user: state.user
+ }),
+ }
+ )
+);
\ No newline at end of file
diff --git a/frontend/src/stores/chatStore.ts b/frontend/src/stores/chatStore.ts
new file mode 100644
index 0000000..ba208ba
--- /dev/null
+++ b/frontend/src/stores/chatStore.ts
@@ -0,0 +1,72 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import type { Message } from '@/types';
+
+interface ChatState {
+ // State
+ messages: Message[];
+ currentConversationId: string | null;
+ isLoading: boolean;
+ conversations: { id: string; title: string; updated_at: string }[];
+
+ // Actions
+ addMessage: (message: Message) => void;
+ setMessages: (messages: Message[]) => void;
+ clearMessages: () => void;
+ setLoading: (loading: boolean) => void;
+ setCurrentConversation: (id: string | null) => void;
+ startNewConversation: () => void;
+ addConversation: (conversation: { id: string; title: string; updated_at: string }) => void;
+}
+
+export const useChatStore = create()(
+ persist(
+ (set) => ({
+ messages: [],
+ currentConversationId: null,
+ isLoading: false,
+ conversations: [],
+
+ addMessage: (message) => {
+ set((state) => ({
+ messages: [...state.messages, message],
+ }));
+ },
+
+ setMessages: (messages) => {
+ set({ messages });
+ },
+
+ clearMessages: () => {
+ set({ messages: [], currentConversationId: null });
+ },
+
+ setLoading: (loading) => {
+ set({ isLoading: loading });
+ },
+
+ setCurrentConversation: (id) => {
+ set({ currentConversationId: id });
+ },
+
+ startNewConversation: () => {
+ set({
+ messages: [],
+ currentConversationId: null
+ });
+ },
+
+ addConversation: (conversation) => {
+ set((state) => ({
+ conversations: [conversation, ...state.conversations].slice(0, 50), // Keep last 50
+ }));
+ },
+ }),
+ {
+ name: 'chat-storage',
+ partialize: (state) => ({
+ conversations: state.conversations
+ }),
+ }
+ )
+);
\ No newline at end of file
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
new file mode 100644
index 0000000..15d30b7
--- /dev/null
+++ b/frontend/src/stores/index.ts
@@ -0,0 +1,3 @@
+export { useAuthStore } from './authStore';
+export { useChatStore } from './chatStore';
+export { useSettingsStore } from './settingsStore';
\ No newline at end of file
diff --git a/frontend/src/stores/settingsStore.ts b/frontend/src/stores/settingsStore.ts
new file mode 100644
index 0000000..a49f863
--- /dev/null
+++ b/frontend/src/stores/settingsStore.ts
@@ -0,0 +1,61 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface SettingsState {
+ // State
+ theme: 'light' | 'dark' | 'system';
+ defaultProvider: string;
+ defaultModel: string;
+ sidebarOpen: boolean;
+
+ // Actions
+ setTheme: (theme: 'light' | 'dark' | 'system') => void;
+ setDefaultProvider: (provider: string) => void;
+ setDefaultModel: (model: string) => void;
+ toggleSidebar: () => void;
+ setSidebarOpen: (open: boolean) => void;
+}
+
+export const useSettingsStore = create()(
+ persist(
+ (set) => ({
+ theme: 'system',
+ defaultProvider: 'openai',
+ defaultModel: 'gpt-4o-mini',
+ sidebarOpen: true,
+
+ setTheme: (theme) => {
+ set({ theme });
+ // Apply theme to document
+ const root = window.document.documentElement;
+ root.classList.remove('light', 'dark');
+
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ root.classList.add(systemTheme);
+ } else {
+ root.classList.add(theme);
+ }
+ },
+
+ setDefaultProvider: (provider) => {
+ set({ defaultProvider: provider });
+ },
+
+ setDefaultModel: (model) => {
+ set({ defaultModel: model });
+ },
+
+ toggleSidebar: () => {
+ set((state) => ({ sidebarOpen: !state.sidebarOpen }));
+ },
+
+ setSidebarOpen: (open) => {
+ set({ sidebarOpen: open });
+ },
+ }),
+ {
+ name: 'settings-storage',
+ }
+ )
+);
\ No newline at end of file
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
new file mode 100644
index 0000000..7319ab2
--- /dev/null
+++ b/frontend/src/types/index.ts
@@ -0,0 +1,75 @@
+// Authentication types
+export interface User {
+ id: string;
+ email?: string;
+ auth_method: 'api_key' | 'jwt';
+}
+
+export interface LoginCredentials {
+ apiKey?: string;
+ email?: string;
+ password?: string;
+}
+
+// Document types
+export interface Document {
+ id: string;
+ filename: string;
+ content_type: string;
+ size: number;
+ created_at: string;
+ status: 'processing' | 'ready' | 'error';
+}
+
+export interface DocumentUploadResponse {
+ document_id: string;
+ filename: string;
+ status: string;
+}
+
+// Chat types
+export interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ sources?: Source[];
+ timestamp: string;
+}
+
+export interface Source {
+ document_id: string;
+ document_name: string;
+ content: string;
+ score: number;
+}
+
+export interface ChatResponse {
+ response: string;
+ sources: Source[];
+}
+
+// Provider types
+export interface Provider {
+ id: string;
+ name: string;
+ available: boolean;
+ configured: boolean;
+ default_model: string;
+ install_command?: string;
+}
+
+export interface Model {
+ id: string;
+ name: string;
+}
+
+// Config types
+export interface SystemConfig {
+ default_llm_provider: string;
+ default_llm_model: string;
+ embedding_provider: string;
+ embedding_model: string;
+ configured_providers: string[];
+ qdrant_host: string;
+ qdrant_port: number;
+}
\ No newline at end of file
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
new file mode 100644
index 0000000..6695da6
--- /dev/null
+++ b/frontend/tailwind.config.js
@@ -0,0 +1,76 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ darkMode: ["class"],
+ content: [
+ "./pages/**/*.{ts,tsx}",
+ "./components/**/*.{ts,tsx}",
+ "./app/**/*.{ts,tsx}",
+ "./src/**/*.{ts,tsx}",
+ ],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+}
\ No newline at end of file
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..d932263
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ "ignoreDeprecations": "6.0",
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Path aliases */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..d3c52ea
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "module": "esnext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..77532d5
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
\ No newline at end of file