style(nextjs): changes home llm selection layout
This commit is contained in:
parent
ad3895ee50
commit
801f103c2a
15 changed files with 1540 additions and 2002 deletions
|
|
@ -147,23 +147,35 @@ thead {
|
|||
|
||||
|
||||
.custom_scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom_scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #4A90E2;
|
||||
border-radius: 6px;
|
||||
background-color: #d1d5db;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.custom_scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #9ca3af;
|
||||
}
|
||||
|
||||
.custom_scrollbar::-webkit-scrollbar-track {
|
||||
background-color: #F0F0F0;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.custom_scrollbar::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styling */
|
||||
.custom_scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d1d5db #f9fafb;
|
||||
}
|
||||
|
||||
|
||||
/* word animation */
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,7 @@ import { Loader2 } from 'lucide-react';
|
|||
import { hasValidLLMConfig } from '@/utils/storeHelpers';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { checkIfSelectedOllamaModelIsPulled } from '@/utils/providerUtils';
|
||||
|
||||
export function StoreInitializer({ children }: { children: React.ReactNode }) {
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -82,17 +83,6 @@ export function StoreInitializer({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}
|
||||
|
||||
const checkIfSelectedOllamaModelIsPulled = async (ollamaModel: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/ppt/ollama/models/available');
|
||||
const models = await response.json();
|
||||
const pulledModels = models.map((model: any) => model.name);
|
||||
return pulledModels.includes(ollamaModel);
|
||||
} catch (error) {
|
||||
console.error('Error checking if selected Ollama model is pulled:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const checkIfSelectedCustomModelIsAvailable = async (customModel: string) => {
|
||||
try {
|
||||
|
|
|
|||
183
servers/nextjs/components/CustomConfig.tsx
Normal file
183
servers/nextjs/components/CustomConfig.tsx
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
"use client";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "./ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CustomConfigProps {
|
||||
customLlmUrl: string;
|
||||
customLlmApiKey: string;
|
||||
customModel: string;
|
||||
customModels: string[];
|
||||
customModelsLoading: boolean;
|
||||
customModelsChecked: boolean;
|
||||
openModelSelect: boolean;
|
||||
onInputChange: (value: string, field: string) => void;
|
||||
onOpenModelSelectChange: (open: boolean) => void;
|
||||
onFetchCustomModels: () => void;
|
||||
}
|
||||
|
||||
export default function CustomConfig({
|
||||
customLlmUrl,
|
||||
customLlmApiKey,
|
||||
customModel,
|
||||
customModels,
|
||||
customModelsLoading,
|
||||
customModelsChecked,
|
||||
openModelSelect,
|
||||
onInputChange,
|
||||
onOpenModelSelectChange,
|
||||
onFetchCustomModels,
|
||||
}: CustomConfigProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
OpenAI Compatible URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your URL"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={customLlmUrl}
|
||||
onChange={(e) =>
|
||||
onInputChange(e.target.value, "custom_llm_url")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
OpenAI Compatible API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your API Key"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={customLlmApiKey}
|
||||
onChange={(e) =>
|
||||
onInputChange(e.target.value, "custom_llm_api_key")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model selection dropdown - only show if models are available */}
|
||||
{customModelsChecked && customModels.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-3 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<p className="text-sm text-amber-800">
|
||||
<strong>Important:</strong> Only models with function
|
||||
calling capabilities (tool calls) or JSON schema support
|
||||
will work.
|
||||
</p>
|
||||
</div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Model
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Popover
|
||||
open={openModelSelect}
|
||||
onOpenChange={onOpenModelSelectChange}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{customModel || "Select a model"}
|
||||
</span>
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search model..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{customModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model}
|
||||
onSelect={(value) => {
|
||||
onInputChange(value, "custom_model");
|
||||
onOpenModelSelectChange(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
customModel === model
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{model}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Check for available models button - show when no models checked or no models found */}
|
||||
{(!customModelsChecked ||
|
||||
(customModelsChecked && customModels.length === 0)) && (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={onFetchCustomModels}
|
||||
disabled={customModelsLoading || !customLlmUrl}
|
||||
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 ${customModelsLoading || !customLlmUrl
|
||||
? "bg-gray-100 border-gray-300 cursor-not-allowed text-gray-500"
|
||||
: "bg-white border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-2 focus:ring-blue-500/20"
|
||||
}`}
|
||||
>
|
||||
{customModelsLoading ? (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Checking for models...
|
||||
</div>
|
||||
) : (
|
||||
"Check for available models"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show message if no models found */}
|
||||
{customModelsChecked && customModels.length === 0 && (
|
||||
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-sm text-yellow-800">
|
||||
No models found. Please make sure models are available.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
servers/nextjs/components/GoogleConfig.tsx
Normal file
27
servers/nextjs/components/GoogleConfig.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
interface GoogleConfigProps {
|
||||
googleApiKey: string;
|
||||
onInputChange: (value: string, field: string) => void;
|
||||
}
|
||||
|
||||
export default function GoogleConfig({ googleApiKey, onInputChange }: GoogleConfigProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Google API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={googleApiKey}
|
||||
onChange={(e) => onInputChange(e.target.value, "google_api_key")}
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
placeholder="Enter your API key"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Your API key will be stored locally and never shared
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
405
servers/nextjs/components/LLMSelection.tsx
Normal file
405
servers/nextjs/components/LLMSelection.tsx
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs";
|
||||
import { Check, ChevronsUpDown, Info } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "./ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import OpenAIConfig from "./OpenAIConfig";
|
||||
import GoogleConfig from "./GoogleConfig";
|
||||
import OllamaConfig from "./OllamaConfig";
|
||||
import CustomConfig from "./CustomConfig";
|
||||
import {
|
||||
OllamaModel,
|
||||
LLMConfig,
|
||||
updateLLMConfig,
|
||||
changeProvider as changeProviderUtil,
|
||||
fetchOllamaModelsWithConfig,
|
||||
setOllamaConfig,
|
||||
fetchCustomModels,
|
||||
} from "@/utils/providerUtils";
|
||||
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
|
||||
|
||||
// Button state interface
|
||||
interface ButtonState {
|
||||
isLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
text: string;
|
||||
showProgress: boolean;
|
||||
progressPercentage?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface LLMProviderSelectionProps {
|
||||
initialLLMConfig: LLMConfig;
|
||||
onConfigChange: (config: LLMConfig) => void;
|
||||
buttonState: ButtonState;
|
||||
setButtonState: (state: ButtonState | ((prev: ButtonState) => ButtonState)) => void;
|
||||
}
|
||||
|
||||
export default function LLMProviderSelection({
|
||||
initialLLMConfig,
|
||||
onConfigChange,
|
||||
setButtonState,
|
||||
}: LLMProviderSelectionProps) {
|
||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>(initialLLMConfig);
|
||||
const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
|
||||
const [customModels, setCustomModels] = useState<string[]>([]);
|
||||
const [customModelsLoading, setCustomModelsLoading] = useState<boolean>(false);
|
||||
const [customModelsChecked, setCustomModelsChecked] = useState<boolean>(false);
|
||||
const [ollamaModelsLoading, setOllamaModelsLoading] = useState<boolean>(false);
|
||||
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(
|
||||
initialLLMConfig.USE_CUSTOM_URL || false
|
||||
);
|
||||
const [openModelSelect, setOpenModelSelect] = useState(false);
|
||||
const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onConfigChange(llmConfig);
|
||||
}, [llmConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
const needsModelSelection =
|
||||
(llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL) ||
|
||||
(llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL);
|
||||
|
||||
const needsApiKey =
|
||||
((llmConfig.IMAGE_PROVIDER === "dall-e-3" || llmConfig.LLM === "openai") && !llmConfig.OPENAI_API_KEY) ||
|
||||
((llmConfig.IMAGE_PROVIDER === "gemini_flash" || llmConfig.LLM === "google") && !llmConfig.GOOGLE_API_KEY) ||
|
||||
(llmConfig.IMAGE_PROVIDER === "pexels" && !llmConfig.PEXELS_API_KEY) ||
|
||||
(llmConfig.IMAGE_PROVIDER === "pixabay" && !llmConfig.PIXABAY_API_KEY);
|
||||
|
||||
setButtonState({
|
||||
isLoading: false,
|
||||
isDisabled: needsModelSelection || needsApiKey,
|
||||
text: needsModelSelection ? "Please Select a Model" : needsApiKey ? "Please Enter API Key" : "Save Configuration",
|
||||
showProgress: false
|
||||
});
|
||||
|
||||
}, [llmConfig]);
|
||||
|
||||
const input_field_changed = (new_value: string, field: string) => {
|
||||
const updatedConfig = updateLLMConfig(llmConfig, field, new_value);
|
||||
setLlmConfig(updatedConfig);
|
||||
};
|
||||
|
||||
const handleProviderChange = (provider: string) => {
|
||||
const newConfig = changeProviderUtil(llmConfig, provider);
|
||||
setLlmConfig(newConfig);
|
||||
if (provider === "ollama") {
|
||||
fetchOllamaModels();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchOllamaModels = async () => {
|
||||
try {
|
||||
setOllamaModelsLoading(true);
|
||||
const result = await fetchOllamaModelsWithConfig(llmConfig);
|
||||
setOllamaModels(result.models);
|
||||
if (result.updatedConfig) {
|
||||
setLlmConfig(result.updatedConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Ollama models:", error);
|
||||
setOllamaModels([]);
|
||||
} finally {
|
||||
setOllamaModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCustomModelsHandler = async () => {
|
||||
try {
|
||||
setCustomModelsLoading(true);
|
||||
const models = await fetchCustomModels(
|
||||
llmConfig.CUSTOM_LLM_URL || "",
|
||||
llmConfig.CUSTOM_LLM_API_KEY || ""
|
||||
);
|
||||
setCustomModels(models);
|
||||
setCustomModelsChecked(true);
|
||||
} catch (error) {
|
||||
console.error("Error fetching custom models:", error);
|
||||
setCustomModels([]);
|
||||
} finally {
|
||||
setCustomModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setOllamaConfigHandler = () => {
|
||||
const updatedConfig = setOllamaConfig(llmConfig, useCustomOllamaUrl);
|
||||
setLlmConfig(updatedConfig);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (llmConfig.LLM === "ollama") {
|
||||
fetchOllamaModels();
|
||||
}
|
||||
}, [llmConfig.LLM]);
|
||||
|
||||
useEffect(() => {
|
||||
setOllamaConfigHandler();
|
||||
}, [useCustomOllamaUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (llmConfig.LLM === "custom") {
|
||||
setCustomModels([]);
|
||||
setCustomModelsChecked(false);
|
||||
setLlmConfig({ ...llmConfig, CUSTOM_MODEL: "" });
|
||||
}
|
||||
}, [llmConfig.CUSTOM_LLM_URL, llmConfig.CUSTOM_LLM_API_KEY]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!llmConfig.IMAGE_PROVIDER) {
|
||||
if (llmConfig.LLM === "openai") {
|
||||
setLlmConfig({ ...llmConfig, IMAGE_PROVIDER: "dall-e-3" });
|
||||
} else if (llmConfig.LLM === "google") {
|
||||
setLlmConfig({ ...llmConfig, IMAGE_PROVIDER: "gemini_flash" });
|
||||
} else {
|
||||
setLlmConfig({ ...llmConfig, IMAGE_PROVIDER: "pexels" });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col mt-10">
|
||||
{/* Provider Selection - Fixed Header */}
|
||||
<div className="p-2 rounded-2xl border border-gray-200">
|
||||
<Tabs
|
||||
value={llmConfig.LLM || "openai"}
|
||||
onValueChange={handleProviderChange}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-4 bg-transparent h-10">
|
||||
<TabsTrigger value="openai">OpenAI</TabsTrigger>
|
||||
<TabsTrigger value="google">Google</TabsTrigger>
|
||||
<TabsTrigger value="ollama">Ollama</TabsTrigger>
|
||||
<TabsTrigger value="custom">Custom</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 pt-0 custom_scrollbar">
|
||||
<Tabs
|
||||
value={llmConfig.LLM || "openai"}
|
||||
onValueChange={handleProviderChange}
|
||||
className="w-full"
|
||||
>
|
||||
{/* OpenAI Content */}
|
||||
<TabsContent value="openai" className="mt-6">
|
||||
<OpenAIConfig
|
||||
openaiApiKey={llmConfig.OPENAI_API_KEY || ""}
|
||||
onInputChange={input_field_changed}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Google Content */}
|
||||
<TabsContent value="google" className="mt-6">
|
||||
<GoogleConfig
|
||||
googleApiKey={llmConfig.GOOGLE_API_KEY || ""}
|
||||
onInputChange={input_field_changed}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Ollama Content */}
|
||||
<TabsContent value="ollama" className="mt-6">
|
||||
<OllamaConfig
|
||||
ollamaModel={llmConfig.OLLAMA_MODEL || ""}
|
||||
ollamaUrl={llmConfig.OLLAMA_URL || ""}
|
||||
useCustomUrl={useCustomOllamaUrl}
|
||||
ollamaModels={ollamaModels}
|
||||
ollamaModelsLoading={ollamaModelsLoading}
|
||||
onInputChange={input_field_changed}
|
||||
onUseCustomUrlChange={setUseCustomOllamaUrl}
|
||||
openModelSelect={openModelSelect}
|
||||
onOpenModelSelectChange={setOpenModelSelect}
|
||||
onModelSelect={(modelName: string) => {
|
||||
input_field_changed(modelName, "ollama_model");
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Custom Content */}
|
||||
<TabsContent value="custom" className="mt-6">
|
||||
<CustomConfig
|
||||
customLlmUrl={llmConfig.CUSTOM_LLM_URL || ""}
|
||||
customLlmApiKey={llmConfig.CUSTOM_LLM_API_KEY || ""}
|
||||
customModel={llmConfig.CUSTOM_MODEL || ""}
|
||||
customModels={customModels}
|
||||
customModelsLoading={customModelsLoading}
|
||||
customModelsChecked={customModelsChecked}
|
||||
openModelSelect={openModelSelect}
|
||||
onInputChange={input_field_changed}
|
||||
onOpenModelSelectChange={setOpenModelSelect}
|
||||
onFetchCustomModels={fetchCustomModelsHandler}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Image Provider Selection */}
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Select Image Provider
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Popover
|
||||
open={openImageProviderSelect}
|
||||
onOpenChange={setOpenImageProviderSelect}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openImageProviderSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{llmConfig.IMAGE_PROVIDER
|
||||
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label ||
|
||||
llmConfig.IMAGE_PROVIDER
|
||||
: "Select image provider"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search provider..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No provider found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{Object.values(IMAGE_PROVIDERS).map(
|
||||
(provider, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={provider.value}
|
||||
onSelect={(value) => {
|
||||
input_field_changed(value, "image_provider");
|
||||
setOpenImageProviderSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
llmConfig.IMAGE_PROVIDER === provider.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{provider.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{provider.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic API Key Input for Image Provider */}
|
||||
{llmConfig.IMAGE_PROVIDER &&
|
||||
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
|
||||
(() => {
|
||||
const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
|
||||
|
||||
// Show info message when using same API key as main provider
|
||||
if (provider.value === "dall-e-3" && llmConfig.LLM === "openai") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (provider.value === "gemini_flash" && llmConfig.LLM === "google") {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// Show API key input for other providers
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{provider.apiKeyFieldLabel}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Enter your ${provider.apiKeyFieldLabel}`}
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={
|
||||
provider.apiKeyField === "PEXELS_API_KEY"
|
||||
? llmConfig.PEXELS_API_KEY || ""
|
||||
: provider.apiKeyField === "PIXABAY_API_KEY"
|
||||
? llmConfig.PIXABAY_API_KEY || ""
|
||||
: ""
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (provider.apiKeyField === "PEXELS_API_KEY") {
|
||||
input_field_changed(e.target.value, "pexels_api_key");
|
||||
} else if (provider.apiKeyField === "PIXABAY_API_KEY") {
|
||||
input_field_changed(e.target.value, "pixabay_api_key");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
API key for {provider.label} image generation
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Model Information */}
|
||||
<div className="mb-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-blue-500 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-blue-900 mb-1">
|
||||
Selected Models
|
||||
</h3>
|
||||
<p className="text-sm text-blue-700">
|
||||
Using{" "}
|
||||
{llmConfig.LLM === "ollama"
|
||||
? llmConfig.OLLAMA_MODEL ?? "xxxxx"
|
||||
: llmConfig.LLM === "custom"
|
||||
? llmConfig.CUSTOM_MODEL ?? "xxxxx"
|
||||
: LLM_PROVIDERS[llmConfig.LLM!]?.model_label || "xxxxx"}{" "}
|
||||
for text generation and{" "}
|
||||
{llmConfig.IMAGE_PROVIDER &&
|
||||
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
|
||||
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER].label
|
||||
: "xxxxx"}{" "}
|
||||
for images
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
226
servers/nextjs/components/OllamaConfig.tsx
Normal file
226
servers/nextjs/components/OllamaConfig.tsx
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
"use client";
|
||||
import { useState } from "react";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "./ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Switch } from "./ui/switch";
|
||||
|
||||
interface OllamaModel {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
size: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface OllamaConfigProps {
|
||||
ollamaModel: string;
|
||||
ollamaUrl: string;
|
||||
useCustomUrl: boolean;
|
||||
ollamaModels: OllamaModel[];
|
||||
ollamaModelsLoading?: boolean;
|
||||
onInputChange: (value: string, field: string) => void;
|
||||
onUseCustomUrlChange: (checked: boolean) => void;
|
||||
openModelSelect: boolean;
|
||||
onOpenModelSelectChange: (open: boolean) => void;
|
||||
onModelSelect?: (modelName: string) => void;
|
||||
}
|
||||
|
||||
export default function OllamaConfig({
|
||||
ollamaModel,
|
||||
ollamaUrl,
|
||||
useCustomUrl,
|
||||
ollamaModels,
|
||||
ollamaModelsLoading = false,
|
||||
onInputChange,
|
||||
onUseCustomUrlChange,
|
||||
openModelSelect,
|
||||
onOpenModelSelectChange,
|
||||
onModelSelect,
|
||||
}: OllamaConfigProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Choose a supported model
|
||||
</label>
|
||||
<div className="w-full">
|
||||
{ollamaModelsLoading ? (
|
||||
<div className="w-full h-12 px-4 py-4 border border-gray-300 rounded-lg bg-gray-50 flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-gray-500" />
|
||||
<span className="text-sm text-gray-600">Loading models...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : ollamaModels && ollamaModels.length > 0 ? (
|
||||
<Popover
|
||||
open={openModelSelect}
|
||||
onOpenChange={onOpenModelSelectChange}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
{ollamaModel && (
|
||||
<div className="w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={
|
||||
ollamaModels?.find(
|
||||
(m) => m.value === ollamaModel
|
||||
)?.icon
|
||||
}
|
||||
alt={`${ollamaModel} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{ollamaModel
|
||||
? ollamaModels?.find(
|
||||
(m) => m.value === ollamaModel
|
||||
)?.label || ollamaModel
|
||||
: "Select a model"}
|
||||
</span>
|
||||
{ollamaModel && (
|
||||
<span className="text-xs text-gray-500 bg-gray-100 rounded-full px-2 py-1">
|
||||
{
|
||||
ollamaModels?.find(
|
||||
(m) => m.value === ollamaModel
|
||||
)?.size
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="start"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search model..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{ollamaModels?.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model.value}
|
||||
onSelect={(value) => {
|
||||
if (onModelSelect) {
|
||||
onModelSelect(value);
|
||||
} else {
|
||||
onInputChange(value, "ollama_model");
|
||||
}
|
||||
onOpenModelSelectChange(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
ollamaModel === model.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={model.icon}
|
||||
alt={`${model.label} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
{model.size}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<div className="w-full border border-gray-300 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-4 h-4 bg-gray-200 rounded-full animate-pulse"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 bg-gray-200 rounded animate-pulse"></div>
|
||||
<div className="h-3 bg-gray-200 rounded w-3/4 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(!ollamaModels || ollamaModels.length === 0) && !ollamaModelsLoading && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
No models available. Please check your Ollama connection.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Use custom Ollama URL
|
||||
</label>
|
||||
<Switch
|
||||
checked={useCustomUrl}
|
||||
onCheckedChange={onUseCustomUrlChange}
|
||||
/>
|
||||
</div>
|
||||
{useCustomUrl && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your Ollama URL"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={ollamaUrl}
|
||||
onChange={(e) =>
|
||||
onInputChange(e.target.value, "ollama_url")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Change this if you are using a custom Ollama instance
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
servers/nextjs/components/OpenAIConfig.tsx
Normal file
27
servers/nextjs/components/OpenAIConfig.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
interface OpenAIConfigProps {
|
||||
openaiApiKey: string;
|
||||
onInputChange: (value: string, field: string) => void;
|
||||
}
|
||||
|
||||
export default function OpenAIConfig({ openaiApiKey, onInputChange }: OpenAIConfigProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
OpenAI API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => onInputChange(e.target.value, "openai_api_key")}
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
placeholder="Enter your API key"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Your API key will be stored locally and never shared
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,8 +12,6 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
duration={2000}
|
||||
richColors={true}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
|
|
|
|||
12
servers/nextjs/package-lock.json
generated
12
servers/nextjs/package-lock.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.13",
|
||||
"cypress": "^14.3.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
|
|
@ -2394,6 +2395,15 @@
|
|||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
|
|
@ -5169,7 +5179,6 @@
|
|||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
|
|
@ -6494,7 +6503,6 @@
|
|||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz",
|
||||
"integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
|
|
|||
91
servers/nextjs/utils/providerConstants.ts
Normal file
91
servers/nextjs/utils/providerConstants.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
export interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
size: string;
|
||||
}
|
||||
|
||||
export interface ImageProviderOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
requiresApiKey?: boolean;
|
||||
apiKeyField?: string;
|
||||
apiKeyFieldLabel?: string;
|
||||
}
|
||||
|
||||
export interface LLMProviderOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
model_value?: string;
|
||||
model_label?: string;
|
||||
}
|
||||
|
||||
export const IMAGE_PROVIDERS: Record<string, ImageProviderOption> = {
|
||||
pexels: {
|
||||
value: "pexels",
|
||||
label: "Pexels",
|
||||
description: "Free stock photo and video platform",
|
||||
icon: "/icons/pexels.png",
|
||||
requiresApiKey: true,
|
||||
apiKeyField: "PEXELS_API_KEY",
|
||||
apiKeyFieldLabel: "Pexels API Key"
|
||||
},
|
||||
pixabay: {
|
||||
value: "pixabay",
|
||||
label: "Pixabay",
|
||||
description: "Free images and videos",
|
||||
icon: "/icons/pixabay.png",
|
||||
requiresApiKey: true,
|
||||
apiKeyField: "PIXABAY_API_KEY",
|
||||
apiKeyFieldLabel: "Pixabay API Key"
|
||||
},
|
||||
"dall-e-3": {
|
||||
value: "dall-e-3",
|
||||
label: "DALL-E 3",
|
||||
description: "OpenAI's latest image generation model",
|
||||
icon: "/icons/dall-e.png",
|
||||
requiresApiKey: true,
|
||||
apiKeyField: "OPENAI_API_KEY",
|
||||
apiKeyFieldLabel: "OpenAI API Key"
|
||||
},
|
||||
gemini_flash: {
|
||||
value: "gemini_flash",
|
||||
label: "Gemini Flash",
|
||||
description: "Google's primary image generation model",
|
||||
icon: "/icons/google.png",
|
||||
requiresApiKey: true,
|
||||
apiKeyField: "GOOGLE_API_KEY",
|
||||
apiKeyFieldLabel: "Google API Key"
|
||||
},
|
||||
};
|
||||
|
||||
export const LLM_PROVIDERS: Record<string, LLMProviderOption> = {
|
||||
openai: {
|
||||
value: "openai",
|
||||
label: "OpenAI",
|
||||
description: "OpenAI's latest image generation model",
|
||||
model_value: "gpt-4.1",
|
||||
model_label: "GPT-4.1"
|
||||
},
|
||||
google: {
|
||||
value: "google",
|
||||
label: "Google",
|
||||
description: "Google's primary image generation model",
|
||||
model_value: "gemini-2.0-flash",
|
||||
model_label: "Gemini 2.0 Flash"
|
||||
},
|
||||
ollama: {
|
||||
value: "ollama",
|
||||
label: "Ollama",
|
||||
description: "Ollama's primary text generation model",
|
||||
},
|
||||
custom: {
|
||||
value: "custom",
|
||||
label: "Custom",
|
||||
description: "Custom LLM",
|
||||
},
|
||||
};
|
||||
284
servers/nextjs/utils/providerUtils.ts
Normal file
284
servers/nextjs/utils/providerUtils.ts
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { toast } from "sonner";
|
||||
|
||||
export interface OllamaModel {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
size: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface DownloadingModel {
|
||||
name: string;
|
||||
size: number | null;
|
||||
downloaded: number | null;
|
||||
status: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export interface LLMConfig {
|
||||
LLM?: string;
|
||||
OPENAI_API_KEY?: string;
|
||||
GOOGLE_API_KEY?: string;
|
||||
OLLAMA_URL?: string;
|
||||
OLLAMA_MODEL?: string;
|
||||
CUSTOM_LLM_URL?: string;
|
||||
CUSTOM_LLM_API_KEY?: string;
|
||||
CUSTOM_MODEL?: string;
|
||||
PEXELS_API_KEY?: string;
|
||||
PIXABAY_API_KEY?: string;
|
||||
IMAGE_PROVIDER?: string;
|
||||
USE_CUSTOM_URL?: boolean;
|
||||
}
|
||||
|
||||
export interface OllamaModelsResult {
|
||||
models: OllamaModel[];
|
||||
updatedConfig?: LLMConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates LLM configuration based on field changes
|
||||
*/
|
||||
export const updateLLMConfig = (
|
||||
currentConfig: LLMConfig,
|
||||
field: string,
|
||||
value: string
|
||||
): LLMConfig => {
|
||||
const fieldMappings: Record<string, keyof LLMConfig> = {
|
||||
openai_api_key: "OPENAI_API_KEY",
|
||||
google_api_key: "GOOGLE_API_KEY",
|
||||
ollama_url: "OLLAMA_URL",
|
||||
ollama_model: "OLLAMA_MODEL",
|
||||
custom_llm_url: "CUSTOM_LLM_URL",
|
||||
custom_llm_api_key: "CUSTOM_LLM_API_KEY",
|
||||
custom_model: "CUSTOM_MODEL",
|
||||
pexels_api_key: "PEXELS_API_KEY",
|
||||
pixabay_api_key: "PIXABAY_API_KEY",
|
||||
image_provider: "IMAGE_PROVIDER",
|
||||
};
|
||||
|
||||
const configKey = fieldMappings[field];
|
||||
if (configKey) {
|
||||
return { ...currentConfig, [configKey]: value };
|
||||
}
|
||||
|
||||
return currentConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the provider and sets appropriate defaults
|
||||
*/
|
||||
export const changeProvider = (
|
||||
currentConfig: LLMConfig,
|
||||
provider: string
|
||||
): LLMConfig => {
|
||||
const newConfig = { ...currentConfig, LLM: provider };
|
||||
|
||||
// Auto Select appropriate image provider based on the text models
|
||||
if (provider === "openai") {
|
||||
newConfig.IMAGE_PROVIDER = "dall-e-3";
|
||||
} else if (provider === "google") {
|
||||
newConfig.IMAGE_PROVIDER = "gemini_flash";
|
||||
} else {
|
||||
newConfig.IMAGE_PROVIDER = "pexels"; // default for ollama and custom
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches supported Ollama models
|
||||
*/
|
||||
export const fetchOllamaModels = async (): Promise<OllamaModel[]> => {
|
||||
try {
|
||||
const response = await fetch("/api/v1/ppt/ollama/models/supported");
|
||||
const models = await response.json();
|
||||
return models || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching ollama models:", error);
|
||||
return []; // Ensure we always return an empty array on error
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches Ollama models and validates current selection
|
||||
* Returns models and updated config if needed
|
||||
*/
|
||||
export const fetchOllamaModelsWithConfig = async (
|
||||
config: LLMConfig
|
||||
): Promise<OllamaModelsResult> => {
|
||||
try {
|
||||
const models = await fetchOllamaModels();
|
||||
|
||||
// Check if currently selected model is still available
|
||||
let updatedConfig: LLMConfig | undefined;
|
||||
if (config.OLLAMA_MODEL && models && models.length > 0) {
|
||||
const isModelAvailable = models.some(
|
||||
(model: OllamaModel) => model.value === config.OLLAMA_MODEL
|
||||
);
|
||||
if (!isModelAvailable) {
|
||||
updatedConfig = { ...config, OLLAMA_MODEL: "" };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
models,
|
||||
updatedConfig
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching ollama models:", error);
|
||||
return {
|
||||
models: [],
|
||||
updatedConfig: { ...config, OLLAMA_MODEL: "" }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const checkIfSelectedOllamaModelIsPulled = async (ollamaModel: string) => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/ppt/ollama/models/available');
|
||||
const models = await response.json();
|
||||
const pulledModels = models.map((model: any) => model.name);
|
||||
return pulledModels.includes(ollamaModel);
|
||||
} catch (error) {
|
||||
console.error('Error checking if selected Ollama model is pulled:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches available custom models
|
||||
*/
|
||||
export const fetchCustomModels = async (
|
||||
url: string,
|
||||
apiKey: string
|
||||
): Promise<string[]> => {
|
||||
try {
|
||||
const response = await fetch("/api/v1/ppt/custom_llm/models/available", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url || "",
|
||||
api_key: apiKey || "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
toast.info("Could not fetch custom models");
|
||||
console.error("Error fetching custom models:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets downloading model state
|
||||
*/
|
||||
export const resetDownloadingModel = (): DownloadingModel => ({
|
||||
name: "",
|
||||
size: null,
|
||||
downloaded: null,
|
||||
status: "",
|
||||
done: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Pulls Ollama model with progress tracking
|
||||
* Returns a promise that resolves with the final downloading model state
|
||||
*/
|
||||
export const pullOllamaModel = async (
|
||||
model: string,
|
||||
onProgress?: (model: DownloadingModel) => void
|
||||
): Promise<DownloadingModel> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/ollama/model/pull?model=${model}`
|
||||
);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
if (data.done && data.status !== "error") {
|
||||
clearInterval(interval);
|
||||
onProgress?.(data);
|
||||
resolve(data);
|
||||
} else if (data.status === "error") {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
reject(new Error("Error occurred while pulling model"));
|
||||
} else {
|
||||
onProgress?.(data);
|
||||
}
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
if (response.status === 403) {
|
||||
reject(new Error("Request to Ollama Not Authorized"));
|
||||
}
|
||||
reject(new Error("Error occurred while pulling model"));
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
reject(error);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets Ollama configuration based on custom URL preference
|
||||
*/
|
||||
export const setOllamaConfig = (
|
||||
currentConfig: LLMConfig,
|
||||
useCustomUrl: boolean
|
||||
): LLMConfig => {
|
||||
let customUrl = "http://localhost:11434";
|
||||
if (!useCustomUrl) {
|
||||
return {
|
||||
...currentConfig,
|
||||
OLLAMA_URL: customUrl,
|
||||
USE_CUSTOM_URL: false,
|
||||
};
|
||||
} else {
|
||||
return { ...currentConfig, USE_CUSTOM_URL: true, OLLAMA_URL: customUrl };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles saving configuration with error handling
|
||||
*/
|
||||
export const handleSaveConfiguration = async (
|
||||
llmConfig: LLMConfig,
|
||||
handleSaveLLMConfig: (config: LLMConfig) => Promise<void>,
|
||||
pullOllamaModels?: () => Promise<void>
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await handleSaveLLMConfig(llmConfig);
|
||||
if (llmConfig.LLM === "ollama" && pullOllamaModels) {
|
||||
await pullOllamaModels();
|
||||
}
|
||||
toast.success("Configuration saved successfully");
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to save configuration",
|
||||
{
|
||||
description: "Failed to save configuration",
|
||||
}
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -5,7 +5,6 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
|
|||
if (!hasValidLLMConfig(llmConfig)) {
|
||||
throw new Error("Provided configuration is not valid");
|
||||
}
|
||||
console.log("StoreHelperLLMConfig: Saving LLM config", llmConfig);
|
||||
await fetch("/api/user-config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(llmConfig),
|
||||
|
|
@ -54,17 +53,17 @@ export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
|
|||
const isLLMConfigValid =
|
||||
llmConfig.LLM === "openai"
|
||||
? OPENAI_API_KEY !== "" &&
|
||||
OPENAI_API_KEY !== null &&
|
||||
OPENAI_API_KEY !== undefined
|
||||
OPENAI_API_KEY !== null &&
|
||||
OPENAI_API_KEY !== undefined
|
||||
: llmConfig.LLM === "google"
|
||||
? GOOGLE_API_KEY !== "" &&
|
||||
? GOOGLE_API_KEY !== "" &&
|
||||
GOOGLE_API_KEY !== null &&
|
||||
GOOGLE_API_KEY !== undefined
|
||||
: llmConfig.LLM === "ollama"
|
||||
? isOllamaConfigValid
|
||||
: llmConfig.LLM === "custom"
|
||||
? isCustomConfigValid
|
||||
: false;
|
||||
: llmConfig.LLM === "ollama"
|
||||
? isOllamaConfigValid
|
||||
: llmConfig.LLM === "custom"
|
||||
? isCustomConfigValid
|
||||
: false;
|
||||
|
||||
return isLLMConfigValid && isImageConfigValid();
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue