Compare commits

...
9 changed files with 344 additions and 68 deletions
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useContext, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -73,26 +73,32 @@ 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 [isLoginModalOpen, setIsLoginModalOpen] = useState<boolean>(false);
const { triggerRecheck } = useLoginModalStore();
const { status } = authentication;
useEffect(() => {
if (status.value !== AUTH_STATUS.LOGGED_OUT) {
setIsLoginModalOpen(true);
if (status.value === AUTH_STATUS.LOGGED_OUT) {
triggerRecheck();
}
}, [status.value]);
}, [status.value, triggerRecheck]);
const tabStore = useTabStore();
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
};
// TODO: For 3d Promptbox to accept this later on
// const { selectedModels } = useModelSelectorStore();
// const selectedModel =
// selectedModels[ModelCategory.Editor3D] ||
// videoGenerationModels[0]?.label;
useEffect(() => {
timelineHeight.value = 0; //timelineHeight.value = 208;
sidePanelWidth.value = 340;
@@ -409,30 +415,30 @@ export const PageEditor = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tabStore.activeTabId, editorEngine]);
return (
<div className="w-screen">
<TopBar loginSignUpPressed={()=> {
setIsLoginModalOpen(true)
}} pageName="Edit Scene" />
{isLoginModalOpen && (
<LoginModal
onClose={() => setIsLoginModalOpen(false)}
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;
}}
/>
)}
<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 />
@@ -565,8 +571,6 @@ export const PageEditor = () => {
/>
</div>
)}
</div>
);
};
@@ -1,2 +1,2 @@
export * from './lib/login-modal';
export * from "./lib/login-modal";
export * from "./lib/useLoginModalStore";
@@ -1,9 +1,7 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { useState, useEffect, useRef } from "react";
import {
faArrowRight,
} from "@fortawesome/pro-solid-svg-icons";
import { faArrowRight } from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import {
CheckSoraSession,
@@ -12,8 +10,9 @@ import {
} from "@storyteller/tauri-api";
import { ArtCraftSignUp } from "./artcraft-signup";
import { UsersApi } from "@storyteller/api";
import { useLoginModalStore } from "./useLoginModalStore";
interface LoginModalProps {
onClose: () => void;
onClose?: () => void;
videoSrc2D?: string;
videoSrc3D?: string;
onOpenChange?: (isOpen: boolean) => void;
@@ -29,9 +28,9 @@ export function LoginModal({
onArtCraftAuthSuccess,
isSignUp: initialIsSignUp = true,
}: LoginModalProps) {
const { isOpen, recheckTrigger, closeModal } = useLoginModalStore();
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
const [isSignUp, setIsSignUp] = useState(initialIsSignUp);
const [errorMessage, setErrorMessage] = useState("");
@@ -61,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 () => {
@@ -149,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
@@ -234,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 ||
@@ -251,21 +260,21 @@ export function LoginModal({
setIsLoading(false);
return;
}
console.log("Path 1")
console.log("Path 1");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
console.log(loginResponse);
} else {
console.log("Path 2")
console.log("Path 2");
loginResponse = await usersApi.Login({
usernameOrEmail: username || email,
password,
});
console.log(loginResponse)
console.log(loginResponse);
}
if (!loginResponse.success) {
setErrorMessage(
loginResponse.errorMessage ||
@@ -276,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
@@ -313,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>
)}
@@ -334,7 +339,8 @@ export function LoginModal({
);
};
return <Transition appear show={isOpen}>
return (
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
enter="ease-out duration-300"
@@ -376,6 +382,7 @@ export function LoginModal({
</div>
</div>
</Transition>
);
}
export default LoginModal;
@@ -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 })),
}));
@@ -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 />;
}
};
+52
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",
@@ -3428,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",
+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",