feat: Add dashboard layout, components, and templates page with loading states

This commit is contained in:
shiva raj badu 2026-02-16 23:20:11 +05:45
parent cb8ad52f0c
commit 07ae990c95
No known key found for this signature in database
31 changed files with 741 additions and 623 deletions

View file

@ -26,19 +26,6 @@ const DashboardSidebar = () => {
const pathname = usePathname();
const activeTab = pathname.split("?")[0].split("/").pop();
const router = useRouter();
const [mounted, setMounted] = React.useState(false);
const [profileMenuOpen, setProfileMenuOpen] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
const handleMenuNavigate = (href: string) => {
setProfileMenuOpen(false);
router.push(href);
};

View file

@ -3,7 +3,7 @@
import React, { useState, useEffect } from "react";
import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard";
import { PresentationGrid } from "@/app/(dashboard)/dashboard/components/PresentationGrid";
import { PresentationGrid } from "@/app/(presentation-generator)/(dashboard)/dashboard/components/PresentationGrid";

View file

@ -11,7 +11,7 @@ import {
} from "@/utils/providerUtils";
import { useRouter, usePathname } from "next/navigation";
import LLMProviderSelection from "@/components/LLMSelection";
import Header from "../../(dashboard)/dashboard/components/Header";
import Header from "../dashboard/components/Header";
import { LLMConfig } from "@/types/llm_config";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
@ -153,29 +153,32 @@ const SettingsPage = () => {
}
return (
<div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
<Header />
<main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
{/* LLM Selection Component */}
<div className="flex-1 overflow-hidden">
<LLMProviderSelection
initialLLMConfig={llmConfig}
onConfigChange={setLlmConfig}
buttonState={buttonState}
setButtonState={setButtonState}
/>
</div>
<div className="h-screen font-instrument_sans flex flex-col overflow-hidden">
<main className="w-full mx-auto px-4 overflow-hidden flex flex-col">
<LLMProviderSelection
initialLLMConfig={llmConfig}
onConfigChange={setLlmConfig}
buttonState={buttonState}
setButtonState={setButtonState}
/>
</main>
{/* Fixed Bottom Button */}
<div className="flex-shrink-0 bg-white border-t border-gray-200 p-4">
<div className="container mx-auto max-w-3xl">
<div className=" mx-auto ">
<button
onClick={handleSaveConfig}
disabled={buttonState.isDisabled}
style={{
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
color: "#101323",
}}
className={`w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${buttonState.isDisabled
? "bg-gray-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
? "bg-gray-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
} text-white`}
>
{buttonState.isLoading ? (

View file

@ -10,7 +10,7 @@ const CreateCustomTemplate = () => {
router.push('/custom-template')
}}
className='w-full rounded-xl border border-[#EDEEEF] cursor-pointer'>
<div className='relative h-[250px] flex justify-center items-center '>
<div className='relative h-[215px] flex justify-center items-center '>
<img src="/card_bg.svg" alt="" className="absolute top-0 z-[1] left-0 w-full h-full object-cover" />
<div className='w-[36px] h-[36px] relative z-[4] rounded-full bg-[#7A5AF8] flex items-center justify-center'
style={{
@ -22,7 +22,7 @@ const CreateCustomTemplate = () => {
</div>
</div>
</div>
<div className='px-5 py-4 bg-white flex items-center gap-4 border-t border-[#EDEEEF]'>
<div className='px-5 py-4 bg-white flex items-center gap-4 border-t border-[#EDEEEF]'>
<div className='bg-[#7A5AF8] w-[45px] h-[45px] rounded-lg p-2 flex items-center justify-center'>
<Sparkles className='w-6 h-6 text-white' />

View file

@ -28,23 +28,15 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
return (
<Card
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="cursor-pointer flex flex-col justify-between relative hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={handleOpen}
>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layoutCount}
</span>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-bold text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
{template.layoutCount}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
@ -58,7 +50,7 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 ? (
) : previewLayouts.length > 0 && (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
@ -77,21 +69,21 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
</div>
);
})
) : (
// Empty state placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-empty-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<span className="text-xs text-gray-400">No preview</span>
</div>
))
)}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<h3 className="text-sm font-bold text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
</Card>
);
}, (prev, next) => {
@ -117,26 +109,14 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
return (
<Card
key={template.id}
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="cursor-pointer relative hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={handleOpen}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layouts.length}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900 capitalize">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
@ -157,6 +137,21 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
})}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<div>
<h3 className="text-sm font-bold text-gray-900 capitalize">
{template.name}
</h3>
<p className="text-xs text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
</div>
<div className="flex items-center gap-2">
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
</Card>
);
});

