mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01fe6cd0e3 | ||
|
|
fd21e37d69 | ||
|
|
45b5f0ed04 |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 341 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 462 KiB |
@@ -0,0 +1,45 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ModelButton } from "~/components/features/ModelSelection/ModelButton";
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { selectedModel, selectedLora } from "~/signals/uiEvents/modelSelection";
|
||||
import { getModelById, getLoraById } from "~/data/models";
|
||||
|
||||
export const SignaledModelButtons = () => {
|
||||
// This makes the component re-render when the selected model or lora changes
|
||||
useSignalEffect(() => {
|
||||
selectedModel.value;
|
||||
selectedLora.value;
|
||||
});
|
||||
|
||||
// Get the selected model's image or use default
|
||||
const selectedModelData = selectedModel.value
|
||||
? getModelById(selectedModel.value)
|
||||
: null;
|
||||
const modelIconSrc = selectedModelData?.imageUrl || "";
|
||||
|
||||
// Get the selected LoRA's image or use default
|
||||
const selectedLoraData = selectedLora.value
|
||||
? getLoraById(selectedLora.value)
|
||||
: null;
|
||||
const loraIconSrc = selectedLoraData?.imageUrl || "";
|
||||
|
||||
return (
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-2")}>
|
||||
<ModelButton
|
||||
iconSrc={modelIconSrc}
|
||||
iconAlt={selectedModelData?.name || "Model"}
|
||||
label={selectedModelData?.name || "Realistic"}
|
||||
subtitle="Model"
|
||||
type="model"
|
||||
/>
|
||||
|
||||
<ModelButton
|
||||
iconSrc={loraIconSrc}
|
||||
iconAlt={selectedLoraData?.name || "LoRA"}
|
||||
label={selectedLoraData?.name || "Detailed Tweaker"}
|
||||
subtitle="LoRA"
|
||||
type="lora"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DialogTitle } from "@headlessui/react";
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { ModelCard } from "./ModelCard";
|
||||
import { loras, isLoraCompatibleWithModel } from "~/data/models";
|
||||
import {
|
||||
selectedModel,
|
||||
selectedLora,
|
||||
dispatchers,
|
||||
} from "~/signals/uiEvents/modelSelection";
|
||||
|
||||
interface LoraSelectionDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const LoraSelectionDialog = ({ onClose }: LoraSelectionDialogProps) => {
|
||||
// This makes component re-render whenever signals change
|
||||
useSignalEffect(() => {
|
||||
selectedModel.value;
|
||||
selectedLora.value;
|
||||
});
|
||||
|
||||
const handleSelectLora = (loraId: string) => {
|
||||
dispatchers.setSelectedLora(loraId);
|
||||
onClose(); // Close the dialog after selection
|
||||
};
|
||||
|
||||
const currentModelId = selectedModel.value;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<DialogTitle className="text-2xl font-bold">Select LoRA</DialogTitle>
|
||||
<p className="text-sm text-gray-400">
|
||||
Pick a LoRA to enhance your selected model
|
||||
{!currentModelId && (
|
||||
<span className="ml-1 text-yellow-500">
|
||||
(Select a base model first)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{loras.map((lora) => (
|
||||
<ModelCard
|
||||
key={lora.id}
|
||||
id={lora.id}
|
||||
name={lora.name}
|
||||
imageUrl={lora.imageUrl}
|
||||
isDownloaded={lora.isDownloaded}
|
||||
isSelected={selectedLora.value === lora.id}
|
||||
isCompatible={
|
||||
currentModelId
|
||||
? isLoraCompatibleWithModel(lora.id, currentModelId)
|
||||
: false
|
||||
}
|
||||
type="lora"
|
||||
onSelect={handleSelectLora}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { BaseDialog } from "~/components/ui/BaseDialog";
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { selectedModel, selectedLora } from "~/signals/uiEvents/modelSelection";
|
||||
import { getModelById, getLoraById } from "~/data/models";
|
||||
import { ModelSelectionDialog } from "./ModelSelectionDialog";
|
||||
import { LoraSelectionDialog } from "./LoraSelectionDialog";
|
||||
|
||||
interface ModelButtonProps {
|
||||
iconSrc: string;
|
||||
iconAlt: string;
|
||||
label?: string;
|
||||
subtitle?: string;
|
||||
type: "model" | "lora";
|
||||
}
|
||||
|
||||
export const ModelButton = ({
|
||||
iconSrc,
|
||||
iconAlt,
|
||||
label,
|
||||
subtitle,
|
||||
type,
|
||||
}: ModelButtonProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Update button label based on selection
|
||||
useSignalEffect(() => {
|
||||
// This will re-render component when these signals change
|
||||
selectedModel.value;
|
||||
selectedLora.value;
|
||||
});
|
||||
|
||||
// Set button text based on current selection
|
||||
let buttonLabel = label;
|
||||
let buttonSubtitle = subtitle;
|
||||
|
||||
if (type === "model" && selectedModel.value) {
|
||||
const model = getModelById(selectedModel.value);
|
||||
if (model) {
|
||||
buttonLabel = model.name;
|
||||
buttonSubtitle = "Model";
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "lora" && selectedLora.value) {
|
||||
const lora = getLoraById(selectedLora.value);
|
||||
if (lora) {
|
||||
buttonLabel = lora.name;
|
||||
buttonSubtitle = "LoRA";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="glass flex w-40 items-center justify-start gap-2 bg-ui-controls/70 p-1"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<img
|
||||
src={iconSrc}
|
||||
alt={iconAlt}
|
||||
className="h-8 w-8 rounded-md bg-white/10"
|
||||
/>
|
||||
{buttonSubtitle ? (
|
||||
<div className="flex flex-col gap-0 truncate text-start">
|
||||
<span className="truncate text-[11px] opacity-60">
|
||||
{buttonSubtitle}
|
||||
</span>
|
||||
<span className="truncate text-[13px] opacity-80">
|
||||
{buttonLabel}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="truncate">{buttonLabel}</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<BaseDialog
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="max-w-3xl"
|
||||
>
|
||||
{type === "model" ? (
|
||||
<ModelSelectionDialog onClose={() => setIsOpen(false)} />
|
||||
) : (
|
||||
<LoraSelectionDialog onClose={() => setIsOpen(false)} />
|
||||
)}
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import { faDownToLine } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
downloadingItems,
|
||||
dispatchers,
|
||||
} from "~/signals/uiEvents/modelSelection";
|
||||
import { downloadModel } from "~/services/modelDownloadService";
|
||||
|
||||
interface ModelCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
isDownloaded: boolean;
|
||||
isSelected: boolean;
|
||||
isCompatible: boolean;
|
||||
type: "model" | "lora";
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ModelCard = ({
|
||||
id,
|
||||
name,
|
||||
imageUrl,
|
||||
isDownloaded,
|
||||
isSelected,
|
||||
isCompatible,
|
||||
type,
|
||||
onSelect,
|
||||
}: ModelCardProps) => {
|
||||
// Local state to track download status and progress
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [localIsDownloaded, setLocalIsDownloaded] = useState(isDownloaded);
|
||||
|
||||
// Update local state when downloadingItems changes
|
||||
useSignalEffect(() => {
|
||||
const downloads = downloadingItems.value;
|
||||
const isCurrentlyDownloading = !!downloads[id];
|
||||
setIsDownloading(isCurrentlyDownloading);
|
||||
|
||||
if (isCurrentlyDownloading) {
|
||||
setProgress(downloads[id].progress);
|
||||
}
|
||||
});
|
||||
|
||||
// Function to handle the download
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
// Use the download service to start the download
|
||||
await downloadModel(id, type);
|
||||
|
||||
// For the dummy implementation, we'll update the local state when download completes
|
||||
const checkDownloadStatus = setInterval(() => {
|
||||
if (!dispatchers.isDownloading(id)) {
|
||||
clearInterval(checkDownloadStatus);
|
||||
setLocalIsDownloaded(true);
|
||||
}
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ${type} ${id}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isCompatible) return; // Do nothing if not compatible
|
||||
|
||||
if (isDownloading) return; // Do nothing if already downloading
|
||||
|
||||
if (localIsDownloaded) {
|
||||
onSelect(id);
|
||||
} else {
|
||||
handleDownload();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative flex aspect-square flex-col items-center overflow-hidden rounded-xl border-2 bg-white/5 transition-all",
|
||||
isSelected ? "border-primary" : "border-transparent",
|
||||
!isCompatible ? "opacity-50 grayscale" : "hover:border-primary",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Selected indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute right-2 top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image container */}
|
||||
<div className="relative h-full w-full overflow-hidden">
|
||||
{/* Downloading indicator */}
|
||||
{isDownloading && (
|
||||
<div className="absolute inset-0 flex h-full w-full flex-col items-center justify-center bg-gray-900/75">
|
||||
<svg
|
||||
className="h-8 w-8 animate-spin text-white/40"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path
|
||||
d="M32 3C35.8083 3 39.5794 3.75011 43.0978 5.20749C46.6163 6.66488 49.8132 8.80101 52.5061 11.4939C55.199 14.1868 57.3351 17.3837 58.7925 20.9022C60.2499 24.4206 61 28.1917 61 32C61 35.8083 60.2499 39.5794 58.7925 43.0978C57.3351 46.6163 55.199 49.8132 52.5061 52.5061C49.8132 55.199 46.6163 57.3351 43.0978 58.7925C39.5794 60.2499 35.8083 61 32 61C28.1917 61 24.4206 60.2499 20.9022 58.7925C17.3837 57.3351 14.1868 55.199 11.4939 52.5061C8.801 49.8132 6.66487 46.6163 5.20749 43.0978C3.7501 39.5794 3 35.8083 3 32C3 28.1917 3.75011 24.4206 5.2075 20.9022C6.66489 17.3837 8.80101 14.1868 11.4939 11.4939C14.1868 8.80099 17.3838 6.66487 20.9022 5.20749C24.4206 3.7501 28.1917 3 32 3L32 3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M32 3C36.5778 3 41.0906 4.08374 45.1692 6.16256C49.2477 8.24138 52.7762 11.2562 55.466 14.9605C58.1558 18.6647 59.9304 22.9531 60.6448 27.4748C61.3591 31.9965 60.9928 36.6232 59.5759 40.9762"
|
||||
stroke="currentColor"
|
||||
strokeWidth="5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-white/90"
|
||||
></path>
|
||||
</svg>
|
||||
<div className="mt-2 text-center font-medium text-white">
|
||||
{progress}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img src={imageUrl} alt={name} className="h-full w-full object-cover" />
|
||||
|
||||
{/* Title */}
|
||||
<div className="absolute bottom-0 mt-2 flex w-full items-center justify-between bg-[#3E3E41]/70 px-2 py-1 text-sm font-medium backdrop-blur-[2px]">
|
||||
{name}
|
||||
{!localIsDownloaded && !isDownloading && (
|
||||
<FontAwesomeIcon icon={faDownToLine} className="text-white" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { DialogTitle } from "@headlessui/react";
|
||||
import { useSignalEffect } from "@preact/signals-react";
|
||||
import { ModelCard } from "./ModelCard";
|
||||
import { models } from "~/data/models";
|
||||
import { selectedModel, dispatchers } from "~/signals/uiEvents/modelSelection";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface ModelSelectionDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ModelSelectionDialog = ({
|
||||
onClose,
|
||||
}: ModelSelectionDialogProps) => {
|
||||
// This makes component re-render whenever signals change
|
||||
useSignalEffect(() => {
|
||||
selectedModel.value;
|
||||
});
|
||||
|
||||
// Ensure a model is selected when the dialog opens
|
||||
useEffect(() => {
|
||||
if (selectedModel.value === null && models.length > 0) {
|
||||
dispatchers.setSelectedModel(models[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectModel = (modelId: string) => {
|
||||
dispatchers.setSelectedModel(modelId);
|
||||
onClose(); // Close the dialog after selection
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<DialogTitle className="text-2xl font-bold">Select Model</DialogTitle>
|
||||
<p className="text-sm text-gray-400">Pick your base generation model</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{models.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
id={model.id}
|
||||
name={model.name}
|
||||
imageUrl={model.imageUrl}
|
||||
isDownloaded={model.isDownloaded}
|
||||
isSelected={selectedModel.value === model.id}
|
||||
isCompatible={true} // Base models are always compatible
|
||||
type="model"
|
||||
onSelect={handleSelectModel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,91 +1,11 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { useState } from "react";
|
||||
import { BaseDialog } from "~/components/ui/BaseDialog";
|
||||
import { DialogTitle } from "@headlessui/react";
|
||||
import { SignaledLoadingIndicator } from "~/KonvaRootComponent/SignaledLoadingIndicator";
|
||||
|
||||
interface ModelButtonProps {
|
||||
iconSrc: string;
|
||||
iconAlt: string;
|
||||
label?: string;
|
||||
subtitle?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ModelButton = ({
|
||||
iconSrc,
|
||||
iconAlt,
|
||||
label,
|
||||
subtitle,
|
||||
children,
|
||||
}: ModelButtonProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="glass flex w-40 items-center justify-start gap-2 bg-ui-controls/70 p-1"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<img
|
||||
src={iconSrc}
|
||||
alt={iconAlt}
|
||||
className="h-8 w-8 rounded-md bg-white/10"
|
||||
/>
|
||||
{subtitle ? (
|
||||
<div className="flex flex-col gap-0 truncate text-start">
|
||||
<span className="truncate text-[11px] opacity-60">{subtitle}</span>
|
||||
<span className="truncate text-[13px] opacity-80">{label}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="truncate">{label}</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<BaseDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
||||
{children}
|
||||
</BaseDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
import { SignaledModelButtons } from "~/KonvaRootComponent/SignaledModelButtons";
|
||||
|
||||
export const ToolbarTopCenter = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Model selection buttons (only shown in dev mode for now) */}
|
||||
{import.meta.env.DEV && (
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-2")}>
|
||||
<ModelButton
|
||||
iconSrc="/resources/icons/model.png"
|
||||
iconAlt="Model"
|
||||
label="Realistic"
|
||||
subtitle="Model"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Select Model
|
||||
</DialogTitle>
|
||||
<p>Model configuration options will go here</p>
|
||||
</div>
|
||||
</ModelButton>
|
||||
|
||||
<ModelButton
|
||||
iconSrc="/resources/icons/lora.png"
|
||||
iconAlt="LoRA"
|
||||
label="Detailed Tweaker"
|
||||
subtitle="LoRA"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Select LoRA
|
||||
</DialogTitle>
|
||||
<p>LoRA configuration options will go here</p>
|
||||
</div>
|
||||
</ModelButton>
|
||||
</div>
|
||||
)}
|
||||
{import.meta.env.DEV && <SignaledModelButtons />}
|
||||
|
||||
{/* Loading models indicator (only shown in dev mode for now) */}
|
||||
{import.meta.env.DEV && <SignaledLoadingIndicator />}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// Model and LoRA type definitions
|
||||
export interface ModelData {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
imageUrl: string;
|
||||
description?: string;
|
||||
isDownloaded: boolean;
|
||||
compatibleLoraIds?: string[]; // LoRAs that work with this model
|
||||
}
|
||||
|
||||
export interface LoraData {
|
||||
id: string;
|
||||
name: string;
|
||||
imageUrl: string;
|
||||
description?: string;
|
||||
isDownloaded: boolean;
|
||||
compatibleModelIds: string[]; // Models this LoRA works with
|
||||
}
|
||||
|
||||
// Available models dummy data
|
||||
export const models: ModelData[] = [
|
||||
{
|
||||
id: "realistic",
|
||||
name: "Realistic",
|
||||
type: "Realistic",
|
||||
imageUrl: "/images/models/realistic.png",
|
||||
description: "Photorealistic image generation",
|
||||
isDownloaded: true,
|
||||
compatibleLoraIds: [
|
||||
"detailed_tweaker",
|
||||
"scenery_enhancer",
|
||||
"portrait_plus",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "flat_2d",
|
||||
name: "Flat 2D",
|
||||
type: "Flat 2D",
|
||||
imageUrl: "/images/models/flat_2d.png",
|
||||
description: "Flat 2D illustration style",
|
||||
isDownloaded: true,
|
||||
compatibleLoraIds: ["toon_enhancer", "detailed_tweaker"],
|
||||
},
|
||||
{
|
||||
id: "ghibli",
|
||||
name: "Ghibli",
|
||||
type: "Ghibli",
|
||||
imageUrl: "/images/models/ghibli.png",
|
||||
description: "Studio Ghibli inspired style",
|
||||
isDownloaded: false,
|
||||
compatibleLoraIds: ["anime_style"],
|
||||
},
|
||||
{
|
||||
id: "scenery",
|
||||
name: "Scenery",
|
||||
type: "Scenery",
|
||||
imageUrl: "/images/models/scenery.png",
|
||||
description: "Beautiful landscape generation",
|
||||
isDownloaded: true,
|
||||
compatibleLoraIds: ["scenery_enhancer"],
|
||||
},
|
||||
{
|
||||
id: "concept",
|
||||
name: "Concept",
|
||||
type: "Concept",
|
||||
imageUrl: "/images/models/concept.png",
|
||||
description: "Abstract concept art generation",
|
||||
isDownloaded: false,
|
||||
compatibleLoraIds: ["detailed_tweaker", "scenery_enhancer"],
|
||||
},
|
||||
];
|
||||
|
||||
// Available LoRAs
|
||||
export const loras: LoraData[] = [
|
||||
{
|
||||
id: "detailed_tweaker",
|
||||
name: "Detailed Tweaker",
|
||||
imageUrl: "/resources/loras/detailedtweaker.jpg",
|
||||
description: "Enhances fine details in generated images",
|
||||
isDownloaded: true,
|
||||
compatibleModelIds: [
|
||||
"realistic",
|
||||
"concept",
|
||||
"anime",
|
||||
"flat_2d",
|
||||
"flux_realtime",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "portrait_plus",
|
||||
name: "Portrait+",
|
||||
imageUrl: "/resources/loras/portraitplus.jpg",
|
||||
description: "Improves portrait generation",
|
||||
isDownloaded: false,
|
||||
compatibleModelIds: ["realistic", "flux_realtime"],
|
||||
},
|
||||
{
|
||||
id: "scenery_enhancer",
|
||||
name: "Scenery Enhancer",
|
||||
imageUrl: "/resources/loras/sceneryenhancer.jpg",
|
||||
description: "Enhances landscape generation",
|
||||
isDownloaded: true,
|
||||
compatibleModelIds: ["realistic", "concept", "scenery"],
|
||||
},
|
||||
{
|
||||
id: "anime_style",
|
||||
name: "Anime Style+",
|
||||
imageUrl: "/resources/loras/animestyle.jpg",
|
||||
description: "Enhances anime art style",
|
||||
isDownloaded: false,
|
||||
compatibleModelIds: ["anime", "ghibli"],
|
||||
},
|
||||
{
|
||||
id: "toon_enhancer",
|
||||
name: "Toon Enhancer",
|
||||
imageUrl: "/resources/loras/toonenhancer.jpg",
|
||||
description: "Enhances cartoon styles",
|
||||
isDownloaded: true,
|
||||
compatibleModelIds: ["cartoon", "flat_2d"],
|
||||
},
|
||||
];
|
||||
|
||||
// Helper functions
|
||||
export const getModelById = (id: string): ModelData | undefined => {
|
||||
return models.find((model) => model.id === id);
|
||||
};
|
||||
|
||||
export const getLoraById = (id: string): LoraData | undefined => {
|
||||
return loras.find((lora) => lora.id === id);
|
||||
};
|
||||
|
||||
export const isLoraCompatibleWithModel = (
|
||||
loraId: string,
|
||||
modelId: string,
|
||||
): boolean => {
|
||||
const lora = getLoraById(loraId);
|
||||
return lora ? lora.compatibleModelIds.includes(modelId) : false;
|
||||
};
|
||||
|
||||
export const getCompatibleLorasForModel = (modelId: string): LoraData[] => {
|
||||
return loras.filter((lora) => lora.compatibleModelIds.includes(modelId));
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { dispatchers } from "~/signals/uiEvents/modelSelection";
|
||||
import { models, loras, ModelData, LoraData } from "~/data/models";
|
||||
|
||||
//THIS IS A VERY DUMMY DOWNLOAD IMPLEMENTATION AND SHOULD BE REPLACED WITH A REAL ONE FROM THE BACKEND OR SOMETHING BECAUSE IM NOT SURE - BFlat
|
||||
|
||||
// // Interface for download progress events from Tauri backend
|
||||
// interface DownloadProgressEvent {
|
||||
// id: string;
|
||||
// progress: number;
|
||||
// type: "model" | "lora";
|
||||
// }
|
||||
|
||||
// // Interface for download completion events from Tauri backend
|
||||
// interface DownloadCompleteEvent {
|
||||
// id: string;
|
||||
// type: "model" | "lora";
|
||||
// success: boolean;
|
||||
// error?: string;
|
||||
// }
|
||||
|
||||
// Initialize listeners for download events (dummy implementation)
|
||||
export const initializeDownloadListeners = async () => {
|
||||
console.log("Download listeners initialized (dummy implementation)");
|
||||
// No actual listeners in the dummy implementation
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// Start downloading a model or lora (dummy implementation)
|
||||
export const downloadModel = async (id: string, type: "model" | "lora") => {
|
||||
try {
|
||||
// Start tracking the download in our state
|
||||
dispatchers.startDownload(id, type);
|
||||
|
||||
// Simulate download progress
|
||||
simulateDownloadProgress(id, type);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Failed to start download for ${type} ${id}:`, error);
|
||||
dispatchers.finishDownload(id);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Simulate download progress
|
||||
const simulateDownloadProgress = (id: string, type: "model" | "lora") => {
|
||||
let progress = 0;
|
||||
|
||||
// Create an interval to update progress
|
||||
const interval = setInterval(() => {
|
||||
progress += 5;
|
||||
|
||||
// Update progress in state
|
||||
dispatchers.updateDownloadProgress(id, progress);
|
||||
|
||||
// When download completes
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
|
||||
// Finish the download
|
||||
setTimeout(() => {
|
||||
dispatchers.finishDownload(id);
|
||||
|
||||
// Update the local data to mark the model/lora as downloaded
|
||||
updateDownloadedState(id, type, true);
|
||||
|
||||
console.log(`Download completed for ${type} ${id}`);
|
||||
}, 500);
|
||||
}
|
||||
}, 200); // Update every 200ms
|
||||
};
|
||||
|
||||
// Update the downloaded state in our local data
|
||||
const updateDownloadedState = (
|
||||
id: string,
|
||||
type: "model" | "lora",
|
||||
isDownloaded: boolean,
|
||||
) => {
|
||||
if (type === "model") {
|
||||
// Find and update the model
|
||||
const modelIndex = models.findIndex((model) => model.id === id);
|
||||
if (modelIndex >= 0) {
|
||||
const updatedModel: ModelData = {
|
||||
...models[modelIndex],
|
||||
isDownloaded,
|
||||
};
|
||||
|
||||
// Update the models array
|
||||
models[modelIndex] = updatedModel;
|
||||
}
|
||||
} else {
|
||||
// Find and update the lora
|
||||
const loraIndex = loras.findIndex((lora) => lora.id === id);
|
||||
if (loraIndex >= 0) {
|
||||
const updatedLora: LoraData = {
|
||||
...loras[loraIndex],
|
||||
isDownloaded,
|
||||
};
|
||||
|
||||
// Update the loras array
|
||||
loras[loraIndex] = updatedLora;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get all available models (dummy implementation)
|
||||
export const fetchAvailableModels = async (): Promise<ModelData[]> => {
|
||||
// Just return the models from the data file
|
||||
return Promise.resolve([...models]);
|
||||
};
|
||||
|
||||
// Get all available loras (dummy implementation)
|
||||
export const fetchAvailableLoras = async (): Promise<LoraData[]> => {
|
||||
// Just return the loras from the data file
|
||||
return Promise.resolve([...loras]);
|
||||
};
|
||||
@@ -28,6 +28,14 @@ import {
|
||||
dispatchers as promptDispatchers,
|
||||
events as promptEvents,
|
||||
} from "./promptSettings";
|
||||
import {
|
||||
dispatchers as loadingIndicatorDispatchers,
|
||||
events as loadingIndicatorEvents,
|
||||
} from "./loadingIndicator";
|
||||
import {
|
||||
dispatchers as modelSelectionDispatchers,
|
||||
events as modelSelectionEvents,
|
||||
} from "./modelSelection";
|
||||
|
||||
export const uiEvents = {
|
||||
...addMediaEvents,
|
||||
@@ -39,6 +47,8 @@ export const uiEvents = {
|
||||
toolbarNode: toolbarNodeEvents,
|
||||
toolbarVideoExtraction: toolbarVideoExtractionEvents,
|
||||
promptEvents: promptEvents,
|
||||
loadingIndicator: loadingIndicatorEvents,
|
||||
modelSelection: modelSelectionEvents,
|
||||
};
|
||||
export const dispatchUiEvents = {
|
||||
...addMediaDispatchers,
|
||||
@@ -50,4 +60,6 @@ export const dispatchUiEvents = {
|
||||
toolbarNode: toolbarNodeDispatchers,
|
||||
toolbarVideoExtraction: toolbarVideoExtractionDispatchers,
|
||||
promptSettings: promptDispatchers,
|
||||
loadingIndicator: loadingIndicatorDispatchers,
|
||||
modelSelection: modelSelectionDispatchers,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { effect, signal } from "@preact/signals-react";
|
||||
import { models } from "~/data/models";
|
||||
|
||||
// Define the signals for selected models and download states
|
||||
export const selectedModel = signal<string | null>(
|
||||
models.length > 0 ? models[0].id : null,
|
||||
);
|
||||
export const selectedLora = signal<string | null>(null);
|
||||
|
||||
// Replace single download tracking with a map to track multiple downloads
|
||||
export interface DownloadInfo {
|
||||
progress: number;
|
||||
type: "model" | "lora";
|
||||
}
|
||||
|
||||
// Map of modelId/loraId to download progress
|
||||
export const downloadingItems = signal<Record<string, DownloadInfo>>({});
|
||||
|
||||
// Dispatchers
|
||||
const setSelectedModel = (modelId: string | null) => {
|
||||
selectedModel.value = modelId;
|
||||
};
|
||||
|
||||
const setSelectedLora = (loraId: string | null) => {
|
||||
selectedLora.value = loraId;
|
||||
};
|
||||
|
||||
// Start downloading a model or lora
|
||||
const startDownload = (id: string, type: "model" | "lora") => {
|
||||
const newDownloads = { ...downloadingItems.value };
|
||||
newDownloads[id] = { progress: 0, type };
|
||||
downloadingItems.value = newDownloads;
|
||||
};
|
||||
|
||||
// Update the progress of a downloading item
|
||||
const updateDownloadProgress = (id: string, progress: number) => {
|
||||
if (!downloadingItems.value[id]) return;
|
||||
|
||||
const newDownloads = { ...downloadingItems.value };
|
||||
newDownloads[id] = {
|
||||
...newDownloads[id],
|
||||
progress: Math.max(0, Math.min(100, Math.round(progress))),
|
||||
};
|
||||
downloadingItems.value = newDownloads;
|
||||
};
|
||||
|
||||
// Complete or cancel a download
|
||||
const finishDownload = (id: string) => {
|
||||
const newDownloads = { ...downloadingItems.value };
|
||||
delete newDownloads[id];
|
||||
downloadingItems.value = newDownloads;
|
||||
};
|
||||
|
||||
// Check if an item is currently downloading
|
||||
const isDownloading = (id: string): boolean => {
|
||||
return !!downloadingItems.value[id];
|
||||
};
|
||||
|
||||
// Get the progress of a downloading item
|
||||
const getDownloadProgress = (id: string): number => {
|
||||
return downloadingItems.value[id]?.progress || 0;
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
const onSelectedModelChanged = (callback: (modelId: string | null) => void) => {
|
||||
effect(() => {
|
||||
callback(selectedModel.value);
|
||||
});
|
||||
};
|
||||
|
||||
const onSelectedLoraChanged = (callback: (loraId: string | null) => void) => {
|
||||
effect(() => {
|
||||
callback(selectedLora.value);
|
||||
});
|
||||
};
|
||||
|
||||
const onDownloadsChanged = (
|
||||
callback: (downloads: Record<string, DownloadInfo>) => void,
|
||||
) => {
|
||||
effect(() => {
|
||||
callback(downloadingItems.value);
|
||||
});
|
||||
};
|
||||
|
||||
// EXPORTS
|
||||
export const dispatchers = {
|
||||
setSelectedModel,
|
||||
setSelectedLora,
|
||||
startDownload,
|
||||
updateDownloadProgress,
|
||||
finishDownload,
|
||||
isDownloading,
|
||||
getDownloadProgress,
|
||||
};
|
||||
|
||||
export const events = {
|
||||
onSelectedModelChanged,
|
||||
onSelectedLoraChanged,
|
||||
onDownloadsChanged,
|
||||
};
|
||||
Reference in New Issue
Block a user