(
+ null
+);
+
+export function showActionReminder(options: ShowActionReminderOptions): void {
+ actionReminderProps.value = {
+ ...options,
+ onClose: () => {
+ isActionReminderOpen.value = false;
+ },
+ };
+ isActionReminderOpen.value = true;
+}
+
+// Example of how to trigger it - BFlat:
+// import { showGlobalActionReminder } from "@storyteller/ui-action-reminder-modal";
+// import openAiLogo from 'path/to/logo.svg';
+//
+// function someAction() {
+// showActionReminder({
+// reminderType: 'soraLogin',
+// onPrimaryAction: () => console.log('Login with Sora!'),
+// openAiLogo: openAiLogo,
+// title: 'Sora Login Needed'
+// });
+// }
diff --git a/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.module.css b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.module.css
new file mode 100644
index 0000000000..d0246c74b9
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.module.css
@@ -0,0 +1,7 @@
+/*
+ * Replace this with your own classes
+ *
+ * e.g.
+ * .container {
+ * }
+*/
\ No newline at end of file
diff --git a/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.spec.tsx b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.spec.tsx
new file mode 100644
index 0000000000..019db4d868
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.spec.tsx
@@ -0,0 +1,12 @@
+import { render } from '@testing-library/react';
+
+import ActionReminderModal from './action-reminder-modal';
+
+describe('ActionReminderModal', () => {
+
+ it('should render successfully', () => {
+ const { baseElement } = render();
+ expect(baseElement).toBeTruthy();
+ });
+
+});
diff --git a/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.tsx b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.tsx
new file mode 100644
index 0000000000..643a3e90a2
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/src/lib/action-reminder-modal.tsx
@@ -0,0 +1,140 @@
+import { Button } from "@storyteller/ui-button";
+import { Modal } from "@storyteller/ui-modal";
+import { ReactNode } from "react";
+import { faRightToBracket } from "@fortawesome/pro-solid-svg-icons";
+
+export type ReminderType = "default" | "soraLogin" | "artcraftLogin";
+
+interface ActionReminderModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ reminderType?: ReminderType;
+ onPrimaryAction: () => void;
+ title?: string;
+ hideTitle?: boolean;
+ message?: ReactNode;
+ children?: ReactNode;
+ primaryActionText?: string;
+ secondaryActionText?: string;
+ onSecondaryAction?: () => void;
+ isLoading?: boolean;
+ openAiLogo?: string;
+ modalClassName?: string;
+}
+
+export function ActionReminderModal({
+ isOpen,
+ onClose,
+ reminderType = "default",
+ onPrimaryAction,
+ title: customTitle,
+ hideTitle = false,
+ message: customMessage,
+ children: customChildren,
+ primaryActionText: customPrimaryActionText,
+ secondaryActionText: customSecondaryActionText,
+ onSecondaryAction,
+ isLoading = false,
+ openAiLogo,
+ modalClassName: customModalClassName = "",
+}: ActionReminderModalProps) {
+ let titleText: string | undefined;
+ let messageContent: ReactNode;
+ let primaryActionBtnText: string;
+ let primaryActionIcon = faRightToBracket;
+
+ if (!hideTitle) {
+ switch (reminderType) {
+ case "soraLogin":
+ titleText = customTitle || "Link Your OpenAI Sora Account";
+ break;
+ case "artcraftLogin":
+ titleText = customTitle || "Login to ArtCraft";
+ break;
+ default:
+ titleText = customTitle || "Action Required";
+ break;
+ }
+ } else {
+ titleText = undefined;
+ }
+
+ switch (reminderType) {
+ case "soraLogin":
+ titleText = customTitle || "Link Your OpenAI Account";
+ messageContent = customMessage || (
+
+ To use this feature, please connect your OpenAI account. This allows
+ us to leverage your existing subscription.
+
+ );
+ primaryActionBtnText = customPrimaryActionText || "Login with OpenAI";
+ break;
+ case "artcraftLogin":
+ messageContent = customMessage || (
+
+ Please log in or sign up to ArtCraft to proceed. This will allow you
+ to save your work and access all features.
+
+ );
+ primaryActionBtnText = customPrimaryActionText || "Login / Sign Up";
+ break;
+ default:
+ messageContent = customMessage || (
+
+ Please complete the required action.
+
+ );
+ primaryActionBtnText = customPrimaryActionText || "Proceed";
+ break;
+ }
+
+ const effectiveSecondaryAction = onSecondaryAction || onClose;
+ const effectiveSecondaryActionText = customSecondaryActionText || "Cancel";
+
+ let modalSpecificClasses = "";
+
+ const finalModalClassName =
+ `${customModalClassName} ${modalSpecificClasses}`.trim();
+
+ return (
+
+
+ {customChildren ? (
+
{customChildren}
+ ) : (
+
{messageContent}
+ )}
+
+
+
+ {(onSecondaryAction || customSecondaryActionText) && (
+
+ )}
+
+
+
+ );
+}
+
+export default ActionReminderModal;
diff --git a/frontend/libs/components/action-reminder-modal/tsconfig.json b/frontend/libs/components/action-reminder-modal/tsconfig.json
new file mode 100644
index 0000000000..d39936c57e
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "../modal"
+ },
+ {
+ "path": "../button"
+ },
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json"
+}
diff --git a/frontend/libs/components/action-reminder-modal/tsconfig.lib.json b/frontend/libs/components/action-reminder-modal/tsconfig.lib.json
new file mode 100644
index 0000000000..36311b175d
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/tsconfig.lib.json
@@ -0,0 +1,53 @@
+{
+ "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"
+ ],
+ "references": [
+ {
+ "path": "../modal/tsconfig.lib.json"
+ },
+ {
+ "path": "../button/tsconfig.lib.json"
+ }
+ ]
+}
diff --git a/frontend/libs/components/action-reminder-modal/tsconfig.spec.json b/frontend/libs/components/action-reminder-modal/tsconfig.spec.json
new file mode 100644
index 0000000000..18b7de51b8
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/tsconfig.spec.json
@@ -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"
+ }
+ ]
+}
diff --git a/frontend/libs/components/action-reminder-modal/vite.config.ts b/frontend/libs/components/action-reminder-modal/vite.config.ts
new file mode 100644
index 0000000000..44ac23a151
--- /dev/null
+++ b/frontend/libs/components/action-reminder-modal/vite.config.ts
@@ -0,0 +1,49 @@
+///
+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/action-reminder-modal',
+ 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-action-reminder-modal',
+ 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,
+ }
+ },
+}));
diff --git a/frontend/libs/components/login-modal/src/lib/ArtCraftSignUp.tsx b/frontend/libs/components/login-modal/src/lib/artcraft-signup.tsx
similarity index 100%
rename from frontend/libs/components/login-modal/src/lib/ArtCraftSignUp.tsx
rename to frontend/libs/components/login-modal/src/lib/artcraft-signup.tsx
diff --git a/frontend/libs/components/login-modal/src/lib/login-modal.tsx b/frontend/libs/components/login-modal/src/lib/login-modal.tsx
index afe6613e85..8d6ca4a4ee 100644
--- a/frontend/libs/components/login-modal/src/lib/login-modal.tsx
+++ b/frontend/libs/components/login-modal/src/lib/login-modal.tsx
@@ -14,7 +14,7 @@ import {
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
-import { ArtCraftSignUp } from "./ArtCraftSignUp";
+import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
interface LoginModalProps {
onClose: () => void;
@@ -42,11 +42,21 @@ export function LoginModal({
const artCraftFormRef = useRef(null);
const [initialArtCraftLoginStatusForUI, setInitialArtCraftLoginStatusForUI] =
useState(null);
+ const [initialSoraSessionStatusForUI, setInitialSoraSessionStatusForUI] =
+ useState(null);
- const uiTotalSteps = initialArtCraftLoginStatusForUI ? 3 : 4;
+ // Determine total steps and current step for UI
+ let uiTotalSteps = 4;
let uiCurrentStep = step;
-
- if (initialArtCraftLoginStatusForUI) {
+ 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;
@@ -67,11 +77,7 @@ export function LoginModal({
useSoraLoginListener((payload: any) => {
console.log("Login success!", payload);
- if (isLoggedInArtCraft) {
- setStep(4);
- } else {
- setStep(2);
- }
+ setStep(4); // Always go to the final step after Sora login
});
// Check session on component mount
@@ -80,8 +86,13 @@ export function LoginModal({
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) {
+ 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);
@@ -96,14 +107,10 @@ export function LoginModal({
setIsOpen(true);
setStep(1);
setIsLoggedInArtCraft(false); // Ensure this is also set
- } else if (loggedIn) {
- // Already logged in, no need to show modal
- setIsLoggedInArtCraft(true);
- // setIsOpen(false); // This would prevent modal from showing if fully logged in
}
}
);
- }, []); // Empty dependency array ensures this runs once on mount
+ }, []);
useEffect(() => {
if (onOpenChange) onOpenChange(isOpen);
@@ -124,14 +131,14 @@ export function LoginModal({
const result = await CheckSoraSession();
const sessionExists = result.state === SoraSessionState.Valid;
if (sessionExists) {
- setStep(step + 1);
+ 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(step + 1);
+ setStep(4); // Always go to the final step after Sora login
}
}
} catch (error) {
@@ -279,7 +286,13 @@ export function LoginModal({
const userInfo = session.data?.user;
if (userInfo) onArtCraftAuthSuccess(userInfo);
}
- setStep(3);
+ // Check for Sora session and skip to step 4 if present
+ const soraSessionExists = await initSession();
+ if (soraSessionExists) {
+ setStep(4);
+ } else {
+ setStep(3);
+ }
} catch (e) {
console.error(e);
setErrorMessage(
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
index ed1ab2e96c..5c9d7aadc8 100644
--- a/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBox2D.tsx
@@ -37,8 +37,13 @@ import {
SoraImageRemix,
SoraImageRemixErrorType,
SoraImageRemixAspectRatio,
+ CheckSoraSession,
+ SoraSessionState,
+ waitForSoraLogin,
} from "@storyteller/tauri-api";
import { SoundRegistry } from "@storyteller/soundboard";
+import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
+import { invoke } from "@tauri-apps/api/core";
interface ReferenceImage {
id: string;
@@ -243,7 +248,30 @@ export const PromptBox2D = ({
setPrompt(e.target.value);
};
+ // Helper to show Sora login reminder and wait for login
+ const handleSoraLoginReminder = async () => {
+ return new Promise((resolve) => {
+ showActionReminder({
+ reminderType: "soraLogin",
+ onPrimaryAction: async () => {
+ await invoke("open_sora_login_command");
+ await waitForSoraLogin();
+ toast.success("Logged in to Sora!");
+ resolve();
+ },
+ });
+ });
+ };
+
const handleTauriEnqueue = async () => {
+ // Check if the Sora session is valid
+ const soraSession = await CheckSoraSession();
+ if (soraSession.state !== SoraSessionState.Valid) {
+ setIsEnqueueing(false);
+ await handleSoraLoginReminder();
+ return;
+ }
+
const api = new PromptsApi();
let image = getCanvasRenderBitmap();
if (image === undefined) {
@@ -403,7 +431,7 @@ export const PromptBox2D = ({
const getCurrentAspectRatioIcon = () => {
const selected = aspectRatioList.find((item) => item.selected);
if (!selected || !selected.icon) return faRectangle;
- const iconElement = selected.icon as React.ReactElement;
+ const iconElement = selected.icon as React.ReactElement;
return iconElement.props.icon;
};
diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts
index f3c5b1ea38..1478157b7d 100644
--- a/frontend/libs/tauri-api/src/index.ts
+++ b/frontend/libs/tauri-api/src/index.ts
@@ -12,3 +12,4 @@ export * from "./lib/sora/LogoutSoraSession.js";
export * from "./lib/sora/SoraImageRemix.js";
export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/system/GetBuildInfo.js";
+export * from "./lib/sora/waitForSoraLogin.js";
diff --git a/frontend/libs/tauri-api/src/lib/sora/waitForSoraLogin.ts b/frontend/libs/tauri-api/src/lib/sora/waitForSoraLogin.ts
new file mode 100644
index 0000000000..45eb58af52
--- /dev/null
+++ b/frontend/libs/tauri-api/src/lib/sora/waitForSoraLogin.ts
@@ -0,0 +1,13 @@
+import { listen } from "@tauri-apps/api/event";
+
+export function waitForSoraLogin(): Promise {
+ return new Promise((resolve) => {
+ let unlisten: () => void;
+ listen("sora-login-success", () => {
+ if (unlisten) unlisten();
+ resolve();
+ }).then((stop) => {
+ unlisten = stop;
+ });
+ });
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e0c1aec879..3b50971b79 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1297,6 +1297,10 @@
"version": "0.0.1",
"extraneous": true
},
+ "libs/components/action-reminder-modal": {
+ "name": "@storyteller/ui-action-reminder-modal",
+ "version": "0.0.1"
+ },
"libs/components/badge": {
"name": "@storyteller/ui-badge",
"version": "0.0.1"
@@ -9171,6 +9175,10 @@
"resolved": "libs/tauri-utils",
"link": true
},
+ "node_modules/@storyteller/ui-action-reminder-modal": {
+ "resolved": "libs/components/action-reminder-modal",
+ "link": true
+ },
"node_modules/@storyteller/ui-badge": {
"resolved": "libs/components/badge",
"link": true
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 24faefa318..35778c31ba 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -128,6 +128,9 @@
},
{
"path": "./libs/components/upload-modal"
+ },
+ {
+ "path": "./libs/components/action-reminder-modal"
}
]
}