Compare commits

...
10 changed files with 307 additions and 216 deletions
@@ -1,38 +1,65 @@
import { useSignals } from "@preact/signals-react/runtime";
import { AUTH_STATUS } from "~/enums";
import { authentication } from "~/signals";
//import { ButtonLink } from "@storyteller/ui-button-link";
import { Button } from "@storyteller/ui-button";
import ProfileDropdown from "./ProfileDropdown";
import { LoginModal } from "@storyteller/ui-login-modal";
import { useState } from "react";
import {
disableHotkeyInput,
DomLevels,
enableHotkeyInput,
} from "~/pages/PageEnigma/signals";
export const AuthButtons = () => {
useSignals();
const [showLoginModal, setShowLoginModal] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
const { status } = authentication;
const { status, userInfo } = authentication;
console.log("Current State:");
console.log(status.value);
if (status.value === AUTH_STATUS.LOGGED_IN) {
console.log("SHOWING DROPDOWN");
return <ProfileDropdown />;
} else {
return (
<div className="flex items-center gap-3.5">
<span className="text-white/20">|</span>
<div className="flex items-center gap-2">
{/* <ButtonLink
to="/login"
variant="secondary"
reloadDocument={true}
className="h-[38px]" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
<>
<div className="flex items-center gap-3.5">
<span className="text-white/20">|</span>
<Button
className="h-[38px]"
onClick={() => {
setIsSignUp(true);
setShowLoginModal(true);
}}
>
Login
</ButtonLink>
<ButtonLink
to="/signup"
reloadDocument={true}
className="h-[38px]" // TODO(bt,2025-04-19): Once we have in-page routing, get rid of this.
>
Sign Up
</ButtonLink> */}
Login / Sign Up
</Button>
</div>
</div>
{showLoginModal && (
<LoginModal
onClose={() => setShowLoginModal(false)}
isSignUp={isSignUp}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onArtCraftAuthSuccess={(userInfo_value) => {
userInfo.value = userInfo_value;
status.value = AUTH_STATUS.LOGGED_IN;
setShowLoginModal(false);
}}
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
/>
)}
</>
);
}
};
@@ -689,7 +689,7 @@ export const PaintSurface = ({
scaleY: finalScaleY,
offsetX: finalOffsetX,
offsetY: finalOffsetY,
});
}, true);
} else {
store.updateNode(
nodeId,
@@ -7,6 +7,15 @@ interface DeleteHotkeysProps {
export const useDeleteHotkeys = ({ onDelete, target = document }: DeleteHotkeysProps): void => {
const handleKeyDown = useCallback((event: KeyboardEvent) => {
// Check if the event target is an input field or contentEditable element
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable ||
target.getAttribute('role') === 'textbox') {
return; // Don't prevent default for input fields
}
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const isDelete = event.key === 'Delete' ||
event.key === 'Backspace' ||
@@ -69,6 +69,14 @@ export const ArtCraftSignUp = ({
}
};
const handleInputFocus = () => {
};
const handleInputBlur = () => {
};
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-3 text-center">
@@ -88,6 +96,8 @@ export const ArtCraftSignUp = ({
placeholder="Username"
required
autoComplete="off"
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
<Input
label="Email"
@@ -98,6 +108,8 @@ export const ArtCraftSignUp = ({
required={isSignUp}
style={isSignUp ? {} : { display: "none" }}
autoComplete="off"
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
</>
) : (
@@ -108,6 +120,8 @@ export const ArtCraftSignUp = ({
placeholder="Email or Username"
required
autoComplete="off"
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
)}
@@ -119,6 +133,8 @@ export const ArtCraftSignUp = ({
placeholder="Password"
required
autoComplete="off"
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
{isSignUp && (
<Input
@@ -129,6 +145,8 @@ export const ArtCraftSignUp = ({
placeholder="Confirm Password"
required
autoComplete="off"
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
)}
<button type="submit" className="hidden" />
@@ -23,6 +23,7 @@ interface LoginModalProps {
openAiLogo?: string;
onOpenChange?: (isOpen: boolean) => void;
onArtCraftAuthSuccess?: (userInfo: any) => void;
isSignUp?: boolean;
}
export function LoginModal({
@@ -32,35 +33,21 @@ export function LoginModal({
openAiLogo,
onOpenChange,
onArtCraftAuthSuccess,
isSignUp: initialIsSignUp = true,
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(true);
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
const [errorMessage, setErrorMessage] = useState("");
const artCraftFormRef = useRef<HTMLFormElement>(null);
const [initialArtCraftLoginStatusForUI, setInitialArtCraftLoginStatusForUI] =
useState<boolean | null>(null);
const [initialSoraSessionStatusForUI, setInitialSoraSessionStatusForUI] =
useState<boolean | null>(null);
const [showDiscord, setShowDiscord] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
// Determine total steps and current step for UI
let uiTotalSteps = 4;
let uiCurrentStep = step;
if (initialSoraSessionStatusForUI) {
// If Sora session exists, remove the Sora step (step 3)
uiTotalSteps = 3;
if (step === 1) uiCurrentStep = 1;
else if (step === 2) uiCurrentStep = 2;
else if (step === 4) uiCurrentStep = 3;
} else if (initialArtCraftLoginStatusForUI) {
// If ArtCraft is logged in but not Sora, keep 4 steps
uiTotalSteps = 4;
if (step === 1) uiCurrentStep = 1;
else if (step === 3) uiCurrentStep = 2;
else if (step === 4) uiCurrentStep = 3;
}
// Now we have 3 steps: Welcome, Sign up/Login, and Success
const uiTotalSteps = 3;
const uiCurrentStep = showSuccess ? 3 : showDiscord ? 2 : step;
const initSession = async () => {
const result = await CheckSoraSession();
@@ -82,34 +69,15 @@ export function LoginModal({
// Check session on component mount
useEffect(() => {
// Run both checks in parallel
Promise.all([initSession(), checkArtCraftLogin()]).then(
([soraSessionExists, loggedIn]) => {
setInitialArtCraftLoginStatusForUI(loggedIn || false); // Set initial status for UI step counting
setInitialSoraSessionStatusForUI(soraSessionExists || false); // Set initial Sora status for UI step counting
if (soraSessionExists && loggedIn) {
setIsLoggedInArtCraft(true);
setIsOpen(false); // Close the modal if fully logged in
return;
} else if (soraSessionExists && !loggedIn) {
// Sora session exists, but not logged in to ArtCraft
setIsOpen(true);
setStep(2);
setIsLoggedInArtCraft(false);
} else if (!soraSessionExists && loggedIn) {
// No Sora session, but logged in to ArtCraft
setIsOpen(true);
setStep(3);
setIsLoggedInArtCraft(true);
} else if (!soraSessionExists) {
// No Sora session, start at step 1
setIsOpen(true);
setStep(1);
setIsLoggedInArtCraft(false); // Ensure this is also set
}
checkArtCraftLogin().then((loggedIn) => {
if (loggedIn) {
setIsLoggedInArtCraft(true);
setIsOpen(false);
} else {
setIsOpen(true);
setStep(1); // Always start at step 1
}
);
});
}, []);
useEffect(() => {
@@ -122,31 +90,7 @@ export function LoginModal({
};
const handleNext = async () => {
if (step === 3) {
setIsLoading(true);
try {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
await new Promise((resolve) => setTimeout(resolve, 3000));
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
if (sessionExists) {
setStep(4); // Always go to the final step after Sora login
}
} else {
alert("Please open the desktop app to login");
await new Promise((resolve) => setTimeout(resolve, 3000));
const sessionExists = true;
if (sessionExists) {
setStep(4); // Always go to the final step after Sora login
}
}
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsLoading(false);
}
} else if (step === 2) {
if (step === 2) {
// Trigger the form submit in ArtCraftSignUp
if (artCraftFormRef.current) {
artCraftFormRef.current.requestSubmit();
@@ -174,12 +118,68 @@ export function LoginModal({
</div>
);
const handleDiscordJoin = () => {
window.open("https://discord.gg/75svZP2Vje", "_blank");
setShowDiscord(false);
setShowSuccess(true);
};
// Step content rendering
const renderStepContent = () => {
if (step === 2 && isLoggedInArtCraft) {
setStep(3);
return null;
if (showSuccess) {
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Thank you for signing in!
</h2>
<p className="text-white/70 mb-6 text-center">
You're all set to start creating amazing content.
</p>
<Button
variant="primary"
onClick={handleClose}
icon={faArrowRight}
iconFlip={true}
className="text-md"
>
Get Started
</Button>
</div>
);
}
if (showDiscord) {
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Join Our Community
</h2>
<p className="text-white/70 mb-6 text-center px-24">
Connect with other creators, share your work, and get the latest updates in our Discord community.
</p>
<div className="flex gap-4">
<Button
variant="secondary"
onClick={() => {
setShowDiscord(false);
setShowSuccess(true);
}}
>
Skip for now
</Button>
<Button
variant="primary"
onClick={handleDiscordJoin}
icon={faDiscord}
className="text-md bg-[#5865F2] hover:bg-[#6a76ff]"
>
Join Discord
</Button>
</div>
</div>
);
}
switch (step) {
case 1:
return (
@@ -240,12 +240,15 @@ export function LoginModal({
try {
let signupResponse, loginResponse;
if (isSignUp) {
console.log("Sign up!")
signupResponse = await usersApi.Signup({
username,
email,
password,
passwordConfirmation,
});
console.log(signupResponse)
if (!signupResponse.success) {
setErrorMessage(
signupResponse.errorMessage ||
@@ -258,41 +261,32 @@ export function LoginModal({
usernameOrEmail: username || email,
password,
});
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
"Login after signup failed, please try again. "
);
setIsLoading(false);
return;
}
} else {
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
"Login failed, please try again."
);
setIsLoading(false);
return;
}
}
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
"Login failed, please try again."
);
setIsLoading(false);
return;
}
setIsLoggedInArtCraft(true);
console.log("ARTCRAFT LOGIN SUCCeSS")
if (onArtCraftAuthSuccess) {
const session = await usersApi.GetSession();
const userInfo = session.data?.user;
console.log("USERINFO")
console.log(userInfo)
if (userInfo) onArtCraftAuthSuccess(userInfo);
}
// Check for Sora session and skip to step 4 if present
const soraSessionExists = await initSession();
if (soraSessionExists) {
setStep(4);
} else {
setStep(3);
}
setShowDiscord(true); // Show Discord step after successful login
} catch (e) {
console.error(e);
setErrorMessage(
@@ -308,53 +302,40 @@ export function LoginModal({
errorMessage={errorMessage}
/>
);
case 3:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Login with your OpenAI account
</h2>
<p className="text-white/70 mb-6 text-center px-16">
We're the application layer for images and video. To provide you
with <b>100% free service</b>, we use your existing AI accounts.
To start off, you'll need to add your OpenAI Sora account. If you
have a ChatGPT subscription, you can log in with Sora.
</p>
<img
src={openAiLogo}
alt="OpenAI Logo"
className="w-72 h-72 mx-auto grow"
/>
</div>
);
case 4:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Thank you for signing in!
</h2>
<p className="text-white/70 mb-6 text-center px-24">
You're all set to start creating amazing content. Join our Discord
community to connect with other creators, share your work, and get
the latest updates.
</p>
<Button
variant="secondary"
onClick={() =>
window.open("https://discord.gg/75svZP2Vje", "_blank")
}
icon={faDiscord}
className="text-md bg-[#5865F2] hover:bg-[#6a76ff] rounded-xl"
>
Join our Discord
</Button>
</div>
);
default:
return null;
}
};
const renderFooterButtons = () => {
if (showSuccess || showDiscord) {
return null; // No footer buttons needed in success or discord states
}
return (
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step === 2 && (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
Back
</Button>
)}
<Button
icon={faArrowRight}
iconFlip={true}
onClick={handleNext}
loading={isLoading}
disabled={isLoading}
>
{step === 1 ? "Continue" : isSignUp ? "Sign up" : "Login"}
</Button>
</div>
);
};
return createPortal(
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
@@ -382,58 +363,16 @@ export function LoginModal({
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-4 p-8 h-full">
<span className="text-sm text-center opacity-60 font-medium">
Step {uiCurrentStep} of {uiTotalSteps}
</span>
{renderProgress()}
{!showSuccess && (
<>
<span className="text-sm text-center opacity-60 font-medium">
Step {uiCurrentStep} of {uiTotalSteps}
</span>
{renderProgress()}
</>
)}
{renderStepContent()}
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step === 2 ? (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
Back
</Button>
) : null}
{step === 3 && (
<Button
variant="secondary"
onClick={handleClose}
disabled={isLoading}
>
Skip for now
</Button>
)}
{step === 4 ? (
<Button
icon={faArrowRight}
iconFlip={true}
onClick={handleClose}
>
Start Creating Now
</Button>
) : (
<>
<Button
icon={step === 3 ? faRightToBracket : faArrowRight}
iconFlip={step !== 3}
onClick={handleNext}
loading={isLoading}
disabled={isLoading}
>
{step === 1
? "Continue"
: step === 2
? isSignUp
? "Sign up"
: "Login"
: "Login with OpenAI"}
</Button>
</>
)}
</div>
{renderFooterButtons()}
</div>
</div>
</TransitionChild>
@@ -5,6 +5,7 @@ import { SoraAccountButton } from "./SoraAccountButton";
import { SoraAccountBlock } from "./SoraAccountBlock";
import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock";
import { FalApiKeyBlock } from "./FalApiKeyBlock";
import { OpenAIApiKeyBlock } from "./OpenAIKeyBlock";
interface AccountInfo {
username: string;
@@ -57,11 +58,9 @@ export const AccountSettingsPane = ({
*/}
<ArtcraftAccountBlock globalAccountLogoutCallback={globalAccountLogoutCallback}/>
<SoraAccountBlock />
<FalApiKeyBlock />
<OpenAIApiKeyBlock/>
{/*
<hr />
@@ -0,0 +1,71 @@
import { useState, useEffect } from "react";
import { GetOpenAIApiKey, SetOpenAIApiKey } from "@storyteller/tauri-api";
import { Input } from "@storyteller/ui-input";
export const OpenAIApiKeyBlock = () => {
const [openAIApiKey, setOpenAIApiKey] = useState<string | undefined>(
undefined
);
const [isCheckingOpenAIApiKey, setIsCheckingOpenAIApiKey] =
useState(false);
useEffect(() => {
const fetchKey = async () => {
setIsCheckingOpenAIApiKey(true);
try {
const result = await GetOpenAIApiKey();
if ("payload" in result) {
setOpenAIApiKey(result.payload.key);
}
} catch (e) {
console.error("Error fetching Fal API key", e);
setOpenAIApiKey(undefined);
} finally {
setIsCheckingOpenAIApiKey(false);
}
};
fetchKey();
}, []);
const syncOpenAIApiKey = async (key: string) => {
await SetOpenAIApiKey(key);
setOpenAIApiKey(key);
};
return (
<div className="flex justify-between items-center">
<span>Open AI API Key:</span>
<Input
value={openAIApiKey}
onChange={(e) => syncOpenAIApiKey(e.target.value)}
placeholder="Enter Open AI API key"
/>
{/*<Button
variant={
isCheckingArtcraftSession
? "secondary"
: isLoggedIn
? "destructive"
: "primary"
}
className="h-[30px]"
onClick={handleArtcraftButton}
disabled={isCheckingArtcraftSession}
>
{isCheckingArtcraftSession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : isLoggedIn ? (
"Log Out"
) : (
"Log In"
)}
</Button>*/}
</div>
);
};
+2
View File
@@ -6,6 +6,8 @@ export * from "./lib/fal/FalHunyuanImageTo3d.js";
export * from "./lib/fal/FalKlingImageToVideo.js";
export * from "./lib/fal/GetFalApiKey.js";
export * from "./lib/fal/SetFalApiKey.js";
export * from "./lib/openai/SetOpenAIApiKey.js";
export * from "./lib/openai/GetOpenAIApiKey.js";
export * from "./lib/settings/GetAppPreferences.js";
export * from "./lib/settings/UpdateAppPreference.js";
export * from "./lib/sora/CheckSoraSession.js";
@@ -0,0 +1,15 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export interface GetOpenAIApiKeySuccessResult extends CommandResult {
payload: {
key: string;
};
}
export type GetOpenAIApiKeyResult = GetOpenAIApiKeySuccessResult | CommandResult;
export const GetOpenAIApiKey = async (): Promise<GetOpenAIApiKeyResult> => {
const result = await invoke("get_openai_api_key_command");
return result as GetOpenAIApiKeyResult;
};
@@ -0,0 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export const SetOpenAIApiKey = async (key: string): Promise<CommandResult> => {
const result = await invoke("set_openai_api_key_command", {
request: {
key: key,
}
});
return result as CommandResult;
};