mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 13:02:37 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a6268b3c | ||
|
|
2f1bc162f8 | ||
|
|
6261e796bf |
@@ -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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
+3
@@ -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 }),
|
||||
}));
|
||||
Reference in New Issue
Block a user