mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-30 03:16:25 +02:00
feat: improved onboarding
This commit is contained in:
parent
2b82f32b8c
commit
cc73e8e565
13 changed files with 999 additions and 887 deletions
|
|
@ -1,407 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { AlertCircle, Bot, Check, CheckCircle, ChevronsUpDown, Plus, Trash2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { LANGUAGES } from "@/contracts/enums/languages";
|
||||
import { getModelsByProvider } from "@/contracts/enums/llm-models";
|
||||
import { LLM_PROVIDERS } from "@/contracts/enums/llm-providers";
|
||||
import { type CreateLLMConfig, useGlobalLLMConfigs, useLLMConfigs } from "@/hooks/use-llm-configs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import InferenceParamsEditor from "../inference-params-editor";
|
||||
|
||||
interface AddProviderStepProps {
|
||||
searchSpaceId: number;
|
||||
onConfigCreated?: () => void;
|
||||
onConfigDeleted?: () => void;
|
||||
}
|
||||
|
||||
export function AddProviderStep({
|
||||
searchSpaceId,
|
||||
onConfigCreated,
|
||||
onConfigDeleted,
|
||||
}: AddProviderStepProps) {
|
||||
const t = useTranslations("onboard");
|
||||
const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(searchSpaceId);
|
||||
const { globalConfigs } = useGlobalLLMConfigs();
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
const [formData, setFormData] = useState<CreateLLMConfig>({
|
||||
name: "",
|
||||
provider: "",
|
||||
custom_provider: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
language: "English",
|
||||
litellm_params: {},
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [modelComboboxOpen, setModelComboboxOpen] = useState(false);
|
||||
|
||||
const handleInputChange = (field: keyof CreateLLMConfig, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const result = await createLLMConfig(formData);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (result) {
|
||||
setFormData({
|
||||
name: "",
|
||||
provider: "",
|
||||
custom_provider: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
language: "English",
|
||||
litellm_params: {},
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
setIsAddingNew(false);
|
||||
// Notify parent component that a config was created
|
||||
onConfigCreated?.();
|
||||
}
|
||||
};
|
||||
|
||||
const selectedProvider = LLM_PROVIDERS.find((p) => p.value === formData.provider);
|
||||
const availableModels = formData.provider ? getModelsByProvider(formData.provider) : [];
|
||||
|
||||
const handleParamsChange = (newParams: Record<string, number | string>) => {
|
||||
setFormData((prev) => ({ ...prev, litellm_params: newParams }));
|
||||
};
|
||||
|
||||
// Reset model when provider changes
|
||||
const handleProviderChange = (value: string) => {
|
||||
handleInputChange("provider", value);
|
||||
setFormData((prev) => ({ ...prev, model_name: "" }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Info Alert */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{t("add_provider_instruction")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Global Configs Notice */}
|
||||
{globalConfigs.length > 0 && (
|
||||
<Alert className="bg-blue-50 border-blue-200 dark:bg-blue-950 dark:border-blue-800">
|
||||
<CheckCircle className="h-4 w-4 text-blue-600" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-200">
|
||||
<strong>{globalConfigs.length} global configuration(s) available!</strong>
|
||||
<br />
|
||||
You can skip adding your own LLM provider and use our pre-configured models in the next
|
||||
step. Or continue here to add your own custom configurations.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Existing Configurations */}
|
||||
{llmConfigs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t("your_llm_configs")}</h3>
|
||||
<div className="grid gap-4">
|
||||
{llmConfigs.map((config) => (
|
||||
<motion.div
|
||||
key={config.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
>
|
||||
<Card className="border-l-4 border-l-primary">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Bot className="w-4 h-4" />
|
||||
<h4 className="font-medium">{config.name}</h4>
|
||||
<Badge variant="secondary">{config.provider}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("model")}: {config.model_name}
|
||||
{config.language && ` • ${t("language")}: ${config.language}`}
|
||||
{config.api_base && ` • ${t("base")}: ${config.api_base}`}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const success = await deleteLLMConfig(config.id);
|
||||
if (success) {
|
||||
onConfigDeleted?.();
|
||||
}
|
||||
}}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add New Provider */}
|
||||
{!isAddingNew ? (
|
||||
<Card className="border-dashed border-2 hover:border-primary/50 transition-colors">
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<Plus className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t("add_provider_title")}</h3>
|
||||
<p className="text-muted-foreground text-center mb-4">{t("add_provider_subtitle")}</p>
|
||||
<Button onClick={() => setIsAddingNew(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t("add_provider_button")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("add_new_llm_provider")}</CardTitle>
|
||||
<CardDescription>{t("configure_new_provider")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t("config_name_required")}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder={t("config_name_placeholder")}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider">{t("provider_required")}</Label>
|
||||
<Select value={formData.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("provider_placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{LLM_PROVIDERS.map((provider) => (
|
||||
<SelectItem key={provider.value} value={provider.value}>
|
||||
{provider.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* language */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">{t("language_optional")}</Label>
|
||||
<Select
|
||||
value={formData.language || "English"}
|
||||
onValueChange={(value) => handleInputChange("language", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("language_placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGES.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.provider === "CUSTOM" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom_provider">{t("custom_provider_name")}</Label>
|
||||
<Input
|
||||
id="custom_provider"
|
||||
placeholder={t("custom_provider_placeholder")}
|
||||
value={formData.custom_provider}
|
||||
onChange={(e) => handleInputChange("custom_provider", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="model_name">{t("model_name_required")}</Label>
|
||||
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={modelComboboxOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className={cn(!formData.model_name && "text-muted-foreground")}>
|
||||
{formData.model_name || t("model_name_placeholder")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start" side="bottom">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={
|
||||
selectedProvider?.example ||
|
||||
t("model_name_placeholder") ||
|
||||
"Type model name..."
|
||||
}
|
||||
value={formData.model_name}
|
||||
onValueChange={(value) => handleInputChange("model_name", value)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className="py-2 text-center text-sm text-muted-foreground">
|
||||
{formData.model_name
|
||||
? `Using custom model: "${formData.model_name}"`
|
||||
: "Type your model name above"}
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{availableModels.length > 0 && (
|
||||
<CommandGroup heading="Suggested Models">
|
||||
{availableModels
|
||||
.filter(
|
||||
(model) =>
|
||||
!formData.model_name ||
|
||||
model.value
|
||||
.toLowerCase()
|
||||
.includes(formData.model_name.toLowerCase()) ||
|
||||
model.label
|
||||
.toLowerCase()
|
||||
.includes(formData.model_name.toLowerCase())
|
||||
)
|
||||
.map((model) => (
|
||||
<CommandItem
|
||||
key={model.value}
|
||||
value={model.value}
|
||||
onSelect={(currentValue) => {
|
||||
handleInputChange("model_name", currentValue);
|
||||
setModelComboboxOpen(false);
|
||||
}}
|
||||
className="flex flex-col items-start py-3"
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 shrink-0",
|
||||
formData.model_name === model.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{model.label}</div>
|
||||
{model.contextWindow && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Context: {model.contextWindow}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{availableModels.length > 0
|
||||
? `Type freely or select from ${availableModels.length} model suggestions`
|
||||
: selectedProvider?.example
|
||||
? `${t("examples")}: ${selectedProvider.example}`
|
||||
: "Type your model name freely"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_key">{t("api_key_required")}</Label>
|
||||
<Input
|
||||
id="api_key"
|
||||
type="password"
|
||||
placeholder={t("api_key_placeholder")}
|
||||
value={formData.api_key}
|
||||
onChange={(e) => handleInputChange("api_key", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_base">{t("api_base_optional")}</Label>
|
||||
<Input
|
||||
id="api_base"
|
||||
placeholder={t("api_base_placeholder")}
|
||||
value={formData.api_base}
|
||||
onChange={(e) => handleInputChange("api_base", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Optional Inference Parameters */}
|
||||
<div className="pt-4">
|
||||
<InferenceParamsEditor
|
||||
params={formData.litellm_params || {}}
|
||||
setParams={handleParamsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? t("adding") : t("add_provider")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsAddingNew(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { AlertCircle, Bot, Brain, CheckCircle, Zap } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
|
||||
|
||||
interface AssignRolesStepProps {
|
||||
searchSpaceId: number;
|
||||
onPreferencesUpdated?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function AssignRolesStep({ searchSpaceId, onPreferencesUpdated }: AssignRolesStepProps) {
|
||||
const t = useTranslations("onboard");
|
||||
const { llmConfigs } = useLLMConfigs(searchSpaceId);
|
||||
const { globalConfigs } = useGlobalLLMConfigs();
|
||||
const { preferences, updatePreferences } = useLLMPreferences(searchSpaceId);
|
||||
|
||||
// Combine global and user-specific configs
|
||||
const allConfigs = [...globalConfigs, ...llmConfigs];
|
||||
|
||||
const ROLE_DESCRIPTIONS = {
|
||||
long_context: {
|
||||
icon: Brain,
|
||||
title: t("long_context_llm_title"),
|
||||
description: t("long_context_llm_desc"),
|
||||
color: "bg-blue-100 text-blue-800 border-blue-200",
|
||||
examples: t("long_context_llm_examples"),
|
||||
},
|
||||
fast: {
|
||||
icon: Zap,
|
||||
title: t("fast_llm_title"),
|
||||
description: t("fast_llm_desc"),
|
||||
color: "bg-green-100 text-green-800 border-green-200",
|
||||
examples: t("fast_llm_examples"),
|
||||
},
|
||||
strategic: {
|
||||
icon: Bot,
|
||||
title: t("strategic_llm_title"),
|
||||
description: t("strategic_llm_desc"),
|
||||
color: "bg-purple-100 text-purple-800 border-purple-200",
|
||||
examples: t("strategic_llm_examples"),
|
||||
},
|
||||
};
|
||||
|
||||
const [assignments, setAssignments] = useState({
|
||||
long_context_llm_id: preferences.long_context_llm_id || "",
|
||||
fast_llm_id: preferences.fast_llm_id || "",
|
||||
strategic_llm_id: preferences.strategic_llm_id || "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setAssignments({
|
||||
long_context_llm_id: preferences.long_context_llm_id || "",
|
||||
fast_llm_id: preferences.fast_llm_id || "",
|
||||
strategic_llm_id: preferences.strategic_llm_id || "",
|
||||
});
|
||||
}, [preferences]);
|
||||
|
||||
const handleRoleAssignment = async (role: string, configId: string) => {
|
||||
const newAssignments = {
|
||||
...assignments,
|
||||
[role]: configId === "" ? "" : parseInt(configId),
|
||||
};
|
||||
|
||||
setAssignments(newAssignments);
|
||||
|
||||
// Auto-save if this assignment completes all roles
|
||||
const hasAllAssignments =
|
||||
newAssignments.long_context_llm_id &&
|
||||
newAssignments.fast_llm_id &&
|
||||
newAssignments.strategic_llm_id;
|
||||
|
||||
if (hasAllAssignments) {
|
||||
const numericAssignments = {
|
||||
long_context_llm_id:
|
||||
typeof newAssignments.long_context_llm_id === "string"
|
||||
? parseInt(newAssignments.long_context_llm_id)
|
||||
: newAssignments.long_context_llm_id,
|
||||
fast_llm_id:
|
||||
typeof newAssignments.fast_llm_id === "string"
|
||||
? parseInt(newAssignments.fast_llm_id)
|
||||
: newAssignments.fast_llm_id,
|
||||
strategic_llm_id:
|
||||
typeof newAssignments.strategic_llm_id === "string"
|
||||
? parseInt(newAssignments.strategic_llm_id)
|
||||
: newAssignments.strategic_llm_id,
|
||||
};
|
||||
|
||||
const success = await updatePreferences(numericAssignments);
|
||||
|
||||
// Refresh parent preferences state
|
||||
if (success && onPreferencesUpdated) {
|
||||
await onPreferencesUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isAssignmentComplete =
|
||||
assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id;
|
||||
|
||||
if (allConfigs.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<AlertCircle className="w-16 h-16 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t("no_llm_configs_found")}</h3>
|
||||
<p className="text-muted-foreground text-center">{t("add_provider_before_roles")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Info Alert */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{t("assign_roles_instruction")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Role Assignment Cards */}
|
||||
<div className="grid gap-6">
|
||||
{Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => {
|
||||
const IconComponent = role.icon;
|
||||
const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments];
|
||||
const assignedConfig = allConfigs.find((config) => config.id === currentAssignment);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={key}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: Object.keys(ROLE_DESCRIPTIONS).indexOf(key) * 0.1 }}
|
||||
>
|
||||
<Card
|
||||
className={`border-l-4 ${currentAssignment ? "border-l-primary" : "border-l-muted"}`}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${role.color}`}>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{role.title}</CardTitle>
|
||||
<CardDescription className="mt-1">{role.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
{currentAssignment && <CheckCircle className="w-5 h-5 text-green-500" />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<strong>{t("use_cases")}:</strong> {role.examples}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">{t("assign_llm_config")}:</Label>
|
||||
<Select
|
||||
value={currentAssignment?.toString() || ""}
|
||||
onValueChange={(value) => handleRoleAssignment(`${key}_llm_id`, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("select_llm_config")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{globalConfigs.length > 0 && (
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{t("global_configs") || "Global Configurations"}
|
||||
</div>
|
||||
)}
|
||||
{globalConfigs
|
||||
.filter((config) => config.id && config.id.toString().trim() !== "")
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{config.provider}
|
||||
</Badge>
|
||||
<span>{config.name}</span>
|
||||
<span className="text-muted-foreground">({config.model_name})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{llmConfigs.length > 0 && globalConfigs.length > 0 && (
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground border-t mt-1">
|
||||
{t("your_configs") || "Your Configurations"}
|
||||
</div>
|
||||
)}
|
||||
{llmConfigs
|
||||
.filter((config) => config.id && config.id.toString().trim() !== "")
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{config.provider}
|
||||
</Badge>
|
||||
<span>{config.name}</span>
|
||||
<span className="text-muted-foreground">({config.model_name})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{assignedConfig && (
|
||||
<div className="mt-3 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Bot className="w-4 h-4" />
|
||||
<span className="font-medium">{t("assigned")}:</span>
|
||||
{assignedConfig.is_global && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="secondary">{assignedConfig.provider}</Badge>
|
||||
<span>{assignedConfig.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("model")}: {assignedConfig.model_name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Status Indicator */}
|
||||
{isAssignmentComplete && (
|
||||
<div className="flex justify-center pt-4">
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-green-50 text-green-700 rounded-lg border border-green-200">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{t("all_roles_assigned_saved")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress Indicator */}
|
||||
<div className="flex justify-center">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{t("progress")}:</span>
|
||||
<div className="flex gap-1">
|
||||
{Object.keys(ROLE_DESCRIPTIONS).map((key, _index) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
assignments[`${key}_llm_id` as keyof typeof assignments]
|
||||
? "bg-primary"
|
||||
: "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span>
|
||||
{t("roles_assigned", {
|
||||
assigned: Object.values(assignments).filter(Boolean).length,
|
||||
total: Object.keys(ROLE_DESCRIPTIONS).length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,22 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import { ArrowRight, Bot, Brain, CheckCircle, Sparkles, Zap } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Bot,
|
||||
Brain,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs";
|
||||
|
||||
const ROLE_ICONS = {
|
||||
long_context: Brain,
|
||||
fast: Zap,
|
||||
strategic: Bot,
|
||||
};
|
||||
|
||||
interface CompletionStepProps {
|
||||
searchSpaceId: number;
|
||||
}
|
||||
|
||||
export function CompletionStep({ searchSpaceId }: CompletionStepProps) {
|
||||
const router = useRouter();
|
||||
const { llmConfigs } = useLLMConfigs(searchSpaceId);
|
||||
const { globalConfigs } = useGlobalLLMConfigs();
|
||||
const { preferences } = useLLMPreferences(searchSpaceId);
|
||||
|
|
@ -32,111 +38,123 @@ export function CompletionStep({ searchSpaceId }: CompletionStepProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Success Message */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="text-center"
|
||||
>
|
||||
<div className="w-20 h-20 mx-auto mb-6 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold mb-2">Setup Complete!</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Configuration Summary */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
Your LLM Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>Here's a summary of your setup</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{Object.entries(assignedConfigs).map(([role, config]) => {
|
||||
if (!config) return null;
|
||||
|
||||
const IconComponent = ROLE_ICONS[role as keyof typeof ROLE_ICONS];
|
||||
const roleDisplayNames = {
|
||||
long_context: "Long Context LLM",
|
||||
fast: "Fast LLM",
|
||||
strategic: "Strategic LLM",
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={role}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + Object.keys(assignedConfigs).indexOf(role) * 0.1 }}
|
||||
className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-background rounded-md">
|
||||
<IconComponent className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{roleDisplayNames[role as keyof typeof roleDisplayNames]}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{config.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{config.is_global && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline">{config.provider}</Badge>
|
||||
<span className="text-sm text-muted-foreground">{config.model_name}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Next Steps */}
|
||||
{/* Next Steps - What would you like to do? */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<Card className="border-primary/20 bg-primary/5">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 bg-primary rounded-md">
|
||||
<ArrowRight className="w-4 h-4 text-primary-foreground" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">Ready to Get Started?</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Click "Complete Setup" to enter your dashboard and start exploring!
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
<Badge variant="secondary">
|
||||
✓ {allConfigs.length} LLM provider{allConfigs.length > 1 ? "s" : ""} available
|
||||
</Badge>
|
||||
{globalConfigs.length > 0 && (
|
||||
<Badge variant="secondary">✓ {globalConfigs.length} Global config(s)</Badge>
|
||||
)}
|
||||
{llmConfigs.length > 0 && (
|
||||
<Badge variant="secondary">✓ {llmConfigs.length} Custom config(s)</Badge>
|
||||
)}
|
||||
<Badge variant="secondary">✓ All roles assigned</Badge>
|
||||
<Badge variant="secondary">✓ Ready to use</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-semibold mb-2">What would you like to do next?</h3>
|
||||
<p className="text-muted-foreground">Choose an option to continue</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Add Sources Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
>
|
||||
<Card className="h-full border-2 hover:border-primary/50 transition-all hover:shadow-lg cursor-pointer group">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-950 rounded-lg flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
|
||||
<FileText className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">Add Sources</CardTitle>
|
||||
<CardDescription>
|
||||
Connect your data sources to start building your knowledge base
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Connect documents and files</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Import from various sources</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Build your knowledge base</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full group-hover:bg-primary/90"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/sources/add`)}
|
||||
>
|
||||
Add Sources
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Start Chatting Card */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
<Card className="h-full border-2 hover:border-primary/50 transition-all hover:shadow-lg cursor-pointer group">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-950 rounded-lg flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
|
||||
<MessageSquare className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">Start Chatting</CardTitle>
|
||||
<CardDescription>
|
||||
Jump right into the AI researcher and start asking questions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>AI-powered conversations</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Research and explore topics</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span>Get instant insights</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full group-hover:bg-primary/90"
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/researcher`)}
|
||||
>
|
||||
Start Chatting
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.9 }}
|
||||
className="flex flex-wrap justify-center gap-2 pt-4"
|
||||
>
|
||||
<Badge variant="secondary">
|
||||
✓ {allConfigs.length} LLM provider{allConfigs.length > 1 ? "s" : ""} available
|
||||
</Badge>
|
||||
{globalConfigs.length > 0 && (
|
||||
<Badge variant="secondary">✓ {globalConfigs.length} Global config(s)</Badge>
|
||||
)}
|
||||
{llmConfigs.length > 0 && (
|
||||
<Badge variant="secondary">✓ {llmConfigs.length} Custom config(s)</Badge>
|
||||
)}
|
||||
<Badge variant="secondary">✓ All roles assigned</Badge>
|
||||
<Badge variant="secondary">✓ Ready to use</Badge>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
752
surfsense_web/components/onboard/setup-llm-step.tsx
Normal file
752
surfsense_web/components/onboard/setup-llm-step.tsx
Normal file
|
|
@ -0,0 +1,752 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
AlertCircle,
|
||||
Bot,
|
||||
Brain,
|
||||
Check,
|
||||
CheckCircle,
|
||||
ChevronDown,
|
||||
ChevronsUpDown,
|
||||
ChevronUp,
|
||||
Plus,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { LANGUAGES } from "@/contracts/enums/languages";
|
||||
import { getModelsByProvider } from "@/contracts/enums/llm-models";
|
||||
import { LLM_PROVIDERS } from "@/contracts/enums/llm-providers";
|
||||
import {
|
||||
type CreateLLMConfig,
|
||||
useGlobalLLMConfigs,
|
||||
useLLMConfigs,
|
||||
useLLMPreferences,
|
||||
} from "@/hooks/use-llm-configs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import InferenceParamsEditor from "../inference-params-editor";
|
||||
|
||||
interface SetupLLMStepProps {
|
||||
searchSpaceId: number;
|
||||
onConfigCreated?: () => void;
|
||||
onConfigDeleted?: () => void;
|
||||
onPreferencesUpdated?: () => Promise<void>;
|
||||
}
|
||||
|
||||
const ROLE_DESCRIPTIONS = {
|
||||
long_context: {
|
||||
icon: Brain,
|
||||
key: "long_context_llm_id" as const,
|
||||
titleKey: "long_context_llm_title",
|
||||
descKey: "long_context_llm_desc",
|
||||
examplesKey: "long_context_llm_examples",
|
||||
color:
|
||||
"bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950 dark:text-blue-200 dark:border-blue-800",
|
||||
},
|
||||
fast: {
|
||||
icon: Zap,
|
||||
key: "fast_llm_id" as const,
|
||||
titleKey: "fast_llm_title",
|
||||
descKey: "fast_llm_desc",
|
||||
examplesKey: "fast_llm_examples",
|
||||
color:
|
||||
"bg-green-100 text-green-800 border-green-200 dark:bg-green-950 dark:text-green-200 dark:border-green-800",
|
||||
},
|
||||
strategic: {
|
||||
icon: Bot,
|
||||
key: "strategic_llm_id" as const,
|
||||
titleKey: "strategic_llm_title",
|
||||
descKey: "strategic_llm_desc",
|
||||
examplesKey: "strategic_llm_examples",
|
||||
color:
|
||||
"bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950 dark:text-purple-200 dark:border-purple-800",
|
||||
},
|
||||
};
|
||||
|
||||
export function SetupLLMStep({
|
||||
searchSpaceId,
|
||||
onConfigCreated,
|
||||
onConfigDeleted,
|
||||
onPreferencesUpdated,
|
||||
}: SetupLLMStepProps) {
|
||||
const t = useTranslations("onboard");
|
||||
const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(searchSpaceId);
|
||||
const { globalConfigs } = useGlobalLLMConfigs();
|
||||
const { preferences, updatePreferences } = useLLMPreferences(searchSpaceId);
|
||||
|
||||
const [isAddingNew, setIsAddingNew] = useState(false);
|
||||
const [formData, setFormData] = useState<CreateLLMConfig>({
|
||||
name: "",
|
||||
provider: "",
|
||||
custom_provider: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
language: "English",
|
||||
litellm_params: {},
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [modelComboboxOpen, setModelComboboxOpen] = useState(false);
|
||||
const [showProviderForm, setShowProviderForm] = useState(false);
|
||||
|
||||
// Role assignments state
|
||||
const [assignments, setAssignments] = useState({
|
||||
long_context_llm_id: preferences.long_context_llm_id || "",
|
||||
fast_llm_id: preferences.fast_llm_id || "",
|
||||
strategic_llm_id: preferences.strategic_llm_id || "",
|
||||
});
|
||||
|
||||
// Combine global and user-specific configs
|
||||
const allConfigs = [...globalConfigs, ...llmConfigs];
|
||||
|
||||
useEffect(() => {
|
||||
setAssignments({
|
||||
long_context_llm_id: preferences.long_context_llm_id || "",
|
||||
fast_llm_id: preferences.fast_llm_id || "",
|
||||
strategic_llm_id: preferences.strategic_llm_id || "",
|
||||
});
|
||||
}, [preferences]);
|
||||
|
||||
const handleInputChange = (field: keyof CreateLLMConfig, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const result = await createLLMConfig(formData);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (result) {
|
||||
setFormData({
|
||||
name: "",
|
||||
provider: "",
|
||||
custom_provider: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
api_base: "",
|
||||
language: "English",
|
||||
litellm_params: {},
|
||||
search_space_id: searchSpaceId,
|
||||
});
|
||||
setIsAddingNew(false);
|
||||
onConfigCreated?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleAssignment = async (role: string, configId: string) => {
|
||||
const newAssignments = {
|
||||
...assignments,
|
||||
[role]: configId === "" ? "" : parseInt(configId),
|
||||
};
|
||||
|
||||
setAssignments(newAssignments);
|
||||
|
||||
// Auto-save if this assignment completes all roles
|
||||
const hasAllAssignments =
|
||||
newAssignments.long_context_llm_id &&
|
||||
newAssignments.fast_llm_id &&
|
||||
newAssignments.strategic_llm_id;
|
||||
|
||||
if (hasAllAssignments) {
|
||||
const numericAssignments = {
|
||||
long_context_llm_id:
|
||||
typeof newAssignments.long_context_llm_id === "string"
|
||||
? parseInt(newAssignments.long_context_llm_id)
|
||||
: newAssignments.long_context_llm_id,
|
||||
fast_llm_id:
|
||||
typeof newAssignments.fast_llm_id === "string"
|
||||
? parseInt(newAssignments.fast_llm_id)
|
||||
: newAssignments.fast_llm_id,
|
||||
strategic_llm_id:
|
||||
typeof newAssignments.strategic_llm_id === "string"
|
||||
? parseInt(newAssignments.strategic_llm_id)
|
||||
: newAssignments.strategic_llm_id,
|
||||
};
|
||||
|
||||
const success = await updatePreferences(numericAssignments);
|
||||
|
||||
if (success && onPreferencesUpdated) {
|
||||
await onPreferencesUpdated();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectedProvider = LLM_PROVIDERS.find((p) => p.value === formData.provider);
|
||||
const availableModels = formData.provider ? getModelsByProvider(formData.provider) : [];
|
||||
|
||||
const handleParamsChange = (newParams: Record<string, number | string>) => {
|
||||
setFormData((prev) => ({ ...prev, litellm_params: newParams }));
|
||||
};
|
||||
|
||||
const handleProviderChange = (value: string) => {
|
||||
handleInputChange("provider", value);
|
||||
setFormData((prev) => ({ ...prev, model_name: "" }));
|
||||
};
|
||||
|
||||
const isAssignmentComplete =
|
||||
assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Global Configs Notice - Prominent at top */}
|
||||
{globalConfigs.length > 0 && (
|
||||
<Alert className="bg-blue-50 border-blue-200 dark:bg-blue-950 dark:border-blue-800">
|
||||
<CheckCircle className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<AlertDescription className="text-blue-800 dark:text-blue-200">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold text-base">
|
||||
{globalConfigs.length} global configuration(s) available!
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
You can skip adding your own LLM provider and use our pre-configured models in the
|
||||
role assignment section below.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
Or expand "Add LLM Provider" to add your own custom configurations.
|
||||
</p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Section 1: Add LLM Providers */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Bot className="w-5 h-5" />
|
||||
{t("add_llm_provider")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("configure_first_provider")}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowProviderForm(!showProviderForm)}
|
||||
className="gap-2"
|
||||
>
|
||||
{showProviderForm ? (
|
||||
<>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
Collapse
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
Expand
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showProviderForm && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Info Alert */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{t("add_provider_instruction")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Existing Configurations */}
|
||||
{llmConfigs.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-muted-foreground">
|
||||
{t("your_llm_configs")}
|
||||
</h4>
|
||||
<div className="grid gap-3">
|
||||
{llmConfigs.map((config) => (
|
||||
<motion.div
|
||||
key={config.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
>
|
||||
<Card className="border-l-4 border-l-primary">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Bot className="w-4 h-4" />
|
||||
<h4 className="font-medium">{config.name}</h4>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{config.provider}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("model")}: {config.model_name}
|
||||
{config.language && ` • ${t("language")}: ${config.language}`}
|
||||
{config.api_base && ` • ${t("base")}: ${config.api_base}`}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const success = await deleteLLMConfig(config.id);
|
||||
if (success) {
|
||||
onConfigDeleted?.();
|
||||
}
|
||||
}}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add New Provider */}
|
||||
{!isAddingNew ? (
|
||||
<Card className="border-dashed border-2 hover:border-primary/50 transition-colors">
|
||||
<CardContent className="flex flex-col items-center justify-center py-8">
|
||||
<Plus className="w-8 h-8 text-muted-foreground mb-3" />
|
||||
<h4 className="font-semibold mb-1">{t("add_provider_title")}</h4>
|
||||
<p className="text-sm text-muted-foreground text-center mb-3">
|
||||
{t("add_provider_subtitle")}
|
||||
</p>
|
||||
<Button onClick={() => setIsAddingNew(true)} size="sm">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t("add_provider_button")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{t("add_new_llm_provider")}</CardTitle>
|
||||
<CardDescription>{t("configure_new_provider")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t("config_name_required")}</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder={t("config_name_placeholder")}
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider">{t("provider_required")}</Label>
|
||||
<Select value={formData.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("provider_placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{LLM_PROVIDERS.map((provider) => (
|
||||
<SelectItem key={provider.value} value={provider.value}>
|
||||
{provider.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language">{t("language_optional")}</Label>
|
||||
<Select
|
||||
value={formData.language || "English"}
|
||||
onValueChange={(value) => handleInputChange("language", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("language_placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGES.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.provider === "CUSTOM" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="custom_provider">{t("custom_provider_name")}</Label>
|
||||
<Input
|
||||
id="custom_provider"
|
||||
placeholder={t("custom_provider_placeholder")}
|
||||
value={formData.custom_provider}
|
||||
onChange={(e) => handleInputChange("custom_provider", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="model_name">{t("model_name_required")}</Label>
|
||||
<Popover open={modelComboboxOpen} onOpenChange={setModelComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={modelComboboxOpen}
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className={cn(!formData.model_name && "text-muted-foreground")}>
|
||||
{formData.model_name || t("model_name_placeholder")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start" side="bottom">
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder={
|
||||
selectedProvider?.example ||
|
||||
t("model_name_placeholder") ||
|
||||
"Type model name..."
|
||||
}
|
||||
value={formData.model_name}
|
||||
onValueChange={(value) => handleInputChange("model_name", value)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className="py-2 text-center text-sm text-muted-foreground">
|
||||
{formData.model_name
|
||||
? `Using custom model: "${formData.model_name}"`
|
||||
: "Type your model name above"}
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{availableModels.length > 0 && (
|
||||
<CommandGroup heading="Suggested Models">
|
||||
{availableModels
|
||||
.filter(
|
||||
(model) =>
|
||||
!formData.model_name ||
|
||||
model.value
|
||||
.toLowerCase()
|
||||
.includes(formData.model_name.toLowerCase()) ||
|
||||
model.label
|
||||
.toLowerCase()
|
||||
.includes(formData.model_name.toLowerCase())
|
||||
)
|
||||
.map((model) => (
|
||||
<CommandItem
|
||||
key={model.value}
|
||||
value={model.value}
|
||||
onSelect={(currentValue) => {
|
||||
handleInputChange("model_name", currentValue);
|
||||
setModelComboboxOpen(false);
|
||||
}}
|
||||
className="flex flex-col items-start py-3"
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4 shrink-0",
|
||||
formData.model_name === model.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{model.label}</div>
|
||||
{model.contextWindow && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Context: {model.contextWindow}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{availableModels.length > 0
|
||||
? `Type freely or select from ${availableModels.length} model suggestions`
|
||||
: selectedProvider?.example
|
||||
? `${t("examples")}: ${selectedProvider.example}`
|
||||
: "Type your model name freely"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_key">{t("api_key_required")}</Label>
|
||||
<Input
|
||||
id="api_key"
|
||||
type="password"
|
||||
placeholder={t("api_key_placeholder")}
|
||||
value={formData.api_key}
|
||||
onChange={(e) => handleInputChange("api_key", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_base">{t("api_base_optional")}</Label>
|
||||
<Input
|
||||
id="api_base"
|
||||
placeholder={t("api_base_placeholder")}
|
||||
value={formData.api_base}
|
||||
onChange={(e) => handleInputChange("api_base", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<InferenceParamsEditor
|
||||
params={formData.litellm_params || {}}
|
||||
setParams={handleParamsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button type="submit" disabled={isSubmitting} size="sm">
|
||||
{isSubmitting ? t("adding") : t("add_provider")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsAddingNew(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-8" />
|
||||
|
||||
{/* Section 2: Assign Roles */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Brain className="w-5 h-5" />
|
||||
{t("assign_llm_roles")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">{t("assign_specific_roles")}</p>
|
||||
</div>
|
||||
|
||||
{allConfigs.length === 0 ? (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{t("add_provider_before_roles")}</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{t("assign_roles_instruction")}</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{Object.entries(ROLE_DESCRIPTIONS).map(([roleKey, role]) => {
|
||||
const IconComponent = role.icon;
|
||||
const currentAssignment = assignments[role.key];
|
||||
const assignedConfig = allConfigs.find((config) => config.id === currentAssignment);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={roleKey}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: Object.keys(ROLE_DESCRIPTIONS).indexOf(roleKey) * 0.1 }}
|
||||
>
|
||||
<Card
|
||||
className={`border-l-4 ${currentAssignment ? "border-l-primary" : "border-l-muted"}`}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${role.color}`}>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">{t(role.titleKey)}</CardTitle>
|
||||
<CardDescription className="mt-1 text-xs">
|
||||
{t(role.descKey)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
{currentAssignment && <CheckCircle className="w-5 h-5 text-green-500" />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<strong>{t("use_cases")}:</strong> {t(role.examplesKey)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">{t("assign_llm_config")}:</Label>
|
||||
<Select
|
||||
value={currentAssignment?.toString() || ""}
|
||||
onValueChange={(value) => handleRoleAssignment(role.key, value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("select_llm_config")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{globalConfigs.length > 0 && (
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||
{t("global_configs") || "Global Configurations"}
|
||||
</div>
|
||||
)}
|
||||
{globalConfigs
|
||||
.filter((config) => config.id && config.id.toString().trim() !== "")
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{config.provider}
|
||||
</Badge>
|
||||
<span className="text-sm">{config.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({config.model_name})
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{llmConfigs.length > 0 && globalConfigs.length > 0 && (
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground border-t mt-1">
|
||||
{t("your_configs") || "Your Configurations"}
|
||||
</div>
|
||||
)}
|
||||
{llmConfigs
|
||||
.filter((config) => config.id && config.id.toString().trim() !== "")
|
||||
.map((config) => (
|
||||
<SelectItem key={config.id} value={config.id.toString()}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{config.provider}
|
||||
</Badge>
|
||||
<span className="text-sm">{config.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({config.model_name})
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{assignedConfig && (
|
||||
<div className="mt-2 p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Bot className="w-4 h-4" />
|
||||
<span className="font-medium">{t("assigned")}:</span>
|
||||
{assignedConfig.is_global && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
🌐 Global
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{assignedConfig.provider}
|
||||
</Badge>
|
||||
<span className="text-sm">{assignedConfig.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{t("model")}: {assignedConfig.model_name}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Status Indicators */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>{t("progress")}:</span>
|
||||
<div className="flex gap-1">
|
||||
{Object.keys(ROLE_DESCRIPTIONS).map((key) => {
|
||||
const roleKey = ROLE_DESCRIPTIONS[key as keyof typeof ROLE_DESCRIPTIONS].key;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
assignments[roleKey] ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span>
|
||||
{t("roles_assigned", {
|
||||
assigned: Object.values(assignments).filter(Boolean).length,
|
||||
total: Object.keys(ROLE_DESCRIPTIONS).length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isAssignmentComplete && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-200 rounded-lg border border-green-200 dark:border-green-800">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">{t("all_roles_assigned_saved")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue