Compare commits

...
15 changed files with 787 additions and 82 deletions
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 />}
+143
View File
@@ -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,
};