Compare commits

...
Author SHA1 Message Date
bflatastic 23a6268b3c persist reference images too 2025-07-16 01:12:03 +07:00
bflatastic 2f1bc162f8 prompt box session persistence 2025-07-16 01:01:35 +07:00
bflatastic 6261e796bf reset canvas button 2025-07-16 00:04:56 +07:00
11 changed files with 303 additions and 154 deletions
@@ -8,6 +8,8 @@ import {
faUndo,
faRedo,
faPaintBrush,
faXmark,
faTrashXmark,
} from "@fortawesome/pro-solid-svg-icons";
import {
faShapes,
@@ -22,6 +24,10 @@ import { hsvaToHex } from "@uiw/color-convert";
import SliderWithIndicator from "./SliderWithIndicator";
import { useSceneStore } from "../../stores/SceneState";
import { Tooltip } from "@storyteller/ui-tooltip";
import {
showActionReminder,
isActionReminderOpen,
} from "@storyteller/ui-action-reminder-modal";
const shapeIconBtn =
"flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-white/10";
@@ -313,6 +319,33 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
const baseBtn =
"relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors border-2 border-transparent";
// Reset canvas function with confirmation
const handleResetCanvas = () => {
showActionReminder({
reminderType: "default",
title: "Reset Canvas",
primaryActionIcon: faTrashXmark,
primaryActionBtnClassName: "bg-red hover:bg-red/80",
message: (
<p className="text-sm text-white/70">
Are you sure you want to reset the canvas? This will clear all your
work and cannot be undone.
</p>
),
primaryActionText: "Reset all",
onPrimaryAction: () => {
store.setNodes([]);
store.lineNodes.forEach((lineNode) => {
store.removeLineNode(lineNode.id, false);
});
store.selectNode(null);
store.copySelectedItems();
store.saveState();
isActionReminderOpen.value = false;
},
});
};
return (
<aside
className={`glass ml-4 flex flex-col items-center gap-3 rounded-xl p-1.5 shadow-lg ${className}`}
@@ -505,6 +538,26 @@ const SideToolbar: React.FC<SideToolbarProps> = ({
</div>
);
})}
{/* Reset canvas button */}
<div className="glass absolute -bottom-14 left-1/2 -translate-x-1/2 rounded-xl border-2 border-red/50 shadow-lg hover:border-red/80">
<div className="relative">
<Tooltip
content="Reset Canvas"
position="right"
closeOnClick={true}
className="ms-1 rounded-md bg-red px-3 py-1"
delay={100}
>
<button
className="flex h-10 w-10 items-center justify-center rounded-lg border-2 border-transparent text-white transition-colors hover:bg-red/50"
onClick={handleResetCanvas}
>
<FontAwesomeIcon icon={faXmark} className="h-5 w-5 text-xl" />
</button>
</Tooltip>
</div>
</div>
</aside>
);
};
@@ -131,6 +131,10 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
onSecondaryAction={currentReminderModalProps.onSecondaryAction}
isLoading={currentReminderModalProps.isLoading}
openAiLogo={currentReminderModalProps.openAiLogo}
primaryActionIcon={currentReminderModalProps.primaryActionIcon}
primaryActionBtnClassName={
currentReminderModalProps.primaryActionBtnClassName
}
/>
)}
@@ -2,6 +2,7 @@
import { signal } from "@preact/signals-react";
import { ReactNode } from "react";
import { ReminderType } from "@storyteller/ui-action-reminder-modal";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
export interface ShowActionReminderOptions {
reminderType: ReminderType;
@@ -13,6 +14,8 @@ export interface ShowActionReminderOptions {
onSecondaryAction?: () => void;
isLoading?: boolean;
openAiLogo?: string;
primaryActionIcon?: IconDefinition;
primaryActionBtnClassName?: string;
}
interface ActionReminderModalFullProps extends ShowActionReminderOptions {
@@ -2,6 +2,8 @@ import { Button } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import { ReactNode } from "react";
import { faRightToBracket } from "@fortawesome/pro-solid-svg-icons";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
export type ReminderType = "default" | "soraLogin" | "artcraftLogin";
@@ -20,6 +22,8 @@ interface ActionReminderModalProps {
isLoading?: boolean;
openAiLogo?: string;
modalClassName?: string;
primaryActionIcon?: IconDefinition;
primaryActionBtnClassName?: string;
}
export function ActionReminderModal({
@@ -35,13 +39,15 @@ export function ActionReminderModal({
secondaryActionText: customSecondaryActionText,
onSecondaryAction,
isLoading = false,
openAiLogo,
modalClassName: customModalClassName = "",
primaryActionIcon: customPrimaryActionIcon = faRightToBracket,
primaryActionBtnClassName: customPrimaryActionBtnClassName = "",
}: ActionReminderModalProps) {
let titleText: string | undefined;
let messageContent: ReactNode;
let primaryActionBtnText: string;
let primaryActionIcon = faRightToBracket;
let primaryActionBtnClassName = "";
if (!hideTitle) {
switch (reminderType) {
@@ -69,6 +75,8 @@ export function ActionReminderModal({
</p>
);
primaryActionBtnText = customPrimaryActionText || "Login with OpenAI";
primaryActionIcon = customPrimaryActionIcon || faRightToBracket;
primaryActionBtnClassName = customPrimaryActionBtnClassName || "";
break;
case "artcraftLogin":
messageContent = customMessage || (
@@ -78,6 +86,8 @@ export function ActionReminderModal({
</p>
);
primaryActionBtnText = customPrimaryActionText || "Login / Sign Up";
primaryActionIcon = customPrimaryActionIcon || faRightToBracket;
primaryActionBtnClassName = customPrimaryActionBtnClassName || "";
break;
default:
messageContent = customMessage || (
@@ -86,6 +96,8 @@ export function ActionReminderModal({
</p>
);
primaryActionBtnText = customPrimaryActionText || "Proceed";
primaryActionIcon = customPrimaryActionIcon || faRightToBracket;
primaryActionBtnClassName = customPrimaryActionBtnClassName || "";
break;
}
@@ -104,7 +116,7 @@ export function ActionReminderModal({
title={titleText}
className={finalModalClassName}
>
<div className="pt-2">
<div>
{customChildren ? (
<div className="space-y-4">{customChildren}</div>
) : (
@@ -117,7 +129,7 @@ export function ActionReminderModal({
loading={isLoading}
disabled={isLoading}
icon={primaryActionIcon}
className="w-full sm:w-auto"
className={twMerge("w-full sm:w-auto", primaryActionBtnClassName)}
>
{primaryActionBtnText}
</Button>
@@ -110,7 +110,7 @@ export function LightboxModal({
</div>
{/* info + actions */}
<div className="flex h-full flex-col py-5 pe-5">
<div className="flex h-full flex-col">
<div className="flex-1 space-y-4">
<div className="text-xl font-medium">
{title || "Image Generation"}
@@ -638,17 +638,18 @@ export const Modal = ({
const sizeRef = useRef<{ width: number; height: number } | null>(null);
const lastSizeRef = useRef<{ width: number; height: number } | null>(null);
// Capture initial size on first open
// Capture initial size on first open (only for resizable modals)
useEffect(() => {
if (isOpen && !size && modalRef.current) {
if (isOpen && !size && modalRef.current && resizable) {
const { width, height } = modalRef.current.getBoundingClientRect();
setSize({ width, height });
sizeRef.current = { width, height };
}
}, [isOpen, size]);
}, [isOpen, size, resizable]);
// Persist size across close / reopen
// Persist size across close / reopen (only for resizable modals)
useEffect(() => {
if (!resizable) return;
if (!isOpen) {
if (sizeRef.current) lastSizeRef.current = { ...sizeRef.current };
} else {
@@ -660,7 +661,7 @@ export const Modal = ({
sizeRef.current = { width, height };
}
}
}, [isOpen]);
}, [isOpen, resizable]);
// Bring to front when user interacts with modal (mouse down anywhere inside)
useEffect(() => {
@@ -809,9 +810,11 @@ export const Modal = ({
}
if (!draggable || !position) {
// If allowBackgroundInteraction, set pointerEvents: 'auto' for modal
// For regular modals, only apply size if resizable to prevent layout issues
return {
...(size ? { width: size.width, height: size.height } : {}),
...(resizable && size
? { width: size.width, height: size.height }
: {}),
...(allowBackgroundInteraction ? { pointerEvents: "auto" } : {}),
};
}
@@ -821,7 +824,7 @@ export const Modal = ({
top: position.y,
margin: 0,
zIndex,
...(size ? { width: size.width, height: size.height } : {}),
...(resizable && size ? { width: size.width, height: size.height } : {}),
...(allowBackgroundInteraction ? { pointerEvents: "auto" } : {}),
};
};
@@ -894,7 +897,7 @@ export const Modal = ({
<DialogTitle
as="div"
className={twMerge(
"mb-5 flex justify-between pb-0 text-xl font-bold text-white"
"mb-4 flex justify-between pb-0 text-xl font-bold text-white"
)}
>
<>
@@ -29,32 +29,26 @@ import {
faRectangle,
} from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { PromptsApi } from "@storyteller/api";
import {
SoraImageRemix,
SoraImageRemixAspectRatio,
CheckSoraSession,
SoraSessionState,
waitForSoraLogin,
// SoraImageRemix,
// SoraImageRemixAspectRatio,
// CheckSoraSession,
// SoraSessionState,
// waitForSoraLogin,
EnqueueContextualEditImage,
EnqueueContextualEditImageModel,
EnqueueContextualEditImageSize,
} from "@storyteller/tauri-api";
import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
import { invoke } from "@tauri-apps/api/core";
// import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
// import { invoke } from "@tauri-apps/api/core";
import { usePrompt2DStore, RefImage } from "./promptStore";
export type AspectRatio = "1:1" | "3:2" | "2:3";
interface ReferenceImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
interface PromptBox2DProps {
uploadImage: ({
title,
@@ -89,30 +83,36 @@ export const PromptBox2D = ({
const { addJobToken } = useJobContext();
//const { lastRenderedBitmap } = useCanvasSignal();
const [prompt, setPrompt] = useState("");
const prompt = usePrompt2DStore((s) => s.prompt);
const setPrompt = usePrompt2DStore((s) => s.setPrompt);
const useSystemPrompt = usePrompt2DStore((s) => s.useSystemPrompt);
const setUseSystemPrompt = usePrompt2DStore((s) => s.setUseSystemPrompt);
const aspectRatio = usePrompt2DStore((s) => s.aspectRatio);
const setAspectRatio = usePrompt2DStore((s) => s.setAspectRatio);
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[]
);
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const referenceImages = usePrompt2DStore((s) => s.referenceImages);
const setReferenceImages = usePrompt2DStore((s) => s.setReferenceImages);
const [uploadingImages, setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
{
label: "3:2",
selected: true,
selected: aspectRatio === "3:2",
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
{
label: "2:3",
selected: false,
selected: aspectRatio === "2:3",
icon: <FontAwesomeIcon icon={faRectangleVertical} className="h-4 w-4" />,
},
{
label: "1:1",
selected: false,
selected: aspectRatio === "1:1",
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
},
]);
@@ -131,6 +131,7 @@ export const PromptBox2D = ({
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
onAspectRatioChange?.(selectedItem.label as AspectRatio);
setAspectRatio(selectedItem.label as AspectRatio);
setAspectRatioList((prev) =>
prev.map((item) => ({
...item,
@@ -156,13 +157,13 @@ export const PromptBox2D = ({
progressCallback: (newState) => {
console.debug("Upload progress:", newState.data);
if (newState.status === UploaderStates.success && newState.data) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: reader.result as string,
file,
mediaToken: newState.data || "",
};
setReferenceImages((prev) => [...prev, referenceImage]);
setReferenceImages([...referenceImages, referenceImage]);
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
@@ -187,7 +188,7 @@ export const PromptBox2D = ({
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
setReferenceImages(referenceImages.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -217,16 +218,17 @@ export const PromptBox2D = ({
};
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
const newRefs = [...referenceImages];
selectedItems.forEach((item) => {
if (!item.fullImage) return;
const referenceImage: ReferenceImage = {
newRefs.push({
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
mediaToken: item.id,
};
setReferenceImages((prev) => [...prev, referenceImage]);
});
});
setReferenceImages(newRefs);
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
@@ -257,19 +259,19 @@ export const PromptBox2D = ({
};
// Helper to show Sora login reminder and wait for login
const handleSoraLoginReminder = async () => {
return new Promise<void>((resolve) => {
showActionReminder({
reminderType: "soraLogin",
onPrimaryAction: async () => {
await invoke("open_sora_login_command");
await waitForSoraLogin();
toast.success("Logged in to Sora!");
resolve();
},
});
});
};
// const handleSoraLoginReminder = async () => {
// return new Promise<void>((resolve) => {
// showActionReminder({
// reminderType: "soraLogin",
// onPrimaryAction: async () => {
// await invoke("open_sora_login_command");
// await waitForSoraLogin();
// toast.success("Logged in to Sora!");
// resolve();
// },
// });
// });
// };
const handleTauriEnqueue = async () => {
// NB(bt): This needs to move to an error handler.
@@ -428,7 +430,7 @@ export const PromptBox2D = ({
const getCurrentAspectRatioIcon = () => {
const selected = aspectRatioList.find((item) => item.selected);
if (!selected || !selected.icon) return faRectangle;
const iconElement = selected.icon as React.ReactElement<any, any>;
const iconElement = selected.icon as React.ReactElement<{ icon: IconProp }>;
return iconElement.props.icon;
};
@@ -22,10 +22,7 @@ import {
faRectangle,
faTableCellsLarge,
} from "@fortawesome/pro-regular-svg-icons";
import { invoke } from "@tauri-apps/api/core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PopoverItem, PopoverMenu } from "@storyteller/ui-popover";
import { Button, ToggleButton } from "@storyteller/ui-button";
import { Tooltip } from "@storyteller/ui-tooltip";
@@ -37,11 +34,9 @@ import {
FocalLengthDragging,
UploadImageArgs,
} from "@storyteller/common";
import { PromptsApi } from "@storyteller/api";
import { SoundRegistry } from "@storyteller/soundboard";
// import { SoundRegistry } from "@storyteller/soundboard";
import { toast } from "@storyteller/ui-toaster";
import { EngineApi } from "@storyteller/api";
import { CameraSettingsModal } from "@storyteller/ui-camera-settings-modal";
import { twMerge } from "tailwind-merge";
@@ -49,26 +44,14 @@ import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal";
import { Modal } from "@storyteller/ui-modal";
import { Signal } from "@preact/signals-react";
import {
CheckSoraSession,
CommandSuccessStatus,
EnqueueContextualEditImage,
EnqueueContextualEditImageModel,
EnqueueContextualEditImageSize,
GetAppPreferences,
SoraImageRemix,
SoraImageRemixAspectRatio,
SoraImageRemixErrorType,
SoraSessionState,
waitForSoraLogin,
// waitForSoraLogin,
} from "@storyteller/tauri-api";
import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
interface ReferenceImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
// import { showActionReminder } from "@storyteller/ui-action-reminder-modal";
import { usePrompt3DStore, RefImage } from "./promptStore";
interface PromptBox3DProps {
cameras: Signal<Camera[]>;
@@ -122,10 +105,13 @@ export const PromptBox3D = ({
const [content, setContent] = useState<React.ReactNode>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [prompt, setPrompt] = useState("");
const prompt = usePrompt3DStore((s) => s.prompt);
const setPrompt = usePrompt3DStore((s) => s.setPrompt);
const useSystemPrompt = usePrompt3DStore((s) => s.useSystemPrompt);
const setUseSystemPrompt = usePrompt3DStore((s) => s.setUseSystemPrompt);
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const referenceImages = usePrompt3DStore((s) => s.referenceImages);
const setReferenceImages = usePrompt3DStore((s) => s.setReferenceImages);
const [uploadingImages, setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
@@ -225,13 +211,13 @@ export const PromptBox3D = ({
progressCallback: (newState) => {
console.debug("Upload progress:", newState.data);
if (newState.status === UploaderStates.success && newState.data) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: reader.result as string,
file,
mediaToken: newState.data || "",
};
setReferenceImages((prev) => [...prev, referenceImage]);
setReferenceImages([...referenceImages, referenceImage]);
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
@@ -255,7 +241,7 @@ export const PromptBox3D = ({
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
setReferenceImages(referenceImages.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -284,16 +270,17 @@ export const PromptBox3D = ({
};
const handleGalleryImages = (selectedItems: GalleryItem[]) => {
const newRefs = [...referenceImages];
selectedItems.forEach((item) => {
if (!item.fullImage) return;
const referenceImage: ReferenceImage = {
newRefs.push({
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "gallery-image"),
mediaToken: item.id,
};
setReferenceImages((prev) => [...prev, referenceImage]);
});
});
setReferenceImages(newRefs);
setIsGalleryModalOpen(false);
setSelectedGalleryImages([]);
};
@@ -395,19 +382,19 @@ export const PromptBox3D = ({
};
// Helper to show Sora login reminder and wait for login
const handleSoraLoginReminder = async () => {
return new Promise<void>((resolve) => {
showActionReminder({
reminderType: "soraLogin",
onPrimaryAction: async () => {
await invoke("open_sora_login_command");
await waitForSoraLogin();
toast.success("Logged in to Sora!");
resolve();
},
});
});
};
// const handleSoraLoginReminder = async () => {
// return new Promise<void>((resolve) => {
// showActionReminder({
// reminderType: "soraLogin",
// onPrimaryAction: async () => {
// await invoke("open_sora_login_command");
// await waitForSoraLogin();
// toast.success("Logged in to Sora!");
// resolve();
// },
// });
// });
// };
const handleTauriEnqueue = async () => {
if (!prompt.trim()) return;
@@ -452,9 +439,7 @@ export const PromptBox3D = ({
const generateResponse = await EnqueueContextualEditImage({
model: EnqueueContextualEditImageModel.GptImage1,
scene_image_media_token: snapshotResult.data!,
image_media_tokens: referenceImages.map(
(image) => image.mediaToken
),
image_media_tokens: referenceImages.map((image) => image.mediaToken),
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
image_count: 1,
@@ -8,7 +8,6 @@ import { Button, ToggleButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import {
EnqueueTextToImage,
EnqueueTextToImageModel,
EnqueueTextToImageSize,
} from "@storyteller/tauri-api";
import {
@@ -26,16 +25,10 @@ import {
faRectangleVertical,
} from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { ModelInfo } from "@storyteller/model-list";
interface ReferenceImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
import { usePromptImageStore, RefImage } from "./promptStore";
interface PromptBoxImageProps {
useJobContext: () => JobContextType;
@@ -59,7 +52,7 @@ export const PromptBoxImage = ({
// for the image media id and url, we need to set the reference image gallery panel.
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
@@ -72,30 +65,35 @@ export const PromptBoxImage = ({
const [isModalOpen, setIsModalOpen] = useState(false);
const [content, setContent] = useState("");
const [prompt, setPrompt] = useState("");
const prompt = usePromptImageStore((s) => s.prompt);
const setPrompt = usePromptImageStore((s) => s.setPrompt);
const useSystemPrompt = usePromptImageStore((s) => s.useSystemPrompt);
const setUseSystemPrompt = usePromptImageStore((s) => s.setUseSystemPrompt);
const aspectRatio = usePromptImageStore((s) => s.aspectRatio);
const setAspectRatio = usePromptImageStore((s) => s.setAspectRatio);
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[]
);
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const [uploadingImages, setUploadingImages] = useState<
const referenceImages = usePromptImageStore((s) => s.referenceImages);
const setReferenceImages = usePromptImageStore((s) => s.setReferenceImages);
const [uploadingImages, _setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
{
label: "3:2",
selected: true,
selected: aspectRatio === "3:2",
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
{
label: "2:3",
selected: false,
selected: aspectRatio === "2:3",
icon: <FontAwesomeIcon icon={faRectangleVertical} className="h-4 w-4" />,
},
{
label: "1:1",
selected: false,
selected: aspectRatio === "1:1",
icon: <FontAwesomeIcon icon={faSquare} className="h-4 w-4" />,
},
]);
@@ -112,7 +110,7 @@ export const PromptBoxImage = ({
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
@@ -123,6 +121,7 @@ export const PromptBoxImage = ({
}, [imageMediaId, url]);
const handleAspectRatioSelect = (selectedItem: PopoverItem) => {
setAspectRatio(selectedItem.label as any);
setAspectRatioList((prev) =>
prev.map((item) => ({
...item,
@@ -132,7 +131,7 @@ export const PromptBoxImage = ({
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
setReferenceImages(referenceImages.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -163,7 +162,7 @@ export const PromptBoxImage = ({
const item = selectedItems[0];
if (!item || !item.fullImage) return;
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
@@ -224,7 +223,7 @@ export const PromptBoxImage = ({
const getCurrentResolutionIcon = () => {
const selected = aspectRatioList.find((item) => item.selected);
if (!selected || !selected.icon) return faRectangle;
const iconElement = selected.icon as React.ReactElement;
const iconElement = selected.icon as React.ReactElement<{ icon: IconProp }>;
return iconElement.props.icon;
};
@@ -241,19 +240,6 @@ export const PromptBoxImage = ({
}
};
const getModelByName = (name: string): EnqueueTextToImageModel => {
switch (name) {
case "Flux Pro Ultra":
return EnqueueTextToImageModel.FluxProUltra;
case "Recraft 3":
return EnqueueTextToImageModel.Recraft3;
case "GPT Image 1 (GPT-4o)":
return EnqueueTextToImageModel.GptImage1;
default:
return EnqueueTextToImageModel.GptImage1;
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
@@ -18,16 +18,11 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { faRectangle } from "@fortawesome/pro-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { ModelInfo } from "@storyteller/model-list";
interface ReferenceImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
import { usePromptVideoStore, RefImage } from "./promptStore";
interface PromptBoxVideoProps {
useJobContext: () => JobContextType;
@@ -51,7 +46,7 @@ export const PromptBoxVideo = ({
// for the image media id and url, we need to set the reference image gallery panel.
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
@@ -64,27 +59,30 @@ export const PromptBoxVideo = ({
console.log("Is this a desktop app?", IsDesktopApp());
const [isModalOpen, setIsModalOpen] = useState(false);
const [content, setContent] = useState("");
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
useJobContext();
const [prompt, setPrompt] = useState("");
const prompt = usePromptVideoStore((s) => s.prompt);
const setPrompt = usePromptVideoStore((s) => s.setPrompt);
const useSystemPrompt = usePromptVideoStore((s) => s.useSystemPrompt);
const setUseSystemPrompt = usePromptVideoStore((s) => s.setUseSystemPrompt);
const resolution = usePromptVideoStore((s) => s.resolution);
const setResolution = usePromptVideoStore((s) => s.setResolution);
const [isEnqueueing, setIsEnqueueing] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[]
);
const [referenceImages, setReferenceImages] = useState<ReferenceImage[]>([]);
const [uploadingImages, setUploadingImages] = useState<
const referenceImages = usePromptVideoStore((s) => s.referenceImages);
const setReferenceImages = usePromptVideoStore((s) => s.setReferenceImages);
const [uploadingImages, _setUploadingImages] = useState<
{ id: string; file: File }[]
>([]);
const [resolutionList, setResolutionList] = useState<PopoverItem[]>([
{
label: "720p",
selected: true,
selected: resolution === "720p",
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
{
label: "480p",
selected: false,
selected: resolution === "480p",
icon: <FontAwesomeIcon icon={faRectangle} className="h-4 w-4" />,
},
]);
@@ -101,7 +99,7 @@ export const PromptBoxVideo = ({
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
@@ -112,6 +110,7 @@ export const PromptBoxVideo = ({
}, [imageMediaId, url]);
const handleResolutionSelect = (selectedItem: PopoverItem) => {
setResolution(selectedItem.label as any);
setResolutionList((prev) =>
prev.map((item) => ({
...item,
@@ -121,7 +120,7 @@ export const PromptBoxVideo = ({
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
setReferenceImages(referenceImages.filter((img) => img.id !== id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -152,7 +151,7 @@ export const PromptBoxVideo = ({
const item = selectedItems[0];
if (!item || !item.fullImage) return;
const referenceImage: ReferenceImage = {
const referenceImage: RefImage = {
id: Math.random().toString(36).substring(7),
url: item.fullImage,
file: new File([], "library-image"),
@@ -211,7 +210,7 @@ export const PromptBoxVideo = ({
const getCurrentResolutionIcon = () => {
const selected = resolutionList.find((item) => item.selected);
if (!selected || !selected.icon) return faRectangle;
const iconElement = selected.icon as React.ReactElement;
const iconElement = selected.icon as React.ReactElement<{ icon: IconProp }>;
return iconElement.props.icon;
};
@@ -0,0 +1,102 @@
import { create } from "zustand";
export interface RefImage {
id: string;
url: string;
file: File;
mediaToken: string;
}
// ----- 2D Prompt Box Store -----
type AspectRatio = "3:2" | "2:3" | "1:1";
interface Prompt2DStore {
prompt: string;
aspectRatio: AspectRatio;
useSystemPrompt: boolean;
referenceImages: RefImage[];
setPrompt: (prompt: string) => void;
setAspectRatio: (ratio: AspectRatio) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
}
export const usePrompt2DStore = create<Prompt2DStore>()((set) => ({
prompt: "",
aspectRatio: "3:2",
useSystemPrompt: true,
referenceImages: [],
setPrompt: (prompt) => set({ prompt }),
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
}));
export { usePrompt2DStore as usePromptStore };
// ----- 3D Prompt Box Store -----
interface Prompt3DStore {
prompt: string;
useSystemPrompt: boolean;
referenceImages: RefImage[];
setPrompt: (prompt: string) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
}
export const usePrompt3DStore = create<Prompt3DStore>()((set) => ({
prompt: "",
useSystemPrompt: true,
referenceImages: [],
setPrompt: (prompt) => set({ prompt }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
}));
// ----- Image Prompt Box Store -----
interface PromptImageStore {
prompt: string;
aspectRatio: AspectRatio;
useSystemPrompt: boolean;
referenceImages: RefImage[];
setPrompt: (prompt: string) => void;
setAspectRatio: (ratio: AspectRatio) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
}
export const usePromptImageStore = create<PromptImageStore>()((set) => ({
prompt: "",
aspectRatio: "3:2",
useSystemPrompt: true,
referenceImages: [],
setPrompt: (prompt) => set({ prompt }),
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
}));
// ----- Video Prompt Box Store -----
type Resolution = "720p" | "480p";
interface PromptVideoStore {
prompt: string;
resolution: Resolution;
useSystemPrompt: boolean;
referenceImages: RefImage[];
setPrompt: (prompt: string) => void;
setResolution: (resolution: Resolution) => void;
setUseSystemPrompt: (value: boolean) => void;
setReferenceImages: (images: RefImage[]) => void;
}
export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
prompt: "",
resolution: "720p",
useSystemPrompt: true,
referenceImages: [],
setPrompt: (prompt) => set({ prompt }),
setResolution: (resolution) => set({ resolution }),
setUseSystemPrompt: (useSystemPrompt) => set({ useSystemPrompt }),
setReferenceImages: (referenceImages) => set({ referenceImages }),
}));