login modal and settings panel update (#590)

- login now flows automatically smoothly to the next step after login
- settings modal: misc/audio panes styling and toggle switch
- account settings disconnect sora account button

remaining TODO: 
- link logout sora endpoint
This commit is contained in:
BFlat authored and GitHub committed 2025-05-08 14:21:01 +07:00
1 parent eec59c218d
commit d43ee0f6c9
31 files changed
+727 -318

No files matched your search

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -8,7 +8,7 @@ import { ErrorDialog, LoadingDots } from "~/components";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { Toaster } from "@storyteller/ui-toaster";
import { EditorLoadingBar } from "./comps/EditorLoadingBar";
import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
// import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
import { useSignals } from "@preact/signals-react/runtime";
import { useEffect, useState } from "react";
import * as gpu from "detect-gpu";
@@ -91,7 +91,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
});
useEffect(() => {
console.log('installing event listeners')
console.log("installing event listeners");
InstallSounds();
InstallImageGenerationSuccess();
InstallImageGenerationFailure();
@@ -117,7 +117,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
/>
<GenerateModals />
<ErrorDialog />
<Wizard />
{/* <Wizard /> */}
</EngineProvider>
<EditorLoadingBar />
<Toaster offsetTop={70} offsetRight={12} />
@@ -14,7 +14,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
iconClassName?: string;
iconFlip?: boolean;
htmlFor?: string;
variant?: "primary" | "secondary" | "action";
variant?: "primary" | "secondary" | "action" | "destructive";
loading?: boolean;
as?: "button" | "link";
href?: string;
@@ -44,6 +44,9 @@ export const Button = ({
case "action": {
return "bg-action hover:bg-action-900 text-white focus-visible:outline-action";
}
case "destructive": {
return "bg-red/70 hover:bg-red/50 text-white focus-visible:outline-red";
}
case "primary":
default: {
return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600";
@@ -46,7 +46,8 @@ export const Input = React.forwardRef(
id={id ? id : label ? label : undefined}
className={twMerge(
"h-10 w-full rounded-lg bg-[#242424] px-3 py-2.5 text-white placeholder-white/50 outline-none",
"border border-[#363636] transition-all duration-150 ease-in-out hover:border-brand-primary/60 focus:border-brand-primary focus:!outline-none",
"border border-[#363636] transition-all duration-150 ease-in-out hover:border-primary/60 focus:border-primary focus:!outline-none",
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[#363636]",
icon && "pl-10",
isError && "outline-red focus:outline-red",
inputClassName
@@ -8,14 +8,18 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { CheckSoraSession, SoraSessionState } from "@storyteller/tauri-api";
import {
CheckSoraSession,
SoraSessionState,
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
interface LoginModalProps {
onClose: () => void;
videoSrc2D: string;
videoSrc3D: string;
openAiLogo: string;
videoSrc2D?: string;
videoSrc3D?: string;
openAiLogo?: string;
}
export function LoginModal({
@@ -26,11 +30,15 @@ export function LoginModal({
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasSession, setHasSession] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoginClicked, setIsLoginClicked] = useState(false);
const totalSteps = 3;
useSoraLoginListener((payload) => {
console.log("Login success!", payload);
setStep(3);
});
// Check session on component mount
useEffect(() => {
const initSession = async () => {
@@ -52,14 +60,12 @@ export function LoginModal({
const handleNext = async () => {
if (step === 2) {
setIsLoading(true);
setIsLoginClicked(true);
try {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
if (sessionExists) {
@@ -69,7 +75,6 @@ export function LoginModal({
alert("Please open the desktop app to login");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const sessionExists = true;
if (sessionExists) {
@@ -169,9 +174,6 @@ export function LoginModal({
alt="OpenAI Logo"
className="w-72 h-72 mx-auto grow"
/>
{/*<div>
<Button onClick={() => handleClose()}>Cancel (temporary)</Button>
</div>*/}
</div>
);
case 3:
@@ -255,17 +257,6 @@ export function LoginModal({
>
{step === 2 ? "Login with OpenAI" : "Continue"}
</Button>
{step > 1 && step !== 3 ? (
<Button
variant="secondary"
onClick={() => {
setStep(step + 1);
}}
disabled={!isLoginClicked}
>
Next
</Button>
) : null}
</>
) : (
<Button
@@ -17,6 +17,7 @@ interface ListDropdownProps {
placeholder?: string;
value?: SelectValue;
id?: string;
className?: string;
}
export const Select = ({
@@ -25,13 +26,14 @@ export const Select = ({
placeholder,
value,
id,
className,
}: ListDropdownProps) => {
const selectedOption = options.find(
(option: SelectOption) => option.value === value
) || { label: placeholder || "", value: "" };
return (
<div className="relative mt-1">
<div className={twMerge("relative", className)}>
<Listbox value={value || ""} onChange={onChange}>
{({ open }) => (
<>
@@ -0,0 +1,112 @@
import { useState, useEffect } from "react";
import { Button } from "@storyteller/ui-button";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import {
CheckSoraSession,
SoraSessionState,
LogoutSoraSession,
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
interface SoraAccountButtonProps {
cacheKey?: string;
cacheTtlMs?: number;
}
export const SoraAccountButton = ({
cacheKey = "soraSettingsSessionCache",
cacheTtlMs = 5 * 60 * 1000,
}: SoraAccountButtonProps) => {
const [soraSession, setSoraSession] = useState<SoraSessionState | null>(null);
const [isCheckingSoraSession, setIsCheckingSoraSession] = useState(false);
useSoraLoginListener(() => {
setIsCheckingSoraSession(true);
CheckSoraSession()
.then((result) => {
setSoraSession(result.state);
localStorage.setItem(
cacheKey,
JSON.stringify({ state: result.state, timestamp: Date.now() })
);
})
.finally(() => setIsCheckingSoraSession(false));
});
useEffect(() => {
const fetchSession = async () => {
setIsCheckingSoraSession(true);
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { state, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < cacheTtlMs) {
setSoraSession(state);
setIsCheckingSoraSession(false);
return;
}
}
const result = await CheckSoraSession();
setSoraSession(result.state);
localStorage.setItem(
cacheKey,
JSON.stringify({ state: result.state, timestamp: Date.now() })
);
} catch (e) {
console.error("Error fetching Sora session", e);
setSoraSession(null);
} finally {
setIsCheckingSoraSession(false);
}
};
fetchSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cacheKey, cacheTtlMs]);
const handleSoraButton = async () => {
if (isCheckingSoraSession) return;
if (soraSession === SoraSessionState.Valid) {
setIsCheckingSoraSession(true);
await LogoutSoraSession();
setSoraSession(SoraSessionState.NotSetUp);
localStorage.removeItem(cacheKey);
setIsCheckingSoraSession(false);
} else {
setIsCheckingSoraSession(true);
try {
await invoke("open_sora_login_command");
} catch (e) {
} finally {
setIsCheckingSoraSession(false);
}
}
};
return (
<Button
variant={
soraSession === SoraSessionState.Valid && !isCheckingSoraSession
? "destructive"
: soraSession === SoraSessionState.NotSetUp
? "primary"
: "secondary"
}
className="h-[30px]"
onClick={handleSoraButton}
disabled={isCheckingSoraSession}
>
{isCheckingSoraSession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : soraSession === SoraSessionState.Valid ? (
"Disconnect"
) : (
"Connect"
)}
</Button>
);
};
@@ -0,0 +1,91 @@
import { useState } from "react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { SoraAccountButton } from "../components/SoraAccountButton";
interface AccountInfo {
username: string;
subscription: string;
credits: number;
}
export const AccountSettingsPane = () => {
const [accountInfo] = useState<AccountInfo>({
username: "-",
subscription: "-",
credits: 0,
});
const [palApiKey, setPalApiKey] = useState("");
const [klingApiKey, setKlingApiKey] = useState("");
return (
<>
<div className="space-y-4">
<div className="flex justify-between">
<span>ArtCraft Account:</span>
<span className="text-white/80">{accountInfo.username}</span>
</div>
<div className="flex justify-between">
<span>ArtCraft Subscription:</span>
<span className="text-white/80">{accountInfo.subscription}</span>
</div>
<div className="flex justify-between">
<span>Credits Remaining:</span>
<span className="text-white/80">{accountInfo.credits}</span>
</div>
<div className="rounded-md p-4 text-sm dark:bg-white/5">
<p className="text-white/60">
Note: You can optionally log into other accounts and use your
credits at those providers. Some features are only available via 3rd
party accounts, such as OpenAI / Sora GPT 4.0 Images. For other
features, ArtCraft credits are consumed unless you log into your
third party account.
</p>
</div>
<div className="flex justify-between items-center">
<span>OpenAI / Sora Account:</span>
<SoraAccountButton />
</div>
<div className="flex justify-between items-center">
<span>Google / Veo Account:</span>
<Button variant="secondary" disabled className="h-[30px]">
Connect
</Button>
</div>
<div className="space-y-4">
<div>
<label htmlFor="pal-api-key" className="mb-2 block">
Pal API Key (optional)
</label>
<Input
id="pal-api-key"
type="password"
value={palApiKey}
onChange={(e) => setPalApiKey((e.target as any).value)}
placeholder="Enter API Key"
disabled
/>
</div>
<div>
<label htmlFor="kling-api-key" className="mb-2 block">
Kling API Key (optional)
</label>
<Input
id="kling-api-key"
type="password"
value={klingApiKey}
onChange={(e) => setKlingApiKey((e.target as any).value)}
placeholder="Enter API Key"
disabled
/>
</div>
</div>
</div>
</>
);
};
@@ -1,10 +1,15 @@
import { useEffect, useState } from "react";
import { SoundRegistry } from "@storyteller/soundboard";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { AppPreferencesPayload, GetAppPreferences } from "@storyteller/tauri-api";
import { faPlay } from "@fortawesome/pro-solid-svg-icons";
import {
AppPreferencesPayload,
GetAppPreferences,
} from "@storyteller/tauri-api";
import { PreferenceName, UpdateAppPreferences } from "@storyteller/tauri-api";
import { Select, SelectValue } from "@storyteller/ui-select";
import { Switch } from "@storyteller/ui-switch";
import { Label } from "@storyteller/ui-label";
// TODO: This is maintained in two places. Here and InstallSounds.
const SOUND_OPTIONS = [
@@ -16,22 +21,21 @@ const SOUND_OPTIONS = [
{ value: "crumble", label: "Crumble" },
];
interface AudioSettingsPaneProps {
}
interface AudioSettingsPaneProps {}
export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
const [preferences, setPreferences] = useState<AppPreferencesPayload|undefined>(undefined);
const [preferences, setPreferences] = useState<
AppPreferencesPayload | undefined
>(undefined);
useEffect(() => {
const fetchData = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
setPreferences(prefs.preferences);
}
};
fetchData();
}, []);
const playSounds = preferences?.play_sounds || false;
const successSound = orNone(preferences?.generation_success_sound);
@@ -40,138 +44,128 @@ export const AudioSettingsPane = (args: AudioSettingsPaneProps) => {
const reloadPreferences = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
setPreferences(prefs.preferences);
}
};
const setPlaySounds = async (checked: boolean) => {
//const value = checked ? "true" : "false";
await UpdateAppPreferences({
preference: PreferenceName.PlaySounds,
preference: PreferenceName.PlaySounds,
value: checked,
});
await reloadPreferences();
}
};
const setSuccessSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationSuccessSound,
preference: PreferenceName.GenerationSuccessSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
};
const setFailureSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationFailureSound,
preference: PreferenceName.GenerationFailureSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
};
const setEnqueueSound = async (val: string) => {
let sendVal = val === "none" ? undefined : val;
await UpdateAppPreferences({
preference: PreferenceName.GenerationEnqueueSound,
preference: PreferenceName.GenerationEnqueueSound,
value: sendVal,
});
SoundRegistry.getInstance().playSound(val);
await reloadPreferences();
}
};
const playSound = (val?: string) => {
if (val !== undefined && val !== "none") {
SoundRegistry.getInstance().playSound(val);
}
}
};
return (<>
<div className="space-y-4">
<div>
<label htmlFor="play-sounds" className="mb-2 block">
Play Notification Sounds for Events?
</label>
<Input
id="play-sounds"
type="checkbox"
checked={playSounds}
onChange={(e) => setPlaySounds((e.target as any).checked)}
/>
return (
<>
<div className="space-y-4">
<div className="flex flex-col">
<Label htmlFor="play-sounds">
Play Notification Sounds for Events?
</Label>
<Switch enabled={playSounds} setEnabled={setPlaySounds} />
</div>
<div className="space-y-1">
<Label htmlFor="success-sound">Success Sound</Label>
<div className="flex items-center gap-2">
<Select
id="success-sound"
value={successSound}
onChange={(val: SelectValue) => setSuccessSound(val as string)}
options={SOUND_OPTIONS}
className="grow"
/>
<Button
variant="primary"
className="w-[40px] h-[40px]"
icon={faPlay}
onClick={() => playSound(successSound)}
/>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="failure-sound">Failure Sound</Label>
<div className="flex items-center gap-2">
<Select
id="failure-sound"
value={failureSound}
onChange={(val: SelectValue) => setFailureSound(val as string)}
options={SOUND_OPTIONS}
className="grow"
/>
<Button
variant="primary"
className="w-[40px] h-[40px]"
icon={faPlay}
onClick={() => playSound(failureSound)}
/>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="enqueue-sound">Enqueue Sound</Label>
<div className="flex items-center gap-2">
<Select
id="enqueue-sound"
value={enqueueSound}
onChange={(val: SelectValue) => setEnqueueSound(val as string)}
options={SOUND_OPTIONS}
className="grow"
/>
<Button
variant="primary"
className="w-[40px] h-[40px]"
icon={faPlay}
onClick={() => playSound(enqueueSound)}
/>
</div>
</div>
</div>
</>
);
};
<div>
<label htmlFor="success-sound" className="mb-2 block">
Success Sound
</label>
<Select
id="success-sound"
value={successSound}
onChange={(val: SelectValue) =>
setSuccessSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(successSound)}
>
Play
</Button>
</div>
<div>
<label htmlFor="failure-sound" className="mb-2 block">
Failure Sound
</label>
<Select
id="failure-sound"
value={failureSound}
onChange={(val: SelectValue) =>
setFailureSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(failureSound)}
>
Play
</Button>
</div>
<div>
<label htmlFor="enqueue-sound" className="mb-2 block">
Enqueue Sound
</label>
<Select
id="enqueue-sound"
value={enqueueSound}
onChange={(val: SelectValue) =>
setEnqueueSound(val as string)
}
options={SOUND_OPTIONS}
/>
<Button
variant="secondary"
className="py-1"
onClick={() => playSound(enqueueSound)}
>
Play
</Button>
</div>
</div>
</>)
}
const orNone = (val: string | undefined | null) : string => {
const orNone = (val: string | undefined | null): string => {
if (!!!val) {
return "none";
}
return val;
}
};
@@ -1,35 +1,47 @@
import { useEffect, useState } from "react";
import { Button } from "@storyteller/ui-button";
import { AppPreferencesPayload, CustomDirectory, GetAppPreferences, SystemDirectory } from "@storyteller/tauri-api";
import {
AppPreferencesPayload,
CustomDirectory,
GetAppPreferences,
SystemDirectory,
} from "@storyteller/tauri-api";
import { PreferenceName, UpdateAppPreferences } from "@storyteller/tauri-api";
import { open } from '@tauri-apps/plugin-dialog';
import { open } from "@tauri-apps/plugin-dialog";
import { Label } from "@storyteller/ui-label";
interface MiscSettingsPaneProps {
}
interface MiscSettingsPaneProps {}
export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
const [preferences, setPreferences] = useState<AppPreferencesPayload|undefined>(undefined);
const [preferences, setPreferences] = useState<
AppPreferencesPayload | undefined
>(undefined);
useEffect(() => {
const fetchData = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
console.log("prefs", prefs);
setPreferences(prefs.preferences);
}
};
fetchData();
}, []);
// NB: This might be a complex type.
const outerDownloadObject = preferences?.preferred_download_directory || {};
const downloadDirectory = ("custom" in outerDownloadObject) ? outerDownloadObject.custom as string : "";
const currentDownloadLabel = ("system" in outerDownloadObject) ? "System Download Directory" : downloadDirectory;
const downloadDirectory =
"custom" in outerDownloadObject
? (outerDownloadObject.custom as string)
: "";
const currentDownloadLabel =
"system" in outerDownloadObject
? "System Download Directory"
: downloadDirectory;
const reloadPreferences = async () => {
const prefs = await GetAppPreferences();
console.log("prefs", prefs)
console.log("prefs", prefs);
setPreferences(prefs.preferences);
}
};
const openDirectoryPicker = async () => {
let directory = await open({
@@ -41,49 +53,44 @@ export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
return; // User dismissed the dialog choice
}
await UpdateAppPreferences({
preference: PreferenceName.PreferredDownloadDirectory,
preference: PreferenceName.PreferredDownloadDirectory,
value: {
custom: directory
custom: directory,
} as CustomDirectory,
});
await reloadPreferences();
}
};
const clearDirectory = async () => {
await UpdateAppPreferences({
preference: PreferenceName.PreferredDownloadDirectory,
preference: PreferenceName.PreferredDownloadDirectory,
value: {
system: "downloads"
system: "downloads",
} as SystemDirectory,
});
await reloadPreferences();
}
};
return (<>
return (
<div className="space-y-4">
<div>
<label htmlFor="download-path" className="mb-2 block">
Default Download Directory
</label>
This is where downloads are placed after downloading.
The current path is <pre>{currentDownloadLabel}</pre>
<Button
variant="primary"
className="py-1"
onClick={openDirectoryPicker}
>
Choose Directory
<div className="space-y-2">
<Label htmlFor="download-path">Default Download Directory</Label>
<p className="text-white/80">
This is where downloads are placed after downloading. The current path
is:
</p>
<div className="bg-black/20 py-1.5 px-2 rounded-md mt-1 text-white/90">
<pre>{currentDownloadLabel}</pre>
</div>
</div>
<div className="flex gap-2">
<Button variant="primary" onClick={openDirectoryPicker}>
Choose Directory
</Button>
<Button
variant="secondary"
className="py-1"
onClick={clearDirectory}
>
Use Default
<Button variant="secondary" onClick={clearDirectory}>
Use Default
</Button>
</div>
</div>
</>)
}
);
};
@@ -0,0 +1,40 @@
import { Select, SelectValue } from "@storyteller/ui-select";
import { useState } from "react";
export const VideoSettingsPane = () => {
const [defaultVideoModel, setDefaultVideoModel] = useState("veo");
const [humanVideoProvider, setHumanVideoProvider] = useState("artcraft");
return (
<div className="space-y-4">
<div>
<label htmlFor="default-video-model" className="mb-2 block">
Default Video Model
</label>
<Select
id="default-video-model"
value={defaultVideoModel}
onChange={(val: SelectValue) => setDefaultVideoModel(val as string)}
options={[
{ value: "veo", label: "Veo" },
{ value: "kling", label: "Kling" },
]}
/>
</div>
<div>
<label htmlFor="human-video-provider" className="mb-2 block">
Human Video Provider
</label>
<Select
id="human-video-provider"
value={humanVideoProvider}
onChange={(val: SelectValue) => setHumanVideoProvider(val as string)}
options={[
{ value: "artcraft", label: "ArtCraft" },
{ value: "pal", label: "Pal" },
]}
/>
</div>
</div>
);
};
@@ -1,5 +1,4 @@
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -10,39 +9,22 @@ import {
faVolumeHigh,
} from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { Input } from "@storyteller/ui-input";
import { Select, SelectValue } from "@storyteller/ui-select";
import { MiscSettingsPane } from "./panes/MiscSettingsPane";
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
import { AccountSettingsPane } from "./panes/AccountSettingsPane";
import { VideoSettingsPane } from "./panes/VideoSettingsPane";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
}
interface AccountInfo {
username: string;
subscription: string;
credits: number;
}
type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
const [accountInfo] = useState<AccountInfo>({
username: "Username [logout]",
subscription: "Pro-tier [change/upgrade]",
credits: 10233,
});
const [selectedSection, setSelectedSection] =
useState<SettingsSection>("misc");
const [palApiKey, setPalApiKey] = useState("");
const [klingApiKey, setKlingApiKey] = useState("");
const [defaultVideoModel, setDefaultVideoModel] = useState("veo");
const [humanVideoProvider, setHumanVideoProvider] = useState("artcraft");
const sections = [
{ id: "audio" as const, label: "Audio", icon: faVolumeHigh },
{ id: "misc" as const, label: "Misc", icon: faCog },
@@ -54,123 +36,17 @@ export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
const renderContent = () => {
switch (selectedSection) {
case "audio":
return (
<AudioSettingsPane />
);
return <AudioSettingsPane />;
case "misc":
return (
<MiscSettingsPane />
);
return <MiscSettingsPane />;
case "accounts":
return (
<div className="space-y-4">
<div className="flex justify-between">
<span>ArtCraft Account:</span>
<span className="text-white/80">{accountInfo.username}</span>
</div>
<div className="flex justify-between">
<span>ArtCraft Subscription:</span>
<span className="text-white/80">{accountInfo.subscription}</span>
</div>
<div className="flex justify-between">
<span>Credits Remaining:</span>
<span className="text-white/80">{accountInfo.credits}</span>
</div>
<div className="rounded-md p-4 text-sm dark:bg-white/5">
<p className="text-white/60">
Note: You can optionally log into other accounts and use your
credits at those providers. Some features are only available via
3rd party accounts, such as OpenAI / Sora GPT 4.0 Images. For
other features, ArtCraft credits are consumed unless you log
into your third party account.
</p>
</div>
<div className="flex justify-between items-center">
<span>OpenAI / Sora Account:</span>
<Button variant="secondary" className="py-1">
Connect
</Button>
</div>
<div className="flex justify-between items-center">
<span>Google / Veo Account:</span>
<Button variant="secondary" className="py-1">
Connect
</Button>
</div>
<div className="space-y-4">
<div>
<label htmlFor="pal-api-key" className="mb-2 block">
Pal API Key (optional)
</label>
<Input
id="pal-api-key"
type="password"
value={palApiKey}
onChange={(e) => setPalApiKey((e.target as any).value)}
placeholder="Enter API Key"
/>
</div>
<div>
<label htmlFor="kling-api-key" className="mb-2 block">
Kling API Key (optional)
</label>
<Input
id="kling-api-key"
type="password"
value={klingApiKey}
onChange={(e) => setKlingApiKey((e.target as any).value)}
placeholder="Enter API Key"
/>
</div>
</div>
</div>
);
return <AccountSettingsPane />;
case "video":
return (
<div className="space-y-4">
<div>
<label htmlFor="default-video-model" className="mb-2 block">
Default Video Model
</label>
<Select
id="default-video-model"
value={defaultVideoModel}
onChange={(val: SelectValue) =>
setDefaultVideoModel(val as string)
}
options={[
{ value: "veo", label: "Veo" },
{ value: "kling", label: "Kling" },
]}
/>
</div>
<div>
<label htmlFor="human-video-provider" className="mb-2 block">
Human Video Provider
</label>
<Select
id="human-video-provider"
value={humanVideoProvider}
onChange={(val: SelectValue) =>
setHumanVideoProvider(val as string)
}
options={[
{ value: "artcraft", label: "ArtCraft" },
{ value: "pal", label: "Pal" },
]}
/>
</div>
</div>
);
return <VideoSettingsPane />;
case "image":
return (
<div>
<button className="text-blue-600">Various Image Settings (TODO)...</button>
<button className="text-blue-600">Various Image Settings...</button>
</div>
);
}
@@ -208,7 +84,7 @@ export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
))}
</div>
</div>
<div className="col-span-8 flex h-full flex-col overflow-auto">
<div className="col-span-8 flex h-full flex-col">
<div className="w-full border-b border-white/10 py-2.5 ps-0">
<h2 className="text-[18px] font-semibold">
{sections.find((s) => s.id === selectedSection)?.label}
@@ -5,21 +5,27 @@
{
"path": "../modal"
},
{
"path": "../label"
},
{
"path": "../switch"
},
{
"path": "../select"
},
{
"path": "../../tauri-api"
},
{
"path": "../../soundboard"
},
{
"path": "../input"
},
{
"path": "../button"
},
{
"path": "../../soundboard"
},
{
"path": "./tsconfig.lib.json"
},
@@ -46,20 +46,26 @@
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../label/tsconfig.lib.json"
},
{
"path": "../switch/tsconfig.lib.json"
},
{
"path": "../select/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../../soundboard/tsconfig.lib.json"
},
{
"path": "../input/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../../soundboard/tsconfig.lib.json"
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-switch
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-switch` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-switch",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/switch';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Switch from './switch';
describe('Switch', () => {
it('should render successfully', () => {
const { baseElement } = render(<Switch />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,36 @@
import { Switch as HeadlessSwitch } from "@headlessui/react";
import clsx from "clsx";
import { Fragment } from "react";
interface SwitchProps {
enabled: boolean;
setEnabled: (enabled: boolean) => void;
className?: string;
}
export function Switch({ enabled, setEnabled, className }: SwitchProps) {
return (
<HeadlessSwitch checked={enabled} onChange={setEnabled} as={Fragment}>
{({ checked, disabled }) => (
<button
className={clsx(
"group inline-flex h-6 w-11 items-center rounded-full",
checked ? "bg-primary" : "bg-action",
disabled && "cursor-not-allowed opacity-50",
className
)}
>
<span className="sr-only">Enable notifications</span>
<span
className={clsx(
"size-4 rounded-full bg-white transition",
checked ? "translate-x-6" : "translate-x-1"
)}
/>
</button>
)}
</HeadlessSwitch>
);
}
export default Switch;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/switch',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-switch',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+5 -4
View File
@@ -1,4 +1,5 @@
export * from './lib/settings/GetAppPreferences.js';
export * from './lib/settings/UpdateAppPreference.js';
export * from './lib/sora/CheckSoraSession.js';
export * from "./lib/settings/GetAppPreferences.js";
export * from "./lib/settings/UpdateAppPreference.js";
export * from "./lib/sora/CheckSoraSession.js";
export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/sora/LogoutSoraSession.js";
@@ -1,8 +1,8 @@
import { invoke } from "@tauri-apps/api/core";
export interface CheckSoraSessionResult {
state: SoraSessionState,
maybe_account_email?: string,
state: SoraSessionState;
maybe_account_email?: string;
}
export enum SoraSessionState {
@@ -11,8 +11,8 @@ export enum SoraSessionState {
Valid = "valid",
}
export const CheckSoraSession = async () : Promise<CheckSoraSessionResult> => {
let result = await invoke("check_sora_session_command");
export const CheckSoraSession = async (): Promise<CheckSoraSessionResult> => {
const result = await invoke("check_sora_session_command");
console.log(">>> sora result", result);
return (result as CheckSoraSessionResult);
}
return result as CheckSoraSessionResult;
};
@@ -0,0 +1,4 @@
export const LogoutSoraSession = async (): Promise<void> => {
// Replace with real Tauri command to logout Sora session - BFlat
return new Promise((resolve) => setTimeout(resolve, 500));
};
@@ -0,0 +1,30 @@
import { useEffect } from "react";
import { listen } from "@tauri-apps/api/event";
type SoraLoginSuccessPayload = {
// TODO
};
export function useSoraLoginListener(
onSuccess: (payload: SoraLoginSuccessPayload) => void
) {
useEffect(() => {
let unlisten: () => void;
const setupListener = async () => {
const stop = await listen<SoraLoginSuccessPayload>(
"sora-login-success",
(event) => {
onSuccess(event.payload);
}
);
unlisten = stop;
};
setupListener();
return () => {
if (unlisten) unlisten();
};
}, [onSuccess]);
}
+8
View File
@@ -2234,6 +2234,10 @@
"@tauri-apps/plugin-dialog": "^2.2.1"
}
},
"libs/components/switch": {
"name": "@storyteller/ui-switch",
"version": "0.0.1"
},
"libs/components/tab-selector": {
"name": "@storyteller/ui-tab-selector",
"version": "0.0.1"
@@ -10340,6 +10344,10 @@
"resolved": "libs/components/settings-modal",
"link": true
},
"node_modules/@storyteller/ui-switch": {
"resolved": "libs/components/switch",
"link": true
},
"node_modules/@storyteller/ui-tab-selector": {
"resolved": "libs/components/tab-selector",
"link": true
+3
View File
@@ -68,6 +68,9 @@
},
{
"path": "./libs/components/login-modal"
},
{
"path": "./libs/components/switch"
}
]
}