View file

@ -1,5 +1,5 @@
import React from "react";
import Header from "@/app/(dashboard)/dashboard/components/Header";
import Header from "@/app/(presentation-generator)/(dashboard)/dashboard/components/Header";
export const APIKeyWarning: React.FC = () => {
return (

View file

@ -1,6 +1,6 @@
import React from "react";
import { Loader2 } from "lucide-react";
import Header from "@/app/(dashboard)/dashboard/components/Header";
import Header from "@/app/(presentation-generator)/(dashboard)/dashboard/components/Header";
interface LoadingSpinnerProps {
message: string;

View file

@ -2,7 +2,7 @@
import React, { useEffect } from "react";
import FontManager from "./components/FontManager";
import Header from "../../(dashboard)/dashboard/components/Header";
import Header from "../(dashboard)/dashboard/components/Header";
import { useCustomLayout } from "./hooks/useCustomLayout";
import { useFontManagement } from "./hooks/useFontManagement";

View file

@ -27,7 +27,7 @@ import MarkdownRenderer from "./MarkdownRenderer";
import { getIconFromFile } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import Header from "@/app/(dashboard)/dashboard/components/Header";
import Header from "@/app/(presentation-generator)/(dashboard)/dashboard/components/Header";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
// Types

View file

@ -1,5 +1,5 @@
import React from 'react'
import Header from '@/app/(dashboard)/dashboard/components/Header'
import Header from '@/app/(presentation-generator)/(dashboard)/dashboard/components/Header'
import { Metadata } from 'next'
import OutlinePage from './components/OutlinePage'
export const metadata: Metadata = {

View file

@ -8,7 +8,7 @@ import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react";
import { useFontLoader } from "../../hooks/useFontLoader";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import TemplateService from "../../services/api/template";
import Header from "../../../(dashboard)/dashboard/components/Header";
import Header from "../../(dashboard)/dashboard/components/Header";
import { toast } from "sonner";
import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates";

View file

@ -13,7 +13,7 @@ import {
CustomTemplates,
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import Header from "../../(dashboard)/dashboard/components/Header";
import Header from "../(dashboard)/dashboard/components/Header";
// Component for rendering custom template card with lazy-loaded previews
const CustomTemplateCard = ({ template }: { template: CustomTemplates }) => {

View file

@ -1,4 +1,4 @@
import Header from "@/app/(dashboard)/dashboard/components/Header";
import Header from "@/app/(presentation-generator)/(dashboard)/dashboard/components/Header";
import { Skeleton } from "@/components/ui/skeleton";
import React from "react";

View file

@ -1,7 +1,7 @@
import React from "react";
import UploadPage from "./components/UploadPage";
import Header from "@/app/(dashboard)/dashboard/components/Header";
import Header from "@/app/(presentation-generator)/(dashboard)/dashboard/components/Header";
import { Metadata } from "next";
export const metadata: Metadata = {

View file

@ -0,0 +1,357 @@
import React from 'react'
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
import { Button } from './ui/button';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command';
import { LLMConfig } from '@/types/llm_config';
import { IMAGE_PROVIDERS } from '@/utils/providerConstants';
import { cn } from '@/lib/utils';
import { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from './ui/select';
const DALLE_3_QUALITY_OPTIONS = [
{
label: "Standard",
value: "standard",
description: "Faster generation with lower cost",
},
{
label: "HD",
value: "hd",
description: "Higher quality images with increased cost",
},
];
const GPT_IMAGE_1_5_QUALITY_OPTIONS = [
{
label: "Low",
value: "low",
description: "Fastest and most cost-effective",
},
{
label: "Medium",
value: "medium",
description: "Balanced quality and speed",
},
{
label: "High",
value: "high",
description: "Best quality with longer generation time",
},
];
const renderQualitySelector = (llmConfig: LLMConfig, input_field_changed: (value: string, field: string) => void) => {
if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
return (
<div className="w-[295px]">
<label className="block text-sm font-medium text-gray-700 mb-2">
DALL·E 3 Image Quality
</label>
<div className="">
<Select value={llmConfig.DALL_E_3_QUALITY} onValueChange={(value) => input_field_changed(value, "dall_e_3_quality")}>
<SelectTrigger 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">
<SelectValue placeholder="Select a quality" />
</SelectTrigger>
<SelectContent>
{DALLE_3_QUALITY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
{/* {DALLE_3_QUALITY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"border rounded-lg p-3 text-left transition-colors",
llmConfig.DALL_E_3_QUALITY === option.value
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
)}
onClick={() =>
input_field_changed(option.value, "dall_e_3_quality")
}
>
<div className="text-sm font-medium text-gray-900">
{option.label}
</div>
<div className="text-xs text-gray-600 mt-1">
{option.description}
</div>
</button>
))} */}
</div>
</div>
);
}
if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
return (
<div className="w-[295px]">
<label className="block text-sm font-medium text-gray-700 mb-2">
GPT Image 1.5 Quality
</label>
<div className="">
<Select
value={llmConfig.GPT_IMAGE_1_5_QUALITY}
onValueChange={(value) => input_field_changed(value, "gpt_image_1_5_quality")}
>
<SelectTrigger
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">
<SelectValue placeholder="Select a quality" />
</SelectTrigger>
<SelectContent>
{GPT_IMAGE_1_5_QUALITY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
{/* {GPT_IMAGE_1_5_QUALITY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"border rounded-lg p-3 text-left transition-colors",
llmConfig.GPT_IMAGE_1_5_QUALITY === option.value
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
)}
onClick={() =>
input_field_changed(option.value, "gpt_image_1_5_quality")
}
>
<div className="text-sm font-medium text-gray-900">
{option.label}
</div>
<div className="text-xs text-gray-600 mt-1">
{option.description}
</div>
</button>
))} */}
</div>
</div>
);
}
return null;
};
const ImageSelectionConfig = ({ isImageGenerationDisabled, openImageProviderSelect, setOpenImageProviderSelect, llmConfig, input_field_changed, getApiKeyValue, handleApiKeyInputChange }: { isImageGenerationDisabled: boolean, openImageProviderSelect: boolean, setOpenImageProviderSelect: (open: boolean) => void, llmConfig: LLMConfig, input_field_changed: (value: string, field: string) => void, getApiKeyValue: (field: string) => string, handleApiKeyInputChange: (field: string, value: string) => void }) => {
return (
<div className='mt-7'>
<div className="p-10 flex justify-between items-center bg-white rounded-[12px]">
<div>
<h4 className="text-xl font-normal text-[#191919]">Image Generation Settings</h4>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Choosing where images come from.
</p>
</div>
<div className='flex items-center gap-4'>
{!isImageGenerationDisabled && (
<>
{/* Image Provider Selection */}
<div className="my-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-[275px] 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>
{renderQualitySelector(llmConfig, input_field_changed)}
{/* 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 === "gpt-image-1.5" &&
llmConfig.LLM === "openai"
) {
return <></>;
}
if (
provider.value === "gemini_flash" &&
llmConfig.LLM === "google"
) {
return <></>;
}
if (
provider.value === "nanobanana_pro" &&
llmConfig.LLM === "google"
) {
return <></>;
}
// Show ComfyUI configuration
if (provider.value === "comfyui") {
return (
<div className=" space-y-4 w-[295px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ComfyUI Server URL
</label>
<div className="relative">
<input
type="text"
placeholder="http://192.168.1.7:8188"
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={llmConfig.COMFYUI_URL || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"comfyui_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>
Use your machine IP address (not localhost) when
running in Docker
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Workflow JSON
</label>
<div className="relative">
<textarea
placeholder='Paste your ComfyUI workflow JSON here (export via "Export (API)" in ComfyUI)'
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 font-mono text-xs"
rows={6}
value={llmConfig.COMFYUI_WORKFLOW || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"comfyui_workflow"
);
}}
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Export your workflow from ComfyUI using &quot;Export
(API)&quot; and paste the JSON here.
</p>
</div>
</div>
);
}
// Show API key input for other providers
return (
<div className=" w-[295px]">
<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 h-12 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
value={getApiKeyValue(provider.apiKeyField || "")}
onChange={(e) =>
handleApiKeyInputChange(
provider.apiKeyField || "",
e.target.value
)
}
/>
</div>
</div>
);
})()}
</>
)}
</div>
</div>
</div>
)
}
export default ImageSelectionConfig

View file

@ -1,19 +1,5 @@
"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 { Switch } from "./ui/switch";
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 AnthropicConfig from "./AnthropicConfig";
@ -23,39 +9,11 @@ import {
updateLLMConfig,
changeProvider as changeProviderUtil,
} from "@/utils/providerUtils";
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
import { LLMConfig } from "@/types/llm_config";
import ImageSelectionConfig from "./ImageSelectionConfig";
const DALLE_3_QUALITY_OPTIONS = [
{
label: "Standard",
value: "standard",
description: "Faster generation with lower cost",
},
{
label: "HD",
value: "hd",
description: "Higher quality images with increased cost",
},
];
const GPT_IMAGE_1_5_QUALITY_OPTIONS = [
{
label: "Low",
value: "low",
description: "Fastest and most cost-effective",
},
{
label: "Medium",
value: "medium",
description: "Balanced quality and speed",
},
{
label: "High",
value: "high",
description: "Best quality with longer generation time",
},
];
// Button state interface
interface ButtonState {
@ -76,6 +34,28 @@ interface LLMProviderSelectionProps {
) => void;
}
const LLM_TABS = [
{
label: 'OpenAI',
value: 'openai',
},
{
label: 'Google',
value: 'google',
},
{
label: 'Anthropic',
value: 'anthropic',
},
{
label: 'Ollama',
value: 'ollama',
},
{
label: 'Custom',
value: 'custom',
},
];
export default function LLMProviderSelection({
initialLLMConfig,
onConfigChange,
@ -84,7 +64,6 @@ export default function LLMProviderSelection({
const [llmConfig, setLlmConfig] = useState<LLMConfig>(initialLLMConfig);
const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
const isImageGenerationDisabled = llmConfig.DISABLE_IMAGE_GENERATION ?? false;
useEffect(() => {
onConfigChange(llmConfig);
}, [llmConfig]);
@ -133,12 +112,12 @@ export default function LLMProviderSelection({
text: needsModelSelection
? "Please Select a Model"
: needsApiKey
? "Please Enter API Key"
: needsOllamaUrl
? "Please Enter Ollama URL"
: needsComfyUIConfig
? "Please Configure ComfyUI"
: "Save Configuration",
? "Please Enter API Key"
: needsOllamaUrl
? "Please Enter Ollama URL"
: needsComfyUIConfig
? "Please Configure ComfyUI"
: "Save Configuration",
showProgress: false,
});
}, [llmConfig]);
@ -254,160 +233,96 @@ export default function LLMProviderSelection({
});
}, [llmConfig.IMAGE_PROVIDER]);
const renderQualitySelector = () => {
if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
return (
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
DALL·E 3 Image Quality
</label>
<div className="grid grid-cols-2 gap-3">
{DALLE_3_QUALITY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"border rounded-lg p-3 text-left transition-colors",
llmConfig.DALL_E_3_QUALITY === option.value
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
)}
onClick={() =>
input_field_changed(option.value, "dall_e_3_quality")
}
>
<div className="text-sm font-medium text-gray-900">
{option.label}
</div>
<div className="text-xs text-gray-600 mt-1">
{option.description}
</div>
</button>
))}
</div>
</div>
);
}
if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
return (
<div className="mb-8">
<label className="block text-sm font-medium text-gray-700 mb-2">
GPT Image 1.5 Quality
</label>
<div className="grid grid-cols-3 gap-3">
{GPT_IMAGE_1_5_QUALITY_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={cn(
"border rounded-lg p-3 text-left transition-colors",
llmConfig.GPT_IMAGE_1_5_QUALITY === option.value
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
)}
onClick={() =>
input_field_changed(option.value, "gpt_image_1_5_quality")
}
>
<div className="text-sm font-medium text-gray-900">
{option.label}
</div>
<div className="text-xs text-gray-600 mt-1">
{option.description}
</div>
</button>
))}
</div>
</div>
);
}
return null;
};
return (
<div className="h-full flex flex-col mt-10">
<div className="h-full flex flex-col">
{/* 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-5 bg-transparent h-10">
<TabsTrigger value="openai">OpenAI</TabsTrigger>
<TabsTrigger value="google">Google</TabsTrigger>
<TabsTrigger value="anthropic">Anthropic</TabsTrigger>
<TabsTrigger value="ollama">Ollama</TabsTrigger>
<TabsTrigger value="custom">Custom</TabsTrigger>
</TabsList>
</Tabs>
<div className="p-1.5 rounded-[41px] bg-white ">
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center '>
{LLM_TABS.map((tab) => (
<button key={tab.value} className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => { handleProviderChange(tab.value) }}
style={{
background: tab.value === llmConfig.LLM ? '#F4F3FF' : 'transparent',
color: tab.value === llmConfig.LLM ? '#5146E5' : '#3A3A3A'
}}
>{tab.label}</button>
))}
</div>
</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 || ""}
openaiModel={llmConfig.OPENAI_MODEL || ""}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
/>
</TabsContent>
<div className="flex-1 bg-[#F9F8F8] p-7 rounded-[20px] overflow-y-auto pt-0 custom_scrollbar">
{/* Google Content */}
<TabsContent value="google" className="mt-6">
<GoogleConfig
googleApiKey={llmConfig.GOOGLE_API_KEY || ""}
googleModel={llmConfig.GOOGLE_MODEL || ""}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
/>
</TabsContent>
{/* OpenAI Content */}
{llmConfig.LLM === "openai" && <div className="mt-6">
<OpenAIConfig
openaiApiKey={llmConfig.OPENAI_API_KEY || ""}
openaiModel={llmConfig.OPENAI_MODEL || ""}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
llmConfig={llmConfig}
/>
</div>}
{/* Anthropic Content */}
<TabsContent value="anthropic" className="mt-6">
<AnthropicConfig
anthropicApiKey={llmConfig.ANTHROPIC_API_KEY || ""}
anthropicModel={llmConfig.ANTHROPIC_MODEL || ""}
extendedReasoning={llmConfig.EXTENDED_REASONING || false}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
/>
</TabsContent>
{/* Google Content */}
{llmConfig.LLM === "google" && <div className="mt-6">
<GoogleConfig
googleApiKey={llmConfig.GOOGLE_API_KEY || ""}
googleModel={llmConfig.GOOGLE_MODEL || ""}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
/>
</div>}
{/* Ollama Content */}
<TabsContent value="ollama" className="mt-6">
<OllamaConfig
ollamaModel={llmConfig.OLLAMA_MODEL || ""}
ollamaUrl={llmConfig.OLLAMA_URL || ""}
useCustomUrl={llmConfig.USE_CUSTOM_URL || false}
onInputChange={input_field_changed}
/>
</TabsContent>
{/* Anthropic Content */}
{llmConfig.LLM === "anthropic" && <div className="mt-6">
<AnthropicConfig
anthropicApiKey={llmConfig.ANTHROPIC_API_KEY || ""}
anthropicModel={llmConfig.ANTHROPIC_MODEL || ""}
extendedReasoning={llmConfig.EXTENDED_REASONING || false}
webGrounding={llmConfig.WEB_GROUNDING || false}
onInputChange={input_field_changed}
/>
</div>}
{/* Ollama Content */}
{llmConfig.LLM === "ollama" && <div className="mt-6">
<OllamaConfig
ollamaModel={llmConfig.OLLAMA_MODEL || ""}
ollamaUrl={llmConfig.OLLAMA_URL || ""}
useCustomUrl={llmConfig.USE_CUSTOM_URL || false}
onInputChange={input_field_changed}
/>
</div>}
{/* Custom Content */}
{llmConfig.LLM === "custom" && <div className="mt-6">
<CustomConfig
customLlmUrl={llmConfig.CUSTOM_LLM_URL || ""}
customLlmApiKey={llmConfig.CUSTOM_LLM_API_KEY || ""}
customModel={llmConfig.CUSTOM_MODEL || ""}
toolCalls={llmConfig.TOOL_CALLS || false}
disableThinking={llmConfig.DISABLE_THINKING || false}
onInputChange={input_field_changed}
/>
</div>}
{/* Custom Content */}
<TabsContent value="custom" className="mt-6">
<CustomConfig
customLlmUrl={llmConfig.CUSTOM_LLM_URL || ""}
customLlmApiKey={llmConfig.CUSTOM_LLM_API_KEY || ""}
customModel={llmConfig.CUSTOM_MODEL || ""}
toolCalls={llmConfig.TOOL_CALLS || false}
disableThinking={llmConfig.DISABLE_THINKING || false}
onInputChange={input_field_changed}
/>
</TabsContent>
</Tabs>
{/* Image Generation Toggle */}
<div className="my-8">
<ImageSelectionConfig
isImageGenerationDisabled={isImageGenerationDisabled}
openImageProviderSelect={openImageProviderSelect}
setOpenImageProviderSelect={setOpenImageProviderSelect}
llmConfig={llmConfig}
input_field_changed={input_field_changed}
getApiKeyValue={getApiKeyValue}
handleApiKeyInputChange={handleApiKeyInputChange}
/>
{/* <div className="my-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">
Disable Image Generation
@ -427,213 +342,12 @@ export default function LLMProviderSelection({
When enabled, slides will not include automatically generated
images.
</p>
</div>
</div> */}
{!isImageGenerationDisabled && (
<>
{/* Image Provider Selection */}
<div className="my-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>
{renderQualitySelector()}
{/* 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 === "gpt-image-1.5" &&
llmConfig.LLM === "openai"
) {
return <></>;
}
if (
provider.value === "gemini_flash" &&
llmConfig.LLM === "google"
) {
return <></>;
}
if (
provider.value === "nanobanana_pro" &&
llmConfig.LLM === "google"
) {
return <></>;
}
// Show ComfyUI configuration
if (provider.value === "comfyui") {
return (
<div className="mb-8 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ComfyUI Server URL
</label>
<div className="relative">
<input
type="text"
placeholder="http://192.168.1.7:8188"
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={llmConfig.COMFYUI_URL || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"comfyui_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>
Use your machine IP address (not localhost) when
running in Docker
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Workflow JSON
</label>
<div className="relative">
<textarea
placeholder='Paste your ComfyUI workflow JSON here (export via "Export (API)" in ComfyUI)'
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 font-mono text-xs"
rows={6}
value={llmConfig.COMFYUI_WORKFLOW || ""}
onChange={(e) => {
input_field_changed(
e.target.value,
"comfyui_workflow"
);
}}
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Export your workflow from ComfyUI using &quot;Export
(API)&quot; and paste the JSON here.
</p>
</div>
</div>
);
}
// 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={getApiKeyValue(provider.apiKeyField)}
onChange={(e) =>
handleApiKeyInputChange(
provider.apiKeyField,
e.target.value
)
}
/>
</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="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>
@ -645,14 +359,14 @@ export default function LLMProviderSelection({
{llmConfig.LLM === "ollama"
? llmConfig.OLLAMA_MODEL ?? "xxxxx"
: llmConfig.LLM === "custom"
? llmConfig.CUSTOM_MODEL ?? "xxxxx"
: llmConfig.LLM === "anthropic"
? llmConfig.ANTHROPIC_MODEL ?? "xxxxx"
: llmConfig.LLM === "google"
? llmConfig.GOOGLE_MODEL ?? "xxxxx"
: llmConfig.LLM === "openai"
? llmConfig.OPENAI_MODEL ?? "xxxxx"
: "xxxxx"}{" "}
? llmConfig.CUSTOM_MODEL ?? "xxxxx"
: llmConfig.LLM === "anthropic"
? llmConfig.ANTHROPIC_MODEL ?? "xxxxx"
: llmConfig.LLM === "google"
? llmConfig.GOOGLE_MODEL ?? "xxxxx"
: llmConfig.LLM === "openai"
? llmConfig.OPENAI_MODEL ?? "xxxxx"
: "xxxxx"}{" "}
for text generation{" "}
{isImageGenerationDisabled ? (
"and image generation is disabled."
@ -660,7 +374,7 @@ export default function LLMProviderSelection({
<>
and{" "}
{llmConfig.IMAGE_PROVIDER &&
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER].label
: "xxxxx"}{" "}
for images
@ -669,7 +383,28 @@ export default function LLMProviderSelection({
</p>
</div>
</div>
</div>
</div> */}
{/* <button
onClick={handleSaveConfig}
disabled={buttonState.isDisabled}
style={{
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)",
color: "#101323",
}}
className={`w-full font-semibold py-3 px-4 rounded-lg transition-all duration-500 ${buttonState.isDisabled
? "bg-gray-400 cursor-not-allowed"
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:ring-4 focus:ring-blue-200"
} text-white`}
>
{buttonState.isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
{buttonState.text}
</div>
) : (
buttonState.text
)}
</button> */}
</div>
</div>
);

View file

@ -14,25 +14,29 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { Switch } from "./ui/switch";
import { LLMConfig } from "@/types/llm_config";
interface OpenAIConfigProps {
openaiApiKey: string;
openaiModel: string;
webGrounding?: boolean;
onInputChange: (value: string | boolean, field: string) => void;
llmConfig: LLMConfig;
}
export default function OpenAIConfig({
openaiApiKey,
openaiModel,
webGrounding,
onInputChange
onInputChange,
llmConfig
}: OpenAIConfigProps) {
const [openModelSelect, setOpenModelSelect] = useState(false);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [modelsChecked, setModelsChecked] = useState(false);
const [apiKey, setApiKey] = useState(openaiApiKey);
const isImageGenerationDisabled = llmConfig.DISABLE_IMAGE_GENERATION ?? false;
const openaiUrl = "https://api.openai.com/v1";
@ -84,152 +88,189 @@ export default function OpenAIConfig({
};
return (
<div className="space-y-6">
<div className="space-y-6 ">
{/* API Key Input */}
<div className="mb-4">
<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) => onApiKeyChange(e.target.value)}
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>
<div className="mb-4 flex items-center justify-between bg-white p-10">
<div className="">
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<div className="mb-4">
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !openaiApiKey}
className={`w-full py-2.5 px-4 rounded-lg transition-all duration-200 border-2 ${modelsLoading || !openaiApiKey
? "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"
}`}
>
{modelsLoading ? (
<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 */}
{modelsChecked && availableModels.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 your API key is valid and has access to OpenAI models.
<h3 className="text-xl font-normal text-[#191919]">OpenAI API key</h3>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Your API key will be stored locally and never shared
</p>
</div>
)}
<div className="flex items-center gap-4">
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select OpenAI Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<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">
<span className="text-sm font-medium text-gray-900">
{openaiModel
? availableModels.find(model => model === openaiModel) || openaiModel
: "Select a model"}
</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)" }}
<div className="relative w-[275px] ">
<div className="flex flex-col justify-start gap-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
OpenAI API Key
</label>
<input
type="text"
value={openaiApiKey}
onChange={(e) => onApiKeyChange(e.target.value)}
className="w-full px-2 py-3 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>
{/* Check for available models button - show when no models checked or no models found */}
{(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
<button
onClick={fetchAvailableModels}
disabled={modelsLoading || !openaiApiKey}
className={` mt-7 py-2.5 bg-[#F7F6F9] px-3.5 rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading || !openaiApiKey
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-blue-600 hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "openai_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
openaiModel === model
? "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">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{modelsLoading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Checking for models...
</span>
) : (
"Check for available models"
)}
</button>
)}
</div>
<div className="w-[295px]">
{/* Show message if no models found */}
{modelsChecked && availableModels.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 your API key is valid and has access to OpenAI models.
</p>
</div>
)}
{/* Model Selection - only show if models are available */}
{modelsChecked && availableModels.length > 0 ? (
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Select OpenAI Model
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<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">
<span className="text-sm font-medium text-gray-900">
{openaiModel
? availableModels.find(model => model === openaiModel) || openaiModel
: "Select a model"}
</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 models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
onInputChange(value, "openai_model");
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
openaiModel === model
? "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">
{model}
</span>
</div>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
{/* Web Grounding Toggle - show at the end, below models dropdown */}
<div>
<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">
Enable Web Grounding
</label>
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
<div>
<h4 className="text-xl font-normal text-[#191919]">Model Controls</h4>
<p className="mt-2 text-sm max-w-[205px] text-gray-500">
Configure web access, image generation, and advanced AI features.
</p>
</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>
If enabled, the model can use web search grounding when available.
</p>
<div className="flex items-center gap-4">
<div className="w-[275px]">
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={!!webGrounding}
onCheckedChange={(checked) => onInputChange(checked, "web_grounding")}
/>
<label className="text-sm font-medium text-gray-700">
Enable Web Grounding
</label>
</div>
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
checked={!!isImageGenerationDisabled}
onCheckedChange={(checked) => onInputChange(checked, "disable_image_generation")}
/>
<label className="text-sm font-medium text-gray-700">
Disable Image Generation
</label>
</div>
</div>
<div className="w-[295px]"></div>
</div>
</div>
</div>
);
}