diff --git a/.gitignore b/.gitignore index 2ad291abd9..d17bbe3a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ **/venv/ **/__pycache__/** private.* -.venv \ No newline at end of file +.venv +**/temp/ \ No newline at end of file diff --git a/README.md b/README.md index 27a5e76b46..80c3ca9c14 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ Choose higher settings (like the t3.xlarge profile above) for raw speed, or lowe ## ๐Ÿ’ฌ Need Help? -**๐Ÿ”— [Join our Discord](https://discord.gg/qPaAuTCv)** for: +**๐Ÿ”— [Join our Discord](https://getmax.im/bifrost-discord)** for: - โ“ Quick setup assistance and troubleshooting - ๐Ÿ’ก Best practices and configuration tips diff --git a/docs/benchmarks.md b/docs/benchmarks.md index f90587654d..8b980914d6 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -43,7 +43,7 @@ _\*Bifrost's overhead is measured at 59 ยตs on t3.medium and 11 ยตs on t3.xlarge **Note**: On the t3.xlarge, we tested with significantly larger response payloads (~10 KB average vs ~1 KB on t3.medium). Even so, response parsing time dropped dramatically thanks to better CPU throughput and Bifrost's optimized memory reuse. -**Disclaimer**: These metrics are measured without the UI enabled. When using the UI, there is no drop in performance - only memory usage increases due to the additional UI build being served. +**Disclaimer**: These metrics are measured without the UI logging enabled. When logging is enabled, there is no drop in performance - only memory usage increases due to the additional log storage being used. --- diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 85b3a21786..3d0545d324 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -38,7 +38,7 @@ cd ../transports-integrations/ ### **๐Ÿ’ฌ Need Help Contributing?** -**๐Ÿ”— [Join our Discord](https://discord.gg/qPaAuTCv)** for: +**๐Ÿ”— [Join our Discord](https://getmax.im/bifrost-discord)** for: - โ“ Quick questions about contributing - ๐Ÿ’ก Discuss your contribution ideas @@ -524,7 +524,7 @@ We value every contribution and recognize contributors: - **๐Ÿ’ฌ [GitHub Discussions](https://github.com/maximhq/bifrost/discussions)** - Questions, ideas, and general discussion - **๐Ÿ› [GitHub Issues](https://github.com/maximhq/bifrost/issues)** - Bug reports and feature requests -- **๐Ÿ”— [Discord Community](https://discord.gg/qPaAuTCv)** - Real-time chat and collaboration +- **๐Ÿ”— [Discord Community](https://getmax.im/bifrost-discord)** - Real-time chat and collaboration --- diff --git a/docs/quickstart/README.md b/docs/quickstart/README.md index 202df6b563..340f437c4b 100644 --- a/docs/quickstart/README.md +++ b/docs/quickstart/README.md @@ -62,7 +62,7 @@ After completing the quick start: ## ๐Ÿ’ก Need Help? -- **[๐Ÿ’ฌ Join Discord](https://discord.gg/qPaAuTCv)** - Real-time setup help and community support +- **[๐Ÿ’ฌ Join Discord](https://getmax.im/bifrost-discord)** - Real-time setup help and community support - **[๐Ÿ” Troubleshooting](../troubleshooting.md)** - Common issues and solutions - **[โ“ FAQ](../faq.md)** - Frequently asked questions - **[๐Ÿ“– Full Documentation](../README.md)** - Complete documentation hub diff --git a/docs/quickstart/go-package.md b/docs/quickstart/go-package.md index 500e533076..5af064102c 100644 --- a/docs/quickstart/go-package.md +++ b/docs/quickstart/go-package.md @@ -193,7 +193,7 @@ response, err := client.ChatCompletionRequest(context.Background(), schemas.Chat ## ๐Ÿ’ฌ Need Help? -**๐Ÿ”— [Join our Discord](https://discord.gg/qPaAuTCv)** for real-time setup assistance and Go-specific support! +**๐Ÿ”— [Join our Discord](https://getmax.im/bifrost-discord)** for real-time setup assistance and Go-specific support! --- diff --git a/docs/quickstart/http-transport.md b/docs/quickstart/http-transport.md index a1a51c5acc..f4f84da90f 100644 --- a/docs/quickstart/http-transport.md +++ b/docs/quickstart/http-transport.md @@ -325,7 +325,7 @@ response, err := http.Post( ## ๐Ÿ’ฌ Need Help? -**๐Ÿ”— [Join our Discord](https://discord.gg/qPaAuTCv)** for real-time setup assistance and HTTP integration support! +**๐Ÿ”— [Join our Discord](https://getmax.im/bifrost-discord)** for real-time setup assistance and HTTP integration support! --- diff --git a/ui/app/config/page.tsx b/ui/app/config/page.tsx index 210d46861f..41da047583 100644 --- a/ui/app/config/page.tsx +++ b/ui/app/config/page.tsx @@ -62,7 +62,6 @@ export default function ConfigPage() { return (
-
{isLoadingProviders || isLoadingMcpClients ? ( ) : ( diff --git a/ui/app/docs/page.tsx b/ui/app/docs/page.tsx index 7eb7203e06..086103e127 100644 --- a/ui/app/docs/page.tsx +++ b/ui/app/docs/page.tsx @@ -56,7 +56,6 @@ const docSections = [ export default function DocsPage() { return (
-
{/* Header */} diff --git a/ui/app/globals.css b/ui/app/globals.css index f2019bcd0b..cd2c7836c6 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -121,3 +121,56 @@ @apply bg-background text-foreground; } } + +@utility custom-scrollbar { + overflow: auto !important; + scrollbar-width: thin; /* Firefox */ + scrollbar-color: rgba(228, 228, 231, 1) transparent; /* Firefox */ + + &::-webkit-scrollbar { + --custom-scrollbar-width: 8px; + --custom-scrollbar-height: 8px; + width: var(--custom-scrollbar-width, 8px); + height: var(--custom-scrollbar-height, 8px); + touch-action: none; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + --tw-bg-opacity: 1 !important; + background-color: rgba(228, 228, 231, var(--tw-bg-opacity)) !important; + border-radius: 8px; + opacity: 0; + visibility: hidden; + } + + &:hover::-webkit-scrollbar-thumb { + opacity: 1; + visibility: visible; + } + + &::-webkit-scrollbar-thumb:hover { + --tw-bg-opacity: 1 !important; + background-color: rgba(82, 82, 91, var(--tw-bg-opacity)) !important; + } + + /* For older WebKit browsers */ + &::-webkit-scrollbar-thumb:horizontal { + background-color: rgba(228, 228, 231, var(--tw-bg-opacity)) !important; + } + + &::-webkit-scrollbar-thumb:vertical { + background-color: rgba(228, 228, 231, var(--tw-bg-opacity)) !important; + } + + &:hover::-webkit-scrollbar-thumb:horizontal { + background-color: rgba(82, 82, 91, var(--tw-bg-opacity)) !important; + } + + &:hover::-webkit-scrollbar-thumb:vertical { + background-color: rgba(82, 82, 91, var(--tw-bg-opacity)) !important; + } +} diff --git a/ui/app/layout.tsx b/ui/app/layout.tsx index 9e0a7f1698..9215fe40fe 100644 --- a/ui/app/layout.tsx +++ b/ui/app/layout.tsx @@ -6,6 +6,7 @@ import { SidebarProvider } from "@/components/ui/sidebar"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "sonner"; import ProgressProvider from "@/components/progress-bar"; +import { WebSocketProvider } from "@/hooks/useWebSocket"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -30,10 +31,12 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - -
{children}
-
+ + + +
{children}
+
+
diff --git a/ui/app/page.tsx b/ui/app/page.tsx index f782c09b63..f4401c68f2 100644 --- a/ui/app/page.tsx +++ b/ui/app/page.tsx @@ -92,7 +92,12 @@ export default function LogsPage() { [pagination.offset, pagination.sort_by, pagination.order, pagination.limit, filters, showEmptyState], ); - const { ws, isConnected: isSocketConnected } = useWebSocket({ onMessage: handleNewLog }); + const { isConnected: isSocketConnected, setMessageHandler } = useWebSocket(); + + // Set up the message handler when the component mounts + useEffect(() => { + setMessageHandler(handleNewLog); + }, [handleNewLog, setMessageHandler]); const fetchLogs = useCallback(async () => { setFetchingLogs(true); @@ -109,17 +114,18 @@ export default function LogsPage() { setLogs(response.logs || []); setTotalItems(response.stats.total_requests); setStats(response.stats); + } - // Only set showEmptyState on initial load and only based on total logs - if (initialLoading) { - // Check if there are any logs globally, not just in the current filter - setShowEmptyState(response.stats.total_requests === 0); - } + // Only set showEmptyState on initial load and only based on total logs + if (initialLoading) { + // Check if there are any logs globally, not just in the current filter + setShowEmptyState(response ? response.stats.total_requests === 0 : true); } } catch { - setError("Failed to fetch logs. Please try again."); + setError("Cannot fetch logs. Please check if logs are enabled in your Bifrost config."); setLogs([]); setTotalItems(0); + setShowEmptyState(true); } finally { setFetchingLogs(false); } @@ -225,11 +231,10 @@ export default function LogsPage() { return (
-
{initialLoading ? ( ) : showEmptyState ? ( - + ) : (
diff --git a/ui/app/plugins/page.tsx b/ui/app/plugins/page.tsx index 1166c1b7c5..37912e10ce 100644 --- a/ui/app/plugins/page.tsx +++ b/ui/app/plugins/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import Header from "@/components/header"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -21,6 +23,8 @@ import { } from "lucide-react"; import Link from "next/link"; import GradientHeader from "@/components/ui/gradient-header"; +import Image from "next/image"; +import { useTheme } from "next-themes"; const featuredPlugins = [ { @@ -54,7 +58,7 @@ const featuredPlugins = [ "Latency simulation", ], icon: Code, - color: "bg-green-500", + color: "bg-blue-500", url: "https://github.com/maximhq/bifrost/tree/main/plugins/mocker", quickStart: { http: "HTTP support coming soon", @@ -66,7 +70,7 @@ const featuredPlugins = [ displayName: "Circuit Breaker", description: "Resilience patterns for handling provider failures and preventing cascade errors", category: "Reliability", - status: "available", + status: "enterprise", httpSupport: false, capabilities: ["Automatic failure detection", "Fallback mechanisms", "Rate limiting", "Health monitoring", "Recovery strategies"], icon: Shield, @@ -101,9 +105,9 @@ const upcomingPlugins = [ ]; export default function PluginsPage() { + const { resolvedTheme } = useTheme(); return (
-
{/* Hero Section */} @@ -161,11 +165,22 @@ export default function PluginsPage() {
-
- -
- - {plugin.status === "production" ? "Production" : "Available"} + {plugin.name == "maxim" ? ( + Maxim + ) : ( +
+ +
+ )} + + + {plugin.status}
diff --git a/ui/components/config/core-settings-list.tsx b/ui/components/config/core-settings-list.tsx index d746aeabbe..ea89e74f10 100644 --- a/ui/components/config/core-settings-list.tsx +++ b/ui/components/config/core-settings-list.tsx @@ -17,18 +17,16 @@ export default function CoreSettingsList() { const [config, setConfig] = useState({ drop_excess_requests: false, initial_pool_size: 300, - log_queue_size: 1000, + enable_logging: true, }); const [droppedRequests, setDroppedRequests] = useState(0); const [isLoading, setIsLoading] = useState(true); const [localValues, setLocalValues] = useState<{ initial_pool_size: string; prometheus_labels: string; - log_queue_size: string; }>({ initial_pool_size: "300", prometheus_labels: "", - log_queue_size: "1000", }); useEffect(() => { @@ -46,7 +44,6 @@ export default function CoreSettingsList() { // Use refs to store timeout IDs const poolSizeTimeoutRef = useRef(undefined); const prometheusLabelsTimeoutRef = useRef(undefined); - const logQueueSizeTimeoutRef = useRef(undefined); useEffect(() => { const fetchConfig = async () => { @@ -58,7 +55,6 @@ export default function CoreSettingsList() { setLocalValues({ initial_pool_size: coreConfig.initial_pool_size?.toString() || "300", prometheus_labels: coreConfig.prometheus_labels || "", - log_queue_size: coreConfig.log_queue_size?.toString() || "1000", }); } setIsLoading(false); @@ -122,26 +118,6 @@ export default function CoreSettingsList() { [updateConfig], ); - const handleLogQueueSizeChange = useCallback( - (value: string) => { - setLocalValues((prev) => ({ ...prev, log_queue_size: value })); - - // Clear existing timeout - if (logQueueSizeTimeoutRef.current) { - clearTimeout(logQueueSizeTimeoutRef.current); - } - - // Set new timeout - logQueueSizeTimeoutRef.current = setTimeout(() => { - const numValue = Number.parseInt(value); - if (!isNaN(numValue) && numValue > 0) { - updateConfig("log_queue_size", numValue); - } - }, 1000); - }, - [updateConfig], - ); - // Cleanup timeouts on unmount useEffect(() => { return () => { @@ -151,9 +127,6 @@ export default function CoreSettingsList() { if (prometheusLabelsTimeoutRef.current) { clearTimeout(prometheusLabelsTimeoutRef.current); } - if (logQueueSizeTimeoutRef.current) { - clearTimeout(logQueueSizeTimeoutRef.current); - } }; }, []); @@ -216,21 +189,17 @@ export default function CoreSettingsList() {
-
- handleLogQueueSizeChange(e.target.value)} - min="1" + handleConfigChange("enable_logging", checked)} />
diff --git a/ui/components/config/provider-form.tsx b/ui/components/config/provider-form.tsx index 601d136457..b5ff91495a 100644 --- a/ui/components/config/provider-form.tsx +++ b/ui/components/config/provider-form.tsx @@ -1,16 +1,15 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { TagInput } from "@/components/ui/tag-input"; -import { Separator } from "@/components/ui/separator"; -import { X, Plus, Save, Key, Globe, Zap, Edit, Info, AlertTriangle } from "lucide-react"; +import { X, Plus, Save, Key, Globe, Zap, Info, AlertTriangle } from "lucide-react"; import { toast } from "sonner"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ProviderResponse, Key as KeyType, @@ -25,10 +24,10 @@ import { } from "@/lib/types/config"; import { apiService } from "@/lib/api"; import isEqual from "lodash.isequal"; -import { PROVIDER_COLORS, PROVIDER_LABELS } from "@/lib/constants/logs"; +import { PROVIDER_LABELS } from "@/lib/constants/logs"; import MetaConfigRenderer from "./meta-config-renderer"; import { Validator } from "@/lib/utils/validation"; -import { Icons } from "@/lib/constants/icons"; +import { renderProviderIcon, ProviderIconType } from "@/lib/constants/icons"; import { PROVIDERS } from "@/lib/constants/logs"; import { cn } from "@/lib/utils"; import { Alert, AlertDescription } from "../ui/alert"; @@ -143,8 +142,6 @@ export default function ProviderForm({ provider, onSave, onCancel, existingProvi const { valid: metaValid, message: metaErrorMessage } = getMetaValidation(); - const showConfigSections = !!provider || selectedProvider !== ""; - useEffect(() => { const currentData = { selectedProvider, @@ -306,17 +303,57 @@ export default function ProviderForm({ provider, onSave, onCancel, existingProvi updateField("metaConfig", { ...metaConfig, [field]: value }); }; + const tabs = useMemo(() => { + const availableTabs = []; + + // Only add API Keys tab if required for this provider + if (keysRequired) { + availableTabs.push({ + id: "api-keys", + label: "API Keys", + }); + } + + // Add Meta Config tab for providers that need it + if (selectedProvider === "azure" || selectedProvider === "bedrock" || selectedProvider === "vertex") { + availableTabs.push({ + id: "meta-config", + label: "Meta Config", + }); + } + + // Network tab is always available + availableTabs.push({ + id: "network", + label: "Network", + }); + + // Performance tab is always available + availableTabs.push({ + id: "performance", + label: "Performance", + }); + + return availableTabs; + }, [keysRequired, selectedProvider]); + + const [selectedTab, setSelectedTab] = useState(tabs[0]?.id || "api-keys"); + + useEffect(() => { + if (!tabs.map((t) => t.id).includes(selectedTab)) { + setSelectedTab(tabs[0]?.id || "api-keys"); + } + }, [tabs]); + return ( - + {provider ? (
- Edit Provider{" "} - - {PROVIDER_LABELS[provider.name]} - + {renderProviderIcon(provider.name as ProviderIconType, { size: 20 })} + {PROVIDER_LABELS[provider.name]}
) : (
Add Provider
@@ -325,322 +362,325 @@ export default function ProviderForm({ provider, onSave, onCancel, existingProvi Configure AI provider settings, API keys, and network options.
- -
-
- {/* Provider Selection */} - {!provider && - (availableProviders.length === 0 ? ( -
All providers have been configured.
- ) : ( + {/* Provider Selection */} + {!provider && + (availableProviders.length === 0 ? ( +
All providers have been configured.
+ ) : ( +
{PROVIDERS.map((p) => ( -
{ - if (availableProviders.includes(p)) { - updateField("selectedProvider", p); - } - }} - > - {Icons[p as keyof typeof Icons]} -
{PROVIDER_LABELS[p as keyof typeof PROVIDER_LABELS]}
-
+ + { + e.preventDefault(); + if (availableProviders.includes(p)) { + updateField("selectedProvider", p); + } + }} + asChild + > + + {renderProviderIcon(p as ProviderIconType, { size: "sm" })} +
{PROVIDER_LABELS[p as keyof typeof PROVIDER_LABELS]}
+
+
+ {!availableProviders.includes(p) && Provider is already configured} +
))}
+
+ ))} + + + + {tabs.map((tab) => ( + + {tab.label} + ))} - - {/* Remaining sections appear only after provider is chosen */} - {showConfigSections && ( - <> - {/* API Keys */} - {keysRequired && ( -
- - + + + {/* API Keys Tab */} + {keysRequired && ( + +
+
+ +

API Keys

+ + + + + + + + +

+ Use env.<VAR> to read the + value from an environment variable. +

+
+
+
+
+ +
+
+ {keys.map((key, index) => ( +
+
+
+
API Key
+ updateKey(index, "value", e.target.value)} + type="text" + className={`flex-1 ${keysRequired && key.value.trim() === "" ? "border-destructive" : ""}`} + /> +
+
+
+ + + + + + + + + +

Determines traffic distribution between keys. Higher weights receive more requests.

+
+
+
+
+ updateKey(index, "weight", e.target.value)} + type="number" + step="0.1" + min="0" + max="1.0" + className="w-20" + /> +
+
+
- - API Keys + - + - -

- Use env.<VAR> to read - the value from an environment variable. -

+ +

Comma-separated list of models this key applies to. Leave blank for all models.

-
+ {keys.length > 1 && ( + - - -
- {keys.map((key, index) => ( -
-
-
-
API Key
- updateKey(index, "value", e.target.value)} - type="text" - className={`flex-1 ${keysRequired && key.value.trim() === "" ? "border-destructive" : ""}`} - /> -
-
-
- - - - - - - - - -

Determines traffic distribution between keys. Higher weights receive more requests.

-
-
-
-
- updateKey(index, "weight", e.target.value)} - type="number" - step="0.1" - min="0.1" - className="w-20" - /> -
-
-
-
- - - - - - - - - -

Comma-separated list of models this key applies to. Leave blank for all models.

-
-
-
-
- updateKey(index, "models", newModels)} - /> -
- {keys.length > 1 && ( - - )} -
- ))} + )}
-
- )} + ))} +
+
+ )} - {/* Meta Config */} + {/* Meta Config Tab */} + {selectedProvider !== "anthropic" && selectedProvider !== "openai" && selectedProvider !== "cohere" && ( + + + )} - {/* Network Configuration */} -
- - - - Network Configuration - - - -
+ {/* Network Tab */} + + {/* Network Configuration */} +
+
+ +

Network Configuration

+
+
+
+ + + updateField("networkConfig", { + ...networkConfig, + base_url: e.target.value, + }) + } + className={baseURLRequired && !networkConfig.base_url ? "border-destructive" : ""} + /> +
+
+
+ + + updateField("networkConfig", { + ...networkConfig, + default_request_timeout_in_seconds: parseInt(e.target.value) || 30, + }) + } + /> +
+
+ + + updateField("networkConfig", { + ...networkConfig, + max_retries: parseInt(e.target.value) || 0, + }) + } + /> +
+
+
+
+ + {/* Proxy Configuration */} +
+
+ +

Proxy Settings

+
+
+
+ + +
+ + {proxyConfig.type !== "none" && proxyConfig.type !== "environment" && ( +
- + - updateField("networkConfig", { - ...networkConfig, - base_url: e.target.value, - }) - } - className={baseURLRequired && !networkConfig.base_url ? "border-destructive" : ""} + placeholder="http://proxy.example.com:8080" + value={proxyConfig.url || ""} + onChange={(e) => updateProxyField("url", e.target.value)} />
- + - updateField("networkConfig", { - ...networkConfig, - default_request_timeout_in_seconds: parseInt(e.target.value) || 30, - }) - } + value={proxyConfig.username || ""} + onChange={(e) => updateProxyField("username", e.target.value)} + placeholder="Proxy username" />
- + - updateField("networkConfig", { - ...networkConfig, - max_retries: parseInt(e.target.value) || 0, - }) - } + type="password" + value={proxyConfig.password || ""} + onChange={(e) => updateProxyField("password", e.target.value)} + placeholder="Proxy password" />
- + )}
+
+
- {/* Performance Configuration */} + {/* Performance Tab */} + +
+ +

Performance Settings

+
+ {performanceChanged && ( + + + + Heads up: Changing concurrency or buffer size may temporarily affect request latency for this provider + while the new settings are being applied. + + + )} +
- - - - Performance Settings - - - {performanceChanged && ( - - - - Heads up: Changing concurrency or buffer size may temporarily affect request latency for this - provider while the new settings are being applied. - - - )} - -
-
- - - updateField("performanceConfig", { - ...performanceConfig, - concurrency: parseInt(e.target.value) || 0, - }) - } - className={!performanceValid ? "border-destructive" : ""} - /> -
-
- - - updateField("performanceConfig", { - ...performanceConfig, - buffer_size: parseInt(e.target.value) || 0, - }) - } - className={!performanceValid ? "border-destructive" : ""} - /> -
-
-
+ + + updateField("performanceConfig", { + ...performanceConfig, + concurrency: parseInt(e.target.value) || 0, + }) + } + className={!performanceValid ? "border-destructive" : ""} + />
- - {/* Proxy Configuration */} -
- - - - Proxy Settings - - -
-
- - -
- - {proxyConfig.type !== "none" && proxyConfig.type !== "environment" && ( -
-
- - updateProxyField("url", e.target.value)} - /> -
-
-
- - updateProxyField("username", e.target.value)} - placeholder="Proxy username" - /> -
-
- - updateProxyField("password", e.target.value)} - placeholder="Proxy password" - /> -
-
-
- )} -
+
+ + + updateField("performanceConfig", { + ...performanceConfig, + buffer_size: parseInt(e.target.value) || 0, + }) + } + className={!performanceValid ? "border-destructive" : ""} + />
- {/* End Proxy Configuration */} - /* end fragment shown when provider selected */ - )} -
+
+
+ {/* Form Actions */} {availableProviders.length > 0 && ( @@ -648,7 +688,6 @@ export default function ProviderForm({ provider, onSave, onCancel, existingProvi - {/* Save button with tooltip explaining disabled state */} diff --git a/ui/components/config/providers-list.tsx b/ui/components/config/providers-list.tsx index e88c0a96b9..2b621ced94 100644 --- a/ui/components/config/providers-list.tsx +++ b/ui/components/config/providers-list.tsx @@ -8,7 +8,7 @@ import { Edit, Trash2, Key, Loader2, Plus } from "lucide-react"; import { toast } from "sonner"; import { ProviderResponse } from "@/lib/types/config"; import { apiService } from "@/lib/api"; -import { PROVIDER_COLORS, PROVIDER_LABELS } from "@/lib/constants/logs"; +import { PROVIDER_LABELS } from "@/lib/constants/logs"; import { AlertDialog, AlertDialogTrigger, @@ -22,6 +22,7 @@ import { } from "@/components/ui/alert-dialog"; import { CardHeader, CardTitle, CardDescription } from "../ui/card"; import ProviderForm from "./provider-form"; +import { ProviderIconType, renderProviderIcon } from "@/lib/constants/icons"; interface ProvidersListProps { providers: ProviderResponse[]; @@ -105,12 +106,9 @@ export default function ProvidersList({ providers, onRefresh }: ProvidersListPro {providers.map((provider) => ( -
-
-
-

{PROVIDER_LABELS[provider.name] || provider.name}

-

{provider.name}

-
+
+ {renderProviderIcon(provider.name as ProviderIconType, { size: 16 })} +

{PROVIDER_LABELS[provider.name] || provider.name}

diff --git a/ui/components/logs/columns.tsx b/ui/components/logs/columns.tsx index 10f2f971a2..aa313b69fe 100644 --- a/ui/components/logs/columns.tsx +++ b/ui/components/logs/columns.tsx @@ -5,7 +5,8 @@ import { LogEntry } from "@/lib/types/logs"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { ArrowUpDown } from "lucide-react"; -import { PROVIDER_COLORS, STATUS_COLORS, Provider, Status, REQUEST_TYPE_LABELS, REQUEST_TYPE_COLORS } from "@/lib/constants/logs"; +import { STATUS_COLORS, Provider, Status, REQUEST_TYPE_LABELS, REQUEST_TYPE_COLORS } from "@/lib/constants/logs"; +import { renderProviderIcon, ProviderIconType } from "@/lib/constants/icons"; export const createColumns = (): ColumnDef[] => [ { @@ -27,7 +28,8 @@ export const createColumns = (): ColumnDef[] => [ cell: ({ row }) => { const provider = row.original.provider as Provider; return ( - + + {renderProviderIcon(provider as ProviderIconType, { size: "sm" })} {provider} ); diff --git a/ui/components/logs/empty-state.tsx b/ui/components/logs/empty-state.tsx index f0bef410b4..1bd4a059a1 100644 --- a/ui/components/logs/empty-state.tsx +++ b/ui/components/logs/empty-state.tsx @@ -3,11 +3,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Copy, RefreshCw, ArrowRight } from "lucide-react"; +import { Copy, RefreshCw, ArrowRight, AlertTriangle } from "lucide-react"; import { CodeEditor } from "./ui/code-editor"; import { toast } from "sonner"; import { useState } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Alert, AlertDescription } from "../ui/alert"; type Provider = "openai" | "anthropic" | "genai"; type Language = "python" | "typescript"; @@ -183,13 +184,14 @@ const CARDS = [ interface EmptyStateProps { isSocketConnected: boolean; + error: string | null; } -export function EmptyState({ isSocketConnected }: EmptyStateProps) { +export function EmptyState({ isSocketConnected, error }: EmptyStateProps) { const [language, setLanguage] = useState("python"); return ( -
+

Welcome to Request Logs

Monitor and analyze all your API requests in real-time

@@ -202,7 +204,14 @@ export function EmptyState({ isSocketConnected }: EmptyStateProps) {
)} -
+ {error && ( + + + {error} + + )} + +
{CARDS.map((card) => (

{card.title}

@@ -219,7 +228,7 @@ export function EmptyState({ isSocketConnected }: EmptyStateProps) { ))}
-
+

Integration Examples

diff --git a/ui/components/progress-bar.tsx b/ui/components/progress-bar.tsx index d6428db8ed..1bbceaa339 100644 --- a/ui/components/progress-bar.tsx +++ b/ui/components/progress-bar.tsx @@ -4,7 +4,7 @@ import { ProgressProvider } from "@bprogress/next/app"; const AppProgressProvider = ({ children }: { children: React.ReactNode }) => { return ( - + {children} ); diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx index 0cb2294770..7ca5051055 100644 --- a/ui/components/sidebar.tsx +++ b/ui/components/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Home, BookOpen, Settings, Puzzle, Zap, ExternalLink, ChevronRight } from "lucide-react"; +import { Home, BookOpen, Settings, Puzzle, ExternalLink, HeartHandshake } from "lucide-react"; import { Sidebar, @@ -20,8 +20,11 @@ import { usePathname } from "next/navigation"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { useTheme } from "next-themes"; -import { useMemo, useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import Image from "next/image"; +import { ThemeToggle } from "./theme-toggle"; +import { useWebSocket } from "@/hooks/useWebSocket"; +import { BookOpenTextIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react"; // Main navigation items const navigationItems = [ @@ -30,7 +33,6 @@ const navigationItems = [ url: "/", icon: Home, description: "Request logs & monitoring", - badge: "Live", }, { title: "Config", @@ -55,15 +57,20 @@ const navigationItems = [ // External links const externalLinks = [ + { + title: "Discord Server", + url: "https://getmax.im/bifrost-discord", + icon: DiscordLogoIcon, + }, { title: "GitHub Repository", url: "https://github.com/maximhq/bifrost", - icon: ExternalLink, + icon: GithubLogoIcon, }, { title: "Full Documentation", url: "https://github.com/maximhq/bifrost/tree/main/docs", - icon: BookOpen, + icon: BookOpenTextIcon, }, ]; @@ -85,16 +92,19 @@ export default function AppSidebar() { // Always render the light theme version for SSR to avoid hydration mismatch const logoSrc = mounted && resolvedTheme === "dark" ? "/bifrost-logo-dark.png" : "/bifrost-logo.png"; + const { isConnected: isWebSocketConnected } = useWebSocket(); + return ( - - - Bifrost - + +
+ + Bifrost + + +
- - @@ -124,17 +134,17 @@ export default function AppSidebar() { {item.description}
-
- {item.badge && ( - - {item.badge} - - )} - {isActive && } -
+ {item.url === "/" && isWebSocketConnected && ( +
+ )} + {item.badge && ( + + {item.badge} + + )} @@ -160,7 +170,7 @@ export default function AppSidebar() { >
- + {item.title}
diff --git a/ui/hooks/useWebSocket.ts b/ui/hooks/useWebSocket.tsx similarity index 50% rename from ui/hooks/useWebSocket.ts rename to ui/hooks/useWebSocket.tsx index 3bbc0dffa1..e9bdda43c9 100644 --- a/ui/hooks/useWebSocket.ts +++ b/ui/hooks/useWebSocket.tsx @@ -1,21 +1,40 @@ -import { useEffect, useRef, useState } from "react"; -import type { LogEntry } from "@/lib/types/logs"; +"use client"; -interface WebSocketHookProps { - onMessage: (log: LogEntry) => void; +import React, { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react"; +import type { LogEntry } from "../lib/types/logs"; + +interface WebSocketContextType { + isConnected: boolean; + ws: React.RefObject; + setMessageHandler: (handler: (log: LogEntry) => void) => void; } +const WebSocketContext = createContext(null); + declare const process: { env: { NEXT_PUBLIC_BIFROST_PORT?: string; }; }; -export function useWebSocket({ onMessage }: WebSocketHookProps) { - const wsRef = useRef(null); +interface WebSocketProviderProps { + children: ReactNode; + onMessage?: (log: LogEntry) => void; +} + +// Global reference to maintain state across component remounts +let globalWsRef: WebSocket | null = null; +let globalMessageHandler: ((log: LogEntry) => void) | null = null; + +export function WebSocketProvider({ children, onMessage }: WebSocketProviderProps) { + const wsRef = useRef(globalWsRef); const reconnectTimeoutRef = useRef | null>(null); const [isConnected, setIsConnected] = useState(false); + const setMessageHandler = (handler: (log: LogEntry) => void) => { + globalMessageHandler = handler; + }; + useEffect(() => { const port = process.env.NEXT_PUBLIC_BIFROST_PORT || "8080"; const connect = () => { @@ -25,6 +44,7 @@ export function useWebSocket({ onMessage }: WebSocketHookProps) { const ws = new WebSocket(`ws://localhost:${port}/ws/logs`); wsRef.current = ws; + globalWsRef = ws; ws.onopen = () => { console.log("WebSocket connected"); @@ -40,7 +60,11 @@ export function useWebSocket({ onMessage }: WebSocketHookProps) { try { const data = JSON.parse(event.data); if (data.type === "log") { - onMessage(data.payload); + if (globalMessageHandler) { + globalMessageHandler(data.payload); + } else if (onMessage) { + onMessage(data.payload); + } } } catch (error) { console.error("Failed to parse WebSocket message:", error); @@ -64,17 +88,21 @@ export function useWebSocket({ onMessage }: WebSocketHookProps) { // Cleanup function return () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } + // Don't close the WebSocket on unmount since it's global if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } - setIsConnected(false); }; - }, [onMessage]); // Add onMessage to dependencies to avoid stale closure + }, [onMessage]); + + return {children}; +} - return { ws: wsRef, isConnected }; +export function useWebSocket() { + const context = useContext(WebSocketContext); + if (!context) { + throw new Error("useWebSocket must be used within a WebSocketProvider"); + } + return context; } diff --git a/ui/lib/constants/icons.tsx b/ui/lib/constants/icons.tsx index 6b783b3079..81c45732fb 100644 --- a/ui/lib/constants/icons.tsx +++ b/ui/lib/constants/icons.tsx @@ -1,143 +1,323 @@ -export const Icons = { - openai: ( - - - - ), - anthropic: ( - - - - ), - bedrock: ( - - - - - - - - - - - ), - cohere: ( - - - - - - ), - vertex: ( - - +"use client"; + +import React from "react"; +import { useTheme } from "next-themes"; + +type IconSize = "xs" | "sm" | "md" | "lg" | "xl" | number; +type IconProps = { + size?: IconSize; + className?: string; +}; + +// Size mapping in pixels +const sizeMap: Record = { + xs: 24, + sm: 32, + md: 40, + lg: 48, + xl: 64, +}; + +// Function to resolve size value +const resolveSize = (size: IconSize): number => { + if (typeof size === "number") return size; + return sizeMap[size] || sizeMap.md; +}; + +// Provider Icons with theme awareness where applicable +export const ProviderIcons = { + anthropic: ({ size = "md", className = "" }: IconProps) => { + const { resolvedTheme } = useTheme(); + const resolvedSize = resolveSize(size); + return resolvedTheme == "light" ? ( + + + ) : ( + + + ); + }, + + azure: ({ size = "md", className = "" }: IconProps) => { + const resolvedSize = resolveSize(size); + + return ( + + + + + + + + + + + + + + + ); + }, + + bedrock: ({ size = "md", className = "" }: IconProps) => { + const resolvedSize = resolveSize(size); + return ( + + + + + + + + + + ); + }, + + cohere: ({ size = "md", className = "" }: IconProps) => { + const resolvedSize = resolveSize(size); + return ( + + + ); + }, + + mistral: ({ size = "md", className = "" }: IconProps) => { + const resolvedSize = resolveSize(size); + + return ( + + + + + + + ); + }, + + ollama: ({ size = "md", className = "" }: IconProps) => { + const { resolvedTheme } = useTheme(); + const resolvedSize = resolveSize(size); + return resolvedTheme == "light" ? ( + - - - - - - - - ), - ollama: ( - - - - ), - mistral: ( - - - - - - - - ), - azure: ( - - - - - - - - - - - - + ) : ( + + + + ); + }, + + openai: ({ size = "md", className = "" }: IconProps) => { + const { resolvedTheme } = useTheme(); + const resolvedSize = resolveSize(size); + + return resolvedTheme === "light" ? ( + + + + ) : ( + + - - - ), + + ); + }, + + vertex: ({ size = "md", className = "" }: IconProps) => { + const resolvedSize = resolveSize(size); + + return ( + + + + + + + + + + + + + + + + + + ); + }, } as const; + +// Helper function to render provider icons +export const renderProviderIcon = (provider: keyof typeof ProviderIcons, props: IconProps = {}) => { + const IconComponent = ProviderIcons[provider]; + return IconComponent ? : null; +}; + +export type ProviderIconType = keyof typeof ProviderIcons; +export default ProviderIcons; diff --git a/ui/lib/constants/logs.ts b/ui/lib/constants/logs.ts index 479a76ce4d..75d09536f3 100644 --- a/ui/lib/constants/logs.ts +++ b/ui/lib/constants/logs.ts @@ -15,17 +15,6 @@ export const PROVIDER_LABELS = { ollama: "Ollama", } as const; -export const PROVIDER_COLORS = { - openai: "bg-cyan-100 text-cyan-800", - anthropic: "bg-orange-100 text-orange-800", - bedrock: "bg-yellow-100 text-yellow-800", - azure: "bg-blue-100 text-blue-800", - cohere: "bg-purple-100 text-purple-800", - vertex: "bg-pink-100 text-pink-800", - mistral: "bg-gray-100 text-gray-800", - ollama: "bg-indigo-100 text-indigo-800", -} as const; - export const STATUS_COLORS = { success: "bg-green-100 text-green-800", error: "bg-red-100 text-red-800", diff --git a/ui/lib/types/config.ts b/ui/lib/types/config.ts index 08afb65744..c0278ab001 100644 --- a/ui/lib/types/config.ts +++ b/ui/lib/types/config.ts @@ -117,7 +117,7 @@ export interface CoreConfig { drop_excess_requests?: boolean; initial_pool_size?: number; prometheus_labels?: string; - log_queue_size?: number; + enable_logging?: boolean; } // Utility types for form handling diff --git a/ui/package-lock.json b/ui/package-lock.json index e71d97f85b..05ad462317 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@bprogress/next": "^3.2.12", "@monaco-editor/react": "^4.7.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", @@ -1127,6 +1128,19 @@ "node": ">=12.4.0" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", + "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@pkgr/core": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", diff --git a/ui/package.json b/ui/package.json index 46f4d1b945..6d05783c81 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,6 +13,7 @@ "dependencies": { "@bprogress/next": "^3.2.12", "@monaco-editor/react": "^4.7.0", + "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7",