Compare commits

...
Author SHA1 Message Date
BFlat d62179c36a Merge branch 'main' into feature/router-priority 2025-06-21 02:22:14 +07:00
bflatastic 783f6f9223 login fixes with zustand states and model router priority tab in settings with drag and drop reordering 2025-06-21 02:18:13 +07:00
ArEnScandCrossPr0duct 744df6bcb8 Fixed issue with image background removal and drag and drop. (#689)
Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-20 12:55:26 -04:00
bflatastic 9a5ab77d3c don't return profile dropdown 2025-06-20 23:07:18 +07:00
Brandon Thomas 3e42ccc5e5 add artcraft/storyteller host to app info for debugging (#688) 2025-06-20 08:36:47 -04:00
BFlat 81bf7d712c feature/pricing modal (#684)
- pricing modal with tiers: free, plus, pro and max
- set up and ready to integrate with stripe
- small bug fix with the library modal buttons being squished

- upgrade button in TopBar is commented out for now
2025-06-20 12:36:37 +07:00
ArEnScandCrossPr0duct e9fe8bdf0d Fix/login (#686)
Moved Login to top level.
Minor bug fix later., logout after login in and exiting, log out and the button
won't trigger the modal

---------

Co-authored-by: CrossPr0duct <alicextensor@gmail.com>
2025-06-20 01:07:18 -04:00
45 changed files with 1161 additions and 171 deletions
@@ -1,5 +1,6 @@
use crate::core::commands::response::shorthand::InfallibleResponse;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::os_platform::OsPlatform;
use crate::services::fal::commands::fal_background_removal_command::FalBackgroundRemovalSuccessResponse;
use base64::prelude::BASE64_STANDARD;
@@ -10,31 +11,38 @@ use image::ImageReader;
use log::info;
use serde_derive::Serialize;
use std::io::Cursor;
use tauri::State;
#[derive(Debug, Serialize)]
pub struct BuildInfoResponse {
pub struct AppInfoResponse {
pub build_timestamp: DateTime<Utc>,
pub git_commit_id: Option<String>,
pub git_commit_short_id: Option<String>,
pub git_commit_timestamp: Option<DateTime<Utc>>,
pub storyteller_host: String,
}
impl SerializeMarker for BuildInfoResponse {}
impl SerializeMarker for AppInfoResponse {}
#[tauri::command]
pub fn get_build_info_command() -> InfallibleResponse<BuildInfoResponse> {
info!("get_build_info_command called...");
pub fn get_app_info_command(
app_env_configs: State<'_, AppEnvConfigs>,
) -> InfallibleResponse<AppInfoResponse> {
info!("get_app_info_command called...");
let build_timestamp = build_metadata::build_timestamp();
let git_commit_id = build_metadata::git_commit_id();
let git_commit_short_id = build_metadata::git_commit_short_id();
let git_commit_timestamp = build_metadata::git_commit_timestamp();
BuildInfoResponse {
let storyteller_host = app_env_configs.storyteller_host.to_api_hostname();
AppInfoResponse {
build_timestamp,
git_commit_id: git_commit_id.map(|s| s.to_string()),
git_commit_short_id: git_commit_short_id.map(|s| s.to_string()),
git_commit_timestamp,
storyteller_host,
}.into()
}
@@ -1,6 +1,6 @@
pub mod app_preferences;
pub mod enqueue;
pub mod flip_image;
pub mod get_build_info_command;
pub mod get_app_info_command;
pub mod platform_info_command;
pub mod response;
+2 -2
View File
@@ -9,7 +9,7 @@ use crate::core::commands::enqueue::image::enqueue_text_to_image_command::enqueu
use crate::core::commands::enqueue::object::enqueue_image_to_3d_object_command::enqueue_image_to_3d_object_command;
use crate::core::commands::enqueue::video::enqueue_image_to_video_command::enqueue_image_to_video_command;
use crate::core::commands::flip_image::flip_image;
use crate::core::commands::get_build_info_command::get_build_info_command;
use crate::core::commands::get_app_info_command::get_app_info_command;
use crate::core::commands::platform_info_command::platform_info_command;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
@@ -175,8 +175,8 @@ pub fn run() {
fal_hunyuan_image_to_3d_command,
fal_kling_image_to_video_command,
flip_image,
get_app_info_command,
get_app_preferences_command,
get_build_info_command,
get_fal_api_key_command,
open_sora_login_command,
platform_info_command,
@@ -2,19 +2,14 @@ import { useSignals } from "@preact/signals-react/runtime";
import { AUTH_STATUS } from "~/enums";
import { authentication } from "~/signals";
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";
// import ProfileDropdown from "./ProfileDropdown";
export const AuthButtons = () => {
export const AuthButtons = ({
loginSignUpPressed,
}: {
loginSignUpPressed: () => void;
}) => {
useSignals();
const [showLoginModal, setShowLoginModal] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
const { status, userInfo } = authentication;
@@ -22,7 +17,7 @@ export const AuthButtons = () => {
console.log(status.value);
if (status.value === AUTH_STATUS.LOGGED_IN) {
console.log("SHOWING DROPDOWN");
return <ProfileDropdown />;
return null;
} else {
return (
<>
@@ -31,34 +26,12 @@ export const AuthButtons = () => {
<Button
className="h-[38px]"
onClick={() => {
setIsSignUp(true);
setShowLoginModal(true);
loginSignUpPressed();
}}
>
Login / Sign Up
</Button>
</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);
}
}}
/>
)}
</>
);
}
@@ -6,6 +6,8 @@ import {
faFilm,
faPaintbrush,
faImage,
faDiamond,
faGem,
} from "@fortawesome/pro-solid-svg-icons";
import { Button } from "@storyteller/ui-button";
import { AuthButtons } from "./AuthButtons";
@@ -34,8 +36,10 @@ import {
is3DSceneLoaded,
set3DPageMounted,
} from "~/pages/PageEnigma/Editor/editor";
import { usePricingModalStore } from "@storyteller/ui-pricing-modal";
interface Props {
pageName: string;
loginSignUpPressed: () => void;
}
const appMenuTabs: MenuIconItem[] = [
@@ -70,7 +74,7 @@ const appMenuTabs: MenuIconItem[] = [
export const topNavMediaId = signal<string>("");
export const topNavMediaUrl = signal<string>("");
export const TopBar = ({ pageName }: Props) => {
export const TopBar = ({ pageName,loginSignUpPressed }: Props) => {
useSignals();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
@@ -92,6 +96,8 @@ export const TopBar = ({ pageName }: Props) => {
);
};
const { toggleModal } = usePricingModalStore();
return (
<>
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
@@ -141,6 +147,14 @@ export const TopBar = ({ pageName }: Props) => {
<div className="flex justify-end gap-3.5">
<div className="flex gap-2">
{/* <Button
variant="primary"
icon={faGem}
onClick={toggleModal}
className="shadow-md shadow-primary-500/50 transition-all duration-300 hover:shadow-md hover:shadow-primary-500/75"
>
Upgrade Now
</Button> */}
<Tooltip content="Settings" position="bottom" delay={300}>
<Button
variant="secondary"
@@ -160,7 +174,7 @@ export const TopBar = ({ pageName }: Props) => {
<Activity />
</div>
<div className="flex justify-end gap-2">
<AuthButtons />
<AuthButtons loginSignUpPressed={loginSignUpPressed}/>
</div>
</div>
</nav>
@@ -614,34 +614,21 @@ export const useSceneStore = create<SceneState>((set, get) => ({
},
// Add method for URL-based images
createImageFromUrl: (x: number, y: number, url: string, width?: number, height?: number) => {
// Similar to createImageFromFile but handles URL
const img = new Image();
img.crossOrigin = "Anonymous"; // Handle CORS if necessary
img.onload = () => {
const aspectRatio = img.width / img.height;
const newWidth = width || (height ? height * aspectRatio : 200); // Default width or calculate from height
const newHeight = height || (width ? width / aspectRatio : newWidth / aspectRatio); // Default height or calculate from width
createImageFromUrl: async (x: number, y: number, url: string, width?: number, height?: number) => {
try {
// Fetch the image and convert to blob
const response = await fetch(url);
const blob = await response.blob();
const node = new Node({
id: generateId(),
type: 'image',
x,
y,
width: newWidth,
height: newHeight,
fill: 'transparent',
imageElement: img, // Use the loaded image object
draggable: true,
imageUrl: url // Store the URL as the source
});
get().addNode(node);
};
img.onerror = () => {
console.error("Error loading image from URL:", url);
// Optionally handle the error, e.g., by creating a placeholder or notifying the user
};
img.src = url;
// Create a File from the blob
const filename = url.split('/').pop() || 'image.png';
const file = new File([blob], filename, { type: blob.type });
// Use existing createImage function
get().createImage(x, y, file, width, height);
} catch (error) {
console.error("Error loading image from URL:", url, error);
}
},
// Update the createImage method to handle both URLs and Files
@@ -10,6 +10,7 @@ import { authentication, pageHeight, pageWidth } from "~/signals";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import ImageToVideo from "../PageVideo/ImageToVideo";
import TextToImage from "../PageImage/TextToImage";
import {
timelineHeight,
sidePanelWidth,
@@ -72,12 +73,20 @@ import {
// videoGenerationModels,
// useModelSelectorStore,
} from "@storyteller/ui-model-selector";
import { LoginModal } from "@storyteller/ui-login-modal";
import { LoginModal, useLoginModalStore } from "@storyteller/ui-login-modal";
import PageDraw from "../PageDraw/PageDraw";
import { useTabStore } from "../Stores/TabState";
export const PageEditor = () => {
useSignals();
const { triggerRecheck } = useLoginModalStore();
const { status } = authentication;
useEffect(() => {
if (status.value === AUTH_STATUS.LOGGED_OUT) {
triggerRecheck();
}
}, [status.value, triggerRecheck]);
const tabStore = useTabStore();
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -408,7 +417,28 @@ export const PageEditor = () => {
return (
<div className="w-screen">
<TopBar pageName="Edit Scene" />
<TopBar
loginSignUpPressed={() => {
console.log("PRESSED");
triggerRecheck();
}}
pageName="Edit Scene"
/>
<LoginModal
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
{tabStore.activeTabId == "3D" && (
<div>
<OnboardingHelper />
@@ -541,24 +571,6 @@ export const PageEditor = () => {
/>
</div>
)}
<LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
onOpenChange={(isOpen: boolean) => {
if (isOpen) {
disableHotkeyInput(DomLevels.DIALOGUE);
} else {
enableHotkeyInput(DomLevels.DIALOGUE);
}
}}
onArtCraftAuthSuccess={(userInfo: any) => {
authentication.status.value = AUTH_STATUS.LOGGED_IN;
authentication.userInfo.value = userInfo;
}}
/>
</div>
);
};
@@ -27,6 +27,7 @@ import { useGenerationCompleteEvent } from "./TauriEvents/generations/useGenerat
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
import { GalleryDragComponent } from "@storyteller/ui-gallery-modal";
import { LoadingDots } from "@storyteller/ui-loading";
import { PricingModal } from "@storyteller/ui-pricing-modal";
import {
isActionReminderOpen,
@@ -132,6 +133,8 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
openAiLogo={currentReminderModalProps.openAiLogo}
/>
)}
<PricingModal />
</EngineProvider>
);
};
@@ -1,15 +0,0 @@
import { useSignals } from "@preact/signals-react/runtime";
import { PageEditor } from "~/pages/PageEnigma/PageEditor";
// TODO: Remove these file
import { currentPage } from "~/signals";
import { Pages } from "~/pages/PageEnigma/constants/page";
export const PageEnigmaComponent = () => {
useSignals();
return (
<>
<PageEditor />
</>
);
};
@@ -29,6 +29,12 @@ export default {
white: colors.white,
gray: colors.gray,
black: colors.black,
blue: colors.blue,
green: colors.green,
yellow: colors.yellow,
purple: colors.purple,
pink: colors.pink,
orange: colors.orange,
primary: {
DEFAULT: "#2d81ff",
"50": "#eef6ff",
+8 -2
View File
@@ -65,10 +65,10 @@
"path": "../../libs/components/button-dropdown"
},
{
"path": "../../libs/tauri-api"
"path": "../../libs/components/action-reminder-modal"
},
{
"path": "../../libs/components/action-reminder-modal"
"path": "../../libs/components/pricing-modal"
},
{
"path": "../../libs/components/toaster"
@@ -82,6 +82,9 @@
{
"path": "../../libs/components/promptbox"
},
{
"path": "../../libs/tauri-api"
},
{
"path": "../../libs/components/popover"
},
@@ -106,6 +109,9 @@
{
"path": "../../libs/components/gravatar"
},
{
"path": "../../libs/components/login-modal"
},
{
"path": "../../libs/components/select"
},
+3 -1
View File
@@ -98,6 +98,7 @@ export class UsersApi extends ApiManager {
};
try {
console.log("calling auth fetch")
const response = await this.authFetch<
{ username_or_email: string; password: string },
{
@@ -110,7 +111,8 @@ export class UsersApi extends ApiManager {
method: "POST",
body: body,
});
console.log(response)
console.log("Auth fetch response!!! above")
return {
success: response.success,
data: response.success
@@ -2,6 +2,15 @@
"files": [],
"include": [],
"references": [
{
"path": "../../tauri-api"
},
{
"path": "../../api"
},
{
"path": "../image-input"
},
{
"path": "../gallery-modal"
},
@@ -43,6 +43,15 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../image-input/tsconfig.lib.json"
},
{
"path": "../gallery-modal/tsconfig.lib.json"
},
@@ -376,7 +376,7 @@ export const GalleryModal = React.memo(
)}
<div className="flex h-full flex-col">
<div className="border-b border-white/10 p-4 py-3">
<div className="grid grid-cols-2 items-center">
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<h2 className="text-xl font-semibold">
{mode === "select" ? "Select Images" : "My Library"}
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../button"
},
{
"path": "./tsconfig.lib.json"
},
@@ -41,5 +41,10 @@
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../button/tsconfig.lib.json"
}
]
}
@@ -2,6 +2,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../loading-spinner"
},
{
"path": "../../tauri-api"
},
@@ -9,7 +12,7 @@
"path": "../button"
},
{
"path": "../close-button"
"path": "../modal"
},
{
"path": "./tsconfig.lib.json"
@@ -43,6 +43,9 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../loading-spinner/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
@@ -50,7 +53,7 @@
"path": "../button/tsconfig.lib.json"
},
{
"path": "../close-button/tsconfig.lib.json"
"path": "../modal/tsconfig.lib.json"
}
]
}
@@ -1,2 +1,2 @@
export * from './lib/login-modal';
export * from "./lib/login-modal";
export * from "./lib/useLoginModalStore";
@@ -1,26 +1,20 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import { useState, useEffect, useRef } from "react";
import {
faArrowRight,
faRightToBracket,
} from "@fortawesome/pro-solid-svg-icons";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import {
CheckSoraSession,
SoraSessionState,
useSoraLoginListener,
} from "@storyteller/tauri-api";
import { invoke } from "@tauri-apps/api/core";
import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
import { useLoginModalStore } from "./useLoginModalStore";
interface LoginModalProps {
onClose: () => void;
onClose?: () => void;
videoSrc2D?: string;
videoSrc3D?: string;
openAiLogo?: string;
onOpenChange?: (isOpen: boolean) => void;
onArtCraftAuthSuccess?: (userInfo: any) => void;
isSignUp?: boolean;
@@ -30,14 +24,13 @@ export function LoginModal({
onClose,
videoSrc2D,
videoSrc3D,
openAiLogo,
onOpenChange,
onArtCraftAuthSuccess,
isSignUp: initialIsSignUp = true,
}: LoginModalProps) {
const { isOpen, recheckTrigger, closeModal } = useLoginModalStore();
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
const [errorMessage, setErrorMessage] = useState("");
@@ -67,26 +60,35 @@ export function LoginModal({
setStep(4); // Always go to the final step after Sora login
});
// Check session on component mount
// Check session on component mount and when recheckTrigger changes
useEffect(() => {
checkArtCraftLogin().then((loggedIn) => {
if (loggedIn) {
setIsLoggedInArtCraft(true);
setIsOpen(false);
closeModal();
} else {
setIsOpen(true);
setStep(1); // Always start at step 1
// Reset all modal state to initial values
setStep(1);
setIsLoading(false);
setIsSignUp(initialIsSignUp);
setErrorMessage("");
setShowDiscord(false);
setShowSuccess(false);
setIsLoggedInArtCraft(false);
const { openModal } = useLoginModalStore.getState();
openModal();
}
});
}, []);
}, [recheckTrigger, closeModal, initialIsSignUp]);
useEffect(() => {
if (onOpenChange) onOpenChange(isOpen);
}, [isOpen, onOpenChange]);
const handleClose = () => {
setIsOpen(false);
onClose();
closeModal();
onClose?.();
};
const handleNext = async () => {
@@ -155,7 +157,8 @@ export function LoginModal({
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.
Connect with other creators, share your work, and get the latest
updates in our Discord community.
</p>
<div className="flex gap-4">
<Button
@@ -240,15 +243,15 @@ export function LoginModal({
try {
let signupResponse, loginResponse;
if (isSignUp) {
console.log("Sign up!")
console.log("Sign up!");
signupResponse = await usersApi.Signup({
username,
email,
password,
passwordConfirmation,
});
console.log(signupResponse)
console.log(signupResponse);
if (!signupResponse.success) {
setErrorMessage(
signupResponse.errorMessage ||
@@ -257,17 +260,21 @@ export function LoginModal({
setIsLoading(false);
return;
}
console.log("Path 1");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse);
} else {
console.log("Path 2");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse);
}
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
@@ -278,12 +285,12 @@ export function LoginModal({
}
setIsLoggedInArtCraft(true);
console.log("ARTCRAFT LOGIN SUCCeSS")
console.log("ARTCRAFT LOGIN SUCCeSS");
if (onArtCraftAuthSuccess) {
const session = await usersApi.GetSession();
const userInfo = session.data?.user;
console.log("USERINFO")
console.log(userInfo)
console.log("USERINFO");
console.log(userInfo);
if (userInfo) onArtCraftAuthSuccess(userInfo);
}
setShowDiscord(true); // Show Discord step after successful login
@@ -315,11 +322,7 @@ export function LoginModal({
return (
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step === 2 && (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
<Button variant="secondary" onClick={handleBack} disabled={isLoading}>
Back
</Button>
)}
@@ -336,7 +339,7 @@ export function LoginModal({
);
};
return createPortal(
return (
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
@@ -378,8 +381,7 @@ export function LoginModal({
</TransitionChild>
</div>
</div>
</Transition>,
document.body
</Transition>
);
}
@@ -0,0 +1,18 @@
import { create } from "zustand";
interface LoginModalStore {
isOpen: boolean;
recheckTrigger: number;
openModal: () => void;
closeModal: () => void;
triggerRecheck: () => void;
}
export const useLoginModalStore = create<LoginModalStore>((set) => ({
isOpen: false,
recheckTrigger: 0,
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
triggerRecheck: () =>
set((state) => ({ recheckTrigger: state.recheckTrigger + 1 })),
}));
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-pricing-modal
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-pricing-modal` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-pricing-modal",
"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,3 @@
export * from "./lib/pricing-modal";
export * from "./lib/pricing-config";
export * from "./lib/pricing-modal-store";
@@ -0,0 +1,95 @@
export interface PricingFeature {
text: string;
included: boolean;
tooltip?: string;
}
export interface PricingTier {
id: string;
name: string;
monthlyPrice: number;
yearlyPrice: number;
originalMonthlyPrice?: number;
originalYearlyPrice?: number;
monthlyPriceId?: string;
yearlyPriceId?: string;
features: PricingFeature[];
isPopular?: boolean;
colorScheme: "dark" | "green" | "purple" | "orange";
}
export const pricingConfig = {
header: {
title: "Purchase a subscription",
subtitle:
"Upgrade to gain access to Pro features and generate more, faster.",
},
yearlyDiscount: 20,
tiers: [
{
id: "free",
name: "Free",
monthlyPrice: 0,
yearlyPrice: 0,
features: [
{ text: "Free daily generations", included: true },
{ text: "Limited access to ArtCraft tools", included: true },
],
colorScheme: "dark" as const,
},
{
id: "plus",
name: "Plus",
monthlyPrice: 8,
yearlyPrice: 96,
originalMonthlyPrice: 10,
originalYearlyPrice: 120,
monthlyPriceId: "price_1234567890abcdef", //dummy
yearlyPriceId: "price_abcdef1234567890", //dummy
features: [
{ text: "~1,010 Flux images", included: true },
{ text: "~36,000 real-time images", included: true },
{ text: "~180 enhanced images", included: true },
{ text: "~6 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "green" as const,
},
{
id: "pro",
name: "Pro",
monthlyPrice: 28,
yearlyPrice: 336,
originalMonthlyPrice: 35,
originalYearlyPrice: 420,
monthlyPriceId: "price_fedcba0987654321", //dummy
yearlyPriceId: "price_123456789fedcba0", //dummy
features: [
{ text: "~5,048 Flux images", included: true },
{ text: "~180,000 real-time images", included: true },
{ text: "~900 enhanced images", included: true },
{ text: "~30 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "purple" as const,
},
{
id: "max",
name: "Max",
monthlyPrice: 48,
yearlyPrice: 576,
originalMonthlyPrice: 60,
originalYearlyPrice: 720,
monthlyPriceId: "price_9876543210fedcba", //dummy
yearlyPriceId: "price_abcdef9876543210", //dummy
features: [
{ text: "~15,142 Flux images", included: true },
{ text: "~540,000 real-time images", included: true },
{ text: "~2,700 enhanced images", included: true },
{ text: "~90 training jobs", included: true },
{ text: "Commercial license", included: true },
],
colorScheme: "orange" as const,
},
] as PricingTier[],
};
@@ -0,0 +1,42 @@
import { create } from "zustand";
interface SubscriptionState {
currentPlanId: string;
hasActiveSubscription: boolean;
customerId?: string;
subscriptionId?: string;
billingCycle?: "monthly" | "yearly";
}
interface PricingModalState {
isOpen: boolean;
subscription: SubscriptionState;
openModal: () => void;
closeModal: () => void;
toggleModal: () => void;
setSubscription: (subscription: Partial<SubscriptionState>) => void;
updateCurrentPlan: (planId: string, hasActiveSubscription: boolean) => void;
}
export const usePricingModalStore = create<PricingModalState>((set) => ({
isOpen: false,
subscription: {
currentPlanId: "free",
hasActiveSubscription: false,
},
openModal: () => set({ isOpen: true }),
closeModal: () => set({ isOpen: false }),
toggleModal: () => set((state) => ({ isOpen: !state.isOpen })),
setSubscription: (subscription) =>
set((state) => ({
subscription: { ...state.subscription, ...subscription },
})),
updateCurrentPlan: (planId, hasActiveSubscription) =>
set((state) => ({
subscription: {
...state.subscription,
currentPlanId: planId,
hasActiveSubscription,
},
})),
}));
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import PricingModal from './pricing-modal';
describe('PricingModal', () => {
it('should render successfully', () => {
const { baseElement } = render(<PricingModal />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,316 @@
import { Modal } from "@storyteller/ui-modal";
import { Button } from "@storyteller/ui-button";
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { pricingConfig, PricingTier } from "./pricing-config";
import { usePricingModalStore } from "./pricing-modal-store";
import { TabSelector } from "@storyteller/ui-tab-selector";
const billingTabs = [
{ id: "yearly", label: "Yearly" },
{ id: "monthly", label: "Monthly" },
];
interface PricingModalProps {
currentPlanId?: string;
hasActiveSubscription?: boolean;
}
export function PricingModal({
currentPlanId,
hasActiveSubscription,
}: PricingModalProps = {}) {
const { isOpen, closeModal, subscription } = usePricingModalStore();
// Use props if provided or fall back to store
const activePlanId = currentPlanId ?? subscription.currentPlanId;
const hasActiveSub =
hasActiveSubscription ?? subscription.hasActiveSubscription;
const [billingType, setBillingType] = useState("yearly");
const isYearly = billingType === "yearly";
const handleUpgrade = async (tierId: string) => {
// TODO: Implement Stripe checkout
const tier = pricingConfig.tiers.find((t) => t.id === tierId);
const priceId = isYearly ? tier?.yearlyPriceId : tier?.monthlyPriceId;
console.log(
`Upgrading to ${tierId} plan with ${
isYearly ? "yearly" : "monthly"
} billing`,
{ priceId, tierId }
);
// Example Stripe checkout implementation:
// await stripe.redirectToCheckout({
// lineItems: [{ price: priceId, quantity: 1 }],
// mode: 'subscription',
// successUrl: `${window.location.origin}/success`,
// cancelUrl: `${window.location.origin}/cancel`,
// });
};
const handleManageSubscription = () => {
// TODO: Redirect to Stripe customer portal
console.log("Managing subscription");
};
const tierHierarchy = { free: 0, basic: 1, pro: 2, max: 3 };
const isCurrentPlan = (tierId: string) => {
return tierId === activePlanId;
};
const getButtonText = (tier: PricingTier) => {
if (isCurrentPlan(tier.id)) {
return "Current Plan";
}
if (activePlanId && activePlanId !== "free") {
const currentTierLevel =
tierHierarchy[activePlanId as keyof typeof tierHierarchy];
const thisTierLevel =
tierHierarchy[tier.id as keyof typeof tierHierarchy];
if (thisTierLevel < currentTierLevel) {
return "Switch Plan";
}
}
return "Upgrade Plan";
};
const getColorSchemeClasses = (
colorScheme: PricingTier["colorScheme"],
isCurrent: boolean
) => {
const baseClasses =
"relative rounded-xl border p-8 h-full flex flex-col transition-all duration-300 ring-[6px] ring-transparent";
switch (colorScheme) {
case "dark":
return twMerge(
baseClasses,
"bg-[#2C2C2C] border-gray-600",
isCurrent && "ring-white/70"
);
case "green":
// Basic
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#002D23] via-[#006B54] to-[#00D28B] border-[#00a873]",
"hover:shadow-[0_0_30px_rgba(0,210,139,0.4)]",
isCurrent && "ring-[#00D28B] shadow-[0_0_30px_rgba(0,210,139,0.4)]"
);
case "purple":
// Pro
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#2D004D] via-[#6400A8] to-[#C03FFF] border-[#9D4CFF]",
"hover:shadow-[0_0_30px_rgba(192,63,255,0.4)]",
isCurrent && "ring-[#C03FFF] shadow-[0_0_30px_rgba(192,63,255,0.4)]"
);
case "orange":
// Max
return twMerge(
baseClasses,
"bg-gradient-to-b from-[#332100] via-[#B35C00] to-[#FFB347] border-[#FF8C00]",
"hover:shadow-[0_0_30px_rgba(255,179,71,0.4)]",
isCurrent && "ring-[#FFB347] shadow-[0_0_30px_rgba(255,179,71,0.4)]"
);
default:
return twMerge(
baseClasses,
"bg-[#2C2C2C] border-gray-600",
"hover:shadow-[0_0_30px_rgba(255,255,255,0.3)]",
isCurrent && "ring-white/70"
);
}
};
const formatPrice = (tier: PricingTier) => {
if (tier.monthlyPrice === 0) {
return {
current: "$0",
original: null,
};
}
if (isYearly) {
const discountedMonthlyPrice = Math.round(tier.yearlyPrice / 12);
const originalMonthlyPrice = tier.originalYearlyPrice
? Math.round(tier.originalYearlyPrice / 12)
: null;
return {
current: `$${discountedMonthlyPrice}`,
original: originalMonthlyPrice ? `$${originalMonthlyPrice}` : null,
};
} else {
const monthlyPrice = tier.originalMonthlyPrice || tier.monthlyPrice;
return {
current: `$${monthlyPrice}`,
original: null,
};
}
};
return (
<Modal
isOpen={isOpen}
onClose={closeModal}
className="rounded-xl bg-[#1A1A1A] max-h-[90vh] max-w-screen-2xl overflow-y-auto flex flex-col"
allowBackgroundInteraction={true}
showClose={true}
closeOnOutsideClick={true}
resizable={false}
backdropClassName="bg-black/80"
>
<div className="p-16 py-24 flex-1 overflow-y-auto min-h-0">
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-5xl font-bold text-white mb-4">
{pricingConfig.header.title}
</h1>
<p className="text-gray-400 text-lg mb-6">
{pricingConfig.header.subtitle}
</p>
{/* Billing Toggle */}
<div className="flex items-center justify-center gap-4 mb-8 relative w-fit mx-auto">
<TabSelector
tabs={billingTabs}
activeTab={billingType}
onTabChange={setBillingType}
className="w-fit"
tabClassName="w-24 text-md"
indicatorClassName="bg-white"
selectedTabClassName="text-black"
/>
<span className="bg-primary text-white px-3 py-0.5 rounded-full text-sm font-medium -top-3 -left-6 absolute pointer-events-none">
-{pricingConfig.yearlyDiscount}%
</span>
</div>
</div>
{/* Pricing Tiers */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-10">
{pricingConfig.tiers.map((tier) => {
const pricing = formatPrice(tier);
return (
<div
key={tier.id}
className={getColorSchemeClasses(
tier.colorScheme,
isCurrentPlan(tier.id)
)}
>
{/* Current Plan Badge */}
{isCurrentPlan(tier.id) && (
<div className="absolute -top-4 right-5 bg-white text-black px-3 py-1 rounded-full text-md font-semibold shadow-xl">
Active
</div>
)}
{/* Tier Header */}
<div className="text-center mb-8">
<h3 className="text-4xl font-bold text-white mb-4">
{tier.name}
</h3>
<div className="flex items-baseline justify-center gap-2">
{pricing.original && (
<span className="text-white/60 line-through text-2xl">
{pricing.original}
</span>
)}
<span className="text-4xl font-bold text-white">
{pricing.current}
</span>
<span className="text-white/60 text-sm">/month</span>
</div>
<p className="text-gray-300 text-xs mt-1">
{isYearly ? "billed yearly" : "billed monthly"}
</p>
</div>
{/* Features */}
<div className="flex-1 space-y-3 mb-6">
{tier.features.map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<div
className={twMerge(
"w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5",
feature.included
? "bg-white text-black"
: "bg-transparent border border-gray-400"
)}
>
{feature.included && (
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</div>
<span
className={twMerge(
"text-sm mt-0.5",
feature.included ? "text-white" : "text-gray-400"
)}
>
{feature.text}
</span>
</div>
))}
</div>
{/* CTA Button */}
<Button
onClick={() => handleUpgrade(tier.id)}
disabled={isCurrentPlan(tier.id)}
className="w-full bg-white text-black hover:bg-white/90 h-12 rounded-xl"
>
{getButtonText(tier)}
</Button>
</div>
);
})}
</div>
{/* Manage Subscription Button - only show if user has a paid plan */}
{hasActiveSub && activePlanId !== "free" && (
<div className="flex justify-center mt-8">
<Button
onClick={handleManageSubscription}
className="bg-transparent border border-white/25 text-white hover:bg-white/10 px-8 py-3 rounded-xl"
>
Manage your subscription
</Button>
</div>
)}
</div>
</Modal>
);
}
// Additional interfaces for Stripe integration
export interface SubscriptionData {
currentPlanId: string;
hasActiveSubscription: boolean;
customerId?: string;
subscriptionId?: string;
billingCycle?: "monthly" | "yearly";
nextBillingDate?: Date;
}
export default PricingModal;
@@ -0,0 +1,19 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../button"
},
{
"path": "../modal"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.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": "../button/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.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"
}
]
}
@@ -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/pricing-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-pricing-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,
}
},
}));
@@ -1,21 +1,21 @@
import { useEffect, useState } from "react";
import {
GetBuildInfo,
GetBuildInfoPayload,
GetAppInfo,
GetAppInfoPayload,
} from "@storyteller/tauri-api";
import { Label } from "@storyteller/ui-label";
interface AboutSettingsPaneProps {}
export const AboutSettingsPane = (args: AboutSettingsPaneProps) => {
const [buildInfo, setBuildInfo] = useState<
GetBuildInfoPayload | undefined
const [appInfo, setAppInfo] = useState<
GetAppInfoPayload | undefined
>(undefined);
useEffect(() => {
const fetchData = async () => {
const prefs = await GetBuildInfo();
setBuildInfo(prefs.payload);
const prefs = await GetAppInfo();
setAppInfo(prefs.payload);
};
fetchData();
}, []);
@@ -28,22 +28,27 @@ export const AboutSettingsPane = (args: AboutSettingsPaneProps) => {
<Label htmlFor="play-sounds">
Build Timestamp
</Label>
<div>{buildInfo?.build_timestamp}</div>
<div>{appInfo?.build_timestamp}</div>
</div>
<div className="space-y-1">
<Label htmlFor="success-sound">Git Commit ID</Label>
<div>{buildInfo?.git_commit_id}</div>
<div>{appInfo?.git_commit_id}</div>
</div>
<div className="space-y-1">
<Label htmlFor="success-sound">Git Commit Short ID</Label>
<div>{buildInfo?.git_commit_short_id}</div>
<div>{appInfo?.git_commit_short_id}</div>
</div>
<div className="space-y-1">
<Label htmlFor="success-sound">Git Commit Timestamp</Label>
<div>{buildInfo?.git_commit_timestamp}</div>
<div>{appInfo?.git_commit_timestamp}</div>
</div>
<div className="space-y-1">
<Label htmlFor="success-sound">Artcraft Host</Label>
<div>{appInfo?.storyteller_host}</div>
</div>
</div>
</>
@@ -5,6 +5,7 @@ import { Button } from "@storyteller/ui-button";
import { UsersApi } from "@storyteller/api";
import { UserInfo } from "@storyteller/api";
import { useLoginModalStore } from "@storyteller/ui-login-modal";
const usersApi = new UsersApi();
@@ -15,6 +16,7 @@ export interface ArtcraftAccountBlockProps {
export const ArtcraftAccountBlock = ({
globalAccountLogoutCallback,
}: ArtcraftAccountBlockProps) => {
const { triggerRecheck } = useLoginModalStore();
const [artcraftSession, setArtcraftSession] = useState<UserInfo | undefined>(
undefined
);
@@ -50,8 +52,9 @@ export const ArtcraftAccountBlock = ({
setIsLoggedIn(false);
setIsCheckingArtcraftSession(false);
globalAccountLogoutCallback(); // TODO: This resets the old global application state
triggerRecheck(); // Trigger modal to recheck session and potentially open
} else {
window.location.href = "/login"; // TODO(bt,2025-05-08): Once we have in-page routing, get rid of this.
triggerRecheck(); // Open login modal instead of redirecting
}
};
@@ -0,0 +1,176 @@
import { useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGripVertical } from "@fortawesome/pro-solid-svg-icons";
interface RouterItem {
id: string;
name: string;
emoji: string;
}
interface SortableItemProps {
id: string;
name: string;
emoji: string;
isUpdating?: boolean;
}
const SortableItem = ({
id,
name,
emoji,
isUpdating = false,
}: SortableItemProps) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`
flex items-center justify-between rounded-lg bg-white/5 p-3 transition-colors duration-200
${
isUpdating
? "cursor-wait opacity-60"
: "cursor-move hover:bg-white/10"
}
${isDragging ? "opacity-50 shadow-lg" : ""}
`}
>
<div className="flex items-center gap-3">
<span className="text-lg">{emoji}</span>
<span className="font-medium">{name}</span>
</div>
<div className="flex items-center">
<FontAwesomeIcon
icon={faGripVertical}
className="text-white/40 text-sm"
/>
</div>
</div>
);
};
export const RouterPrioritySettingsPane = () => {
const [items, setItems] = useState<RouterItem[]>([
{ id: "artcraft-3d", name: "ArtCraft 3D", emoji: "🎨" },
{ id: "gpt-image-1", name: "GPT Image 1 (GPT-4o)", emoji: "🤖" },
{ id: "flux-pro-ultra", name: "Flux Pro Ultra", emoji: "⚡" },
{ id: "recraft-3", name: "Recraft 3", emoji: "🌟" },
{ id: "flux-kontext", name: "Flux.1 Kontext", emoji: "🔮" },
]);
const [isUpdating, setIsUpdating] = useState(false);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const updateRouterPriorityOnBackend = async (newOrder: RouterItem[]) => {
try {
setIsUpdating(true);
console.log("Updating router priority on backend:", newOrder);
// Simulate API call delay
await new Promise((resolve) => setTimeout(resolve, 500)); //remove this - BFlat
//api call here
console.log("Router priority updated successfully");
} catch (error) {
console.error("Failed to update router priority:", error);
} finally {
setIsUpdating(false);
}
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems((prevItems) => {
const oldIndex = prevItems.findIndex((item) => item.id === active.id);
const newIndex = prevItems.findIndex((item) => item.id === over?.id);
const newOrder = arrayMove(prevItems, oldIndex, newIndex);
// Send update to backend
updateRouterPriorityOnBackend(newOrder);
return newOrder;
});
}
};
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
Model Router Priority
{isUpdating && (
<span className="text-xs bg-blue-500/20 text-blue-400 px-2 py-1 rounded-full animate-pulse">
Updating...
</span>
)}
</h3>
<p className="text-sm text-white/70 mb-4">
Drag and drop to reorder model priority. Higher items will be tried
first.
</p>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{items.map((item) => (
<SortableItem
key={item.id}
id={item.id}
name={item.name}
emoji={item.emoji}
isUpdating={isUpdating}
/>
))}
</div>
</SortableContext>
</DndContext>
</div>
);
};
@@ -6,12 +6,14 @@ import {
faCog,
faVolumeHigh,
faCircleInfo,
faRoute,
} 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/AccountSettings/AccountSettingsPane";
import { AboutSettingsPane } from "./panes/AboutSettingsPane";
import { RouterPrioritySettingsPane } from "./panes/RouterPrioritySettingsPane";
interface SettingsModalProps {
isOpen: boolean;
@@ -20,8 +22,12 @@ interface SettingsModalProps {
initialSection?: SettingsSection;
}
//type SettingsSection = "misc" | "audio" | "accounts" | "video" | "image";
type SettingsSection = "general" | "accounts" | "alerts" | "about";
type SettingsSection =
| "general"
| "accounts"
| "alerts"
| "about"
| "router_priority";
export const SettingsModal = ({
isOpen,
@@ -37,6 +43,11 @@ export const SettingsModal = ({
{ id: "accounts" as const, label: "Accounts", icon: faUser },
{ id: "alerts" as const, label: "Alerts", icon: faVolumeHigh },
{ id: "about" as const, label: "About", icon: faCircleInfo },
{
id: "router_priority" as const,
label: "Model Router Priority",
icon: faRoute,
},
//{ id: "video" as const, label: "Video", icon: faVideo },
//{ id: "image" as const, label: "Image", icon: faImage },
];
@@ -55,6 +66,8 @@ export const SettingsModal = ({
);
case "about":
return <AboutSettingsPane />;
case "router_priority":
return <RouterPrioritySettingsPane />;
}
};
@@ -15,6 +15,9 @@ interface TabSelectorProps {
className?: string;
disabled?: boolean;
disabledMessage?: string;
tabClassName?: string;
indicatorClassName?: string;
selectedTabClassName?: string;
}
export const TabSelector: React.FC<TabSelectorProps> = ({
@@ -24,6 +27,9 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
className,
disabled,
disabledMessage,
tabClassName,
indicatorClassName,
selectedTabClassName,
}) => {
// Find the index of the active tab
const selectedIndex = tabs.findIndex((tab) => tab.id === activeTab);
@@ -63,7 +69,10 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
<TabList className="glass glass-no-hover relative inline-flex min-w-fit overflow-x-auto rounded-lg p-0.5 py-1 !shadow-none">
{/* Animated indicator */}
<div
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-primary/30 transition-all duration-200 ease-in-out"
className={twMerge(
"absolute top-1 z-10 h-[calc(100%-8px)] rounded-md bg-primary/30 transition-all duration-200 ease-in-out",
indicatorClassName
)}
style={{
left: indicatorStyle.left,
width: indicatorStyle.width,
@@ -83,8 +92,11 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
twMerge(
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out",
"focus-visible:outline-none focus-visible:ring-0",
selected ? "text-white" : "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
selected
? twMerge("text-white", selectedTabClassName)
: "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : "",
tabClassName
)
}
>
+1 -1
View File
@@ -14,5 +14,5 @@ export * from "./lib/sora/CheckSoraSession.js";
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/system/GetAppInfo.js";
export * from "./lib/sora/waitForSoraLogin.js";
@@ -1,22 +1,23 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export interface GetBuildInfoSuccess extends CommandResult {
payload: GetBuildInfoPayload;
export interface GetAppInfoSuccess extends CommandResult {
payload: GetAppInfoPayload;
}
export interface GetBuildInfoPayload {
export interface GetAppInfoPayload {
build_timestamp: string;
git_commit_id?: string | null;
git_commit_short_id?: string | null;
git_commit_timestamp?: string | null;
storyteller_host?: string | null;
}
// Returns the Success and Error variants directly.
// Throws on Network/Tauri errors.
export const GetBuildInfo = async () : Promise<GetBuildInfoSuccess> => {
export const GetAppInfo = async () : Promise<GetAppInfoSuccess> => {
try {
return await invoke("get_build_info_command") as GetBuildInfoSuccess;
return await invoke("get_app_info_command") as GetAppInfoSuccess;
} catch (error) {
// NB: Endpoint should be infalliable
throw error;
+60
View File
@@ -13,6 +13,9 @@
"libs/**/*"
],
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.2",
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
@@ -1466,6 +1469,10 @@
"name": "@storyteller/ui-popover",
"version": "0.0.1"
},
"libs/components/pricing-modal": {
"name": "@storyteller/ui-pricing-modal",
"version": "0.0.1"
},
"libs/components/promptbox": {
"name": "@storyteller/ui-promptbox",
"version": "0.0.1"
@@ -3424,6 +3431,55 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.0.tgz",
@@ -9765,6 +9821,10 @@
"resolved": "libs/components/popover",
"link": true
},
"node_modules/@storyteller/ui-pricing-modal": {
"resolved": "libs/components/pricing-modal",
"link": true
},
"node_modules/@storyteller/ui-promptbox": {
"resolved": "libs/components/promptbox",
"link": true
+3
View File
@@ -5,6 +5,9 @@
"scripts": {},
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.2",
"@preact/signals-react": "^3.0.1",
"@remix-run/serve": "^2.15.0",
+3
View File
@@ -140,6 +140,9 @@
},
{
"path": "./libs/components/image-input"
},
{
"path": "./libs/components/pricing-modal"
}
]
}