Compare commits

...
23 changed files with 459 additions and 103 deletions
@@ -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 (
<div className="w-screen">
@@ -356,6 +375,7 @@ export const PageEditor = () => {
{display3d && (
<>
<OnboardingHelper />
<div
className="relative flex w-screen"
style={{ height: "calc(100vh - 68px)" }}
@@ -446,6 +466,18 @@ export const PageEditor = () => {
type="bricks"
message={editorLoader.value.message}
/>
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-2">
<ModelSelector
items={imageGenerationModels}
category={ModelCategory.Editor3D}
panelTitle="Select Model"
panelClassName="min-w-[280px]"
buttonClassName="bg-transparent p-0 text-lg hover:bg-transparent text-white/80 hover:text-white"
showIconsInList
triggerLabel="Model"
/>
</div>
</div>
</div>
</div>
@@ -477,7 +509,7 @@ export const PageEditor = () => {
)}
<LoginModal
onClose={() => { }}
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
@@ -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<HTMLDivElement>(null);
const [selectedModel, setSelectedModel] = useState<string>(
"GPT Image 1 (GPT-4o)",
);
const [useSystemPrompt, setUseSystemPrompt] = useState(false);
const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState<string>("");
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: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
selected: selectedModel === "GPT Image 1 (GPT-4o)",
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Flux Pro Ultra",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
selected: selectedModel === "Flux Pro Ultra",
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Recraft 3",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
selected: selectedModel === "Recraft 3",
description: "Fast and high-quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
];
const handleModelSelect = (item: PopoverItem) => {
setSelectedModel(item.label);
};
return (
<div
ref={containerRef}
@@ -73,13 +44,12 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
<PromptBoxImage
useJobContext={() => {
// 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) => {
<BackgroundGallery />
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-2">
<PopoverMenu
items={modelList}
onSelect={handleModelSelect}
<ModelSelector
items={imageGenerationModels}
category={ModelCategory.TextToImage}
mode="hoverSelect"
panelTitle="Select Model"
panelClassName="min-w-[280px]"
@@ -1,11 +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 { PromptBoxVideo } from "@storyteller/ui-promptbox";
import BackgroundGallery from "./BackgroundGallery";
import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover";
import { faFilm, faClock } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSignals } from "@preact/signals-react/runtime";
import {
ModelCategory,
ModelSelector,
useModelSelectorStore,
videoGenerationModels,
} from "@storyteller/ui-model-selector";
interface ImageToVideoProps {
imageMediaId?: string;
@@ -14,14 +16,10 @@ interface ImageToVideoProps {
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const [selectedModel, setSelectedModel] = useState<string>("Kling 2.0");
const [useSystemPrompt, setUseSystemPrompt] = useState(false);
const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState<string>("");
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: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
selected: selectedModel === "Kling 2.0",
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Runway",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
selected: selectedModel === "Runway",
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
];
const handleModelSelect = (item: PopoverItem) => {
setSelectedModel(item.label);
};
return (
<div
ref={containerRef}
@@ -73,7 +50,7 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
}}
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) => {
<BackgroundGallery />
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-2">
<PopoverMenu
items={modelList}
onSelect={handleModelSelect}
mode="hoverSelect"
<ModelSelector
items={videoGenerationModels}
category={ModelCategory.ImageToVideo}
panelTitle="Select Model"
panelClassName="min-w-[280px]"
buttonClassName="bg-transparent p-0 text-lg hover:bg-transparent text-white/80 hover:text-white"
+3
View File
@@ -28,6 +28,9 @@
"noEmit": true
},
"references": [
{
"path": "../../libs/components/model-selector"
},
{
"path": "../../libs/components/pagination"
},
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './lib/enums';
export * from './lib/interfaces';
export * from './lib/types';
export * from "./lib/enums";
export * from "./lib/interfaces";
export * from "./lib/types";
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-model-selector
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-model-selector` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-model-selector",
"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,4 @@
export * from "./lib/model-selector";
export * from "./lib/model-selector-store";
export * from "./lib/model-lists";
export * from "./lib/model-categories";
@@ -0,0 +1,10 @@
/**
* Defines the unique categories for each product or context where a model
* selection is required. Using an enum prevents typos and ensures type safety.
*/
export enum ModelCategory {
TextToImage = "text-to-image",
ImageToVideo = "image-to-video",
Canvas2D = "2d-canvas",
Editor3D = "3d-editor",
}
@@ -0,0 +1,51 @@
import type { PopoverItem } from "@storyteller/ui-popover";
import { faClock, faFilm, faImage } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export type ModelList = Omit<PopoverItem, "selected">[];
export const allModels = {
video: [
{
label: "Kling 2.0",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Runway",
icon: <FontAwesomeIcon icon={faFilm} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "2 min.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
],
image: [
{
label: "GPT Image 1 (GPT-4o)",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Slow, ultra instructive model",
badges: [{ label: "45 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Flux Pro Ultra",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "High quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Recraft 3",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "Fast and high-quality model",
badges: [{ label: "15 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
{
label: "Flux.1 Kontext",
icon: <FontAwesomeIcon icon={faImage} className="h-4 w-4" />,
description: "New model with advanced context",
badges: [{ label: "25 sec.", icon: <FontAwesomeIcon icon={faClock} /> }],
},
],
};
export const videoGenerationModels: ModelList = allModels.video;
export const imageGenerationModels: ModelList = allModels.image;
@@ -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<ModelSelectorState>((set) => ({
selectedModels: {},
setSelectedModel: (category, modelLabel) =>
set((state) => ({
selectedModels: {
...state.selectedModels,
[category]: modelLabel,
},
})),
}));
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import ModelSelector from './model-selector';
describe('ModelSelector', () => {
it('should render successfully', () => {
const { baseElement } = render(<ModelSelector />);
expect(baseElement).toBeTruthy();
});
});
@@ -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<PopoverItem, "selected">[];
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 (
<PopoverMenu
items={modelList}
onSelect={handleModelSelect}
mode="hoverSelect"
{...popoverProps}
/>
);
}
export default ModelSelector;
@@ -0,0 +1,16 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../popover"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.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"
}
]
}
@@ -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/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,
}
},
}));
@@ -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"
},
@@ -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"
}
]
}
+38 -1
View File
@@ -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",
+2 -1
View File
@@ -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",
+3
View File
@@ -131,6 +131,9 @@
},
{
"path": "./libs/components/action-reminder-modal"
},
{
"path": "./libs/components/model-selector"
}
]
}