indexdb.put models list even without filehash, always put models files list into indexdb
This commit is contained in:
@@ -48,9 +48,6 @@ import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck";
|
||||
const AppIsDirtyEventListener = React.lazy(
|
||||
() => import("./topbar/AppIsDirtyEventListener"),
|
||||
);
|
||||
const ModelsListDrawer = React.lazy(
|
||||
() => import("./model-manager/models-list-drawer/ModelsListDrawer"),
|
||||
);
|
||||
|
||||
const usedWsEvents = [
|
||||
// InstallProgress.tsx
|
||||
|
||||
@@ -46,6 +46,7 @@ const ShareDialog = lazy(() => import("../share/ShareDialog"));
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import { TOPBAR_BUTTON_HEIGHT } from "../const";
|
||||
import DownloadSpaceJsonDialog from "../spacejson/DownloadSpaceJsonDialog";
|
||||
|
||||
export default function DropdownTitle() {
|
||||
const {
|
||||
@@ -126,12 +127,6 @@ export default function DropdownTitle() {
|
||||
URL.revokeObjectURL(url);
|
||||
}, [curFlowID]);
|
||||
|
||||
const handleDownloadSpaceJson = useCallback(async () => {
|
||||
console.log("app.graph", app.graph);
|
||||
const graph = app.graph.serialize();
|
||||
console.log("graph serialize", graph);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HoverMenu
|
||||
@@ -142,8 +137,8 @@ export default function DropdownTitle() {
|
||||
height={TOPBAR_BUTTON_HEIGHT + "px"}
|
||||
aria-label="menu"
|
||||
size={"sm"}
|
||||
// backgroundColor={"#4a4d6b"}
|
||||
backgroundColor={"teal.600"}
|
||||
backgroundColor={"#434554"}
|
||||
// backgroundColor={"teal.600"}
|
||||
>
|
||||
File
|
||||
<IconChevronDown size={20} />
|
||||
@@ -179,13 +174,13 @@ export default function DropdownTitle() {
|
||||
>
|
||||
Download
|
||||
</MenuItem>
|
||||
{/* <MenuItem
|
||||
onClick={handleDownloadSpaceJson}
|
||||
<MenuItem
|
||||
onClick={() => setRoute("downloadSpaceJson")}
|
||||
icon={<IconDownload size={20} />}
|
||||
iconSpacing={1}
|
||||
>
|
||||
Download .space.json
|
||||
</MenuItem> */}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => setRoute("saveAsModal")}
|
||||
icon={<IconDeviceFloppy size={20} />}
|
||||
@@ -226,6 +221,7 @@ export default function DropdownTitle() {
|
||||
{route == "versionHistory" && (
|
||||
<VersionHistoryDrawer onClose={() => setRoute("root")} />
|
||||
)}
|
||||
{route == "downloadSpaceJson" && <DownloadSpaceJsonDialog />}
|
||||
{route === "saveAsModal" && (
|
||||
<Modal isOpen={true} onClose={handleOnCloseModal}>
|
||||
<ModalOverlay />
|
||||
|
||||
@@ -93,6 +93,10 @@ export class UserSettingsTable extends TableBase<UserSettings> {
|
||||
await instance.getSetting("autoSave").then((res) => {
|
||||
instance._autoSave = res ?? true;
|
||||
});
|
||||
// overwrite legacy comfyspace.art
|
||||
await instance.upsert({
|
||||
cloudHost: instance.defaultSettings.cloudHost,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { getAllModelsList } from "../../Api";
|
||||
import type { ModelsListRespItem } from "../types";
|
||||
import type { ModelsListRespItem, ModelsListRespItemFromApi } from "../types";
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
|
||||
type ModelsListRespItemFromApi = ModelsListRespItem & { date: number };
|
||||
|
||||
export const useUpdateModels = () => {
|
||||
// all model types
|
||||
const [modelTypeList, setModelTypeList] = useState<string[]>(["checkpoints"]);
|
||||
@@ -20,7 +18,7 @@ export const useUpdateModels = () => {
|
||||
initData();
|
||||
api.addEventListener(
|
||||
"model_list",
|
||||
(e: { detail: ModelsListRespItemFromApi[] }) => {
|
||||
async (e: { detail: ModelsListRespItemFromApi[] }) => {
|
||||
updateModels(e.detail);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,24 +6,7 @@ import { Model } from "../../types/dbTypes";
|
||||
import type { DragEvent } from "react";
|
||||
|
||||
import { userSettingsTable } from "../../db-tables/WorkspaceDB";
|
||||
|
||||
interface ResponsePartial {
|
||||
id: number;
|
||||
modelId: number;
|
||||
name: string;
|
||||
model: {
|
||||
name: string;
|
||||
type: string;
|
||||
nsfw: boolean;
|
||||
};
|
||||
images: {
|
||||
url: string;
|
||||
nsfw: "None" | "Soft" | "Mature" | "X";
|
||||
width: number;
|
||||
height: number;
|
||||
hash: string;
|
||||
}[];
|
||||
}
|
||||
import { fetchCivitModelFromHashKey } from "../../utils/civitUtils";
|
||||
|
||||
interface Props {
|
||||
data: ModelsListRespItem;
|
||||
@@ -59,19 +42,8 @@ export function ModelItem({ data }: Props) {
|
||||
}
|
||||
if (data.file_hash != null) {
|
||||
try {
|
||||
const url = `https://civitai.com/api/v1/model-versions/by-hash/${data.file_hash}`;
|
||||
const resp = await fetch(url);
|
||||
const json: ResponsePartial = await resp.json();
|
||||
let image_url: string | undefined;
|
||||
const showNsfwThumbnail = await userSettingsTable?.getSetting(
|
||||
"showNsfwModelThumbnail",
|
||||
);
|
||||
if (showNsfwThumbnail === true) {
|
||||
image_url = json?.images?.[0]?.url;
|
||||
} else if (!json.model.nsfw) {
|
||||
const sfwImage = json.images.find((i) => i.nsfw === "None");
|
||||
image_url = sfwImage?.url;
|
||||
}
|
||||
const json = await fetchCivitModelFromHashKey(data.file_hash);
|
||||
const image_url = json.imageUrl;
|
||||
image_url && setUrl(image_url);
|
||||
|
||||
const newModel: Model = {
|
||||
@@ -79,9 +51,9 @@ export function ModelItem({ data }: Props) {
|
||||
fileHash: data.file_hash,
|
||||
fileFolder: data.model_type,
|
||||
fileName: data.model_name + data.model_extension,
|
||||
modelName: json.model.name,
|
||||
civitModelID: String(json.modelId),
|
||||
civitModelVersionID: String(json.id),
|
||||
modelName: json.modelName ?? null,
|
||||
civitModelID: json.civitModelID,
|
||||
civitModelVersionID: json.civitModelVersionID,
|
||||
imageUrl: image_url ?? null,
|
||||
};
|
||||
indexdb.models.put(newModel);
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Button, Stack } from "@chakra-ui/react";
|
||||
import { lazy, useEffect, DragEvent, useContext } from "react";
|
||||
import { lazy, useEffect, DragEvent, useContext, Suspense } from "react";
|
||||
import ModelsListDrawer from "../models-list-drawer/ModelsListDrawer";
|
||||
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import "./index.css";
|
||||
import { WorkspaceContext } from "../../WorkspaceContext";
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
|
||||
import { Model } from "../../types/dbTypes";
|
||||
import { fetchCivitModelFromHashKey } from "../../utils/civitUtils";
|
||||
import { indexdb } from "../../db-tables/indexdb";
|
||||
import type { ModelsListRespItemFromApi } from "../types";
|
||||
|
||||
const AddMissingModelsButton = lazy(
|
||||
() => import("./InstallMissingModelsButton"),
|
||||
@@ -31,6 +37,35 @@ export default function ModelManagerTopbar() {
|
||||
|
||||
useEffect(() => {
|
||||
app.canvasEl.addEventListener("drop", handleModelDrop);
|
||||
api.addEventListener(
|
||||
"model_list",
|
||||
async (e: { detail: ModelsListRespItemFromApi[] }) => {
|
||||
console.log("model_list", e.detail);
|
||||
const modelsPromises = e.detail?.map(async (item) => {
|
||||
let newModel: Model = {
|
||||
id: item.model_name + "@" + item.model_type,
|
||||
modelName: null,
|
||||
fileHash: item.file_hash ?? null,
|
||||
fileFolder: item.model_type,
|
||||
fileName: item.model_name + item.model_extension,
|
||||
};
|
||||
if (!item.file_hash) return newModel;
|
||||
const json = await fetchCivitModelFromHashKey(item.file_hash);
|
||||
newModel = {
|
||||
...newModel,
|
||||
modelName: json.modelName ?? null,
|
||||
civitModelID: json.civitModelID,
|
||||
civitModelVersionID: json.civitModelVersionID,
|
||||
imageUrl: json.imageUrl ?? null,
|
||||
};
|
||||
return newModel;
|
||||
});
|
||||
const models = (await Promise.all(modelsPromises)).filter(
|
||||
(model) => model != null,
|
||||
);
|
||||
indexdb.models.bulkPut(models);
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
app.canvasEl.removeEventListener("drop", handleModelDrop);
|
||||
};
|
||||
@@ -48,7 +83,9 @@ export default function ModelManagerTopbar() {
|
||||
Models
|
||||
</Button>
|
||||
<div style={{ position: "absolute", top: "38px", left: "0px" }}>
|
||||
<AddMissingModelsButton />
|
||||
<Suspense>
|
||||
<AddMissingModelsButton />
|
||||
</Suspense>
|
||||
</div>
|
||||
{route === "modelList" && (
|
||||
<ModelsListDrawer onClose={() => setRoute("root")} />
|
||||
|
||||
@@ -135,3 +135,5 @@ export interface ModelsListRespItem {
|
||||
preview?: string;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export type ModelsListRespItemFromApi = ModelsListRespItem & { date: number };
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
Stack,
|
||||
Tag,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
DepsResult,
|
||||
ModelFile,
|
||||
extractAndFetchFileNames,
|
||||
} from "./handleDownloadSpaceJson";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import {
|
||||
IconAlertCircle,
|
||||
IconEscalator,
|
||||
IconQuestionMark,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function DownloadSpaceJsonDialog() {
|
||||
const { route, setRoute } = useContext(WorkspaceContext);
|
||||
const [result, setResult] = useState<DepsResult>();
|
||||
useEffect(() => {
|
||||
console.log("app.graph", app.graph);
|
||||
const graph = app.graph.serialize();
|
||||
console.log("graph serialize", graph);
|
||||
extractAndFetchFileNames(graph.nodes ?? []).then((result) => {
|
||||
console.log(result);
|
||||
setResult(result);
|
||||
});
|
||||
}, []);
|
||||
if (!result) {
|
||||
return <div>Preparing...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={() => setRoute("root")} size={"xl"}>
|
||||
{/* <ModalOverlay /> */}
|
||||
<ModalContent>
|
||||
<ModalBody p={5}>
|
||||
<Stack gap={5}>
|
||||
<Heading size={"md"}>Workflow Resouce Dependencies</Heading>
|
||||
|
||||
<Stack>
|
||||
<Heading size={"sm"}>Models ({result.models.length})</Heading>
|
||||
{result.models.map((modelFile) => (
|
||||
<ModelDepsItem modelFile={modelFile} />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{result.models.length > 0 && (
|
||||
<Stack>
|
||||
<Heading size={"sm"}>Images ({result.images.length})</Heading>
|
||||
{result.images.map((image) => (
|
||||
<p key={image.filename}>{image.filename}</p>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelDepsItem({ modelFile }: { modelFile: ModelFile }) {
|
||||
if (!modelFile.models?.length) {
|
||||
return (
|
||||
<Stack>
|
||||
<Flex>
|
||||
<p>{modelFile.filename}</p>
|
||||
</Flex>
|
||||
<Input placeholder={"Please enter model download url"} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
if (modelFile.models.length === 1) {
|
||||
const model = modelFile.models[0];
|
||||
return (
|
||||
<Flex key={model.id} gap={2}>
|
||||
<Tag>{model.fileFolder}</Tag>
|
||||
<p>{modelFile.filename}</p>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack ml={2}>
|
||||
<p key={modelFile.filename}>{modelFile.filename}</p>
|
||||
{modelFile.models?.map((model) => (
|
||||
<Flex key={model.id} gap={2}>
|
||||
<Tag>{model.fileFolder}</Tag>
|
||||
<p>{model.modelName}</p>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { indexdb } from "../db-tables/indexdb";
|
||||
import { Model } from "../types/dbTypes";
|
||||
|
||||
type LiteNode = {
|
||||
id: number;
|
||||
type: string;
|
||||
widgets_values?: Array<string | number>;
|
||||
};
|
||||
|
||||
export type ModelFile = {
|
||||
filename: string;
|
||||
nodeType: string;
|
||||
models?: Model[];
|
||||
};
|
||||
|
||||
type ImageFile = {
|
||||
filename: string;
|
||||
nodeType: string;
|
||||
};
|
||||
|
||||
export type DepsResult = {
|
||||
models: ModelFile[];
|
||||
images: ImageFile[];
|
||||
};
|
||||
|
||||
async function fetchModelData(
|
||||
filename: string,
|
||||
nodeType: string,
|
||||
): Promise<ModelFile> {
|
||||
const res =
|
||||
(await indexdb.models.where("fileName").equals(filename).toArray()) ?? [];
|
||||
|
||||
const modelFile: ModelFile = { filename, nodeType, models: res };
|
||||
return modelFile;
|
||||
}
|
||||
|
||||
export async function extractAndFetchFileNames(
|
||||
nodes: LiteNode[],
|
||||
): Promise<DepsResult> {
|
||||
let modelPromises: Promise<ModelFile>[] = [];
|
||||
let images: ImageFile[] = [];
|
||||
const modelFileExtensions = [".safetensors", ".bin"];
|
||||
const imageFileExtensions = [".jpeg", ".jpg", ".png", ".gif"];
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node.widgets_values) {
|
||||
node.widgets_values.forEach((value) => {
|
||||
if (typeof value != "string") return;
|
||||
// Check if it's a model file
|
||||
if (modelFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||
modelPromises.push(fetchModelData(value, node.type));
|
||||
}
|
||||
// Check if it's an image file
|
||||
if (imageFileExtensions.some((ext) => value.endsWith(ext))) {
|
||||
images.push({ filename: value, nodeType: node.type });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all the model data fetch operations to complete
|
||||
const models = await Promise.all(modelPromises);
|
||||
|
||||
return { models, images };
|
||||
}
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
IconLock,
|
||||
} from "@tabler/icons-react";
|
||||
import DropdownTitle from "../components/DropdownTitle";
|
||||
import { lazy, useCallback, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
Suspense,
|
||||
lazy,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import EditFlowName from "../components/EditFlowName";
|
||||
import { WorkspaceContext } from "../WorkspaceContext";
|
||||
import { PanelPosition } from "../types/dbTypes";
|
||||
@@ -97,7 +104,9 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||
<IconTriangleInvertedFilled size={8} />
|
||||
</HStack>
|
||||
</Button>
|
||||
<ModelManagerTopbar />
|
||||
<Suspense fallback={<div style={{ width: "60px" }} />}>
|
||||
<ModelManagerTopbar />
|
||||
</Suspense>
|
||||
<Tooltip label="New workflow">
|
||||
<Button
|
||||
size={"sm"}
|
||||
|
||||
@@ -46,11 +46,11 @@ export type LocalCache = {
|
||||
export type Model = {
|
||||
id: string;
|
||||
fileName: string;
|
||||
modelName: string;
|
||||
fileFolder: string;
|
||||
fileHash: string;
|
||||
civitModelID: string;
|
||||
civitModelVersionID: string;
|
||||
fileHash: string | null; // files are not guaranteed to finish hashing
|
||||
modelName: string | null;
|
||||
civitModelID?: string;
|
||||
civitModelVersionID?: string;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export type WorkspaceRoute =
|
||||
| "gallery"
|
||||
| "versionHistory"
|
||||
| "saveAsModal"
|
||||
| "modelList";
|
||||
| "modelList"
|
||||
| "downloadSpaceJson";
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { userSettingsTable } from "../db-tables/WorkspaceDB";
|
||||
import { Model } from "../types/dbTypes";
|
||||
|
||||
const CIVIT_API_KEY_STORAGE_KEY = "WORKSPACE_CIVIT_API_KEY_STORAGE_KEY";
|
||||
export function getCivitApiKey() {
|
||||
return localStorage.getItem(CIVIT_API_KEY_STORAGE_KEY);
|
||||
@@ -6,3 +9,53 @@ export function getCivitApiKey() {
|
||||
export function setCivitApiKey(apiKey: string) {
|
||||
localStorage.setItem(CIVIT_API_KEY_STORAGE_KEY, apiKey);
|
||||
}
|
||||
|
||||
interface ResponsePartial {
|
||||
id: number;
|
||||
modelId: number;
|
||||
name: string;
|
||||
model: {
|
||||
name: string;
|
||||
type: string;
|
||||
nsfw: boolean;
|
||||
};
|
||||
images: {
|
||||
url: string;
|
||||
nsfw: "None" | "Soft" | "Mature" | "X";
|
||||
width: number;
|
||||
height: number;
|
||||
hash: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export async function fetchCivitModelFromHashKey(filehash: string): Promise<{
|
||||
modelName?: string;
|
||||
civitModelID?: string;
|
||||
civitModelVersionID?: string;
|
||||
imageUrl?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = `https://civitai.com/api/v1/model-versions/by-hash/${filehash}`;
|
||||
const resp = await fetch(url);
|
||||
const json: ResponsePartial = await resp.json();
|
||||
let image_url: string | undefined;
|
||||
const showNsfwThumbnail = await userSettingsTable?.getSetting(
|
||||
"showNsfwModelThumbnail",
|
||||
);
|
||||
if (showNsfwThumbnail === true) {
|
||||
image_url = json?.images?.[0]?.url;
|
||||
} else if (!json.model.nsfw) {
|
||||
const sfwImage = json.images.find((i) => i.nsfw === "None");
|
||||
image_url = sfwImage?.url;
|
||||
}
|
||||
|
||||
return {
|
||||
modelName: json.model.name,
|
||||
civitModelID: String(json.modelId),
|
||||
civitModelVersionID: String(json.id),
|
||||
imageUrl: image_url ?? undefined,
|
||||
};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user