353 lines
15 KiB
TypeScript
353 lines
15 KiB
TypeScript
"use client";
|
|
import { useState, useEffect, useMemo } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { Loader2, Download, CheckCircle } from "lucide-react";
|
|
import { useSelector } from "react-redux";
|
|
import { RootState } from "@/store/store";
|
|
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
|
|
import LLMProviderSelection from "./LLMSelection";
|
|
import {
|
|
checkIfSelectedOllamaModelIsPulled,
|
|
pullOllamaModel,
|
|
} from "@/utils/providerUtils";
|
|
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";
|
|
import FinalStep from "./OnBoarding/FinalStep";
|
|
|
|
// Button state interface
|
|
interface ButtonState {
|
|
isLoading: boolean;
|
|
isDisabled: boolean;
|
|
text: string;
|
|
showProgress: boolean;
|
|
progressPercentage?: number;
|
|
status?: string;
|
|
}
|
|
|
|
const FINAL_STEP_CONFETTI_PIECES = [
|
|
// left: denser at top
|
|
{ side: "left", offset: 1, top: 3, width: 28, height: 10, color: "#F59E0B", rotate: 12 },
|
|
{ side: "left", offset: 7, top: 5, width: 18, height: 7, color: "#7C3AED", rotate: -10 },
|
|
{ side: "left", offset: 12, top: 7, width: 20, height: 7, color: "#14B8A6", rotate: 22 },
|
|
{ side: "left", offset: 3, top: 10, width: 22, height: 8, color: "#22C55E", rotate: -18 },
|
|
{ side: "left", offset: 9, top: 12, width: 24, height: 8, color: "#E11D48", rotate: 18 },
|
|
{ side: "left", offset: 14, top: 15, width: 18, height: 7, color: "#F43F5E", rotate: 23 },
|
|
{ side: "left", offset: 5, top: 18, width: 20, height: 7, color: "#0EA5E9", rotate: -12 },
|
|
{ side: "left", offset: 11, top: 21, width: 26, height: 9, color: "#2563EB", rotate: 20 },
|
|
{ side: "left", offset: 2, top: 24, width: 19, height: 7, color: "#14B8A6", rotate: -16 },
|
|
{ side: "left", offset: 8, top: 28, width: 21, height: 8, color: "#FB7185", rotate: 27 },
|
|
{ side: "left", offset: 13, top: 32, width: 20, height: 7, color: "#06B6D4", rotate: 16 },
|
|
{ side: "left", offset: 3, top: 36, width: 24, height: 9, color: "#EAB308", rotate: -22 },
|
|
{ side: "left", offset: 10, top: 41, width: 18, height: 7, color: "#A855F7", rotate: -14 },
|
|
{ side: "left", offset: 2, top: 50, width: 30, height: 10, color: "#EC4899", rotate: -28 },
|
|
{ side: "left", offset: 13, top: 58, width: 19, height: 7, color: "#22C55E", rotate: 17 },
|
|
{ side: "left", offset: 5, top: 66, width: 24, height: 8, color: "#8B5CF6", rotate: 14 },
|
|
{ side: "left", offset: 11, top: 74, width: 18, height: 7, color: "#3B82F6", rotate: 12 },
|
|
{ side: "left", offset: 4, top: 82, width: 20, height: 7, color: "#14B8A6", rotate: 21 },
|
|
{ side: "left", offset: 7, top: 90, width: 24, height: 8, color: "#D946EF", rotate: -26 },
|
|
|
|
// right: denser at top
|
|
{ side: "right", offset: 1, top: 4, width: 30, height: 10, color: "#F97316", rotate: -14 },
|
|
{ side: "right", offset: 8, top: 6, width: 19, height: 7, color: "#0EA5E9", rotate: 12 },
|
|
{ side: "right", offset: 13, top: 9, width: 20, height: 7, color: "#22C55E", rotate: -20 },
|
|
{ side: "right", offset: 4, top: 12, width: 24, height: 8, color: "#EC4899", rotate: 20 },
|
|
{ side: "right", offset: 10, top: 15, width: 22, height: 8, color: "#06B6D4", rotate: -18 },
|
|
{ side: "right", offset: 15, top: 18, width: 20, height: 7, color: "#22C55E", rotate: -25 },
|
|
{ side: "right", offset: 5, top: 21, width: 18, height: 7, color: "#8B5CF6", rotate: 19 },
|
|
{ side: "right", offset: 12, top: 24, width: 21, height: 8, color: "#F43F5E", rotate: 14 },
|
|
{ side: "right", offset: 2, top: 28, width: 26, height: 9, color: "#84CC16", rotate: 15 },
|
|
{ side: "right", offset: 9, top: 33, width: 21, height: 8, color: "#F97316", rotate: -11 },
|
|
{ side: "right", offset: 14, top: 38, width: 20, height: 7, color: "#A855F7", rotate: -19 },
|
|
{ side: "right", offset: 4, top: 44, width: 19, height: 7, color: "#F43F5E", rotate: 20 },
|
|
{ side: "right", offset: 2, top: 52, width: 28, height: 10, color: "#FACC15", rotate: 25 },
|
|
{ side: "right", offset: 12, top: 60, width: 18, height: 7, color: "#14B8A6", rotate: -15 },
|
|
{ side: "right", offset: 6, top: 68, width: 24, height: 8, color: "#22C55E", rotate: -17 },
|
|
{ side: "right", offset: 1, top: 76, width: 20, height: 7, color: "#A855F7", rotate: 14 },
|
|
{ side: "right", offset: 13, top: 84, width: 20, height: 7, color: "#3B82F6", rotate: -24 },
|
|
{ side: "right", offset: 5, top: 92, width: 26, height: 9, color: "#EAB308", rotate: 18 },
|
|
] as const;
|
|
|
|
const getTaperedSideOffset = (offset: number, top: number) => {
|
|
const taperMultiplier = Math.max(0.72, 1.85 - top * 0.012);
|
|
return Math.min(29, Number((offset * taperMultiplier).toFixed(2)));
|
|
};
|
|
|
|
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);
|
|
|
|
const [downloadingModel, setDownloadingModel] = useState<{
|
|
name: string;
|
|
size: number | null;
|
|
downloaded: number | null;
|
|
status: string;
|
|
done: boolean;
|
|
} | null>(null);
|
|
const [showDownloadModal, setShowDownloadModal] = useState<boolean>(false);
|
|
const [buttonState, setButtonState] = useState<ButtonState>({
|
|
isLoading: false,
|
|
isDisabled: false,
|
|
text: "Save Configuration",
|
|
showProgress: false
|
|
});
|
|
|
|
const canChangeKeys = config.can_change_keys;
|
|
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]);
|
|
|
|
const handleSaveConfig = async () => {
|
|
trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname });
|
|
try {
|
|
setButtonState(prev => ({
|
|
...prev,
|
|
isLoading: true,
|
|
isDisabled: true,
|
|
text: "Saving Configuration..."
|
|
}));
|
|
// 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");
|
|
setButtonState(prev => ({
|
|
...prev,
|
|
isLoading: false,
|
|
isDisabled: false,
|
|
text: "Save Configuration"
|
|
}));
|
|
// 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");
|
|
setButtonState(prev => ({
|
|
...prev,
|
|
isLoading: false,
|
|
isDisabled: false,
|
|
text: "Save Configuration"
|
|
}));
|
|
}
|
|
};
|
|
|
|
const handleModelDownload = async () => {
|
|
try {
|
|
await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
|
|
}
|
|
finally {
|
|
setDownloadingModel(null);
|
|
setShowDownloadModal(false);
|
|
}
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
if (downloadingModel && downloadingModel.downloaded !== null && downloadingModel.size !== null) {
|
|
const percentage = Math.round(((downloadingModel.downloaded / downloadingModel.size) * 100));
|
|
setButtonState({
|
|
isLoading: true,
|
|
isDisabled: true,
|
|
text: `Downloading Model (${percentage}%)`,
|
|
showProgress: true,
|
|
progressPercentage: percentage,
|
|
status: downloadingModel.status
|
|
});
|
|
}
|
|
|
|
if (downloadingModel && downloadingModel.done) {
|
|
setTimeout(() => {
|
|
setShowDownloadModal(false);
|
|
setDownloadingModel(null);
|
|
toast.info("Model downloaded successfully!");
|
|
}, 2000);
|
|
}
|
|
}, [downloadingModel]);
|
|
|
|
useEffect(() => {
|
|
if (!canChangeKeys) {
|
|
router.push("/upload");
|
|
}
|
|
}, [canChangeKeys, router]);
|
|
|
|
if (!canChangeKeys) {
|
|
return null;
|
|
}
|
|
|
|
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>
|
|
|
|
// {/* 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 z-10">
|
|
{step === 3 && (
|
|
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden>
|
|
{FINAL_STEP_CONFETTI_PIECES.map((piece, index) => (
|
|
<span
|
|
key={`${piece.side}-${index}`}
|
|
className="absolute rounded-[3px]"
|
|
style={{
|
|
top: `${piece.top}%`,
|
|
...(piece.side === "left"
|
|
? { left: `${getTaperedSideOffset(piece.offset, piece.top)}%` }
|
|
: { right: `${getTaperedSideOffset(piece.offset, piece.top)}%` }),
|
|
width: `${piece.width}px`,
|
|
height: `${piece.height}px`,
|
|
backgroundColor: piece.color,
|
|
transform: `rotate(${piece.rotate}deg)`,
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
<OnBoardingHeader currentStep={step} />
|
|
{step === 1 && <ModeSelectStep setStep={setStep} setSelectedMode={setSelectedMode} />}
|
|
{step === 2 && selectedMode === "presenton" && <PresentonMode currentStep={step} setStep={setStep} />}
|
|
{step === 2 && selectedMode === "image" && <GenerationWithImage />}
|
|
{step === 3 && <FinalStep />}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|