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"
}
]
}