Merge branch 'dev' into aller
This commit is contained in:
+40
-59
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import server
|
||||
from aiohttp import web
|
||||
import aiohttp
|
||||
@@ -43,16 +44,16 @@ async def save_db(request):
|
||||
json_data = data['json']
|
||||
|
||||
file_name = f'{db_dir_path}/{table}.json'
|
||||
if not os.path.exists(db_dir_path):
|
||||
os.makedirs(db_dir_path)
|
||||
|
||||
# Write the JSON data to the specified file
|
||||
with open(file_name, 'w') as file:
|
||||
file.write(json.dumps(json_data, indent=4))
|
||||
|
||||
# Offload file writing to a separate thread
|
||||
def write_json_string_to_db(file_name, json_data):
|
||||
if not os.path.exists(db_dir_path):
|
||||
os.makedirs(db_dir_path)
|
||||
# Write the JSON data to the specified file
|
||||
with open(file_name, 'w') as file:
|
||||
file.write(json.dumps(json_data, indent=4))
|
||||
await asyncio.to_thread(write_json_string_to_db, file_name, json_data)
|
||||
return web.Response(text=f"JSON saved to {file_name}")
|
||||
|
||||
|
||||
def read_table(table):
|
||||
if not table:
|
||||
return None
|
||||
@@ -69,44 +70,12 @@ def read_table(table):
|
||||
async def get_workspace(request):
|
||||
# Extract the table parameter from the query string
|
||||
table = request.query.get('table')
|
||||
data = read_table(table)
|
||||
data = await asyncio.to_thread(read_table, table)
|
||||
return web.json_response(data)
|
||||
|
||||
BACKUP_DIR = os.path.join(workspace_path, "backup")
|
||||
MAX_BACKUP_FILES = 20
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/workspace/save_backup")
|
||||
async def save_backup(request):
|
||||
try:
|
||||
data = await request.json()
|
||||
file_path = data.get('file_path')
|
||||
json_str = data.get('json_str')
|
||||
|
||||
file_path = os.path.join(BACKUP_DIR, file_path)
|
||||
if not file_path or not json_str:
|
||||
return web.Response(text=json.dumps({"error": "file_path and json_str are required"}), status=400)
|
||||
directory = os.path.dirname(file_path)
|
||||
# Create the directory if it does not exist
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
with open(file_path, 'w') as file:
|
||||
file.write(json_str)
|
||||
|
||||
# Check the number of files in the directory after writing the new file
|
||||
files = [f for f in os.listdir(directory) if os.path.isfile(
|
||||
os.path.join(directory, f))]
|
||||
if len(files) > MAX_BACKUP_FILES:
|
||||
# Find the oldest file (smallest filename)
|
||||
oldest_file = min(files, key=lambda x: x)
|
||||
# Delete the oldest file
|
||||
os.remove(os.path.join(directory, oldest_file))
|
||||
|
||||
return web.Response(text=json.dumps({"message": "File saved successfully"}), status=200)
|
||||
except Exception as e:
|
||||
return web.Response(text=json.dumps({"error": str(e)}), status=500)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/workspace/list_backup")
|
||||
async def list_backup(request):
|
||||
try:
|
||||
@@ -170,13 +139,17 @@ async def update_file(request):
|
||||
data = await request.json()
|
||||
file_path = data['file_path']
|
||||
json_str = data['json_str']
|
||||
my_workflows_dir = get_my_workflows_dir()
|
||||
full_path = os.path.join(my_workflows_dir, file_path)
|
||||
# Create the directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
|
||||
with open(full_path, 'w', encoding='utf-8') as file:
|
||||
file.write(json_str)
|
||||
def write_json_to_file(json_str):
|
||||
my_workflows_dir = get_my_workflows_dir()
|
||||
full_path = os.path.join(my_workflows_dir, file_path)
|
||||
# Create the directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, 'w', encoding='utf-8') as file:
|
||||
file.write(json_str)
|
||||
|
||||
# Offload the file update to a separate thread
|
||||
await asyncio.to_thread(write_json_to_file, json_str)
|
||||
return web.Response(text="File updated successfully")
|
||||
|
||||
|
||||
@@ -185,22 +158,30 @@ async def delete_file(request):
|
||||
data = await request.json()
|
||||
file_path = data['file_path']
|
||||
delete_empty_folder = data['deleteEmptyFolder']
|
||||
my_workflows_dir = get_my_workflows_dir()
|
||||
full_path = os.path.join(my_workflows_dir, file_path)
|
||||
|
||||
if os.path.exists(full_path):
|
||||
os.remove(full_path)
|
||||
def sync_delete_file(file_path, delete_empty_folder):
|
||||
my_workflows_dir = get_my_workflows_dir()
|
||||
full_path = os.path.join(my_workflows_dir, file_path)
|
||||
|
||||
# Check if the directory is empty after deleting the file
|
||||
directory = os.path.dirname(full_path)
|
||||
if delete_empty_folder and not os.listdir(directory):
|
||||
# If the directory is empty, remove the directory
|
||||
os.rmdir(directory)
|
||||
return web.Response(text="File and empty directory deleted successfully")
|
||||
if os.path.exists(full_path):
|
||||
os.remove(full_path)
|
||||
directory = os.path.dirname(full_path)
|
||||
if delete_empty_folder and not os.listdir(directory):
|
||||
# If the directory is empty, remove the directory
|
||||
os.rmdir(directory)
|
||||
return "File and empty directory deleted successfully"
|
||||
else:
|
||||
return "File deleted successfully"
|
||||
else:
|
||||
return web.Response(text="File deleted successfully")
|
||||
return "File not found"
|
||||
|
||||
# Run the synchronous file operation in a separate thread
|
||||
response_text = await asyncio.to_thread(sync_delete_file, file_path, delete_empty_folder)
|
||||
|
||||
if response_text == "File not found":
|
||||
return web.Response(text=response_text, status=404)
|
||||
else:
|
||||
return web.Response(text="File not found", status=404)
|
||||
return web.Response(text=response_text)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/workspace/rename_file")
|
||||
|
||||
@@ -17,8 +17,6 @@ export async function getDB(table: Table): Promise<string | undefined> {
|
||||
}
|
||||
|
||||
export async function saveDB(table: Table, jsonData: string) {
|
||||
// const tableBackupFile = table + "/" + Date.now() + ".json";
|
||||
// saveBackup(tableBackupFile, jsonData);
|
||||
try {
|
||||
const response = await fetch("/workspace/save_db", {
|
||||
method: "POST",
|
||||
@@ -73,25 +71,6 @@ export async function deleteFile(file_path: string, deleteEmptyFolder = false) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveBackup(file_path: string, jsonData: string) {
|
||||
try {
|
||||
const response = await fetch("/workspace/save_backup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_path: file_path,
|
||||
json_str: jsonData,
|
||||
}),
|
||||
});
|
||||
const result = await response.text();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("Error saving workspace backup:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listBackup(dir: string) {
|
||||
try {
|
||||
const response = await fetch("/workspace/list_backup", {
|
||||
|
||||
+34
-4
@@ -91,11 +91,16 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const setCurFlowIDAndName = (id: string, name: string) => {
|
||||
const setCurFlowIDAndName = (id: string | null, name: string) => {
|
||||
// curID null is when you deleted current workflow
|
||||
curFlowID.current = id;
|
||||
setFlowID(id);
|
||||
setCurFlowName(name);
|
||||
workflowsTable?.updateCurWorkflowID(id);
|
||||
if (id == null) {
|
||||
document.title = "ComfyUI";
|
||||
return;
|
||||
}
|
||||
if (getWorkflowIdInUrlHash()) {
|
||||
const newUrlHash = generateUrlHashWithFlowId(id);
|
||||
window.location.hash = newUrlHash;
|
||||
@@ -198,13 +203,19 @@ export default function App() {
|
||||
setRoute("root");
|
||||
};
|
||||
|
||||
const loadWorkflowID = (id: string) => {
|
||||
const loadWorkflowID = (id: string | null) => {
|
||||
// curID null is when you deleted current workflow
|
||||
if (id === null) {
|
||||
setCurFlowIDAndName(null, "");
|
||||
app.graph.clear();
|
||||
return;
|
||||
}
|
||||
const autoSaveEnabled = userSettingsTable?.getSetting("autoSave") ?? true;
|
||||
if (autoSaveEnabled || !isDirty) {
|
||||
loadWorkflowIDImpl(id);
|
||||
return;
|
||||
}
|
||||
showDialog(`Do you want to save the current workflow, ${curFlowName}?`, [
|
||||
showDialog(`Do you want to save the changes you made to, ${curFlowName}?`, [
|
||||
{
|
||||
label: "Open in new tab",
|
||||
icon: <IconExternalLink />,
|
||||
@@ -379,8 +390,27 @@ export default function App() {
|
||||
checkIsDirtyImpl(workflowsTable?.curWorkflow);
|
||||
if (!autoSaveEnabled && isDirty) {
|
||||
e.preventDefault(); // For modern browsers
|
||||
e.returnValue = "You have unsaved changes"; // For older browsers
|
||||
e.returnValue = "You have unsaved changes!"; // For older browsers
|
||||
}
|
||||
showDialog(
|
||||
`Please save or discard your changes before leaving, or your changes will be lost.`,
|
||||
[
|
||||
{
|
||||
label: "Save",
|
||||
colorScheme: "teal",
|
||||
onClick: () => {
|
||||
saveCurWorkflow();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Discard",
|
||||
colorScheme: "red",
|
||||
onClick: () => {
|
||||
discardUnsavedChanges();
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Flex,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useEffect, useState, useRef, useCallback, useContext } from "react";
|
||||
import {
|
||||
workflowsTable,
|
||||
isFolder,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
IconPlus,
|
||||
IconFolder,
|
||||
} from "@tabler/icons-react";
|
||||
import { RecentFilesContext } from "../WorkspaceContext";
|
||||
import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext";
|
||||
import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu";
|
||||
import { sortFileItem } from "../utils";
|
||||
import WorkflowListItem from "./WorkflowListItem";
|
||||
@@ -53,7 +53,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
>([]);
|
||||
const aloneFlowsAndFoldersRef = useRef<Array<Folder | Workflow>>([]);
|
||||
const allFlowsRef = useRef<Array<Workflow>>([]);
|
||||
|
||||
const { loadWorkflowID } = useContext(WorkspaceContext);
|
||||
const [selectedTag, setSelectedTag] = useState<string>();
|
||||
const [multipleState, setMultipleState] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
@@ -119,6 +119,9 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
const onDelete = useCallback(
|
||||
async (id: string) => {
|
||||
await workflowsTable?.deleteFlow(id);
|
||||
if (workflowsTable?.curWorkflow?.id === id) {
|
||||
loadWorkflowID?.(null);
|
||||
}
|
||||
await loadLatestWorkflows();
|
||||
},
|
||||
[selectedTag, debounceSearchValue],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Folder, Workflow } from "./types/dbTypes";
|
||||
export const WorkspaceContext = createContext<{
|
||||
curFlowID: string | null;
|
||||
onDuplicateWorkflow?: (flowID: string, newFlowName?: string) => void;
|
||||
loadWorkflowID: (id: string) => void;
|
||||
loadWorkflowID: (id: string | null) => void;
|
||||
saveCurWorkflow: () => void;
|
||||
discardUnsavedChanges: () => void;
|
||||
isDirty: boolean;
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function DropdownTitle({ onClick }: { onClick?: () => void }) {
|
||||
const [closeTimeoutId, setCloseTimeoutId] = useState<number>();
|
||||
|
||||
const delayedClose = () => {
|
||||
setCloseTimeoutId(setTimeout(() => setIsOpen(false), 200)); // delay of 500ms
|
||||
setCloseTimeoutId(setTimeout(() => setIsOpen(false), 300)); // delay of 300ms
|
||||
};
|
||||
|
||||
const onOpen = () => {
|
||||
@@ -142,7 +142,7 @@ export default function DropdownTitle({ onClick }: { onClick?: () => void }) {
|
||||
>
|
||||
Save
|
||||
</MenuItem>
|
||||
<Tooltip label="This will revert current workflow to your last saved version. You will lose all changes made since your last save.">
|
||||
<Tooltip label="Revert workflow to your last saved version. You will lose all changes made since your last save.">
|
||||
<MenuItem
|
||||
onClick={discardUnsavedChanges}
|
||||
icon={<IconArrowBackUpDouble size={20} />}
|
||||
|
||||
@@ -26,7 +26,11 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
const instance = new WorkflowsTable();
|
||||
return instance;
|
||||
}
|
||||
public updateCurWorkflowID(id: string) {
|
||||
public updateCurWorkflowID(id: string | null) {
|
||||
if (id == null) {
|
||||
this._curWorkflow = null;
|
||||
return;
|
||||
}
|
||||
this.get(id).then((w) => {
|
||||
this._curWorkflow = w ?? null;
|
||||
});
|
||||
@@ -88,13 +92,13 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
}
|
||||
//update indexdb
|
||||
await indexdb.workflows.update(id, newWorkflow);
|
||||
//update curWorkflow ram
|
||||
//update curWorkflow RAM
|
||||
if (this._curWorkflow && this._curWorkflow.id === id) {
|
||||
this._curWorkflow = newWorkflow;
|
||||
}
|
||||
await this.saveDiskDB();
|
||||
// save to my_workflows/
|
||||
if (input.name !== null || input.parentFolderID !== null) {
|
||||
if (input.name != null || input.parentFolderID != null) {
|
||||
// renamed file or moved file folder
|
||||
await deleteJsonFileMyWorkflows(before);
|
||||
await saveJsonFileMyWorkflows(after);
|
||||
|
||||
@@ -1,57 +1,81 @@
|
||||
import { Button, AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, Select } from "@chakra-ui/react";
|
||||
import {
|
||||
Button,
|
||||
AlertDialog,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
Select,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getAllFoldersList } from "../../Api";
|
||||
|
||||
interface ChooseFolderProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectFolder: (folderPath: string) => void;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectFolder: (folderPath: string) => void;
|
||||
}
|
||||
export default function ChooseFolder({ isOpen, onClose, selectFolder }: ChooseFolderProps) {
|
||||
const [folderPath, setFolderPath] = useState("");
|
||||
const [foldersList, setFoldersList] = useState<string[]>([]);
|
||||
export default function ChooseFolder({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectFolder,
|
||||
}: ChooseFolderProps) {
|
||||
const [folderPath, setFolderPath] = useState("");
|
||||
const [foldersList, setFoldersList] = useState<string[]>([]);
|
||||
|
||||
const cancelRef = useRef(null);
|
||||
const cancelRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
const initData = async () => {
|
||||
const folders_list = await getAllFoldersList();
|
||||
if (folders_list) setFoldersList(folders_list);
|
||||
};
|
||||
const initData = async () => {
|
||||
const folders_list = await getAllFoldersList();
|
||||
if (folders_list) {
|
||||
setFoldersList(
|
||||
folders_list.filter(
|
||||
(folderPath) =>
|
||||
!["custom_nodes", "config", "saved_prompts"].includes(folderPath),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
|
||||
Choose Folder
|
||||
</AlertDialogHeader>
|
||||
return (
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Choose Folder
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Select placeholder='Select option' value={folderPath} onChange={e => setFolderPath(e.target.value)}>
|
||||
{foldersList.map((folderPath) => (
|
||||
<option value={folderPath}>{folderPath}</option>
|
||||
))}
|
||||
</Select>
|
||||
</AlertDialogBody>
|
||||
<AlertDialogBody>
|
||||
<Select
|
||||
placeholder="Select option"
|
||||
value={folderPath}
|
||||
onChange={(e) => setFolderPath(e.target.value)}
|
||||
>
|
||||
{foldersList.map((folderPath) => (
|
||||
<option value={folderPath}>{folderPath}</option>
|
||||
))}
|
||||
</Select>
|
||||
</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => selectFolder(folderPath)} ml={3}>
|
||||
Confirm
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => selectFolder(folderPath)} ml={3}>
|
||||
Confirm
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import {
|
||||
Button,
|
||||
HStack,
|
||||
Text,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
IconButton,
|
||||
Heading,
|
||||
Checkbox,
|
||||
Spinner,
|
||||
useToast,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { CivitiModel, CivitiModelFileVersion } from "../types";
|
||||
import { installModelsApi } from "../api/modelsApi";
|
||||
import ModelCard from "./ModelCard";
|
||||
@@ -59,19 +55,23 @@ const MODEL_TYPE_TO_FOLDER_MAPPING: Record<MODEL_TYPE, string> = {
|
||||
Upscaler: "upscale_models",
|
||||
VAE: "vae",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onclose: () => void;
|
||||
searchQuery?: string;
|
||||
modelType?: MODEL_TYPE;
|
||||
}
|
||||
export default function InatallModelsModal({
|
||||
onclose,
|
||||
}: {
|
||||
onclose: () => void;
|
||||
}) {
|
||||
searchQuery: searchQueryProp = "",
|
||||
modelType: modelTypeProp,
|
||||
}: Props) {
|
||||
const [models, setModels] = useState<CivitiModel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modelType, setModelType] = useState<MODEL_TYPE | undefined>(
|
||||
"Checkpoint",
|
||||
);
|
||||
const [modelType, setModelType] = useState(modelTypeProp);
|
||||
const toast = useToast();
|
||||
const [installing, setInstalling] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState(searchQueryProp);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const file = useRef<CivitiModelFileVersion>();
|
||||
const loadData = useCallback(async () => {
|
||||
@@ -175,7 +175,7 @@ export default function InatallModelsModal({
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [searchQuery, modelType]);
|
||||
}, [modelType]);
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={true} onClose={onclose} blockScrollOnMount={true}>
|
||||
@@ -186,7 +186,11 @@ export default function InatallModelsModal({
|
||||
<Heading size={"md"} mr={2}>
|
||||
Models
|
||||
</Heading>
|
||||
<InstallModelSearchBar setSearchQuery={setSearchQuery} />
|
||||
<InstallModelSearchBar
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onSearch={loadData}
|
||||
/>
|
||||
<Button size={"sm"} py={1} mr={8} onClick={customUrlDownload}>
|
||||
Custom URL Install
|
||||
</Button>
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Flex, Input, Text } from "@chakra-ui/react";
|
||||
import { Button, Flex, Input } from "@chakra-ui/react";
|
||||
|
||||
export default function InstallModelSearchBar({
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
onSearch,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
onSearch: () => void;
|
||||
}) {
|
||||
const [text, setText] = useState("");
|
||||
// const debounceSearchValue = useDebounce(text, 1000);
|
||||
// useEffect(() => {
|
||||
// if (debounceSearchValue !== "") {
|
||||
// setSearchQuery(debounceSearchValue);
|
||||
// }
|
||||
// }, [debounceSearchValue]);
|
||||
return (
|
||||
<Flex gap={1} alignItems={"center"} grow={1}>
|
||||
<Input
|
||||
placeholder="Search models in CivitAI"
|
||||
width={"60%"}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyUp={(e) => {
|
||||
e.code === "Enter" && setSearchQuery(text);
|
||||
e.code === "Enter" && onSearch();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size={"sm"}
|
||||
onClick={() => setSearchQuery(text)}
|
||||
colorScheme="teal"
|
||||
>
|
||||
<Button size={"sm"} onClick={() => onSearch()} colorScheme="teal">
|
||||
Search
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function InstallModelsButton() {
|
||||
</Button>
|
||||
|
||||
{showInstallModels && (
|
||||
<InatallModelsModal onclose={() => setShowInstallModels(false)} />
|
||||
<InatallModelsModal modelType="Checkpoint" onclose={() => setShowInstallModels(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
import { HStack, Progress, Stack, Text, useColorMode, useToast } from "@chakra-ui/react";
|
||||
import {
|
||||
HStack,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
useColorMode,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Queue = { save_path: string, progress: number };
|
||||
type Queue = { save_path: string; progress: number };
|
||||
|
||||
export default function InstallProgress() {
|
||||
const { colorMode } = useColorMode();
|
||||
@@ -16,8 +23,7 @@ export default function InstallProgress() {
|
||||
});
|
||||
api.addEventListener("download_error", (e: { detail: string }) => {
|
||||
toast({
|
||||
title:
|
||||
"Download Error",
|
||||
title: "Download Error",
|
||||
description: e.detail,
|
||||
status: "error",
|
||||
duration: 4000,
|
||||
@@ -27,13 +33,32 @@ export default function InstallProgress() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack spacing={5} pos="absolute" bottom="0" left="0" width="50%" zIndex={80} backgroundColor={colorMode === "light" ? "white" : '#242424'} paddingX={5}>
|
||||
<Stack
|
||||
spacing={5}
|
||||
pos="absolute"
|
||||
bottom="0"
|
||||
left="0"
|
||||
width="50%"
|
||||
zIndex={80}
|
||||
backgroundColor={colorMode === "light" ? "white" : "#242424"}
|
||||
paddingX={5}
|
||||
>
|
||||
{queue.map(({ save_path, progress }) => (
|
||||
<HStack>
|
||||
<Text fontSize={16} width="40%">{save_path.replace(/^.*[\\/]/, '')}</Text>
|
||||
<Progress isIndeterminate={!progress} hasStripe width="60%" value={progress} />
|
||||
<Text fontSize={16} width="40%">
|
||||
{save_path.replace(/^.*[\\/]/, "")}
|
||||
</Text>
|
||||
<Progress
|
||||
isIndeterminate={!progress}
|
||||
hasStripe
|
||||
width="50%"
|
||||
value={progress}
|
||||
/>
|
||||
<Text fontSize={16} width="10%">
|
||||
{progress.toFixed(1)}%
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
Flex,
|
||||
Grid,
|
||||
GridItem,
|
||||
Heading,
|
||||
Portal,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
import InstallModelsButton from "../install-models/InstallModelsButton";
|
||||
import InatallModelsModal from "../install-models/InatallModelsModal";
|
||||
import { IconExternalLink } from "@tabler/icons-react";
|
||||
|
||||
export interface MissingModel {
|
||||
class_type: string;
|
||||
input_name: string;
|
||||
received_value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
missingModels: MissingModel[];
|
||||
}
|
||||
|
||||
export default function MissingModelsListDrawer({
|
||||
onClose,
|
||||
missingModels,
|
||||
}: Props) {
|
||||
const [showInstallModels, setShowInstallModels] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
app.canvasEl.addEventListener("click", onClose);
|
||||
return () => {
|
||||
app.canvasEl.removeEventListener("click", onClose);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const DRAWER_WIDTH = 440;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Portal>
|
||||
<Box style={{ width: DRAWER_WIDTH }}>
|
||||
<Card
|
||||
width={DRAWER_WIDTH}
|
||||
height={"100vh"}
|
||||
p={4}
|
||||
gap={2}
|
||||
position={"fixed"}
|
||||
top={0}
|
||||
left={0}
|
||||
shadow={"xl"}
|
||||
zIndex={1000}
|
||||
overflowY={"auto"}
|
||||
>
|
||||
<CardHeader>
|
||||
<Flex justifyContent={"space-between"} alignContent={"center"}>
|
||||
<Heading size={"md"} mr={2}>
|
||||
Models
|
||||
</Heading>
|
||||
<InstallModelsButton />
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<Grid templateColumns="1" gap={1} marginTop={2}>
|
||||
{missingModels.map((model) => {
|
||||
return (
|
||||
<GridItem p={3} shadow="md" borderWidth="1px">
|
||||
<VStack align="start">
|
||||
<Text>{model.class_type}</Text>
|
||||
<Text fontWeight="bold">Input:</Text>
|
||||
<Text>{model.input_name}</Text>
|
||||
<Text fontWeight="bold">Received Value:</Text>
|
||||
<Text color="red.400">{model.received_value}</Text>
|
||||
</VStack>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mt={5}
|
||||
iconSpacing={1}
|
||||
leftIcon={<IconExternalLink size={20} />}
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`https://civitai.com/search/models?sortBy=models_v5&query=${formatSearchQuery(model.received_value)}`,
|
||||
"_blank",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Search in CivitAI
|
||||
</Button>
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Box>
|
||||
</Portal>
|
||||
{showInstallModels && (
|
||||
<InatallModelsModal
|
||||
searchQuery={searchQuery}
|
||||
onclose={() => setShowInstallModels(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSearchQuery(query: string): string {
|
||||
// Remove file extension
|
||||
let formattedQuery = query
|
||||
.replace(/\.[^/.]+$/, "")
|
||||
// Replace special characters with space
|
||||
// .replace(/[^a-zA-Z0-9]/g, " ")
|
||||
// Add space before capital letters
|
||||
// .replace(/([A-Z])/g, " $1")
|
||||
.trim();
|
||||
|
||||
return formattedQuery;
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Box, Button, HStack } from "@chakra-ui/react";
|
||||
import { Button } from "@chakra-ui/react";
|
||||
// @ts-ignore
|
||||
import { app } from "/scripts/app.js";
|
||||
// @ts-ignore
|
||||
import { api } from "/scripts/api.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import MissingModelsListDrawer, {
|
||||
MissingModel,
|
||||
} from "../missing-models-drawer/MissingModelsListDrawer";
|
||||
interface Props {}
|
||||
|
||||
interface NodeError {
|
||||
@@ -22,9 +25,9 @@ interface NodeError {
|
||||
}
|
||||
|
||||
export default function InstallMissingModelsButton({}: Props) {
|
||||
const [missngCount, setMissingCount] = useState(0);
|
||||
const [showMyModels, setShowMyModels] = useState(false);
|
||||
const [missingModels, setMissingModels] = useState<MissingModel[]>([]);
|
||||
useEffect(() => {
|
||||
const validateInput = async (input: string) => {};
|
||||
// monkey patch queue prompt api to catch errors
|
||||
const queuePrompt = app.queuePrompt as Function;
|
||||
app.queuePrompt = async function () {
|
||||
@@ -36,38 +39,54 @@ export default function InstallMissingModelsButton({}: Props) {
|
||||
string,
|
||||
NodeError
|
||||
>;
|
||||
setMissingCount(Object.keys(nodeErrors).length);
|
||||
Object.values(nodeErrors).forEach((nodeError) => {
|
||||
nodeError.errors.forEach((error) => {
|
||||
if (error.type === "value_not_in_list") {
|
||||
const { input_name, received_value } = error.extra_info;
|
||||
console.log(
|
||||
"invalid_input",
|
||||
nodeError.class_type,
|
||||
input_name,
|
||||
received_value
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
setMissingModels(
|
||||
Object.values(nodeErrors).flatMap((nodeError) =>
|
||||
nodeError.errors
|
||||
.filter((error) => error.type === "value_not_in_list")
|
||||
.map((error) => {
|
||||
const { input_name, received_value } = error.extra_info;
|
||||
return {
|
||||
class_type: nodeError.class_type,
|
||||
input_name,
|
||||
received_value,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
const graphJson = app.graph.serialize();
|
||||
console.log(graphJson);
|
||||
fetch("/model_manager/find_missing_models", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
workflow: graphJson,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
// const graphJson = app.graph.serialize();
|
||||
// console.log(graphJson);
|
||||
// fetch("/model_manager/find_missing_models", {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify({
|
||||
// workflow: graphJson,
|
||||
// }),
|
||||
// })
|
||||
// .then((res) => res.json())
|
||||
// .then((res) => {
|
||||
// console.log(res);
|
||||
// });
|
||||
}, []);
|
||||
if (missingModels.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button size={"sm"} aria-label="missing models" px={2}>
|
||||
Missing Models {missngCount > 0 ? `(${missngCount})` : ""}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
size={"sm"}
|
||||
aria-label="missing models"
|
||||
px={2}
|
||||
onClick={() => setShowMyModels(true)}
|
||||
>
|
||||
Install Missing ({missingModels.length})
|
||||
</Button>
|
||||
{showMyModels && (
|
||||
<MissingModelsListDrawer
|
||||
onClose={() => setShowMyModels(false)}
|
||||
missingModels={missingModels}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function ModelManagerTopbar({}: Props) {
|
||||
}, []);
|
||||
return (
|
||||
<HStack position={"fixed"} top={2} right={2} gap={2}>
|
||||
{/* <AddMissingModelsButton /> */}
|
||||
<AddMissingModelsButton />
|
||||
|
||||
<Button
|
||||
size={"sm"}
|
||||
|
||||
Reference in New Issue
Block a user