mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 13:28:06 -05:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4027b873 | ||
|
|
d6c1eaae5d | ||
|
|
d6abe9fb88 | ||
|
|
1e758b1b6c | ||
|
|
cd40edb5f9 | ||
|
|
3838cc3f21 |
@@ -37,9 +37,6 @@
|
||||
"src/components/footer"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../libs/components/tutorial-modal/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/button/tsconfig.lib.json"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../libs/components/tutorial-modal"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/components/button"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,13 @@ import { TutorialModalButton } from "@storyteller/ui-tutorial-modal";
|
||||
export interface BaseSelectorImage {
|
||||
url: string;
|
||||
mediaToken: string;
|
||||
|
||||
// Link to the thumbnail *template* URL of the image
|
||||
// This is not a URL itself! It must be converted into a URL by replacing parameters.
|
||||
thumbnailUrlTemplate?: string;
|
||||
|
||||
// Link to the full image URL of the full image asset
|
||||
fullImageUrl?: string;
|
||||
}
|
||||
|
||||
export type BaseImageSelectorProps = {
|
||||
@@ -41,13 +48,13 @@ export const BaseImageSelector = ({
|
||||
setSelectedGalleryImages([]);
|
||||
};
|
||||
|
||||
const handleImageSelect = (id: string) => {
|
||||
const handleImageSelect = (mediaToken: string) => {
|
||||
// If already selected, deselect it
|
||||
// Else if not selected and under max limit, select it
|
||||
if (selectedGalleryImages.includes(id)) {
|
||||
if (selectedGalleryImages.includes(mediaToken)) {
|
||||
setSelectedGalleryImages([]);
|
||||
} else {
|
||||
setSelectedGalleryImages([id]);
|
||||
setSelectedGalleryImages([mediaToken]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,12 +63,14 @@ export const BaseImageSelector = ({
|
||||
if (selectedItems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = selectedItems[0];
|
||||
if (!item.fullImage) return;
|
||||
if (!item.fullImage) {
|
||||
return
|
||||
}
|
||||
const referenceImage: BaseSelectorImage = {
|
||||
url: item.fullImage,
|
||||
mediaToken: item.id,
|
||||
thumbnailUrlTemplate: item.thumbnailUrlTemplate,
|
||||
};
|
||||
sendImageEvent(referenceImage);
|
||||
};
|
||||
@@ -92,6 +101,7 @@ export const BaseImageSelector = ({
|
||||
// Attempt to resolve the CDN URL for the uploaded image token; fallback to data URL
|
||||
(async () => {
|
||||
let finalUrl = reader.result as string;
|
||||
let thumbnailUrlTemplate = undefined;
|
||||
try {
|
||||
const api = new MediaFilesApi();
|
||||
const result = await api.GetMediaFileByToken({
|
||||
@@ -102,6 +112,12 @@ export const BaseImageSelector = ({
|
||||
result.data.media_links?.cdn_url ||
|
||||
result.data.public_bucket_url ||
|
||||
finalUrl;
|
||||
// NB(bt,2025-10-09): I think `thumbnail_template` is wrong and that
|
||||
// `maybe_thumbnail_template` is the correct API field, but upstream
|
||||
// seems to imply otherwise. Let's simply check both keys for now.
|
||||
thumbnailUrlTemplate =
|
||||
result.data.media_links?.thumbnail_template ||
|
||||
(result.data.media_links as any)?.maybe_thumbnail_template;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
@@ -111,8 +127,10 @@ export const BaseImageSelector = ({
|
||||
}
|
||||
|
||||
const referenceImage: BaseSelectorImage = {
|
||||
url: finalUrl,
|
||||
mediaToken,
|
||||
url: finalUrl,
|
||||
fullImageUrl: finalUrl,
|
||||
thumbnailUrlTemplate: thumbnailUrlTemplate,
|
||||
};
|
||||
|
||||
toast.success("Image uploaded successfully!");
|
||||
|
||||
@@ -71,6 +71,8 @@ export const HistoryStack = ({
|
||||
({
|
||||
url: editedImage.cdn_url,
|
||||
mediaToken: editedImage.media_token,
|
||||
thumbnailUrlTemplate: editedImage.maybe_thumbnail_template,
|
||||
fullImageUrl: editedImage.cdn_url,
|
||||
}) as BaseSelectorImage,
|
||||
),
|
||||
};
|
||||
@@ -111,6 +113,18 @@ export const HistoryStack = ({
|
||||
prevPendingCountRef.current = current;
|
||||
}, [pendingPlaceholders.length]);
|
||||
|
||||
const getImageThumbnailSource = (image: BaseSelectorImage) => {
|
||||
if (!!image.thumbnailUrlTemplate) {
|
||||
// NB: this is the preferred way to populate the history stack thumbnails
|
||||
return image.thumbnailUrlTemplate.replace("{WIDTH}", "256");
|
||||
}
|
||||
if (image.url.startsWith("data:")) {
|
||||
return image.url;
|
||||
}
|
||||
console.warn("Using older image source for history stack image", image);
|
||||
return `${image.url}?historystack+${sessionRandBuster.current}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-auto w-20 rounded-lg">
|
||||
<div className="glass rounded-lg p-1.5">
|
||||
@@ -202,13 +216,10 @@ export const HistoryStack = ({
|
||||
onClick={() => handleSelectWithPreload(image)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
image.url.startsWith("data:")
|
||||
? image.url
|
||||
: `${image.url}?historystack+${sessionRandBuster.current}`
|
||||
}
|
||||
src={getImageThumbnailSource(image)}
|
||||
alt=""
|
||||
crossOrigin="anonymous"
|
||||
// NB(bt,2025-10-09): We shouldn't need CORS here.
|
||||
//crossOrigin="anonymous"
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 8080",
|
||||
"dev": "echo 'editor3d' && vite dev",
|
||||
"production-preview": "echo 'editor3d dev with production preview' && vite build && vite preview --port 5173",
|
||||
"staging-build": "vite build",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint playground/* .",
|
||||
"typecheck": "tsc",
|
||||
|
||||
@@ -156,9 +156,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../../libs/api"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/api-enums"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
{
|
||||
"path": "../tauri-utils"
|
||||
},
|
||||
{
|
||||
"path": "../api-enums"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../tauri-utils/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../api-enums/tsconfig.lib.json"
|
||||
}
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
@@ -47,6 +47,9 @@ export interface GalleryItem {
|
||||
id: string;
|
||||
label: string;
|
||||
thumbnail: string | null;
|
||||
// Thumbnail template is not a usable URL yet. It has some variables
|
||||
// like `{WIDTH}` which must be replaced downstream.
|
||||
thumbnailUrlTemplate?: string;
|
||||
fullImage?: string | null;
|
||||
createdAt: string;
|
||||
mediaClass?: string;
|
||||
@@ -266,6 +269,9 @@ export const GalleryModal = React.memo(
|
||||
"{WIDTH}",
|
||||
thumbnail_size.toString()
|
||||
),
|
||||
// TODO(bt): Thumbnail template URL may be wrong for videos and other asset types, but I'm
|
||||
// trying to plumb it through for the inpainting editor, which is just images.
|
||||
thumbnailUrlTemplate: item.media_links.maybe_thumbnail_template,
|
||||
fullImage: item.media_links.cdn_url,
|
||||
createdAt: item.created_at,
|
||||
mediaClass:
|
||||
|
||||
@@ -43,8 +43,9 @@ export const TEXT_TO_IMAGE_PAGE_MODEL_LIST : ModelList =
|
||||
const set : Set<Model> = new Set();
|
||||
set.add(IMAGE_MODELS_BY_ID.get("flux_pro_1_1")!); // Don't put Midjourney first!
|
||||
IMAGE_MODELS
|
||||
.filter((model) => !model.usesInpaintingMask) // We can't use masked inpainting models here
|
||||
.filter((model) => model.id !== "flux_pro_kontext_max") // NB: Flux Pro Kontext Max requires an input image
|
||||
.filter((model) => model.canTextToImage)
|
||||
//.filter((model) => !model.usesInpaintingMask) // We can't use masked inpainting models here
|
||||
//.filter((model) => model.id !== "flux_pro_kontext_max") // NB: Flux Pro Kontext Max requires an input image
|
||||
.forEach((m) => set.add(m));
|
||||
return Array.from(set);
|
||||
})(),
|
||||
|
||||
@@ -27,6 +27,10 @@ export class ImageModel extends Model {
|
||||
// Maximum number of image prompts that can be attached
|
||||
readonly maxImagePromptCount: number;
|
||||
|
||||
// Whether the model can be used for text-to-image
|
||||
// If true, it'll be displayed on the text-to-image page.
|
||||
readonly canTextToImage: boolean;
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
tauriId: string;
|
||||
@@ -42,6 +46,7 @@ export class ImageModel extends Model {
|
||||
usesInpaintingMask?: boolean;
|
||||
canUseImagePrompt?: boolean;
|
||||
maxImagePromptCount?: number;
|
||||
canTextToImage?: boolean;
|
||||
tags?: ModelTag[];
|
||||
}) {
|
||||
if (args.maxGenerationCount < 1) {
|
||||
@@ -62,5 +67,6 @@ export class ImageModel extends Model {
|
||||
this.usesInpaintingMask = args.usesInpaintingMask ?? false;
|
||||
this.canUseImagePrompt = args.canUseImagePrompt ?? false;
|
||||
this.maxImagePromptCount = Math.max(0, args.maxImagePromptCount ?? 1);
|
||||
this.canTextToImage = args.canTextToImage === false ? false : true; // Default to true !
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.Midjourney,
|
||||
selectorName: "Midjourney",
|
||||
selectorDescription: "Incredible style and quality",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorDescription: "Stunning style and quality",
|
||||
selectorBadges: ["45 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canUseImagePrompt: false,
|
||||
@@ -24,8 +24,8 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
selectorName: "Flux Pro 1.1 Ultra",
|
||||
selectorDescription: "High quality model",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorDescription: "Higher quality model",
|
||||
selectorBadges: ["35 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canUseImagePrompt: true,
|
||||
@@ -39,7 +39,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
selectorName: "Flux Pro 1.1",
|
||||
selectorDescription: "High quality model",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorBadges: ["25 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canUseImagePrompt: true,
|
||||
@@ -52,8 +52,8 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
selectorName: "Flux 1 Dev",
|
||||
selectorDescription: "High quality model",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorDescription: "Fast, but lower quality",
|
||||
selectorBadges: ["10 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canUseImagePrompt: true,
|
||||
@@ -66,8 +66,8 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
selectorName: "Flux 1 Schnell",
|
||||
selectorDescription: "High quality model",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorDescription: "Fastest image gen, but lowest quality",
|
||||
selectorBadges: ["10 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canUseImagePrompt: true,
|
||||
@@ -79,14 +79,15 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
fullName: "Gemini 2.5 Flash",
|
||||
category: "image",
|
||||
creator: ModelCreator.Google,
|
||||
selectorName: "Gemini 2.5 Flash",
|
||||
selectorDescription: "Ultra instructive model",
|
||||
selectorBadges: ["35 sec."],
|
||||
selectorName: "Nano Banana",
|
||||
selectorDescription: "Incredibly instructive editing",
|
||||
selectorBadges: ["25 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 1,
|
||||
canEditImages: true,
|
||||
canUseImagePrompt: true,
|
||||
maxImagePromptCount: 6,
|
||||
canTextToImage: false,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
}),
|
||||
new ImageModel({
|
||||
@@ -96,8 +97,8 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.OpenAi,
|
||||
selectorName: "GPT Image 1 (GPT-4o)",
|
||||
selectorDescription: "Slow, ultra instructive model",
|
||||
selectorBadges: ["45 sec."],
|
||||
selectorDescription: "Slow, but super smart",
|
||||
selectorBadges: ["60 sec."],
|
||||
maxGenerationCount: 1,
|
||||
defaultGenerationCount: 1,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
@@ -117,6 +118,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canEditImages: true,
|
||||
canTextToImage: false,
|
||||
tags: [ModelTag.InstructiveEdit],
|
||||
}),
|
||||
new ImageModel({
|
||||
@@ -132,6 +134,7 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
defaultGenerationCount: 1, // NB: Fal only allows one image for some reason!
|
||||
canEditImages: true,
|
||||
usesInpaintingMask: true,
|
||||
canTextToImage: false,
|
||||
}),
|
||||
new ImageModel({
|
||||
id: "flux_dev_juggernaut_inpaint",
|
||||
@@ -140,12 +143,13 @@ export const IMAGE_MODELS: ImageModel[] = [
|
||||
category: "image",
|
||||
creator: ModelCreator.BlackForestLabs,
|
||||
selectorName: "Flux Dev Juggernaut",
|
||||
selectorDescription: "Fast inpainting",
|
||||
selectorBadges: ["15 sec."],
|
||||
selectorDescription: "Fast inpainting, low quality",
|
||||
selectorBadges: ["10 sec."],
|
||||
maxGenerationCount: 4,
|
||||
defaultGenerationCount: 4,
|
||||
canEditImages: true,
|
||||
usesInpaintingMask: true,
|
||||
canTextToImage: false,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from "./lib/openai/SetOpenAIApiKey.js";
|
||||
export * from "./lib/providers/GetProviderOrder.js";
|
||||
export * from "./lib/providers/Provider.js";
|
||||
export * from "./lib/artcraft/task_queue/GetTaskQueue.js";
|
||||
export * from "./lib/artcraft/task_queue/MarkTaskAsDismissed.js";
|
||||
export * from "./lib/providers/SetProviderOrder.js";
|
||||
export * from "./lib/settings/GetAppPreferences.js";
|
||||
export * from "./lib/settings/UpdateAppPreference.js";
|
||||
|
||||
@@ -24,11 +24,32 @@ interface GetTaskQueueSuccess extends CommandResult {
|
||||
|
||||
export const GetTaskQueue = async () : Promise<GetTaskQueueResponse> => {
|
||||
try {
|
||||
let result = await invoke("get_task_queue_command") as GetTaskQueueSuccess;
|
||||
return result.payload;
|
||||
const result = await invoke("get_task_queue_command") as GetTaskQueueSuccess;
|
||||
|
||||
const tasks = result?.payload?.tasks || [];
|
||||
|
||||
// Convert timestamps to Date objects
|
||||
const newTasks : TaskQueueItem[] = tasks.map((task) => {
|
||||
return {
|
||||
id: task.id,
|
||||
task_status: task.task_status,
|
||||
task_type: task.task_type,
|
||||
model_type: task.model_type,
|
||||
provider: task.provider,
|
||||
provider_job_id: task.provider_job_id,
|
||||
created_at: new Date(task.created_at),
|
||||
updated_at: new Date(task.updated_at),
|
||||
completed_at: task.completed_at ? new Date(task.completed_at) : undefined,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tasks: newTasks,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary just for testing -
|
||||
(window as any).test_get_task_queue = GetTaskQueue;
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# This works on Linux and MacOS to launch the frontend dev server
|
||||
|
||||
root_dir=$(pwd)
|
||||
frontend_path="${root_dir}/frontend"
|
||||
|
||||
echo "Running Artcraft Frontend in Production Build + Preview Mode... (It won't hot reload!)"
|
||||
echo ""
|
||||
|
||||
echo "You'll need to launch the Rust dev server as a second script!"
|
||||
echo ""
|
||||
|
||||
# Kill any process running on port 5741, which will block startup
|
||||
if lsof -i tcp:5173 &>/dev/null; then
|
||||
lsof -i tcp:5173 -t | xargs kill -9
|
||||
echo "Killed process running on port 5173"
|
||||
else
|
||||
echo "No process running on port 5173"
|
||||
fi
|
||||
|
||||
pushd "${frontend_path}" || exit
|
||||
|
||||
npm install --verbose
|
||||
|
||||
export VITE_ENVIRONMENT_TYPE="production"
|
||||
|
||||
nx production-preview editor3d
|
||||
|
||||
popd || exit
|
||||
Reference in New Issue
Block a user