diff --git a/frontend/components.json b/frontend/components.json index 8e7f1638..53d2101e 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -10,9 +10,13 @@ "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", "aliases": { "components": "components", "utils": "lib/utils", "ui": "components/ui" + }, + "registries": { + "@magicui": "https://magicui.design/r/{name}.json" } -} \ No newline at end of file +} diff --git a/frontend/components/logo/ibm-logo.tsx b/frontend/components/logo/ibm-logo.tsx index 6f7fc2cd..44b6e08c 100644 --- a/frontend/components/logo/ibm-logo.tsx +++ b/frontend/components/logo/ibm-logo.tsx @@ -11,7 +11,7 @@ export default function IBMLogo(props: React.SVGProps) { IBM Logo ); diff --git a/frontend/components/logo/openai-logo.tsx b/frontend/components/logo/openai-logo.tsx index 639c130e..330211b9 100644 --- a/frontend/components/logo/openai-logo.tsx +++ b/frontend/components/logo/openai-logo.tsx @@ -23,7 +23,7 @@ export default function OpenAILogo(props: React.SVGProps) { diff --git a/frontend/components/ui/dot-pattern.tsx b/frontend/components/ui/dot-pattern.tsx new file mode 100644 index 00000000..aa4b2028 --- /dev/null +++ b/frontend/components/ui/dot-pattern.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { motion } from "motion/react"; +import type React from "react"; +import { useEffect, useId, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; + +/** + * DotPattern Component Props + * + * @param {number} [width=16] - The horizontal spacing between dots + * @param {number} [height=16] - The vertical spacing between dots + * @param {number} [x=0] - The x-offset of the entire pattern + * @param {number} [y=0] - The y-offset of the entire pattern + * @param {number} [cx=1] - The x-offset of individual dots + * @param {number} [cy=1] - The y-offset of individual dots + * @param {number} [cr=1] - The radius of each dot + * @param {string} [className] - Additional CSS classes to apply to the SVG container + * @param {boolean} [glow=false] - Whether dots should have a glowing animation effect + */ +interface DotPatternProps extends React.SVGProps { + width?: number; + height?: number; + x?: number; + y?: number; + cx?: number; + cy?: number; + cr?: number; + className?: string; + glow?: boolean; + [key: string]: unknown; +} + +/** + * DotPattern Component + * + * A React component that creates an animated or static dot pattern background using SVG. + * The pattern automatically adjusts to fill its container and can optionally display glowing dots. + * + * @component + * + * @see DotPatternProps for the props interface. + * + * @example + * // Basic usage + * + * + * // With glowing effect and custom spacing + * + * + * @notes + * - The component is client-side only ("use client") + * - Automatically responds to container size changes + * - When glow is enabled, dots will animate with random delays and durations + * - Uses Motion for animations + * - Dots color can be controlled via the text color utility classes + */ + +export function DotPattern({ + width = 16, + height = 16, + x = 0, + y = 0, + cx = 1, + cy = 1, + cr = 1, + className, + glow = false, + ...props +}: DotPatternProps) { + const id = useId(); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const { width, height } = containerRef.current.getBoundingClientRect(); + setDimensions({ width, height }); + } + }; + + updateDimensions(); + window.addEventListener("resize", updateDimensions); + return () => window.removeEventListener("resize", updateDimensions); + }, []); + + const dots = Array.from( + { + length: + Math.ceil(dimensions.width / width) * + Math.ceil(dimensions.height / height), + }, + (_, i) => { + const col = i % Math.ceil(dimensions.width / width); + const row = Math.floor(i / Math.ceil(dimensions.width / width)); + return { + x: col * width + cx, + y: row * height + cy, + delay: Math.random() * 5, + duration: Math.random() * 3 + 2, + }; + }, + ); + + return ( + + ); +} diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx index 5ba0eba0..04599fd0 100644 --- a/frontend/components/ui/input.tsx +++ b/frontend/components/ui/input.tsx @@ -1,3 +1,4 @@ +import { Eye, EyeOff } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -12,6 +13,11 @@ const Input = React.forwardRef( const [hasValue, setHasValue] = React.useState( Boolean(props.value || props.defaultValue), ); + const [showPassword, setShowPassword] = React.useState(false); + + const handleTogglePassword = () => { + setShowPassword(!showPassword); + }; const handleChange = (e: React.ChangeEvent) => { setHasValue(e.target.value.length > 0); @@ -23,8 +29,8 @@ const Input = React.forwardRef( return ( ); - } + }, ); Input.displayName = "Input"; diff --git a/frontend/public/images/background.png b/frontend/public/images/background.png deleted file mode 100644 index 66d44a8a..00000000 Binary files a/frontend/public/images/background.png and /dev/null differ diff --git a/frontend/src/app/api/queries/useGetModelsQuery.ts b/frontend/src/app/api/queries/useGetModelsQuery.ts index 4ce55bd3..3a5eb77e 100644 --- a/frontend/src/app/api/queries/useGetModelsQuery.ts +++ b/frontend/src/app/api/queries/useGetModelsQuery.ts @@ -90,7 +90,6 @@ export const useGetOllamaModelsQuery = ( queryKey: ["models", "ollama", params], queryFn: getOllamaModels, retry: 2, - enabled: !!params?.endpoint, // Only run if endpoint is provided staleTime: 0, // Always fetch fresh data gcTime: 0, // Don't cache results ...options, diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index c2347f1b..1639a4be 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -6,7 +6,9 @@ import { Suspense, useEffect } from "react"; import GoogleLogo from "@/components/logo/google-logo"; import Logo from "@/components/logo/logo"; import { Button } from "@/components/ui/button"; +import { DotPattern } from "@/components/ui/dot-pattern"; import { useAuth } from "@/contexts/auth-context"; +import { cn } from "@/lib/utils"; import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery"; function LoginPageContent() { @@ -53,15 +55,19 @@ function LoginPageContent() { } return ( -
-
+
+ +

Welcome to OpenRAG

@@ -72,7 +78,7 @@ function LoginPageContent() { Continue with Google

-
+

Systems Operational

Privacy Policy

diff --git a/frontend/src/app/onboarding/components/advanced.tsx b/frontend/src/app/onboarding/components/advanced.tsx index bb0089d5..20764aed 100644 --- a/frontend/src/app/onboarding/components/advanced.tsx +++ b/frontend/src/app/onboarding/components/advanced.tsx @@ -47,8 +47,7 @@ export function AdvancedOnboarding({ {hasEmbeddingModels && ( @@ -63,8 +62,7 @@ export function AdvancedOnboarding({ {hasLanguageModels && ( @@ -79,7 +77,7 @@ export function AdvancedOnboarding({ {(hasLanguageModels || hasEmbeddingModels) && } diff --git a/frontend/src/app/onboarding/components/ibm-onboarding.tsx b/frontend/src/app/onboarding/components/ibm-onboarding.tsx index 550f9d6b..63e3fe6a 100644 --- a/frontend/src/app/onboarding/components/ibm-onboarding.tsx +++ b/frontend/src/app/onboarding/components/ibm-onboarding.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { LabelInput } from "@/components/label-input"; +import { LabelWrapper } from "@/components/label-wrapper"; import IBMLogo from "@/components/logo/ibm-logo"; import { useDebouncedValue } from "@/lib/debounce"; import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation"; @@ -7,6 +8,7 @@ import { useGetIBMModelsQuery } from "../../api/queries/useGetModelsQuery"; import { useModelSelection } from "../hooks/useModelSelection"; import { useUpdateSettings } from "../hooks/useUpdateSettings"; import { AdvancedOnboarding } from "./advanced"; +import { ModelSelector } from "./model-selector"; export function IBMOnboarding({ setSettings, @@ -17,10 +19,42 @@ export function IBMOnboarding({ sampleDataset: boolean; setSampleDataset: (dataset: boolean) => void; }) { - const [endpoint, setEndpoint] = useState(""); + const [endpoint, setEndpoint] = useState("https://us-south.ml.cloud.ibm.com"); const [apiKey, setApiKey] = useState(""); const [projectId, setProjectId] = useState(""); + const options = [ + { + value: "https://us-south.ml.cloud.ibm.com", + label: "https://us-south.ml.cloud.ibm.com", + default: true, + }, + { + value: "https://eu-de.ml.cloud.ibm.com", + label: "https://eu-de.ml.cloud.ibm.com", + default: false, + }, + { + value: "https://eu-gb.ml.cloud.ibm.com", + label: "https://eu-gb.ml.cloud.ibm.com", + default: false, + }, + { + value: "https://au-syd.ml.cloud.ibm.com", + label: "https://au-syd.ml.cloud.ibm.com", + default: false, + }, + { + value: "https://jp-tok.ml.cloud.ibm.com", + label: "https://jp-tok.ml.cloud.ibm.com", + default: false, + }, + { + value: "https://ca-tor.ml.cloud.ibm.com", + label: "https://ca-tor.ml.cloud.ibm.com", + default: false, + }, + ]; const debouncedEndpoint = useDebouncedValue(endpoint, 500); const debouncedApiKey = useDebouncedValue(apiKey, 500); const debouncedProjectId = useDebouncedValue(projectId, 500); @@ -68,19 +102,26 @@ export function IBMOnboarding({ return ( <>
- setEndpoint(e.target.value)} - /> + > + + - Invalid configuration or connection failed + Connection failed. Check your configuration.

)} - {modelsData && - (modelsData.language_models?.length > 0 || - modelsData.embedding_models?.length > 0) && ( -

- Configuration is valid -

- )}
} diff --git a/frontend/src/app/onboarding/components/model-selector.tsx b/frontend/src/app/onboarding/components/model-selector.tsx index 7a74bed2..dfed52ee 100644 --- a/frontend/src/app/onboarding/components/model-selector.tsx +++ b/frontend/src/app/onboarding/components/model-selector.tsx @@ -21,6 +21,9 @@ export function ModelSelector({ value, onValueChange, icon, + placeholder = "Select model...", + searchPlaceholder = "Search model...", + noOptionsPlaceholder = "No models available", }: { options: { value: string; @@ -29,6 +32,9 @@ export function ModelSelector({ }[]; value: string; icon?: React.ReactNode; + placeholder?: string; + searchPlaceholder?: string; + noOptionsPlaceholder?: string; onValueChange: (value: string) => void; }) { const [open, setOpen] = useState(false); @@ -50,7 +56,7 @@ export function ModelSelector({ > {value ? (
-
{icon}
+ {icon &&
{icon}
} {options.find((framework) => framework.value === value)?.label} {options.find((framework) => framework.value === value) ?.default && ( @@ -60,18 +66,18 @@ export function ModelSelector({ )}
) : options.length === 0 ? ( - "No models available" + noOptionsPlaceholder ) : ( - "Select model..." + placeholder )} - + - No model found. + {noOptionsPlaceholder} {options.map((option) => ( void; }) { - const [endpoint, setEndpoint] = useState(""); + const [endpoint, setEndpoint] = useState("http://localhost:11434"); + const [showConnecting, setShowConnecting] = useState(false); const debouncedEndpoint = useDebouncedValue(endpoint, 500); // Fetch models from API when endpoint is provided (debounced) @@ -41,6 +42,25 @@ export function OllamaOnboarding({ embeddingModels, } = useModelSelection(modelsData); + // Handle delayed display of connecting state + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (debouncedEndpoint && isLoadingModels) { + timeoutId = setTimeout(() => { + setShowConnecting(true); + }, 500); + } else { + setShowConnecting(false); + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [debouncedEndpoint, isLoadingModels]); + const handleSampleDatasetChange = (dataset: boolean) => { setSampleDataset(dataset); }; @@ -57,74 +77,75 @@ export function OllamaOnboarding({ ); // Check validation state based on models query - const isConnecting = debouncedEndpoint && isLoadingModels; const hasConnectionError = debouncedEndpoint && modelsError; const hasNoModels = modelsData && !modelsData.language_models?.length && !modelsData.embedding_models?.length; - const isValidConnection = - modelsData && - (modelsData.language_models?.length > 0 || - modelsData.embedding_models?.length > 0); return ( <>
setEndpoint(e.target.value)} /> - {isConnecting && ( + {showConnecting && (

Connecting to Ollama server...

)} {hasConnectionError && (

- Can’t reach Ollama at {debouncedEndpoint}. Update the endpoint or + Can’t reach Ollama at {debouncedEndpoint}. Update the base URL or start the server.

)} {hasNoModels && (

- No models found. Please install some models on your Ollama server. -

- )} - {isValidConnection && ( -

- Connected successfully + No models found. Install embedding and agent models on your Ollama + server.

)}
} + noOptionsPlaceholder={ + isLoadingModels + ? "Loading models..." + : "No embedding models detected. Install an embedding model to continue." + } value={embeddingModel} onValueChange={setEmbeddingModel} /> } + noOptionsPlaceholder={ + isLoadingModels + ? "Loading models..." + : "No language models detected. Install a language model to continue." + } value={languageModel} onValueChange={setLanguageModel} /> diff --git a/frontend/src/app/onboarding/components/openai-onboarding.tsx b/frontend/src/app/onboarding/components/openai-onboarding.tsx index cf18fb53..236097a4 100644 --- a/frontend/src/app/onboarding/components/openai-onboarding.tsx +++ b/frontend/src/app/onboarding/components/openai-onboarding.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { LabelInput } from "@/components/label-input"; +import { LabelWrapper } from "@/components/label-wrapper"; import OpenAILogo from "@/components/logo/openai-logo"; +import { Switch } from "@/components/ui/switch"; import { useDebouncedValue } from "@/lib/debounce"; import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation"; import { useGetOpenAIModelsQuery } from "../../api/queries/useGetModelsQuery"; @@ -18,6 +20,7 @@ export function OpenAIOnboarding({ setSampleDataset: (dataset: boolean) => void; }) { const [apiKey, setApiKey] = useState(""); + const [getFromEnv, setGetFromEnv] = useState(true); const debouncedApiKey = useDebouncedValue(apiKey, 500); // Fetch models from API when API key is provided @@ -26,7 +29,12 @@ export function OpenAIOnboarding({ isLoading: isLoadingModels, error: modelsError, } = useGetOpenAIModelsQuery( - debouncedApiKey ? { apiKey: debouncedApiKey } : undefined, + getFromEnv + ? { apiKey: "" } + : debouncedApiKey + ? { apiKey: debouncedApiKey } + : undefined, + { enabled: debouncedApiKey !== "" || getFromEnv }, ); // Use custom hook for model selection logic const { @@ -41,6 +49,15 @@ export function OpenAIOnboarding({ setSampleDataset(dataset); }; + const handleGetFromEnvChange = (fromEnv: boolean) => { + setGetFromEnv(fromEnv); + if (fromEnv) { + setApiKey(""); + } + setLanguageModel(""); + setEmbeddingModel(""); + }; + // Update settings when values change useUpdateSettings( "openai", @@ -53,33 +70,41 @@ export function OpenAIOnboarding({ ); return ( <> -
- setApiKey(e.target.value)} - /> - {isLoadingModels && ( -

- Validating API key... -

+
+ + + + {!getFromEnv && ( +
+ setApiKey(e.target.value)} + /> + {isLoadingModels && ( +

+ Validating API key... +

+ )} + {modelsError && ( +

+ Invalid OpenAI API key. Verify or replace the key. +

+ )} +
)} - {modelsError && ( -

- Invalid API key -

- )} - {modelsData && - (modelsData.language_models?.length > 0 || - modelsData.embedding_models?.length > 0) && ( -

- API Key is valid -

- )}
} diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index c58abfea..a82e5fab 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -4,8 +4,8 @@ import { useRouter } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; import { toast } from "sonner"; import { - type OnboardingVariables, - useOnboardingMutation, + type OnboardingVariables, + useOnboardingMutation, } from "@/app/api/mutations/useOnboardingMutation"; import IBMLogo from "@/components/logo/ibm-logo"; import OllamaLogo from "@/components/logo/ollama-logo"; @@ -13,198 +13,208 @@ import OpenAILogo from "@/components/logo/openai-logo"; import { ProtectedRoute } from "@/components/protected-route"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardFooter, - CardHeader, + Card, + CardContent, + CardFooter, + CardHeader, } from "@/components/ui/card"; +import { DotPattern } from "@/components/ui/dot-pattern"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { - Tooltip, - TooltipContent, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import { useGetSettingsQuery } from "../api/queries/useGetSettingsQuery"; import { IBMOnboarding } from "./components/ibm-onboarding"; import { OllamaOnboarding } from "./components/ollama-onboarding"; import { OpenAIOnboarding } from "./components/openai-onboarding"; function OnboardingPage() { - const { data: settingsDb, isLoading: isSettingsLoading } = - useGetSettingsQuery(); + const { data: settingsDb, isLoading: isSettingsLoading } = + useGetSettingsQuery(); - const redirect = "/"; + const redirect = "/"; - const router = useRouter(); + const router = useRouter(); - // Redirect if already authenticated or in no-auth mode - useEffect(() => { - if (!isSettingsLoading && settingsDb && settingsDb.edited) { - router.push(redirect); - } - }, [isSettingsLoading, settingsDb, router]); + // Redirect if already authenticated or in no-auth mode + useEffect(() => { + if (!isSettingsLoading && settingsDb && settingsDb.edited) { + router.push(redirect); + } + }, [isSettingsLoading, settingsDb, router]); - const [modelProvider, setModelProvider] = useState("openai"); + const [modelProvider, setModelProvider] = useState("openai"); - const [sampleDataset, setSampleDataset] = useState(true); + const [sampleDataset, setSampleDataset] = useState(true); - const handleSetModelProvider = (provider: string) => { - setModelProvider(provider); - setSettings({ - model_provider: provider, - embedding_model: "", - llm_model: "", - }); - }; + const handleSetModelProvider = (provider: string) => { + setModelProvider(provider); + setSettings({ + model_provider: provider, + embedding_model: "", + llm_model: "", + }); + }; - const [settings, setSettings] = useState({ - model_provider: modelProvider, - embedding_model: "", - llm_model: "", - }); + const [settings, setSettings] = useState({ + model_provider: modelProvider, + embedding_model: "", + llm_model: "", + }); - // Mutations - const onboardingMutation = useOnboardingMutation({ - onSuccess: (data) => { - toast.success("Onboarding completed successfully!"); - console.log("Onboarding completed successfully", data); - router.push(redirect); - }, - onError: (error) => { - toast.error("Failed to complete onboarding", { - description: error.message, - }); - }, - }); + // Mutations + const onboardingMutation = useOnboardingMutation({ + onSuccess: (data) => { + toast.success("Onboarding completed successfully!"); + console.log("Onboarding completed successfully", data); + router.push(redirect); + }, + onError: (error) => { + toast.error("Failed to complete onboarding", { + description: error.message, + }); + }, + }); - const handleComplete = () => { - if ( - !settings.model_provider || - !settings.llm_model || - !settings.embedding_model - ) { - toast.error("Please complete all required fields"); - return; - } + const handleComplete = () => { + if ( + !settings.model_provider || + !settings.llm_model || + !settings.embedding_model + ) { + toast.error("Please complete all required fields"); + return; + } - // Prepare onboarding data - const onboardingData: OnboardingVariables = { - model_provider: settings.model_provider, - llm_model: settings.llm_model, - embedding_model: settings.embedding_model, - sample_data: sampleDataset, - }; + // Prepare onboarding data + const onboardingData: OnboardingVariables = { + model_provider: settings.model_provider, + llm_model: settings.llm_model, + embedding_model: settings.embedding_model, + sample_data: sampleDataset, + }; - // Add API key if available - if (settings.api_key) { - onboardingData.api_key = settings.api_key; - } + // Add API key if available + if (settings.api_key) { + onboardingData.api_key = settings.api_key; + } - // Add endpoint if available - if (settings.endpoint) { - onboardingData.endpoint = settings.endpoint; - } + // Add endpoint if available + if (settings.endpoint) { + onboardingData.endpoint = settings.endpoint; + } - // Add project_id if available - if (settings.project_id) { - onboardingData.project_id = settings.project_id; - } + // Add project_id if available + if (settings.project_id) { + onboardingData.project_id = settings.project_id; + } - onboardingMutation.mutate(onboardingData); - }; + onboardingMutation.mutate(onboardingData); + }; - const isComplete = !!settings.llm_model && !!settings.embedding_model; + const isComplete = !!settings.llm_model && !!settings.embedding_model; - return ( -
-
-
-

- Configure your models -

-

[description of task]

-
- - - - - - - OpenAI - - - - IBM - - - - Ollama - - - - - - - - - - - - - - - - - - - - - - {!isComplete ? "Please fill in all required fields" : ""} - - - - -
-
- ); + return ( +
+ + +
+
+

+ Connect a model provider +

+
+ + + + + + + OpenAI + + + + IBM + + + + Ollama + + + + + + + + + + + + + + + + + + +
+ +
+
+ {!isComplete && ( + + Please fill in all required fields + + )} +
+
+
+
+
+ ); } export default function ProtectedOnboardingPage() { - return ( - - Loading onboarding...
}> - - - - ); + return ( + + Loading onboarding...
}> + + + + ); } diff --git a/src/main.py b/src/main.py index f78e07bc..7df80b22 100644 --- a/src/main.py +++ b/src/main.py @@ -392,8 +392,6 @@ async def startup_tasks(services): """Startup tasks""" logger.info("Starting startup tasks") await init_index() - # Sample data ingestion is now handled by the onboarding endpoint when sample_data=True - logger.info("Sample data ingestion moved to onboarding endpoint") async def initialize_services(): @@ -927,7 +925,8 @@ async def create_app(): "/settings", require_auth(services["session_manager"])( partial( - settings.update_settings, session_manager=services["session_manager"] + settings.update_settings, + session_manager=services["session_manager"], ) ), methods=["POST"], @@ -939,7 +938,7 @@ async def create_app(): partial( models.get_openai_models, models_service=services["models_service"], - session_manager=services["session_manager"] + session_manager=services["session_manager"], ) ), methods=["GET"], @@ -950,7 +949,7 @@ async def create_app(): partial( models.get_ollama_models, models_service=services["models_service"], - session_manager=services["session_manager"] + session_manager=services["session_manager"], ) ), methods=["GET"], @@ -961,7 +960,7 @@ async def create_app(): partial( models.get_ibm_models, models_service=services["models_service"], - session_manager=services["session_manager"] + session_manager=services["session_manager"], ) ), methods=["GET", "POST"], @@ -970,10 +969,7 @@ async def create_app(): Route( "/onboarding", require_auth(services["session_manager"])( - partial( - settings.onboarding, - flows_service=services["flows_service"] - ) + partial(settings.onboarding, flows_service=services["flows_service"]) ), methods=["POST"], ), @@ -983,7 +979,7 @@ async def create_app(): require_auth(services["session_manager"])( partial( settings.update_docling_preset, - session_manager=services["session_manager"] + session_manager=services["session_manager"], ) ), methods=["PATCH"],