diff --git a/frontend/src/components/providers/auth-provider.tsx b/frontend/src/components/providers/auth-provider.tsx
index 39994ba..b369a47 100644
--- a/frontend/src/components/providers/auth-provider.tsx
+++ b/frontend/src/components/providers/auth-provider.tsx
@@ -2,6 +2,7 @@
import * as React from "react"
import { createContext, useContext, useEffect, useState } from "react"
+import { apiClient } from "@/lib/api-client"
interface User {
id: string
@@ -49,19 +50,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const validateToken = async (token: string) => {
try {
- const response = await fetch("/api/auth/me", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- })
+ // Temporarily set token in localStorage for apiClient to use
+ const previousToken = localStorage.getItem('token')
+ localStorage.setItem('token', token)
- if (response.ok) {
- const userData = await response.json()
- setUser(userData)
- } else {
- // Token is invalid
- localStorage.removeItem("access_token")
- localStorage.removeItem("refresh_token")
+ const userData = await apiClient.get("/api-internal/v1/auth/me")
+ setUser(userData)
+
+ // Restore previous token if different
+ if (previousToken && previousToken !== token) {
+ localStorage.setItem('token', previousToken)
}
} catch (error) {
console.error("Token validation failed:", error)
@@ -74,24 +72,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const login = async (username: string, password: string) => {
try {
- const response = await fetch("/api/auth/login", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ username, password }),
- })
-
- if (!response.ok) {
- const error = await response.json()
- throw new Error(error.detail || "Login failed")
- }
-
- const data = await response.json()
+ const data = await apiClient.post("/api-internal/v1/auth/login", { username, password })
// Store tokens
localStorage.setItem("access_token", data.access_token)
localStorage.setItem("refresh_token", data.refresh_token)
+ localStorage.setItem("token", data.access_token) // Also set token for apiClient
// Get user info
await validateToken(data.access_token)
@@ -102,24 +88,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const register = async (username: string, email: string, password: string) => {
try {
- const response = await fetch("/api/auth/register", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ username, email, password }),
- })
-
- if (!response.ok) {
- const error = await response.json()
- throw new Error(error.detail || "Registration failed")
- }
-
- const data = await response.json()
+ const data = await apiClient.post("/api-internal/v1/auth/register", { username, email, password })
// Store tokens
localStorage.setItem("access_token", data.access_token)
localStorage.setItem("refresh_token", data.refresh_token)
+ localStorage.setItem("token", data.access_token) // Also set token for apiClient
// Get user info
await validateToken(data.access_token)
@@ -131,6 +105,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const logout = () => {
localStorage.removeItem("access_token")
localStorage.removeItem("refresh_token")
+ localStorage.removeItem("token") // Also clear token for apiClient
setUser(null)
}
@@ -141,20 +116,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
throw new Error("No refresh token available")
}
- const response = await fetch("/api/auth/refresh", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ refresh_token }),
- })
-
- if (!response.ok) {
- throw new Error("Token refresh failed")
- }
-
- const data = await response.json()
+ const data = await apiClient.post("/api-internal/v1/auth/refresh", { refresh_token })
localStorage.setItem("access_token", data.access_token)
+ localStorage.setItem("token", data.access_token) // Also set token for apiClient
return data.access_token
} catch (error) {
diff --git a/frontend/src/components/rag/collection-manager.tsx b/frontend/src/components/rag/collection-manager.tsx
index 7e65a0b..7cfe1fc 100644
--- a/frontend/src/components/rag/collection-manager.tsx
+++ b/frontend/src/components/rag/collection-manager.tsx
@@ -12,6 +12,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { Progress } from "@/components/ui/progress"
import { Plus, Database, Trash2, FileText, Calendar, AlertCircle, CheckCircle2, Clock, Settings, ExternalLink } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
+import { apiClient } from "@/lib/api-client"
interface Collection {
id: string
@@ -126,32 +127,19 @@ export function CollectionManager({
setCreating(true)
try {
- const response = await fetch('/api/rag/collections', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- body: JSON.stringify({
- name: newCollectionName.trim(),
- description: newCollectionDescription.trim() || undefined,
- }),
+ const data = await apiClient.post('/api-internal/v1/rag/collections', {
+ name: newCollectionName.trim(),
+ description: newCollectionDescription.trim() || undefined,
+ })
+
+ onCollectionCreated(data.collection)
+ setShowCreateDialog(false)
+ setNewCollectionName("")
+ setNewCollectionDescription("")
+ toast({
+ title: "Success",
+ description: "Collection created successfully",
})
-
- if (response.ok) {
- const data = await response.json()
- onCollectionCreated(data.collection)
- setShowCreateDialog(false)
- setNewCollectionName("")
- setNewCollectionDescription("")
- toast({
- title: "Success",
- description: "Collection created successfully",
- })
- } else {
- const error = await response.json()
- throw new Error(error.message || 'Failed to create collection')
- }
} catch (error) {
toast({
title: "Error",
@@ -167,23 +155,13 @@ export function CollectionManager({
setDeleting(collectionId)
try {
- const response = await fetch(`/api/rag/collections/${collectionId}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
+ await apiClient.delete(`/api-internal/v1/rag/collections/${collectionId}`)
+
+ onCollectionDeleted(collectionId)
+ toast({
+ title: "Success",
+ description: "Collection deleted successfully",
})
-
- if (response.ok) {
- onCollectionDeleted(collectionId)
- toast({
- title: "Success",
- description: "Collection deleted successfully",
- })
- } else {
- const error = await response.json()
- throw new Error(error.message || 'Failed to delete collection')
- }
} catch (error) {
toast({
title: "Error",
diff --git a/frontend/src/components/rag/document-browser.tsx b/frontend/src/components/rag/document-browser.tsx
index f5e74c7..b5a83e4 100644
--- a/frontend/src/components/rag/document-browser.tsx
+++ b/frontend/src/components/rag/document-browser.tsx
@@ -11,6 +11,9 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Search, FileText, Trash2, Eye, Download, Calendar, Hash, FileIcon, Filter } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
+import { apiClient } from "@/lib/api-client"
+import { config } from "@/lib/config"
+import { downloadFile } from "@/lib/file-download"
interface Collection {
id: string
@@ -72,16 +75,8 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
const loadDocuments = async () => {
setLoading(true)
try {
- const response = await fetch('/api/rag/documents', {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- })
-
- if (response.ok) {
- const data = await response.json()
- setDocuments(data.documents || [])
- }
+ const data = await apiClient.get('/api-internal/v1/rag/documents')
+ setDocuments(data.documents || [])
} catch (error) {
console.error('Failed to load documents:', error)
} finally {
@@ -124,23 +119,13 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
setDeleting(documentId)
try {
- const response = await fetch(`/api/rag/documents/${documentId}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
+ await apiClient.delete(`/api-internal/v1/rag/documents/${documentId}`)
+
+ setDocuments(prev => prev.filter(doc => doc.id !== documentId))
+ toast({
+ title: "Success",
+ description: "Document deleted successfully",
})
-
- if (response.ok) {
- setDocuments(prev => prev.filter(doc => doc.id !== documentId))
- toast({
- title: "Success",
- description: "Document deleted successfully",
- })
- } else {
- const error = await response.json()
- throw new Error(error.message || 'Failed to delete document')
- }
} catch (error) {
toast({
title: "Error",
@@ -154,26 +139,10 @@ export function DocumentBrowser({ collections, selectedCollection, onCollectionS
const handleDownloadDocument = async (document: Document) => {
try {
- const response = await fetch(`/api/rag/documents/${document.id}/download`, {
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- })
-
- if (response.ok) {
- const blob = await response.blob()
- const url = window.URL.createObjectURL(blob)
- const linkElement = window.document.createElement('a')
- linkElement.style.display = 'none'
- linkElement.href = url
- linkElement.download = document.original_filename
- window.document.body.appendChild(linkElement)
- linkElement.click()
- window.URL.revokeObjectURL(url)
- window.document.body.removeChild(linkElement)
- } else {
- throw new Error('Download failed')
- }
+ await downloadFile(
+ `/api-internal/v1/rag/documents/${document.id}/download`,
+ document.original_filename
+ )
} catch (error) {
toast({
title: "Error",
diff --git a/frontend/src/components/rag/document-upload.tsx b/frontend/src/components/rag/document-upload.tsx
index d22ebb3..387bc8c 100644
--- a/frontend/src/components/rag/document-upload.tsx
+++ b/frontend/src/components/rag/document-upload.tsx
@@ -10,6 +10,8 @@ import { Progress } from "@/components/ui/progress"
import { Badge } from "@/components/ui/badge"
import { Upload, FileText, X, AlertCircle, CheckCircle2, Loader2 } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
+import { config } from "@/lib/config"
+import { uploadFile } from "@/lib/file-download"
interface Collection {
id: string
@@ -88,48 +90,39 @@ export function DocumentUpload({ collections, selectedCollection, onDocumentUplo
await new Promise(resolve => setTimeout(resolve, 200))
updateProgress(60)
- const response = await fetch('/api/rag/documents', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
- },
- body: formData,
- })
+ await uploadFile(
+ '/api-internal/v1/rag/documents',
+ uploadingFile.file,
+ { collection_id: targetCollection }
+ )
updateProgress(80)
+ updateProgress(90)
+
+ // Set processing status
+ setUploadingFiles(prev =>
+ prev.map(f => f.id === uploadingFile.id ? { ...f, status: 'processing', progress: 95 } : f)
+ )
- if (response.ok) {
- updateProgress(90)
-
- // Set processing status
- setUploadingFiles(prev =>
- prev.map(f => f.id === uploadingFile.id ? { ...f, status: 'processing', progress: 95 } : f)
- )
+ // Simulate processing time
+ await new Promise(resolve => setTimeout(resolve, 1000))
- // Simulate processing time
- await new Promise(resolve => setTimeout(resolve, 1000))
+ // Complete
+ setUploadingFiles(prev =>
+ prev.map(f => f.id === uploadingFile.id ? { ...f, status: 'completed', progress: 100 } : f)
+ )
- // Complete
- setUploadingFiles(prev =>
- prev.map(f => f.id === uploadingFile.id ? { ...f, status: 'completed', progress: 100 } : f)
- )
+ toast({
+ title: "Success",
+ description: `${uploadingFile.file.name} uploaded successfully`,
+ })
- toast({
- title: "Success",
- description: `${uploadingFile.file.name} uploaded successfully`,
- })
+ onDocumentUploaded()
- onDocumentUploaded()
-
- // Remove completed file after 3 seconds
- setTimeout(() => {
- setUploadingFiles(prev => prev.filter(f => f.id !== uploadingFile.id))
- }, 3000)
-
- } else {
- const error = await response.json()
- throw new Error(error.message || 'Upload failed')
- }
+ // Remove completed file after 3 seconds
+ setTimeout(() => {
+ setUploadingFiles(prev => prev.filter(f => f.id !== uploadingFile.id))
+ }, 3000)
} catch (error) {
setUploadingFiles(prev =>
prev.map(f => f.id === uploadingFile.id ? {
diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..bee96db
--- /dev/null
+++ b/frontend/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes
) {
+ return (
+
+ )
+}
+
+export { Skeleton }
\ No newline at end of file
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx
index 8506433..b3e4299 100644
--- a/frontend/src/contexts/AuthContext.tsx
+++ b/frontend/src/contexts/AuthContext.tsx
@@ -21,36 +21,33 @@ interface AuthContextType {
const AuthContext = createContext(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
- const [user, setUser] = useState(null)
- const [token, setToken] = useState(null)
- const [isLoading, setIsLoading] = useState(true)
- const [isMounted, setIsMounted] = useState(false)
- const router = useRouter()
-
- useEffect(() => {
- setIsMounted(true)
-
- // Check for existing session on mount (client-side only)
+ // Initialize state with values from localStorage if available (synchronous)
+ const getInitialAuth = () => {
if (typeof window !== "undefined") {
const storedToken = localStorage.getItem("token")
if (storedToken) {
- // In a real app, validate the token with the backend
- // For now, just set a demo user - also handle both email domains
- setUser({
- id: "1",
- email: "admin@example.com",
- name: "Admin User",
- role: "admin"
- })
- setToken(storedToken)
- // Ensure we have a fresh token with extended expiration
- const freshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlzX3N1cGVydXNlciI6dHJ1ZSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNzg3Mzg5NjM3fQ.DKAx-rpNvrlRxb0YG1C63QWDvH63pIAsi8QniFvDXmc"
+ // Ensure we have the correct token
+ const freshToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlzX3N1cGVydXNlciI6dHJ1ZSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNzU2NjE4Mzk2fQ.DFZOtAzJbpF_PcKhj2DWRDXUvTKFss-8lEt5H3ST2r0"
localStorage.setItem("token", freshToken)
- setToken(freshToken)
+ return {
+ user: {
+ id: "1",
+ email: "admin@example.com",
+ name: "Admin User",
+ role: "admin"
+ },
+ token: freshToken
+ }
}
}
- setIsLoading(false)
- }, [])
+ return { user: null, token: null }
+ }
+
+ const initialAuth = getInitialAuth()
+ const [user, setUser] = useState(initialAuth.user)
+ const [token, setToken] = useState(initialAuth.token)
+ const [isLoading, setIsLoading] = useState(false) // Not loading if we already have auth
+ const router = useRouter()
const login = async (email: string, password: string) => {
setIsLoading(true)
@@ -65,15 +62,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
role: "admin"
}
- const authToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlzX3N1cGVydXNlciI6dHJ1ZSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNzg3Mzg5NjM3fQ.DKAx-rpNvrlRxb0YG1C63QWDvH63pIAsi8QniFvDXmc"
+ const authToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImlzX3N1cGVydXNlciI6dHJ1ZSwicm9sZSI6InN1cGVyX2FkbWluIiwiZXhwIjoxNzU2NjE4Mzk2fQ.DFZOtAzJbpF_PcKhj2DWRDXUvTKFss-8lEt5H3ST2r0"
- setUser(demoUser)
- setToken(authToken)
+ // Store in localStorage first to ensure it's immediately available
if (typeof window !== "undefined") {
// Use the actual JWT token for API calls
localStorage.setItem("token", authToken)
localStorage.setItem("user", JSON.stringify(demoUser))
}
+
+ // Then update state
+ setUser(demoUser)
+ setToken(authToken)
+
+ // Wait a tick to ensure state has propagated
+ await new Promise(resolve => setTimeout(resolve, 50))
} else {
throw new Error("Invalid credentials")
}
diff --git a/frontend/src/contexts/ModulesContext.tsx b/frontend/src/contexts/ModulesContext.tsx
index 3c51e9e..c7a9270 100644
--- a/frontend/src/contexts/ModulesContext.tsx
+++ b/frontend/src/contexts/ModulesContext.tsx
@@ -1,6 +1,7 @@
"use client"
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react"
+import { apiClient } from "@/lib/api-client"
interface Module {
name: string
@@ -42,26 +43,7 @@ export function ModulesProvider({ children }: { children: ReactNode }) {
setIsLoading(true)
setError(null)
- const token = localStorage.getItem("token")
- if (!token) {
- setError("No authentication token")
- return
- }
-
- const response = await fetch("/api/modules", {
- headers: {
- "Authorization": `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- // Disable caching to ensure fresh data
- cache: "no-store"
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch modules: ${response.status}`)
- }
-
- const data: ModulesResponse = await response.json()
+ const data: ModulesResponse = await apiClient.get("/api-internal/v1/modules/")
setModules(data.modules)
diff --git a/frontend/src/contexts/PluginContext.tsx b/frontend/src/contexts/PluginContext.tsx
index aa1f5a1..6ca7416 100644
--- a/frontend/src/contexts/PluginContext.tsx
+++ b/frontend/src/contexts/PluginContext.tsx
@@ -5,6 +5,7 @@
*/
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { useAuth } from './AuthContext';
+import { apiClient } from '@/lib/api-client';
export interface PluginInfo {
id: string;
@@ -111,21 +112,20 @@ export const PluginProvider: React.FC = ({ children }) => {
throw new Error('Authentication required');
}
- const response = await fetch(`/api/v1/plugins${endpoint}`, {
- ...options,
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- });
+ const method = (options.method || 'GET').toLowerCase() as 'get' | 'post' | 'put' | 'delete';
+ const body = options.body ? JSON.parse(options.body as string) : undefined;
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.detail || `HTTP ${response.status}`);
+ if (method === 'get') {
+ return await apiClient.get(`/api-internal/v1/plugins${endpoint}`);
+ } else if (method === 'post') {
+ return await apiClient.post(`/api-internal/v1/plugins${endpoint}`, body);
+ } else if (method === 'put') {
+ return await apiClient.put(`/api-internal/v1/plugins${endpoint}`, body);
+ } else if (method === 'delete') {
+ return await apiClient.delete(`/api-internal/v1/plugins${endpoint}`);
}
- return response.json();
+ throw new Error(`Unsupported method: ${method}`);
};
const refreshInstalledPlugins = useCallback(async () => {
@@ -369,24 +369,15 @@ export const PluginProvider: React.FC = ({ children }) => {
if (schema && pluginName === 'zammad') {
// Populate chatbot options for Zammad
try {
- const chatbotsResponse = await fetch('/api/v1/chatbot/list', {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json',
- }
- });
+ const chatbotsData = await apiClient.get('/api-internal/v1/chatbot/list');
+ const chatbots = chatbotsData.chatbots || [];
- if (chatbotsResponse.ok) {
- const chatbotsData = await chatbotsResponse.json();
- const chatbots = chatbotsData.chatbots || [];
-
- if (schema.properties?.chatbot_id) {
- schema.properties.chatbot_id.type = 'select';
- schema.properties.chatbot_id.options = chatbots.map((chatbot: any) => ({
- value: chatbot.id,
- label: `${chatbot.name} (${chatbot.chatbot_type})`
- }));
- }
+ if (schema.properties?.chatbot_id) {
+ schema.properties.chatbot_id.type = 'select';
+ schema.properties.chatbot_id.options = chatbots.map((chatbot: any) => ({
+ value: chatbot.id,
+ label: `${chatbot.name} (${chatbot.chatbot_type})`
+ }));
}
} catch (chatbotError) {
console.warn('Failed to load chatbots for Zammad configuration:', chatbotError);
@@ -394,33 +385,24 @@ export const PluginProvider: React.FC = ({ children }) => {
// Populate model options for AI settings
try {
- const modelsResponse = await fetch('/api/v1/llm/models', {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json',
- }
- });
+ const modelsData = await apiClient.get('/api-internal/v1/llm/models');
+ const models = modelsData.data || [];
- if (modelsResponse.ok) {
- const modelsData = await modelsResponse.json();
- const models = modelsData.data || [];
-
- const modelOptions = models.map((model: any) => ({
- value: model.id,
- label: model.id
- }));
+ const modelOptions = models.map((model: any) => ({
+ value: model.id,
+ label: model.id
+ }));
- // Set model options for AI summarization
- if (schema.properties?.ai_summarization?.properties?.model) {
- schema.properties.ai_summarization.properties.model.type = 'select';
- schema.properties.ai_summarization.properties.model.options = modelOptions;
- }
+ // Set model options for AI summarization
+ if (schema.properties?.ai_summarization?.properties?.model) {
+ schema.properties.ai_summarization.properties.model.type = 'select';
+ schema.properties.ai_summarization.properties.model.options = modelOptions;
+ }
- // Set model options for draft settings
- if (schema.properties?.draft_settings?.properties?.model) {
- schema.properties.draft_settings.properties.model.type = 'select';
- schema.properties.draft_settings.properties.model.options = modelOptions;
- }
+ // Set model options for draft settings
+ if (schema.properties?.draft_settings?.properties?.model) {
+ schema.properties.draft_settings.properties.model.type = 'select';
+ schema.properties.draft_settings.properties.model.options = modelOptions;
}
} catch (modelError) {
console.warn('Failed to load models for Zammad configuration:', modelError);
@@ -430,23 +412,14 @@ export const PluginProvider: React.FC = ({ children }) => {
if (schema && pluginName === 'signal') {
// Populate model options for Signal bot
try {
- const modelsResponse = await fetch('/api/v1/llm/models', {
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json',
- }
- });
+ const modelsData = await apiClient.get('/api-internal/v1/llm/models');
+ const models = modelsData.models || [];
- if (modelsResponse.ok) {
- const modelsData = await modelsResponse.json();
- const models = modelsData.models || [];
-
- if (schema.properties?.model) {
- schema.properties.model.options = models.map((model: any) => ({
- value: model.id,
- label: model.name || model.id
- }));
- }
+ if (schema.properties?.model) {
+ schema.properties.model.options = models.map((model: any) => ({
+ value: model.id,
+ label: model.name || model.id
+ }));
}
} catch (modelError) {
console.warn('Failed to load models for Signal configuration:', modelError);
diff --git a/frontend/src/hooks/useBudgetStatus.ts b/frontend/src/hooks/useBudgetStatus.ts
index fddc317..56f8318 100644
--- a/frontend/src/hooks/useBudgetStatus.ts
+++ b/frontend/src/hooks/useBudgetStatus.ts
@@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from 'react'
+import { apiClient } from '@/lib/api-client'
interface BudgetData {
id: string
@@ -39,19 +40,7 @@ export function useBudgetStatus(autoRefresh = true, refreshInterval = 30000) {
try {
setLoading(true)
- const response = await fetch('/api/v1/llm/budget/status')
-
- if (!response.ok) {
- if (response.status === 401) {
- throw new Error('Authentication failed')
- }
- if (response.status === 403) {
- throw new Error('Insufficient permissions')
- }
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
- }
-
- const data = await response.json()
+ const data = await apiClient.get('/api-internal/v1/llm/budget/status')
setBudgetStatus(data)
setError(null)
setLastRefresh(new Date())
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 0000000..571c35d
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,103 @@
+events {
+ worker_connections 1024;
+}
+
+http {
+ upstream backend {
+ server enclava-backend:8000;
+ }
+
+ upstream frontend {
+ server enclava-frontend:3000;
+ }
+
+ server {
+ listen 80;
+ server_name localhost;
+
+ # Frontend routes
+ location / {
+ proxy_pass http://frontend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket support for Next.js HMR
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Internal API routes - proxy to backend (for frontend only)
+ location /api-internal/ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # CORS headers for frontend
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
+
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ }
+
+ # Public API routes - proxy to backend (for external clients)
+ location /api/ {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # CORS headers for external clients
+ add_header 'Access-Control-Allow-Origin' '*' always;
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
+ add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
+
+ # Handle preflight requests
+ if ($request_method = 'OPTIONS') {
+ add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
+ add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
+ add_header 'Access-Control-Max-Age' 1728000;
+ add_header 'Content-Type' 'text/plain; charset=utf-8';
+ add_header 'Content-Length' 0;
+ return 204;
+ }
+ }
+
+ # Health check endpoints
+ location /health {
+ proxy_pass http://backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+# OpenAI-compatible endpoints are now at /api/v1/ (handled by Public API routes above)
+
+ # Static files with caching
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ proxy_pass http://frontend;
+ proxy_set_header Host $host;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+ }
+}
\ No newline at end of file