feat: OnBoarding two pages design
This commit is contained in:
parent
afa60af22c
commit
ed76eff535
17 changed files with 1170 additions and 143 deletions
|
|
@ -32,7 +32,7 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
|
|||
|
||||
const fetchUserConfigState = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
|
||||
let canChangeKeys = false;
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
// Electron mode: use IPC
|
||||
|
|
@ -74,13 +74,13 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
|
|||
}
|
||||
dispatch(setLLMConfig(llmConfig));
|
||||
const isValid = hasValidLLMConfig(llmConfig);
|
||||
|
||||
|
||||
// Allow access to pdf-maker without LLM configuration (needed for PPTX export)
|
||||
if (route.startsWith('/pdf-maker')) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (isValid) {
|
||||
// Check if the selected Ollama model is pulled
|
||||
if (llmConfig.LLM === 'ollama' && llmConfig.OLLAMA_MODEL) {
|
||||
|
|
@ -134,12 +134,12 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
|
|||
api_key: llmConfig.CUSTOM_LLM_API_KEY,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Custom model check failed with status:', response.status);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
return data.includes(llmConfig.CUSTOM_MODEL);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -122,9 +122,9 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center'>
|
||||
<div className='flex justify-end items-center'>
|
||||
<Switch
|
||||
checked={isImageGenerationDisabled}
|
||||
checked={!isImageGenerationDisabled}
|
||||
className=''
|
||||
onCheckedChange={(checked) => handleChangeImageGenerationDisabled(checked)}
|
||||
onCheckedChange={(checked) => handleChangeImageGenerationDisabled(!checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ const TextProvider = ({
|
|||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
<div className="w-[275px]">
|
||||
<div className="w-[205px]">
|
||||
<div className="flex items-center mb-4 gap-2.5 ">
|
||||
<Switch
|
||||
checked={!!llmConfig.WEB_GROUNDING}
|
||||
|
|
@ -481,9 +481,10 @@ const TextProvider = ({
|
|||
|
||||
|
||||
</div>
|
||||
<div className="w-[295px]"></div>
|
||||
{/* <div className="w-[295px]"></div> */}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
|
|||
{/* Outlines content */}
|
||||
|
||||
{outlines && outlines.length > 0 && (
|
||||
<div className="bg-[#F9F8F8] p-7 rounded-[20px] overflow-y-auto custom_scrollbar">
|
||||
<div className="bg-[#F9F8F8] p-7 rounded-[20px] overflow-y-auto custom_scrollbar">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
|
|
|
|||
|
|
@ -119,12 +119,12 @@ export function OutlineItem({
|
|||
}, [isStreaming, isActiveStreaming, isStableStreaming, slideOutline.content])
|
||||
|
||||
return (
|
||||
<div className="mb-4 bg-white rounded-[12px] shadow-sm p-10 relative">
|
||||
<div className="mb-4 bg-white rounded-[12px] group shadow-sm p-10 relative">
|
||||
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`flex items-start gap-3 md:gap-4 rounded-[8px] ${isDragging ? "opacity-50" : ""}`}
|
||||
className={`flex items-start gap-3 md:gap-4 rounded-[8px] ${isDragging ? "opacity-50" : ""}`}
|
||||
>
|
||||
|
||||
<div
|
||||
|
|
@ -165,7 +165,7 @@ export function OutlineItem({
|
|||
</div>
|
||||
|
||||
|
||||
<div className="absolute -top-3 -right-3 flex gap-1 sm:gap-2 items-center">
|
||||
<div className="hidden group-hover:flex absolute -top-3 -right-3 gap-1 sm:gap-2 items-center">
|
||||
|
||||
<ToolTip content="Delete Slide">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const OutlinePage: React.FC = () => {
|
|||
/>
|
||||
|
||||
<Wrapper className="h-full flex flex-col w-full relative">
|
||||
<div className="flex-grow w-full overflow-y-hidden mx-auto ">
|
||||
<div className="flex-grow w-full hidden-scrollbar mx-auto ">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
||||
<TabsList className="my-4 h-auto w-fit rounded-full border border-[#DFDFE1] bg-[#F8F8F9] p-1.5">
|
||||
<TabsTrigger
|
||||
|
|
@ -76,7 +76,7 @@ const OutlinePage: React.FC = () => {
|
|||
</TabsList>
|
||||
|
||||
<div className="flex-grow w-full mx-auto">
|
||||
<TabsContent value={TABS.OUTLINE} className="h-[calc(100vh-16rem)] overflow-y-auto custom_scrollbar"
|
||||
<TabsContent value={TABS.OUTLINE} className="h-[calc(100vh-16rem)] overflow-y-auto hide-scrollbar"
|
||||
>
|
||||
<div>
|
||||
<OutlineContent
|
||||
|
|
@ -91,7 +91,7 @@ const OutlinePage: React.FC = () => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={TABS.LAYOUTS} className="h-[calc(100vh-16rem)] overflow-y-auto custom_scrollbar">
|
||||
<TabsContent value={TABS.LAYOUTS} className="h-[calc(100vh-16rem)] overflow-y-auto hide-scrollbar">
|
||||
<div>
|
||||
<TemplateSelection
|
||||
selectedTemplate={selectedTemplate}
|
||||
|
|
@ -103,7 +103,7 @@ const OutlinePage: React.FC = () => {
|
|||
</Tabs>
|
||||
{/* Fixed Button */}
|
||||
|
||||
<div className="absolute bottom-36 -right-10 z-50">
|
||||
<div className="absolute bottom-0 -right-10 z-50">
|
||||
<GenerateButton
|
||||
outlineCount={outlines.length}
|
||||
loadingState={loadingState}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
import React, { useEffect, useMemo, useCallback, memo } from "react";
|
||||
|
||||
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
|
||||
import { templates} from "@/app/presentation-templates";
|
||||
import { templates } from "@/app/presentation-templates";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { TemplateWithData } from "@/app/presentation-templates/utils";
|
||||
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { CustomTemplateCard } from "./CustomTemplateCard";
|
||||
import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate";
|
||||
|
||||
// Memoized layout preview for built-in templates
|
||||
const BuiltInLayoutPreview = memo(({ layout, templateId, index }: {
|
||||
|
|
@ -137,12 +138,10 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
|
|||
}
|
||||
if (customTemplates.length === 0) {
|
||||
return (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-gray-500">No custom templates yet.</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Custom templates you create will appear here.
|
||||
</p>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
|
||||
<CreateCustomTemplate />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: {
|
|||
dispatch(updateTheme(null))
|
||||
}
|
||||
|
||||
console.log('presentation data', presentationData)
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo
|
|||
if (!llmConfig.LLM) {
|
||||
llmConfig.LLM = 'openai';
|
||||
}
|
||||
if (!llmConfig.IMAGE_PROVIDER) {
|
||||
llmConfig.IMAGE_PROVIDER = 'gpt-image-1.5';
|
||||
}
|
||||
dispatch(setLLMConfig(llmConfig));
|
||||
const isValid = hasValidLLMConfig(llmConfig);
|
||||
if (isValid) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ import {
|
|||
import { LLMConfig } from "@/types/llm_config";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import { usePathname } from "next/navigation";
|
||||
import OnBoardingSlidebar from "./OnBoarding/OnBoardingSlidebar";
|
||||
import OnBoardingHeader from "./OnBoarding/OnBoardingHeader";
|
||||
import ModeSelectStep from "./OnBoarding/ModeSelectStep";
|
||||
import PresentonMode from "./OnBoarding/PresentonMode";
|
||||
import GenerationWithImage from "./OnBoarding/GenerationWithImage";
|
||||
|
||||
// Button state interface
|
||||
interface ButtonState {
|
||||
|
|
@ -28,9 +33,11 @@ interface ButtonState {
|
|||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [step, setStep] = useState<number>(1)
|
||||
const [selectedMode, setSelectedMode] = useState<string>("presenton")
|
||||
const config = useSelector((state: RootState) => state.userConfig);
|
||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>(config.llm_config);
|
||||
|
||||
console.log('config', config);
|
||||
const [downloadingModel, setDownloadingModel] = useState<{
|
||||
name: string;
|
||||
size: number | null;
|
||||
|
|
@ -144,124 +151,133 @@ export default function Home() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
|
||||
<main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
|
||||
{/* Branding Header */}
|
||||
<div className="text-center mb-2 mt-4 flex-shrink-0">
|
||||
<div className="flex items-center justify-center gap-3 mb-2">
|
||||
<img src="/Logo.png" alt="Presenton Logo" className="h-12" />
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Open-source AI presentation generator
|
||||
</p>
|
||||
</div>
|
||||
// <div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
|
||||
// <main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
|
||||
// {/* Branding Header */}
|
||||
// <div className="text-center mb-2 mt-4 flex-shrink-0">
|
||||
// <div className="flex items-center justify-center gap-3 mb-2">
|
||||
// <img src="/Logo.png" alt="Presenton Logo" className="h-12" />
|
||||
// </div>
|
||||
// <p className="text-gray-600 text-sm">
|
||||
// Open-source AI presentation generator
|
||||
// </p>
|
||||
// </div>
|
||||
|
||||
{/* Main Configuration Card */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<LLMProviderSelection
|
||||
initialLLMConfig={llmConfig}
|
||||
onConfigChange={setLlmConfig}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
/>
|
||||
</div>
|
||||
// {/* Main Configuration Card */}
|
||||
// <div className="flex-1 overflow-hidden">
|
||||
// <LLMProviderSelection
|
||||
// initialLLMConfig={llmConfig}
|
||||
// onConfigChange={setLlmConfig}
|
||||
// buttonState={buttonState}
|
||||
// setButtonState={setButtonState}
|
||||
// />
|
||||
// </div>
|
||||
// </main>
|
||||
|
||||
// {/* Download Progress Modal */}
|
||||
// {showDownloadModal && downloadingModel && (
|
||||
// <div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
// <div className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl max-w-md w-full p-6 relative">
|
||||
// {/* Modal Content */}
|
||||
// <div className="text-center">
|
||||
// {/* Icon */}
|
||||
// <div className="mb-4">
|
||||
// {downloadingModel.done ? (
|
||||
// <CheckCircle className="w-12 h-12 text-green-600 mx-auto" />
|
||||
// ) : (
|
||||
// <Download className="w-12 h-12 text-blue-600 mx-auto animate-pulse" />
|
||||
// )}
|
||||
// </div>
|
||||
|
||||
// {/* Title */}
|
||||
// <h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
// {downloadingModel.done ? "Download Complete!" : "Downloading Model"}
|
||||
// </h3>
|
||||
|
||||
// {/* Model Name */}
|
||||
// <p className="text-sm text-gray-600 mb-6">
|
||||
// {llmConfig.OLLAMA_MODEL}
|
||||
// </p>
|
||||
|
||||
// {/* Progress Bar */}
|
||||
// {downloadProgress > 0 && (
|
||||
// <div className="mb-4">
|
||||
// <div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
// <div
|
||||
// className="bg-blue-600 h-3 rounded-full transition-all duration-300 ease-out"
|
||||
// style={{ width: `${downloadProgress}%` }}
|
||||
// />
|
||||
// </div>
|
||||
// <p className="text-sm text-gray-600 mt-2">
|
||||
// {downloadProgress}% Complete
|
||||
// </p>
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {/* Status */}
|
||||
// {downloadingModel.status && (
|
||||
// <div className="flex items-center justify-center gap-2 mb-4">
|
||||
// <CheckCircle className="w-4 h-4 text-green-600" />
|
||||
// <span className="text-sm font-medium text-green-700 capitalize">
|
||||
// {downloadingModel.status}
|
||||
// </span>
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {/* Status Message */}
|
||||
// {downloadingModel.status && downloadingModel.status !== "pulled" && (
|
||||
// <div className="text-xs text-gray-500">
|
||||
// {downloadingModel.status === "downloading" && "Downloading model files..."}
|
||||
// {downloadingModel.status === "verifying" && "Verifying model integrity..."}
|
||||
// {downloadingModel.status === "pulling" && "Pulling model from registry..."}
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {/* Download Info */}
|
||||
// {downloadingModel.downloaded && downloadingModel.size && (
|
||||
// <div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
// <div className="flex justify-between text-xs text-gray-600">
|
||||
// <span>Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB</span>
|
||||
// <span>Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB</span>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// )}
|
||||
|
||||
// {/* 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">
|
||||
// <button
|
||||
// onClick={handleSaveConfig}
|
||||
// disabled={buttonState.isDisabled}
|
||||
// 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>
|
||||
// </div>
|
||||
<div className="flex h-screen ">
|
||||
<OnBoardingSlidebar />
|
||||
<main className="w-full pl-20 pr-8 max-w-[1440px] mx-auto relative">
|
||||
<OnBoardingHeader currentStep={step} />
|
||||
{step === 1 && <ModeSelectStep setStep={setStep} setSelectedMode={setSelectedMode} />}
|
||||
{step === 2 && selectedMode === "presenton" && <PresentonMode currentStep={step} setStep={setStep} />}
|
||||
{step === 2 && selectedMode === "image" && <GenerationWithImage />}
|
||||
</main>
|
||||
|
||||
{/* Download Progress Modal */}
|
||||
{showDownloadModal && downloadingModel && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl max-w-md w-full p-6 relative">
|
||||
{/* Modal Content */}
|
||||
<div className="text-center">
|
||||
{/* Icon */}
|
||||
<div className="mb-4">
|
||||
{downloadingModel.done ? (
|
||||
<CheckCircle className="w-12 h-12 text-green-600 mx-auto" />
|
||||
) : (
|
||||
<Download className="w-12 h-12 text-blue-600 mx-auto animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{downloadingModel.done ? "Download Complete!" : "Downloading Model"}
|
||||
</h3>
|
||||
|
||||
{/* Model Name */}
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
{llmConfig.OLLAMA_MODEL}
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{downloadProgress > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-blue-600 h-3 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{downloadProgress}% Complete
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
{downloadingModel.status && (
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-700 capitalize">
|
||||
{downloadingModel.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Message */}
|
||||
{downloadingModel.status && downloadingModel.status !== "pulled" && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{downloadingModel.status === "downloading" && "Downloading model files..."}
|
||||
{downloadingModel.status === "verifying" && "Verifying model integrity..."}
|
||||
{downloadingModel.status === "pulling" && "Pulling model from registry..."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Info */}
|
||||
{downloadingModel.downloaded && downloadingModel.size && (
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex justify-between text-xs text-gray-600">
|
||||
<span>Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB</span>
|
||||
<span>Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
onClick={handleSaveConfig}
|
||||
disabled={buttonState.isDisabled}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
11
servers/nextjs/components/OnBoarding/GenerationWithImage.tsx
Normal file
11
servers/nextjs/components/OnBoarding/GenerationWithImage.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
|
||||
const GenerationWithImage = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GenerationWithImage
|
||||
56
servers/nextjs/components/OnBoarding/ModeSelectStep.tsx
Normal file
56
servers/nextjs/components/OnBoarding/ModeSelectStep.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { ChevronRight } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
const ModeSelectStep = ({ setStep, setSelectedMode }: { setStep: (step: number) => void, setSelectedMode: (mode: string) => void }) => {
|
||||
return (
|
||||
<div className='max-w-[650px]'>
|
||||
<div className='mb-[70px]'>
|
||||
|
||||
<h2 className='mb-4 text-black text-[26px] font-normal '>Let’s set up your AI workspace</h2>
|
||||
<p className='text-[##000000CC] text-xl font-normal'>First, choose the intelligence behind your presentation generation.</p>
|
||||
</div>
|
||||
<div className='space-y-5'>
|
||||
<div onClick={() => {
|
||||
setSelectedMode("presenton")
|
||||
setStep(2)
|
||||
}} className='border border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'>
|
||||
<div className='flex items-center gap-6'>
|
||||
<div className='rounded-[4px] bg-[#F4F3FF] p-[12px] w-[58px] h-[58px] flex items-center justify-center'>
|
||||
<img src='/logo-with-bg.png' alt='presenton' className='w-full h-full object-contain' />
|
||||
</div>
|
||||
<div className=''>
|
||||
<div className='flex items-start gap-2 relative '>
|
||||
|
||||
<h3 className='text-black text-[18px] font-medium'>Presenton</h3>
|
||||
<p className='bg-[#F4F3FF] px-3 py-1.5 rounded-[30px] text-[#7A5AF8] text-[9px] absolute left-[95px] top-[-10px]'>PPTX</p>
|
||||
</div>
|
||||
<p className='text-[#999999] text-[14px] font-normal'>Optimized for fast, structured slide generation.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className='w-6 h-6 text-[#B3B3B3]' />
|
||||
</div>
|
||||
<div onClick={() => {
|
||||
setSelectedMode("image")
|
||||
setStep(2)
|
||||
}} className='border border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'>
|
||||
<div className='flex items-center gap-6'>
|
||||
<div className='rounded-[4px] bg-[#FFF6ED] p-[12px] w-[58px] h-[58px] flex items-center justify-center'>
|
||||
<img src='/image_mode.png' alt='presenton' className='w-full h-full object-contain' />
|
||||
</div>
|
||||
<div className=''>
|
||||
<div className='flex items-start gap-2 relative '>
|
||||
|
||||
<h3 className='text-black text-[18px] font-medium'>Generate with Image Model</h3>
|
||||
|
||||
</div>
|
||||
<p className='text-[#999999] text-[14px] font-normal'>Generate presentations with visual layouts and elements.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className='w-6 h-6 text-[#B3B3B3]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModeSelectStep
|
||||
36
servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx
Normal file
36
servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react'
|
||||
|
||||
|
||||
const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
|
||||
return (
|
||||
<div className='flex items-center justify-end gap-1 mt-7 mb-[52px]'>
|
||||
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className={`${currentStep >= 1 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
|
||||
1
|
||||
</div>
|
||||
<p className='text-[#010000] text-xs '>Select Mode</p>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="1" viewBox="0 0 22 1" fill="none">
|
||||
<path d="M0 0.5H21.5" stroke="#ECECEF" />
|
||||
</svg>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className={`${currentStep >= 2 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
|
||||
2
|
||||
</div>
|
||||
<p className='text-[#010000] text-xs '>Choose Providers</p>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="1" viewBox="0 0 22 1" fill="none">
|
||||
<path d="M0 0.5H21.5" stroke="#ECECEF" />
|
||||
</svg>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className={`${currentStep >= 3 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}>
|
||||
3
|
||||
</div>
|
||||
<p className='text-[#010000] text-xs '>Finish Setup</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnBoardingHeader
|
||||
19
servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx
Normal file
19
servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
|
||||
const OnBoardingSlidebar = () => {
|
||||
return (
|
||||
<div className='bg-[#F6F6F9] w-[300px] relative'>
|
||||
<img src="/Logo.png" alt="Presenton logo" className="absolute top-0 left-0 w-[128px] m-6" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="591" viewBox="0 0 296 591" fill="none">
|
||||
<path d="M291.5 183.5C311.916 183.5 328.5 200.271 328.5 221C328.5 241.729 311.916 258.5 291.5 258.5C271.084 258.5 254.5 241.729 254.5 221C254.5 200.271 271.084 183.5 291.5 183.5Z" stroke="#EDEEEF" strokeWidth="3" />
|
||||
<path d="M291.5 131.238C340.408 131.238 380.089 171.407 380.09 220.998C380.09 270.589 340.408 310.758 291.5 310.758C242.591 310.758 202.91 270.589 202.91 220.998C202.91 171.407 242.591 131.238 291.5 131.238Z" stroke="#EDEEEF" strokeWidth="3" />
|
||||
<path d="M291.5 43.6289C388.173 43.6289 466.576 123.021 466.576 220.998C466.576 318.975 388.174 398.368 291.5 398.368C194.826 398.368 116.424 318.975 116.424 220.998C116.424 123.022 194.826 43.629 291.5 43.6289Z" stroke="#EDEEEF" strokeWidth="3" />
|
||||
<path d="M287.5 -62.5C434.322 -62.5 553.5 64.115 553.5 220.5C553.5 376.885 434.322 503.5 287.5 503.5C140.678 503.5 21.5 376.885 21.5 220.5C21.5 64.115 140.678 -62.5 287.5 -62.5Z" stroke="#EDEEEF" strokeWidth="3" />
|
||||
<path d="M291 -176.5C495.019 -176.5 660.5 -5.07604 660.5 206.5C660.5 418.076 495.019 589.5 291 589.5C86.9809 589.5 -78.5 418.076 -78.5 206.5C-78.5 -5.07604 86.9809 -176.5 291 -176.5Z" stroke="#EDEEEF" strokeWidth="3" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnBoardingSlidebar
|
||||
883
servers/nextjs/components/OnBoarding/PresentonMode.tsx
Normal file
883
servers/nextjs/components/OnBoarding/PresentonMode.tsx
Normal file
|
|
@ -0,0 +1,883 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
import { Button } from '../ui/button';
|
||||
import { Check, CheckCircle, ChevronLeft, ChevronRight, ChevronUp, Download, Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
|
||||
import { DALLE_3_QUALITY_OPTIONS, GPT_IMAGE_1_5_QUALITY_OPTIONS, IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LLMConfig } from '@/types/llm_config';
|
||||
import { RootState } from '@/store/store';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { toast } from 'sonner';
|
||||
import ToolTip from '../ToolTip';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Select, SelectItem, SelectContent, SelectValue, SelectTrigger } from '../ui/select';
|
||||
import { MixpanelEvent, trackEvent } from '@/utils/mixpanel';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { handleSaveLLMConfig } from '@/utils/storeHelpers';
|
||||
import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel } from '@/utils/providerUtils';
|
||||
|
||||
const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [openProviderSelect, setOpenProviderSelect] = useState(false);
|
||||
const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
|
||||
const userConfigState = useSelector((state: RootState) => state.userConfig);
|
||||
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [openModelSelect, setOpenModelSelect] = useState(false);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [modelsChecked, setModelsChecked] = useState(false);
|
||||
const [showDownloadModal, setShowDownloadModal] = useState(false);
|
||||
const [savingConfig, setSavingConfig] = useState(false);
|
||||
const [llmConfig, setLlmConfig] = useState<LLMConfig>(
|
||||
userConfigState.llm_config
|
||||
);
|
||||
const [downloadingModel, setDownloadingModel] = useState<{
|
||||
name: string;
|
||||
size: number | null;
|
||||
downloaded: number | null;
|
||||
status: string;
|
||||
done: boolean;
|
||||
} | null>(null);
|
||||
|
||||
const handleProviderChange = (provider: string) => {
|
||||
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
LLM: provider
|
||||
}));
|
||||
setOpenProviderSelect(false);
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(false);
|
||||
if (currentModelField) {
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
[currentModelField]: ''
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const currentModelField = useMemo(() => {
|
||||
switch (llmConfig.LLM) {
|
||||
case 'openai':
|
||||
return 'OPENAI_MODEL';
|
||||
case 'google':
|
||||
return 'GOOGLE_MODEL';
|
||||
case 'anthropic':
|
||||
return 'ANTHROPIC_MODEL';
|
||||
case 'ollama':
|
||||
return 'OLLAMA_MODEL';
|
||||
case 'custom':
|
||||
return 'CUSTOM_MODEL';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, [llmConfig.LLM]);
|
||||
const currentApiKeyField = useMemo(() => {
|
||||
switch (llmConfig.LLM) {
|
||||
case 'openai':
|
||||
return 'OPENAI_API_KEY';
|
||||
case 'google':
|
||||
return 'GOOGLE_API_KEY';
|
||||
case 'anthropic':
|
||||
return 'ANTHROPIC_API_KEY';
|
||||
case 'custom':
|
||||
return 'CUSTOM_LLM_API_KEY';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, [llmConfig.LLM]);
|
||||
|
||||
|
||||
|
||||
const getFieldValue = (field?: string) => {
|
||||
if (!field) return "";
|
||||
return (llmConfig as Record<string, string | undefined>)[field] || "";
|
||||
};
|
||||
|
||||
const currentApiKey = currentApiKeyField ? ((llmConfig as Record<string, unknown>)[currentApiKeyField] as string || '') : '';
|
||||
const currentModel = currentModelField ? ((llmConfig as Record<string, unknown>)[currentModelField] as string || '') : '';
|
||||
|
||||
const fetchAvailableModels = async () => {
|
||||
if (llmConfig.LLM === 'openai' && !currentApiKey) return;
|
||||
if (llmConfig.LLM === 'google' && !currentApiKey) return;
|
||||
if (llmConfig.LLM === 'anthropic' && !currentApiKey) return;
|
||||
if (llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL) return;
|
||||
|
||||
setModelsLoading(true);
|
||||
try {
|
||||
let response: Response;
|
||||
if (llmConfig.LLM === 'google') {
|
||||
response = await fetch('/api/v1/ppt/google/models/available', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_key: currentApiKey
|
||||
}),
|
||||
});
|
||||
} else if (llmConfig.LLM === 'anthropic') {
|
||||
response = await fetch('/api/v1/ppt/anthropic/models/available', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_key: currentApiKey
|
||||
}),
|
||||
});
|
||||
} else if (llmConfig.LLM === 'ollama') {
|
||||
response = await fetch('/api/v1/ppt/ollama/models/supported');
|
||||
} else {
|
||||
response = await fetch('/api/v1/ppt/openai/models/available', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: llmConfig.LLM === 'custom' ? llmConfig.CUSTOM_LLM_URL : LLM_PROVIDERS[llmConfig.LLM!]?.url || '',
|
||||
api_key: currentApiKey
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const normalizedModels: string[] = llmConfig.LLM === 'ollama'
|
||||
? Array.isArray(data)
|
||||
? data.map((model: { value?: string; label?: string }) => model.value || model.label || '').filter(Boolean)
|
||||
: []
|
||||
: Array.isArray(data)
|
||||
? data
|
||||
: [];
|
||||
|
||||
setAvailableModels(normalizedModels);
|
||||
setModelsChecked(true);
|
||||
|
||||
if (normalizedModels.length > 0 && currentModelField) {
|
||||
if (llmConfig[currentModelField] && normalizedModels.includes(llmConfig[currentModelField])) {
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
[currentModelField]: llmConfig[currentModelField]
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const preferredDefault =
|
||||
llmConfig.LLM === 'openai'
|
||||
? 'gpt-4.1'
|
||||
: llmConfig.LLM === 'google'
|
||||
? 'models/gemini-2.5-flash'
|
||||
: llmConfig.LLM === 'anthropic'
|
||||
? 'claude-sonnet-4-20250514'
|
||||
: normalizedModels[0];
|
||||
|
||||
const nextModel = normalizedModels.includes(preferredDefault) ? preferredDefault : normalizedModels[0];
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
[currentModelField]: nextModel
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch models');
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
toast.error(`Failed to fetch ${LLM_PROVIDERS[llmConfig.LLM!]?.label} models`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
toast.error('Error fetching models');
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderQualitySelector = (llmConfig: LLMConfig) => {
|
||||
if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
|
||||
return (
|
||||
<div className="w-full ">
|
||||
<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) => setLlmConfig((prev) => ({
|
||||
...prev,
|
||||
DALL_E_3_QUALITY: value
|
||||
}))}>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<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) => setLlmConfig((prev) => ({
|
||||
...prev,
|
||||
GPT_IMAGE_1_5_QUALITY: value
|
||||
}))}
|
||||
>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const handleModelDownload = async () => {
|
||||
try {
|
||||
await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
|
||||
}
|
||||
finally {
|
||||
setDownloadingModel(null);
|
||||
setShowDownloadModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname });
|
||||
try {
|
||||
setSavingConfig(true);
|
||||
// API: save config
|
||||
trackEvent(MixpanelEvent.Home_SaveConfiguration_API_Call);
|
||||
// API CALL: save config
|
||||
await handleSaveLLMConfig(llmConfig);
|
||||
|
||||
if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) {
|
||||
// API: check model pulled
|
||||
trackEvent(MixpanelEvent.Home_CheckOllamaModelPulled_API_Call);
|
||||
const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL);
|
||||
if (!isPulled) {
|
||||
setShowDownloadModal(true);
|
||||
// API: download model
|
||||
trackEvent(MixpanelEvent.Home_DownloadOllamaModel_API_Call);
|
||||
await handleModelDownload();
|
||||
}
|
||||
}
|
||||
toast.info("Configuration saved successfully");
|
||||
setSavingConfig(false);
|
||||
// Track navigation from -> to
|
||||
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
|
||||
router.push("/upload");
|
||||
} catch (error) {
|
||||
toast.info(error instanceof Error ? error.message : "Failed to save configuration");
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const downloadProgress = useMemo(() => {
|
||||
if (downloadingModel && downloadingModel.downloaded !== null && downloadingModel.size !== null) {
|
||||
return Math.round((downloadingModel.downloaded / downloadingModel.size) * 100);
|
||||
}
|
||||
return 0;
|
||||
}, [downloadingModel?.downloaded, downloadingModel?.size]);
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full max-w-[640px]'>
|
||||
<p className='px-2.5 py-0.5 w-fit text-[#7A5AF8] rounded-[50px] border border-[#EDEEEF] text-[10px] font-medium mb-5'>PRESENTON</p>
|
||||
<div className='mb-[54px]'>
|
||||
|
||||
<h2 className='mb-4 text-black text-[26px] font-normal '>Choose your content providers</h2>
|
||||
<p className='text-[##000000CC] text-xl font-normal'>Select the AI engines that will generate your slide text and visuals.</p>
|
||||
</div>
|
||||
{/* Text Provider */}
|
||||
<div className='p-3 border border-[#EDEEEF] rounded-[11px] '>
|
||||
<div className="flex items-center gap-6 mb-7">
|
||||
<div className='w-[60px] h-[60px] rounded-[4px] flex items-center justify-center'
|
||||
style={{ backgroundColor: '#4C55541A' }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M20 6.6665V33.3332" stroke="#4C5554" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M6.66666 11.6665V8.33317C6.66666 7.89114 6.84225 7.46722 7.15481 7.15466C7.46737 6.8421 7.8913 6.6665 8.33332 6.6665H31.6667C32.1087 6.6665 32.5326 6.8421 32.8452 7.15466C33.1577 7.46722 33.3333 7.89114 33.3333 8.33317V11.6665" stroke="#4C5554" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M15 33.3335H25" stroke="#4C5554" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
|
||||
<h3 className="text-xl font-normal text-[#191919] pb-1.5">Text Generation Settings</h3>
|
||||
<p className=" text-sm text-gray-500">
|
||||
Choosing where text contets come from
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start gap-4 '>
|
||||
<div className="flex flex-col justify-start w-full ">
|
||||
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Text Provider
|
||||
</label>
|
||||
<Popover
|
||||
open={openProviderSelect}
|
||||
onOpenChange={setOpenProviderSelect}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openProviderSelect}
|
||||
className=" 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.LLM
|
||||
? LLM_PROVIDERS[llmConfig.LLM]
|
||||
?.label || llmConfig.LLM
|
||||
: "Select text provider"}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronUp className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0 w-[215px] "
|
||||
align="start"
|
||||
|
||||
>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search provider..." />
|
||||
<CommandList className='hide-scrollbar'>
|
||||
<CommandEmpty>No provider found.</CommandEmpty>
|
||||
<CommandGroup >
|
||||
{Object.values(LLM_PROVIDERS).map(
|
||||
(provider, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={provider.value}
|
||||
onSelect={() => handleProviderChange(provider.value)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
llmConfig.LLM === 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 className="relative flex flex-col justify-end items-end w-full ">
|
||||
<div className="flex flex-col justify-start w-full ">
|
||||
<label className="block text-sm font-medium capitalize text-gray-700 mb-2">
|
||||
{llmConfig.LLM === 'ollama' ? 'Ollama URL' : llmConfig.LLM === 'custom' ? 'Custom LLM API Key' : `${llmConfig.LLM} API Key`}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={llmConfig.LLM === 'ollama' ? 'text' : showApiKey ? 'text' : 'password'}
|
||||
|
||||
value={llmConfig.LLM === 'ollama' ? llmConfig.OLLAMA_URL : currentApiKey}
|
||||
onChange={(e) => setLlmConfig(prev => ({
|
||||
...prev,
|
||||
[currentApiKeyField]: 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={llmConfig.LLM === 'ollama' ? 'http://localhost:11434' : `Enter your ${llmConfig.LLM} API key`}
|
||||
/>
|
||||
{llmConfig.LLM !== 'ollama' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey((prev) => !prev)}
|
||||
className='absolute right-2 top-1/2 -translate-y-1/2 bg-white px-2 py-1 cursor-pointer'
|
||||
>
|
||||
{showApiKey ? <Eye className='w-4 h-4 text-gray-500' /> : <EyeOff className='w-4 h-4 text-gray-500' />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{llmConfig.LLM === 'custom' && (
|
||||
<input
|
||||
type="text"
|
||||
value={llmConfig.CUSTOM_LLM_URL}
|
||||
onChange={(e) => setLlmConfig(prev => ({
|
||||
...prev,
|
||||
CUSTOM_LLM_URL: e.target.value
|
||||
}))}
|
||||
className="w-full mt-2 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="OpenAI-compatible URL"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{llmConfig.LLM !== 'ollama' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
|
||||
|
||||
<button
|
||||
onClick={fetchAvailableModels}
|
||||
disabled={
|
||||
modelsLoading ||
|
||||
(llmConfig.LLM === 'openai' && !currentApiKey) ||
|
||||
(llmConfig.LLM === 'google' && !currentApiKey) ||
|
||||
(llmConfig.LLM === 'anthropic' && !currentApiKey) ||
|
||||
(llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL)
|
||||
}
|
||||
className={`mt-4 py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
|
||||
? " border-gray-300 cursor-not-allowed text-gray-500"
|
||||
: " border-[#EDEEEF] text-[#101323] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
|
||||
}`}
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Checking for models...
|
||||
</span>
|
||||
) : (
|
||||
"Check models"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='flex items-start gap-4 mt-4'>
|
||||
<p className='text-sm font-medium text-gray-700 mb-2 w-full'></p>
|
||||
|
||||
{/* Model Selection - only show if models are available */}
|
||||
{modelsChecked && availableModels.length > 0 && (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{llmConfig.LLM === 'ollama' ? 'Choose a supported model' : `Select ${LLM_PROVIDERS[llmConfig.LLM!]?.label} 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"
|
||||
>
|
||||
<span className="text-sm truncate font-medium text-gray-900">
|
||||
{
|
||||
currentModel
|
||||
? availableModels.find(model => model === currentModel) || currentModel
|
||||
:
|
||||
"Select a model"
|
||||
}
|
||||
</span>
|
||||
|
||||
<ChevronUp 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) => {
|
||||
if (currentModelField) {
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
[currentModelField]: value
|
||||
}));
|
||||
}
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
currentModel === 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Image Provider */}
|
||||
<div className='p-3 border border-[#EDEEEF] rounded-[11px] mt-5'>
|
||||
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center'>
|
||||
<div className='flex justify-end items-center'>
|
||||
<Switch
|
||||
checked={!llmConfig.DISABLE_IMAGE_GENERATION}
|
||||
className=''
|
||||
onCheckedChange={(checked) => setLlmConfig(prev => ({
|
||||
...prev,
|
||||
DISABLE_IMAGE_GENERATION: !checked
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</ToolTip>
|
||||
<div className=" mb-7 flex items-center gap-6">
|
||||
<div className='w-[60px] h-[60px] px-[13.5px] py-[14.2px] rounded-[4px] flex items-center justify-center'
|
||||
style={{ backgroundColor: '#F4F3FF' }}
|
||||
>
|
||||
<img src="/image-markup.svg" className='w-full h-full object-cover' alt='image-markup' />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<h3 className="text-xl font-normal text-[#191919] ">Image Generation Settings</h3>
|
||||
<p className=" text-sm text-gray-500">
|
||||
Choosing where images come from
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{!llmConfig.DISABLE_IMAGE_GENERATION && (
|
||||
<div className='flex gap-4'>
|
||||
{/* Image Provider Selection */}
|
||||
<div className="w-full">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
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 capitalize text-gray-900">
|
||||
{llmConfig.IMAGE_PROVIDER
|
||||
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
|
||||
?.label || llmConfig.IMAGE_PROVIDER
|
||||
: 'Select Image Provider'}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronUp className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0 w-full"
|
||||
align="start"
|
||||
|
||||
>
|
||||
<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) => {
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
IMAGE_PROVIDER: value
|
||||
}));
|
||||
setOpenImageProviderSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
llmConfig.IMAGE_PROVIDER === provider.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{provider.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{provider.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Dynamic API Key Input for Image Provider */}
|
||||
{llmConfig.IMAGE_PROVIDER &&
|
||||
IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
|
||||
(() => {
|
||||
const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
|
||||
|
||||
|
||||
|
||||
// Show ComfyUI configuration
|
||||
if (provider.value === "comfyui") {
|
||||
return (
|
||||
<div className=" space-y-4 w-full">
|
||||
<div className=''>
|
||||
<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) => {
|
||||
setLlmConfig(prev => ({
|
||||
...prev,
|
||||
COMFYUI_URL: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show API key input for other providers
|
||||
return (
|
||||
<div className="w-full ">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
{provider.apiKeyFieldLabel}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
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={getFieldValue(provider.apiKeyField)}
|
||||
onChange={(e) => {
|
||||
setLlmConfig((prev) => ({
|
||||
...prev,
|
||||
[provider.apiKeyField as keyof LLMConfig]: e.target.value
|
||||
}))
|
||||
}
|
||||
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey((prev) => !prev)}
|
||||
className='absolute right-2 top-1/2 -translate-y-1/2 bg-white px-2 py-1 cursor-pointer'
|
||||
>
|
||||
{showApiKey ? <Eye className='w-4 h-4 text-gray-500' /> : <EyeOff className='w-4 h-4 text-gray-500' />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
</div>
|
||||
)}
|
||||
{!llmConfig.DISABLE_IMAGE_GENERATION && <div className='flex justify-end items-center mt-[18px]'>
|
||||
<div className='w-full flex items-center gap-4'>
|
||||
<p className='w-full'></p>
|
||||
{renderQualitySelector(llmConfig)}
|
||||
</div>
|
||||
{llmConfig.IMAGE_PROVIDER === "comfyui" && <div className='w-full'>
|
||||
<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={3}
|
||||
value={llmConfig.COMFYUI_WORKFLOW || ""}
|
||||
onChange={(e) => {
|
||||
setLlmConfig((prev) => ({
|
||||
...prev,
|
||||
COMFYUI_WORKFLOW: e.target.value
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div className='absolute bottom-16 mr-8 max-w-[1440px] right-0 flex justify-end items-center gap-2.5 '>
|
||||
<button
|
||||
disabled={currentStep === 1}
|
||||
onClick={() => {
|
||||
setStep(currentStep - 1);
|
||||
}}
|
||||
className='border border-[#EDEEEF] rounded-[53px] px-4 py-1 h-[36px]'>
|
||||
<ChevronLeft className='w-4 h-4 text-gray-500' />
|
||||
</button>
|
||||
<button
|
||||
|
||||
disabled={savingConfig || downloadProgress > 0}
|
||||
onClick={handleSaveConfig}
|
||||
className='border border-[#EDEEEF] bg-[#7C51F8] rounded-[58px] px-5 py-2.5 text-white text-xs font-semibold'>
|
||||
Continue to Finish
|
||||
</button>
|
||||
</div>
|
||||
{/* Download Progress Modal */}
|
||||
{showDownloadModal && downloadingModel && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl max-w-md w-full p-6 relative">
|
||||
{/* Modal Content */}
|
||||
<div className="text-center">
|
||||
{/* Icon */}
|
||||
<div className="mb-4">
|
||||
{downloadingModel.done ? (
|
||||
<CheckCircle className="w-12 h-12 text-green-600 mx-auto" />
|
||||
) : (
|
||||
<Download className="w-12 h-12 text-blue-600 mx-auto animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{downloadingModel.done ? "Download Complete!" : "Downloading Model"}
|
||||
</h3>
|
||||
|
||||
{/* Model Name */}
|
||||
<p className="text-sm text-gray-600 mb-6">
|
||||
{llmConfig.OLLAMA_MODEL}
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{downloadProgress > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="bg-blue-600 h-3 rounded-full transition-all duration-300 ease-out"
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{downloadProgress}% Complete
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status */}
|
||||
{downloadingModel.status && (
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-sm font-medium text-green-700 capitalize">
|
||||
{downloadingModel.status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Message */}
|
||||
{downloadingModel.status && downloadingModel.status !== "pulled" && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{downloadingModel.status === "downloading" && "Downloading model files..."}
|
||||
{downloadingModel.status === "verifying" && "Verifying model integrity..."}
|
||||
{downloadingModel.status === "pulling" && "Pulling model from registry..."}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Download Info */}
|
||||
{downloadingModel.downloaded && downloadingModel.size && (
|
||||
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex justify-between text-xs text-gray-600">
|
||||
<span>Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB</span>
|
||||
<span>Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PresentonMode
|
||||
BIN
servers/nextjs/public/image_mode.png
Normal file
BIN
servers/nextjs/public/image_mode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
|
|
@ -7,7 +7,11 @@ interface UserConfigState {
|
|||
}
|
||||
|
||||
const initialState: UserConfigState = {
|
||||
llm_config: {},
|
||||
llm_config: {
|
||||
LLM: "openai",
|
||||
IMAGE_PROVIDER: "gpt-image-1.5",
|
||||
|
||||
},
|
||||
can_change_keys: false,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue