diff --git a/cmd/server/main.go b/cmd/server/main.go index 346a3d2..5cdfd67 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -63,10 +63,15 @@ func main() { v1Router := r.PathPrefix("/v1/gateway").Subrouter() v1.SetupV1Routes(v1Router, routerInstance, catalogLoader, jobStore, jobWorker) - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./ui")))) + r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("./frontend/dist/assets")))) - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.ServeFile(w, r, filepath.Join("ui", "templates", "index.html")) + r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := filepath.Join("frontend", "dist", r.URL.Path) + if _, err := os.Stat(path); err == nil { + http.ServeFile(w, r, path) + return + } + http.ServeFile(w, r, filepath.Join("frontend", "dist", "index.html")) }) port := cfg.Port diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..460e575 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8080 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..74872fd --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,50 @@ +# 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/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..78cd18f --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,28 @@ +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' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..072a57e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f34ac70 --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,7 @@ +#root { + max-width: none; + width: 100%; + margin: 0; + padding: 0; + text-align: left; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..192c35c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,30 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { ThemeProvider } from './contexts/ThemeContext'; +import { HistoryProvider } from './contexts/HistoryContext'; +import Layout from './components/Layout/Layout'; +import Dashboard from './pages/Dashboard'; +import History from './pages/History'; +import Settings from './pages/Settings'; +import About from './pages/About'; +import './App.css'; + +function App() { + return ( + + + + + + } /> + } /> + } /> + } /> + + + + + + ); +} + +export default App; 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/components/Dashboard/ModelStatus.tsx b/frontend/src/components/Dashboard/ModelStatus.tsx new file mode 100644 index 0000000..8c86942 --- /dev/null +++ b/frontend/src/components/Dashboard/ModelStatus.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import { api } from '../../utils/api'; +import { ModelStatus as ModelStatusType } from '../../types'; +import { MODEL_ICONS } from '../../utils/constants'; +import { Loader2 } from 'lucide-react'; + +const ModelStatus: React.FC = () => { + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchStatus = async () => { + try { + setLoading(true); + setError(null); + const data = await api.fetchStatus(); + setStatus(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch status'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStatus(); + const interval = setInterval(fetchStatus, 30000); + return () => clearInterval(interval); + }, []); + + if (loading && !status) { + return ( +
+

Model Status

+
+ +
+
+ ); + } + + if (error) { + return ( +
+

Model Status

+
{error}
+
+ ); + } + + return ( +
+

Model Status

+
+ {status && Object.entries(status).map(([model, available]) => ( +
+
+ {MODEL_ICONS[model as keyof typeof MODEL_ICONS] || '🤖'} +
+
{model}
+
{available ? 'Available' : 'Unavailable'}
+
+ ))} +
+
+ ); +}; + +export default ModelStatus; diff --git a/frontend/src/components/Dashboard/QueryForm.tsx b/frontend/src/components/Dashboard/QueryForm.tsx new file mode 100644 index 0000000..6812e99 --- /dev/null +++ b/frontend/src/components/Dashboard/QueryForm.tsx @@ -0,0 +1,233 @@ +import React, { useState } from 'react'; +import { api, generateRequestId } from '../../utils/api'; +import { SingleModelResponse, MultiModelResponse } from '../../types'; +import { MODEL_VERSIONS, TASK_TYPES } from '../../utils/constants'; +import { Loader2 } from 'lucide-react'; +import { useHistory } from '../../contexts/HistoryContext'; + +interface QueryFormProps { + onResponse: (response: SingleModelResponse | MultiModelResponse, isMultiModel: boolean) => void; +} + +const QueryForm: React.FC = ({ onResponse }) => { + const [query, setQuery] = useState(''); + const [taskType, setTaskType] = useState(''); + const [selectedModels, setSelectedModels] = useState<{ [key: string]: boolean }>({ + openai: true, + gemini: true, + mistral: true, + claude: true, + }); + const [modelVersions, setModelVersions] = useState<{ [key: string]: string }>({ + openai: 'gpt-3.5-turbo', + gemini: 'gemini-pro', + mistral: 'mistral-small', + claude: 'claude-3-sonnet-20240229', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { addToHistory } = useHistory(); + + const selectedModelsList = Object.entries(selectedModels) + .filter(([, selected]) => selected) + .map(([model]) => model); + + const handleModelToggle = (model: string) => { + setSelectedModels(prev => ({ ...prev, [model]: !prev[model] })); + }; + + const handleVersionChange = (model: string, version: string) => { + setModelVersions(prev => ({ ...prev, [model]: version })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!query.trim()) { + setError('Please enter a query'); + return; + } + + if (selectedModelsList.length === 0) { + setError('Please select at least one model'); + return; + } + + setLoading(true); + setError(null); + + try { + const isMultiModel = selectedModelsList.length > 1; + const requestId = generateRequestId(); + + if (isMultiModel) { + const selectedVersions: { [key: string]: string } = {}; + selectedModelsList.forEach(model => { + selectedVersions[model] = modelVersions[model]; + }); + + const request = { + query, + models: selectedModelsList, + task_type: taskType || undefined, + request_id: requestId, + model_versions: selectedVersions, + }; + + const response = await api.submitParallelQuery(request); + onResponse(response, true); + + addToHistory({ + query, + models: selectedModelsList, + taskType: taskType || undefined, + response: JSON.stringify(response.responses), + responseTime: response.elapsed_time_ms, + cached: false, + }); + } else { + const model = selectedModelsList[0]; + const request = { + query, + model, + task_type: taskType || undefined, + request_id: requestId, + model_version: modelVersions[model], + }; + + const response = await api.submitQuery(request); + onResponse(response, false); + + addToHistory({ + query, + model, + taskType: taskType || undefined, + response: response.response, + responseTime: response.response_time_ms, + cached: response.cached, + tokens: response.total_tokens || response.num_tokens, + }); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to submit query'); + } finally { + setLoading(false); + } + }; + + return ( +
+

Query LLM

+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Model Selection */} +
+ +
+
+ {Object.keys(selectedModels).map((model) => ( +
+ handleModelToggle(model)} + className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500" + /> + + {selectedModels[model] && ( + + )} +
+ ))} +
+
+ 1 + ? 'bg-blue-600 text-white' + : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300' + }`}> + {selectedModelsList.length} selected + + + Select multiple models to compare responses + +
+
+
+ + {/* Task Type */} +
+ + +
+ + {/* Query Input */} +
+ +