better settings form validation, grouped model selection (#383)
* better form validation, grouped model selection * bump version * fix fe build issue * fix test * change linting error * Fixed integration tests * fixed tests * sample commit --------- Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
parent
a553fb7d9b
commit
1385fd5d5c
14 changed files with 1906 additions and 1798 deletions
|
|
@ -17,8 +17,22 @@ import {
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type ModelOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
default?: boolean;
|
||||||
|
provider?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupedModelOption = {
|
||||||
|
group: string;
|
||||||
|
options: ModelOption[];
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export function ModelSelector({
|
export function ModelSelector({
|
||||||
options,
|
options,
|
||||||
|
groupedOptions,
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange,
|
onValueChange,
|
||||||
icon,
|
icon,
|
||||||
|
|
@ -28,33 +42,40 @@ export function ModelSelector({
|
||||||
custom = false,
|
custom = false,
|
||||||
hasError = false,
|
hasError = false,
|
||||||
}: {
|
}: {
|
||||||
options: {
|
options?: ModelOption[];
|
||||||
value: string;
|
groupedOptions?: GroupedModelOption[];
|
||||||
label: string;
|
|
||||||
default?: boolean;
|
|
||||||
}[];
|
|
||||||
value: string;
|
value: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
noOptionsPlaceholder?: string;
|
noOptionsPlaceholder?: string;
|
||||||
custom?: boolean;
|
custom?: boolean;
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string, provider?: string) => void;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
|
||||||
|
// Flatten grouped options or use regular options
|
||||||
|
const allOptions =
|
||||||
|
groupedOptions?.flatMap((group) => group.options) || options || [];
|
||||||
|
|
||||||
|
// Find the group icon for the selected value
|
||||||
|
const selectedOptionGroup = groupedOptions?.find((group) =>
|
||||||
|
group.options.some((opt) => opt.value === value)
|
||||||
|
);
|
||||||
|
const selectedIcon = selectedOptionGroup?.icon || icon;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
value &&
|
value &&
|
||||||
value !== "" &&
|
value !== "" &&
|
||||||
!options.find((option) => option.value === value) &&
|
!allOptions.find((option) => option.value === value) &&
|
||||||
!custom
|
!custom
|
||||||
) {
|
) {
|
||||||
onValueChange("");
|
onValueChange("");
|
||||||
}
|
}
|
||||||
}, [options, value, custom, onValueChange]);
|
}, [allOptions, value, custom, onValueChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen} modal={false}>
|
<Popover open={open} onOpenChange={setOpen} modal={false}>
|
||||||
|
|
@ -63,7 +84,7 @@ export function ModelSelector({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
disabled={options.length === 0}
|
disabled={allOptions.length === 0}
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-full gap-2 justify-between font-normal text-sm",
|
"w-full gap-2 justify-between font-normal text-sm",
|
||||||
|
|
@ -72,24 +93,18 @@ export function ModelSelector({
|
||||||
>
|
>
|
||||||
{value ? (
|
{value ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{icon && <div className="w-4 h-4">{icon}</div>}
|
{selectedIcon && <div className="w-4 h-4">{selectedIcon}</div>}
|
||||||
{options.find((framework) => framework.value === value)?.label ||
|
{allOptions.find((framework) => framework.value === value)
|
||||||
value}
|
?.label || value}
|
||||||
{/* {options.find((framework) => framework.value === value)
|
|
||||||
?.default && (
|
|
||||||
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
|
||||||
Default
|
|
||||||
</span>
|
|
||||||
)} */}
|
|
||||||
{custom &&
|
{custom &&
|
||||||
value &&
|
value &&
|
||||||
!options.find((framework) => framework.value === value) && (
|
!allOptions.find((framework) => framework.value === value) && (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
CUSTOM
|
CUSTOM
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : options.length === 0 ? (
|
) : allOptions.length === 0 ? (
|
||||||
noOptionsPlaceholder
|
noOptionsPlaceholder
|
||||||
) : (
|
) : (
|
||||||
placeholder
|
placeholder
|
||||||
|
|
@ -113,42 +128,52 @@ export function ModelSelector({
|
||||||
onWheel={(e) => e.stopPropagation()}
|
onWheel={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
|
<CommandEmpty>{noOptionsPlaceholder}</CommandEmpty>
|
||||||
<CommandGroup>
|
{groupedOptions ? (
|
||||||
{options.map((option) => (
|
groupedOptions.map((group) => (
|
||||||
<CommandItem
|
<CommandGroup
|
||||||
key={option.value}
|
key={group.group}
|
||||||
value={option.value}
|
heading={
|
||||||
onSelect={(currentValue) => {
|
<div className="flex items-center gap-2">
|
||||||
if (currentValue !== value) {
|
{group.icon && (
|
||||||
onValueChange(currentValue);
|
<div className="w-4 h-4">{group.icon}</div>
|
||||||
}
|
)}
|
||||||
setOpen(false);
|
<span>{group.group}</span>
|
||||||
}}
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
{group.options.map((option) => (
|
||||||
className={cn(
|
<CommandItem
|
||||||
"mr-2 h-4 w-4",
|
key={option.value}
|
||||||
value === option.value ? "opacity-100" : "opacity-0"
|
value={option.value}
|
||||||
)}
|
onSelect={(currentValue) => {
|
||||||
/>
|
if (currentValue !== value) {
|
||||||
<div className="flex items-center gap-2">
|
onValueChange(currentValue, option.provider);
|
||||||
{option.label}
|
}
|
||||||
{/* {option.default && (
|
setOpen(false);
|
||||||
<span className="text-xs text-foreground p-1 rounded-md bg-muted"> // DISABLING DEFAULT TAG FOR NOW
|
}}
|
||||||
Default
|
>
|
||||||
</span>
|
<CheckIcon
|
||||||
)} */}
|
className={cn(
|
||||||
</div>
|
"mr-2 h-4 w-4",
|
||||||
</CommandItem>
|
value === option.value ? "opacity-100" : "opacity-0"
|
||||||
))}
|
)}
|
||||||
{custom &&
|
/>
|
||||||
searchValue &&
|
<div className="flex items-center gap-2">
|
||||||
!options.find((option) => option.value === searchValue) && (
|
{option.label}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<CommandGroup>
|
||||||
|
{allOptions.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={searchValue}
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
onSelect={(currentValue) => {
|
onSelect={(currentValue) => {
|
||||||
if (currentValue !== value) {
|
if (currentValue !== value) {
|
||||||
onValueChange(currentValue);
|
onValueChange(currentValue, option.provider);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|
@ -156,18 +181,44 @@ export function ModelSelector({
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"mr-2 h-4 w-4",
|
||||||
value === searchValue ? "opacity-100" : "opacity-0"
|
value === option.value ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{searchValue}
|
{option.label}
|
||||||
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
|
||||||
Custom
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)}
|
))}
|
||||||
</CommandGroup>
|
{custom &&
|
||||||
|
searchValue &&
|
||||||
|
!allOptions.find(
|
||||||
|
(option) => option.value === searchValue
|
||||||
|
) && (
|
||||||
|
<CommandItem
|
||||||
|
value={searchValue}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
if (currentValue !== value) {
|
||||||
|
onValueChange(currentValue);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
value === searchValue ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{searchValue}
|
||||||
|
<span className="text-xs text-foreground p-1 rounded-md bg-muted">
|
||||||
|
Custom
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const OnboardingUpload = ({ onComplete }: OnboardingUploadProps) => {
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [currentStep, setCurrentStep] = useState<number | null>(null);
|
const [currentStep, setCurrentStep] = useState<number | null>(null);
|
||||||
|
|
||||||
const STEP_LIST = ["Uploading your document", "Generating embeddings", "Ingesting document", "Processing your document"];
|
const STEP_LIST = ["Uploading your document", "Processing your document"];
|
||||||
|
|
||||||
// Query tasks to track completion
|
// Query tasks to track completion
|
||||||
const { data: tasks } = useGetTasksQuery({
|
const { data: tasks } = useGetTasksQuery({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
|
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
|
||||||
|
|
@ -8,134 +9,150 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth
|
||||||
import AnthropicLogo from "@/components/logo/anthropic-logo";
|
import AnthropicLogo from "@/components/logo/anthropic-logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useDebouncedValue } from "@/lib/debounce";
|
|
||||||
import {
|
import {
|
||||||
AnthropicSettingsForm,
|
AnthropicSettingsForm,
|
||||||
type AnthropicSettingsFormData,
|
type AnthropicSettingsFormData,
|
||||||
} from "./anthropic-settings-form";
|
} from "./anthropic-settings-form";
|
||||||
|
|
||||||
const AnthropicSettingsDialog = ({
|
const AnthropicSettingsDialog = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const methods = useForm<AnthropicSettingsFormData>({
|
const methods = useForm<AnthropicSettingsFormData>({
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, watch, formState } = methods;
|
const { handleSubmit, watch } = methods;
|
||||||
const apiKey = watch("apiKey");
|
const apiKey = watch("apiKey");
|
||||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
|
||||||
|
|
||||||
const {
|
const { refetch: validateCredentials } = useGetAnthropicModelsQuery(
|
||||||
isLoading: isLoadingModels,
|
{
|
||||||
error: modelsError,
|
apiKey: apiKey,
|
||||||
} = useGetAnthropicModelsQuery(
|
},
|
||||||
{
|
{
|
||||||
apiKey: debouncedApiKey,
|
enabled: false,
|
||||||
},
|
}
|
||||||
{
|
);
|
||||||
enabled: !!debouncedApiKey && open,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasValidationError = !!modelsError || !!formState.errors.apiKey;
|
const settingsMutation = useUpdateSettingsMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
// Update provider health cache to healthy since backend validated the setup
|
||||||
|
const healthData: ProviderHealthResponse = {
|
||||||
|
status: "healthy",
|
||||||
|
message: "Provider is configured and working correctly",
|
||||||
|
provider: "anthropic",
|
||||||
|
};
|
||||||
|
queryClient.setQueryData(["provider", "health"], healthData);
|
||||||
|
|
||||||
const settingsMutation = useUpdateSettingsMutation({
|
toast.success(
|
||||||
onSuccess: () => {
|
"Anthropic credentials saved. Configure models in the Settings page."
|
||||||
// Update provider health cache to healthy since backend validated the setup
|
);
|
||||||
const healthData: ProviderHealthResponse = {
|
setOpen(false);
|
||||||
status: "healthy",
|
},
|
||||||
message: "Provider is configured and working correctly",
|
});
|
||||||
provider: "anthropic",
|
|
||||||
};
|
|
||||||
queryClient.setQueryData(["provider", "health"], healthData);
|
|
||||||
|
|
||||||
toast.success("Anthropic credentials saved. Configure models in the Settings page.");
|
const onSubmit = async (data: AnthropicSettingsFormData) => {
|
||||||
setOpen(false);
|
// Clear any previous validation errors
|
||||||
},
|
setValidationError(null);
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (data: AnthropicSettingsFormData) => {
|
// Only validate if a new API key was entered
|
||||||
const payload: {
|
if (data.apiKey) {
|
||||||
anthropic_api_key?: string;
|
setIsValidating(true);
|
||||||
} = {};
|
const result = await validateCredentials();
|
||||||
|
setIsValidating(false);
|
||||||
|
|
||||||
// Only include api_key if a value was entered
|
if (result.isError) {
|
||||||
if (data.apiKey) {
|
setValidationError(result.error);
|
||||||
payload.anthropic_api_key = data.apiKey;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Submit the update
|
const payload: {
|
||||||
settingsMutation.mutate(payload);
|
anthropic_api_key?: string;
|
||||||
};
|
} = {};
|
||||||
|
|
||||||
return (
|
// Only include api_key if a value was entered
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
if (data.apiKey) {
|
||||||
<DialogContent className="max-w-2xl">
|
payload.anthropic_api_key = data.apiKey;
|
||||||
<FormProvider {...methods}>
|
}
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
|
|
||||||
<DialogHeader className="mb-2">
|
|
||||||
<DialogTitle className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 rounded flex items-center justify-center bg-white border">
|
|
||||||
<AnthropicLogo className="text-black" />
|
|
||||||
</div>
|
|
||||||
Anthropic Setup
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<AnthropicSettingsForm
|
// Submit the update
|
||||||
modelsError={modelsError}
|
settingsMutation.mutate(payload);
|
||||||
isLoadingModels={isLoadingModels}
|
};
|
||||||
/>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
return (
|
||||||
{settingsMutation.isError && (
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<motion.div
|
<DialogContent className="max-w-2xl">
|
||||||
key="error"
|
<FormProvider {...methods}>
|
||||||
initial={{ opacity: 0, y: 10 }}
|
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<DialogHeader className="mb-2">
|
||||||
exit={{ opacity: 0, y: -10 }}
|
<DialogTitle className="flex items-center gap-3">
|
||||||
>
|
<div className="w-8 h-8 rounded flex items-center justify-center bg-white border">
|
||||||
<p className="rounded-lg border border-destructive p-4">
|
<AnthropicLogo className="text-black" />
|
||||||
{settingsMutation.error?.message}
|
</div>
|
||||||
</p>
|
Anthropic Setup
|
||||||
</motion.div>
|
</DialogTitle>
|
||||||
)}
|
</DialogHeader>
|
||||||
</AnimatePresence>
|
|
||||||
<DialogFooter className="mt-4">
|
<AnthropicSettingsForm
|
||||||
<Button
|
modelsError={validationError}
|
||||||
variant="outline"
|
isLoadingModels={isValidating}
|
||||||
type="button"
|
/>
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
<AnimatePresence mode="wait">
|
||||||
Cancel
|
{settingsMutation.isError && (
|
||||||
</Button>
|
<motion.div
|
||||||
<Button
|
key="error"
|
||||||
type="submit"
|
initial={{ opacity: 0, y: 10 }}
|
||||||
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
exit={{ opacity: 0, y: -10 }}
|
||||||
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"}
|
>
|
||||||
</Button>
|
<p className="rounded-lg border border-destructive p-4">
|
||||||
</DialogFooter>
|
{settingsMutation.error?.message}
|
||||||
</form>
|
</p>
|
||||||
</FormProvider>
|
</motion.div>
|
||||||
</DialogContent>
|
)}
|
||||||
</Dialog>
|
</AnimatePresence>
|
||||||
);
|
<DialogFooter className="mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={settingsMutation.isPending || isValidating}
|
||||||
|
>
|
||||||
|
{settingsMutation.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: isValidating
|
||||||
|
? "Validating..."
|
||||||
|
: "Save"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AnthropicSettingsDialog;
|
export default AnthropicSettingsDialog;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,7 +21,6 @@ import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettings
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
|
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useDebouncedValue } from "@/lib/debounce";
|
|
||||||
|
|
||||||
const OllamaSettingsDialog = ({
|
const OllamaSettingsDialog = ({
|
||||||
open,
|
open,
|
||||||
|
|
@ -31,6 +31,8 @@ const OllamaSettingsDialog = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { isAuthenticated, isNoAuthMode } = useAuth();
|
const { isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const { data: settings = {} } = useGetSettingsQuery({
|
const { data: settings = {} } = useGetSettingsQuery({
|
||||||
enabled: isAuthenticated || isNoAuthMode,
|
enabled: isAuthenticated || isNoAuthMode,
|
||||||
|
|
@ -47,24 +49,18 @@ const OllamaSettingsDialog = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, watch, formState } = methods;
|
const { handleSubmit, watch } = methods;
|
||||||
const endpoint = watch("endpoint");
|
const endpoint = watch("endpoint");
|
||||||
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
|
||||||
|
|
||||||
const {
|
const { refetch: validateCredentials } = useGetOllamaModelsQuery(
|
||||||
isLoading: isLoadingModels,
|
|
||||||
error: modelsError,
|
|
||||||
} = useGetOllamaModelsQuery(
|
|
||||||
{
|
{
|
||||||
endpoint: debouncedEndpoint,
|
endpoint: endpoint,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: formState.isDirty && !!debouncedEndpoint && open,
|
enabled: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasValidationError = !!modelsError || !!formState.errors.endpoint;
|
|
||||||
|
|
||||||
const settingsMutation = useUpdateSettingsMutation({
|
const settingsMutation = useUpdateSettingsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Update provider health cache to healthy since backend validated the setup
|
// Update provider health cache to healthy since backend validated the setup
|
||||||
|
|
@ -75,12 +71,27 @@ const OllamaSettingsDialog = ({
|
||||||
};
|
};
|
||||||
queryClient.setQueryData(["provider", "health"], healthData);
|
queryClient.setQueryData(["provider", "health"], healthData);
|
||||||
|
|
||||||
toast.success("Ollama endpoint saved. Configure models in the Settings page.");
|
toast.success(
|
||||||
|
"Ollama endpoint saved. Configure models in the Settings page."
|
||||||
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: OllamaSettingsFormData) => {
|
const onSubmit = async (data: OllamaSettingsFormData) => {
|
||||||
|
// Clear any previous validation errors
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
|
// Validate endpoint by fetching models
|
||||||
|
setIsValidating(true);
|
||||||
|
const result = await validateCredentials();
|
||||||
|
setIsValidating(false);
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
setValidationError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
settingsMutation.mutate({
|
settingsMutation.mutate({
|
||||||
ollama_endpoint: data.endpoint,
|
ollama_endpoint: data.endpoint,
|
||||||
});
|
});
|
||||||
|
|
@ -101,8 +112,8 @@ const OllamaSettingsDialog = ({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<OllamaSettingsForm
|
<OllamaSettingsForm
|
||||||
modelsError={modelsError}
|
modelsError={validationError}
|
||||||
isLoadingModels={isLoadingModels}
|
isLoadingModels={isValidating}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
|
|
@ -129,9 +140,13 @@ const OllamaSettingsDialog = ({
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels}
|
disabled={settingsMutation.isPending || isValidating}
|
||||||
>
|
>
|
||||||
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"}
|
{settingsMutation.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: isValidating
|
||||||
|
? "Validating..."
|
||||||
|
: "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
import { useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
|
import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettingsMutation";
|
||||||
|
|
@ -8,134 +9,150 @@ import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealth
|
||||||
import OpenAILogo from "@/components/logo/openai-logo";
|
import OpenAILogo from "@/components/logo/openai-logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useDebouncedValue } from "@/lib/debounce";
|
|
||||||
import {
|
import {
|
||||||
OpenAISettingsForm,
|
OpenAISettingsForm,
|
||||||
type OpenAISettingsFormData,
|
type OpenAISettingsFormData,
|
||||||
} from "./openai-settings-form";
|
} from "./openai-settings-form";
|
||||||
|
|
||||||
const OpenAISettingsDialog = ({
|
const OpenAISettingsDialog = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const methods = useForm<OpenAISettingsFormData>({
|
const methods = useForm<OpenAISettingsFormData>({
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, watch, formState } = methods;
|
const { handleSubmit, watch } = methods;
|
||||||
const apiKey = watch("apiKey");
|
const apiKey = watch("apiKey");
|
||||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
|
||||||
|
|
||||||
const {
|
const { refetch: validateCredentials } = useGetOpenAIModelsQuery(
|
||||||
isLoading: isLoadingModels,
|
{
|
||||||
error: modelsError,
|
apiKey: apiKey,
|
||||||
} = useGetOpenAIModelsQuery(
|
},
|
||||||
{
|
{
|
||||||
apiKey: debouncedApiKey,
|
enabled: false,
|
||||||
},
|
}
|
||||||
{
|
);
|
||||||
enabled: !!debouncedApiKey && open,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasValidationError = !!modelsError || !!formState.errors.apiKey;
|
const settingsMutation = useUpdateSettingsMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
// Update provider health cache to healthy since backend validated the setup
|
||||||
|
const healthData: ProviderHealthResponse = {
|
||||||
|
status: "healthy",
|
||||||
|
message: "Provider is configured and working correctly",
|
||||||
|
provider: "openai",
|
||||||
|
};
|
||||||
|
queryClient.setQueryData(["provider", "health"], healthData);
|
||||||
|
|
||||||
const settingsMutation = useUpdateSettingsMutation({
|
toast.success(
|
||||||
onSuccess: () => {
|
"OpenAI credentials saved. Configure models in the Settings page."
|
||||||
// Update provider health cache to healthy since backend validated the setup
|
);
|
||||||
const healthData: ProviderHealthResponse = {
|
setOpen(false);
|
||||||
status: "healthy",
|
},
|
||||||
message: "Provider is configured and working correctly",
|
});
|
||||||
provider: "openai",
|
|
||||||
};
|
|
||||||
queryClient.setQueryData(["provider", "health"], healthData);
|
|
||||||
|
|
||||||
toast.success("OpenAI credentials saved. Configure models in the Settings page.");
|
const onSubmit = async (data: OpenAISettingsFormData) => {
|
||||||
setOpen(false);
|
// Clear any previous validation errors
|
||||||
},
|
setValidationError(null);
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (data: OpenAISettingsFormData) => {
|
// Only validate if a new API key was entered
|
||||||
const payload: {
|
if (data.apiKey) {
|
||||||
openai_api_key?: string;
|
setIsValidating(true);
|
||||||
} = {};
|
const result = await validateCredentials();
|
||||||
|
setIsValidating(false);
|
||||||
|
|
||||||
// Only include api_key if a value was entered
|
if (result.isError) {
|
||||||
if (data.apiKey) {
|
setValidationError(result.error);
|
||||||
payload.openai_api_key = data.apiKey;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Submit the update
|
const payload: {
|
||||||
settingsMutation.mutate(payload);
|
openai_api_key?: string;
|
||||||
};
|
} = {};
|
||||||
|
|
||||||
return (
|
// Only include api_key if a value was entered
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
if (data.apiKey) {
|
||||||
<DialogContent className="max-w-2xl">
|
payload.openai_api_key = data.apiKey;
|
||||||
<FormProvider {...methods}>
|
}
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
|
|
||||||
<DialogHeader className="mb-2">
|
|
||||||
<DialogTitle className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 rounded flex items-center justify-center bg-white border">
|
|
||||||
<OpenAILogo className="text-black" />
|
|
||||||
</div>
|
|
||||||
OpenAI Setup
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<OpenAISettingsForm
|
// Submit the update
|
||||||
modelsError={modelsError}
|
settingsMutation.mutate(payload);
|
||||||
isLoadingModels={isLoadingModels}
|
};
|
||||||
/>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
return (
|
||||||
{settingsMutation.isError && (
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<motion.div
|
<DialogContent className="max-w-2xl">
|
||||||
key="error"
|
<FormProvider {...methods}>
|
||||||
initial={{ opacity: 0, y: 10 }}
|
<form onSubmit={handleSubmit(onSubmit)} className="grid gap-4">
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<DialogHeader className="mb-2">
|
||||||
exit={{ opacity: 0, y: -10 }}
|
<DialogTitle className="flex items-center gap-3">
|
||||||
>
|
<div className="w-8 h-8 rounded flex items-center justify-center bg-white border">
|
||||||
<p className="rounded-lg border border-destructive p-4">
|
<OpenAILogo className="text-black" />
|
||||||
{settingsMutation.error?.message}
|
</div>
|
||||||
</p>
|
OpenAI Setup
|
||||||
</motion.div>
|
</DialogTitle>
|
||||||
)}
|
</DialogHeader>
|
||||||
</AnimatePresence>
|
|
||||||
<DialogFooter className="mt-4">
|
<OpenAISettingsForm
|
||||||
<Button
|
modelsError={validationError}
|
||||||
variant="outline"
|
isLoadingModels={isValidating}
|
||||||
type="button"
|
/>
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
<AnimatePresence mode="wait">
|
||||||
Cancel
|
{settingsMutation.isError && (
|
||||||
</Button>
|
<motion.div
|
||||||
<Button
|
key="error"
|
||||||
type="submit"
|
initial={{ opacity: 0, y: 10 }}
|
||||||
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
>
|
exit={{ opacity: 0, y: -10 }}
|
||||||
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"}
|
>
|
||||||
</Button>
|
<p className="rounded-lg border border-destructive p-4">
|
||||||
</DialogFooter>
|
{settingsMutation.error?.message}
|
||||||
</form>
|
</p>
|
||||||
</FormProvider>
|
</motion.div>
|
||||||
</DialogContent>
|
)}
|
||||||
</Dialog>
|
</AnimatePresence>
|
||||||
);
|
<DialogFooter className="mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={settingsMutation.isPending || isValidating}
|
||||||
|
>
|
||||||
|
{settingsMutation.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: isValidating
|
||||||
|
? "Validating..."
|
||||||
|
: "Save"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OpenAISettingsDialog;
|
export default OpenAISettingsDialog;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
|
|
@ -18,7 +19,6 @@ import { useUpdateSettingsMutation } from "@/app/api/mutations/useUpdateSettings
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
|
import type { ProviderHealthResponse } from "@/app/api/queries/useProviderHealthQuery";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { useDebouncedValue } from "@/lib/debounce";
|
|
||||||
|
|
||||||
const WatsonxSettingsDialog = ({
|
const WatsonxSettingsDialog = ({
|
||||||
open,
|
open,
|
||||||
|
|
@ -28,6 +28,8 @@ const WatsonxSettingsDialog = ({
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const methods = useForm<WatsonxSettingsFormData>({
|
const methods = useForm<WatsonxSettingsFormData>({
|
||||||
mode: "onSubmit",
|
mode: "onSubmit",
|
||||||
|
|
@ -38,31 +40,22 @@ const WatsonxSettingsDialog = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleSubmit, watch, formState } = methods;
|
const { handleSubmit, watch } = methods;
|
||||||
const endpoint = watch("endpoint");
|
const endpoint = watch("endpoint");
|
||||||
const apiKey = watch("apiKey");
|
const apiKey = watch("apiKey");
|
||||||
const projectId = watch("projectId");
|
const projectId = watch("projectId");
|
||||||
|
|
||||||
const debouncedEndpoint = useDebouncedValue(endpoint, 500);
|
const { refetch: validateCredentials } = useGetIBMModelsQuery(
|
||||||
const debouncedApiKey = useDebouncedValue(apiKey, 500);
|
|
||||||
const debouncedProjectId = useDebouncedValue(projectId, 500);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLoading: isLoadingModels,
|
|
||||||
error: modelsError,
|
|
||||||
} = useGetIBMModelsQuery(
|
|
||||||
{
|
{
|
||||||
endpoint: debouncedEndpoint,
|
endpoint: endpoint,
|
||||||
apiKey: debouncedApiKey,
|
apiKey: apiKey,
|
||||||
projectId: debouncedProjectId,
|
projectId: projectId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!debouncedEndpoint && !!debouncedApiKey && !!debouncedProjectId && open,
|
enabled: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasValidationError = !!modelsError || !!formState.errors.endpoint || !!formState.errors.apiKey || !!formState.errors.projectId;
|
|
||||||
|
|
||||||
const settingsMutation = useUpdateSettingsMutation({
|
const settingsMutation = useUpdateSettingsMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Update provider health cache to healthy since backend validated the setup
|
// Update provider health cache to healthy since backend validated the setup
|
||||||
|
|
@ -72,12 +65,27 @@ const WatsonxSettingsDialog = ({
|
||||||
provider: "watsonx",
|
provider: "watsonx",
|
||||||
};
|
};
|
||||||
queryClient.setQueryData(["provider", "health"], healthData);
|
queryClient.setQueryData(["provider", "health"], healthData);
|
||||||
toast.success("watsonx credentials saved. Configure models in the Settings page.");
|
toast.success(
|
||||||
|
"watsonx credentials saved. Configure models in the Settings page."
|
||||||
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: WatsonxSettingsFormData) => {
|
const onSubmit = async (data: WatsonxSettingsFormData) => {
|
||||||
|
// Clear any previous validation errors
|
||||||
|
setValidationError(null);
|
||||||
|
|
||||||
|
// Validate credentials by fetching models
|
||||||
|
setIsValidating(true);
|
||||||
|
const result = await validateCredentials();
|
||||||
|
setIsValidating(false);
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
setValidationError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload: {
|
const payload: {
|
||||||
watsonx_endpoint: string;
|
watsonx_endpoint: string;
|
||||||
watsonx_api_key?: string;
|
watsonx_api_key?: string;
|
||||||
|
|
@ -111,10 +119,10 @@ const WatsonxSettingsDialog = ({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<WatsonxSettingsForm
|
<WatsonxSettingsForm
|
||||||
modelsError={modelsError}
|
modelsError={validationError}
|
||||||
isLoadingModels={isLoadingModels}
|
isLoadingModels={isValidating}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{settingsMutation.isError && (
|
{settingsMutation.isError && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -139,9 +147,13 @@ const WatsonxSettingsDialog = ({
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={settingsMutation.isPending || hasValidationError || isLoadingModels}
|
disabled={settingsMutation.isPending || isValidating}
|
||||||
>
|
>
|
||||||
{settingsMutation.isPending ? "Saving..." : isLoadingModels ? "Validating..." : "Save"}
|
{settingsMutation.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: isValidating
|
||||||
|
? "Validating..."
|
||||||
|
: "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,189 +1,204 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { IngestSettings as IngestSettingsType } from "./types";
|
import { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
useGetIBMModelsQuery,
|
||||||
|
useGetOllamaModelsQuery,
|
||||||
|
useGetOpenAIModelsQuery,
|
||||||
|
} from "@/app/api/queries/useGetModelsQuery";
|
||||||
|
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||||
|
import type { ModelOption } from "@/app/onboarding/components/model-selector";
|
||||||
|
import {
|
||||||
|
getFallbackModels,
|
||||||
|
type ModelProvider,
|
||||||
|
} from "@/app/settings/helpers/model-helpers";
|
||||||
|
import { ModelSelectItems } from "@/app/settings/helpers/model-select-item";
|
||||||
import { LabelWrapper } from "@/components/label-wrapper";
|
import { LabelWrapper } from "@/components/label-wrapper";
|
||||||
import {
|
import {
|
||||||
Select,
|
Collapsible,
|
||||||
SelectContent,
|
CollapsibleContent,
|
||||||
SelectTrigger,
|
CollapsibleTrigger,
|
||||||
SelectValue,
|
} from "@/components/ui/collapsible";
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { ModelSelectItems } from "@/app/settings/helpers/model-select-item";
|
|
||||||
import { getFallbackModels, type ModelProvider } from "@/app/settings/helpers/model-helpers";
|
|
||||||
import { NumberInput } from "@/components/ui/inputs/number-input";
|
import { NumberInput } from "@/components/ui/inputs/number-input";
|
||||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
|
||||||
import {
|
import {
|
||||||
useGetOpenAIModelsQuery,
|
Select,
|
||||||
useGetOllamaModelsQuery,
|
SelectContent,
|
||||||
useGetIBMModelsQuery,
|
SelectTrigger,
|
||||||
} from "@/app/api/queries/useGetModelsQuery";
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useEffect } from "react";
|
import type { IngestSettings as IngestSettingsType } from "./types";
|
||||||
|
|
||||||
interface IngestSettingsProps {
|
interface IngestSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
settings?: IngestSettingsType;
|
settings?: IngestSettingsType;
|
||||||
onSettingsChange?: (settings: IngestSettingsType) => void;
|
onSettingsChange?: (settings: IngestSettingsType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IngestSettings = ({
|
export const IngestSettings = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
settings,
|
settings,
|
||||||
onSettingsChange,
|
onSettingsChange,
|
||||||
}: IngestSettingsProps) => {
|
}: IngestSettingsProps) => {
|
||||||
const { isAuthenticated, isNoAuthMode } = useAuth();
|
const { isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
|
|
||||||
// Fetch settings from API to get current embedding model
|
// Fetch settings from API to get current embedding model
|
||||||
const { data: apiSettings = {} } = useGetSettingsQuery({
|
const { data: apiSettings = {} } = useGetSettingsQuery({
|
||||||
enabled: isAuthenticated || isNoAuthMode,
|
enabled: isAuthenticated || isNoAuthMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the current provider from API settings
|
// Get the current provider from API settings
|
||||||
const currentProvider = (apiSettings.providers?.model_provider ||
|
const currentProvider = (apiSettings.knowledge?.embedding_provider ||
|
||||||
"openai") as ModelProvider;
|
"openai") as ModelProvider;
|
||||||
|
|
||||||
// Fetch available models based on provider
|
// Fetch available models based on provider
|
||||||
const { data: openaiModelsData } = useGetOpenAIModelsQuery(undefined, {
|
const { data: openaiModelsData } = useGetOpenAIModelsQuery(undefined, {
|
||||||
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "openai",
|
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "openai",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: ollamaModelsData } = useGetOllamaModelsQuery(undefined, {
|
const { data: ollamaModelsData } = useGetOllamaModelsQuery(undefined, {
|
||||||
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "ollama",
|
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "ollama",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: ibmModelsData } = useGetIBMModelsQuery(undefined, {
|
const { data: ibmModelsData } = useGetIBMModelsQuery(undefined, {
|
||||||
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "watsonx",
|
enabled: (isAuthenticated || isNoAuthMode) && currentProvider === "watsonx",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select the appropriate models data based on provider
|
// Select the appropriate models data based on provider
|
||||||
const modelsData =
|
const modelsData =
|
||||||
currentProvider === "openai"
|
currentProvider === "openai"
|
||||||
? openaiModelsData
|
? openaiModelsData
|
||||||
: currentProvider === "ollama"
|
: currentProvider === "ollama"
|
||||||
? ollamaModelsData
|
? ollamaModelsData
|
||||||
: currentProvider === "watsonx"
|
: currentProvider === "watsonx"
|
||||||
? ibmModelsData
|
? ibmModelsData
|
||||||
: openaiModelsData;
|
: openaiModelsData;
|
||||||
|
|
||||||
// Get embedding model from API settings
|
// Get embedding model from API settings
|
||||||
const apiEmbeddingModel =
|
const apiEmbeddingModel =
|
||||||
apiSettings.knowledge?.embedding_model ||
|
apiSettings.knowledge?.embedding_model ||
|
||||||
modelsData?.embedding_models?.find((m) => m.default)?.value ||
|
modelsData?.embedding_models?.find((m) => m.default)?.value ||
|
||||||
"text-embedding-3-small";
|
"text-embedding-3-small";
|
||||||
|
|
||||||
// Default settings - use API embedding model
|
// Default settings - use API embedding model
|
||||||
const defaultSettings: IngestSettingsType = {
|
const defaultSettings: IngestSettingsType = {
|
||||||
chunkSize: 1000,
|
chunkSize: 1000,
|
||||||
chunkOverlap: 200,
|
chunkOverlap: 200,
|
||||||
ocr: false,
|
ocr: false,
|
||||||
pictureDescriptions: false,
|
pictureDescriptions: false,
|
||||||
embeddingModel: apiEmbeddingModel,
|
embeddingModel: apiEmbeddingModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use provided settings or defaults
|
// Use provided settings or defaults
|
||||||
const currentSettings = settings || defaultSettings;
|
const currentSettings = settings || defaultSettings;
|
||||||
|
|
||||||
// Update settings when API embedding model changes
|
// Update settings when API embedding model changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (apiEmbeddingModel && (!settings || settings.embeddingModel !== apiEmbeddingModel)) {
|
if (
|
||||||
onSettingsChange?.({ ...currentSettings, embeddingModel: apiEmbeddingModel });
|
apiEmbeddingModel &&
|
||||||
}
|
(!settings || settings.embeddingModel !== apiEmbeddingModel)
|
||||||
}, [apiEmbeddingModel]);
|
) {
|
||||||
|
onSettingsChange?.({
|
||||||
|
...currentSettings,
|
||||||
|
embeddingModel: apiEmbeddingModel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [apiEmbeddingModel, settings, onSettingsChange, currentSettings]);
|
||||||
|
|
||||||
const handleSettingsChange = (newSettings: Partial<IngestSettingsType>) => {
|
const handleSettingsChange = (newSettings: Partial<IngestSettingsType>) => {
|
||||||
const updatedSettings = { ...currentSettings, ...newSettings };
|
const updatedSettings = { ...currentSettings, ...newSettings };
|
||||||
onSettingsChange?.(updatedSettings);
|
onSettingsChange?.(updatedSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
className="border rounded-xl p-4 border-border"
|
className="border rounded-xl p-4 border-border"
|
||||||
>
|
>
|
||||||
<CollapsibleTrigger className="flex items-center gap-2 justify-between w-full -m-4 p-4 rounded-md transition-colors">
|
<CollapsibleTrigger className="flex items-center gap-2 justify-between w-full -m-4 p-4 rounded-md transition-colors">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={`h-4 w-4 text-muted-foreground transition-transform duration-200 ${
|
className={`h-4 w-4 text-muted-foreground transition-transform duration-200 ${
|
||||||
isOpen ? "rotate-90" : ""
|
isOpen ? "rotate-90" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">Ingest settings</span>
|
<span className="text-sm font-medium">Ingest settings</span>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
<CollapsibleContent className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-up-2 data-[state=open]:slide-down-2">
|
<CollapsibleContent className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-up-2 data-[state=open]:slide-down-2">
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{/* Embedding model selection */}
|
{/* Embedding model selection */}
|
||||||
<LabelWrapper
|
<LabelWrapper
|
||||||
helperText="Model used for knowledge ingest and retrieval"
|
helperText="Model used for knowledge ingest and retrieval"
|
||||||
id="embedding-model-select"
|
id="embedding-model-select"
|
||||||
label="Embedding model"
|
label="Embedding model"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
disabled={false}
|
disabled={false}
|
||||||
value={currentSettings.embeddingModel}
|
value={currentSettings.embeddingModel}
|
||||||
onValueChange={(value) => handleSettingsChange({ embeddingModel: value })}
|
onValueChange={(value) =>
|
||||||
>
|
handleSettingsChange({ embeddingModel: value })
|
||||||
<Tooltip>
|
}
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<SelectTrigger id="embedding-model-select">
|
<Tooltip>
|
||||||
<SelectValue placeholder="Select an embedding model" />
|
<TooltipTrigger asChild>
|
||||||
</SelectTrigger>
|
<SelectTrigger id="embedding-model-select">
|
||||||
</TooltipTrigger>
|
<SelectValue placeholder="Select an embedding model" />
|
||||||
<TooltipContent>
|
</SelectTrigger>
|
||||||
Choose the embedding model for this upload
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipContent>
|
||||||
</Tooltip>
|
Choose the embedding model for this upload
|
||||||
<SelectContent>
|
</TooltipContent>
|
||||||
<ModelSelectItems
|
</Tooltip>
|
||||||
models={modelsData?.embedding_models}
|
<SelectContent>
|
||||||
fallbackModels={getFallbackModels(currentProvider).embedding}
|
<ModelSelectItems
|
||||||
provider={currentProvider}
|
models={modelsData?.embedding_models}
|
||||||
/>
|
fallbackModels={
|
||||||
</SelectContent>
|
getFallbackModels(currentProvider)
|
||||||
</Select>
|
.embedding as ModelOption[]
|
||||||
</LabelWrapper>
|
}
|
||||||
</div>
|
provider={currentProvider}
|
||||||
<div className="mt-6">
|
/>
|
||||||
<div className="flex items-center gap-4 w-full mb-6">
|
</SelectContent>
|
||||||
<div className="w-full">
|
</Select>
|
||||||
<NumberInput
|
</LabelWrapper>
|
||||||
id="chunk-size"
|
</div>
|
||||||
label="Chunk size"
|
<div className="mt-6">
|
||||||
value={currentSettings.chunkSize}
|
<div className="flex items-center gap-4 w-full mb-6">
|
||||||
onChange={(value) => handleSettingsChange({ chunkSize: value })}
|
<div className="w-full">
|
||||||
unit="characters"
|
<NumberInput
|
||||||
/>
|
id="chunk-size"
|
||||||
</div>
|
label="Chunk size"
|
||||||
<div className="w-full">
|
value={currentSettings.chunkSize}
|
||||||
<NumberInput
|
onChange={(value) => handleSettingsChange({ chunkSize: value })}
|
||||||
id="chunk-overlap"
|
unit="characters"
|
||||||
label="Chunk overlap"
|
/>
|
||||||
value={currentSettings.chunkOverlap}
|
</div>
|
||||||
onChange={(value) =>
|
<div className="w-full">
|
||||||
handleSettingsChange({ chunkOverlap: value })
|
<NumberInput
|
||||||
}
|
id="chunk-overlap"
|
||||||
unit="characters"
|
label="Chunk overlap"
|
||||||
/>
|
value={currentSettings.chunkOverlap}
|
||||||
</div>
|
onChange={(value) =>
|
||||||
</div>
|
handleSettingsChange({ chunkOverlap: value })
|
||||||
|
}
|
||||||
|
unit="characters"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* <div className="flex gap-2 items-center justify-between">
|
{/* <div className="flex gap-2 items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold pb-2">Table Structure</div>
|
<div className="text-sm font-semibold pb-2">Table Structure</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
|
|
@ -199,39 +214,39 @@ export const IngestSettings = ({
|
||||||
/>
|
/>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-b pb-3 mb-3">
|
<div className="flex items-center justify-between border-b pb-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold pb-2">OCR</div>
|
<div className="text-sm font-semibold pb-2">OCR</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Extracts text from images/PDFs. Ingest is slower when enabled.
|
Extracts text from images/PDFs. Ingest is slower when enabled.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={currentSettings.ocr}
|
checked={currentSettings.ocr}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleSettingsChange({ ocr: checked })
|
handleSettingsChange({ ocr: checked })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm pb-2 font-semibold">
|
<div className="text-sm pb-2 font-semibold">
|
||||||
Picture descriptions
|
Picture descriptions
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Adds captions for images. Ingest is more expensive when enabled.
|
Adds captions for images. Ingest is more expensive when enabled.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={currentSettings.pictureDescriptions}
|
checked={currentSettings.pictureDescriptions}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
handleSettingsChange({ pictureDescriptions: checked })
|
handleSettingsChange({ pictureDescriptions: checked })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "openrag"
|
name = "openrag"
|
||||||
version = "0.1.32"
|
version = "0.1.33"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
|
||||||
|
|
@ -750,4 +750,4 @@ def get_agent_config():
|
||||||
|
|
||||||
def get_embedding_model() -> str:
|
def get_embedding_model() -> str:
|
||||||
"""Return the currently configured embedding model."""
|
"""Return the currently configured embedding model."""
|
||||||
return get_openrag_config().knowledge.embedding_model
|
return get_openrag_config().knowledge.embedding_model or EMBED_MODEL if DISABLE_INGEST_WITH_LANGFLOW else ""
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import copy
|
import copy
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from agentd.tool_decorator import tool
|
from agentd.tool_decorator import tool
|
||||||
from config.settings import clients, INDEX_NAME, get_embedding_model
|
from config.settings import EMBED_MODEL, clients, INDEX_NAME, get_embedding_model
|
||||||
from auth_context import get_auth_context
|
from auth_context import get_auth_context
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
|
@ -34,8 +34,8 @@ class SearchService:
|
||||||
|
|
||||||
# Strategy: Use provided model, or default to the configured embedding
|
# Strategy: Use provided model, or default to the configured embedding
|
||||||
# model. This assumes documents are embedded with that model by default.
|
# model. This assumes documents are embedded with that model by default.
|
||||||
# Future enhancement: Could auto-detect available models in corpus
|
# Future enhancement: Could auto-detect available models in corpus.
|
||||||
embedding_model = embedding_model or get_embedding_model()
|
embedding_model = embedding_model or get_embedding_model() or EMBED_MODEL
|
||||||
embedding_field_name = get_embedding_field_name(embedding_model)
|
embedding_field_name = get_embedding_field_name(embedding_model)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ async def onboard_system():
|
||||||
transport = httpx.ASGITransport(app=app)
|
transport = httpx.ASGITransport(app=app)
|
||||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||||
onboarding_payload = {
|
onboarding_payload = {
|
||||||
"model_provider": "openai",
|
"llm_provider": "openai",
|
||||||
|
"embedding_provider": "openai",
|
||||||
"embedding_model": "text-embedding-3-small",
|
"embedding_model": "text-embedding-3-small",
|
||||||
"llm_model": "gpt-4o-mini",
|
"llm_model": "gpt-4o-mini",
|
||||||
"sample_data": False,
|
"sample_data": False,
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ async def test_upload_and_search_endpoint(tmp_path: Path, disable_langflow_inges
|
||||||
# Ensure we route uploads to traditional processor and disable startup ingest
|
# Ensure we route uploads to traditional processor and disable startup ingest
|
||||||
os.environ["DISABLE_INGEST_WITH_LANGFLOW"] = "true" if disable_langflow_ingest else "false"
|
os.environ["DISABLE_INGEST_WITH_LANGFLOW"] = "true" if disable_langflow_ingest else "false"
|
||||||
os.environ["DISABLE_STARTUP_INGEST"] = "true"
|
os.environ["DISABLE_STARTUP_INGEST"] = "true"
|
||||||
|
os.environ["EMBEDDING_MODEL"] = "text-embedding-3-small"
|
||||||
|
os.environ["EMBEDDING_PROVIDER"] = "openai"
|
||||||
# Force no-auth mode so endpoints bypass authentication
|
# Force no-auth mode so endpoints bypass authentication
|
||||||
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = ""
|
os.environ["GOOGLE_OAUTH_CLIENT_ID"] = ""
|
||||||
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = ""
|
os.environ["GOOGLE_OAUTH_CLIENT_SECRET"] = ""
|
||||||
|
|
|
||||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -2352,7 +2352,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openrag"
|
name = "openrag"
|
||||||
version = "0.1.32"
|
version = "0.1.33"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agentd" },
|
{ name = "agentd" },
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue