This commit is contained in:
Lucas Oliveira 2025-12-03 17:27:08 -03:00
parent 97c9fdb2c6
commit b20da9ab58
2 changed files with 309 additions and 309 deletions

View file

@ -11,170 +11,170 @@ import { useUpdateSettings } from "../_hooks/useUpdateSettings";
import { ModelSelector } from "./model-selector"; import { ModelSelector } from "./model-selector";
export function OllamaOnboarding({ export function OllamaOnboarding({
setSettings, setSettings,
sampleDataset, sampleDataset,
setSampleDataset, setSampleDataset,
setIsLoadingModels, setIsLoadingModels,
isEmbedding = false, isEmbedding = false,
alreadyConfigured = false, alreadyConfigured = false,
existingEndpoint, existingEndpoint,
}: { }: {
setSettings: Dispatch<SetStateAction<OnboardingVariables>>; setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
sampleDataset: boolean; sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void; setSampleDataset: (dataset: boolean) => void;
setIsLoadingModels?: (isLoading: boolean) => void; setIsLoadingModels?: (isLoading: boolean) => void;
isEmbedding?: boolean; isEmbedding?: boolean;
alreadyConfigured?: boolean; alreadyConfigured?: boolean;
existingEndpoint?: string; existingEndpoint?: string;
}) { }) {
const [endpoint, setEndpoint] = useState( const [endpoint, setEndpoint] = useState(
alreadyConfigured alreadyConfigured
? undefined ? undefined
: existingEndpoint || `http://localhost:11434`, : existingEndpoint || `http://localhost:11434`,
); );
const [showConnecting, setShowConnecting] = useState(false); const [showConnecting, setShowConnecting] = useState(false);
const debouncedEndpoint = useDebouncedValue(endpoint, 500); const debouncedEndpoint = useDebouncedValue(endpoint, 500);
// Fetch models from API when endpoint is provided (debounced) // Fetch models from API when endpoint is provided (debounced)
const { const {
data: modelsData, data: modelsData,
isLoading: isLoadingModels, isLoading: isLoadingModels,
error: modelsError, error: modelsError,
} = useGetOllamaModelsQuery( } = useGetOllamaModelsQuery(
debouncedEndpoint ? { endpoint: debouncedEndpoint } : undefined, debouncedEndpoint ? { endpoint: debouncedEndpoint } : undefined,
{ enabled: !!debouncedEndpoint || alreadyConfigured || alreadyConfigured }, { enabled: !!debouncedEndpoint || alreadyConfigured || alreadyConfigured },
); );
// Use custom hook for model selection logic // Use custom hook for model selection logic
const { const {
languageModel, languageModel,
embeddingModel, embeddingModel,
setLanguageModel, setLanguageModel,
setEmbeddingModel, setEmbeddingModel,
languageModels, languageModels,
embeddingModels, embeddingModels,
} = useModelSelection(modelsData, isEmbedding); } = useModelSelection(modelsData, isEmbedding);
// Handle delayed display of connecting state // Handle delayed display of connecting state
useEffect(() => { useEffect(() => {
let timeoutId: NodeJS.Timeout; let timeoutId: NodeJS.Timeout;
if (debouncedEndpoint && isLoadingModels) { if (debouncedEndpoint && isLoadingModels) {
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
setIsLoadingModels?.(true); setIsLoadingModels?.(true);
setShowConnecting(true); setShowConnecting(true);
}, 500); }, 500);
} else { } else {
setShowConnecting(false); setShowConnecting(false);
setIsLoadingModels?.(false); setIsLoadingModels?.(false);
} }
return () => { return () => {
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
}; };
}, [debouncedEndpoint, isLoadingModels, setIsLoadingModels]); }, [debouncedEndpoint, isLoadingModels, setIsLoadingModels]);
// Update settings when values change // Update settings when values change
useUpdateSettings( useUpdateSettings(
"ollama", "ollama",
{ {
endpoint, endpoint,
languageModel, languageModel,
embeddingModel, embeddingModel,
}, },
setSettings, setSettings,
isEmbedding, isEmbedding,
); );
// Check validation state based on models query // Check validation state based on models query
const hasConnectionError = debouncedEndpoint && modelsError; const hasConnectionError = debouncedEndpoint && modelsError;
const hasNoModels = const hasNoModels =
modelsData && modelsData &&
!modelsData.language_models?.length && !modelsData.language_models?.length &&
!modelsData.embedding_models?.length; !modelsData.embedding_models?.length;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<LabelInput <LabelInput
label="Ollama Base URL" label="Ollama Base URL"
helperText="Base URL of your Ollama server" helperText="Base URL of your Ollama server"
id="api-endpoint" id="api-endpoint"
required required
placeholder={ placeholder={
alreadyConfigured alreadyConfigured
? "http://••••••••••••••••••••" ? "http://••••••••••••••••••••"
: "http://localhost:11434" : "http://localhost:11434"
} }
value={endpoint} value={endpoint}
onChange={(e) => setEndpoint(e.target.value)} onChange={(e) => setEndpoint(e.target.value)}
disabled={alreadyConfigured} disabled={alreadyConfigured}
/> />
{alreadyConfigured && ( {alreadyConfigured && (
<p className="text-mmd text-muted-foreground"> <p className="text-mmd text-muted-foreground">
Reusing endpoint from model provider selection. Reusing endpoint from model provider selection.
</p> </p>
)} )}
{showConnecting && ( {showConnecting && (
<p className="text-mmd text-muted-foreground"> <p className="text-mmd text-muted-foreground">
Connecting to Ollama server... Connecting to Ollama server...
</p> </p>
)} )}
{hasConnectionError && ( {hasConnectionError && (
<p className="text-mmd text-accent-amber-foreground"> <p className="text-mmd text-accent-amber-foreground">
Can't reach Ollama at {debouncedEndpoint}. Update the base URL or Can't reach Ollama at {debouncedEndpoint}. Update the base URL or
start the server. start the server.
</p> </p>
)} )}
{hasNoModels && ( {hasNoModels && (
<p className="text-mmd text-accent-amber-foreground"> <p className="text-mmd text-accent-amber-foreground">
No models found. Install embedding and agent models on your Ollama No models found. Install embedding and agent models on your Ollama
server. server.
</p> </p>
)} )}
</div> </div>
{isEmbedding && setEmbeddingModel && ( {isEmbedding && setEmbeddingModel && (
<LabelWrapper <LabelWrapper
label="Embedding model" label="Embedding model"
helperText="Model used for knowledge ingest and retrieval" helperText="Model used for knowledge ingest and retrieval"
id="embedding-model" id="embedding-model"
required={true} required={true}
> >
<ModelSelector <ModelSelector
options={embeddingModels} options={embeddingModels}
icon={<OllamaLogo className="w-4 h-4" />} icon={<OllamaLogo className="w-4 h-4" />}
noOptionsPlaceholder={ noOptionsPlaceholder={
isLoadingModels isLoadingModels
? "Loading models..." ? "Loading models..."
: "No embedding models detected. Install an embedding model to continue." : "No embedding models detected. Install an embedding model to continue."
} }
value={embeddingModel} value={embeddingModel}
onValueChange={setEmbeddingModel} onValueChange={setEmbeddingModel}
/> />
</LabelWrapper> </LabelWrapper>
)} )}
{!isEmbedding && setLanguageModel && ( {!isEmbedding && setLanguageModel && (
<LabelWrapper <LabelWrapper
label="Language model" label="Language model"
helperText="Model used for chat" helperText="Model used for chat"
id="embedding-model" id="embedding-model"
required={true} required={true}
> >
<ModelSelector <ModelSelector
options={languageModels} options={languageModels}
icon={<OllamaLogo className="w-4 h-4" />} icon={<OllamaLogo className="w-4 h-4" />}
noOptionsPlaceholder={ noOptionsPlaceholder={
isLoadingModels isLoadingModels
? "Loading models..." ? "Loading models..."
: "No language models detected. Install a language model to continue." : "No language models detected. Install a language model to continue."
} }
value={languageModel} value={languageModel}
onValueChange={setLanguageModel} onValueChange={setLanguageModel}
/> />
</LabelWrapper> </LabelWrapper>
)} )}
</div> </div>
); );
} }

