artcraft account login/logout (#595)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2025-05-08 21:33:38 -04:00
1 parent a155b419e1
commit b64ed411b1
8 files changed
+287 -43

No files matched your search

@@ -17,11 +17,14 @@ import { downloadFileFromUrl } from "@storyteller/api";
import { TabSelector, TabItem } from "@storyteller/ui-tab-selector";
import { Signal } from "@preact/signals-react";
import { useSignals } from "@preact/signals-react/runtime";
import { setLogoutStates } from "~/signals/authentication/utilities";
function isEditorPath(path: string) {
if (path === "/") return true;
if (path === "/idealenigma/") return true;
return false;
}
interface Props {
pageName: string;
appTabIdSignal: Signal<string>;
@@ -118,6 +121,7 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
globalAccountLogoutCallback={() => setLogoutStates()}
/>
<GalleryModal
+1
View File
@@ -1,4 +1,5 @@
export * from "./lib/enums/index.js";
export * from "./lib/models/Users.js";
export * from "./lib/AnalyticsApi.js";
export * from "./lib/BillingApi.js";
export * from "./lib/EngineApi.js";
@@ -1,7 +1,9 @@
import { useState } from "react";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { SoraAccountButton } from "../components/SoraAccountButton";
import { SoraAccountButton } from "./SoraAccountButton";
import { SoraAccountBlock } from "./SoraAccountBlock";
import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock";
interface AccountInfo {
username: string;
@@ -9,7 +11,13 @@ interface AccountInfo {
credits: number;
}
export const AccountSettingsPane = () => {
interface AccountSettingsPaneProps {
globalAccountLogoutCallback: () => void;
}
export const AccountSettingsPane = ({
globalAccountLogoutCallback,
}: AccountSettingsPaneProps) => {
const [accountInfo] = useState<AccountInfo>({
username: "-",
subscription: "-",
@@ -22,6 +30,7 @@ export const AccountSettingsPane = () => {
return (
<>
<div className="space-y-4">
{/*
<div className="flex justify-between">
<span>ArtCraft Account:</span>
<span className="text-white/80">{accountInfo.username}</span>
@@ -44,11 +53,17 @@ export const AccountSettingsPane = () => {
third party account.
</p>
</div>
*/}
<div className="flex justify-between items-center">
<span>OpenAI / Sora Account:</span>
<SoraAccountButton />
</div>
<ArtcraftAccountBlock globalAccountLogoutCallback={globalAccountLogoutCallback}/>
<SoraAccountBlock />
{/*
<hr />
<h2>Coming Soon...</h2>
<div className="flex justify-between items-center">
<span>Google / Veo Account:</span>
@@ -57,20 +72,14 @@ export const AccountSettingsPane = () => {
</Button>
</div>
<div className="flex justify-between items-center">
<span>Midjourney 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)
@@ -84,7 +93,34 @@ export const AccountSettingsPane = () => {
disabled
/>
</div>
<div>
<label htmlFor="pal-api-key" className="mb-2 block">
Fal 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="pal-api-key" className="mb-2 block">
Replicate 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>
*/}
</div>
</>
);
@@ -0,0 +1,84 @@
import { useState, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "@storyteller/ui-button";
import { UsersApi } from "@storyteller/api";
import { UserInfo } from "@storyteller/api";
const usersApi = new UsersApi();
export interface ArtcraftAccountBlockProps {
globalAccountLogoutCallback: () => void,
}
export const ArtcraftAccountBlock = ({
globalAccountLogoutCallback,
}: ArtcraftAccountBlockProps) => {
const [artcraftSession, setArtcraftSession] = useState<UserInfo| undefined>(undefined);
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [isCheckingArtcraftSession, setIsCheckingArtcraftSession] = useState(false);
useEffect(() => {
const fetchSession = async () => {
setIsCheckingArtcraftSession(true);
try {
const result = await usersApi.GetSession();
console.log(">>> result", result);
setArtcraftSession(result?.data?.user);
setIsLoggedIn(result?.data?.loggedIn || false);
} catch (e) {
console.error("Error fetching Artcraft session", e);
setArtcraftSession(undefined);
setIsLoggedIn(false);
} finally {
setIsCheckingArtcraftSession(false);
}
};
fetchSession();
}, []);
const handleArtcraftButton = async () => {
if (isCheckingArtcraftSession) return;
if (isLoggedIn) {
setIsCheckingArtcraftSession(true);
await usersApi.Logout();
setArtcraftSession(undefined);
setIsLoggedIn(false);
setIsCheckingArtcraftSession(false);
globalAccountLogoutCallback(); // TODO: This resets the old global application state
} else {
window.location.href = "/login"; // TODO(bt,2025-05-08): Once we have in-page routing, get rid of this.
}
};
return (
<div className="flex justify-between items-center">
<span>ArtCraft Account:</span>
<pre>{artcraftSession?.display_name}</pre>
<Button
variant={
isLoggedIn && !isCheckingArtcraftSession
? "destructive"
: !isLoggedIn
? "primary"
: "secondary"
}
className="h-[30px]"
onClick={handleArtcraftButton}
disabled={isCheckingArtcraftSession}
>
{isCheckingArtcraftSession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : isLoggedIn ? (
"Log Out"
) : (
"Log In"
)}
</Button>
</div>
)
}
@@ -0,0 +1,119 @@
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,
CheckSoraSessionResult,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
const DEFAULT_CACHE_TTL_MS = 1 * 60 * 1000; // 1 minute
interface SoraAccountBlockProps {
cacheKey?: string;
cacheTtlMs?: number;
}
export const SoraAccountBlock = ({
cacheKey = "soraSettingsSessionCache",
cacheTtlMs = DEFAULT_CACHE_TTL_MS,
}: SoraAccountBlockProps) => {
const [soraSession, setSoraSession] = useState<CheckSoraSessionResult| undefined>(undefined);
const [isCheckingSoraSession, setIsCheckingSoraSession] = useState(false);
useSoraLoginListener(() => {
setIsCheckingSoraSession(true);
CheckSoraSession()
.then((result) => {
setSoraSession(result);
localStorage.setItem(
cacheKey,
JSON.stringify({ session: result, timestamp: Date.now() })
);
})
.finally(() => setIsCheckingSoraSession(false));
});
useEffect(() => {
const fetchSession = async () => {
setIsCheckingSoraSession(true);
try {
const cached = localStorage.getItem(cacheKey);
if (cached) {
const { session, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < cacheTtlMs) {
setSoraSession(session);
setIsCheckingSoraSession(false);
return;
}
}
const result = await CheckSoraSession();
setSoraSession(result);
localStorage.setItem(
cacheKey,
JSON.stringify({ session: result, timestamp: Date.now() })
);
} catch (e) {
console.error("Error fetching Sora session", e);
setSoraSession(undefined);
} finally {
setIsCheckingSoraSession(false);
}
};
fetchSession();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cacheKey, cacheTtlMs]);
const handleSoraButton = async () => {
if (isCheckingSoraSession) return;
if (soraSession?.state === SoraSessionState.Valid) {
setIsCheckingSoraSession(true);
await LogoutSoraSession();
setSoraSession({state: SoraSessionState.NotSetUp});
localStorage.removeItem(cacheKey);
setIsCheckingSoraSession(false);
} else {
setIsCheckingSoraSession(true);
try {
await invoke("open_sora_login_command");
} catch (e) {
} finally {
setIsCheckingSoraSession(false);
}
}
};
return(
<div className="flex justify-between items-center">
<span>OpenAI / Sora Account:</span>
<pre>{soraSession?.maybe_account_email || ""}</pre>
<Button
variant={
soraSession?.state === SoraSessionState.Valid && !isCheckingSoraSession
? "destructive"
: soraSession?.state === SoraSessionState.NotSetUp
? "primary"
: "secondary"
}
className="h-[30px]"
onClick={handleSoraButton}
disabled={isCheckingSoraSession}
>
{isCheckingSoraSession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : soraSession?.state === SoraSessionState.Valid ? (
"Disconnect"
) : (
"Connect"
)}
</Button>
</div>
)
}
@@ -3,52 +3,51 @@ import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faUser,
faVideo,
faImage,
faCog,
faVolumeHigh,
} from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { MiscSettingsPane } from "./panes/MiscSettingsPane";
import { AudioSettingsPane } from "./panes/AudioSettingsPane";
import { AccountSettingsPane } from "./panes/AccountSettingsPane";
import { VideoSettingsPane } from "./panes/VideoSettingsPane";
import { AccountSettingsPane } from "./panes/AccountSettings/AccountSettingsPane";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
globalAccountLogoutCallback: () => void;
}
type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
//type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
type SettingsSection = "general" | "accounts" | "alerts";
export const SettingsModal = ({ isOpen, onClose }: SettingsModalProps) => {
export const SettingsModal = ({ isOpen, onClose, globalAccountLogoutCallback }: SettingsModalProps) => {
const [selectedSection, setSelectedSection] =
useState<SettingsSection>("misc");
useState<SettingsSection>("general");
const sections = [
{ id: "audio" as const, label: "Audio", icon: faVolumeHigh },
{ id: "misc" as const, label: "Misc", icon: faCog },
{ id: "general" as const, label: "General", icon: faCog },
{ id: "accounts" as const, label: "Accounts", icon: faUser },
{ id: "video" as const, label: "Video", icon: faVideo },
{ id: "image" as const, label: "Image", icon: faImage },
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
//{ id: "video" as const, label: "Video", icon: faVideo },
//{ id: "image" as const, label: "Image", icon: faImage },
];
const renderContent = () => {
switch (selectedSection) {
case "audio":
case "alerts":
return <AudioSettingsPane />;
case "misc":
case "general":
return <MiscSettingsPane />;
case "accounts":
return <AccountSettingsPane />;
case "video":
return <VideoSettingsPane />;
case "image":
return (
<div>
<button className="text-blue-600">Various Image Settings...</button>
</div>
);
return <AccountSettingsPane globalAccountLogoutCallback={globalAccountLogoutCallback} />;
//case "video":
// return <VideoSettingsPane />;
//case "image":
// return (
// <div>
// <button className="text-blue-600">Various Image Settings...</button>
// </div>
// );
}
};
@@ -1,4 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
export const LogoutSoraSession = async (): Promise<void> => {
// Replace with real Tauri command to logout Sora session - BFlat
return new Promise((resolve) => setTimeout(resolve, 500));
return await invoke("sora_logout_command");
};