diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/PageEditor.tsx index 92a652ab66..aa39e8cf8d 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, useRef } from "react"; +import React, { useContext, useEffect } from "react"; import { useSignals } from "@preact/signals-react/runtime"; import { TopBar } from "~/components"; import { Controls3D } from "./comps/Controls3D"; @@ -31,7 +31,12 @@ import { PopoverItem } from "@storyteller/ui-popover"; import { LoadingDots } from "@storyteller/ui-loading"; import { OnboardingHelper } from "./comps/OnboardingHelper"; import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay"; -import { appTabId, is3DEditorInitialized, is3DPageMounted, set3DPageMounted, setAppTabId } from "~/signals/appTab"; +import { + appTabId, + is3DEditorInitialized, + set3DPageMounted, + setAppTabId, +} from "~/signals/appTab"; import { KonvaCanvasContainer } from "../Page2d/KonvaCanvasContainer"; import { KonvaRootComponent } from "../Page2d/KonvaRootComponent"; import { @@ -68,6 +73,14 @@ import { onImageDrop, removeImageDropListener, } from "@storyteller/ui-gallery-modal"; +import { + imageGenerationModels, + ModelCategory, + // ModelCategory, + ModelSelector, + // videoGenerationModels, + // useModelSelectorStore, +} from "@storyteller/ui-model-selector"; export const PageEditor = () => { useSignals(); @@ -76,6 +89,12 @@ export const PageEditor = () => { 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; @@ -343,7 +362,7 @@ export const PageEditor = () => { } set3DPageMounted(true); - }, [currentAppTabId, editorEngine, is3DInit]) + }, [currentAppTabId, editorEngine, is3DInit]); return (
@@ -356,6 +375,7 @@ export const PageEditor = () => { {display3d && ( <> +
{ type="bricks" message={editorLoader.value.message} /> + +
+ +
@@ -477,7 +509,7 @@ export const PageEditor = () => { )} { }} + onClose={() => {}} videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4" videoSrc3D="/resources/videos/artcraft-3d-demo.mp4" openAiLogo="/resources/images/openai-logo.png" diff --git a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx index b629e4d58d..db0c9f7031 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageImage/TextToImage.tsx @@ -1,10 +1,13 @@ -import { useState, useRef } from "react"; -import { JobContextType } from "libs/common/src/lib/types"; +import { useRef } from "react"; +import { JobContextType } from "@storyteller/common"; import { PromptBoxImage } from "@storyteller/ui-promptbox"; import BackgroundGallery from "./BackgroundGallery"; -import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover"; -import { faClock, faImage } from "@fortawesome/pro-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + imageGenerationModels, + ModelCategory, + ModelSelector, + useModelSelectorStore, +} from "@storyteller/ui-model-selector"; interface TextToImageProps { imageMediaId?: string; @@ -13,14 +16,10 @@ interface TextToImageProps { const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => { const containerRef = useRef(null); - const [selectedModel, setSelectedModel] = useState( - "GPT Image 1 (GPT-4o)", - ); - const [useSystemPrompt, setUseSystemPrompt] = useState(false); - const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false); - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreviewUrl, setImagePreviewUrl] = useState(""); - const [isDialogOpen, setIsDialogOpen] = useState(false); + const { selectedModels } = useModelSelectorStore(); + const selectedModel = + selectedModels[ModelCategory.TextToImage] || + imageGenerationModels[0]?.label; const jobContext: JobContextType = { jobTokens: [], @@ -29,34 +28,6 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => { clearJobTokens: () => {}, }; - const modelList: PopoverItem[] = [ - { - label: "GPT Image 1 (GPT-4o)", - icon: , - selected: selectedModel === "GPT Image 1 (GPT-4o)", - description: "Slow, ultra instructive model", - badges: [{ label: "45 sec.", icon: }], - }, - { - label: "Flux Pro Ultra", - icon: , - selected: selectedModel === "Flux Pro Ultra", - description: "High quality model", - badges: [{ label: "15 sec.", icon: }], - }, - { - label: "Recraft 3", - icon: , - selected: selectedModel === "Recraft 3", - description: "Fast and high-quality model", - badges: [{ label: "15 sec.", icon: }], - }, - ]; - - const handleModelSelect = (item: PopoverItem) => { - setSelectedModel(item.label); - }; - return (
{ { - // TODO: Implement job context logic console.log("Using job context"); return jobContext; }} model={selectedModel} imageMediaId={imageMediaId} - url={imageUrl ?? imagePreviewUrl ?? undefined} + url={imageUrl ?? undefined} onEnqueuePressed={async () => { console.log("Enqueue pressed"); }} @@ -88,9 +58,9 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
- { const containerRef = useRef(null); - const [selectedModel, setSelectedModel] = useState("Kling 2.0"); - - const [useSystemPrompt, setUseSystemPrompt] = useState(false); - const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false); - - const [selectedImage, setSelectedImage] = useState(null); - const [imagePreviewUrl, setImagePreviewUrl] = useState(""); - const [isDialogOpen, setIsDialogOpen] = useState(false); + const { selectedModels } = useModelSelectorStore(); + const selectedModel = + selectedModels[ModelCategory.ImageToVideo] || + videoGenerationModels[0]?.label; const jobContext: JobContextType = { jobTokens: [], @@ -30,27 +28,6 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => { clearJobTokens: () => {}, }; - const modelList: PopoverItem[] = [ - { - label: "Kling 2.0", - icon: , - selected: selectedModel === "Kling 2.0", - description: "High quality model", - badges: [{ label: "2 min.", icon: }], - }, - { - label: "Runway", - icon: , - selected: selectedModel === "Runway", - description: "Fast and high-quality model", - badges: [{ label: "2 min.", icon: }], - }, - ]; - - const handleModelSelect = (item: PopoverItem) => { - setSelectedModel(item.label); - }; - return (
{ }} model={selectedModel} imageMediaId={imageMediaId} - url={imageUrl ?? imagePreviewUrl ?? undefined} + url={imageUrl ?? undefined} onEnqueuePressed={async () => { console.log("Enqueue pressed"); }} @@ -82,10 +59,9 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
- []; + +export const allModels = { + video: [ + { + label: "Kling 2.0", + icon: , + description: "High quality model", + badges: [{ label: "2 min.", icon: }], + }, + { + label: "Runway", + icon: , + description: "Fast and high-quality model", + badges: [{ label: "2 min.", icon: }], + }, + ], + image: [ + { + label: "GPT Image 1 (GPT-4o)", + icon: , + description: "Slow, ultra instructive model", + badges: [{ label: "45 sec.", icon: }], + }, + { + label: "Flux Pro Ultra", + icon: , + description: "High quality model", + badges: [{ label: "15 sec.", icon: }], + }, + { + label: "Recraft 3", + icon: , + description: "Fast and high-quality model", + badges: [{ label: "15 sec.", icon: }], + }, + { + label: "Flux.1 Kontext", + icon: , + description: "New model with advanced context", + badges: [{ label: "25 sec.", icon: }], + }, + ], +}; + +export const videoGenerationModels: ModelList = allModels.video; +export const imageGenerationModels: ModelList = allModels.image; diff --git a/frontend/libs/components/model-selector/src/lib/model-selector-store.ts b/frontend/libs/components/model-selector/src/lib/model-selector-store.ts new file mode 100644 index 0000000000..0339e174cd --- /dev/null +++ b/frontend/libs/components/model-selector/src/lib/model-selector-store.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { ModelCategory } from "./model-categories"; + +interface ModelSelectorState { + selectedModels: { [category in ModelCategory]?: string }; + setSelectedModel: (category: ModelCategory, modelLabel: string) => void; +} + +export const useModelSelectorStore = create((set) => ({ + selectedModels: {}, + setSelectedModel: (category, modelLabel) => + set((state) => ({ + selectedModels: { + ...state.selectedModels, + [category]: modelLabel, + }, + })), +})); diff --git a/frontend/libs/components/model-selector/src/lib/model-selector.spec.tsx b/frontend/libs/components/model-selector/src/lib/model-selector.spec.tsx new file mode 100644 index 0000000000..5648146f67 --- /dev/null +++ b/frontend/libs/components/model-selector/src/lib/model-selector.spec.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; + +import ModelSelector from './model-selector'; + +describe('ModelSelector', () => { + + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + +}); diff --git a/frontend/libs/components/model-selector/src/lib/model-selector.tsx b/frontend/libs/components/model-selector/src/lib/model-selector.tsx new file mode 100644 index 0000000000..448c81961d --- /dev/null +++ b/frontend/libs/components/model-selector/src/lib/model-selector.tsx @@ -0,0 +1,48 @@ +import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover"; +import { useModelSelectorStore } from "./model-selector-store"; +import { useMemo } from "react"; +import { ModelCategory } from "./model-categories"; + +interface ModelSelectorProps { + items: Omit[]; + category: ModelCategory; + mode?: "hoverSelect" | "default" | "toggle" | "button"; + panelTitle?: string; + buttonClassName?: string; + panelClassName?: string; + showIconsInList?: boolean; + triggerLabel?: string; +} + +export function ModelSelector({ + items, + category, + ...popoverProps +}: ModelSelectorProps) { + const { selectedModels, setSelectedModel } = useModelSelectorStore(); + const selectedModel = selectedModels[category] || items[0]?.label; + + const handleModelSelect = (item: PopoverItem) => { + setSelectedModel(category, item.label); + }; + + const modelList = useMemo( + () => + items.map((item) => ({ + ...item, + selected: item.label === selectedModel, + })), + [items, selectedModel] + ); + + return ( + + ); +} + +export default ModelSelector; diff --git a/frontend/libs/components/model-selector/tsconfig.json b/frontend/libs/components/model-selector/tsconfig.json new file mode 100644 index 0000000000..8a5b74e22f --- /dev/null +++ b/frontend/libs/components/model-selector/tsconfig.json @@ -0,0 +1,16 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "../popover" + }, + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/frontend/libs/components/model-selector/tsconfig.lib.json b/frontend/libs/components/model-selector/tsconfig.lib.json new file mode 100644 index 0000000000..887f1806e6 --- /dev/null +++ b/frontend/libs/components/model-selector/tsconfig.lib.json @@ -0,0 +1,50 @@ +{ + "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": "../popover/tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/model-selector/tsconfig.spec.json b/frontend/libs/components/model-selector/tsconfig.spec.json new file mode 100644 index 0000000000..18b7de51b8 --- /dev/null +++ b/frontend/libs/components/model-selector/tsconfig.spec.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "jsx": "react-jsx" + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/model-selector/vite.config.ts b/frontend/libs/components/model-selector/vite.config.ts new file mode 100644 index 0000000000..8d1b26f8f3 --- /dev/null +++ b/frontend/libs/components/model-selector/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/components/model-selector', + 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-model-selector', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es' as const] + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react','react-dom','react/jsx-runtime'] + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + } + }, +})); diff --git a/frontend/libs/components/promptbox/tsconfig.json b/frontend/libs/components/promptbox/tsconfig.json index 271c32e9e5..de722cb539 100644 --- a/frontend/libs/components/promptbox/tsconfig.json +++ b/frontend/libs/components/promptbox/tsconfig.json @@ -2,18 +2,18 @@ "files": [], "include": [], "references": [ - { - "path": "../../tauri-api" - }, { "path": "../camera-settings-modal" }, { - "path": "../toaster" + "path": "../action-reminder-modal" }, { "path": "../../soundboard" }, + { + "path": "../../tauri-api" + }, { "path": "../gallery-modal" }, @@ -38,6 +38,9 @@ { "path": "../../common" }, + { + "path": "../toaster" + }, { "path": "./tsconfig.lib.json" }, diff --git a/frontend/libs/components/promptbox/tsconfig.lib.json b/frontend/libs/components/promptbox/tsconfig.lib.json index 7dc89b03ea..862f241932 100644 --- a/frontend/libs/components/promptbox/tsconfig.lib.json +++ b/frontend/libs/components/promptbox/tsconfig.lib.json @@ -43,18 +43,18 @@ "src/**/*.tsx" ], "references": [ - { - "path": "../../tauri-api/tsconfig.lib.json" - }, { "path": "../camera-settings-modal/tsconfig.lib.json" }, { - "path": "../toaster/tsconfig.lib.json" + "path": "../action-reminder-modal/tsconfig.lib.json" }, { "path": "../../soundboard/tsconfig.lib.json" }, + { + "path": "../../tauri-api/tsconfig.lib.json" + }, { "path": "../gallery-modal/tsconfig.lib.json" }, @@ -78,6 +78,9 @@ }, { "path": "../../common/tsconfig.lib.json" + }, + { + "path": "../toaster/tsconfig.lib.json" } ] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b50971b79..66353492cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,8 @@ "react-device-detect": "^2.2.3", "react-dom": "18.3.1", "react-hot-toast": "^2.5.2", - "react-router-dom": "6.29.0" + "react-router-dom": "6.29.0", + "zustand": "^5.0.5" }, "devDependencies": { "@babel/core": "^7.14.5", @@ -1385,6 +1386,10 @@ "name": "@storyteller/ui-modal", "version": "0.0.1" }, + "libs/components/model-selector": { + "name": "@storyteller/ui-model-selector", + "version": "0.0.1" + }, "libs/components/pagination": { "name": "@storyteller/ui-pagination", "version": "0.0.1" @@ -9263,6 +9268,10 @@ "resolved": "libs/components/modal", "link": true }, + "node_modules/@storyteller/ui-model-selector": { + "resolved": "libs/components/model-selector", + "link": true + }, "node_modules/@storyteller/ui-pagination": { "resolved": "libs/components/pagination", "link": true @@ -28141,6 +28150,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 45676c25aa..4ff7efe96d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,8 @@ "react-device-detect": "^2.2.3", "react-dom": "18.3.1", "react-hot-toast": "^2.5.2", - "react-router-dom": "6.29.0" + "react-router-dom": "6.29.0", + "zustand": "^5.0.5" }, "devDependencies": { "@babel/core": "^7.14.5", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 35778c31ba..a373b2d78e 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -131,6 +131,9 @@ }, { "path": "./libs/components/action-reminder-modal" + }, + { + "path": "./libs/components/model-selector" } ] }