View file

@ -5,9 +5,9 @@ import { LabelInput } from "@/components/label-input";
import { LabelWrapper } from "@/components/label-wrapper"; import { LabelWrapper } from "@/components/label-wrapper";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/lib/debounce"; import { useDebouncedValue } from "@/lib/debounce";
import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation"; import type { OnboardingVariables } from "../../api/mutations/useOnboardingMutation";
@ -17,161 +17,161 @@ import { useUpdateSettings } from "../_hooks/useUpdateSettings";
import { AdvancedOnboarding } from "./advanced"; import { AdvancedOnboarding } from "./advanced";
export function OpenAIOnboarding({ export function OpenAIOnboarding({
setSettings, setSettings,
sampleDataset, sampleDataset,
setSampleDataset, setSampleDataset,
setIsLoadingModels, setIsLoadingModels,
isEmbedding = false, isEmbedding = false,
hasEnvApiKey = false, hasEnvApiKey = false,
alreadyConfigured = false, alreadyConfigured = false,
}: { }: {
setSettings: Dispatch<SetStateAction<OnboardingVariables>>; setSettings: Dispatch<SetStateAction<OnboardingVariables>>;
sampleDataset: boolean; sampleDataset: boolean;
setSampleDataset: (dataset: boolean) => void; setSampleDataset: (dataset: boolean) => void;
setIsLoadingModels?: (isLoading: boolean) => void; setIsLoadingModels?: (isLoading: boolean) => void;
isEmbedding?: boolean; isEmbedding?: boolean;
hasEnvApiKey?: boolean; hasEnvApiKey?: boolean;
alreadyConfigured?: boolean; alreadyConfigured?: boolean;
}) { }) {
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [getFromEnv, setGetFromEnv] = useState( const [getFromEnv, setGetFromEnv] = useState(
hasEnvApiKey && !alreadyConfigured, hasEnvApiKey && !alreadyConfigured,
); );
const debouncedApiKey = useDebouncedValue(apiKey, 500); const debouncedApiKey = useDebouncedValue(apiKey, 500);
// Fetch models from API when API key is provided // Fetch models from API when API key is provided
const { const {
data: modelsData, data: modelsData,
isLoading: isLoadingModels, isLoading: isLoadingModels,
error: modelsError, error: modelsError,
} = useGetOpenAIModelsQuery( } = useGetOpenAIModelsQuery(
getFromEnv getFromEnv
? { apiKey: "" } ? { apiKey: "" }
: debouncedApiKey : debouncedApiKey
? { apiKey: debouncedApiKey } ? { apiKey: debouncedApiKey }
: undefined, : undefined,
{ {
// Only validate when the user opts in (env) or provides a key. // Only validate when the user opts in (env) or provides a key.
// If a key was previously configured, let the user decide to reuse or replace it // If a key was previously configured, let the user decide to reuse or replace it
// without triggering an immediate validation error. // without triggering an immediate validation error.
enabled: debouncedApiKey !== "" || getFromEnv || alreadyConfigured, enabled: debouncedApiKey !== "" || getFromEnv || alreadyConfigured,
}, },
); );
// Use custom hook for model selection logic // Use custom hook for model selection logic
const { const {
languageModel, languageModel,
embeddingModel, embeddingModel,
setLanguageModel, setLanguageModel,
setEmbeddingModel, setEmbeddingModel,
languageModels, languageModels,
embeddingModels, embeddingModels,
} = useModelSelection(modelsData, isEmbedding); } = useModelSelection(modelsData, isEmbedding);
const handleSampleDatasetChange = (dataset: boolean) => { const handleSampleDatasetChange = (dataset: boolean) => {
setSampleDataset(dataset); setSampleDataset(dataset);
}; };
const handleGetFromEnvChange = (fromEnv: boolean) => { const handleGetFromEnvChange = (fromEnv: boolean) => {
setGetFromEnv(fromEnv); setGetFromEnv(fromEnv);
if (fromEnv) { if (fromEnv) {
setApiKey(""); setApiKey("");
} }
setEmbeddingModel?.(""); setEmbeddingModel?.("");
setLanguageModel?.(""); setLanguageModel?.("");
}; };
useEffect(() => { useEffect(() => {
setIsLoadingModels?.(isLoadingModels); setIsLoadingModels?.(isLoadingModels);
}, [isLoadingModels, setIsLoadingModels]); }, [isLoadingModels, setIsLoadingModels]);
// Update settings when values change // Update settings when values change
useUpdateSettings( useUpdateSettings(
"openai", "openai",
{ {
apiKey, apiKey,
languageModel, languageModel,
embeddingModel, embeddingModel,
}, },
setSettings, setSettings,
isEmbedding, isEmbedding,
); );
return ( return (
<> <>
<div className="space-y-5"> <div className="space-y-5">
{!alreadyConfigured && ( {!alreadyConfigured && (
<LabelWrapper <LabelWrapper
label="Use environment OpenAI API key" label="Use environment OpenAI API key"
id="get-api-key" id="get-api-key"
description="Reuse the key from your environment config. Turn off to enter a different key." description="Reuse the key from your environment config. Turn off to enter a different key."
flex flex
> >
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <div>
<Switch <Switch
checked={getFromEnv} checked={getFromEnv}
onCheckedChange={handleGetFromEnvChange} onCheckedChange={handleGetFromEnvChange}
disabled={!hasEnvApiKey} disabled={!hasEnvApiKey}
/> />
</div> </div>
</TooltipTrigger> </TooltipTrigger>
{!hasEnvApiKey && ( {!hasEnvApiKey && (
<TooltipContent> <TooltipContent>
OpenAI API key not detected in the environment. OpenAI API key not detected in the environment.
</TooltipContent> </TooltipContent>
)} )}
</Tooltip> </Tooltip>
</LabelWrapper> </LabelWrapper>
)} )}
{(!getFromEnv || alreadyConfigured) && ( {(!getFromEnv || alreadyConfigured) && (
<div className="space-y-1"> <div className="space-y-1">
<LabelInput <LabelInput
label="OpenAI API key" label="OpenAI API key"
helperText="The API key for your OpenAI account." helperText="The API key for your OpenAI account."
className={modelsError ? "!border-destructive" : ""} className={modelsError ? "!border-destructive" : ""}
id="api-key" id="api-key"
type="password" type="password"
required required
placeholder={ placeholder={
alreadyConfigured alreadyConfigured
? "sk-•••••••••••••••••••••••••••••••••••••••••" ? "sk-•••••••••••••••••••••••••••••••••••••••••"
: "sk-..." : "sk-..."
} }
value={apiKey} value={apiKey}
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
// Even if a key exists, allow replacing it to avoid getting stuck on stale creds. // Even if a key exists, allow replacing it to avoid getting stuck on stale creds.
disabled={false} disabled={false}
/> />
{alreadyConfigured && ( {alreadyConfigured && (
<p className="text-mmd text-muted-foreground"> <p className="text-mmd text-muted-foreground">
Existing OpenAI key detected. You can reuse it or enter a new Existing OpenAI key detected. You can reuse it or enter a new
one. one.
</p> </p>
)} )}
{isLoadingModels && ( {isLoadingModels && (
<p className="text-mmd text-muted-foreground"> <p className="text-mmd text-muted-foreground">
Validating API key... Validating API key...
</p> </p>
)} )}
{modelsError && ( {modelsError && (
<p className="text-mmd text-destructive"> <p className="text-mmd text-destructive">
Invalid OpenAI API key. Verify or replace the key. Invalid OpenAI API key. Verify or replace the key.
</p> </p>
)} )}
</div> </div>
)} )}
</div> </div>
<AdvancedOnboarding <AdvancedOnboarding
icon={<OpenAILogo className="w-4 h-4" />} icon={<OpenAILogo className="w-4 h-4" />}
languageModels={languageModels} languageModels={languageModels}
embeddingModels={embeddingModels} embeddingModels={embeddingModels}
languageModel={languageModel} languageModel={languageModel}
embeddingModel={embeddingModel} embeddingModel={embeddingModel}
sampleDataset={sampleDataset} sampleDataset={sampleDataset}
setLanguageModel={setLanguageModel} setLanguageModel={setLanguageModel}
setSampleDataset={handleSampleDatasetChange} setSampleDataset={handleSampleDatasetChange}
setEmbeddingModel={setEmbeddingModel} setEmbeddingModel={setEmbeddingModel}
/> />
</> </>
); );
} }