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