Merge pull request #191 from 11cafe/share-cloud

share to lcoud
This commit is contained in:
Weixuan Fu
2024-02-16 15:29:46 +08:00
committed by GitHub
18 changed files with 285 additions and 768 deletions
+1
View File
@@ -16,6 +16,7 @@ import json
from .version_control import update_version_if_outdated
from .service.model_manager.model_installer import download_url_with_wget
from .service.model_manager.model_list import get_model_list
WEB_DIRECTORY = "entry"
DEFAULT_USER = "guest"
NODE_CLASS_MAPPINGS = {}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{r as o,j as e,B as p,P as f}from"./input.js";import{app as l}from"/scripts/app.js";import{G as x,I as j}from"./ModelManagerTopbar-Zgx14Jgk.js";import{T as h,H as v,a3 as m,B as _,a5 as w,a6 as M,al as y,F as E,ab as I}from"./App-jF2-L8lr.js";import{V as S}from"./chunk-NTCQBYKE-5Z1buvob.js";import{I as b,G as k}from"./chunk-JARCRF6W-71z2fzu3.js";import"/scripts/api.js";function C({model:s}){const[n,t]=o.useState([]),r=D(s.received_value),d=async()=>{const i=await(await fetch(`https://huggingface.co/api/models?limit=3&search=${r}&full=true`)).json(),a=[];i.slice(0,1).forEach(({modelId:c,siblings:g})=>{a.push({name:c,downloadUrl:`https://huggingface.co/${c}/resolve/main/`,url:`https://huggingface.co/${c}/tree/main`})}),t(a)};return o.useEffect(()=>{d()},[]),e.jsx(x,{p:3,shadow:"md",borderWidth:"1px",children:e.jsxs(S,{align:"start",children:[e.jsx(h,{children:s.class_type}),e.jsxs(v,{ml:3,children:[e.jsxs(h,{children:[s.input_name,": "]}),e.jsx(h,{color:"red.400",children:s.received_value})]}),n.map(({url:u,name:i})=>e.jsx(p,{size:"sm",onClick:a=>{a.stopPropagation(),window.open(u,"_blank")},leftIcon:e.jsx(b,{width:5,src:"https://huggingface.co/datasets/huggingface/brand-assets/resolve/main/hf-logo.png"}),rightIcon:e.jsx(m,{size:20}),children:i})),e.jsx(p,{colorScheme:"blue",mt:5,iconSpacing:1,leftIcon:e.jsx(m,{size:20}),size:"sm",onClick:()=>{window.open(`https://civitai.com/search/models?sortBy=models_v5&query=${r}`,"_blank")},children:"Search in CivitAI"})]})})}function D(s){return s.replace(/^.*(\\|\/|\:)/,"").replace(/\.[^/.]+$/,"").replaceAll("_"," ").replace(/([A-Z])/g," $1").trim()}function z({onClose:s,missingModels:n}){o.useEffect(()=>(l.canvasEl.addEventListener("click",s),()=>{l.canvasEl.removeEventListener("click",s)}),[]);const t=440;return e.jsx(f,{children:e.jsx(_,{style:{width:t},children:e.jsxs(w,{width:t,height:"100vh",p:4,gap:2,position:"fixed",top:0,left:0,shadow:"xl",zIndex:M,overflowY:"auto",children:[e.jsx(y,{children:e.jsxs(E,{justifyContent:"space-between",alignContent:"center",children:[e.jsxs(I,{size:"md",mr:2,children:["Missing Models (",n.length,")"]}),e.jsx(j,{})]})}),e.jsx(k,{templateColumns:"1",gap:1,marginTop:2,children:n.map(r=>e.jsx(C,{model:r},r.received_value))})]})})})}function F({}){const[s,n]=o.useState(!1),[t,r]=o.useState([]);return o.useEffect(()=>{const d=l.queuePrompt;l.queuePrompt=async function(){try{await d.apply(l,[...arguments])}finally{const u=l.lastNodeErrors??{};r(Object.values(u).flatMap(i=>i.errors.filter(a=>a.type==="value_not_in_list").map(a=>{const{input_name:c,received_value:g}=a.extra_info;return{class_type:i.class_type,input_name:c,received_value:g}})))}}},[]),t.length===0?null:e.jsxs(e.Fragment,{children:[e.jsxs(p,{size:"sm","aria-label":"missing models",px:2,onClick:()=>n(!0),children:["Install Missing (",t.length,")"]}),s&&e.jsx(z,{onClose:()=>n(!1),missingModels:t})]})}export{F as default};
-1
View File
@@ -1 +0,0 @@
import{G as U,I as C}from"./chunk-JARCRF6W-71z2fzu3.js";import{f as x,D as b,j as p,aa as S,r as h}from"./input.js";import{ac as y,h as v,a9 as E}from"./App-jF2-L8lr.js";var V=x(function(o,s){const{columns:l,spacingX:a,spacingY:n,spacing:r,minChildWidth:e,...c}=o,i=b(),f=e?A(e,i):M(l);return p.jsx(U,{ref:s,gap:r,columnGap:a,rowGap:n,templateColumns:f,...c})});V.displayName="SimpleGrid";function k(t){return typeof t=="number"?`${t}px`:t}function A(t,o){return y(t,s=>{const l=S("sizes",s,k(s))(o);return s===null?null:`repeat(auto-fit, minmax(${l}, 1fr))`})}function M(t){return y(t,o=>o===null?null:`repeat(${o}, minmax(0, 1fr))`)}var R=v("chevron-right","IconChevronRight",[["path",{d:"M9 6l6 6l-6 6",key:"svg-0"}]]);function O(t){return new Promise((o,s)=>{const l=new FileReader;l.onload=a=>{const n=new Uint8Array(a.target.result),r=new DataView(n.buffer);r.getUint32(0)!==2303741511&&(console.error("Not a valid PNG file"),s());let e=8;const c={};for(;e<n.length;){const i=r.getUint32(e),f=String.fromCharCode(...n.slice(e+4,e+8));if(f==="tEXt"||f=="comf"){let u=e+8;for(;n[u]!==0;)u++;const g=String.fromCharCode(...n.slice(e+8,u)),d=n.slice(u+1,e+8+i),m=Array.from(d).map(w=>String.fromCharCode(w)).join("");c[g]=m}e+=12+i}o(c)},l.readAsArrayBuffer(t)})}function D(t){const o=new Uint16Array(t.slice(0,2))[0]===18761;function s(r,e,c){const i=t.slice(r,r+c);if(c===2)return new DataView(i.buffer,i.byteOffset,i.byteLength).getUint16(0,e);if(c===4)return new DataView(i.buffer,i.byteOffset,i.byteLength).getUint32(0,e)}const l=s(4,o,4);function a(r){const e=s(r,o,2),c={};for(let i=0;i<e;i++){const f=r+2+i*12,u=s(f,o,2),g=s(f+2,o,2),d=s(f+4,o,4),m=s(f+8,o,4);let w;g===2&&(w=String.fromCharCode(...t.slice(m,m+d-1))),c[u]=w}return c}return a(l)}function G(t){return new Promise(o=>{const s=new FileReader;s.onload=l=>{var c;const a=new Uint8Array((c=l==null?void 0:l.target)==null?void 0:c.result),n=new DataView(a.buffer);if(n.getUint32(0)!==1380533830||n.getUint32(8)!==1464156752){console.error("Not a valid WEBP file"),o();return}let r=12;const e={};for(;r<a.length;){const i=n.getUint32(r+4,!0);if(String.fromCharCode(...a.slice(r,r+4))==="EXIF"){String.fromCharCode(...a.slice(r+8,r+8+6))=="Exif\0\0"&&(r+=6);const u=D(a.slice(r+8,r+8+i));for(const g in u){const d=u[g],m=d.indexOf(":");e[d.slice(0,m)]=d.slice(m+1)}}r+=8+i}o(e)},s.readAsArrayBuffer(t)})}function N(t){return!!(t!=null&&t.endsWith(".webm")||t!=null&&t.endsWith(".mp4"))}function F(t){return new Promise(o=>{const s=new FileReader;s.onload=l=>{const a=new Uint8Array(l.target.result),n=new DataView(a.buffer),r=new TextDecoder;if(n.getUint32(0)==440786851){let e=12;for(;e<a.length-16;){if(n.getUint16(e)==17543&&String.fromCharCode(...a.slice(e-7,e))==="COMMENT"){const i=n.getUint32(e+2),f=Math.clz32(i)+1;if(f<4){const u=i>>8*(4-f)&~(1<<7*f),g=r.decode(a.slice(e+2+f,e+2+f+u)),d=JSON.parse(g);o(d);return}}e+=1}}else if(n.getUint32(4)==1718909296&&n.getUint32(8)==1769172845){let e=a.length-4;for(;e>16;){if(n.getUint32(e)==1684108385&&n.getUint32(e-8)==2841865588){n.getUint32(e+4),n.getUint32(e+8);const c=n.getUint32(e-4)-4*4,i=r.decode(a.slice(e+12,e+12+c)),f=JSON.parse(i);o(f);return}e-=1}}else{console.error("Unknown magic: "+n.getUint32(0)),o();return}},s.readAsArrayBuffer(t)})}function $({mediaLocalPath:t,size:o,autoPlay:s,hideBrokenImage:l,onBrokenLink:a}){const[n,r]=h.useState(!0);return h.useEffect(()=>{l&&(async()=>{try{if(!(await fetch(`/workspace/view_media?filename=${t}`)).ok)throw new Error("Media not found");r(!0)}catch{r(!1),a==null||a()}})()},[]),n?E(t)?p.jsx(C,{borderRadius:3,boxSize:`${o}px`,objectFit:"cover",src:`/workspace/view_media?filename=${t}`,alt:"workflow image renamed or moved from output folder"}):p.jsx("video",{width:o,height:o,src:`/workspace/view_media?filename=${t}`,loop:!0,autoPlay:s??!1,muted:!0,children:p.jsx("track",{kind:"captions"})}):null}export{R as I,$ as M,V as S,F as a,G as b,O as g,N as i};
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.dragPanelIcon{display:none!important}.workspaceManagerPanel:hover .dragPanelIcon{display:block!important}
@@ -1 +0,0 @@
.drag-model-manager-top-bar-icon{visibility:hidden!important}.model-manager-top-bar:hover .drag-model-manager-top-bar-icon{visibility:visible!important}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{j as t}from"./App-jF2-L8lr.js";import{f as o,j as s}from"./input.js";var e=o((a,r)=>s.jsx(t,{align:"center",...a,direction:"column",ref:r}));e.displayName="VStack";export{e as V};
-688
View File
File diff suppressed because one or more lines are too long
+14 -7
View File
@@ -1,5 +1,5 @@
import React, { useState, useRef } from "react";
import { Box, Button, Card, useOutsideClick } from "@chakra-ui/react";
import React, { useState, useRef, useEffect } from "react";
import { Box, Button, Card } from "@chakra-ui/react";
import { IconChevronDown } from "@tabler/icons-react";
export interface CustomSelectorOption {
@@ -17,10 +17,18 @@ export default function CustomSelector({ options }: Props) {
const [selectedOption, setSelectedOption] = useState<
CustomSelectorOption | undefined
>(options[0]);
useOutsideClick({
ref: ref,
handler: () => setIsOpen(false),
});
useEffect(() => {
const handleClickOutside = (event: any) => {
if (ref.current && !ref.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
const toggleDropdown = () => setIsOpen(!isOpen);
@@ -42,7 +50,6 @@ export default function CustomSelector({ options }: Props) {
borderWidth="1px"
p="2"
position="absolute"
zIndex="dropdown"
>
{options.map((option) => (
<Button
+3 -8
View File
@@ -198,23 +198,18 @@ export default function DropdownTitle() {
>
Versions History
</MenuItem>
{/* <MenuItem
<MenuItem
onClick={() => setIsShareOpen(true)}
icon={<IconShare2 size={20} />}
iconSpacing={1}
>
Share
</MenuItem> */}
</MenuItem>
</MenuList>
</Menu>
}
/>
{isShareOpen && workflowsTable?.curWorkflow && (
<ShareDialog
workflow={workflowsTable?.curWorkflow}
onClose={() => setIsShareOpen(false)}
/>
)}
{isShareOpen && <ShareDialog onClose={() => setIsShareOpen(false)} />}
{isVersionHistoryOpen && (
<VersionHistoryDrawer onClose={() => setIsVersionHistoryOpen(false)} />
)}
+6
View File
@@ -48,6 +48,12 @@ export class TableBase<T extends TableBaseModel> {
await this.saveDiskDB();
return newItem;
}
public async update(id: string, changes: Partial<T>): Promise<T | null> {
await indexdb[this.tableName].update(id, changes);
await this.saveDiskDB();
return (await this.get(id)) ?? null;
}
public async listAll(): Promise<T[]> {
const list = await indexdb[this.tableName].toArray();
return list as any[];
+1
View File
@@ -37,6 +37,7 @@ export class UserSettingsTable extends TableBase<UserSettings> {
autoSave: true,
twoWaySync: false,
foldersOnTop: false,
cloudHost: "https://www.comfyspace.art",
};
}
+243 -34
View File
@@ -1,31 +1,59 @@
import {
Button,
Flex,
HStack,
Input,
Link,
Modal,
ModalBody,
ModalContent,
ModalHeader,
ModalOverlay,
Select,
Stack,
Text,
useToast,
} from "@chakra-ui/react";
import { Workflow } from "../types/dbTypes";
import { useState } from "react";
import { Workflow, WorkflowVersion } from "../types/dbTypes";
import { useEffect, useRef, useState } from "react";
import CustomDropdown, {
CustomSelectorOption,
} from "../components/CustomSelector";
import {
IconCloud,
IconCopy,
IconExternalLink,
IconLink,
IconLock,
IconWorld,
} from "@tabler/icons-react";
import { workflowVersionsTable } from "../db-tables/WorkspaceDB";
import {
userSettingsTable,
workflowVersionsTable,
workflowsTable,
} from "../db-tables/WorkspaceDB";
// @ts-ignore
import { api } from "/scripts/api.js";
// @ts-ignore
import { app } from "/scripts/app.js";
interface Props {
onClose: () => void;
workflow: Workflow;
}
type CreatedCloudflowVersionEvent = {
createdVer: WorkflowVersion;
localWorkflowID: string;
localVersionID: string;
};
function generateRandomKey(length: number) {
// Generate a random array of bytes
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
// Convert the bytes to a hex string
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
}
const privacyOptions: CustomSelectorOption[] = [
@@ -37,44 +65,225 @@ const privacyOptions: CustomSelectorOption[] = [
},
{ label: "Public", value: "PUBLIC", icon: <IconWorld /> },
];
export default function ShareDialog({ onClose, workflow }: Props) {
export default function ShareDialog({ onClose }: Props) {
const [versionName, setVersionName] = useState(
"version " + getCurDateString(),
);
// const onShare = async () => {
// const url = await workflowVersionsTable?.add({
// workflowID: workflow.id,
// name: versionName,
// createTime: Date.now(),
// privacy: "public",
// json: workflow.json,
// remoteUrl: "",
// });
// if (url) {
// alert(url);
// }
// };
const [localVersions, setLocalVersions] = useState<WorkflowVersion[]>([]);
const [loading, setLoading] = useState(false);
const [cloudHost, setCloudHost] = useState("");
const cloudHostRef = useRef("");
const [workflow, setWorkflow] = useState<Workflow>();
const toast = useToast();
const handleShareWorkflowSuccess = async (event: MessageEvent) => {
const detail = event.data;
if (
event.origin !== cloudHostRef.current ||
detail.type !== "share_workflow_success"
) {
console.log("event.origin !== cloudHost", event.origin !== cloudHost);
return;
}
console.log("share_workflow_success", detail);
const localID = detail.localWorkflowID;
const localVerID = detail.localVersionID;
const cloudVersionID = detail.version.id;
const cloudID = detail.version.workflowID;
cloudID &&
localID &&
(await workflowsTable?.updateFlow(localID, {
cloudID: cloudID,
cloudURL: cloudHost + "/workflow/" + cloudID,
}));
localVerID &&
cloudVersionID &&
(await workflowVersionsTable?.update(localVerID, {
cloudID: cloudVersionID,
}));
loadData();
};
useEffect(() => {
loadData();
window.addEventListener("message", handleShareWorkflowSuccess);
return () => {
window.removeEventListener("message", handleShareWorkflowSuccess);
};
}, []);
const getNodeDefs = () => {
const allNodes = app.graph._nodes;
const nodeDefs = {};
for (let n of allNodes) {
// @ts-ignore
if (n.type in LiteGraph.registered_node_types) {
// @ts-ignore
nodeDefs[n.type] = LiteGraph.registered_node_types[n.type].nodeData;
}
}
return nodeDefs;
};
const loadData = async () => {
const host =
import.meta.env.MODE === "production"
? await userSettingsTable?.getSetting("cloudHost")
: "http://localhost:3000";
if (host) {
setCloudHost(host);
cloudHostRef.current = host;
}
const workflow = workflowsTable?.curWorkflow ?? undefined;
setWorkflow(workflow);
workflowVersionsTable?.listByWorkflowID(workflow!.id).then((vers) => {
setLocalVersions(vers);
});
};
const copyTextToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
toast({
title: "Copied to clipboard",
status: "success",
duration: 2000,
isClosable: true,
});
})
.catch((err) => {
console.error("Failed to copy text: ", err);
});
};
const onShare = async () => {
setLoading(true);
const secretKey = generateRandomKey(32);
const host =
import.meta.env.MODE === "production"
? ((await userSettingsTable?.getSetting("cloudHost")) as string)
: "http://localhost:3000";
const nodeDefs = getNodeDefs();
const newVer = await workflowVersionsTable?.add({
workflowID: workflow!.id,
name: versionName,
createTime: Date.now(),
json: workflow!.json,
});
if (!newVer) {
alert("Failed to create new version, please try again.");
setLoading(false);
return;
}
const protocol = window.location.protocol;
const baseHost = window.location.host;
const baseUrl = `${protocol}//${baseHost}`;
const popupUrl =
host + `/share_workflow_confirm/${secretKey}?redirectUrl=${baseUrl}`;
const sharePopup = window.open(
popupUrl,
"Share Workflow",
"width=800,height=800",
);
setLoading(false);
const handleChildReady = (event: MessageEvent) => {
if (event.origin === host && event.data === "child_ready") {
const curWorkflow = workflowsTable?.curWorkflow;
console.log("Child window is ready.inputdata", {
workflow: curWorkflow,
version: newVer,
nodeDefs: nodeDefs,
});
// Send data to the new window after it loads
sharePopup!.postMessage(
{
workflow: curWorkflow,
version: newVer,
nodeDefs: nodeDefs,
},
host,
);
window.removeEventListener("message", handleChildReady);
}
};
window.addEventListener("message", handleChildReady);
};
const cloudWorkflowID = workflowsTable?.curWorkflow?.cloudID;
return (
<Modal isOpen={true} onClose={onClose} size={"lg"}>
<ModalOverlay />
<ModalContent width={["98%", "90%", "50%"]} maxWidth={"600px"}>
<ModalHeader>Share "{workflow.name}"</ModalHeader>
<ModalHeader>Share "{workflow?.name}"</ModalHeader>
<ModalBody pb={10}>
<Text mb={3}>Version name</Text>
<HStack>
<Input
value={versionName}
maxWidth={440}
onChange={(e) => {
setVersionName(e.target.value);
}}
/>
</HStack>
<Text mt={4} mb={3}></Text>
<CustomDropdown options={privacyOptions} />
<Stack gap={6}>
<CustomDropdown options={privacyOptions} />
{cloudWorkflowID && (
<HStack spacing={2} color="teal.400">
<Link
href={cloudHost + "/workflow/" + cloudWorkflowID}
isExternal
>
{cloudHost + "/workflow/" + cloudWorkflowID}{" "}
<IconExternalLink
size={20}
style={{ display: "inline-block", verticalAlign: "middle" }}
/>
</Link>
<Button
width={"180px"}
size={"sm"}
leftIcon={<IconCopy />}
onClick={() => {
copyTextToClipboard(
cloudHost +
"/workflow/" +
workflowsTable?.curWorkflow?.cloudID,
);
}}
>
Copy link
</Button>
</HStack>
)}
<HStack>
<Text>New Version</Text>
<Input
value={versionName}
maxWidth={440}
onChange={(e) => {
setVersionName(e.target.value);
}}
/>
</HStack>
{localVersions.slice(0, 4).map((ver) => {
return (
<HStack key={ver.id} spacing={4} alignItems={"center"} mb={3}>
<Text>{ver.name}</Text>
{ver.cloudID && (
<a
href={cloudHost + "/workflow_ver/" + ver.cloudID}
target="_blank"
rel="noopener noreferrer"
style={{ textDecoration: "none" }}
>
<Button
size={"xs"}
colorScheme="teal"
leftIcon={<IconCloud />}
rightIcon={<IconExternalLink size={18} />}
>
Shared
</Button>
</a>
)}
</HStack>
);
})}
</Stack>
<HStack justifyContent={"flex-end"} mt={16}>
<Button colorScheme="teal">Share</Button>
<Button colorScheme="teal" onClick={onShare} isDisabled={loading}>
{loading ? "Sharing" : "Share"}
</Button>
</HStack>
</ModalBody>
</ModalContent>
+7 -4
View File
@@ -1,16 +1,19 @@
import { Button } from "@chakra-ui/react";
import { IconUsersGroup } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { IconCloud } from "@tabler/icons-react";
import { workflowsTable } from "../db-tables/WorkspaceDB";
export function SharedTopbarButton({}) {
const isCloudflow = workflowsTable?.curWorkflow?.cloudID;
if (!isCloudflow) return null;
return (
<>
<Button
onClick={() => {}}
onClick={() => {
window.open(workflowsTable?.curWorkflow?.cloudURL);
}}
aria-label={"Shared"}
size={"sm"}
leftIcon={<IconUsersGroup />}
leftIcon={<IconCloud />}
>
Public
</Button>
+2
View File
@@ -14,6 +14,7 @@ import EditFlowName from "../components/EditFlowName";
import { WorkspaceContext } from "../WorkspaceContext";
import { PanelPosition } from "../types/dbTypes";
import "./Topbar.css";
import { SharedTopbarButton } from "../share/SharedTopbarButton";
interface Props {
positionStyle: PanelPosition;
@@ -103,6 +104,7 @@ export function Topbar({
<DropdownTitle />
</HStack>
)}
<SharedTopbarButton />
{curFlowID && isDirty ? (
<Tooltip label="Save workflow">
<IconButton
+8 -2
View File
@@ -14,6 +14,9 @@ export interface Workflow extends SortableItem {
parentFolderID?: string;
mediaIDs?: string[];
coverMediaPath?: string;
cloudID?: string;
cloudURL?: string;
privacy?: WorkflowPrivacy;
}
export interface TableBaseModel {
@@ -56,8 +59,8 @@ export type WorkflowVersion = {
workflowID: string;
json: string;
createTime: number;
privacy?: "public" | "private";
remoteUrl?: string;
cloudID?: string;
nodeDefs?: string; //for cloud workflow version
};
export type UserSettings = {
@@ -72,6 +75,7 @@ export type UserSettings = {
twoWaySync?: boolean;
foldersOnTop?: boolean;
showNsfwModelThumbnail?: boolean;
cloudHost: string;
};
export interface PanelPosition {
@@ -117,4 +121,6 @@ export enum EFlowOperationType {
DELETE = "delete",
}
type WorkflowPrivacy = "PUBLIC" | "PRIVATE" | "UNLISTED";
export const WORKSPACE_INDEXDB_NAME = "comfyui_workspace_db";