diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index c917fb4f40..85f8f84e44 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx @@ -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,21 +73,21 @@ 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(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) => { event.stopPropagation(); @@ -415,33 +415,30 @@ export const PageEditor = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [tabStore.activeTabId, editorEngine]); - - - return (
- { - console.log("PRESSED") - setIsLoginModalOpen(true) - }} pageName="Edit Scene" /> - {isLoginModalOpen && ( - 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; - }} - /> - )} + { + console.log("PRESSED"); + triggerRecheck(); + }} + pageName="Edit Scene" + /> + { + 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" && (
@@ -574,8 +571,6 @@ export const PageEditor = () => { />
)} - -
); }; diff --git a/frontend/libs/components/login-modal/src/index.ts b/frontend/libs/components/login-modal/src/index.ts index 0c1ddf7a40..0e62b51f8c 100644 --- a/frontend/libs/components/login-modal/src/index.ts +++ b/frontend/libs/components/login-modal/src/index.ts @@ -1,2 +1,2 @@ - -export * from './lib/login-modal'; +export * from "./lib/login-modal"; +export * from "./lib/useLoginModalStore"; diff --git a/frontend/libs/components/login-modal/src/lib/login-modal.tsx b/frontend/libs/components/login-modal/src/lib/login-modal.tsx index be6f396836..95cb79fcb2 100644 --- a/frontend/libs/components/login-modal/src/lib/login-modal.tsx +++ b/frontend/libs/components/login-modal/src/lib/login-modal.tsx @@ -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

- 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.

)} @@ -334,7 +339,8 @@ export function LoginModal({ ); }; - return + return ( +
+ ); } export default LoginModal; diff --git a/frontend/libs/components/login-modal/src/lib/useLoginModalStore.ts b/frontend/libs/components/login-modal/src/lib/useLoginModalStore.ts new file mode 100644 index 0000000000..1b917f51bc --- /dev/null +++ b/frontend/libs/components/login-modal/src/lib/useLoginModalStore.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; + +interface LoginModalStore { + isOpen: boolean; + recheckTrigger: number; + openModal: () => void; + closeModal: () => void; + triggerRecheck: () => void; +} + +export const useLoginModalStore = create((set) => ({ + isOpen: false, + recheckTrigger: 0, + openModal: () => set({ isOpen: true }), + closeModal: () => set({ isOpen: false }), + triggerRecheck: () => + set((state) => ({ recheckTrigger: state.recheckTrigger + 1 })), +})); diff --git a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx index 57c94085d6..f5788dc516 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/AccountSettings/ArtcraftAccountBlock.tsx @@ -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( 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 } }; diff --git a/frontend/libs/components/settings-modal/src/lib/panes/RouterPrioritySettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/RouterPrioritySettingsPane.tsx new file mode 100644 index 0000000000..919ca0ae0d --- /dev/null +++ b/frontend/libs/components/settings-modal/src/lib/panes/RouterPrioritySettingsPane.tsx @@ -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 ( +
+
+ {emoji} + {name} +
+
+ +
+
+ ); +}; + +export const RouterPrioritySettingsPane = () => { + const [items, setItems] = useState([ + { 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 ( +
+
+

+ Model Router Priority + {isUpdating && ( + + Updating... + + )} +

+

+ Drag and drop to reorder model priority. Higher items will be tried + first. +

+
+ + + +
+ {items.map((item) => ( + + ))} +
+
+
+
+ ); +}; diff --git a/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx b/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx index 089066a832..70a3737b0a 100644 --- a/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx +++ b/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx @@ -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 ; + case "router_priority": + return ; } }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 592df2538e..a46c60692b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index e8097f6e0d..b139910233 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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",