send2trash, fix indexdb opening error by try catch, restructure and simplify share dialog

Beta
This commit is contained in:
Weixuan Fu
2024-06-17 11:15:33 +08:00
committed by GitHub
50 changed files with 307 additions and 811 deletions
+13 -15
View File
@@ -5,17 +5,18 @@ import folder_paths
import shutil
import os
import sys
import subprocess # don't remove this
from urllib.parse import urlparse
import subprocess
import subprocess
import os
import json
from .service.model_manager.model_installer import download_url_with_wget
from .service.model_manager.model_list import get_model_list
from .service.media_service import *
from .service.file_sync_service import *
from .service.db_service import get_my_workflows_dir
from .service.node_service import *
try:
from send2trash import send2trash
except ImportError:
send2trash = None
WEB_DIRECTORY = "entry"
NODE_CLASS_MAPPINGS = {}
@@ -112,31 +113,28 @@ async def update_file(request):
await asyncio.to_thread(write_json_to_file, json_str)
return web.Response(text="File updated successfully")
@server.PromptServer.instance.routes.post("/workspace/delete_file")
async def delete_file(request):
data = await request.json()
file_path = data['file_path']
delete_empty_folder = data['deleteEmptyFolder']
def sync_delete_file(file_path, delete_empty_folder):
def sync_delete_file(file_path):
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)
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"
if send2trash:
send2trash(full_path)
else:
return "File deleted successfully"
os.remove(full_path)
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
return "File deleted successfully"
else:
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)
response_text = await asyncio.to_thread(sync_delete_file, file_path)
if response_text == "File not found":
return web.Response(text=response_text, status=404)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{r as u,j as E}from"./input.js";import{Z as x,a5 as r,aj as C,x as a,i as W,T as j,E as d,ak as A,al as L,k as h,am as R}from"./App-eQybp91e.js";import{u as O}from"./useDebounceFn-ld478fd0.js";function M(){const{isDirty:p,setIsDirty:g,setRoute:v,saveCurWorkflow:T,setCurFlowIDAndName:b}=u.useContext(x),w=u.useRef(p),[S,D]=u.useState(!1),k=u.useRef(null);u.useEffect(()=>{w.current=p},[p]),u.useEffect(()=>{var n,i;const c=e=>{var s;if(document.visibilityState==="hidden")return;const t=L(e);if(t)switch(t===d.openSpotlightSearch&&e.preventDefault(),window.dispatchEvent(new CustomEvent(A,{detail:{shortcutType:t}})),t){case d.SAVE:T();break;case d.SAVE_AS:v("saveAsModal");break;case d.openSpotlightSearch:v("spotlightSearch");break}else(s=e.target)!=null&&s.matches("input, textarea")&&Object.keys(r.canvas.selected_nodes??{}).length&&m()},o=r.graph.onConfigure;return r.graph.onConfigure=function(){o==null||o.apply(this,arguments),setTimeout(()=>{var t,s,l,f;const e=(s=(t=r.graph.extra)==null?void 0:t[C])==null?void 0:s.id;(e==null||e!=((f=(l=a)==null?void 0:l.curWorkflow)==null?void 0:f.id))&&b(null)},500)},document.addEventListener("click",e=>{Object.keys(r.canvas.selected_nodes??{}).length&&(r.canvas.node_over!=null||r.canvas.node_capturing_input!=null||r.canvas.node_widget!=null)&&m()}),document.addEventListener("keydown",c),(i=(n=h)==null?void 0:n.settings)!=null&&i.autoSave&&h.settings.autoSaveDuration&&(k.current=setInterval(()=>{_()},h.settings.autoSaveDuration*1e3)),()=>{document.removeEventListener("keydown",c),k.current&&clearInterval(k.current)}},[]);const _=async()=>{var o,n,i,e,t,s;if((n=(o=a)==null?void 0:o.curWorkflow)!=null&&n.saveLock||!((e=(i=a)==null?void 0:i.curWorkflow)!=null&&e.id)||!w.current)return;const c=JSON.stringify(r.graph.serialize());await((t=a)==null?void 0:t.updateFlow(a.curWorkflow.id,{json:c})),(s=R)==null||s.create({workflowID:a.curWorkflow.id,isAutoSave:!0,json:c}),g(!1)},I=async()=>{var o,n,i,e,t,s,l,f,y;if((n=(o=a)==null?void 0:o.curWorkflow)!=null&&n.saveLock||w.current)return;const c=r.graph.serialize();JSON.stringify(c)!==((e=(i=a)==null?void 0:i.curWorkflow)==null?void 0:e.json)&&(g(!0),(s=(t=h)==null?void 0:t.settings)!=null&&s.autoSave&&((f=(l=a)==null?void 0:l.curWorkflow)!=null&&f.id)&&!S&&(await((y=a)==null?void 0:y.latestVersionCheck())||D(!0)))},[m,N]=O(I,900);return S?E.jsx(W,{label:"This workflow was changed by another tab, so you are not working on the latest version. Please refresh page to see the latest version of this workflow.",children:E.jsx(j,{colorScheme:"yellow",children:"⚠️Outdated version"})}):null}export{M as default};
import{r as u,j as E}from"./input.js";import{X as x,a3 as r,af as C,x as a,i as W,T as A,E as d,ag as L,ah as R,k as h,ai as j}from"./App-Pi18TWng.js";import{u as O}from"./useDebounceFn-ld478fd0.js";function M(){const{isDirty:p,setIsDirty:k,setRoute:v,saveCurWorkflow:T,setCurFlowIDAndName:b}=u.useContext(x),w=u.useRef(p),[S,D]=u.useState(!1),g=u.useRef(null);u.useEffect(()=>{w.current=p},[p]),u.useEffect(()=>{var n,i;const c=e=>{var s;if(document.visibilityState==="hidden")return;const t=R(e);if(t)switch(t===d.openSpotlightSearch&&e.preventDefault(),window.dispatchEvent(new CustomEvent(L,{detail:{shortcutType:t}})),t){case d.SAVE:T();break;case d.SAVE_AS:v("saveAsModal");break;case d.openSpotlightSearch:v("spotlightSearch");break}else(s=e.target)!=null&&s.matches("input, textarea")&&Object.keys(r.canvas.selected_nodes??{}).length&&m()},o=r.graph.onConfigure;return r.graph.onConfigure=function(){o==null||o.apply(this,arguments),setTimeout(()=>{var t,s,l,f;const e=(s=(t=r.graph.extra)==null?void 0:t[C])==null?void 0:s.id;(e==null||e!=((f=(l=a)==null?void 0:l.curWorkflow)==null?void 0:f.id))&&b(null)},500)},document.addEventListener("click",e=>{Object.keys(r.canvas.selected_nodes??{}).length&&(r.canvas.node_over!=null||r.canvas.node_capturing_input!=null||r.canvas.node_widget!=null)&&m()}),document.addEventListener("keydown",c),(i=(n=h)==null?void 0:n.settings)!=null&&i.autoSave&&h.settings.autoSaveDuration&&(g.current=setInterval(()=>{_()},h.settings.autoSaveDuration*1e3)),()=>{document.removeEventListener("keydown",c),g.current&&clearInterval(g.current)}},[]);const _=async()=>{var o,n,i,e,t,s;if((n=(o=a)==null?void 0:o.curWorkflow)!=null&&n.saveLock||!((e=(i=a)==null?void 0:i.curWorkflow)!=null&&e.id)||!w.current)return;const c=JSON.stringify(r.graph.serialize());await((t=a)==null?void 0:t.updateFlow(a.curWorkflow.id,{json:c})),(s=j)==null||s.create({workflowID:a.curWorkflow.id,isAutoSave:!0,json:c}),k(!1)},I=async()=>{var o,n,i,e,t,s,l,f,y;if((n=(o=a)==null?void 0:o.curWorkflow)!=null&&n.saveLock||w.current)return;const c=r.graph.serialize();JSON.stringify(c)!==((e=(i=a)==null?void 0:i.curWorkflow)==null?void 0:e.json)&&(k(!0),(s=(t=h)==null?void 0:t.settings)!=null&&s.autoSave&&((f=(l=a)==null?void 0:l.curWorkflow)!=null&&f.id)&&!S&&(await((y=a)==null?void 0:y.latestVersionCheck())||D(!0)))},[m,N]=O(I,900);return S?E.jsx(W,{label:"This workflow was changed by another tab, so you are not working on the latest version. Please refresh page to see the latest version of this workflow.",children:E.jsx(A,{colorScheme:"yellow",children:"⚠️Outdated version"})}):null}export{M as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{d as a}from"./App-eQybp91e.js";var p=a("copy","IconCopy",[["path",{d:"M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z",key:"svg-0"}],["path",{d:"M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1",key:"svg-1"}]]);export{p as I};
import{d as a}from"./App-Pi18TWng.js";var p=a("copy","IconCopy",[["path",{d:"M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z",key:"svg-0"}],["path",{d:"M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1",key:"svg-1"}]]);export{p as I};
@@ -1 +1 @@
import{d as a}from"./App-eQybp91e.js";var r=a("search","IconSearch",[["path",{d:"M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0",key:"svg-0"}],["path",{d:"M21 21l-6 -6",key:"svg-1"}]]);export{r as I};
import{d as a}from"./App-Pi18TWng.js";var r=a("search","IconSearch",[["path",{d:"M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0",key:"svg-0"}],["path",{d:"M21 21l-6 -6",key:"svg-1"}]]);export{r as I};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{f as x,j as l,g as i,h as b,l as C,o as E,n as I,r as f,a3 as P}from"./input.js";import{ac as z}from"./App-eQybp91e.js";var v=x(function(o,s){const{children:t,placeholder:n,className:r,...a}=o;return l.jsxs(i.select,{...a,ref:s,className:b("chakra-select",r),children:[n&&l.jsx("option",{value:"",children:n}),t]})});v.displayName="SelectField";function F(e,o){const s={},t={};for(const[n,r]of Object.entries(e))o.includes(n)?s[n]=r:t[n]=r;return[s,t]}var H=x((e,o)=>{var s;const t=C("Select",e),{rootProps:n,placeholder:r,icon:a,color:c,height:_,h:d,minH:h,minHeight:y,iconColor:p,iconSize:u,...j}=E(e),[g,N]=F(j,P),m=z(N),k={width:"100%",height:"fit-content",position:"relative",color:c},w={paddingEnd:"2rem",...t.field,_focus:{zIndex:"unset",...(s=t.field)==null?void 0:s._focus}};return l.jsxs(i.div,{className:"chakra-select__wrapper",__css:k,...g,...n,children:[l.jsx(v,{ref:o,height:d??_,minH:h??y,placeholder:r,...m,__css:w,children:e.children}),l.jsx(S,{"data-disabled":I(m.disabled),...(p||c)&&{color:p||c},__css:t.icon,...u&&{fontSize:u},children:a})]})});H.displayName="Select";var M=e=>l.jsx("svg",{viewBox:"0 0 24 24",...e,children:l.jsx("path",{fill:"currentColor",d:"M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"})}),R=i("div",{baseStyle:{position:"absolute",display:"inline-flex",alignItems:"center",justifyContent:"center",pointerEvents:"none",top:"50%",transform:"translateY(-50%)"}}),S=e=>{const{children:o=l.jsx(M,{}),...s}=e,t=f.cloneElement(o,{role:"presentation",className:"chakra-select__icon",focusable:!1,"aria-hidden":!0,style:{width:"1em",height:"1em",color:"currentColor"}});return l.jsx(R,{...s,className:"chakra-select__icon-wrapper",children:f.isValidElement(o)?t:null})};S.displayName="SelectIcon";export{H as S};
import{f as x,j as l,g as i,e as b,k as C,o as E,l as I,r as f,a3 as P}from"./input.js";import{aa as z}from"./App-Pi18TWng.js";var v=x(function(o,s){const{children:t,placeholder:n,className:r,...a}=o;return l.jsxs(i.select,{...a,ref:s,className:b("chakra-select",r),children:[n&&l.jsx("option",{value:"",children:n}),t]})});v.displayName="SelectField";function F(e,o){const s={},t={};for(const[n,r]of Object.entries(e))o.includes(n)?s[n]=r:t[n]=r;return[s,t]}var H=x((e,o)=>{var s;const t=C("Select",e),{rootProps:n,placeholder:r,icon:a,color:c,height:_,h:d,minH:h,minHeight:y,iconColor:p,iconSize:u,...j}=E(e),[g,N]=F(j,P),m=z(N),k={width:"100%",height:"fit-content",position:"relative",color:c},w={paddingEnd:"2rem",...t.field,_focus:{zIndex:"unset",...(s=t.field)==null?void 0:s._focus}};return l.jsxs(i.div,{className:"chakra-select__wrapper",__css:k,...g,...n,children:[l.jsx(v,{ref:o,height:d??_,minH:h??y,placeholder:r,...m,__css:w,children:e.children}),l.jsx(S,{"data-disabled":I(m.disabled),...(p||c)&&{color:p||c},__css:t.icon,...u&&{fontSize:u},children:a})]})});H.displayName="Select";var M=e=>l.jsx("svg",{viewBox:"0 0 24 24",...e,children:l.jsx("path",{fill:"currentColor",d:"M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"})}),R=i("div",{baseStyle:{position:"absolute",display:"inline-flex",alignItems:"center",justifyContent:"center",pointerEvents:"none",top:"50%",transform:"translateY(-50%)"}}),S=e=>{const{children:o=l.jsx(M,{}),...s}=e,t=f.cloneElement(o,{role:"presentation",className:"chakra-select__icon",focusable:!1,"aria-hidden":!0,style:{width:"1em",height:"1em",color:"currentColor"}});return l.jsx(R,{...s,className:"chakra-select__icon-wrapper",children:f.isValidElement(o)?t:null})};S.displayName="SelectIcon";export{H as S};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
import{b as D,r as p,j as t,g as i,a0 as v,f as j,l as T,o as B,H as E,h as $}from"./input.js";import{u as z}from"./chunk-7D6N5TE5-lFR77IBa.js";var[Z,F]=D({name:"CheckboxGroupContext",strict:!1});function M(o){const[s,n]=p.useState(o),[e,r]=p.useState(!1);return o!==s&&(r(!0),n(o)),e}function W(o){return t.jsx(i.svg,{width:"1.2em",viewBox:"0 0 12 10",style:{fill:"none",strokeWidth:2,stroke:"currentColor",strokeDasharray:16},...o,children:t.jsx("polyline",{points:"1.5 6 4.5 9 10.5 1"})})}function X(o){return t.jsx(i.svg,{width:"1.2em",viewBox:"0 0 24 24",style:{stroke:"currentColor",strokeWidth:4},...o,children:t.jsx("line",{x1:"21",x2:"3",y1:"12",y2:"12"})})}function H(o){const{isIndeterminate:s,isChecked:n,...e}=o,r=s?X:W;return n||s?t.jsx(i.div,{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%"},children:t.jsx(r,{...e})}):null}var L={display:"inline-flex",alignItems:"center",justifyContent:"center",verticalAlign:"top",userSelect:"none",flexShrink:0},O={cursor:"pointer",display:"inline-flex",alignItems:"center",verticalAlign:"top",position:"relative"},q=v({from:{opacity:0,strokeDashoffset:16,transform:"scale(0.95)"},to:{opacity:1,strokeDashoffset:0,transform:"scale(1)"}}),J=v({from:{opacity:0},to:{opacity:1}}),K=v({from:{transform:"scaleX(0.65)"},to:{transform:"scaleX(1)"}}),Q=j(function(s,n){const e=F(),r={...e,...s},a=T("Checkbox",r),c=B(s),{spacing:x="0.5rem",className:C,children:m,iconColor:u,iconSize:d,icon:f=t.jsx(H,{}),isChecked:g,isDisabled:k=e==null?void 0:e.isDisabled,onChange:h,inputProps:w,..._}=c;let y=g;e!=null&&e.value&&c.value&&(y=e.value.includes(c.value));let b=h;e!=null&&e.onChange&&c.value&&(b=E(e.onChange,h));const{state:l,getInputProps:A,getCheckboxProps:S,getLabelProps:G,getRootProps:P}=z({..._,isDisabled:k,isChecked:y,onChange:b}),I=M(l.isChecked),R=p.useMemo(()=>({animation:I?l.isIndeterminate?`${J} 20ms linear, ${K} 200ms linear`:`${q} 200ms linear`:void 0,fontSize:d,color:u,...a.icon}),[u,d,I,l.isIndeterminate,a.icon]),N=p.cloneElement(f,{__css:R,isIndeterminate:l.isIndeterminate,isChecked:l.isChecked});return t.jsxs(i.label,{__css:{...O,...a.container},className:$("chakra-checkbox",C),...P(),children:[t.jsx("input",{className:"chakra-checkbox__input",...A(w,n)}),t.jsx(i.span,{__css:{...L,...a.control},className:"chakra-checkbox__control",...S(),children:N}),m&&t.jsx(i.span,{className:"chakra-checkbox__label",...G(),__css:{marginStart:x,...a.label},children:m})]})});Q.displayName="Checkbox";var U=j(function(s,n){const{templateAreas:e,gap:r,rowGap:a,columnGap:c,column:x,row:C,autoFlow:m,autoRows:u,templateRows:d,autoColumns:f,templateColumns:g,...k}=s,h={display:"grid",gridTemplateAreas:e,gridGap:r,gridRowGap:a,gridColumnGap:c,gridAutoColumns:f,gridColumn:x,gridRow:C,gridAutoFlow:m,gridAutoRows:u,gridTemplateRows:d,gridTemplateColumns:g};return t.jsx(i.div,{ref:n,__css:h,...k})});U.displayName="Grid";export{Q as C,U as G};
+1
View File
@@ -0,0 +1 @@
import{b as D,r as h,j as t,g as i,$ as v,f as j,k as T,o as $,H as B,e as E}from"./input.js";import{u as z}from"./chunk-7D6N5TE5-ZfhYDM9i.js";var[Z,F]=D({name:"CheckboxGroupContext",strict:!1});function M(o){const[s,n]=h.useState(o),[e,r]=h.useState(!1);return o!==s&&(r(!0),n(o)),e}function W(o){return t.jsx(i.svg,{width:"1.2em",viewBox:"0 0 12 10",style:{fill:"none",strokeWidth:2,stroke:"currentColor",strokeDasharray:16},...o,children:t.jsx("polyline",{points:"1.5 6 4.5 9 10.5 1"})})}function X(o){return t.jsx(i.svg,{width:"1.2em",viewBox:"0 0 24 24",style:{stroke:"currentColor",strokeWidth:4},...o,children:t.jsx("line",{x1:"21",x2:"3",y1:"12",y2:"12"})})}function H(o){const{isIndeterminate:s,isChecked:n,...e}=o,r=s?X:W;return n||s?t.jsx(i.div,{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100%"},children:t.jsx(r,{...e})}):null}var L={display:"inline-flex",alignItems:"center",justifyContent:"center",verticalAlign:"top",userSelect:"none",flexShrink:0},O={cursor:"pointer",display:"inline-flex",alignItems:"center",verticalAlign:"top",position:"relative"},q=v({from:{opacity:0,strokeDashoffset:16,transform:"scale(0.95)"},to:{opacity:1,strokeDashoffset:0,transform:"scale(1)"}}),J=v({from:{opacity:0},to:{opacity:1}}),K=v({from:{transform:"scaleX(0.65)"},to:{transform:"scaleX(1)"}}),Q=j(function(s,n){const e=F(),r={...e,...s},a=T("Checkbox",r),c=$(s),{spacing:x="0.5rem",className:C,children:m,iconColor:u,iconSize:d,icon:k=t.jsx(H,{}),isChecked:f,isDisabled:g=e==null?void 0:e.isDisabled,onChange:p,inputProps:w,..._}=c;let y=f;e!=null&&e.value&&c.value&&(y=e.value.includes(c.value));let b=p;e!=null&&e.onChange&&c.value&&(b=B(e.onChange,p));const{state:l,getInputProps:A,getCheckboxProps:S,getLabelProps:G,getRootProps:P}=z({..._,isDisabled:g,isChecked:y,onChange:b}),I=M(l.isChecked),R=h.useMemo(()=>({animation:I?l.isIndeterminate?`${J} 20ms linear, ${K} 200ms linear`:`${q} 200ms linear`:void 0,fontSize:d,color:u,...a.icon}),[u,d,I,l.isIndeterminate,a.icon]),N=h.cloneElement(k,{__css:R,isIndeterminate:l.isIndeterminate,isChecked:l.isChecked});return t.jsxs(i.label,{__css:{...O,...a.container},className:E("chakra-checkbox",C),...P(),children:[t.jsx("input",{className:"chakra-checkbox__input",...A(w,n)}),t.jsx(i.span,{__css:{...L,...a.control},className:"chakra-checkbox__control",...S(),children:N}),m&&t.jsx(i.span,{className:"chakra-checkbox__label",...G(),__css:{marginStart:x,...a.label},children:m})]})});Q.displayName="Checkbox";var U=j(function(s,n){const{templateAreas:e,gap:r,rowGap:a,columnGap:c,column:x,row:C,autoFlow:m,autoRows:u,templateRows:d,autoColumns:k,templateColumns:f,...g}=s,p={display:"grid",gridTemplateAreas:e,gridGap:r,gridRowGap:a,gridColumnGap:c,gridAutoColumns:k,gridColumn:x,gridRow:C,gridAutoFlow:m,gridAutoRows:u,gridTemplateRows:d,gridTemplateColumns:f};return t.jsx(i.div,{ref:n,__css:p,...g})});U.displayName="Grid";export{Q as C,U as G};
@@ -1 +1 @@
import{g as t}from"./App-eQybp91e.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};
import{g as t}from"./App-Pi18TWng.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};
@@ -1 +1 @@
import{f as l,p as f,o as h,j as e,g as n,h as m,l as y,r as o}from"./input.js";import{u as w}from"./chunk-7D6N5TE5-lFR77IBa.js";var N=l(function(a,i){const s=f("Link",a),{className:c,isExternal:t,...r}=h(a);return e.jsx(n.a,{target:t?"_blank":void 0,rel:t?"noopener":void 0,ref:i,className:m("chakra-link",c),...r,__css:s})});N.displayName="Link";var j=l(function(a,i){const s=y("Switch",a),{spacing:c="0.5rem",children:t,...r}=h(a),{getIndicatorProps:p,getInputProps:u,getCheckboxProps:x,getRootProps:_,getLabelProps:g}=w(r),b=o.useMemo(()=>({display:"inline-block",position:"relative",verticalAlign:"middle",lineHeight:0,...s.container}),[s.container]),S=o.useMemo(()=>({display:"inline-flex",flexShrink:0,justifyContent:"flex-start",boxSizing:"content-box",cursor:"pointer",...s.track}),[s.track]),d=o.useMemo(()=>({userSelect:"none",marginStart:c,...s.label}),[c,s.label]);return e.jsxs(n.label,{..._(),className:m("chakra-switch",a.className),__css:b,children:[e.jsx("input",{className:"chakra-switch__input",...u({},i)}),e.jsx(n.span,{...x(),className:"chakra-switch__track",__css:S,children:e.jsx(n.span,{__css:s.thumb,className:"chakra-switch__thumb",...p()})}),t&&e.jsx(n.span,{className:"chakra-switch__label",...g(),__css:d,children:t})]})});j.displayName="Switch";export{N as L,j as S};
import{f as l,n as f,o as h,j as e,g as n,e as m,k as y,r as o}from"./input.js";import{u as w}from"./chunk-7D6N5TE5-ZfhYDM9i.js";var N=l(function(a,i){const s=f("Link",a),{className:c,isExternal:t,...r}=h(a);return e.jsx(n.a,{target:t?"_blank":void 0,rel:t?"noopener":void 0,ref:i,className:m("chakra-link",c),...r,__css:s})});N.displayName="Link";var j=l(function(a,i){const s=y("Switch",a),{spacing:c="0.5rem",children:t,...r}=h(a),{getIndicatorProps:u,getInputProps:p,getCheckboxProps:x,getRootProps:_,getLabelProps:g}=w(r),b=o.useMemo(()=>({display:"inline-block",position:"relative",verticalAlign:"middle",lineHeight:0,...s.container}),[s.container]),S=o.useMemo(()=>({display:"inline-flex",flexShrink:0,justifyContent:"flex-start",boxSizing:"content-box",cursor:"pointer",...s.track}),[s.track]),d=o.useMemo(()=>({userSelect:"none",marginStart:c,...s.label}),[c,s.label]);return e.jsxs(n.label,{..._(),className:m("chakra-switch",a.className),__css:b,children:[e.jsx("input",{className:"chakra-switch__input",...p({},i)}),e.jsx(n.span,{...x(),className:"chakra-switch__track",__css:S,children:e.jsx(n.span,{__css:s.thumb,className:"chakra-switch__thumb",...u()})}),t&&e.jsx(n.span,{className:"chakra-switch__label",...g(),__css:d,children:t})]})});j.displayName="Switch";export{N as L,j as S};
+1
View File
@@ -0,0 +1 @@
import{k as m}from"./App-Pi18TWng.js";const r="WORKSPACE_CIVIT_API_KEY_STORAGE_KEY";function g(){return localStorage.getItem(r)}function h(e){localStorage.setItem(r,e)}function v(e){return`https://civitai.com/api/download/models/${e}`}async function w(e){var s,a,n;try{const l=`https://civitai.com/api/v1/model-versions/by-hash/${e}`,t=await(await fetch(l)).json();let i;if(await((s=m)==null?void 0:s.getSetting("showNsfwModelThumbnail"))===!0)i=(n=(a=t==null?void 0:t.images)==null?void 0:a[0])==null?void 0:n.url;else if(!t.model.nsfw){const o=t.images.find(c=>c.nsfwLevel==1);i=o==null?void 0:o.url}return{modelName:t.model.name,civitModelID:String(t.modelId),civitModelVersionID:String(t.id),imageUrl:i??void 0}}catch{return{}}}export{v as a,w as f,g,h as s};
+2 -2
View File
@@ -679,10 +679,10 @@ Error generating stack: `+i.message+`
}
${aB}
`}),zw=document.createElement("div");document.body.append(zw);const lB=Ar.lazy(()=>Xk(()=>import("./App-eQybp91e.js").then(e=>e.aT),__vite__mapDeps([0,1])).then(({default:e})=>({default:e}))),uB={hide:-1,auto:"auto",base:0,docked:10,dropdown:1e7,sticky:11e6,banner:12e6,overlay:13e6,modal:14e6,popover:15e6,skipLink:16e6,toast:17e6,tooltip:18e6},cB=Pz({zIndices:uB,components:{Card:{baseStyle:e=>({container:{backgroundColor:e.colorMode==="dark"?"#171B21":"#F6F8FA"}})}}});gd.createRoot(zw).render(E.jsx(Ar.StrictMode,{children:E.jsxs(BI,{resetCSS:!1,disableGlobalStyle:!0,theme:cB,children:[E.jsx(sB,{scope:".workspace_manager"}),E.jsx(DP,{initialColorMode:"dark"}),E.jsx(iB,{children:E.jsx(S.Suspense,{children:E.jsx(lB,{})})})]})}));const Hf=document.body,dB={attributes:!0,attributeFilter:["class"]},fB=function(e,t){const r=document.documentElement;r.style.colorScheme==="dark"&&r.style.removeProperty("color-scheme");for(const n of e)if(n.type==="attributes"&&n.attributeName==="class")for(const o of Hf.classList)o.includes("chakra")&&Hf.classList.remove(o)},hB=new MutationObserver(fB);hB.observe(Hf,dB);export{Qh as $,PB as A,CB as B,TB as C,_B as D,TP as E,lr as F,oi as G,vB as H,Ya as I,Ap as J,yB as K,Ka as L,yw as M,Ew as N,_w as O,ja as P,Tw as Q,Aw as R,$w as S,Pw as T,Tp as U,xb as V,EB as W,os as X,Ar as Y,pB as Z,Xk as _,kB as a,vP as a0,rB as a1,nB as a2,gB as a3,mn as b,wB as c,i3 as d,Bl as e,pt as f,_e as g,Be as h,Mx as i,E as j,SB as k,Xb as l,If as m,_v as n,Iu as o,Bu as p,mB as q,S as r,Ei as s,Gf as t,IP as u,uw as v,bB as w,Kt as x,yg as y,uO as z};
`}),zw=document.createElement("div");document.body.append(zw);const lB=Ar.lazy(()=>Xk(()=>import("./App-Pi18TWng.js").then(e=>e.aJ),__vite__mapDeps([0,1])).then(({default:e})=>({default:e}))),uB={hide:-1,auto:"auto",base:0,docked:10,dropdown:1e7,sticky:11e6,banner:12e6,overlay:13e6,modal:14e6,popover:15e6,skipLink:16e6,toast:17e6,tooltip:18e6},cB=Pz({config:{initialColorMode:"dark",useSystemColorMode:!1},zIndices:uB,components:{Card:{baseStyle:e=>({container:{backgroundColor:e.colorMode==="dark"?"#171B21":"#F6F8FA"}})}}});gd.createRoot(zw).render(E.jsx(Ar.StrictMode,{children:E.jsxs(BI,{resetCSS:!1,disableGlobalStyle:!0,theme:cB,children:[E.jsx(sB,{scope:".workspace_manager"}),E.jsx(DP,{initialColorMode:"dark"}),E.jsx(iB,{children:E.jsx(S.Suspense,{children:E.jsx(lB,{})})})]})}));const Hf=document.body,dB={attributes:!0,attributeFilter:["class"]},fB=function(e,t){const r=document.documentElement;r.style.colorScheme==="dark"&&r.style.removeProperty("color-scheme");for(const n of e)if(n.type==="attributes"&&n.attributeName==="class")for(const o of Hf.classList)o.includes("chakra")&&Hf.classList.remove(o)},hB=new MutationObserver(fB);hB.observe(Hf,dB);export{vP as $,PB as A,CB as B,TB as C,_B as D,TP as E,lr as F,oi as G,vB as H,Ya as I,Ap as J,yB as K,Ka as L,yw as M,Ew as N,_w as O,ja as P,Tw as Q,Aw as R,$w as S,Pw as T,xb as U,EB as V,os as W,Ar as X,pB as Y,Qh as Z,Xk as _,kB as a,Tp as a0,rB as a1,nB as a2,gB as a3,mn as b,wB as c,i3 as d,Be as e,pt as f,_e as g,Mx as h,SB as i,E as j,Xb as k,_v as l,If as m,Bu as n,Iu as o,mB as p,Ei as q,S as r,Gf as s,uw as t,IP as u,bB as v,Kt as w,yg as x,Bl as y,uO as z};
function __vite__mapDeps(indexes) {
if (!__vite__mapDeps.viteFileDeps) {
__vite__mapDeps.viteFileDeps = ["workspace_web/App-eQybp91e.js","workspace_web/assets/App-JXePnJiV.css"]
__vite__mapDeps.viteFileDeps = ["workspace_web/App-Pi18TWng.js","workspace_web/assets/App-JXePnJiV.css"]
}
return indexes.map((i) => __vite__mapDeps.viteFileDeps[i])
}
+1
View File
@@ -0,0 +1 @@
send2trash
+1 -1
View File
@@ -9,7 +9,7 @@ from .model_manager.model_preview import get_thumbnail_for_image_file
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
video_extensions = ['.mp4', '.mov', '.avi', '.webm', '.mkv']
API_URL = 'https://www.nodecafe.org/api/image/upload'
API_URL = 'https://www.nodecafe.co/api/image/upload'
def view_media(filename, isPreview = False, isInput = False):
if not filename:
return web.Response(status=404)
+9 -1
View File
@@ -5,6 +5,10 @@ import os
from pathlib import Path
import server
from .db_service import get_my_workflows_dir
try:
from send2trash import send2trash
except ImportError:
send2trash = None
@server.PromptServer.instance.routes.post('/workspace/folder/create')
async def create_folder(request):
@@ -31,7 +35,11 @@ def delete_folder_sync(reqJson):
folder_path = reqJson.get('path')
folder_path = os.path.join(get_my_workflows_dir(), folder_path)
try:
shutil.rmtree(folder_path)
if send2trash:
send2trash(folder_path)
else:
shutil.rmtree(folder_path)
print("❌⛔️send2trash is not available. Deleting file permanently. Please `pip install send2trash`")
return {"success": True}
except Exception as e:
return {"success": False, "error": str(e)}
+39 -41
View File
@@ -157,7 +157,7 @@ export default function App() {
if (id) {
const flow = await workflowsTable?.get(id);
flow && setCurFlowIDAndName(flow);
if (flow && flow.json != latestWf) {
if (!flow?.saveLock && flow && flow.json != latestWf) {
setIsDirty(true);
}
}
@@ -167,47 +167,45 @@ export default function App() {
}
fetchApi("/workspace/deduplicate_workflow_ids");
const twoway = await userSettingsTable?.getSetting("twoWaySync");
!twoway &&
indexdb.cache.get(UPGRADE_TO_2WAY_SYNC_KEY).then(async (value) => {
if (value?.value !== "true") {
const workflowsCount = await indexdb.workflows.count();
if (workflowsCount == 0) {
// empty workflows, enable 2way sync
await userSettingsTable?.upsert({ twoWaySync: true });
await indexdb.cache.put({
id: UPGRADE_TO_2WAY_SYNC_KEY,
value: "true",
});
return;
}
const myWorkflowsDir =
await userSettingsTable?.getSetting("myWorkflowsDir");
showDialog(
<EnableTwowaySyncConfirm
myWorkflowsDir={myWorkflowsDir ?? "undefined"}
/>,
[
{
label: "I have downloaded all my workflows and ready to enable",
onClick: async () => {
await userSettingsTable?.upsert({ twoWaySync: true });
if (await userSettingsTable?.getSetting("twoWaySync")) {
indexdb.cache.put({
id: UPGRADE_TO_2WAY_SYNC_KEY,
value: "true",
});
location.reload();
}
},
colorScheme: "red",
},
],
{
closeOnOverlayClick: false,
if (!twoway) {
const workflowsCount = await indexdb.workflows.count();
if (workflowsCount == 0) {
// empty workflows, enable 2way sync
await userSettingsTable?.upsert({ twoWaySync: true });
await indexdb.cache.put({
id: UPGRADE_TO_2WAY_SYNC_KEY,
value: "true",
});
return;
}
const myWorkflowsDir =
await userSettingsTable?.getSetting("myWorkflowsDir");
showDialog(
<EnableTwowaySyncConfirm
myWorkflowsDir={myWorkflowsDir ?? "undefined"}
/>,
[
{
label: "I have downloaded all my workflows and ready to enable",
onClick: async () => {
await userSettingsTable?.upsert({ twoWaySync: true });
if (await userSettingsTable?.getSetting("twoWaySync")) {
indexdb.cache.put({
id: UPGRADE_TO_2WAY_SYNC_KEY,
value: "true",
});
location.reload();
}
},
);
}
});
colorScheme: "red",
},
],
{
closeOnOverlayClick: false,
},
);
}
};
const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => {
-9
View File
@@ -44,7 +44,6 @@ import CreateVersionDialog from "./CreateVersionDialog";
import HoverMenu from "./HoverMenu";
const ShareDialog = lazy(() => import("../share/ShareDialog"));
import { TOPBAR_BUTTON_HEIGHT } from "../const";
import DownloadSpaceJsonDialog from "../spacejson/DownloadSpaceJsonDialog";
import { downloadJsonFile } from "../utils/downloadJsonFile";
import { SharedTopbarButton } from "../share/SharedTopbarButton";
import { app } from "../utils/comfyapp";
@@ -186,13 +185,6 @@ export default function DropdownTitle() {
>
Versions History
</MenuItem>
{/* <MenuItem
onClick={() => setRoute("downloadSpaceJson")}
icon={<IconDownload size={20} />}
iconSpacing={1}
>
Save runnable workflow🧪
</MenuItem> */}
<MenuItem
onClick={() => setRoute("share")}
icon={<IconShare2 size={20} />}
@@ -212,7 +204,6 @@ export default function DropdownTitle() {
{route == "versionHistory" && (
<VersionHistoryDrawer onClose={() => setRoute("root")} />
)}
{route == "downloadSpaceJson" && <DownloadSpaceJsonDialog />}
{route === "saveAsModal" && (
<Modal isOpen={true} onClose={handleOnCloseModal}>
<ModalOverlay />
+1 -1
View File
@@ -81,7 +81,7 @@ export default function EditFlowName({
as="div"
color="white"
onClick={startEdit}
maxW={200}
maxW={160}
fontWeight={500}
whiteSpace="nowrap"
overflow="hidden"
+1 -2
View File
@@ -44,9 +44,8 @@ export class ChangelogsTable extends TableBase<Changelog> {
createTime: Date.now(),
isAutoSave: input.isAutoSave,
};
await this.deleteLogsExceedLimit(input.workflowID);
await indexdb.changelogs.add(change);
// this.saveDiskDB(); // its not that important to save to disk for changelog table
await this.deleteLogsExceedLimit(input.workflowID);
return change;
}
async deleteLogsExceedLimit(workflowID: string) {
-93
View File
@@ -1,59 +1,6 @@
import { deleteFile, updateFile } from "../Api";
import { foldersTable, workflowsTable, userSettingsTable } from "./WorkspaceDB";
import {
COMFYSPACE_TRACKING_FIELD_NAME,
LEGACY_COMFYSPACE_TRACKING_FIELD_NAME,
} from "../const";
import { toFileNameFriendly } from "../utils";
import { Folder, Workflow } from "../types/dbTypes";
import { TwowayFolderSyncAPI } from "../apis/TwowaySyncFolderApi";
export async function saveJsonFileMyWorkflows(workflow: Workflow) {
const file_path = await generateFilePath(workflow);
if (file_path == null) {
return;
}
const fullPath = await generateFilePathAbsolute(workflow);
await workflowsTable?.updateMetaInfo(workflow.id, {
filePath: fullPath ?? undefined,
});
const json = workflow.json;
const flow = JSON.parse(json);
flow.extra[COMFYSPACE_TRACKING_FIELD_NAME] = {
id: workflow.id,
name: workflow.name,
};
delete flow.extra[LEGACY_COMFYSPACE_TRACKING_FIELD_NAME];
await updateFile(file_path, JSON.stringify(flow));
}
export async function deleteJsonFileMyWorkflows(workflow: Workflow) {
if (workflow.name == null) {
return;
}
const file_path = await generateFilePath(workflow);
file_path != null && deleteFile(file_path);
}
export async function generateFilePath(
workflow: Workflow,
): Promise<string | null> {
let filePath = toFileNameFriendly(workflow.name) + ".json";
let curFolderID = workflow.parentFolderID;
while (curFolderID != null) {
const folder = await foldersTable?.get(curFolderID);
if (folder == null) {
break;
}
const folderName = folder.name;
filePath = `${folderName}/${filePath}`;
curFolderID = folder.parentFolderID ?? undefined;
}
return filePath ?? null;
}
export async function genFolderRelPath(
folderId: string | null,
): Promise<string> {
@@ -71,46 +18,6 @@ export async function genFolderRelPath(
return filePath;
}
export async function generateFilePathAbsolute(
workflow: Workflow,
): Promise<string | null> {
const subPath = await generateFilePath(workflow);
let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir");
if (!myWorkflowsDir) {
console.error("myWorkflowsDir is not set");
return null;
}
if (!myWorkflowsDir.endsWith("/")) {
myWorkflowsDir = myWorkflowsDir + "/";
}
return myWorkflowsDir + subPath;
}
export async function generateFolderPath(id: string): Promise<string | null> {
const folder = await foldersTable?.get(id);
let parentFolderID = folder?.parentFolderID;
let folderPath = folder?.name;
while (parentFolderID) {
const folder = await foldersTable?.get(parentFolderID);
if (folder == null) {
break;
}
folderPath = `${folder.name}/${folderPath}`;
parentFolderID = folder.parentFolderID ?? undefined;
}
let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir");
if (!myWorkflowsDir) {
console.error("myWorkflowsDir is not set");
return null;
}
if (!myWorkflowsDir.endsWith("/")) {
myWorkflowsDir = myWorkflowsDir + "/";
}
return myWorkflowsDir + folderPath;
}
export async function getFileCountInFolder(folderId: string): Promise<number> {
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
-10
View File
@@ -2,7 +2,6 @@ import { Table, workflowsTable } from "./WorkspaceDB";
import { TableBase } from "./TableBase";
import { Media } from "../types/dbTypes";
import { indexdb } from "./indexdb";
import { getMetadataFromUrl } from "../gallery/utils.ts";
export class MediaTable extends TableBase<Media> {
static readonly TABLE_NAME: Table = "media";
@@ -25,20 +24,11 @@ export class MediaTable extends TableBase<Media> {
const format = input.localPath.split(".").pop();
if (format == null) return null;
const res = await getMetadataFromUrl(
`/workspace/view_media?filename=${input.localPath}`,
).catch((e) => {
console.error("get meta error:", e);
return { prompt: "" };
});
const md: Media = {
id: input.localPath,
localPath: input.localPath,
workflowID: input.workflowID,
createTime: Date.now(),
workflowJSON: JSON.stringify(res?.prompt),
format: format,
};
await workflowsTable?.updateMetaInfo(input.workflowID, {
latestImage: md.localPath,
+1 -1
View File
@@ -43,7 +43,7 @@ export class UserSettingsTable extends TableBase<UserSettings> {
autoSaveDuration: 3,
twoWaySync: false,
foldersOnTop: false,
cloudHost: "https://www.nodecafe.org",
cloudHost: "https://www.nodecafe.co",
overwriteCurWorkflowWhenDroppingFileToCanvas: false,
maximumChangelogNumber: 80,
hideCoverImage: false,
+1 -28
View File
@@ -8,10 +8,6 @@ import { sortFileItem } from "../utils";
import { nanoid } from "nanoid";
import { TableBase } from "./TableBase";
import { indexdb } from "./indexdb";
import {
deleteJsonFileMyWorkflows,
saveJsonFileMyWorkflows,
} from "./DiskFileUtils";
import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types";
import { defaultGraph } from "../defaultGraph";
import { scanMyWorkflowsDir } from "../utils/twowaySyncUtils";
@@ -51,11 +47,7 @@ export class WorkflowsTable extends TableBase<Workflow> {
}
/**
* Check whether the currently opened workflow is the latest version and is consistent with the DB.
* Through updateTime comparison, if it is inconsistent, it means that a newer version already exists in the DB.
* If the automatic save function is turned on at this time, the latest version in the DB will be overwritten.
* Therefore, the user needs to confirm whether to continue to enable automatic saving.
* @returns boolean
* Check whether the currently opened workflow is the latest version and is consistent with the DB
*/
public async latestVersionCheck() {
if (this._curWorkflow) {
@@ -146,8 +138,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
if (twoWaySyncEnabled) {
await TwowaySyncAPI.createWorkflow(newWorkflow);
return await this.get(newWorkflow.id);
} else {
saveJsonFileMyWorkflows(newWorkflow);
}
return newWorkflow;
}
@@ -197,15 +187,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
}
if (twoWaySyncEnabled) {
await TwowaySyncAPI.moveWorkflow(oldWorkflow, change.parentFolderID!);
} else {
await saveJsonFileMyWorkflows(newWorkflow);
await deleteJsonFileMyWorkflows(oldWorkflow);
// update parent folder updateTime
if (newWorkflow?.parentFolderID != null) {
await indexdb.folders?.update(newWorkflow.parentFolderID, {
updateTime: Date.now(),
});
}
}
}
public async genLastManualSavedJson(id: string) {
@@ -240,8 +221,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
const twoWaySyncEnabled = await userSettingsTable?.getSetting("twoWaySync");
if (twoWaySyncEnabled) {
after && (await TwowaySyncAPI.saveWorkflow(after));
} else {
after && (await saveJsonFileMyWorkflows(after));
}
}
@@ -294,8 +273,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
} catch (e) {
console.error("batchCreateFlows error", e);
}
} else {
await saveJsonFileMyWorkflows(newWorkflow);
}
}
try {
@@ -314,8 +291,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
await userSettingsTable?.getSetting("twoWaySync");
if (twoWaySyncEnabled) {
await TwowaySyncAPI.deleteWorkflow(workflow);
} else {
deleteJsonFileMyWorkflows({ ...workflow });
}
}
//add to IndexDB
@@ -331,8 +306,6 @@ export class WorkflowsTable extends TableBase<Workflow> {
await userSettingsTable?.getSetting("twoWaySync");
if (twoWaySyncEnabled) {
await TwowaySyncAPI.deleteWorkflow(flow);
} else {
deleteJsonFileMyWorkflows({ ...flow });
}
}
await indexdb.workflows.bulkDelete(ids);
+4
View File
@@ -35,6 +35,10 @@ const zIndices = {
};
const theme = extendTheme({
config: {
initialColorMode: "dark",
useSystemColorMode: false,
},
zIndices,
components: {
Card: {
@@ -14,7 +14,7 @@ import { useEffect, useState } from "react";
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../../const";
import type {
DepsResult,
ModelFile,
ModelDep,
} from "../../spacejson/handleDownloadSpaceJson";
import { indexdb } from "../../db-tables/indexdb";
import { app } from "../../utils/comfyapp";
@@ -32,8 +32,8 @@ export interface MissingModel {
interface Props {
model: MissingModel;
setSelectedModelDeps: (deps: ModelFile[]) => void;
selectedModelDeps: ModelFile[];
setSelectedModelDeps: (deps: ModelDep[]) => void;
selectedModelDeps: ModelDep[];
foldersList: Record<string, string[]>;
setFoldersList: (folders: Record<string, string[]>) => void;
}
@@ -46,7 +46,7 @@ export default function MissingModelItem({
setFoldersList,
}: Props) {
const [suggestedModel, setSuggestedModel] = useState<
ModelFile | { existingModel: string }
ModelDep | { existingModel: string }
>();
const modelDeps: DepsResult["models"] =
app.graph.extra?.[COMFYSPACE_TRACKING_FIELD_NAME]?.deps?.models;
@@ -76,7 +76,7 @@ export default function MissingModelItem({
const selectedModelNames = selectedModelDeps.map((dep) => dep.filename);
function isExistingModel(
model: ModelFile | { existingModel: string },
model: ModelDep | { existingModel: string },
): model is { existingModel: string } {
return (model as { existingModel: string }).existingModel != null;
}
@@ -12,7 +12,7 @@ import {
import { useCallback, useContext, useEffect, useState } from "react";
import MissingModelItem from "./MissingModelItem";
import { DRAWER_Z_INDEX } from "../../const";
import type { ModelFile } from "../../spacejson/handleDownloadSpaceJson";
import type { ModelDep } from "../../spacejson/handleDownloadSpaceJson";
import { getAllFoldersList } from "../../Api";
import { installModelsApi } from "../api/modelsApi";
import { getCivitApiKey } from "../../utils/civitUtils";
@@ -40,7 +40,7 @@ export default function MissingModelsListDrawer({
app.canvasEl.removeEventListener("click", onClose);
};
}, []);
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelFile[]>([]);
const [selectedModelDeps, setSelectedModelDeps] = useState<ModelDep[]>([]);
const [foldersList, setFoldersList] = useState<Record<string, string[]>>({});
const { setRoute } = useContext(WorkspaceContext);
const loadData = useCallback(async () => {
@@ -77,7 +77,7 @@ export default function MissingModelsListDrawer({
}
installModelsApi({
file_hash: model.fileHash ?? undefined,
filename: model.filename,
filename: model.name,
save_path: foldersList[model.fileFolder][0],
url,
force_filename: true,
@@ -13,10 +13,6 @@ import type { ModelsListRespItemFromApi } from "../types";
import InatallModelsModal from "../install-models/InstallModelsModal";
import { TOPBAR_BUTTON_HEIGHT } from "../../const";
const AddMissingModelsButton = lazy(
() => import("./InstallMissingModelsButton"),
);
export default function ModelManagerTopbar() {
const { setRoute, route } = useContext(WorkspaceContext);
@@ -75,11 +71,6 @@ export default function ModelManagerTopbar() {
>
Models
</Button>
<div style={{ position: "absolute", top: "38px", left: "0px" }}>
<Suspense>
<AddMissingModelsButton />
</Suspense>
</div>
{route === "modelList" && (
<ModelsListDrawer onClose={() => setRoute("root")} />
)}
+20 -28
View File
@@ -1,27 +1,29 @@
import {
Input,
Stack,
useToast,
Text,
HStack,
Flex,
Button,
} from "@chakra-ui/react";
import { useEffect, useRef } from "react";
import { Input, Stack, useToast, Text, Flex, Button } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { userSettingsTable } from "../db-tables/WorkspaceDB";
export default function CloudHostSetting() {
const inputRef = useRef<HTMLInputElement>(null);
const toast = useToast();
const [text, setText] = useState("");
const getSettings = () => {
userSettingsTable?.getSetting("cloudHost").then((res) => {
inputRef.current?.setAttribute("value", res ?? "");
setText(res ?? "");
});
};
useEffect(() => {
getSettings();
}, []);
const submitChange = async (text: string) => {
await userSettingsTable?.upsert({ cloudHost: text });
getSettings();
toast({
title: "Setting saved. Please refresh to see the changes.",
status: "success",
duration: 4000,
});
};
return (
<Stack>
@@ -30,26 +32,16 @@ export default function CloudHostSetting() {
</Text>
<Flex gap={2} alignItems={"center"}>
<Input
ref={inputRef}
onBlur={() => {
console.log("onBlur", inputRef.current?.value);
userSettingsTable?.upsert({ cloudHost: inputRef.current?.value });
getSettings();
toast({
title: "Setting saved. Please refresh to see the changes.",
status: "success",
duration: 4000,
});
}}
value={text}
onChange={(e) => setText(e.target.value)}
onBlur={() => submitChange(text)}
/>
<Button
size={"sm"}
onClick={() => {
inputRef.current?.setAttribute(
"value",
userSettingsTable?.defaultSettings.cloudHost!,
);
inputRef.current?.blur();
const cloudHost = userSettingsTable?.defaultSettings.cloudHost!;
setText(cloudHost);
submitChange(cloudHost);
}}
>
Reset
+97 -100
View File
@@ -10,7 +10,6 @@ import {
ModalContent,
ModalHeader,
ModalOverlay,
Radio,
RadioGroup,
Stack,
Switch,
@@ -19,14 +18,10 @@ import {
useToast,
} from "@chakra-ui/react";
import { WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes";
import { useEffect, useRef, useState } from "react";
import { useContext, useEffect, useState } from "react";
import CustomSelector from "../components/CustomSelector";
import { IconCopy, IconExternalLink } from "@tabler/icons-react";
import {
userSettingsTable,
workflowVersionsTable,
workflowsTable,
} from "../db-tables/WorkspaceDB";
import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB";
// @ts-ignore
import { api } from "/scripts/api.js";
import {
@@ -37,18 +32,30 @@ import {
privacyOptions,
} from "./shareUtils";
import ShareDialogWorkflowVersionRadio from "./ShareDialogWorkflowVersionRadio";
import ResourceDepsForm from "../spacejson/ResourceDepsForm";
import { app } from "../utils/comfyapp";
import { useStateRef } from "../customHooks/useStateRef";
import { nanoid } from "nanoid";
import { WorkspaceContext } from "../WorkspaceContext";
import {
DepsResult,
WorkspaceInfoDeps,
extractAndFetchFileNames,
} from "../spacejson/handleDownloadSpaceJson";
import { fetchApi } from "../Api";
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
interface Props {
onClose: () => void;
}
export default function ShareDialog({ onClose }: Props) {
const [versionName, setVersionName] = useState("v" + getCurDateString());
const [localVersions, setLocalVersions] = useState<WorkflowVersion[]>([]);
const [versionName, setVersionName, versionNameRef] = useStateRef(
"v" + getCurDateString(),
);
const { saveCurWorkflow } = useContext(WorkspaceContext);
const [deps, setDeps] = useState<DepsResult>();
const [uploadingImage, setUploadingImage] = useState(false);
const [loading, setLoading] = useState(false);
const cloudHost = userSettingsTable?.settings?.cloudHost;
const [privacy, setPrivacy, privacyRef] =
@@ -56,10 +63,8 @@ export default function ShareDialog({ onClose }: Props) {
const [selectedVersion, setSelectedVersion] = useState<
string | "new_version"
>("new_version");
const cloudHostRef = useRef("");
const workflow = workflowsTable?.curWorkflow;
const toast = useToast();
const formRef = useRef<HTMLFormElement>(null);
const [enableDeps, setEnableDeps] = useState(true);
const handleShareWorkflowSuccess = async (event: MessageEvent) => {
const detail = event.data;
@@ -71,7 +76,6 @@ export default function ShareDialog({ onClose }: Props) {
}
const localID = detail.localWorkflowID;
const localVerID = detail.localVersionID;
const cloudVersionID = detail.version.id;
const cloudID = detail.version.workflowID;
@@ -79,56 +83,34 @@ export default function ShareDialog({ onClose }: Props) {
localID &&
(await workflowsTable?.updateMetaInfo(localID, {
cloudID: cloudID,
cloudOrigin: event.origin,
privacy: privacyRef.current,
}));
localVerID &&
cloudVersionID &&
(await workflowVersionsTable?.update(localVerID, {
cloudID: cloudVersionID,
cloudOrigin: event.origin,
}));
loadData();
window.open(
cloudHostRef.current + "/workflow/" + cloudID + "/" + cloudVersionID,
cloudHost + "/workflow/" + cloudID + "/" + cloudVersionID,
"_blank",
);
setLoading(false);
};
useEffect(() => {
loadData();
const graph = app.graph.serialize();
extractAndFetchFileNames(graph.nodes ?? []).then((depsRes) => {
setDeps(depsRes);
});
window.addEventListener("message", handleShareWorkflowSuccess);
const shareDepsToCloud = (event: any) => {
if (event.detail) {
pushToCloud(event.detail ? JSON.stringify(event.detail) : undefined);
}
};
window.addEventListener("workspace_info_deps_updated", shareDepsToCloud);
return () => {
window.removeEventListener("message", handleShareWorkflowSuccess);
window.removeEventListener(
"workspace_info_deps_updated",
shareDepsToCloud,
);
};
}, []);
const loadData = async () => {
const host =
import.meta.env.MODE === "production"
? await userSettingsTable?.getSetting("cloudHost")
: "http://localhost:3000";
if (host) {
cloudHostRef.current = host;
}
if (workflow?.cloudID && workflow.cloudOrigin) {
fetchCloudWorkflowPrivacy(workflow).then((privacy) => {
setPrivacy(privacy);
});
}
workflowVersionsTable?.listByWorkflowID(workflow!.id).then((vers) => {
setLocalVersions(vers);
});
};
const copyTextToClipboard = (text: string) => {
navigator.clipboard
@@ -145,7 +127,24 @@ export default function ShareDialog({ onClose }: Props) {
console.error("Failed to copy text: ", err);
});
};
const pushToCloud = async (jsonToShare?: string) => {
const pushToCloud = async (imageDeps?: DepsResult["images"]) => {
const graph = app.graph.serialize();
(graph.extra ||= {})[COMFYSPACE_TRACKING_FIELD_NAME] ||= {};
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].deps = {
models: deps?.models ?? {},
images: imageDeps ?? {},
nodeRepos: deps?.nodeRepos ?? [],
} as WorkspaceInfoDeps;
await saveCurWorkflow();
const apiPrompt = await app
.graphToPrompt(app.graph)
.then((data: { output: any; workflow: any }) => {
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].apiPrompt = data.output;
return data.output;
});
const jsonToShare = JSON.stringify(graph);
setLoading(true);
const secretKey = generateRandomKey(32);
const host =
@@ -153,30 +152,15 @@ export default function ShareDialog({ onClose }: Props) {
? ((await userSettingsTable?.getSetting("cloudHost")) as string)
: "http://localhost:3000";
let nodeDefs: Object;
let version: WorkflowVersion | undefined;
try {
nodeDefs = getNodeDefs();
if (selectedVersion === "new_version") {
version = await workflowVersionsTable?.add({
workflowID: workflow!.id,
name: versionName,
createTime: Date.now(),
json: jsonToShare ?? JSON.stringify(app.graph.serialize()),
});
} else {
version = await workflowVersionsTable?.get(selectedVersion);
}
if (!version) {
alert(`Failed to find version: ${selectedVersion}, please try again.`);
setLoading(false);
return;
}
} catch (e) {
console.error(e);
setLoading(false);
alert("Failed to share workflow, please try again.");
return;
}
nodeDefs = getNodeDefs();
const version: WorkflowVersion = {
id: nanoid(),
workflowID: workflow!.id,
name: versionNameRef.current,
createTime: Date.now(),
api_prompt: JSON.stringify(apiPrompt),
json: jsonToShare ?? JSON.stringify(app.graph.serialize()),
};
const protocol = window.location.protocol;
const baseHost = window.location.host;
const baseUrl = `${protocol}//${baseHost}`;
@@ -205,12 +189,33 @@ export default function ShareDialog({ onClose }: Props) {
};
window.addEventListener("message", handleChildReady);
};
const onShare = async () => {
const imageDeps = deps?.images ?? {};
if (enableDeps) {
formRef.current?.requestSubmit();
} else {
pushToCloud();
// upload images
setUploadingImage(true);
const imageDepsToUpload = Object.values(deps?.images ?? {})
// .filter((i) => !i.url)
.map((i) => i.filename);
if (imageDepsToUpload.length) {
const uploadResp = await fetchApi("/workspace/upload_image", {
method: "POST",
body: JSON.stringify({
images: imageDepsToUpload,
}),
});
const json = (await uploadResp.json()) as Record<string, string>;
Object.keys(json).forEach((key) => {
if (imageDeps?.[key]) {
imageDeps[key].url = json[key];
}
});
}
setUploadingImage(false);
}
pushToCloud(imageDeps);
};
const cloudWorkflowID = workflowsTable?.curWorkflow?.cloudID;
@@ -284,7 +289,6 @@ export default function ShareDialog({ onClose }: Props) {
</Button>
</HStack>
)}
{/* <Text>Choose a version to share:</Text> */}
<RadioGroup
gap={4}
onChange={(val) => {
@@ -294,7 +298,6 @@ export default function ShareDialog({ onClose }: Props) {
>
<Stack>
<HStack mb={5} alignItems={"center"}>
{/* <Radio key={"new_version"} value="new_version" /> */}
<Input
value={versionName}
width={"60%"}
@@ -309,37 +312,31 @@ export default function ShareDialog({ onClose }: Props) {
<Badge colorScheme="purple">Version Name</Badge>
</Flex>
</HStack>
{/* {localVersions.slice(0, 4).map((ver) => {
return (
<ShareDialogWorkflowVersionRadio
key={ver.id}
version={ver}
cloudHost={cloudHost}
cloudWorkflowID={cloudWorkflowID ?? null}
/>
);
})} */}
</Stack>
</RadioGroup>
<Stack borderRadius={6} borderWidth={"1px"} p={2}>
<Switch
isChecked={enableDeps}
onChange={() => setEnableDeps(!enableDeps)}
fontWeight={"600"}
>
Share Resource Links
</Switch>
{enableDeps && (
<>
<span style={{ color: "GrayText" }}>
You can disable reource sharing if you don't want to resolve
submit errors
</span>
<ResourceDepsForm ref={formRef} />
</>
)}
</Stack>
{Object.keys(deps?.images ?? {}).length > 0 && (
<Stack borderRadius={6} borderWidth={"1px"} p={2}>
<Switch
isChecked={enableDeps}
onChange={() => setEnableDeps(!enableDeps)}
fontWeight={"600"}
>
Share input images
</Switch>
<span style={{ color: "GrayText" }}>
If enabled your input images will be uploaded and shared to{" "}
{cloudHost}
</span>
{enableDeps && (
<ResourceDepsForm
deps={deps ?? null}
setDeps={setDeps}
uploadingImage={uploadingImage}
/>
)}
</Stack>
)}
</Stack>
</ModalBody>
</ModalContent>
@@ -1,54 +0,0 @@
import { useContext, useRef } from "react";
import { WorkspaceContext } from "../WorkspaceContext";
import {
Badge,
Button,
HStack,
Heading,
Modal,
ModalBody,
ModalContent,
Text,
Tooltip,
} from "@chakra-ui/react";
import { IconInfoCircle } from "@tabler/icons-react";
import ResourceDepsForm from "./ResourceDepsForm";
export default function DownloadSpaceJsonDialog() {
const { setRoute } = useContext(WorkspaceContext);
const formRef = useRef<HTMLFormElement>(null);
return (
<Modal isOpen={true} onClose={() => setRoute("root")} size={"xl"}>
<ModalContent>
<ModalBody p={5} gap={5}>
<HStack>
<Heading size={"md"}>Add Resouce Dependencies</Heading>
<Tooltip
label="runnable workflows include all resource dependecy download urls
(models, cursom nodes, input images) and can be one-click
installed and run in any ComfyUI with workpace-manager installed."
>
<Badge borderRadius={10}>
<IconInfoCircle size={20} />
</Badge>
</Tooltip>
</HStack>
<Text color={"GrayText"}>Export runnable workflow</Text>
<ResourceDepsForm ref={formRef} />
<HStack justifyContent={"flex-end"}>
<Button
colorScheme="teal"
size="sm"
onClick={() => formRef.current?.requestSubmit()}
>
Next
</Button>
<Button size="sm" onClick={() => setRoute("share")}>
Skip
</Button>
</HStack>
</ModalBody>
</ModalContent>
</Modal>
);
}
+32 -323
View File
@@ -1,335 +1,44 @@
import {
forwardRef,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { WorkspaceContext } from "../WorkspaceContext";
import {
Button,
Flex,
HStack,
Heading,
IconButton,
Image,
Input,
Spinner,
Stack,
Tag,
Text,
Tooltip,
} from "@chakra-ui/react";
import {
DepsResult,
ModelFile,
WorkspaceInfoDeps,
extractAndFetchFileNames,
} from "./handleDownloadSpaceJson";
import { IconExternalLink, IconPencil, IconRefresh } from "@tabler/icons-react";
import { getHgModelInfoUrlFromDownloadUrl } from "../utils/civitUtils";
import { fetchApi, getAllModelsList } from "../Api";
import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const";
import { indexdb } from "../db-tables/indexdb";
import { app } from "../utils/comfyapp";
import { HStack, Heading, Image, Spinner, Stack } from "@chakra-ui/react";
import { DepsResult } from "./handleDownloadSpaceJson";
export default forwardRef<HTMLFormElement>(
function ResourceDepsForm(_props, ref) {
const { saveCurWorkflow, setRoute } = useContext(WorkspaceContext);
const [deps, setDeps] = useState<DepsResult>();
const [errors, setErrors] = useState<Record<string, string>>({});
const [uploadingImage, setUploadingImage] = useState(false);
const init = useCallback(async () => {
const graph = app.graph.serialize();
extractAndFetchFileNames(graph.nodes ?? []).then((depsRes) => {
setDeps(depsRes);
});
}, []);
const onRefreshModelsList = useCallback(async () => {
setDeps(undefined);
await getAllModelsList();
window.addEventListener("model_list_updated", () => {
init();
window.removeEventListener("model_list_updated", () => {});
});
}, []);
useEffect(() => {
init();
}, []);
const validateURL = (url: string) => {
const pattern = new RegExp(
"^(https?:\\/\\/)?" +
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" +
"((\\d{1,3}\\.){3}\\d{1,3}))" +
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" +
"(\\?[;&a-z\\d%_.~+=-]*)?" +
"(\\#[-a-z\\d_]*)?$",
"i",
);
return !!pattern.test(url);
};
const handleSubmit = async (event: any) => {
event.preventDefault();
event.stopPropagation();
const formData = new FormData(event.target);
// validate model deps and save model urls to db
const saveModelUrlDBPromises: Promise<ModelFile>[] =
Object.values(deps?.models ?? {}).map(async (model) => {
const inputDownloadUrl = formData.get(model.filename)?.toString();
if (inputDownloadUrl?.length) {
// because the models table is storing filename without extension as id, which should be migrated
const baseName = getBaseFileName(model.filename);
await indexdb.models.update(baseName + "@" + model.fileFolder, {
downloadUrl: inputDownloadUrl,
});
}
const infoUrl = model.infoUrl?.length
? model.infoUrl
: inputDownloadUrl
? getHgModelInfoUrlFromDownloadUrl(inputDownloadUrl)
: null;
return {
filename: model.filename,
nodeType: model.nodeType,
fileHash: model.fileHash,
fileFolder: model.fileFolder,
downloadUrl: inputDownloadUrl ?? model.downloadUrl ?? null,
infoUrl: infoUrl ?? null,
inputName: model.inputName,
};
}) ?? [];
const modelDepsArr = await Promise.all(saveModelUrlDBPromises);
if (!validateModelDeps(modelDepsArr)) {
return;
}
setErrors({});
// upload images
setUploadingImage(true);
const imageDepsToUpload = Object.values(deps?.images ?? {})
.filter((i) => !i.url)
.map((i) => i.filename);
const imageDeps = deps?.images ?? {};
if (imageDepsToUpload.length) {
const uploadResp = await fetchApi("/workspace/upload_image", {
method: "POST",
body: JSON.stringify({
images: imageDepsToUpload,
}),
});
const json = (await uploadResp.json()) as Record<string, string>;
Object.keys(json).forEach((key) => {
if (imageDeps?.[key]) {
imageDeps[key].url = json[key];
}
});
}
setUploadingImage(false);
// save deps to graph
const modelDeps: WorkspaceInfoDeps["models"] = {};
modelDepsArr.forEach((model) => {
modelDeps[model.filename] = model;
});
const graph = app.graph.serialize();
(graph.extra ||= {})[COMFYSPACE_TRACKING_FIELD_NAME] ||= {};
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].deps = {
models: modelDeps,
images: imageDeps ?? {},
nodeRepos: deps?.nodeRepos ?? [],
} as WorkspaceInfoDeps;
await app
.graphToPrompt(app.graph)
.then((data: { output: any; workflow: any }) => {
graph.extra[COMFYSPACE_TRACKING_FIELD_NAME].apiPrompt = data.output;
});
await saveCurWorkflow();
window.dispatchEvent(
new CustomEvent("workspace_info_deps_updated", {
detail: graph,
}),
);
console.log("graph", graph);
};
const validateModelDeps = useCallback((modelDeps: ModelFile[]) => {
let isValid = true;
modelDeps.forEach((model) => {
const key = model.filename;
if (!model.downloadUrl) {
setErrors((prev) => ({
...prev,
[key]: "Invalid URL",
}));
isValid = false;
return;
}
if (!validateURL(model.downloadUrl)) {
setErrors((prev) => ({ ...prev, [key]: "Invalid URL" }));
isValid = false;
return;
}
if (!model.fileFolder) {
setErrors((prev) => ({
...prev,
[key]: "Model file not found, please re-scan models folder",
}));
isValid = false;
return;
}
});
return isValid;
}, []);
const imageDepsArr = Object.values(deps?.images ?? {});
const modelDepsArr = Object.values(deps?.models ?? []);
if (!deps) {
return <Spinner size="md" color="teal.400" />;
}
return (
<form onSubmit={handleSubmit} ref={ref}>
<Stack gap={5}>
<Stack>
<Heading size={"sm"}>Models ({modelDepsArr.length})</Heading>
{modelDepsArr.map((modelFile, index) => (
<ModelDepsItem
modelFile={modelFile}
key={index}
errors={errors}
onClickRefetchModelList={onRefreshModelsList}
/>
))}
</Stack>
{imageDepsArr.length > 0 && (
<Stack>
<HStack>
<Heading size={"sm"}>Images ({imageDepsArr.length})</Heading>
{/* <Tag colorScheme="yellow">Will be uploaded as url</Tag> */}
<p style={{ color: "GrayText" }}>Will be uploaded as url</p>
</HStack>
{uploadingImage && (
<span>
<Spinner size="md" color="teal.400" /> Uploading
</span>
)}
{imageDepsArr.map((image) => (
<Stack key={image.filename}>
<p>{image.filename}</p>
<Image
width={250}
src={`/workspace/view_media?filename=${image.filename}&isPreview=true&isInput=true`}
/>
</Stack>
))}
</Stack>
)}
</Stack>
</form>
);
},
);
function getBaseFileName(filename: string) {
const parts = filename.split(".");
if (parts.length > 1) {
parts.pop();
return parts.join(".");
} else {
return filename;
}
}
function ModelDepsItem({
modelFile,
errors,
onClickRefetchModelList,
export default function ResourceDepsForm({
deps,
uploadingImage,
}: {
modelFile: DepsResult["models"][0];
errors: Record<string, string>;
onClickRefetchModelList: () => void;
deps: DepsResult | null;
setDeps: (deps: DepsResult) => void;
uploadingImage: boolean;
}) {
const inputKey = modelFile.filename;
const [isEditing, setIsEditing] = useState(!modelFile.downloadUrl);
if (!modelFile.fileFolder) {
return (
<Stack>
<Flex gap={2}>
<p>{modelFile.filename}</p>
<Tag size={"sm"} colorScheme={"red"}>
Model file not found
</Tag>
</Flex>
<Button
size={"sm"}
leftIcon={<IconRefresh />}
width={"fit-content"}
borderColor={"red"}
borderWidth={errors[inputKey] != null ? 2 : 0}
onClick={onClickRefetchModelList}
>
Fetch model file
</Button>
</Stack>
);
const imageDepsArr = Object.values(deps?.images ?? {});
if (!deps) {
return <Spinner size="md" color="teal.400" />;
}
return (
<Stack ml={2}>
<Flex key={modelFile.filename + modelFile.nodeType} gap={2}>
<Tag height={"fit-content"} colorScheme={"teal"}>
{modelFile.fileFolder}
</Tag>
{modelFile.downloadUrl ? (
<Stack gap={5}>
{imageDepsArr.length > 0 && (
<Stack>
<HStack>
<a
href={modelFile.infoUrl ?? modelFile.downloadUrl}
target="_blank"
>
{modelFile.filename}
</a>
<IconExternalLink size={18} />
<IconButton
ml={3}
aria-label="edit"
icon={<IconPencil size={18} />}
size={"xs"}
onClick={() => setIsEditing(!isEditing)}
/>
<Heading size={"sm"}>Images ({imageDepsArr.length})</Heading>
{/* <Tag colorScheme="yellow">Will be uploaded as url</Tag> */}
<p style={{ color: "GrayText" }}>Will be uploaded as url</p>
</HStack>
) : (
<Stack>
{uploadingImage && (
<span>
{modelFile.filename} <span>❓</span>
<Spinner size="md" color="teal.400" /> Uploading
</span>
</Stack>
)}
</Flex>
<Stack style={{ display: isEditing ? "block" : "none" }}>
<Tooltip
label={"Model download link, supports Huggingface and Civitai"}
>
<Input
name={inputKey}
defaultValue={modelFile.downloadUrl ?? ""}
size={"sm"}
isInvalid={errors[inputKey] != null}
placeholder={"https://civitai.com/api/download/models/324524"}
/>
</Tooltip>
</Stack>
<Text color={"red.400"}>{errors[inputKey]}</Text>
)}
{imageDepsArr.map((image) => (
<Stack key={image.filename}>
<p>{image.filename}</p>
<Image
width={250}
src={`/workspace/view_media?filename=${image.filename}&isPreview=true&isInput=true`}
/>
</Stack>
))}
</Stack>
)}
</Stack>
);
}
+22 -26
View File
@@ -13,13 +13,18 @@ type LiteNode = {
widgets_values?: Array<string | number>;
};
export type ModelFile = {
filename: string;
fileFolder: string | null;
fileHash: string | null;
downloadUrl: string | null;
export type ModelDep = {
name: string;
folder: string | null;
hash: string | null;
url: string | null;
// TODO: will be deperecated
filename?: string;
fileFolder?: string | null;
fileHash?: string | null;
downloadUrl?: string | null;
// optional info
infoUrl: string | null;
infoUrl?: string | null;
nodeType?: string;
inputName?: string;
};
@@ -40,12 +45,12 @@ export type DepsResult = {
nodeRepos: NodeRepo[];
};
export type DepsResultModel = ModelFile & {
export type DepsResultModel = ModelDep & {
length: number;
};
export type WorkspaceInfoDeps = {
models: Record<string, ModelFile>;
models: Record<string, ModelDep>;
images: DepsResult["images"];
nodeRepos: DepsResult["nodeRepos"];
};
@@ -57,20 +62,18 @@ async function fetchModelData(
): Promise<DepsResultModel> {
const res =
(await indexdb.models.where("fileName").equals(filename).toArray()) ?? [];
console.log("fetch modeldata", res);
const first = res.at(0);
const modelFile: DepsResultModel = {
filename,
name: filename,
nodeType,
fileHash: first?.fileHash ?? null,
fileFolder: first?.fileFolder ?? null,
downloadUrl: first?.civitModelVersionID
hash: first?.fileHash ?? null,
folder: first?.fileFolder ?? null,
url: first?.civitModelVersionID
? getCivitModelDownloadUrl(first.civitModelVersionID)
: first?.downloadUrl ?? null,
inputName: inputName,
length: res.length,
infoUrl: first?.civitModelID
? getCivitModelPageUrl(first.civitModelID, first.civitModelVersionID)
: getHgModelInfoUrlFromDownloadUrl(first?.downloadUrl ?? "") ?? null,
};
return modelFile;
}
@@ -94,13 +97,9 @@ export async function extractAndFetchFileNames(
if (typeof value != "string") return;
// Check if it's a model file
if (modelFileExtensions.some((ext) => value.endsWith(ext))) {
if (curDeps?.models?.[value]) {
modelPromises.push(Promise.resolve(curDeps.models[value]));
} else {
modelPromises.push(
fetchModelData(value, node.class_type, inputName),
);
}
modelPromises.push(
fetchModelData(value, node.class_type, inputName),
);
}
// Check if it's an image file
if (imageFileExtensions.some((ext) => value.endsWith(ext))) {
@@ -134,10 +133,7 @@ export async function extractAndFetchFileNames(
const models = await Promise.all(modelPromises);
const modelsMap: Record<string, DepsResultModel> = {};
models.forEach((model) => {
// if (model.nodeType) {
// model.gitRepo = reposMapping[model.nodeType];
// }
modelsMap[model.filename] = model;
modelsMap[model.name] = model;
});
const nodeRepos: Record<string, NodeRepo> = {};
Object.values(reposMapping).forEach((repo) => {
+1 -3
View File
@@ -10,10 +10,8 @@ export interface SortableItem {
export interface Workflow extends SortableItem {
id: string;
json: string;
lastSavedJson?: string; // TODO will be deprecated
name: string;
createTime: number;
filePath?: string; // TODO will be deprecated
privacy?: WorkflowPrivacy;
tags?: string[];
parentFolderID?: string | null; //TODO remove undefined, use null only
@@ -67,6 +65,7 @@ export type WorkflowVersion = {
workflowID: string;
json: string;
createTime: number;
api_prompt: string;
cloudID?: string;
cloudOrigin?: string;
authorID?: string;
@@ -131,7 +130,6 @@ export type Media = {
workflowJSON?: string;
createTime: number;
localPath: string;
format: string;
};
/**
+8 -2
View File
@@ -4,7 +4,7 @@ import {
ScanLocalFolder,
scanLocalFiles,
} from "../apis/TwowaySyncApi";
import { isFolder, userSettingsTable } from "../db-tables/WorkspaceDB";
import { isFolder } from "../db-tables/WorkspaceDB";
import { indexdb } from "../db-tables/indexdb";
import { Folder, Workflow } from "../types/dbTypes";
import { sortFileItem } from "../utils";
@@ -34,7 +34,13 @@ export async function scanMyWorkflowsDir(
return folder;
} else {
// is workflow
const existingWorkflow = (await indexdb.workflows?.get(file.id)) ?? {
const existingWorkflow = (await indexdb.workflows
?.get(file.id)
.catch((err) => {
console.error("Error getting workflow from indexdb", err);
return null;
})) ?? {
createTime: Date.now(),
updateTime: Date.now(),
};