Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5e2f36cd | ||
|
|
0b0015fbd1 | ||
|
|
abb4a4798c | ||
|
|
95238469d6 | ||
|
|
86f14b5bce | ||
|
|
84802f5c4e | ||
|
|
68b4d93639 | ||
|
|
5d349153d2 | ||
|
|
ea675fcd4c | ||
|
|
4c715b815a | ||
|
|
bef6ce35de | ||
|
|
d8951df35f | ||
|
|
38fea1e79f | ||
|
|
66ad3ab4c2 | ||
|
|
488d2aee8c | ||
|
|
c4628f6e4c | ||
|
|
9ba349d36a | ||
|
|
08ab93127e | ||
|
|
de641f0acf | ||
|
|
d4d7e98487 | ||
|
|
967a77d3a3 | ||
|
|
a4cd5db360 | ||
|
|
576a6744a4 | ||
|
|
3e5ff7702e | ||
|
|
009589630d | ||
|
|
8eb2ce3e10 | ||
|
|
e3a1d24304 | ||
|
|
2d033570f4 | ||
|
|
7ae25aa162 | ||
|
|
4b37de9ec5 | ||
|
|
b0d1bcc303 | ||
|
|
2193dd287d | ||
|
|
81bde40aeb | ||
|
|
317f699c46 | ||
|
|
0d1bb2aaf4 |
@@ -19,7 +19,7 @@ import requests
|
|||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.types import ASGIApp, Scope, Receive, Send
|
from starlette.types import ASGIApp, Scope, Receive, Send
|
||||||
|
import modal
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
@@ -180,6 +180,8 @@ class Item(BaseModel):
|
|||||||
models: List[Model]
|
models: List[Model]
|
||||||
callback_url: str
|
callback_url: str
|
||||||
model_volume_name: str
|
model_volume_name: str
|
||||||
|
run_timeout: Optional[int] = Field(default=60 * 5)
|
||||||
|
idle_timeout: Optional[int] = Field(default=60)
|
||||||
gpu: GPUType = Field(default=GPUType.T4)
|
gpu: GPUType = Field(default=GPUType.T4)
|
||||||
|
|
||||||
@field_validator('gpu')
|
@field_validator('gpu')
|
||||||
@@ -234,6 +236,14 @@ class UploadType(str, Enum):
|
|||||||
checkpoint = "checkpoint"
|
checkpoint = "checkpoint"
|
||||||
lora = "lora"
|
lora = "lora"
|
||||||
embedding = "embedding"
|
embedding = "embedding"
|
||||||
|
clip = "clip"
|
||||||
|
clip_vision = "clip_vision"
|
||||||
|
configs = "configs"
|
||||||
|
controlnet = "controlnet"
|
||||||
|
upscale_models = "upscale_models"
|
||||||
|
vae = "vae"
|
||||||
|
ipadapter = "ipadapter"
|
||||||
|
other = "other"
|
||||||
|
|
||||||
class UploadBody(BaseModel):
|
class UploadBody(BaseModel):
|
||||||
download_url: str
|
download_url: str
|
||||||
@@ -249,8 +259,46 @@ UPLOAD_TYPE_DIR_MAP = {
|
|||||||
UploadType.checkpoint: "checkpoints",
|
UploadType.checkpoint: "checkpoints",
|
||||||
UploadType.lora: "loras",
|
UploadType.lora: "loras",
|
||||||
UploadType.embedding: "embeddings",
|
UploadType.embedding: "embeddings",
|
||||||
|
UploadType.clip: "clip",
|
||||||
|
UploadType.clip_vision: "clip_vision",
|
||||||
|
UploadType.configs: "configs",
|
||||||
|
UploadType.controlnet: "controlnet",
|
||||||
|
UploadType.upscale_models: "upscale_models",
|
||||||
|
UploadType.vae: "vae",
|
||||||
|
UploadType.ipadapter: "ipadapter",
|
||||||
|
UploadType.other: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DeleteBody(BaseModel):
|
||||||
|
volume_name: str
|
||||||
|
path: str
|
||||||
|
file_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/delete-volume-model")
|
||||||
|
async def delete_model(body: DeleteBody):
|
||||||
|
global last_activity_time
|
||||||
|
last_activity_time = time.time()
|
||||||
|
logger.info(f"Extended inactivity time to {global_timeout}")
|
||||||
|
|
||||||
|
full_path = f"{body.path.rstrip('/')}/{body.file_name}"
|
||||||
|
|
||||||
|
rm_process = await asyncio.subprocess.create_subprocess_exec("modal", "volume", "rm", body.volume_name, full_path,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,)
|
||||||
|
await rm_process.wait()
|
||||||
|
|
||||||
|
logger.info(f"Successfully deleted: {full_path} from volume: {body.volume_name}")
|
||||||
|
stdout, stderr = await rm_process.communicate()
|
||||||
|
if stdout:
|
||||||
|
logger.info(f"cp_process stdout: {stdout.decode()}")
|
||||||
|
if stderr:
|
||||||
|
logger.info(f"cp_process stderr: {stderr.decode()}")
|
||||||
|
|
||||||
|
if rm_process.returncode == 0:
|
||||||
|
return JSONResponse(status_code=200, content={"status":f"Successfully deleted {full_path} from volume {body.volume_name}"})
|
||||||
|
else:
|
||||||
|
return JSONResponse(status_code=500, content={"status": "error", "error": stderr.decode()})
|
||||||
|
|
||||||
@app.post("/upload-volume")
|
@app.post("/upload-volume")
|
||||||
async def upload_model(body: UploadBody):
|
async def upload_model(body: UploadBody):
|
||||||
@@ -265,12 +313,16 @@ async def upload_model(body: UploadBody):
|
|||||||
|
|
||||||
|
|
||||||
async def upload_logic(body: UploadBody):
|
async def upload_logic(body: UploadBody):
|
||||||
folder_path = f"/app/builds/{body.volume_id}"
|
folder_path = f"/app/builds/{body.volume_id}-{uuid4()}"
|
||||||
|
|
||||||
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume-builder", folder_path)
|
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume_builder", folder_path)
|
||||||
await cp_process.wait()
|
await cp_process.wait()
|
||||||
|
|
||||||
upload_path = UPLOAD_TYPE_DIR_MAP[body.upload_type]
|
upload_path = UPLOAD_TYPE_DIR_MAP[body.upload_type]
|
||||||
|
if upload_path == "":
|
||||||
|
# TODO: deal with custom paths
|
||||||
|
pass
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"volume_names": {
|
"volume_names": {
|
||||||
body.volume_name: {"download_url": body.download_url, "folder_path": upload_path}
|
body.volume_name: {"download_url": body.download_url, "folder_path": upload_path}
|
||||||
@@ -284,16 +336,22 @@ async def upload_logic(body: UploadBody):
|
|||||||
"volume_id": body.volume_id,
|
"volume_id": body.volume_id,
|
||||||
"folder_path": upload_path,
|
"folder_path": upload_path,
|
||||||
},
|
},
|
||||||
"civitai_api_key": os.environ.get('CIVITAI_API_KEY')
|
"civitai_api_key": os.environ.get('CIVITAI_API_KEY'),
|
||||||
|
"app_name": f"vol_name_{uuid4()}"
|
||||||
}
|
}
|
||||||
with open(f"{folder_path}/config.py", "w") as f:
|
with open(f"{folder_path}/config.py", "w") as f:
|
||||||
f.write("config = " + json.dumps(config))
|
f.write("config = " + json.dumps(config))
|
||||||
|
|
||||||
await asyncio.subprocess.create_subprocess_shell(
|
process = await asyncio.subprocess.create_subprocess_shell(
|
||||||
f"modal run app.py",
|
f"python runner.py",
|
||||||
cwd=folder_path,
|
cwd=folder_path,
|
||||||
env={**os.environ, "COLUMNS": "10000"}
|
env={**os.environ, "COLUMNS": "10000"}
|
||||||
)
|
)
|
||||||
|
await process.wait()
|
||||||
|
|
||||||
|
# import modal
|
||||||
|
# modal.deploy_stub(stub)
|
||||||
|
# stub["download_model"].web_url
|
||||||
|
|
||||||
@app.post("/create")
|
@app.post("/create")
|
||||||
async def create_machine(item: Item):
|
async def create_machine(item: Item):
|
||||||
@@ -391,7 +449,9 @@ async def build_logic(item: Item):
|
|||||||
"gpu": item.gpu,
|
"gpu": item.gpu,
|
||||||
"public_model_volume": public_model_volume_name,
|
"public_model_volume": public_model_volume_name,
|
||||||
"private_model_volume": item.model_volume_name,
|
"private_model_volume": item.model_volume_name,
|
||||||
"pip": list(pip_modules)
|
"pip": list(pip_modules),
|
||||||
|
"run_timeout": item.run_timeout,
|
||||||
|
"idle_timeout": item.idle_timeout,
|
||||||
}
|
}
|
||||||
with open(f"{folder_path}/config.py", "w") as f:
|
with open(f"{folder_path}/config.py", "w") as f:
|
||||||
f.write("config = " + json.dumps(config))
|
f.write("config = " + json.dumps(config))
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from fastapi import FastAPI, Request, HTTPException
|
|||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from volume_setup import volumes
|
from volume_setup import volumes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import TCPConnector
|
||||||
# deploy_test = False
|
# deploy_test = False
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -36,7 +38,9 @@ if not deploy_test:
|
|||||||
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
|
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
|
||||||
|
|
||||||
dockerfile_image = (
|
dockerfile_image = (
|
||||||
modal.Image.debian_slim()
|
modal.Image.debian_slim(
|
||||||
|
python_version="3.11.1"
|
||||||
|
)
|
||||||
.apt_install("git", "wget")
|
.apt_install("git", "wget")
|
||||||
.pip_install(
|
.pip_install(
|
||||||
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
|
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
|
||||||
@@ -49,12 +53,12 @@ if not deploy_test:
|
|||||||
|
|
||||||
# Install comfyui manager
|
# Install comfyui manager
|
||||||
"cd /comfyui/custom_nodes && git clone https://github.com/ltdrdata/ComfyUI-Manager.git",
|
"cd /comfyui/custom_nodes && git clone https://github.com/ltdrdata/ComfyUI-Manager.git",
|
||||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && git reset --hard 9c86f62b912f4625fe2b929c7fc61deb9d16f6d3",
|
"cd /comfyui/custom_nodes/ComfyUI-Manager && git reset --hard 8dd801435bb75aa1d24b7e382bac070a4c18bc51",
|
||||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && pip install -r requirements.txt",
|
"cd /comfyui/custom_nodes/ComfyUI-Manager && pip install -r requirements.txt",
|
||||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && mkdir startup-scripts",
|
"cd /comfyui/custom_nodes/ComfyUI-Manager && mkdir startup-scripts",
|
||||||
)
|
)
|
||||||
.run_commands(f"cat /comfyui/server.py")
|
# .run_commands(f"cat /comfyui/server.py")
|
||||||
.run_commands(f"ls /comfyui/app")
|
# .run_commands(f"ls /comfyui/app")
|
||||||
# .run_commands(
|
# .run_commands(
|
||||||
# # Install comfy deploy
|
# # Install comfy deploy
|
||||||
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
|
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
|
||||||
@@ -82,7 +86,7 @@ if not deploy_test:
|
|||||||
# Time to wait between API check attempts in milliseconds
|
# Time to wait between API check attempts in milliseconds
|
||||||
COMFY_API_AVAILABLE_INTERVAL_MS = 50
|
COMFY_API_AVAILABLE_INTERVAL_MS = 50
|
||||||
# Maximum number of API check attempts
|
# Maximum number of API check attempts
|
||||||
COMFY_API_AVAILABLE_MAX_RETRIES = 500
|
COMFY_API_AVAILABLE_MAX_RETRIES = 1000
|
||||||
# Time to wait between poll attempts in milliseconds
|
# Time to wait between poll attempts in milliseconds
|
||||||
COMFY_POLLING_INTERVAL_MS = 250
|
COMFY_POLLING_INTERVAL_MS = 250
|
||||||
# Maximum number of poll attempts
|
# Maximum number of poll attempts
|
||||||
@@ -91,48 +95,34 @@ COMFY_POLLING_MAX_RETRIES = 1000
|
|||||||
COMFY_HOST = "127.0.0.1:8188"
|
COMFY_HOST = "127.0.0.1:8188"
|
||||||
|
|
||||||
|
|
||||||
def check_server(url, retries=50, delay=500):
|
async def check_server(url, retries=50, delay=500):
|
||||||
import requests
|
import aiohttp
|
||||||
import time
|
# for i in range(retries):
|
||||||
"""
|
while True:
|
||||||
Check if a server is reachable via HTTP GET request
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- url (str): The URL to check
|
|
||||||
- retries (int, optional): The number of times to attempt connecting to the server. Default is 50
|
|
||||||
- delay (int, optional): The time in milliseconds to wait between retries. Default is 500
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the server is reachable within the given number of retries, otherwise False
|
|
||||||
"""
|
|
||||||
|
|
||||||
for i in range(retries):
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(url)
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
# If the response status code is 200, the server is up and running
|
# If the response status code is 200, the server is up and running
|
||||||
if response.status_code == 200:
|
if response.status == 200:
|
||||||
print(f"runpod-worker-comfy - API is reachable")
|
print(f"comfy-modal - API is reachable")
|
||||||
return True
|
return True
|
||||||
except requests.RequestException as e:
|
except Exception as e:
|
||||||
# If an exception occurs, the server may not be ready
|
# If an exception occurs, the server may not be ready
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# print(f"runpod-worker-comfy - trying")
|
|
||||||
|
|
||||||
# Wait for the specified delay before retrying
|
# Wait for the specified delay before retrying
|
||||||
time.sleep(delay / 1000)
|
await asyncio.sleep(delay / 1000)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"runpod-worker-comfy - Failed to connect to server at {url} after {retries} attempts."
|
f"comfy-modal - Failed to connect to server at {url} after {retries} attempts."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_status(prompt_id):
|
async def check_status(prompt_id):
|
||||||
req = urllib.request.Request(
|
async with aiohttp.ClientSession() as session:
|
||||||
f"http://{COMFY_HOST}/comfyui-deploy/check-status?prompt_id={prompt_id}")
|
async with session.get(f"http://{COMFY_HOST}/comfyui-deploy/check-status?prompt_id={prompt_id}") as response:
|
||||||
return json.loads(urllib.request.urlopen(req).read())
|
return await response.json()
|
||||||
|
|
||||||
|
|
||||||
class Input(BaseModel):
|
class Input(BaseModel):
|
||||||
@@ -142,12 +132,12 @@ class Input(BaseModel):
|
|||||||
file_upload_endpoint: str
|
file_upload_endpoint: str
|
||||||
|
|
||||||
|
|
||||||
def queue_workflow_comfy_deploy(data: Input):
|
async def queue_workflow_comfy_deploy(data: Input):
|
||||||
data_str = data.json()
|
data_str = data.json()
|
||||||
data_bytes = data_str.encode('utf-8')
|
data_bytes = data_str.encode('utf-8')
|
||||||
req = urllib.request.Request(
|
async with aiohttp.ClientSession() as session:
|
||||||
f"http://{COMFY_HOST}/comfyui-deploy/run", data=data_bytes)
|
async with session.post(f"http://{COMFY_HOST}/comfyui-deploy/run", data=data_bytes) as response:
|
||||||
return json.loads(urllib.request.urlopen(req).read())
|
return await response.json()
|
||||||
|
|
||||||
|
|
||||||
class RequestInput(BaseModel):
|
class RequestInput(BaseModel):
|
||||||
@@ -158,45 +148,141 @@ image = Image.debian_slim()
|
|||||||
|
|
||||||
target_image = image if deploy_test else dockerfile_image
|
target_image = image if deploy_test else dockerfile_image
|
||||||
|
|
||||||
@stub.cls(image=target_image, gpu=config["gpu"] ,volumes=volumes, timeout=60 * 10, container_idle_timeout=60)
|
run_timeout = config["run_timeout"]
|
||||||
|
idle_timeout = config["idle_timeout"]
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
@stub.cls(
|
||||||
|
image=target_image,
|
||||||
|
gpu=config["gpu"] ,
|
||||||
|
volumes=volumes,
|
||||||
|
timeout=(config["run_timeout"] + 20),
|
||||||
|
container_idle_timeout=config["idle_timeout"],
|
||||||
|
allow_concurrent_inputs=1,
|
||||||
|
)
|
||||||
class ComfyDeployRunner:
|
class ComfyDeployRunner:
|
||||||
|
|
||||||
|
machine_logs = []
|
||||||
|
|
||||||
|
async def read_stream(self, stream, isStderr):
|
||||||
|
import time
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
line = await stream.readline()
|
||||||
|
if line:
|
||||||
|
l = line.decode('utf-8').strip()
|
||||||
|
|
||||||
|
if l == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isStderr:
|
||||||
|
print(l, flush=True)
|
||||||
|
self.machine_logs.append({
|
||||||
|
"logs": l,
|
||||||
|
"timestamp": time.time()
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# is error
|
||||||
|
# logger.error(l)
|
||||||
|
print(l, flush=True)
|
||||||
|
self.machine_logs.append({
|
||||||
|
"logs": l,
|
||||||
|
"timestamp": time.time()
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# Handle the cancellation here if needed
|
||||||
|
break # Break out of the loop on cancellation
|
||||||
|
|
||||||
@enter()
|
@enter()
|
||||||
def setup(self):
|
async def setup(self):
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Make sure that the ComfyUI API is available
|
# Make sure that the ComfyUI API is available
|
||||||
print(f"comfy-modal - check server")
|
print(f"comfy-modal - check server")
|
||||||
|
|
||||||
command = ["python", "main.py",
|
self.server_process = await asyncio.subprocess.create_subprocess_shell(
|
||||||
"--disable-auto-launch", "--disable-metadata"]
|
f"python main.py --disable-auto-launch --disable-metadata",
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd="/comfyui",
|
||||||
|
# env={**os.environ, "COLUMNS": "10000"}
|
||||||
|
)
|
||||||
|
|
||||||
self.server_process = subprocess.Popen(command, cwd="/comfyui")
|
@exit()
|
||||||
|
async def cleanup(self, exc_type, exc_value, traceback):
|
||||||
|
print(f"comfy-modal - cleanup", exc_type, exc_value, traceback)
|
||||||
|
# Get the current event loop
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
check_server(
|
# Check if the event loop is closed
|
||||||
|
if loop.is_closed():
|
||||||
|
print("The event loop is closed.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.server_process.terminate()
|
||||||
|
await self.server_process.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print("Issues when cleaning up", e)
|
||||||
|
print("The event loop is open.")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
async def run(self, input: Input):
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
stdout_task = asyncio.create_task(
|
||||||
|
self.read_stream(self.server_process.stdout, False))
|
||||||
|
stderr_task = asyncio.create_task(
|
||||||
|
self.read_stream(self.server_process.stderr, True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
class TimeoutError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def timeout_handler(signum, frame):
|
||||||
|
data = json.dumps({
|
||||||
|
"run_id": input.prompt_id,
|
||||||
|
"status": "timeout",
|
||||||
|
"time": datetime.now().isoformat()
|
||||||
|
}).encode('utf-8')
|
||||||
|
req = urllib.request.Request(input.status_endpoint, data=data, method='POST')
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
raise TimeoutError("Operation timed out")
|
||||||
|
|
||||||
|
signal.signal(signal.SIGALRM, timeout_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
signal.alarm(run_timeout)
|
||||||
|
|
||||||
|
ok = await check_server(
|
||||||
f"http://{COMFY_HOST}",
|
f"http://{COMFY_HOST}",
|
||||||
COMFY_API_AVAILABLE_MAX_RETRIES,
|
COMFY_API_AVAILABLE_MAX_RETRIES,
|
||||||
COMFY_API_AVAILABLE_INTERVAL_MS,
|
COMFY_API_AVAILABLE_INTERVAL_MS,
|
||||||
)
|
)
|
||||||
|
|
||||||
@exit()
|
if not ok:
|
||||||
def cleanup(self, exc_type, exc_value, traceback):
|
raise Exception("ComfyUI API is not available")
|
||||||
self.server_process.terminate()
|
# Set an alarm for some seconds in the future
|
||||||
|
|
||||||
@method()
|
|
||||||
def run(self, input: Input):
|
|
||||||
data = json.dumps({
|
data = json.dumps({
|
||||||
"run_id": input.prompt_id,
|
"run_id": input.prompt_id,
|
||||||
"status": "started",
|
"status": "started",
|
||||||
"time": datetime.now().isoformat()
|
"time": datetime.now().isoformat()
|
||||||
}).encode('utf-8')
|
}).encode('utf-8')
|
||||||
req = urllib.request.Request(input.status_endpoint, data=data, method='POST')
|
async with aiohttp.ClientSession() as session:
|
||||||
urllib.request.urlopen(req)
|
async with session.post(input.status_endpoint, data=data) as response:
|
||||||
|
pass
|
||||||
|
|
||||||
job_input = input
|
job_input = input
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queued_workflow = queue_workflow_comfy_deploy(job_input) # queue_workflow(workflow)
|
queued_workflow = await queue_workflow_comfy_deploy(job_input) # queue_workflow(workflow)
|
||||||
prompt_id = queued_workflow["prompt_id"]
|
prompt_id = queued_workflow["prompt_id"]
|
||||||
print(f"comfy-modal - queued workflow with ID {prompt_id}")
|
print(f"comfy-modal - queued workflow with ID {prompt_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -210,22 +296,16 @@ class ComfyDeployRunner:
|
|||||||
status = ""
|
status = ""
|
||||||
try:
|
try:
|
||||||
print("getting request")
|
print("getting request")
|
||||||
while retries < COMFY_POLLING_MAX_RETRIES:
|
# while retries < COMFY_POLLING_MAX_RETRIES:
|
||||||
status_result = check_status(prompt_id=prompt_id)
|
while True:
|
||||||
# history = get_history(prompt_id)
|
status_result = await check_status(prompt_id=prompt_id)
|
||||||
|
|
||||||
# Exit the loop if we have found the history
|
|
||||||
# if prompt_id in history and history[prompt_id].get("outputs"):
|
|
||||||
# break
|
|
||||||
|
|
||||||
# Exit the loop if we have found the status both success or failed
|
|
||||||
if 'status' in status_result and (status_result['status'] == 'success' or status_result['status'] == 'failed'):
|
if 'status' in status_result and (status_result['status'] == 'success' or status_result['status'] == 'failed'):
|
||||||
status = status_result['status']
|
status = status_result['status']
|
||||||
print(status)
|
print(status)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Wait before trying again
|
# Wait before trying again
|
||||||
time.sleep(COMFY_POLLING_INTERVAL_MS / 1000)
|
await asyncio.sleep(COMFY_POLLING_INTERVAL_MS / 1000)
|
||||||
retries += 1
|
retries += 1
|
||||||
else:
|
else:
|
||||||
return {"error": "Max retries reached while waiting for image generation"}
|
return {"error": "Max retries reached while waiting for image generation"}
|
||||||
@@ -236,9 +316,60 @@ class ComfyDeployRunner:
|
|||||||
|
|
||||||
result = {"status": status}
|
result = {"status": status}
|
||||||
|
|
||||||
|
except TimeoutError:
|
||||||
|
print("Operation timed out")
|
||||||
|
return {"status": "failed"}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error occurred: {str(e)}")
|
||||||
|
data = json.dumps({
|
||||||
|
"run_id": input.prompt_id,
|
||||||
|
"status": "failed",
|
||||||
|
"time": datetime.now().isoformat()
|
||||||
|
}).encode('utf-8')
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(input.status_endpoint, data=data) as response:
|
||||||
|
print("response", response)
|
||||||
|
self.machine_logs.append({
|
||||||
|
"logs": str(e),
|
||||||
|
"timestamp": time.time()
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
signal.alarm(0)
|
||||||
|
|
||||||
|
print("uploading log_data")
|
||||||
|
data = json.dumps({
|
||||||
|
"run_id": input.prompt_id,
|
||||||
|
"time": datetime.now().isoformat(),
|
||||||
|
"log_data": self.machine_logs
|
||||||
|
}).encode('utf-8')
|
||||||
|
print("my logs", len(self.machine_logs))
|
||||||
|
# Clear logs
|
||||||
|
timeout = aiohttp.ClientTimeout(total=60) # 60 seconds total timeout
|
||||||
|
# Use HTTP/1.1 explicitly and increase the connection pool size
|
||||||
|
connector = TCPConnector(limit=100, force_close=True, enable_cleanup_closed=True)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
|
||||||
|
try:
|
||||||
|
async with session.post(input.status_endpoint, data=data) as response:
|
||||||
|
print("response", response)
|
||||||
|
# Process your response here
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
print("Request timed out")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
print("uploaded log_data")
|
||||||
|
# print(data)
|
||||||
|
self.machine_logs = []
|
||||||
|
finally:
|
||||||
|
stdout_task.cancel()
|
||||||
|
stderr_task.cancel()
|
||||||
|
await stdout_task
|
||||||
|
await stderr_task
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@web_app.post("/run")
|
@web_app.post("/run")
|
||||||
async def post_run(request_input: RequestInput):
|
async def post_run(request_input: RequestInput):
|
||||||
if not deploy_test:
|
if not deploy_test:
|
||||||
@@ -252,10 +383,12 @@ async def post_run(request_input: RequestInput):
|
|||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
|
|
||||||
model = ComfyDeployRunner()
|
model = ComfyDeployRunner()
|
||||||
call = model.run.spawn(request_input.input)
|
call = await model.run.spawn.aio(request_input.input)
|
||||||
|
|
||||||
|
print("call", call)
|
||||||
|
|
||||||
# call = run.spawn()
|
# call = run.spawn()
|
||||||
return {"call_id": call.object_id}
|
return {"call_id": None}
|
||||||
|
|
||||||
return {"call_id": None}
|
return {"call_id": None}
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ config = {
|
|||||||
"gpu": "T4",
|
"gpu": "T4",
|
||||||
"public_model_volume": "model-store",
|
"public_model_volume": "model-store",
|
||||||
"private_model_volume": "private-model-store",
|
"private_model_volume": "private-model-store",
|
||||||
"pip": []
|
"pip": [],
|
||||||
|
"run_timeout": 60 * 5,
|
||||||
|
"idle_timeout": 60
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public:
|
|||||||
loras: loras
|
loras: loras
|
||||||
upscale_models: upscale_models
|
upscale_models: upscale_models
|
||||||
vae: vae
|
vae: vae
|
||||||
|
ipadapter: ipadapter
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
base_path: /private_models/
|
base_path: /private_models/
|
||||||
@@ -21,3 +23,4 @@ private:
|
|||||||
loras: loras
|
loras: loras
|
||||||
upscale_models: upscale_models
|
upscale_models: upscale_models
|
||||||
vae: vae
|
vae: vae
|
||||||
|
ipadapter: ipadapter
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"comfyui": "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
"comfyui": "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
||||||
"git_custom_nodes": {
|
"git_custom_nodes": {
|
||||||
"https://github.com/BennyKok/comfyui-deploy.git": {
|
"https://github.com/BennyKok/comfyui-deploy.git": {
|
||||||
"hash": "a838cb7ad425e5652c3931fbafdc886b53c48a22",
|
"hash": "df46e3a0e5ad93fa71f5d216997e376af33b2a6d",
|
||||||
"disabled": false
|
"disabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-11
@@ -1,10 +1,18 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
import modal
|
import modal
|
||||||
from config import config
|
from config import config
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
stub = modal.Stub()
|
stub = modal.Stub(config["app_name"])
|
||||||
|
vol_name_to_links = config["volume_names"]
|
||||||
|
vol_name_to_path = config["volume_paths"]
|
||||||
|
callback_url = config["callback_url"]
|
||||||
|
callback_body = config["callback_body"]
|
||||||
|
civitai_key = config["civitai_api_key"]
|
||||||
|
web_app = FastAPI()
|
||||||
|
|
||||||
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
|
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
|
||||||
def is_valid_name(name: str) -> bool:
|
def is_valid_name(name: str) -> bool:
|
||||||
@@ -21,12 +29,6 @@ def create_volumes(volume_names, paths):
|
|||||||
|
|
||||||
return path_to_vol
|
return path_to_vol
|
||||||
|
|
||||||
vol_name_to_links = config["volume_names"]
|
|
||||||
vol_name_to_path = config["volume_paths"]
|
|
||||||
callback_url = config["callback_url"]
|
|
||||||
callback_body = config["callback_body"]
|
|
||||||
civitai_key = config["civitai_api_key"]
|
|
||||||
|
|
||||||
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
|
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
|
||||||
image = (
|
image = (
|
||||||
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
|
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
|
||||||
@@ -45,7 +47,7 @@ def download_model(volume_name, download_config):
|
|||||||
modified_download_url = download_url + ("&" if "?" in download_url else "?") + "token=" + civitai_key # civitai requires auth
|
modified_download_url = download_url + ("&" if "?" in download_url else "?") + "token=" + civitai_key # civitai requires auth
|
||||||
print('downloading', modified_download_url)
|
print('downloading', modified_download_url)
|
||||||
|
|
||||||
subprocess.run(["wget", modified_download_url , "--content-disposition", "-P", model_store_path])
|
subprocess.run(["wget", modified_download_url , "--content-disposition", "-P", model_store_path, "-nv"])
|
||||||
subprocess.run(["ls", "-la", volume_base_path])
|
subprocess.run(["ls", "-la", volume_base_path])
|
||||||
subprocess.run(["ls", "-la", model_store_path])
|
subprocess.run(["ls", "-la", model_store_path])
|
||||||
volumes[volume_base_path].commit()
|
volumes[volume_base_path].commit()
|
||||||
@@ -56,11 +58,12 @@ def download_model(volume_name, download_config):
|
|||||||
print(f"finished! sending to {callback_url}")
|
print(f"finished! sending to {callback_url}")
|
||||||
pprint({**status, **callback_body})
|
pprint({**status, **callback_body})
|
||||||
|
|
||||||
@stub.local_entrypoint()
|
@stub.function(image=image)
|
||||||
|
# @modal.asgi_app()
|
||||||
def simple_download():
|
def simple_download():
|
||||||
import requests
|
import requests
|
||||||
try:
|
try:
|
||||||
list(download_model.starmap([(vol_name, link) for vol_name,link in vol_name_to_links.items()]))
|
list(download_model.starmap([(vol_name, download_conf) for vol_name,download_conf in vol_name_to_links.items()]))
|
||||||
except modal.exception.FunctionTimeoutError as e:
|
except modal.exception.FunctionTimeoutError as e:
|
||||||
status = {"status": "failed", "error_logs": f"{str(e)}", "timeout": timeout}
|
status = {"status": "failed", "error_logs": f"{str(e)}", "timeout": timeout}
|
||||||
requests.post(callback_url, json={**status, **callback_body})
|
requests.post(callback_url, json={**status, **callback_body})
|
||||||
@@ -71,4 +74,3 @@ def simple_download():
|
|||||||
requests.post(callback_url, json={**status, **callback_body})
|
requests.post(callback_url, json={**status, **callback_body})
|
||||||
print(f"finished! sending to {callback_url}")
|
print(f"finished! sending to {callback_url}")
|
||||||
pprint({**status, **callback_body})
|
pprint({**status, **callback_body})
|
||||||
|
|
||||||
+1
@@ -15,4 +15,5 @@ config = {
|
|||||||
"folder_path": "checkpoints",
|
"folder_path": "checkpoints",
|
||||||
},
|
},
|
||||||
"civitai_api_key": "",
|
"civitai_api_key": "",
|
||||||
|
"app_name": "",
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import modal
|
||||||
|
import requests
|
||||||
|
from app import stub
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
modal.runner.deploy_stub(stub)
|
||||||
|
print("deployed stub")
|
||||||
|
# web_url = stub["simple_download"].web_url
|
||||||
|
f = modal.Function.lookup(config['app_name'], "simple_download")
|
||||||
|
f.spawn()
|
||||||
|
# print(f"web_url: {web_url}")
|
||||||
|
# requests.post(web_url)
|
||||||
+62
-12
@@ -26,6 +26,8 @@ import threading
|
|||||||
api = None
|
api = None
|
||||||
api_task = None
|
api_task = None
|
||||||
prompt_metadata = {}
|
prompt_metadata = {}
|
||||||
|
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
|
||||||
|
cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true'
|
||||||
|
|
||||||
def post_prompt(json_data):
|
def post_prompt(json_data):
|
||||||
prompt_server = server.PromptServer.instance
|
prompt_server = server.PromptServer.instance
|
||||||
@@ -157,6 +159,8 @@ async def websocket_handler(request):
|
|||||||
try:
|
try:
|
||||||
# Send initial state to the new client
|
# Send initial state to the new client
|
||||||
await send("status", { 'sid': sid }, sid)
|
await send("status", { 'sid': sid }, sid)
|
||||||
|
|
||||||
|
if cd_enable_log:
|
||||||
await send_first_time_log(sid)
|
await send_first_time_log(sid)
|
||||||
|
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
@@ -236,7 +240,12 @@ class Status(Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
UPLOADING = "uploading"
|
UPLOADING = "uploading"
|
||||||
|
|
||||||
|
# Global variable to keep track of the last read line number
|
||||||
|
last_read_line_number = 0
|
||||||
|
|
||||||
def update_run(prompt_id, status: Status):
|
def update_run(prompt_id, status: Status):
|
||||||
|
global last_read_line_number
|
||||||
|
|
||||||
if prompt_id not in prompt_metadata:
|
if prompt_id not in prompt_metadata:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -251,15 +260,49 @@ def update_run(prompt_id, status: Status):
|
|||||||
"run_id": prompt_id,
|
"run_id": prompt_id,
|
||||||
"status": status.value,
|
"status": status.value,
|
||||||
}
|
}
|
||||||
prompt_metadata[prompt_id]['status'] = status
|
|
||||||
print(f"Status: {status.value}")
|
print(f"Status: {status.value}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requests.post(status_endpoint, json=body)
|
requests.post(status_endpoint, json=body)
|
||||||
|
|
||||||
|
if cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
|
||||||
|
try:
|
||||||
|
with open(comfyui_file_path, 'r') as log_file:
|
||||||
|
# log_data = log_file.read()
|
||||||
|
# Move to the last read line
|
||||||
|
all_log_data = log_file.read() # Read all log data
|
||||||
|
print("All log data before skipping:", all_log_data) # Log all data before skipping
|
||||||
|
log_file.seek(0) # Reset file pointer to the beginning
|
||||||
|
|
||||||
|
for _ in range(last_read_line_number):
|
||||||
|
next(log_file)
|
||||||
|
log_data = log_file.read()
|
||||||
|
# Update the last read line number
|
||||||
|
last_read_line_number += log_data.count('\n')
|
||||||
|
print("last_read_line_number", last_read_line_number)
|
||||||
|
print("log_data", log_data)
|
||||||
|
print("log_data.count(n)", log_data.count('\n'))
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"run_id": prompt_id,
|
||||||
|
"log_data": [
|
||||||
|
{
|
||||||
|
"logs": log_data,
|
||||||
|
# "timestamp": time.time(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
requests.post(status_endpoint, json=body)
|
||||||
|
except Exception as log_error:
|
||||||
|
print(f"Error reading log file: {log_error}")
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_type = type(e).__name__
|
error_type = type(e).__name__
|
||||||
stack_trace = traceback.format_exc().strip()
|
stack_trace = traceback.format_exc().strip()
|
||||||
print(f"Error occurred while updating run: {e} {stack_trace}")
|
print(f"Error occurred while updating run: {e} {stack_trace}")
|
||||||
|
finally:
|
||||||
|
prompt_metadata[prompt_id]['status'] = status
|
||||||
|
|
||||||
|
|
||||||
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
|
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
|
||||||
@@ -387,20 +430,25 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
|
|||||||
"prompt_id": prompt_id,
|
"prompt_id": prompt_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async def handle_upload(prompt_id, data, key, content_type_key, default_content_type):
|
||||||
|
items = data.get(key, [])
|
||||||
|
for item in items:
|
||||||
|
await upload_file(
|
||||||
|
prompt_id,
|
||||||
|
item.get("filename"),
|
||||||
|
subfolder=item.get("subfolder"),
|
||||||
|
type=item.get("type"),
|
||||||
|
content_type=item.get(content_type_key, default_content_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Upload files in the background
|
# Upload files in the background
|
||||||
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
|
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
|
||||||
try:
|
try:
|
||||||
images = data.get('images', [])
|
await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
|
||||||
for image in images:
|
await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
|
||||||
await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder"), type=image.get("type"), content_type=image.get("content_type", "image/png"))
|
# This will also be mp4
|
||||||
|
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
|
||||||
files = data.get('files', [])
|
|
||||||
for file in files:
|
|
||||||
await upload_file(prompt_id, file.get("filename"), subfolder=file.get("subfolder"), type=file.get("type"), content_type=file.get("content_type", "image/png"))
|
|
||||||
|
|
||||||
gifs = data.get('gifs', [])
|
|
||||||
for gif in gifs:
|
|
||||||
await upload_file(prompt_id, gif.get("filename"), subfolder=gif.get("subfolder"), type=gif.get("type"), content_type=gif.get("format", "image/gif"))
|
|
||||||
|
|
||||||
if have_upload:
|
if have_upload:
|
||||||
await update_file_status(prompt_id, data, False, node_id=node_id)
|
await update_file_status(prompt_id, data, False, node_id=node_id)
|
||||||
@@ -441,6 +489,7 @@ prompt_server.send_json = send_json_override.__get__(prompt_server, server.Promp
|
|||||||
root_path = os.path.dirname(os.path.abspath(__file__))
|
root_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
||||||
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
||||||
|
comfyui_file_path = os.path.join(two_dirs_up, 'comfyui.log')
|
||||||
|
|
||||||
last_read_line = 0
|
last_read_line = 0
|
||||||
|
|
||||||
@@ -480,4 +529,5 @@ def run_in_new_thread(coroutine):
|
|||||||
t.start()
|
t.start()
|
||||||
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
||||||
|
|
||||||
|
if cd_enable_log:
|
||||||
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import threading
|
|||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
|
# Running with export CD_ENABLE_LOG=true; python main.py
|
||||||
|
|
||||||
|
# Check for 'cd-enable-log' flag in input arguments
|
||||||
|
# cd_enable_log = '--cd-enable-log' in sys.argv
|
||||||
|
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
def setup():
|
||||||
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
||||||
|
|
||||||
original_stdout = sys.stdout
|
original_stdout = sys.stdout
|
||||||
@@ -47,6 +54,10 @@ class StreamToLogger():
|
|||||||
sys.stdout = StreamToLogger(logging.INFO)
|
sys.stdout = StreamToLogger(logging.INFO)
|
||||||
sys.stderr = StreamToLogger(logging.ERROR)
|
sys.stderr = StreamToLogger(logging.ERROR)
|
||||||
|
|
||||||
|
if cd_enable_log:
|
||||||
|
print("** Comfy Deploy logging enabled")
|
||||||
|
setup()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the absolute path of the script's directory
|
# Get the absolute path of the script's directory
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|||||||
+18
-4
@@ -50,11 +50,22 @@ const ext = {
|
|||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const { workflow, error } = data;
|
const { workflow, workflow_id, error } = data;
|
||||||
if (error) {
|
if (error) {
|
||||||
infoDialog.showMessage("Unable to load this workflow", error);
|
infoDialog.showMessage("Unable to load this workflow", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding a delay to wait for the intial graph to load
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
workflow?.nodes.forEach((x) => {
|
||||||
|
if (x?.type === "ComfyDeploy") {
|
||||||
|
x.widgets_values[1] = workflow_id;
|
||||||
|
// x.widgets_values[2] = workflow_version.version;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** @type {LGraph} */
|
/** @type {LGraph} */
|
||||||
app.loadGraphData(workflow);
|
app.loadGraphData(workflow);
|
||||||
})
|
})
|
||||||
@@ -682,15 +693,18 @@ export class ConfigDialog extends ComfyDialog {
|
|||||||
</label>
|
</label>
|
||||||
<label style="color: white; width: 100%;">
|
<label style="color: white; width: 100%;">
|
||||||
Endpoint:
|
Endpoint:
|
||||||
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${data.endpoint
|
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${
|
||||||
|
data.endpoint
|
||||||
}">
|
}">
|
||||||
</label>
|
</label>
|
||||||
<label style="color: white;">
|
<label style="color: white;">
|
||||||
API Key: ${data.displayName ?? ""}
|
API Key: ${data.displayName ?? ""}
|
||||||
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${data.apiKey
|
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
|
||||||
|
data.apiKey
|
||||||
}">
|
}">
|
||||||
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
|
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
|
||||||
${data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
|
${
|
||||||
|
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TYPE "workflow_run_status" ADD VALUE 'timeout';--> statement-breakpoint
|
||||||
|
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "run_log" text;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "comfyui_deploy"."workflow_runs" DROP COLUMN "run_log";
|
||||||
|
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "run_log" jsonb;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -337,6 +337,20 @@
|
|||||||
"when": 1706384528895,
|
"when": 1706384528895,
|
||||||
"tag": "0047_gifted_starbolt",
|
"tag": "0047_gifted_starbolt",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 48,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1706600255919,
|
||||||
|
"tag": "0048_dear_korath",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 49,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1706631744127,
|
||||||
|
"tag": "0049_sweet_hex",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,8 @@ import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
|||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import {
|
import {
|
||||||
WorkflowRunStatusSchema,
|
WorkflowRunStatusSchema,
|
||||||
userUsageTable,
|
|
||||||
workflowRunOutputs,
|
workflowRunOutputs,
|
||||||
workflowRunsTable,
|
workflowRunsTable,
|
||||||
workflowTable,
|
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
import { getCurrentPlan } from "@/server/getCurrentPlan";
|
import { getCurrentPlan } from "@/server/getCurrentPlan";
|
||||||
import { stripe } from "@/server/stripe";
|
import { stripe } from "@/server/stripe";
|
||||||
@@ -18,6 +16,7 @@ const Request = z.object({
|
|||||||
status: WorkflowRunStatusSchema.optional(),
|
status: WorkflowRunStatusSchema.optional(),
|
||||||
time: z.coerce.date().optional(),
|
time: z.coerce.date().optional(),
|
||||||
output_data: z.any().optional(),
|
output_data: z.any().optional(),
|
||||||
|
log_data: z.any().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -26,7 +25,26 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
if (!data || error) return error;
|
if (!data || error) return error;
|
||||||
|
|
||||||
const { run_id, status, time, output_data } = data;
|
const { run_id, status, time, output_data, log_data } = data;
|
||||||
|
|
||||||
|
if (log_data) {
|
||||||
|
// It successfully started, update the started_at time
|
||||||
|
await db
|
||||||
|
.update(workflowRunsTable)
|
||||||
|
.set({
|
||||||
|
run_log: log_data,
|
||||||
|
})
|
||||||
|
.where(eq(workflowRunsTable.id, run_id));
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (status == "started" && time != undefined) {
|
if (status == "started" && time != undefined) {
|
||||||
// It successfully started, update the started_at time
|
// It successfully started, update the started_at time
|
||||||
@@ -48,6 +66,9 @@ export async function POST(request: Request) {
|
|||||||
.where(eq(workflowRunsTable.id, run_id));
|
.where(eq(workflowRunsTable.id, run_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ended =
|
||||||
|
status === "success" || status === "failed" || status === "timeout";
|
||||||
|
|
||||||
if (output_data) {
|
if (output_data) {
|
||||||
const workflow_run_output = await db.insert(workflowRunOutputs).values({
|
const workflow_run_output = await db.insert(workflowRunOutputs).values({
|
||||||
run_id: run_id,
|
run_id: run_id,
|
||||||
@@ -58,8 +79,7 @@ export async function POST(request: Request) {
|
|||||||
.update(workflowRunsTable)
|
.update(workflowRunsTable)
|
||||||
.set({
|
.set({
|
||||||
status: status,
|
status: status,
|
||||||
ended_at:
|
ended_at: ended ? new Date() : null,
|
||||||
status === "success" || status === "failed" ? new Date() : null,
|
|
||||||
})
|
})
|
||||||
.where(eq(workflowRunsTable.id, run_id))
|
.where(eq(workflowRunsTable.id, run_id))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -67,10 +87,7 @@ export async function POST(request: Request) {
|
|||||||
// Need to filter out only comfy deploy serverless
|
// Need to filter out only comfy deploy serverless
|
||||||
// Also multiply with the gpu selection
|
// Also multiply with the gpu selection
|
||||||
if (workflow_run.machine_type == "comfy-deploy-serverless") {
|
if (workflow_run.machine_type == "comfy-deploy-serverless") {
|
||||||
if (
|
if (ended && workflow_run.user_id) {
|
||||||
(status === "success" || status === "failed") &&
|
|
||||||
workflow_run.user_id
|
|
||||||
) {
|
|
||||||
const sub = await getCurrentPlan({
|
const sub = await getCurrentPlan({
|
||||||
user_id: workflow_run.user_id,
|
user_id: workflow_run.user_id,
|
||||||
org_id: workflow_run.org_id,
|
org_id: workflow_run.org_id,
|
||||||
@@ -91,12 +108,16 @@ export async function POST(request: Request) {
|
|||||||
durationInSec *= 4;
|
durationInSec *= 4;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
await stripe.subscriptionItems.createUsageRecord(
|
await stripe.subscriptionItems.createUsageRecord(
|
||||||
sub.subscription_item_api_id,
|
sub.subscription_item_api_id,
|
||||||
{
|
{
|
||||||
quantity: durationInSec,
|
quantity: durationInSec,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export default async function Page({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{machine.status !== "building" && machine.build_log && (
|
{machine.status !== "building" && machine.build_log && (
|
||||||
<LogsViewer logs={JSON.parse(machine.build_log)} />
|
<LogsViewer
|
||||||
|
logs={JSON.parse(machine.build_log)}
|
||||||
|
className="h-full max-h-[600px]"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -16,11 +16,19 @@ const freeTierSeconds = 30000;
|
|||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const sub = await getCurrentPlanWithAuth();
|
const sub = await getCurrentPlanWithAuth();
|
||||||
|
|
||||||
const data = sub?.subscription_item_api_id
|
let data: Awaited<
|
||||||
|
ReturnType<typeof stripe.subscriptionItems.listUsageRecordSummaries>
|
||||||
|
> | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = sub?.subscription_item_api_id
|
||||||
? await stripe.subscriptionItems.listUsageRecordSummaries(
|
? await stripe.subscriptionItems.listUsageRecordSummaries(
|
||||||
sub?.subscription_item_api_id,
|
sub?.subscription_item_api_id,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4 flex items-center justify-center">
|
<div className="mt-4 flex items-center justify-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LoadingWrapper } from "@/components/LoadingWrapper";
|
import { LoadingWrapper } from "@/components/LoadingWrapper";
|
||||||
import { DeploymentsTable } from "@/components/RunsTable";
|
import { DeploymentsTable } from "@/components/DeploymentsTable";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
export default async function Page({
|
export default async function Page({
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export default async function Page({
|
|||||||
const workflow_id = params.workflow_id;
|
const workflow_id = params.workflow_id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full h-fit min-w-0">
|
<Card className="w-full h-fit min-w-0 relative">
|
||||||
<CardHeader className="relative">
|
<CardHeader>
|
||||||
<CardTitle>Run</CardTitle>
|
<CardTitle>Run</CardTitle>
|
||||||
<div className="absolute right-6 top-6">
|
{/* <div className="absolute right-6 top-6">
|
||||||
<RouteRefresher interval={5000} autoRefresh={false} />
|
<RouteRefresher interval={5000} autoRefresh={false} />
|
||||||
</div>
|
</div> */}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
OpenEditButton,
|
OpenEditButton,
|
||||||
RunWorkflowButton,
|
RunWorkflowButton,
|
||||||
VersionSelect,
|
VersionSelect,
|
||||||
ViewWorkflowDetailsButton,
|
|
||||||
} from "@/components/VersionSelect";
|
} from "@/components/VersionSelect";
|
||||||
|
import { ViewWorkflowDetailsButton } from "@/components/ViewWorkflowDetailsButton";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ export async function CodeBlock(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full text-sm">
|
<div className="relative w-full text-sm">
|
||||||
{/* max-w-[calc(32rem-1.5rem-1.5rem)] */}
|
|
||||||
{/* <div className=""> */}
|
|
||||||
<p
|
<p
|
||||||
// tabIndex={1}
|
|
||||||
className="[&>pre]:p-4 rounded-lg max-h-96 overflow-auto w-full"
|
className="[&>pre]:p-4 rounded-lg max-h-96 overflow-auto w-full"
|
||||||
style={{
|
style={{
|
||||||
overflowWrap: "break-word",
|
overflowWrap: "break-word",
|
||||||
@@ -28,7 +25,6 @@ export async function CodeBlock(props: {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
|
||||||
<CopyButton className="absolute right-2 top-2" text={props.code} />
|
<CopyButton className="absolute right-2 top-2" text={props.code} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { CopyButton } from "@/components/CopyButton";
|
||||||
|
import type { StringLiteralUnion } from "shikiji";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { highlight } from "../server/highlight";
|
||||||
|
|
||||||
|
export function CodeBlockClient({
|
||||||
|
code,
|
||||||
|
lang,
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
lang: StringLiteralUnion<string>;
|
||||||
|
}) {
|
||||||
|
const { data } = useSWR(code, async () => {
|
||||||
|
return highlight(code.trim(), lang);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full text-sm">
|
||||||
|
{data && (
|
||||||
|
<p
|
||||||
|
className="[&>pre]:p-4 rounded-lg max-h-96 overflow-auto w-full"
|
||||||
|
style={{
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: data,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CopyButton className="absolute right-2 top-2" text={code} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,10 +7,12 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
export function CopyButton({
|
export function CopyButton({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
text: string;
|
text: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -21,7 +23,7 @@ export function CopyButton({
|
|||||||
}}
|
}}
|
||||||
className={cn(" p-2 min-h-0 aspect-square", className)}
|
className={cn(" p-2 min-h-0 aspect-square", className)}
|
||||||
>
|
>
|
||||||
<Copy size={14} />
|
{children} <Copy size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,13 +69,16 @@ const client = new ComfyDeployClient({
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const jsClientCreateRunTemplate = `
|
const jsClientCreateRunTemplate = `
|
||||||
const { run_id } = await client.run("<ID>", {
|
const { run_id } = await client.run({
|
||||||
|
deployment_id: "<ID>",
|
||||||
inputs: {}
|
inputs: {}
|
||||||
});
|
});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const jsClientCreateRunNoInputsTemplate = `
|
const jsClientCreateRunNoInputsTemplate = `
|
||||||
const { run_id } = await client.run("<ID>");
|
const { run_id } = await client.run({
|
||||||
|
deployment_id: "<ID>"
|
||||||
|
});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const clientTemplate_checkStatus = `
|
const clientTemplate_checkStatus = `
|
||||||
@@ -119,10 +122,7 @@ export function DeploymentDisplay({
|
|||||||
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
|
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
|
||||||
<div>
|
<div>
|
||||||
Install the node comfydeploy SDK
|
Install the node comfydeploy SDK
|
||||||
<CodeBlock
|
<CodeBlock lang="bash" code={`npm i comfydeploy`} />
|
||||||
lang="bash"
|
|
||||||
code={`npm i comfydeploy`}
|
|
||||||
/>
|
|
||||||
Initialize your client
|
Initialize your client
|
||||||
</div>
|
</div>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
@@ -197,8 +197,7 @@ function formatCode(
|
|||||||
if (inputs && inputs.length > 0) {
|
if (inputs && inputs.length > 0) {
|
||||||
codeTemplate = codeTemplate.replace(
|
codeTemplate = codeTemplate.replace(
|
||||||
"inputs: {}",
|
"inputs: {}",
|
||||||
`inputs: ${
|
`inputs: ${JSON.stringify(
|
||||||
JSON.stringify(
|
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
inputs.map((x) => {
|
inputs.map((x) => {
|
||||||
return [x?.input_id, ""];
|
return [x?.input_id, ""];
|
||||||
@@ -209,8 +208,7 @@ function formatCode(
|
|||||||
)
|
)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
||||||
.join("\n")
|
.join("\n")}`,
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
codeTemplate = codeTemplate.replace(
|
codeTemplate = codeTemplate.replace(
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { findAllDeployments } from "../server/findAllRuns";
|
||||||
|
import { DeploymentDisplay } from "./DeploymentDisplay";
|
||||||
|
|
||||||
|
export async function DeploymentsTable(props: { workflow_id: string }) {
|
||||||
|
const allRuns = await findAllDeployments(props.workflow_id);
|
||||||
|
|
||||||
|
const headersList = headers();
|
||||||
|
const host = headersList.get("host") || "";
|
||||||
|
const protocol = headersList.get("x-forwarded-proto") || "";
|
||||||
|
const domain = `${protocol}://${host}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-auto h-fit w-full">
|
||||||
|
<Table className="">
|
||||||
|
<TableCaption>A list of your deployments</TableCaption>
|
||||||
|
<TableHeader className="bg-background top-0 sticky">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className=" w-[100px]">Environment</TableHead>
|
||||||
|
<TableHead className=" w-[100px]">Version</TableHead>
|
||||||
|
<TableHead className="">Machine</TableHead>
|
||||||
|
<TableHead className=" text-right">Updated At</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{allRuns.map((run) => (
|
||||||
|
<DeploymentDisplay deployment={run} key={run.id} domain={domain} />
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,33 @@ import * as React from "react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { UnknownKeysParam, ZodObject, ZodRawShape, z } from "zod";
|
import type { UnknownKeysParam, ZodObject, ZodRawShape, z } from "zod";
|
||||||
|
|
||||||
|
type ContextType = [Partial<any>, React.Dispatch<React.SetStateAction<any>>];
|
||||||
|
const AutoFormValueContext = React.createContext<ContextType | null>(null);
|
||||||
|
|
||||||
|
function AutoFormValueProvider<Z extends ZodObject<any, any>>({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: ContextType;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AutoFormValueContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AutoFormValueContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAutoFormValueContext<Z extends ZodObject<any, any>>() {
|
||||||
|
const context = React.useContext(AutoFormValueContext);
|
||||||
|
|
||||||
|
// if (!context) {
|
||||||
|
// throw new Error("useInsertModal must be used within a InsertModalProvider");
|
||||||
|
// }
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
export function InsertModal<
|
export function InsertModal<
|
||||||
K extends ZodRawShape,
|
K extends ZodRawShape,
|
||||||
Y extends UnknownKeysParam,
|
Y extends UnknownKeysParam,
|
||||||
@@ -41,7 +68,10 @@ export function InsertModal<
|
|||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Partial<z.infer<Z>>>({});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AutoFormValueProvider value={[values, setValues]}>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
{/* <DialogTrigger disabled={props.disabled}> */}
|
{/* <DialogTrigger disabled={props.disabled}> */}
|
||||||
{props.tooltip ? (
|
{props.tooltip ? (
|
||||||
@@ -74,13 +104,17 @@ export function InsertModal<
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* </DialogTrigger> */}
|
{/* </DialogTrigger> */}
|
||||||
<DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
|
<DialogContent
|
||||||
|
className={cn("sm:max-w-[425px]", props.dialogClassName)}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{props.title}</DialogTitle>
|
<DialogTitle>{props.title}</DialogTitle>
|
||||||
<DialogDescription>{props.description}</DialogDescription>
|
<DialogDescription>{props.description}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{/* <ScrollArea> */}
|
{/* <ScrollArea> */}
|
||||||
<AutoForm
|
<AutoForm
|
||||||
|
values={values}
|
||||||
|
onValuesChange={setValues}
|
||||||
fieldConfig={props.fieldConfig}
|
fieldConfig={props.fieldConfig}
|
||||||
formSchema={props.formSchema}
|
formSchema={props.formSchema}
|
||||||
onSubmit={async (data) => {
|
onSubmit={async (data) => {
|
||||||
@@ -100,6 +134,7 @@ export function InsertModal<
|
|||||||
{/* </ScrollArea> */}
|
{/* </ScrollArea> */}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
</AutoFormValueProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +173,7 @@ export function UpdateModal<
|
|||||||
}, [props.data]);
|
}, [props.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AutoFormValueProvider value={[values, setValues]}>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
{props.trigger ?? (
|
{props.trigger ?? (
|
||||||
<DialogTrigger
|
<DialogTrigger
|
||||||
@@ -150,7 +186,9 @@ export function UpdateModal<
|
|||||||
{props.trigger}
|
{props.trigger}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
)}
|
)}
|
||||||
<DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
|
<DialogContent
|
||||||
|
className={cn("sm:max-w-[425px]", props.dialogClassName)}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{props.title}</DialogTitle>
|
<DialogTitle>{props.title}</DialogTitle>
|
||||||
<DialogDescription>{props.description}</DialogDescription>
|
<DialogDescription>{props.description}</DialogDescription>
|
||||||
@@ -182,5 +220,6 @@ export function UpdateModal<
|
|||||||
</AutoForm>
|
</AutoForm>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
</AutoFormValueProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function LiveStatus({
|
|||||||
(state) =>
|
(state) =>
|
||||||
state.data
|
state.data
|
||||||
.filter((x) => x.id === run.id)
|
.filter((x) => x.id === run.id)
|
||||||
.sort((a, b) => b.timestamp - a.timestamp)?.[0]
|
.sort((a, b) => b.timestamp - a.timestamp)?.[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
let status = run.status;
|
let status = run.status;
|
||||||
@@ -51,7 +51,9 @@ export function LiveStatus({
|
|||||||
<>
|
<>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{data && status != "success"
|
{data && status != "success"
|
||||||
? `${data.json.event} - ${data.json.data.node}`
|
? `${data.json.event}${
|
||||||
|
data.json.data.node ? " - " + data.json.data.node : ""
|
||||||
|
}`
|
||||||
: "-"}
|
: "-"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="truncate text-right">
|
<TableCell className="truncate text-right">
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export type LogsType = {
|
export type LogsType = {
|
||||||
machine_id?: string;
|
machine_id?: string;
|
||||||
logs: string;
|
logs: string;
|
||||||
timestamp: number;
|
timestamp?: number;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export function LogsViewer({ logs }: { logs: LogsType }) {
|
export function LogsViewer({
|
||||||
|
logs,
|
||||||
|
hideTimestamp,
|
||||||
|
className,
|
||||||
|
stickToBottom = true,
|
||||||
|
}: {
|
||||||
|
logs: LogsType;
|
||||||
|
hideTimestamp?: boolean;
|
||||||
|
className?: string;
|
||||||
|
stickToBottom?: boolean;
|
||||||
|
}) {
|
||||||
const container = useRef<HTMLDivElement | null>(null);
|
const container = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!stickToBottom) return;
|
||||||
|
|
||||||
// console.log(logs.length, container.current);
|
// console.log(logs.length, container.current);
|
||||||
if (container.current) {
|
if (container.current) {
|
||||||
const scrollHeight = container.current.scrollHeight;
|
const scrollHeight = container.current.scrollHeight;
|
||||||
@@ -21,11 +35,12 @@ export function LogsViewer({ logs }: { logs: LogsType }) {
|
|||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [logs.length]);
|
}, [logs.length, stickToBottom]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(ref) => {
|
ref={(ref) => {
|
||||||
|
if (!stickToBottom) return;
|
||||||
if (!container.current && ref) {
|
if (!container.current && ref) {
|
||||||
const scrollHeight = ref.scrollHeight;
|
const scrollHeight = ref.scrollHeight;
|
||||||
|
|
||||||
@@ -36,10 +51,31 @@ export function LogsViewer({ logs }: { logs: LogsType }) {
|
|||||||
}
|
}
|
||||||
container.current = ref;
|
container.current = ref;
|
||||||
}}
|
}}
|
||||||
className="flex flex-col text-xs p-2 overflow-y-scroll max-h-[400px] whitespace-break-spaces"
|
className={cn(
|
||||||
|
"h-full w-full flex flex-col text-xs p-2 overflow-y-scroll whitespace-break-spaces",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{logs.map((x, i) => (
|
{logs.map((x, i) => (
|
||||||
<div key={i}>{x.logs}</div>
|
<div
|
||||||
|
key={i}
|
||||||
|
className="hover:bg-gray-100 flex flex-row items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
navigator.clipboard.writeText(x.logs);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!hideTimestamp && x.timestamp != undefined && (
|
||||||
|
<>
|
||||||
|
<span className="w-[150px] flex-shrink-0">
|
||||||
|
{new Date(x.timestamp * 1000).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<div className="h-full w-[1px] bg-stone-400 flex-shrink-0"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Display timestamp */}
|
||||||
|
<div>{x.logs}</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export function MachineBuildLog({
|
export function MachineBuildLog({
|
||||||
@@ -41,7 +41,7 @@ export function MachineBuildLog({
|
|||||||
reconnectAttempts: 20,
|
reconnectAttempts: 20,
|
||||||
reconnectInterval: 1000,
|
reconnectInterval: 1000,
|
||||||
queryParams: query,
|
queryParams: query,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectionStatus = getConnectionStatus(readyState);
|
const connectionStatus = getConnectionStatus(readyState);
|
||||||
@@ -57,59 +57,70 @@ export function MachineBuildLog({
|
|||||||
setLogs((logs) => [...(logs ?? []), message.data]);
|
setLogs((logs) => [...(logs ?? []), message.data]);
|
||||||
} else if (message?.event === "FINISHED") {
|
} else if (message?.event === "FINISHED") {
|
||||||
setFinished(true);
|
setFinished(true);
|
||||||
setStatus(message.data.status)
|
setStatus(message.data.status);
|
||||||
}
|
}
|
||||||
}, [lastMessage]);
|
}, [lastMessage]);
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{connectionStatus}
|
{connectionStatus}
|
||||||
<LogsViewer logs={logs} />
|
<LogsViewer logs={logs} className="h-full max-h-[600px]" />
|
||||||
|
|
||||||
<AlertDialog open={finished}>
|
<AlertDialog open={finished}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
{
|
{status == "succuss" ? (
|
||||||
status == "succuss" ? (
|
|
||||||
<>
|
<>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Machine Built</AlertDialogTitle>
|
<AlertDialogTitle>Machine Built</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Your machine is built, you can now integrate your API, or directly run to check this machines.
|
Your machine is built, you can now integrate your API, or
|
||||||
|
directly run to check this machines.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction onClick={() => {
|
<AlertDialogAction
|
||||||
router.push("/workflows")
|
onClick={() => {
|
||||||
}}>See Workflows</AlertDialogAction>
|
router.push("/workflows");
|
||||||
<AlertDialogAction onClick={() => {
|
}}
|
||||||
router.push("/machines")
|
>
|
||||||
}}>See All Machines</AlertDialogAction>
|
See Workflows
|
||||||
</AlertDialogFooter></>
|
</AlertDialogAction>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/machines");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
See All Machines
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Machine Failed</AlertDialogTitle>
|
<AlertDialogTitle>Machine Failed</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Something went wrong with the machine build, please check the log.
|
Something went wrong with the machine build, please check the
|
||||||
Possible cause could be conflits with custom nodes, build got stuck, timeout, or too many custom nodes installed.
|
log. Possible cause could be conflits with custom nodes, build
|
||||||
Please attempt a rebuild or remove some of the custom nodes.
|
got stuck, timeout, or too many custom nodes installed. Please
|
||||||
|
attempt a rebuild or remove some of the custom nodes.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>See logs</AlertDialogCancel>
|
<AlertDialogCancel>See logs</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={() => {
|
<AlertDialogAction
|
||||||
router.push("/machines")
|
onClick={() => {
|
||||||
}}>Back to machines</AlertDialogAction>
|
router.push("/machines");
|
||||||
</AlertDialogFooter></>
|
}}
|
||||||
)
|
>
|
||||||
}
|
Back to machines
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type State = {
|
|||||||
json: {
|
json: {
|
||||||
event: string;
|
event: string;
|
||||||
data: any;
|
data: any;
|
||||||
}
|
},
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ function MachineWS({
|
|||||||
const logs = useStore((x) =>
|
const logs = useStore((x) =>
|
||||||
x.logs
|
x.logs
|
||||||
.filter((p) => p.machine_id === machine.id)
|
.filter((p) => p.machine_id === machine.id)
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
);
|
);
|
||||||
const [sid, setSid] = useState("");
|
const [sid, setSid] = useState("");
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ function MachineWS({
|
|||||||
// queryParams: {
|
// queryParams: {
|
||||||
// clientId: sid,
|
// clientId: sid,
|
||||||
// },
|
// },
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectionStatus = getConnectionStatus(readyState);
|
const connectionStatus = getConnectionStatus(readyState);
|
||||||
@@ -135,7 +135,9 @@ function MachineWS({
|
|||||||
You can view your run's outputs here
|
You can view your run's outputs here
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<LogsViewer logs={logs} />
|
<div className="h-[400px]">
|
||||||
|
<LogsViewer logs={logs} hideTimestamp />
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { InsertModal } from "./InsertModal";
|
import { InsertModal } from "./InsertModal";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -30,9 +37,9 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { ArrowUpDown } from "lucide-react";
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { addModel } from "@/server/curdModel";
|
import { addModel, deleteModel } from "@/server/curdModel";
|
||||||
import { downloadUrlModelSchema } from "@/server/addCivitaiModelSchema";
|
import { downloadUrlModelSchema } from "@/server/addCivitaiModelSchema";
|
||||||
import { modelEnumType } from "@/db/schema";
|
import { modelEnumType } from "@/db/schema";
|
||||||
|
|
||||||
@@ -192,10 +199,16 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
|||||||
lora: "green",
|
lora: "green",
|
||||||
embedding: "violet",
|
embedding: "violet",
|
||||||
vae: "teal",
|
vae: "teal",
|
||||||
|
clip: "default",
|
||||||
|
clip_vision: "default",
|
||||||
|
configs: "default",
|
||||||
|
controlnet: "default",
|
||||||
|
upscale_models: "default",
|
||||||
|
ipadapter: "default",
|
||||||
};
|
};
|
||||||
|
|
||||||
function getBadgeColor(modelType: modelEnumType) {
|
function getBadgeColor(modelType: modelEnumType) {
|
||||||
return model_type_map[modelType] || "default";
|
return model_type_map[modelType]
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = getBadgeColor(row.original.model_type);
|
const color = getBadgeColor(row.original.model_type);
|
||||||
@@ -225,35 +238,35 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
// TODO: deletion and editing for future sprint
|
// TODO: deletion and editing for future sprint
|
||||||
// {
|
{
|
||||||
// id: "actions",
|
id: "actions",
|
||||||
// enableHiding: false,
|
enableHiding: false,
|
||||||
// cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
// const checkpoint = row.original;
|
const model = row.original;
|
||||||
//
|
|
||||||
// return (
|
return (
|
||||||
// <DropdownMenu>
|
<DropdownMenu>
|
||||||
// <DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
// <Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
// <span className="sr-only">Open menu</span>
|
<span className="sr-only">Open menu</span>
|
||||||
// <MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
// </Button>
|
</Button>
|
||||||
// </DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
// <DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
// <DropdownMenuItem
|
<DropdownMenuItem
|
||||||
// className="text-destructive"
|
className="text-destructive"
|
||||||
// onClick={() => {
|
onClick={() => {
|
||||||
// deleteWorkflow(checkpoint.id);
|
deleteModel(model.id);
|
||||||
// }}
|
}}
|
||||||
// >
|
>
|
||||||
// Delete Workflow
|
Delete Model
|
||||||
// </DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
// </DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
// </DropdownMenu>
|
</DropdownMenu>
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ModelList({ data }: { data: ModelItemList[] }) {
|
export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||||
|
|||||||
@@ -1,27 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import useSWR from "swr";
|
||||||
import { DownloadButton } from "./DownloadButton";
|
import { DownloadButton } from "./DownloadButton";
|
||||||
import { getFileDownloadUrl } from "@/server/getFileDownloadUrl";
|
import { getFileDownloadUrl } from "@/server/getFileDownloadUrl";
|
||||||
|
|
||||||
export async function OutputRender(props: {
|
export function OutputRender(props: {
|
||||||
run_id: string;
|
run_id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { data: url } = useSWR(
|
||||||
|
"run-outputs+" + props.run_id + props.filename,
|
||||||
|
async () => {
|
||||||
|
return await getFileDownloadUrl(
|
||||||
|
`outputs/runs/${props.run_id}/${props.filename}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!url) return <></>;
|
||||||
|
|
||||||
|
if (props.filename.endsWith(".mp4") || props.filename.endsWith(".webm")) {
|
||||||
|
return (
|
||||||
|
<video controls autoPlay className="w-[400px]">
|
||||||
|
<source src={url} type="video/mp4" />
|
||||||
|
<source src={url} type="video/webm" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
props.filename.endsWith(".png") ||
|
props.filename.endsWith(".png") ||
|
||||||
props.filename.endsWith(".gif") ||
|
props.filename.endsWith(".gif") ||
|
||||||
props.filename.endsWith(".jpg") ||
|
props.filename.endsWith(".jpg") ||
|
||||||
props.filename.endsWith(".jpeg")
|
props.filename.endsWith(".jpeg")
|
||||||
) {
|
) {
|
||||||
const url = await getFileDownloadUrl(
|
|
||||||
`outputs/runs/${props.run_id}/${props.filename}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return <img className="max-w-[200px]" alt={props.filename} src={url} />;
|
return <img className="max-w-[200px]" alt={props.filename} src={url} />;
|
||||||
} else {
|
} else {
|
||||||
const url = await getFileDownloadUrl(
|
|
||||||
`outputs/runs/${props.run_id}/${props.filename}`
|
|
||||||
);
|
|
||||||
// console.log(url);
|
|
||||||
|
|
||||||
return <DownloadButton filename={props.filename} href={url} />;
|
return <DownloadButton filename={props.filename} href={url} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { RunInputs } from "@/components/RunInputs";
|
import { RunInputs } from "@/components/RunInputs";
|
||||||
import { RunOutputs } from "@/components/RunOutputs";
|
import { RunOutputs } from "@/components/RunOutputs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -17,10 +19,10 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
|
import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
|
||||||
import { type findAllRuns } from "@/server/findAllRuns";
|
import { type findAllRuns } from "@/server/findAllRuns";
|
||||||
import { Suspense } from "react";
|
|
||||||
import { LiveStatus } from "./LiveStatus";
|
import { LiveStatus } from "./LiveStatus";
|
||||||
|
import { LoadingWrapper } from "@/components/LoadingWrapper";
|
||||||
|
|
||||||
export async function RunDisplay({
|
export function RunDisplay({
|
||||||
run,
|
run,
|
||||||
}: {
|
}: {
|
||||||
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
||||||
@@ -73,9 +75,9 @@ export async function RunDisplay({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="max-h-96 overflow-y-scroll">
|
<div className="max-h-96 overflow-y-scroll">
|
||||||
<RunInputs run={run} />
|
<RunInputs run={run} />
|
||||||
<Suspense>
|
<LoadingWrapper tag="output">
|
||||||
<RunOutputs run_id={run.id} />
|
<RunOutputs run={run} />
|
||||||
</Suspense>
|
</LoadingWrapper>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import type { findAllRuns } from "@/server/findAllRuns";
|
import type { findAllRuns } from "@/server/findAllRuns";
|
||||||
|
|
||||||
export async function RunInputs({
|
export function RunInputs({
|
||||||
run,
|
run,
|
||||||
}: {
|
}: {
|
||||||
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
import { OutputRender } from "./OutputRender";
|
import { OutputRender } from "./OutputRender";
|
||||||
import { CodeBlock } from "@/components/CodeBlock";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -8,10 +19,25 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import type { findAllRuns } from "@/server/findAllRuns";
|
||||||
import { getRunsOutput } from "@/server/getRunsOutput";
|
import { getRunsOutput } from "@/server/getRunsOutput";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { LogsViewer } from "@/components/LogsViewer";
|
||||||
|
import { CopyButton } from "@/components/CopyButton";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { CodeBlockClient } from "@/components/CodeBlockClient";
|
||||||
|
|
||||||
|
export function RunOutputs({
|
||||||
|
run,
|
||||||
|
}: { run: Awaited<ReturnType<typeof findAllRuns>>[0] }) {
|
||||||
|
const { data, isValidating, error } = useSWR(
|
||||||
|
"run-outputs+" + run.id,
|
||||||
|
async () => {
|
||||||
|
return await getRunsOutput(run.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export async function RunOutputs({ run_id }: { run_id: string }) {
|
|
||||||
const outputs = await getRunsOutput(run_id);
|
|
||||||
return (
|
return (
|
||||||
<Table className="table-fixed">
|
<Table className="table-fixed">
|
||||||
<TableHeader className="bg-background top-0 sticky">
|
<TableHeader className="bg-background top-0 sticky">
|
||||||
@@ -21,7 +47,43 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{outputs?.map((run) => {
|
<TableRow key={run.id}>
|
||||||
|
<TableCell className="break-words">Run log</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{run.run_log ? (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="secondary" className="w-fit">
|
||||||
|
View Log <ExternalLink size={14} />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[1000px] h-full max-h-[600px] grid grid-rows-[auto,1fr,auto]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Run Log</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<LogsViewer logs={run.run_log} stickToBottom={false} />
|
||||||
|
<DialogFooter>
|
||||||
|
<CopyButton
|
||||||
|
className="w-fit aspect-auto p-4"
|
||||||
|
text={JSON.stringify(run.run_log)}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</CopyButton>
|
||||||
|
<DialogClose>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
) : (
|
||||||
|
"No log available"
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
{data?.map((run) => {
|
||||||
const fileName =
|
const fileName =
|
||||||
run.data.images?.[0].filename ||
|
run.data.images?.[0].filename ||
|
||||||
run.data.files?.[0].filename ||
|
run.data.files?.[0].filename ||
|
||||||
@@ -32,7 +94,7 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
|||||||
<TableRow key={run.id}>
|
<TableRow key={run.id}>
|
||||||
<TableCell>Output</TableCell>
|
<TableCell>Output</TableCell>
|
||||||
<TableCell className="">
|
<TableCell className="">
|
||||||
<CodeBlock
|
<CodeBlockClient
|
||||||
code={JSON.stringify(run.data, null, 2)}
|
code={JSON.stringify(run.data, null, 2)}
|
||||||
lang="json"
|
lang="json"
|
||||||
/>
|
/>
|
||||||
@@ -45,7 +107,7 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
|||||||
<TableRow key={run.id}>
|
<TableRow key={run.id}>
|
||||||
<TableCell className="break-words">{fileName}</TableCell>
|
<TableCell className="break-words">{fileName}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<OutputRender run_id={run_id} filename={fileName} />
|
<OutputRender run_id={run.run_id} filename={fileName} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -7,38 +9,53 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { parseAsInteger } from "next-usequerystate";
|
import { parseAsInteger } from "next-usequerystate";
|
||||||
import { headers } from "next/headers";
|
|
||||||
import {
|
import {
|
||||||
findAllDeployments,
|
|
||||||
findAllRunsWithCounts,
|
findAllRunsWithCounts,
|
||||||
|
getAllRunstableContent,
|
||||||
} from "../server/findAllRuns";
|
} from "../server/findAllRuns";
|
||||||
import { DeploymentDisplay } from "./DeploymentDisplay";
|
|
||||||
import { PaginationControl } from "./PaginationControl";
|
import { PaginationControl } from "./PaginationControl";
|
||||||
import { RunDisplay } from "./RunDisplay";
|
import { RunDisplay } from "./RunDisplay";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||||
|
|
||||||
const itemPerPage = 6;
|
const itemPerPage = 6;
|
||||||
const pageParser = parseAsInteger.withDefault(1);
|
const pageParser = parseAsInteger.withDefault(1);
|
||||||
|
|
||||||
export async function RunsTable(props: {
|
export function RunsTable(props: {
|
||||||
workflow_id: string;
|
workflow_id: string;
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: any };
|
||||||
}) {
|
}) {
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 5000));
|
const page = pageParser.parse(props.searchParams?.page ?? undefined) ?? 1;
|
||||||
const page = pageParser.parseServerSide(
|
const { data, error, isLoading, isValidating } = useSWR(
|
||||||
props.searchParams?.page ?? undefined
|
"runs+" + page,
|
||||||
);
|
async () => {
|
||||||
const { allRuns, total } = await findAllRunsWithCounts({
|
const data = await findAllRunsWithCounts({
|
||||||
workflow_id: props.workflow_id,
|
workflow_id: props.workflow_id,
|
||||||
limit: itemPerPage,
|
limit: itemPerPage,
|
||||||
offset: (page - 1) * itemPerPage,
|
offset: (page - 1) * itemPerPage,
|
||||||
});
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// suspense: false,
|
||||||
|
refreshInterval: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{isValidating ? (
|
||||||
|
<div className="absolute right-8 top-8">
|
||||||
|
<LoadingIcon />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="overflow-auto h-fit w-full">
|
<div className="overflow-auto h-fit w-full">
|
||||||
<Table className="">
|
<Table className="">
|
||||||
{allRuns.length === 0 && (
|
{/* {data?.allRuns.length === 0 && (
|
||||||
<TableCaption>A list of your recent runs.</TableCaption>
|
<TableCaption>A list of your recent runs.</TableCaption>
|
||||||
)}
|
)} */}
|
||||||
<TableHeader className="bg-background top-0 sticky">
|
<TableHeader className="bg-background top-0 sticky">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="truncate">Number</TableHead>
|
<TableHead className="truncate">Number</TableHead>
|
||||||
@@ -52,49 +69,19 @@ export async function RunsTable(props: {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{allRuns.map((run) => (
|
{data?.allRuns.map((run) => (
|
||||||
<RunDisplay run={run} key={run.id} />
|
<RunDisplay run={run} key={run.id} />
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Math.ceil(total / itemPerPage) > 0 && (
|
{data && Math.ceil(data.total / itemPerPage) > 0 && (
|
||||||
<PaginationControl
|
<PaginationControl
|
||||||
totalPage={Math.ceil(total / itemPerPage)}
|
totalPage={Math.ceil(data.total / itemPerPage)}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DeploymentsTable(props: { workflow_id: string }) {
|
|
||||||
const allRuns = await findAllDeployments(props.workflow_id);
|
|
||||||
|
|
||||||
const headersList = headers();
|
|
||||||
const host = headersList.get("host") || "";
|
|
||||||
const protocol = headersList.get("x-forwarded-proto") || "";
|
|
||||||
const domain = `${protocol}://${host}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-auto h-fit w-full">
|
|
||||||
<Table className="">
|
|
||||||
<TableCaption>A list of your deployments</TableCaption>
|
|
||||||
<TableHeader className="bg-background top-0 sticky">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className=" w-[100px]">Environment</TableHead>
|
|
||||||
<TableHead className=" w-[100px]">Version</TableHead>
|
|
||||||
<TableHead className="">Machine</TableHead>
|
|
||||||
<TableHead className=" text-right">Updated At</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{allRuns.map((run) => (
|
|
||||||
<DeploymentDisplay deployment={run} key={run.id} domain={domain} />
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export function StatusBadge({
|
|||||||
);
|
);
|
||||||
case "success":
|
case "success":
|
||||||
return <Badge variant="success">{status}</Badge>;
|
return <Badge variant="success">{status}</Badge>;
|
||||||
|
case "timeout":
|
||||||
|
return <Badge variant="amber">{status}</Badge>;
|
||||||
case "failed":
|
case "failed":
|
||||||
return <Badge variant="destructive">{status}</Badge>;
|
return <Badge variant="destructive">{status}</Badge>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||||
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
|
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -28,36 +27,19 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import type { showcaseMediaNullable } from "@/db/schema";
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import type { showcaseMediaNullable, workflowAPINodeType } from "@/db/schema";
|
|
||||||
import { checkStatus, createRun } from "@/server/createRun";
|
import { checkStatus, createRun } from "@/server/createRun";
|
||||||
import { createDeployments } from "@/server/curdDeploments";
|
import { createDeployments } from "@/server/curdDeploments";
|
||||||
import type { getMachines } from "@/server/curdMachine";
|
import type { getMachines } from "@/server/curdMachine";
|
||||||
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
||||||
import {
|
import { Copy, Edit, MoreVertical, Play } from "lucide-react";
|
||||||
Copy,
|
|
||||||
Edit,
|
|
||||||
ExternalLink,
|
|
||||||
Info,
|
|
||||||
MoreVertical,
|
|
||||||
Play,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
|
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
|
||||||
import { callServerPromise } from "./callServerPromise";
|
import { callServerPromise } from "./callServerPromise";
|
||||||
import fetcher from "./fetcher";
|
|
||||||
import { ButtonAction } from "@/components/ButtonActionLoader";
|
import { ButtonAction } from "@/components/ButtonActionLoader";
|
||||||
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
|
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
@@ -125,29 +107,48 @@ export function MachineSelect({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectedMachineStore = {
|
||||||
|
selectedMachine: string | undefined;
|
||||||
|
setSelectedMachine: (machine: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectedMachineStore = create<SelectedMachineStore>((set) => ({
|
||||||
|
selectedMachine: undefined,
|
||||||
|
setSelectedMachine: (machine) => set(() => ({ selectedMachine: machine })),
|
||||||
|
}));
|
||||||
|
|
||||||
export function useSelectedMachine(
|
export function useSelectedMachine(
|
||||||
machines: Awaited<ReturnType<typeof getMachines>>,
|
machines: Awaited<ReturnType<typeof getMachines>>,
|
||||||
): [string, (v: string) => void] {
|
): [string, (v: string) => void] {
|
||||||
const searchParams = useSearchParams();
|
const { selectedMachine, setSelectedMachine } = selectedMachineStore();
|
||||||
const pathname = usePathname();
|
return [selectedMachine ?? machines?.[0]?.id ?? "", setSelectedMachine];
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const createQueryString = useCallback(
|
// const searchParams = useSearchParams();
|
||||||
(name: string, value: string) => {
|
// const pathname = usePathname();
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
// const router = useRouter();
|
||||||
params.set(name, value);
|
|
||||||
|
|
||||||
return params.toString();
|
// const createQueryString = useCallback(
|
||||||
},
|
// (name: string, value: string) => {
|
||||||
[searchParams],
|
// const params = new URLSearchParams(searchParams.toString());
|
||||||
);
|
// params.set(name, value);
|
||||||
|
|
||||||
return [
|
// return params.toString();
|
||||||
searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
// },
|
||||||
(v: string) => {
|
// [searchParams],
|
||||||
router.push(pathname + "?" + createQueryString("machine", v));
|
// );
|
||||||
},
|
|
||||||
];
|
// return [
|
||||||
|
// searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
||||||
|
// (v: string) => {
|
||||||
|
// // window.history.pushState(
|
||||||
|
// // "new url",
|
||||||
|
// // "",
|
||||||
|
// // pathname + "?" + createQueryString("machine", v),
|
||||||
|
// // );
|
||||||
|
// // router.push(pathname + "?" + createQueryString("machine", v));
|
||||||
|
// router.replace(pathname + "?" + createQueryString("machine", v));
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
}
|
}
|
||||||
|
|
||||||
type PublicRunStore = {
|
type PublicRunStore = {
|
||||||
@@ -500,150 +501,3 @@ export function getWorkflowVersionFromVersionIndex(
|
|||||||
|
|
||||||
return workflow_version;
|
return workflow_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ViewWorkflowDetailsButton({
|
|
||||||
workflow,
|
|
||||||
}: {
|
|
||||||
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
|
|
||||||
}) {
|
|
||||||
const [version] = useQueryState("version", {
|
|
||||||
defaultValue: workflow?.versions[0].version ?? 1,
|
|
||||||
...parseAsInteger,
|
|
||||||
});
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
isLoading: isNodesIndexLoading,
|
|
||||||
} = useSWR(
|
|
||||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
|
|
||||||
fetcher,
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupedByAuxName = useMemo(() => {
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
// console.log(data);
|
|
||||||
|
|
||||||
const workflow_version = getWorkflowVersionFromVersionIndex(
|
|
||||||
workflow,
|
|
||||||
version,
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = workflow_version?.workflow_api;
|
|
||||||
|
|
||||||
if (!api) return null;
|
|
||||||
|
|
||||||
const crossCheckedApi = Object.entries(api)
|
|
||||||
.map(([_, value]) => {
|
|
||||||
const classType = value.class_type;
|
|
||||||
const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
|
|
||||||
nodeArray[0].includes(classType),
|
|
||||||
);
|
|
||||||
return classTypeData ? { node: value, classTypeData } : null;
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null);
|
|
||||||
|
|
||||||
// console.log(crossCheckedApi);
|
|
||||||
|
|
||||||
const groupedByAuxName = crossCheckedApi.reduce(
|
|
||||||
(acc, data) => {
|
|
||||||
if (!data) return acc;
|
|
||||||
|
|
||||||
const { node, classTypeData } = data;
|
|
||||||
const auxName = classTypeData[1][1].title_aux;
|
|
||||||
// console.log(auxName);
|
|
||||||
if (!acc[auxName]) {
|
|
||||||
acc[auxName] = {
|
|
||||||
url: classTypeData[0],
|
|
||||||
node: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
acc[auxName].node.push(node);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
node: z.infer<typeof workflowAPINodeType>[];
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// console.log(groupedByAuxName);
|
|
||||||
|
|
||||||
return groupedByAuxName;
|
|
||||||
}, [version, data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
|
||||||
<Button className="gap-2" variant="outline">
|
|
||||||
Details <Info size={14} />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Workflow Details</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
View your custom nodes, models, external files used in this workflow
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="overflow-auto max-h-[400px] w-full">
|
|
||||||
<Table>
|
|
||||||
<TableHeader className="bg-background top-0 sticky">
|
|
||||||
<TableRow>
|
|
||||||
<TableHead className="w-[200px]">File</TableHead>
|
|
||||||
<TableHead className="">Output</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{groupedByAuxName &&
|
|
||||||
Object.entries(groupedByAuxName).map(([key, group]) => {
|
|
||||||
// const filePath
|
|
||||||
return (
|
|
||||||
<TableRow key={key}>
|
|
||||||
<TableCell className="break-words">
|
|
||||||
<a
|
|
||||||
href={group.url}
|
|
||||||
target="_blank"
|
|
||||||
className="hover:underline"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
<ExternalLink
|
|
||||||
className="inline-block ml-1"
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="flex flex-wrap gap-2">
|
|
||||||
{group.node.map((x) => (
|
|
||||||
<Badge key={x.class_type} variant="outline">
|
|
||||||
{x.class_type}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button className="w-fit" onClick={() => setOpen(false)}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/* </div> */}
|
|
||||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
"use client";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import type { workflowAPINodeType } from "@/db/schema";
|
||||||
|
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
||||||
|
import { ExternalLink, Info } from "lucide-react";
|
||||||
|
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import type { z } from "zod";
|
||||||
|
import fetcher from "./fetcher";
|
||||||
|
import { getWorkflowVersionFromVersionIndex } from "./VersionSelect";
|
||||||
|
|
||||||
|
export function ViewWorkflowDetailsButton({
|
||||||
|
workflow,
|
||||||
|
}: {
|
||||||
|
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
|
||||||
|
}) {
|
||||||
|
const [version] = useQueryState("version", {
|
||||||
|
defaultValue: workflow?.versions[0].version ?? 1,
|
||||||
|
...parseAsInteger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
isLoading: isNodesIndexLoading,
|
||||||
|
} = useSWR(
|
||||||
|
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedByAuxName = useMemo(() => {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const workflow_version = getWorkflowVersionFromVersionIndex(
|
||||||
|
workflow,
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = workflow_version?.workflow_api;
|
||||||
|
|
||||||
|
if (!api) return null;
|
||||||
|
|
||||||
|
const crossCheckedApi = Object.entries(api)
|
||||||
|
.map(([_, value]) => {
|
||||||
|
const classType = value.class_type;
|
||||||
|
const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
|
||||||
|
nodeArray[0].includes(classType),
|
||||||
|
);
|
||||||
|
return classTypeData ? { node: value, classTypeData } : null;
|
||||||
|
})
|
||||||
|
.filter((item) => item !== null);
|
||||||
|
|
||||||
|
// console.log(crossCheckedApi);
|
||||||
|
const groupedByAuxName = crossCheckedApi.reduce(
|
||||||
|
(acc, data) => {
|
||||||
|
if (!data) return acc;
|
||||||
|
|
||||||
|
const { node, classTypeData } = data;
|
||||||
|
const auxName = classTypeData[1][1].title_aux;
|
||||||
|
// console.log(auxName);
|
||||||
|
if (!acc[auxName]) {
|
||||||
|
acc[auxName] = {
|
||||||
|
url: classTypeData[0],
|
||||||
|
node: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
acc[auxName].node.push(node);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
node: z.infer<typeof workflowAPINodeType>[];
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// console.log(groupedByAuxName);
|
||||||
|
return groupedByAuxName;
|
||||||
|
}, [version, data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||||
|
<Button className="gap-2" variant="outline">
|
||||||
|
Details <Info size={14} />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Workflow Details</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
View your custom nodes, models, external files used in this workflow
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="overflow-auto max-h-[400px] w-full">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-background top-0 sticky">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-[200px]">File</TableHead>
|
||||||
|
<TableHead className="">Output</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{groupedByAuxName &&
|
||||||
|
Object.entries(groupedByAuxName).map(([key, group]) => {
|
||||||
|
// const filePath
|
||||||
|
return (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell className="break-words">
|
||||||
|
<a
|
||||||
|
href={group.url}
|
||||||
|
target="_blank"
|
||||||
|
className="hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
<ExternalLink
|
||||||
|
className="inline-block ml-1"
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex flex-wrap gap-2">
|
||||||
|
{group.node.map((x) => (
|
||||||
|
<Badge key={x.class_type} variant="outline">
|
||||||
|
{x.class_type}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button className="w-fit">Close</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</div>
|
||||||
|
{/* </div> */}
|
||||||
|
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -53,9 +53,8 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { z } from "zod";
|
import { getBranchInfo } from "./getBranchInfo";
|
||||||
|
|
||||||
export function SnapshotPickerView({
|
export function SnapshotPickerView({
|
||||||
field,
|
field,
|
||||||
@@ -131,7 +130,7 @@ export function SnapshotPickerView({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled={key.endsWith("comfyui-deploy.git")}
|
disabled={key.endsWith("comfyui-deploy")}
|
||||||
// className="opacity-50"
|
// className="opacity-50"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newNodeList = {
|
const newNodeList = {
|
||||||
@@ -148,6 +147,29 @@ export function SnapshotPickerView({
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
// className="opacity-50"
|
||||||
|
onClick={async () => {
|
||||||
|
const newNodeList = {
|
||||||
|
...field.value.git_custom_nodes,
|
||||||
|
};
|
||||||
|
|
||||||
|
const branchInfo = await getBranchInfo(key);
|
||||||
|
|
||||||
|
if (!branchInfo) return;
|
||||||
|
|
||||||
|
newNodeList[key].hash = branchInfo?.commit.sha;
|
||||||
|
|
||||||
|
const nodeList = newNodeList;
|
||||||
|
const newValue = {
|
||||||
|
...field.value,
|
||||||
|
git_custom_nodes: nodeList,
|
||||||
|
};
|
||||||
|
field.onChange(newValue);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -268,24 +290,6 @@ type CustomNodeList = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepoSchema = z.object({
|
|
||||||
default_branch: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const BranchInfoSchema = z.object({
|
|
||||||
commit: z.object({
|
|
||||||
sha: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
function extractRepoName(repoUrl: string) {
|
|
||||||
const url = new URL(repoUrl);
|
|
||||||
const pathParts = url.pathname.split("/");
|
|
||||||
const repoName = pathParts[2].replace(".git", "");
|
|
||||||
const author = pathParts[1];
|
|
||||||
return `${author}/${repoName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CustomNodesSelector({
|
function CustomNodesSelector({
|
||||||
field,
|
field,
|
||||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||||
@@ -362,43 +366,7 @@ function CustomNodesSelector({
|
|||||||
delete newNodeList[currentValue];
|
delete newNodeList[currentValue];
|
||||||
nodeList = newNodeList;
|
nodeList = newNodeList;
|
||||||
} else {
|
} else {
|
||||||
const repoName = extractRepoName(currentValue);
|
const branchInfo = await getBranchInfo(currentValue);
|
||||||
const id = toast.loading(`Fetching repo info...`);
|
|
||||||
const repo = await fetch(
|
|
||||||
`https://api.github.com/repos/${repoName}`,
|
|
||||||
)
|
|
||||||
.then((x) => x.json())
|
|
||||||
.then((x) => {
|
|
||||||
console.log(x);
|
|
||||||
return x;
|
|
||||||
})
|
|
||||||
.then((x) => RepoSchema.parse(x))
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.error(
|
|
||||||
`Failed to fetch repo info ${e.message}`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!repo) return;
|
|
||||||
const branch = repo.default_branch;
|
|
||||||
const branchInfo = await fetch(
|
|
||||||
`https://api.github.com/repos/${repoName}/branches/${branch}`,
|
|
||||||
)
|
|
||||||
.then((x) => x.json())
|
|
||||||
.then((x) => BranchInfoSchema.parse(x))
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast.dismiss(id);
|
|
||||||
toast.error(
|
|
||||||
`Failed to fetch branch info ${e.message}`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.dismiss(id);
|
|
||||||
|
|
||||||
if (!branchInfo) return;
|
if (!branchInfo) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const RepoSchema = z.object({
|
||||||
|
default_branch: z.string(),
|
||||||
|
});
|
||||||
|
const BranchInfoSchema = z.object({
|
||||||
|
commit: z.object({
|
||||||
|
sha: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
function extractRepoName(repoUrl: string) {
|
||||||
|
const url = new URL(repoUrl);
|
||||||
|
const pathParts = url.pathname.split("/");
|
||||||
|
const repoName = pathParts[2].replace(".git", "");
|
||||||
|
const author = pathParts[1];
|
||||||
|
return `${author}/${repoName}`;
|
||||||
|
}
|
||||||
|
export async function getBranchInfo(gitUrl: string) {
|
||||||
|
const repoName = extractRepoName(gitUrl);
|
||||||
|
const id = toast.loading(`Fetching repo info...`);
|
||||||
|
const repo = await fetch(`https://api.github.com/repos/${repoName}`)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => {
|
||||||
|
console.log(x);
|
||||||
|
return x;
|
||||||
|
})
|
||||||
|
.then((x) => RepoSchema.parse(x))
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error(`Failed to fetch repo info ${e.message}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repo) return;
|
||||||
|
const branch = repo.default_branch;
|
||||||
|
const branchInfo = await fetch(
|
||||||
|
`https://api.github.com/repos/${repoName}/branches/${branch}`,
|
||||||
|
)
|
||||||
|
.then((x) => x.json())
|
||||||
|
.then((x) => BranchInfoSchema.parse(x))
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
toast.dismiss(id);
|
||||||
|
toast.error(`Failed to fetch branch info ${e.message}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.dismiss(id);
|
||||||
|
return branchInfo;
|
||||||
|
}
|
||||||
+17
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import { LogsType } from "@/components/LogsViewer";
|
||||||
import { CivitaiModelResponse } from "@/types/civitai";
|
import { CivitaiModelResponse } from "@/types/civitai";
|
||||||
import { type InferSelectModel, relations } from "drizzle-orm";
|
import { type InferSelectModel, relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
@@ -6,10 +7,10 @@ import {
|
|||||||
jsonb,
|
jsonb,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
pgSchema,
|
pgSchema,
|
||||||
|
real,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uuid,
|
uuid,
|
||||||
real,
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||||
import { TypeOf, z } from "zod";
|
import { TypeOf, z } from "zod";
|
||||||
@@ -104,6 +105,7 @@ export const workflowRunStatus = pgEnum("workflow_run_status", [
|
|||||||
"failed",
|
"failed",
|
||||||
"started",
|
"started",
|
||||||
"queued",
|
"queued",
|
||||||
|
"timeout",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const deploymentEnvironment = pgEnum("deployment_environment", [
|
export const deploymentEnvironment = pgEnum("deployment_environment", [
|
||||||
@@ -148,8 +150,9 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
|
|||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
workflow_inputs:
|
workflow_inputs: jsonb("workflow_inputs").$type<
|
||||||
jsonb("workflow_inputs").$type<Record<string, string | number>>(),
|
Record<string, string | number>
|
||||||
|
>(),
|
||||||
workflow_id: uuid("workflow_id")
|
workflow_id: uuid("workflow_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => workflowTable.id, {
|
.references(() => workflowTable.id, {
|
||||||
@@ -172,6 +175,7 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
|
|||||||
machine_type: machinesType("machine_type"),
|
machine_type: machinesType("machine_type"),
|
||||||
user_id: text("user_id"),
|
user_id: text("user_id"),
|
||||||
org_id: text("org_id"),
|
org_id: text("org_id"),
|
||||||
|
run_log: jsonb("run_log").$type<LogsType>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const workflowRunRelations = relations(
|
export const workflowRunRelations = relations(
|
||||||
@@ -295,8 +299,9 @@ export const deploymentsTable = dbSchema.table("deployments", {
|
|||||||
.references(() => machinesTable.id),
|
.references(() => machinesTable.id),
|
||||||
share_slug: text("share_slug").unique(),
|
share_slug: text("share_slug").unique(),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
showcase_media:
|
showcase_media: jsonb("showcase_media").$type<
|
||||||
jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(),
|
z.infer<typeof showcaseMedia>
|
||||||
|
>(),
|
||||||
environment: deploymentEnvironment("environment").notNull(),
|
environment: deploymentEnvironment("environment").notNull(),
|
||||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||||
@@ -391,7 +396,13 @@ export const modelTypes = [
|
|||||||
"lora",
|
"lora",
|
||||||
"embedding",
|
"embedding",
|
||||||
"vae",
|
"vae",
|
||||||
] as const
|
"clip",
|
||||||
|
"clip_vision",
|
||||||
|
"configs",
|
||||||
|
"controlnet",
|
||||||
|
"upscale_models",
|
||||||
|
"ipadapter",
|
||||||
|
] as const;
|
||||||
export const modelType = pgEnum("model_type", modelTypes);
|
export const modelType = pgEnum("model_type", modelTypes);
|
||||||
export type modelEnumType = (typeof modelTypes)[number];
|
export type modelEnumType = (typeof modelTypes)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ const getOutputRoute = createRoute({
|
|||||||
input_image: "https://somestatic.png",
|
input_image: "https://somestatic.png",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
run_log: (schema) =>
|
||||||
|
schema.run_log.openapi({
|
||||||
|
type: "object",
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
logs: "some logs",
|
||||||
|
timestamp: 1706631877.3831277,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -81,7 +91,7 @@ export const registerGetOutputRoute = (app: App) => {
|
|||||||
code: 400,
|
code: 400,
|
||||||
message: "Workflow not found",
|
message: "Workflow not found",
|
||||||
},
|
},
|
||||||
400
|
400,
|
||||||
);
|
);
|
||||||
|
|
||||||
return c.json(run, 200);
|
return c.json(run, 200);
|
||||||
@@ -94,7 +104,7 @@ export const registerGetOutputRoute = (app: App) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,26 +16,14 @@ export const insertCustomMachineSchema = createInsertSchema(machinesTable, {
|
|||||||
schema.snapshot.default({
|
schema.snapshot.default({
|
||||||
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
||||||
git_custom_nodes: {
|
git_custom_nodes: {
|
||||||
"https://github.com/BennyKok/comfyui-deploy.git": {
|
"https://github.com/bennykok/comfyui-deploy": {
|
||||||
hash: "a838cb7ad425e5652c3931fbafdc886b53c48a22",
|
hash: "df46e3a0e5ad93fa71f5d216997e376af33b2a6d",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
file_custom_nodes: [],
|
file_custom_nodes: [],
|
||||||
}),
|
}),
|
||||||
models: (schema) =>
|
models: (schema) => schema.models.default([]),
|
||||||
schema.models.default([
|
|
||||||
{
|
|
||||||
name: "v1-5-pruned-emaonly.ckpt",
|
|
||||||
type: "checkpoints",
|
|
||||||
base: "SD1.5",
|
|
||||||
save_path: "default",
|
|
||||||
description: "Stable Diffusion 1.5 base model",
|
|
||||||
reference: "https://huggingface.co/runwayml/stable-diffusion-v1-5",
|
|
||||||
filename: "v1-5-pruned-emaonly.ckpt",
|
|
||||||
url: "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt",
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const addCustomMachineSchema = insertCustomMachineSchema.pick({
|
export const addCustomMachineSchema = insertCustomMachineSchema.pick({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { withServerPromise } from "./withServerPromise";
|
import { withServerPromise } from "./withServerPromise";
|
||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { downloadUrlModelSchema } from "./addCivitaiModelSchema";
|
import { downloadUrlModelSchema } from "./addCivitaiModelSchema";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
@@ -210,6 +211,47 @@ export const addModelDownloadUrl = withServerPromise(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const deleteModel = withServerPromise(
|
||||||
|
async (modelId: string) => {
|
||||||
|
const model = await db.query.modelTable.findFirst({
|
||||||
|
where: eq(modelTable.id, modelId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the model does not exist, throw an error or return a message
|
||||||
|
if (!model) {
|
||||||
|
throw new Error("Model not found");
|
||||||
|
// Or return { error: "Model not found" }; if you prefer to handle it without throwing
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumes = await retrieveModelVolumes();
|
||||||
|
if (
|
||||||
|
model.status === "success" && !!model.folder_path && !!model.model_name
|
||||||
|
) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${process.env.MODAL_BUILDER_URL!}/delete-volume-model`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
volume_name: volumes[0].volume_name,
|
||||||
|
path: model.folder_path,
|
||||||
|
file_name: model.model_name,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!result.ok) {
|
||||||
|
const error_log = await result.text();
|
||||||
|
throw new Error(`Error: ${result.statusText} ${error_log}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.delete(modelTable).where(eq(modelTable.id, modelId));
|
||||||
|
revalidatePath("/storage");
|
||||||
|
return { message: "Model Deleted" };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const getCivitaiModelRes = async (civitaiUrl: string) => {
|
export const getCivitaiModelRes = async (civitaiUrl: string) => {
|
||||||
const { url, modelVersionId } = getUrl(civitaiUrl);
|
const { url, modelVersionId } = getUrl(civitaiUrl);
|
||||||
const civitaiModelRes = await fetch(url)
|
const civitaiModelRes = await fetch(url)
|
||||||
@@ -301,8 +343,8 @@ export const addCivitaiModel = withServerPromise(
|
|||||||
model_name: selectedModelVersion.files[0].name,
|
model_name: selectedModelVersion.files[0].name,
|
||||||
civitai_id: civitaiModelRes.id.toString(),
|
civitai_id: civitaiModelRes.id.toString(),
|
||||||
civitai_version_id: selectedModelVersionId,
|
civitai_version_id: selectedModelVersionId,
|
||||||
civitai_url: data.url, // TODO: need to confirm
|
civitai_url: data.url,
|
||||||
civitai_download_url: selectedModelVersion.files[0].downloadUrl,
|
civitai_download_url: selectedModelVersion.files[0].downloadUrl, // there is an issue when a model hoster might put multiple different types of files i.e. their training data.
|
||||||
civitai_model_response: civitaiModelRes,
|
civitai_model_response: civitaiModelRes,
|
||||||
user_volume_id: volumes[0].id,
|
user_volume_id: volumes[0].id,
|
||||||
model_type,
|
model_type,
|
||||||
@@ -312,6 +354,7 @@ export const addCivitaiModel = withServerPromise(
|
|||||||
const b = a[0];
|
const b = a[0];
|
||||||
|
|
||||||
await uploadModel(data, b, volumes[0]);
|
await uploadModel(data, b, volumes[0]);
|
||||||
|
revalidatePath("/storage");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { RunDisplay } from "@/components/RunDisplay";
|
||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import { deploymentsTable, workflowRunsTable } from "@/db/schema";
|
import { deploymentsTable, workflowRunsTable } from "@/db/schema";
|
||||||
import { count, desc, eq, sql } from "drizzle-orm";
|
import { count, desc, eq, sql } from "drizzle-orm";
|
||||||
@@ -56,6 +57,15 @@ export async function findAllRuns({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllRunstableContent(props: RunsSearchTypes) {
|
||||||
|
const data = await findAllRunsWithCounts(props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: data?.allRuns.map((run) => <RunDisplay run={run} key={run.id} />),
|
||||||
|
total: data?.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function findAllRunsWithCounts(props: RunsSearchTypes) {
|
export async function findAllRunsWithCounts(props: RunsSearchTypes) {
|
||||||
const a = await db
|
const a = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import { and, desc, eq, isNull, or } from "drizzle-orm";
|
import { and, desc, eq, isNull, ne, or } from "drizzle-orm";
|
||||||
import { subscriptionStatusTable } from "@/db/schema";
|
import { subscriptionStatusTable } from "@/db/schema";
|
||||||
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||||
import { auth } from "@clerk/nextjs";
|
import { auth } from "@clerk/nextjs";
|
||||||
@@ -28,6 +28,7 @@ export async function getCurrentPlan({ user_id, org_id }: APIKeyUserType) {
|
|||||||
isNull(subscriptionStatusTable.org_id),
|
isNull(subscriptionStatusTable.org_id),
|
||||||
eq(subscriptionStatusTable.org_id, ""),
|
eq(subscriptionStatusTable.org_id, ""),
|
||||||
),
|
),
|
||||||
|
ne(subscriptionStatusTable.status, "deleted"),
|
||||||
),
|
),
|
||||||
orderBy: desc(subscriptionStatusTable.created_at),
|
orderBy: desc(subscriptionStatusTable.created_at),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,24 +49,26 @@ export async function getRunsData(run_id: string, user?: APIKeyUserType) {
|
|||||||
for (let i = 0; i < data.outputs.length; i++) {
|
for (let i = 0; i < data.outputs.length; i++) {
|
||||||
const output = data.outputs[i];
|
const output = data.outputs[i];
|
||||||
|
|
||||||
if (output.data?.images !== undefined) {
|
if (output.data?.images !== undefined)
|
||||||
for (let j = 0; j < output.data?.images.length; j++) {
|
replaceUrls(output.data?.images, data.id);
|
||||||
const element = output.data?.images[j];
|
|
||||||
element.url = replaceCDNUrl(
|
if (output.data?.files !== undefined)
|
||||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
replaceUrls(output.data?.files, data.id);
|
||||||
);
|
|
||||||
}
|
if (output.data?.gifs !== undefined)
|
||||||
} else if (output.data?.files !== undefined) {
|
replaceUrls(output.data?.gifs, data.id);
|
||||||
for (let j = 0; j < output.data?.files.length; j++) {
|
|
||||||
const element = output.data?.files[j];
|
|
||||||
element.url = replaceCDNUrl(
|
|
||||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceUrls(dataType: any[], dataId: string) {
|
||||||
|
for (let j = 0; j < dataType.length; j++) {
|
||||||
|
const element = dataType[j];
|
||||||
|
element.url = replaceCDNUrl(
|
||||||
|
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${dataId}/${element.filename}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { RunOutputs } from "@/components/RunOutputs";
|
|
||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import { workflowRunOutputs } from "@/db/schema";
|
import { workflowRunOutputs } from "@/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function getRunsOutputDisplay(run_id: string) {
|
|
||||||
return <RunOutputs run_id={run_id} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRunsOutput(run_id: string) {
|
export async function getRunsOutput(run_id: string) {
|
||||||
// throw new Error("Not implemented");
|
|
||||||
return await db
|
return await db
|
||||||
.select()
|
.select()
|
||||||
.from(workflowRunOutputs)
|
.from(workflowRunOutputs)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import type { StringLiteralUnion } from "shikiji";
|
||||||
|
import { getHighlighter } from "shikiji";
|
||||||
|
|
||||||
|
export async function highlight(
|
||||||
|
code: string,
|
||||||
|
lang: StringLiteralUnion<string>,
|
||||||
|
) {
|
||||||
|
const highlighter = await getHighlighter({
|
||||||
|
themes: ["one-dark-pro"],
|
||||||
|
langs: [lang],
|
||||||
|
});
|
||||||
|
return highlighter.codeToHtml(code.trim(), {
|
||||||
|
lang: lang,
|
||||||
|
theme: "one-dark-pro",
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user