fix: sonner and proper error message
This commit is contained in:
parent
929f5b4883
commit
3e469cb945
9 changed files with 430 additions and 182 deletions
|
|
@ -60,7 +60,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
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")}>
|
||||
<Select value={llmConfig.DALL_E_3_QUALITY || 'standard'} 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>
|
||||
|
|
@ -84,7 +84,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
|
|||
</label>
|
||||
<div className="">
|
||||
<Select
|
||||
value={llmConfig.GPT_IMAGE_1_5_QUALITY}
|
||||
value={llmConfig.GPT_IMAGE_1_5_QUALITY || 'low'}
|
||||
onValueChange={(value) => input_field_changed(value, "GPT_IMAGE_1_5_QUALITY")}
|
||||
>
|
||||
<SelectTrigger
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Loader2, Download, CheckCircle, ChevronRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { notify } from "@/components/ui/sonner";
|
||||
import { RootState } from "@/store/store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
|
||||
import {
|
||||
getLLMConfigValidationError,
|
||||
handleSaveLLMConfig,
|
||||
} from "@/utils/storeHelpers";
|
||||
import {
|
||||
checkIfSelectedOllamaModelIsPulled,
|
||||
pullOllamaModel,
|
||||
|
|
@ -52,6 +56,7 @@ const SettingsPage = () => {
|
|||
done: boolean;
|
||||
} | null>(null);
|
||||
const [showDownloadModal, setShowDownloadModal] = useState<boolean>(false);
|
||||
const downloadAbortRef = React.useRef<AbortController | null>(null);
|
||||
|
||||
const downloadProgress = React.useMemo(() => {
|
||||
if (
|
||||
|
|
@ -68,6 +73,11 @@ const SettingsPage = () => {
|
|||
|
||||
const handleSaveConfig = async () => {
|
||||
trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname });
|
||||
const validationError = getLLMConfigValidationError(llmConfig);
|
||||
if (validationError) {
|
||||
notify.error("Cannot save settings", validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
|
|
@ -84,11 +94,24 @@ const SettingsPage = () => {
|
|||
);
|
||||
if (!isPulled) {
|
||||
setShowDownloadModal(true);
|
||||
setDownloadingModel({
|
||||
name: llmConfig.OLLAMA_MODEL || "",
|
||||
size: null,
|
||||
downloaded: null,
|
||||
status: "pulling",
|
||||
done: false,
|
||||
});
|
||||
trackEvent(MixpanelEvent.Settings_DownloadOllamaModel_API_Call);
|
||||
await handleModelDownload();
|
||||
const downloadOutcome = await handleModelDownload();
|
||||
if (downloadOutcome === "cancelled") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
toast.info("Configuration saved successfully");
|
||||
notify.info(
|
||||
"Settings saved",
|
||||
"Your configuration was saved successfully."
|
||||
);
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
|
|
@ -98,7 +121,11 @@ const SettingsPage = () => {
|
|||
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
|
||||
router.push("/upload");
|
||||
} catch (error) {
|
||||
toast.info(error instanceof Error ? error.message : "Failed to save configuration");
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Something went wrong while saving.";
|
||||
notify.error("Could not save settings", message);
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
|
|
@ -108,13 +135,38 @@ const SettingsPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleModelDownload = async () => {
|
||||
const handleModelDownload = async (): Promise<"completed" | "cancelled"> => {
|
||||
const ac = new AbortController();
|
||||
downloadAbortRef.current = ac;
|
||||
try {
|
||||
await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
|
||||
}
|
||||
finally {
|
||||
await pullOllamaModel(
|
||||
llmConfig.OLLAMA_MODEL!,
|
||||
setDownloadingModel,
|
||||
ac.signal
|
||||
);
|
||||
return "completed";
|
||||
} catch (e) {
|
||||
const aborted = e instanceof Error && e.name === "AbortError";
|
||||
if (aborted) {
|
||||
setDownloadingModel(null);
|
||||
setShowDownloadModal(false);
|
||||
setButtonState({
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
text: "Save Configuration",
|
||||
showProgress: false,
|
||||
});
|
||||
notify.info(
|
||||
"Download cancelled",
|
||||
"The Ollama model download was stopped. Your settings are already saved—you can save again to retry the download."
|
||||
);
|
||||
return "cancelled";
|
||||
}
|
||||
setDownloadingModel(null);
|
||||
setShowDownloadModal(false);
|
||||
throw e;
|
||||
} finally {
|
||||
downloadAbortRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -141,7 +193,10 @@ const SettingsPage = () => {
|
|||
setTimeout(() => {
|
||||
setShowDownloadModal(false);
|
||||
setDownloadingModel(null);
|
||||
toast.info("Model downloaded successfully!");
|
||||
notify.success(
|
||||
"Model ready",
|
||||
"The Ollama model finished downloading successfully."
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
}, [downloadingModel]);
|
||||
|
|
@ -336,6 +391,19 @@ const SettingsPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!downloadingModel.done && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-lg border-gray-300 text-gray-800 hover:bg-gray-50"
|
||||
onClick={() => downloadAbortRef.current?.abort()}
|
||||
>
|
||||
Cancel download
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getApiUrl } from '@/utils/api';
|
|||
import { LLM_PROVIDERS } from '@/utils/providerConstants';
|
||||
import { Check, Loader2, Eye, EyeOff, ChevronUp } from 'lucide-react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner';
|
||||
import { notify } from '@/components/ui/sonner';
|
||||
|
||||
|
||||
interface OpenAIConfigProps {
|
||||
|
|
@ -190,11 +190,17 @@ const TextProvider = ({
|
|||
console.error('Failed to fetch models');
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
toast.error(`Failed to fetch ${modelLabel} models`);
|
||||
notify.error(
|
||||
'Could not load models',
|
||||
`The server could not list ${modelLabel} models. Check your API key or endpoint and try again.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching models:', error);
|
||||
toast.error('Error fetching models');
|
||||
notify.error(
|
||||
'Could not load models',
|
||||
'Something went wrong while contacting the provider. Check your network and try again.'
|
||||
);
|
||||
setAvailableModels([]);
|
||||
setModelsChecked(true);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Loader2, Download, CheckCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { notify } from "@/components/ui/sonner";
|
||||
import { RootState } from "@/store/store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
|
||||
import {
|
||||
getLLMConfigValidationError,
|
||||
handleSaveLLMConfig,
|
||||
} from "@/utils/storeHelpers";
|
||||
import {
|
||||
checkIfSelectedOllamaModelIsPulled,
|
||||
pullOllamaModel,
|
||||
|
|
@ -48,6 +52,7 @@ const SettingsPage = () => {
|
|||
done: boolean;
|
||||
} | null>(null);
|
||||
const [showDownloadModal, setShowDownloadModal] = useState<boolean>(false);
|
||||
const downloadAbortRef = React.useRef<AbortController | null>(null);
|
||||
|
||||
const downloadProgress = React.useMemo(() => {
|
||||
if (
|
||||
|
|
@ -64,6 +69,11 @@ const SettingsPage = () => {
|
|||
|
||||
const handleSaveConfig = async () => {
|
||||
trackEvent(MixpanelEvent.Settings_SaveConfiguration_Button_Clicked, { pathname });
|
||||
const validationError = getLLMConfigValidationError(llmConfig);
|
||||
if (validationError) {
|
||||
notify.error("Cannot save settings", validationError);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
|
|
@ -80,11 +90,24 @@ const SettingsPage = () => {
|
|||
);
|
||||
if (!isPulled) {
|
||||
setShowDownloadModal(true);
|
||||
setDownloadingModel({
|
||||
name: llmConfig.OLLAMA_MODEL || "",
|
||||
size: null,
|
||||
downloaded: null,
|
||||
status: "pulling",
|
||||
done: false,
|
||||
});
|
||||
trackEvent(MixpanelEvent.Settings_DownloadOllamaModel_API_Call);
|
||||
await handleModelDownload();
|
||||
const downloadOutcome = await handleModelDownload();
|
||||
if (downloadOutcome === "cancelled") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
toast.info("Configuration saved successfully");
|
||||
notify.info(
|
||||
"Settings saved",
|
||||
"Your configuration was saved successfully."
|
||||
);
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
|
|
@ -94,7 +117,11 @@ const SettingsPage = () => {
|
|||
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
|
||||
router.push("/upload");
|
||||
} catch (error) {
|
||||
toast.info(error instanceof Error ? error.message : "Failed to save configuration");
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Something went wrong while saving.";
|
||||
notify.error("Could not save settings", message);
|
||||
setButtonState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
|
|
@ -104,13 +131,38 @@ const SettingsPage = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleModelDownload = async () => {
|
||||
const handleModelDownload = async (): Promise<"completed" | "cancelled"> => {
|
||||
const ac = new AbortController();
|
||||
downloadAbortRef.current = ac;
|
||||
try {
|
||||
await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
|
||||
}
|
||||
finally {
|
||||
await pullOllamaModel(
|
||||
llmConfig.OLLAMA_MODEL!,
|
||||
setDownloadingModel,
|
||||
ac.signal
|
||||
);
|
||||
return "completed";
|
||||
} catch (e) {
|
||||
const aborted = e instanceof Error && e.name === "AbortError";
|
||||
if (aborted) {
|
||||
setDownloadingModel(null);
|
||||
setShowDownloadModal(false);
|
||||
setButtonState({
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
text: "Save Configuration",
|
||||
showProgress: false,
|
||||
});
|
||||
notify.info(
|
||||
"Download cancelled",
|
||||
"The Ollama model download was stopped. Your settings are already saved—you can save again to retry the download."
|
||||
);
|
||||
return "cancelled";
|
||||
}
|
||||
setDownloadingModel(null);
|
||||
setShowDownloadModal(false);
|
||||
throw e;
|
||||
} finally {
|
||||
downloadAbortRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -137,7 +189,10 @@ const SettingsPage = () => {
|
|||
setTimeout(() => {
|
||||
setShowDownloadModal(false);
|
||||
setDownloadingModel(null);
|
||||
toast.info("Model downloaded successfully!");
|
||||
notify.success(
|
||||
"Model ready",
|
||||
"The Ollama model finished downloading successfully."
|
||||
);
|
||||
}, 2000);
|
||||
}
|
||||
}, [downloadingModel]);
|
||||
|
|
@ -272,6 +327,19 @@ const SettingsPage = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!downloadingModel.done && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="rounded-lg border-gray-300 text-gray-800 hover:bg-gray-50"
|
||||
onClick={() => downloadAbortRef.current?.abort()}
|
||||
>
|
||||
Cancel download
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ export default function Home() {
|
|||
<img src="/right-confetti.png" alt="presenton" className='w-full h-full object-contain' />
|
||||
</div>
|
||||
)}
|
||||
<OnBoardingHeader currentStep={step} />
|
||||
<OnBoardingHeader currentStep={step} setStep={setStep} />
|
||||
{step === 1 && <ModeSelectStep selectedMode={selectedMode} setStep={setStep} setSelectedMode={setSelectedMode} />}
|
||||
{step === 2 && selectedMode === "presenton" && <PresentonMode currentStep={step} setStep={setStep} />}
|
||||
{step === 2 && selectedMode === "image" && <GenerationWithImage />}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import React from 'react'
|
||||
|
||||
|
||||
const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
|
||||
const OnBoardingHeader = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => {
|
||||
return (
|
||||
<div className='relative z-20 flex items-center font-syne 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`}>
|
||||
<div className='flex items-center gap-1 cursor-pointer'
|
||||
onClick={() => {
|
||||
if (currentStep > 1) {
|
||||
setStep(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>
|
||||
|
|
@ -14,8 +20,14 @@ const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
|
|||
<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`}>
|
||||
<div className='flex items-center gap-1 cursor-pointer'
|
||||
onClick={() => {
|
||||
if (currentStep > 2) {
|
||||
setStep(2);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
|
@ -24,7 +36,7 @@ const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
|
|||
<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`}>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,17 @@
|
|||
|
||||
import type React from "react"
|
||||
import { BadgeCheck, Loader2, ShieldAlert } from "lucide-react"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
import { Toaster as Sonner, toast as sonnerToast } from "sonner"
|
||||
|
||||
/** Toasts with both title and description (matches styled [data-title] / [data-description]). */
|
||||
export const notify = {
|
||||
error: (title: string, description: string) =>
|
||||
sonnerToast.error(title, { description }),
|
||||
success: (title: string, description: string) =>
|
||||
sonnerToast.success(title, { description }),
|
||||
info: (title: string, description: string) =>
|
||||
sonnerToast.info(title, { description }),
|
||||
} as const
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
|
|
@ -19,29 +29,42 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
/* Constrain toast width similar to the design mock */
|
||||
|
||||
/* Neutral "card" toast container */
|
||||
[data-sonner-toast][data-styled="true"] {
|
||||
border-radius: 10px !important;
|
||||
|
||||
border: 1px solid #E1E1E5 !important;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.06) !important;
|
||||
padding: 8px !important;
|
||||
gap: 10px !important;
|
||||
backdrop-filter: blur(6px) !important;
|
||||
background: white !important;
|
||||
/* Near Sonner default width on desktop; nearly full width on narrow screens */
|
||||
[data-sonner-toaster] {
|
||||
--width: min(100dvw - 1.5rem, 22.5rem) !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
@media (min-width: 640px) {
|
||||
[data-sonner-toaster] {
|
||||
--width: min(100dvw - 2rem, 24rem) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Neutral "card" toast container — design tokens */
|
||||
[data-sonner-toast][data-styled="true"] {
|
||||
border-radius: 10px !important;
|
||||
|
||||
border: 1px solid var(--Base-Gray-700, #e1e1e5) !important;
|
||||
background: rgba(255, 255, 255, 0.6) !important;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.06) !important;
|
||||
padding: clamp(9px, 0.5rem + 0.35vw, 12px) clamp(11px, 0.65rem + 0.5vw, 14px) !important;
|
||||
gap: clamp(8px, 0.5rem + 0.35vw, 11px) !important;
|
||||
backdrop-filter: blur(6px) !important;
|
||||
-webkit-backdrop-filter: blur(6px) !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
/* Typography — slight scale-up from original 12px, capped modestly */
|
||||
[data-sonner-toast][data-styled="true"] [data-title] {
|
||||
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
"Noto Sans", sans-serif !important;
|
||||
font-size: 12px !important;
|
||||
font-size: clamp(0.8125rem, 0.8rem + 0.12vw, 0.9375rem) !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 14px !important;
|
||||
letter-spacing: 0.04em !important;
|
||||
line-height: 1.35 !important;
|
||||
letter-spacing: 0.03em !important;
|
||||
color: rgb(15 23 42) !important; /* slate-900 */
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
|
@ -50,23 +73,24 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
"Noto Sans", sans-serif !important;
|
||||
font-size: 10px !important;
|
||||
font-size: clamp(0.6875rem, 0.67rem + 0.1vw, 0.8125rem) !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 10px !important; /* 100% */
|
||||
line-height: 1.4 !important;
|
||||
letter-spacing: 0.03em !important;
|
||||
color: rgb(100 116 139) !important; /* slate-500 */
|
||||
}
|
||||
|
||||
[data-sonner-toast][data-styled="true"] [data-content] {
|
||||
gap: 2px !important;
|
||||
gap: clamp(2px, 0.15vw, 5px) !important;
|
||||
flex: 1 1 auto !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Left icon badge */
|
||||
[data-sonner-toast][data-styled="true"] [data-icon] {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
width: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
|
||||
height: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
|
||||
flex-shrink: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
|
|
@ -75,8 +99,8 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
}
|
||||
|
||||
[data-sonner-toast][data-styled="true"] [data-icon] svg {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
width: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
|
||||
height: clamp(20px, 1.15rem + 0.35vw, 22px) !important;
|
||||
}
|
||||
|
||||
/* Per-type icon colors */
|
||||
|
|
@ -103,12 +127,13 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
/* Outline buttons like the mock ("Got it!") */
|
||||
[data-sonner-toast][data-styled="true"] [data-button] {
|
||||
height: auto !important;
|
||||
padding: 4px 8px !important;
|
||||
padding: clamp(4px, 0.3rem + 0.2vw, 7px)
|
||||
clamp(7px, 0.5rem + 0.25vw, 10px) !important;
|
||||
border-radius: 6px !important;
|
||||
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
"Noto Sans", sans-serif !important;
|
||||
font-size: 10px !important;
|
||||
font-size: clamp(0.625rem, 0.62rem + 0.08vw, 0.75rem) !important;
|
||||
font-weight: 400 !important;
|
||||
background: rgb(255 255 255) !important;
|
||||
color: #3F3F3F !important;
|
||||
|
|
@ -127,21 +152,22 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
white-space: nowrap !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
padding: 4px 8px !important;
|
||||
padding: clamp(4px, 0.3rem + 0.2vw, 7px)
|
||||
clamp(7px, 0.5rem + 0.25vw, 10px) !important;
|
||||
border-radius: 6px !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: 0 !important;
|
||||
align-self: center !important;
|
||||
background: rgb(255 255 255) !important;
|
||||
color: #3F3F3F !important;
|
||||
border: 1px solid #EDEEEF !important;
|
||||
color: #3f3f3f !important;
|
||||
border: 1px solid #edeeef !important;
|
||||
box-shadow: none !important;
|
||||
font-family: var(--font-syne), ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
||||
"Noto Sans", sans-serif !important;
|
||||
font-size: 10px !important;
|
||||
font-size: clamp(0.625rem, 0.62rem + 0.08vw, 0.75rem) !important;
|
||||
font-weight: 400 !important;
|
||||
line-height: 14px !important;
|
||||
line-height: 1.3 !important;
|
||||
letter-spacing: 0.02em !important;
|
||||
}
|
||||
|
||||
|
|
@ -153,11 +179,14 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
background: rgb(248 250 252) !important; /* slate-50 */
|
||||
}
|
||||
|
||||
/* Dark mode */
|
||||
/* Dark mode — same radius, border weight, shadow; frosted dark surface */
|
||||
.dark [data-sonner-toast][data-styled="true"] {
|
||||
background: rgb(2 6 23) !important; /* slate-950 */
|
||||
border: 1px solid rgba(148, 163, 184, 0.22) !important; /* slate-400 @ 22% */
|
||||
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.45) !important;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.22) !important;
|
||||
background: rgba(2, 6, 23, 0.6) !important;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.06) !important;
|
||||
backdrop-filter: blur(6px) !important;
|
||||
-webkit-backdrop-filter: blur(6px) !important;
|
||||
}
|
||||
|
||||
.dark [data-sonner-toast][data-styled="true"] [data-title] {
|
||||
|
|
@ -189,11 +218,8 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
}
|
||||
`}</style>
|
||||
<Sonner
|
||||
style={{
|
||||
zIndex: 999999999,
|
||||
background: 'red',
|
||||
}}
|
||||
className="toaster group z-50 bg-white"
|
||||
style={{ zIndex: 999999999 }}
|
||||
className="toaster group z-50 bg-transparent"
|
||||
icons={{ ...defaultIcons, ...(icons ?? {}) }}
|
||||
toastOptions={{
|
||||
closeButtonAriaLabel: "Dismiss notification",
|
||||
|
|
|
|||
|
|
@ -110,47 +110,97 @@ export const resetDownloadingModel = (): DownloadingModel => ({
|
|||
done: false,
|
||||
});
|
||||
|
||||
function abortPullError(): Error {
|
||||
const err = new Error("Download cancelled");
|
||||
err.name = "AbortError";
|
||||
return err;
|
||||
}
|
||||
|
||||
function isAbortError(e: unknown): boolean {
|
||||
return e instanceof Error && e.name === "AbortError";
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls Ollama model with progress tracking
|
||||
* Returns a promise that resolves with the final downloading model state
|
||||
* Pulls Ollama model with progress tracking.
|
||||
* Pass an AbortSignal to stop polling (e.g. user cancels download).
|
||||
*/
|
||||
export const pullOllamaModel = async (
|
||||
model: string,
|
||||
onProgress?: (model: DownloadingModel) => void
|
||||
onProgress?: (model: DownloadingModel) => void,
|
||||
signal?: AbortSignal
|
||||
): Promise<DownloadingModel> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (interval !== null) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
};
|
||||
|
||||
const onAbort = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
onProgress?.(resetDownloadingModel());
|
||||
reject(abortPullError());
|
||||
};
|
||||
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", onAbort);
|
||||
|
||||
interval = setInterval(async () => {
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
getApiUrl(`/api/v1/ppt/ollama/model/pull?model=${model}`)
|
||||
);
|
||||
if (settled) return;
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
if (data.done && data.status !== "error") {
|
||||
clearInterval(interval);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
onProgress?.(data);
|
||||
resolve(data);
|
||||
} else if (data.status === "error") {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
onProgress?.(resetDownloadingModel());
|
||||
reject(new Error("Error occurred while pulling model"));
|
||||
} else {
|
||||
onProgress?.(data);
|
||||
}
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
onProgress?.(resetDownloadingModel());
|
||||
if (response.status === 403) {
|
||||
reject(new Error("Request to Ollama Not Authorized"));
|
||||
} else {
|
||||
reject(new Error("Error occurred while pulling model"));
|
||||
}
|
||||
reject(new Error("Error occurred while pulling model"));
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(interval);
|
||||
const resetData = resetDownloadingModel();
|
||||
onProgress?.(resetData);
|
||||
if (settled) return;
|
||||
if (isAbortError(error)) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
onProgress?.(resetDownloadingModel());
|
||||
reject(error);
|
||||
}
|
||||
}, 1000);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,118 @@ import { setLLMConfig } from "@/store/slices/userConfig";
|
|||
import { store } from "@/store/store";
|
||||
import { LLMConfig } from "@/types/llm_config";
|
||||
|
||||
function isProvided(value: unknown): boolean {
|
||||
return value !== "" && value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user-facing validation message, or null when the config is valid.
|
||||
*/
|
||||
export const getLLMConfigValidationError = (
|
||||
llmConfig: LLMConfig
|
||||
): string | null => {
|
||||
if (!llmConfig.LLM) {
|
||||
return "Select a text provider.";
|
||||
}
|
||||
|
||||
if (!llmConfig.DISABLE_IMAGE_GENERATION && !llmConfig.IMAGE_PROVIDER) {
|
||||
return "Select an image provider, or turn off image generation.";
|
||||
}
|
||||
|
||||
const llm = llmConfig.LLM;
|
||||
|
||||
if (llm === "openai") {
|
||||
if (!isProvided(llmConfig.OPENAI_API_KEY)) {
|
||||
return "OpenAI API key is required.";
|
||||
}
|
||||
if (!isProvided(llmConfig.OPENAI_MODEL)) {
|
||||
return 'No OpenAI model selected. Use "Check models" after entering your API key, then choose a model.';
|
||||
}
|
||||
} else if (llm === "google") {
|
||||
if (!isProvided(llmConfig.GOOGLE_API_KEY)) {
|
||||
return "Google API key is required.";
|
||||
}
|
||||
if (!isProvided(llmConfig.GOOGLE_MODEL)) {
|
||||
return 'No Google model selected. Use "Check models" after entering your API key, then choose a model.';
|
||||
}
|
||||
} else if (llm === "anthropic") {
|
||||
if (!isProvided(llmConfig.ANTHROPIC_API_KEY)) {
|
||||
return "Anthropic API key is required.";
|
||||
}
|
||||
if (!isProvided(llmConfig.ANTHROPIC_MODEL)) {
|
||||
return 'No Anthropic model selected. Use "Check models" after entering your API key, then choose a model.';
|
||||
}
|
||||
} else if (llm === "ollama") {
|
||||
if (!isProvided(llmConfig.OLLAMA_URL)) {
|
||||
return "Ollama server URL is required.";
|
||||
}
|
||||
if (!isProvided(llmConfig.OLLAMA_MODEL)) {
|
||||
return "Select an Ollama model. If none appear, confirm Ollama is running and reachable.";
|
||||
}
|
||||
} else if (llm === "custom") {
|
||||
if (!isProvided(llmConfig.CUSTOM_LLM_URL)) {
|
||||
return "Enter your custom LLM endpoint URL (OpenAI-compatible).";
|
||||
}
|
||||
if (!isProvided(llmConfig.CUSTOM_MODEL)) {
|
||||
return 'No model selected for your custom endpoint. Use "Check models" after entering the URL, then choose a model.';
|
||||
}
|
||||
} else if (llm === "codex") {
|
||||
if (!isProvided(llmConfig.CODEX_MODEL)) {
|
||||
return "Select a Codex model.";
|
||||
}
|
||||
} else {
|
||||
return "Unsupported or unknown text provider.";
|
||||
}
|
||||
|
||||
if (!llmConfig.DISABLE_IMAGE_GENERATION) {
|
||||
switch (llmConfig.IMAGE_PROVIDER) {
|
||||
case "pexels":
|
||||
if (!isProvided(llmConfig.PEXELS_API_KEY)) {
|
||||
return "Pexels API key is required.";
|
||||
}
|
||||
break;
|
||||
case "pixabay":
|
||||
if (!isProvided(llmConfig.PIXABAY_API_KEY)) {
|
||||
return "Pixabay API key is required.";
|
||||
}
|
||||
break;
|
||||
case "dall-e-3":
|
||||
if (!isProvided(llmConfig.OPENAI_API_KEY)) {
|
||||
return "OpenAI API key is required for DALL·E 3.";
|
||||
}
|
||||
break;
|
||||
case "gpt-image-1.5":
|
||||
if (!isProvided(llmConfig.OPENAI_API_KEY)) {
|
||||
return "OpenAI API key is required for GPT Image 1.5.";
|
||||
}
|
||||
break;
|
||||
case "gemini_flash":
|
||||
if (!isProvided(llmConfig.GOOGLE_API_KEY)) {
|
||||
return "Google API key is required for Gemini Flash image generation.";
|
||||
}
|
||||
break;
|
||||
case "nanobanana_pro":
|
||||
if (!isProvided(llmConfig.GOOGLE_API_KEY)) {
|
||||
return "Google API key is required for NanoBanana Pro.";
|
||||
}
|
||||
break;
|
||||
case "comfyui":
|
||||
if (!isProvided(llmConfig.COMFYUI_URL)) {
|
||||
return "ComfyUI server URL is required.";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return "Select a valid image provider.";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
|
||||
if (!hasValidLLMConfig(llmConfig)) {
|
||||
throw new Error("Provided configuration is not valid");
|
||||
const validationError = getLLMConfigValidationError(llmConfig);
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
|
||||
// Check if running in Electron environment
|
||||
|
|
@ -22,96 +131,5 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
|
|||
store.dispatch(setLLMConfig(llmConfig));
|
||||
};
|
||||
|
||||
export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
|
||||
if (!llmConfig.LLM) return false;
|
||||
if (!llmConfig.DISABLE_IMAGE_GENERATION && !llmConfig.IMAGE_PROVIDER)
|
||||
return false;
|
||||
|
||||
const isOpenAIConfigValid =
|
||||
llmConfig.OPENAI_MODEL !== "" &&
|
||||
llmConfig.OPENAI_MODEL !== null &&
|
||||
llmConfig.OPENAI_MODEL !== undefined &&
|
||||
llmConfig.OPENAI_API_KEY !== "" &&
|
||||
llmConfig.OPENAI_API_KEY !== null &&
|
||||
llmConfig.OPENAI_API_KEY !== undefined;
|
||||
|
||||
const isGoogleConfigValid =
|
||||
llmConfig.GOOGLE_MODEL !== "" &&
|
||||
llmConfig.GOOGLE_MODEL !== null &&
|
||||
llmConfig.GOOGLE_MODEL !== undefined &&
|
||||
llmConfig.GOOGLE_API_KEY !== "" &&
|
||||
llmConfig.GOOGLE_API_KEY !== null &&
|
||||
llmConfig.GOOGLE_API_KEY !== undefined;
|
||||
|
||||
const isAnthropicConfigValid =
|
||||
llmConfig.ANTHROPIC_MODEL !== "" &&
|
||||
llmConfig.ANTHROPIC_MODEL !== null &&
|
||||
llmConfig.ANTHROPIC_MODEL !== undefined &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== "" &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== null &&
|
||||
llmConfig.ANTHROPIC_API_KEY !== undefined;
|
||||
|
||||
const isOllamaConfigValid =
|
||||
llmConfig.OLLAMA_MODEL !== "" &&
|
||||
llmConfig.OLLAMA_MODEL !== null &&
|
||||
llmConfig.OLLAMA_MODEL !== undefined &&
|
||||
llmConfig.OLLAMA_URL !== "" &&
|
||||
llmConfig.OLLAMA_URL !== null &&
|
||||
llmConfig.OLLAMA_URL !== undefined;
|
||||
|
||||
const isCustomConfigValid =
|
||||
llmConfig.CUSTOM_LLM_URL !== "" &&
|
||||
llmConfig.CUSTOM_LLM_URL !== null &&
|
||||
llmConfig.CUSTOM_LLM_URL !== undefined &&
|
||||
llmConfig.CUSTOM_MODEL !== "" &&
|
||||
llmConfig.CUSTOM_MODEL !== null &&
|
||||
llmConfig.CUSTOM_MODEL !== undefined;
|
||||
|
||||
const isCodexConfigValid =
|
||||
llmConfig.CODEX_MODEL !== "" &&
|
||||
llmConfig.CODEX_MODEL !== null &&
|
||||
llmConfig.CODEX_MODEL !== undefined;
|
||||
|
||||
const shouldValidateImages = !llmConfig.DISABLE_IMAGE_GENERATION;
|
||||
|
||||
const isImageConfigValid = () => {
|
||||
if (!shouldValidateImages) {
|
||||
return true;
|
||||
}
|
||||
switch (llmConfig.IMAGE_PROVIDER) {
|
||||
case "pexels":
|
||||
return llmConfig.PEXELS_API_KEY && llmConfig.PEXELS_API_KEY !== "";
|
||||
case "pixabay":
|
||||
return llmConfig.PIXABAY_API_KEY && llmConfig.PIXABAY_API_KEY !== "";
|
||||
case "dall-e-3":
|
||||
return llmConfig.OPENAI_API_KEY && llmConfig.OPENAI_API_KEY !== "";
|
||||
case "gpt-image-1.5":
|
||||
return llmConfig.OPENAI_API_KEY && llmConfig.OPENAI_API_KEY !== "";
|
||||
case "gemini_flash":
|
||||
return llmConfig.GOOGLE_API_KEY && llmConfig.GOOGLE_API_KEY !== "";
|
||||
case "nanobanana_pro":
|
||||
return llmConfig.GOOGLE_API_KEY && llmConfig.GOOGLE_API_KEY !== "";
|
||||
case "comfyui":
|
||||
return llmConfig.COMFYUI_URL && llmConfig.COMFYUI_URL !== "";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isLLMConfigValid =
|
||||
llmConfig.LLM === "openai"
|
||||
? isOpenAIConfigValid
|
||||
: llmConfig.LLM === "google"
|
||||
? isGoogleConfigValid
|
||||
: llmConfig.LLM === "anthropic"
|
||||
? isAnthropicConfigValid
|
||||
: llmConfig.LLM === "ollama"
|
||||
? isOllamaConfigValid
|
||||
: llmConfig.LLM === "custom"
|
||||
? isCustomConfigValid
|
||||
: llmConfig.LLM === "codex"
|
||||
? isCodexConfigValid
|
||||
: false;
|
||||
|
||||
return isLLMConfigValid && isImageConfigValid();
|
||||
};
|
||||
export const hasValidLLMConfig = (llmConfig: LLMConfig) =>
|
||||
getLLMConfigValidationError(llmConfig) === null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue