presenton/servers/nextjs/app/settings/SettingPage.tsx

258 lines
8.8 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import Header from "../dashboard/components/Header";
import { Loader2, Download, CheckCircle } from "lucide-react";
import { toast } from "sonner";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
import {
checkIfSelectedOllamaModelIsPulled,
pullOllamaModel,
LLMConfig
} from "@/utils/providerUtils";
import { useRouter } from "next/navigation";
import LLMProviderSelection from "@/components/LLMSelection";
// Button state interface
interface ButtonState {
isLoading: boolean;
isDisabled: boolean;
text: string;
showProgress: boolean;
progressPercentage?: number;
status?: string;
}
const SettingsPage = () => {
const router = useRouter();
const userConfigState = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(userConfigState.llm_config);
const canChangeKeys = userConfigState.can_change_keys;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [buttonState, setButtonState] = useState<ButtonState>({
isLoading: false,
isDisabled: false,
text: "Save Configuration",
showProgress: false
});
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 downloadProgress = React.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 () => {
try {
setIsLoading(true);
setButtonState(prev => ({
...prev,
isLoading: true,
isDisabled: true,
text: "Saving Configuration..."
}));
await handleSaveLLMConfig(llmConfig);
if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) {
const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL);
if (!isPulled) {
setShowDownloadModal(true);
await handleModelDownload();
}
}
toast.info("Configuration saved successfully");
setIsLoading(false);
setButtonState(prev => ({
...prev,
isLoading: false,
isDisabled: false,
text: "Save Configuration"
}));
router.back();
} catch (error) {
console.error("Error:", error);
toast.info(
error instanceof Error
? error.message
: "Failed to save configuration"
);
setIsLoading(false);
setButtonState(prev => ({
...prev,
isLoading: false,
isDisabled: false,
text: "Save Configuration"
}));
}
};
const handleModelDownload = async () => {
try {
await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
} catch (error) {
console.error("Error downloading model:", error);
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("/dashboard");
}
}, [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">
<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>
</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">
<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>
{/* 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 SettingsPage;