feat: Add dashboard layout, components, and templates page with loading states
This commit is contained in:
parent
cb8ad52f0c
commit
07ae990c95
31 changed files with 741 additions and 623 deletions
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
@ -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";
|
||||
|
||||
|
||||
|
||||
|
|
@ -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 ? (
|
||||
|
|
@ -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' />
|
||||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
357
servers/nextjs/components/ImageSelectionConfig.tsx
Normal file
357
servers/nextjs/components/ImageSelectionConfig.tsx
Normal 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 "Export
|
||||
(API)" 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
|
||||
|
|
@ -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 "Export
|
||||
(API)" 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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue