mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 05:20:58 -05:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d62179c36a | ||
|
|
783f6f9223 | ||
|
|
744df6bcb8 | ||
|
|
9a5ab77d3c | ||
|
|
3e42ccc5e5 | ||
|
|
81bf7d712c | ||
|
|
e9fe8bdf0d |
+13
-5
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
+4
-1
@@ -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
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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";
|
||||
|
||||
+6
-5
@@ -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;
|
||||
Generated
+60
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/image-input"
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/pricing-modal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user