feat: conflict dist/copilot_web

This commit is contained in:
ai-business-hql
2025-07-09 14:56:53 +08:00
32 changed files with 1805 additions and 1088 deletions
+2 -2
View File
@@ -166,7 +166,7 @@ We welcome any form of contribution! Feel free to make issues, pull requests, or
For any queries or suggestions, please feel free to contact: ComfyUI-Copilot@service.alibaba.com.
<div align="center">
<img src="assets/qrcode.jpg" width="20%"/>
<img src="assets/qrcode.png" width="20%"/>
WeChat
@@ -174,7 +174,7 @@ For any queries or suggestions, please feel free to contact: ComfyUI-Copilot@ser
Discord
</div>
---
## 📚 License
+1 -1
View File
@@ -158,7 +158,7 @@ https://github.com/user-attachments/assets/0372faf4-eb64-4aad-82e6-5fd69f349c2c
微信服务群:
<div align="center">
<img src='https://github.com/AIDC-AI/ComfyUI-Copilot/blob/main/assets/qrcode.jpg' width='300'>
<img src='https://github.com/AIDC-AI/ComfyUI-Copilot/blob/main/assets/qrcode.png' width='300'>
</div>
## 📚 许可证
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 181 KiB

-14
View File
@@ -1,14 +0,0 @@
function getImportPath(filename) {
return `./${filename}`;
}
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-D1kzpNKI.js","copilot_web/input.js","copilot_web/vendor-markdown-Mq66gLHN.js","copilot_web/vendor-react-6F4zURhM.js","copilot_web/message-components-Dfta1k8v.js","copilot_web/assets/input-l5zbI7RM.css","copilot_web/fonts.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
if (apiBase) {
// 有 API base 时,使用完整路径
return `${apiBase.substring(1)}/${path}`;
} else {
// 没有 API base 时,使用相对路径
return `./${path}`;
}
}))))=>i.map(i=>d[i]);
import{_ as a}from"./input.js";import{j as t}from"./vendor-markdown-Mq66gLHN.js";import{r,R as i}from"./vendor-react-6F4zURhM.js";import{C as n}from"./message-components-Dfta1k8v.js";/* empty css */const o={EXPLAIN_NODE:"copilot:explain-node"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-D1kzpNKI.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function p(){const[e,s]=r.useState(!1);return r.useEffect(()=>{const l=()=>{s(!0)};return window.addEventListener(o.EXPLAIN_NODE,l),()=>window.removeEventListener(o.EXPLAIN_NODE,l)},[]),t.jsx(n,{children:t.jsx("div",{className:"h-full w-full flex flex-col",children:t.jsx(r.Suspense,{fallback:t.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:t.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>s(!1)})})})})}export{p as default};
File diff suppressed because one or more lines are too long
-4
View File
@@ -1,4 +0,0 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as s}from"./vendor-markdown-Mq66gLHN.js";import{W as x,g as j,a as d}from"./message-components-Dfta1k8v.js";import{r as P}from"./vendor-react-6F4zURhM.js";function G({content:C,name:D="Assistant",avatar:H,latestInput:N,installedNodes:z,onAddMessage:y}){const _=JSON.parse(C),v=_.ext?.find(e=>e.type==="workflow")?.data||[],[p,g]=P.useState({}),W=async e=>{if(!e.id){console.error("No workflow id provided");return}const i=String(e.id);if(!p[i]){g(t=>({...t,[i]:!0})),x.trackEvent({event_type:"workflow_accept",message_type:"workflow",message_id:_.message_id,data:{workflow_id:e.id,workflow_name:e.name}});try{const t=await x.getOptimizedWorkflow(e.id,N);if(t.workflow){const r=new Set;if(t.workflow.nodes)for(const o of t.workflow.nodes)r.add(o.type);else for(const o of Object.values(t.workflow))r.add(o.class_type);const a=Array.from(r).filter(o=>!z.includes(o));if(console.log("[WorkflowOption] Missing node types:",a),a.length>0){try{console.log("[WorkflowOption] Fetching info for missing nodes");const o=await x.batchGetNodeInfo(a);console.log("[WorkflowOption] Received node infos:",o);const m={text:"",ext:[{type:"node_install_guide",data:o.map(l=>({name:l.name,repository_url:l.github_url}))}]},c={id:j(),role:"ai",content:JSON.stringify(m),format:"markdown",name:"Assistant",metadata:{pendingWorkflow:t.workflow,optimizedParams:t.optimized_params}};y?.(c)}catch(o){console.error("[WorkflowOption] Error fetching node info:",o),alert("Error checking required nodes. Please try again.")}finally{g(o=>({...o,[i]:!1}))}return}O(t.workflow,t.optimized_params)}}catch(t){console.error("Failed to optimize workflow:",t),alert("Failed to optimize workflow. Please try again.")}finally{g(t=>({...t,[i]:!1}))}}},O=(e,i)=>{if(e.nodes)d.loadGraphData(e);else{d.loadApiJson(e);const r=Object.keys(e),a=Object.keys(d.graph._nodes_by_id)[0],o=d.graph._nodes_by_id[a],m=o?o.pos[0]:0,c=o?o.pos[1]:0,l=250,f=60,A=20,E=60,b=50,I=1e3;let k=m,h=c,u=0;for(const F of r){const n=d.graph._nodes_by_id[F];if(n){u>I&&(k+=l+E,u=0,h=c);const M=n.inputs?n.inputs.length:0,S=n.outputs?n.outputs.length:0,J=n.widgets?n.widgets.length:0,L=Math.max(M,S)+J,w=A*L+f;n.size[0]=l,n.size[1]=w,n.pos[0]=k,n.pos[1]=h,u+=w+b,h+=w+b}}}for(const[r,a,o,m,c]of i){const l=d.graph._nodes_by_id[r].widgets;for(const f of l)f.name===m&&(f.value=c)}d.graph.setDirtyCanvas(!1,!0);const t={id:j(),role:"tool",content:JSON.stringify({text:"The workflow has been successfully loaded to the canvas",ext:[]}),format:"markdown",name:"Assistant"};y?.(t)};return s.jsx("div",{className:"space-y-3",children:v.length>0&&s.jsx("div",{className:"flex flex-col space-y-4",children:v.map((e,i)=>{const t=e.id?String(e.id):"";return s.jsxs("div",{className:"flex items-center gap-4 p-4 rounded-lg border border-gray-200 hover:bg-gray-50",children:[e.image&&s.jsx("img",{src:e.image,alt:e.name,className:"w-14 h-14 object-cover rounded-lg",onError:r=>{const a=r.target;a.onerror=null,a.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E"}}),s.jsxs("div",{className:"flex-1 break-words flex flex-col h-[4.5rem] justify-between",children:[s.jsx("div",{children:s.jsx("h3",{className:"font-medium text-sm line-clamp-2 h-10 overflow-hidden",children:e.name})}),s.jsxs("div",{className:"flex justify-between items-center mt-1",children:[e.description&&s.jsxs("div",{className:"relative group",children:[s.jsx("div",{className:"w-5 h-5 flex items-center justify-center text-gray-500 cursor-help",children:s.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:"w-4 h-4",children:s.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"})})}),s.jsx("div",{className:"absolute bottom-full left-0 mb-2 hidden group-hover:block",children:s.jsx("div",{className:"bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words",children:e.description})})]}),s.jsx("button",{onClick:()=>W(e),disabled:p[t],className:`px-3 py-1.5 ${p[t]?"bg-gray-400 cursor-not-allowed":"bg-blue-500 hover:bg-blue-600"} text-white rounded-md transition-colors text-xs`,children:p[t]?"Loading...":"Accept"})]})]})]},i)})})})}export{G as WorkflowOption};
-14
View File
@@ -1,14 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="cc-icon-logo" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="cc-icon-logo" horiz-adv-x="1673"
d="M953.240 271.522c7.894-6.874 12.965-16.634 19.585-24.709 11.100-13.542 23.393-25.939 36.892-37.094 56.683-46.839 134.714-63.303 205.904-46.280 36.563 8.744 71.347 27.153 98.834 52.889 6.503 6.089 26.996 19.767 21.187 30.485-9.427 17.396-31.912 31.962-45.898 45.893-4.414 4.397-10.841 13.638-17.632 14.025-8.042 0.458-18.046-13.623-23.549-18.443-14.098-12.353-30.474-20.840-48.239-26.551-86.646-27.857-171.451 32.122-189.787 118.031-19.441 91.078 54.091 198.993 153.313 189.040 24.603-2.468 46.791-9.195 68.243-21.640 8.668-5.028 22.977-21.652 32.930-21.475 6.976 0.124 13.251 9.647 17.670 14.093l36.477 36.459c6.590 6.636 14.250 12.156 6.741 21.193-15.330 18.450-37.542 32.228-58.519 43.305-118.109 62.355-262.374 13.472-323.782-103.311-35.469-67.452-25.685-150.041-68.487-214.140-30.167-45.177-80.951-74.275-136.022-70.413-19.842 1.391-39.756 6.076-57.653 14.847-104.915 51.407-113.944 202.931-23.532 272.699 38.111 29.409 90.649 36.425 135.309 18.880 11.810-4.640 22.625-10.821 32.945-18.183 4.417-3.151 10.625-9.812 16.473-9.484 6.426 0.363 12.309 8.692 16.473 12.855 11.977 11.973 23.349 24.496 35.298 36.475 4.345 4.355 14.559 11.600 12.080 18.825-3.723 10.854-20.787 20.481-29.729 26.835-33.085 23.507-73.643 37.831-114.130 40.263-129.389 7.774-246.857-87.826-260.937-218.877-11.557-107.563 36.224-215.138 135.041-266.113 84.024-43.342 187.873-30.941 261.204 27.158 19.671 15.585 46.151 38.408 55.300 62.466z" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

-1
View File
File diff suppressed because one or more lines are too long
-38
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-4
View File
@@ -1,4 +0,0 @@
function getImportPath(filename) {
return `./${filename}`;
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-Copilot"
description = "Your Intelligent Assistant for Comfy-UI."
version = "1.3.5"
version = "1.3.6"
license = {file = "LICENSE"}
[project.urls]
+1186 -2
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -14,7 +14,10 @@
"build:prod": "npx tailwindcss -i ./src/scoped-tailwind.css -o ./src/output.css && VITE_API_BASE_URL=https://comfyui-copilot-server.onrender.com vite build && node scripts/post-build.js"
},
"dependencies": {
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@heroicons/react": "^2.2.0",
"@tanstack/react-virtual": "^3.13.12",
"ahooks": "^3.9.0",
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7",
"fuse.js": "^7.0.0",
+127 -12
View File
@@ -9,7 +9,7 @@ import { LoadingMessage } from "./messages/LoadingMessage";
import { generateUUID } from "../../utils/uuid";
import { app } from "../../utils/comfyapp";
import { addNodeOnGraph } from "../../utils/graphUtils";
import React, { lazy, Suspense } from "react";
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState } from "react";
// Define types for ext items to avoid implicit any
interface ExtItem {
@@ -27,6 +27,16 @@ interface NodeWithPosition {
pos: [number, number];
}
const enum LoadMoreStatus {
USED = 'used',
NOT_USED = 'not_used'
}
interface LoadMoreButtonProps {
id?: string
buttonType?: 'loadmore';
buttonStatus?: LoadMoreStatus
}
interface MessageListProps {
messages: Message[];
onOptionClick: (option: string) => void;
@@ -40,15 +50,28 @@ const getAvatar = (name?: string) => {
return `https://ui-avatars.com/api/?name=${name || 'User'}&background=random`;
};
type FinallyMessageProps = Message | LoadMoreButtonProps
// Use lazy loading for components that are conditionally rendered
const LazyWorkflowOption = lazy(() => import('./messages/WorkflowOption').then(m => ({ default: m.WorkflowOption })));
const LazyNodeSearch = lazy(() => import('./messages/NodeSearch').then(m => ({ default: m.NodeSearch })));
const LazyDownstreamSubgraphs = lazy(() => import('./messages/DownstreamSubgraphs').then(m => ({ default: m.DownstreamSubgraphs })));
const LazyNodeInstallGuide = lazy(() => import('./messages/NodeInstallGuide').then(m => ({ default: m.NodeInstallGuide })));
// 默认显示3轮回答,也就是找到列表最后的3条role是ai的数据
const DEFAULT_COUNT = 3;
export function MessageList({ messages, latestInput, onOptionClick, installedNodes, onAddMessage, loading }: MessageListProps) {
// 使用useMemo缓存renderMessage函数
const renderMessage = React.useMemo(() => (message: Message) => {
const [currentIndex, setCurrentIndex] = useState<number>(0)
const [currentMessages, setCurrentMessages] = useState<FinallyMessageProps[]>([])
const parentRef = useRef<HTMLDivElement>(null)
const lastMessagesCount = useRef<number>(0)
const currentScrollHeight = useRef<number>(0)
const isLoadHistory = useRef<boolean>(false)
// 渲染对应的消息组件
const renderMessage = (message: Message) => {
// 移除频繁的日志输出
// console.log('[MessageList] Rendering message:', message);
@@ -199,7 +222,7 @@ export function MessageList({ messages, latestInput, onOptionClick, installedNod
} else if (message.metadata?.pendingWorkflow) {
const workflow = message.metadata.pendingWorkflow;
const optimizedParams = message.metadata.optimizedParams;
// 支持不同格式的工作流
if(workflow.nodes) {
app.loadGraphData(workflow);
@@ -337,17 +360,109 @@ export function MessageList({ messages, latestInput, onOptionClick, installedNod
}
return null;
}, [installedNodes, onOptionClick, onAddMessage, latestInput]); // Added dependencies that are used inside the function
}
// 使用useMemo缓存消息列表
const messageElements = React.useMemo(() =>
messages?.map(renderMessage),
[messages, renderMessage]
);
const renderLoadMore = (message: LoadMoreButtonProps) => {
const isNotUsed = message?.buttonStatus === LoadMoreStatus.NOT_USED
return <div className='flex justify-center items-center'>
{
isNotUsed ? <button
className='w-full h-[24px] text-gray-700 text-xs bg-gray-50 hover:!bg-gray-200 px-2 py-1 rounded-md'
onClick={handleLoadMore}
>
Load more
</button> : <div className='w-full h-[24px] bg-transparent' />
}
</div>
}
function isLoadMoreButtonProps(item: any): item is LoadMoreButtonProps {
return item && typeof item === 'object' && item.buttonType === 'loadmore';
}
const handleLoadMore = () => {
isLoadHistory.current = true;
setCurrentIndex(index => index + 1);
}
useEffect(() => {
let list: FinallyMessageProps[] = []
if (lastMessagesCount.current > 0 && lastMessagesCount.current < messages.length) {
list = messages.slice(lastMessagesCount.current)
if (messages[lastMessagesCount.current].role === 'user') {
list.unshift({
id: `${messages[lastMessagesCount.current].id}_loadmore`,
buttonType: 'loadmore',
buttonStatus: LoadMoreStatus.USED
})
}
setCurrentMessages(pre => [...pre, ...list]);
} else {
let count = 0;
let endIndex = messages?.length
for (let i = messages?.length - 1; i >= 0; i--) {
if (messages[i].role === 'ai') {
if (count > DEFAULT_COUNT + currentIndex) {
list.unshift({
id: `${messages[i].id}_loadmore`,
buttonType: 'loadmore',
buttonStatus: LoadMoreStatus.NOT_USED
})
break;
} else {
count++;
if (endIndex < messages.length) {
if (list?.length > 0) {
list.unshift({
id: `${messages[i].id}_loadmore`,
buttonType: 'loadmore',
buttonStatus: LoadMoreStatus.USED
})
}
list = [
...messages?.slice(Math.min(endIndex, i + 1), endIndex + 1),
...list
];
}
endIndex = i;
}
}
}
setCurrentMessages(list)
}
lastMessagesCount.current = messages.length
}, [messages, currentIndex])
useLayoutEffect(() => {
if (parentRef?.current) {
if (isLoadHistory.current) {
// 加载历史数据需要修改scrolltop保证当前视图不变
parentRef.current.scrollTop = parentRef.current.scrollHeight - currentScrollHeight.current
isLoadHistory.current = false
} else {
parentRef.current.scrollTop = parentRef.current.scrollHeight
}
currentScrollHeight.current = parentRef.current.scrollHeight
}
}, [currentMessages])
return (
<div className="flex flex-col gap-4 w-full">
{messageElements}
<div
className='overflow-y-auto w-full h-full'
ref={parentRef}
style={{
contain: "strict"
}}
>
{
currentMessages?.map((message) => <div
key={message.id}
>
{
!!message && (isLoadMoreButtonProps(message) ? renderLoadMore(message as LoadMoreButtonProps) : renderMessage(message as Message))
}
</div>)
}
{loading && <LoadingMessage />}
</div>
);
@@ -3,6 +3,7 @@
// Copyright (C) 2025 ComfyUI-Copilot Authors
// Licensed under the MIT License.
import React from 'react'
import { MemoizedReactMarkdown } from "../../markdown";
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
@@ -26,16 +27,16 @@ interface AIMessageProps {
// Card component for node explanation intent
const NodeExplainCard = ({ content }: { content: React.ReactNode }) => {
return (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-1 shadow-sm">
<div className="rounded-lg shadow-sm">
<div className="flex items-center mb-1">
<div className="bg-blue-100 rounded-full p-1 mr-1">
<svg className="h-4 w-4 text-blue-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="rounded-full mr-1">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-sm font-medium text-blue-800">Node Usage Guide</h3>
<h3 className="text-base font-bold">Node Usage Guide</h3>
</div>
<div className="markdown-content text-blue-900">
<div className="markdown-content">
{content}
</div>
</div>
@@ -63,7 +64,7 @@ const NodeParamsCard = ({ content }: { content: React.ReactNode }) => {
};
export function AIMessage({ content, name = 'Assistant', avatar, format, onOptionClick, extComponent, metadata }: AIMessageProps) {
const markdownWrapper = useRef<HTMLDivElement | null>()
const markdownWrapper = useRef<HTMLDivElement | null>(null)
// Renders markdown content with customized styles and components
const renderMarkdown = (text: string, specialClass?: string) => {
@@ -9,7 +9,7 @@ import rehypeKatex from 'rehype-katex';
import rehypeExternalLinks from 'rehype-external-links';
import { generateUUID } from "../../../utils/uuid";
import { useState } from "react";
import { Tooltip } from 'antd';
interface WorkflowOptionProps {
content: string;
name?: string;
@@ -227,61 +227,114 @@ export function WorkflowOption({ content, name = 'Assistant', avatar, latestInpu
onAddMessage?.(successMessage);
};
return (
// <BaseMessage avatar={avatar} name={name}>
<div className="space-y-3">
{workflows.length > 0 && (
<div className="flex flex-col space-y-4">
{workflows.map((workflow: Workflow, index: number) => {
// 确保workflow.id存在并转换为字符串
const workflowId = workflow.id ? String(workflow.id) : '';
return (
<div key={index} className="flex items-center gap-4 p-4 rounded-lg border border-gray-200 hover:bg-gray-50">
{workflow.image && (
<img
src={workflow.image}
alt={workflow.name}
className="w-14 h-14 object-cover rounded-lg"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.onerror = null; // 防止循环触发
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E";
}}
/>
)}
<div className="flex-1 break-words flex flex-col h-[4.5rem] justify-between">
<div>
<h3 className="font-medium text-sm line-clamp-2 h-10 overflow-hidden">{workflow.name}</h3>
</div>
<div className="flex justify-between items-center mt-1">
{workflow.description && (
<div className="relative group">
<div className="w-5 h-5 flex items-center justify-center text-gray-500 cursor-help">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
</div>
<div className="absolute bottom-full left-0 mb-2 hidden group-hover:block">
<div className="bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words">
{workflow.description}
</div>
</div>
</div>
)}
<button
onClick={() => handleAcceptWorkflow(workflow)}
disabled={loadingWorkflows[workflowId]}
className={`px-3 py-1.5 ${loadingWorkflows[workflowId]
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-600'} text-white rounded-md transition-colors text-xs`}
>
{loadingWorkflows[workflowId] ? 'Loading...' : 'Accept'}
</button>
</div>
const loadWorkflowItem = (workflow: Workflow, index: number) => {
const workflowId = workflow.id ? String(workflow.id) : '';
return (
<div key={index} className="flex items-center gap-4 p-4 rounded-lg border border-gray-200 hover:bg-gray-50">
{workflow.image && (
<img
src={workflow.image}
alt={workflow.name}
className="w-14 h-14 object-cover rounded-lg"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.onerror = null; // 防止循环触发
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E";
}}
/>
)}
<div className="flex-1 break-words flex flex-col h-[4.5rem] justify-between">
<div>
<h3 className="font-medium text-sm line-clamp-2 h-10 overflow-hidden">{workflow.name}</h3>
</div>
<div className="flex justify-between items-center mt-1">
{workflow.description && (
<div className="relative group">
<div className="w-5 h-5 flex items-center justify-center text-gray-500 cursor-help">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
</div>
<div className="absolute bottom-full left-0 mb-2 hidden group-hover:!block">
<div className="bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words">
{workflow.description}
</div>
</div>
);
})}
</div>
)}
<button
onClick={() => handleAcceptWorkflow(workflow)}
disabled={loadingWorkflows[workflowId]}
className={`px-3 py-1.5 ${loadingWorkflows[workflowId]
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-500 hover:bg-blue-600'} text-white rounded-md transition-colors text-xs`}
>
{loadingWorkflows[workflowId] ? 'Loading...' : 'Accept'}
</button>
</div>
</div>
</div>
);
}
const loadWorkflowItemV2 = (workflow: Workflow, index: number) => {
const workflowId = workflow.id ? String(workflow.id) : '';
return (
<div
key={index}
className="flex flex-col items-center gap-4 p-2 rounded-lg border border-gray-200 hover:bg-gray-50"
>
<div className='flex flex-row'>
<h3 className="flex-1 font-medium text-sm line-clamp-2 h-10 overflow-hidden">{workflow.name}</h3>
<div className='flex items-start'>
{workflow.description && <Tooltip
arrow={false}
placement="top"
title={<div className="bg-gray-900 text-white text-xs rounded-md py-2 px-3 min-w-[400px] whitespace-normal break-words">
{workflow.description}
</div>}
>
<div className="w-5 h-5 flex items-center justify-center text-gray-500 cursor-help">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-4 h-4">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
</div>
</Tooltip>}
</div>
</div>
{workflow.image && (
<div className='aspect-[3/4] rounded-lg overflow-hidden'>
<img
src={workflow.image}
alt={workflow.name}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.onerror = null; // 防止循环触发
target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56' viewBox='0 0 56 56' fill='none'%3E%3Crect width='56' height='56' fill='%23F3F4F6'/%3E%3Cpath d='M28 28C30.2091 28 32 26.2091 32 24C32 21.7909 30.2091 20 28 20C25.7909 20 24 21.7909 24 24C24 26.2091 25.7909 28 28 28Z' fill='%239CA3AF'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.7253 37.6307C19.8278 35.1533 22.6897 33.6 26 33.6H30C33.3103 33.6 36.1722 35.1533 37.2747 37.6307C37.6419 38.4561 37.0611 39.2 36.1694 39.2H19.8306C18.9389 39.2 18.3581 38.4561 18.7253 37.6307Z' fill='%239CA3AF'/%3E%3C/svg%3E";
}}
/>
</div>
)}
<button
onClick={() => handleAcceptWorkflow(workflow)}
disabled={loadingWorkflows[workflowId]}
className={`w-full px-3 py-1 ${loadingWorkflows[workflowId]
? 'bg-[#d2d2d2] cursor-not-allowed'
: 'bg-[#ffffff] hover:bg-[#eee]'} text-black rounded-lg text-xs`}
>
{loadingWorkflows[workflowId] ? 'Loading...' : 'Accept'}
</button>
</div>
);
}
return (
// <BaseMessage avatar={avatar} name={name}>
<div className="space-y-3 flex justify-center">
{workflows.length > 0 && (
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
{workflows.map((workflow: Workflow, index: number) => loadWorkflowItemV2(workflow, index))}
</div>
)}
</div>
@@ -49,7 +49,7 @@ export const ParameterDebugInterface: React.FC<ParameterDebugInterfaceProps> = (
}
};
}, [props.visible, dispatch]);
// The original component handles most of the actual work
return <OriginalParameterDebugInterface {...props} />;
};
@@ -5,6 +5,7 @@ import React from 'react';
import { ImageModal } from '../modals/ImageModal';
import { GeneratedImage } from '../types/parameterDebugTypes';
import { HistoryItem, formatDate, formatNodeNameWithParams } from '../utils/historyUtils';
import VirtualGrid from '../../virtualGrid';
interface HistoryItemScreenProps {
historyItem: HistoryItem;
@@ -26,9 +27,6 @@ export const HistoryItemScreen: React.FC<HistoryItemScreenProps> = ({
selectedImageIndex,
handleSelectImage,
handleClose,
currentPage,
handlePageChange,
imagesPerPage,
modalVisible,
modalImageUrl,
modalImageParams,
@@ -37,19 +35,47 @@ export const HistoryItemScreen: React.FC<HistoryItemScreenProps> = ({
}) => {
const { timestamp, nodeName, generatedImages, params } = historyItem;
// Calculate total pages
const actualTotalPages = Math.ceil(generatedImages.length / imagesPerPage);
// Calculate current page's image index range
const startIndex = (currentPage - 1) * imagesPerPage;
const endIndex = Math.min(startIndex + imagesPerPage, generatedImages.length);
const currentImages = generatedImages.slice(startIndex, endIndex);
const renderItem = (image: GeneratedImage, index: number) => (
<div
key={index.toString()}
className={`relative flex flex-col rounded-lg overflow-hidden border ${selectedImageIndex === index ? 'border-pink-500 ring-2 ring-pink-300' : 'border-gray-200'}`}
onClick={(e) => handleSelectImage(index, e)}
>
<button
className="absolute top-2 right-2 bg-white bg-opacity-75 rounded-full p-0.5 text-gray-600 hover:text-gray-900 shadow-sm z-10"
onClick={(e) => openImageModal(image.url, image.params, e)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
</button>
<div className="aspect-square">
<img
src={image.url}
alt={`Generated image ${index+1}`}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2 text-xs text-gray-600 bg-white max-h-16 overflow-y-auto">
{Object.entries(image.params)
// Filter out nodeParams and other complex objects from display
.filter(([paramName, value]) =>
paramName !== 'nodeParams' &&
typeof value !== 'object'
)
.map(([paramName, value]) => (
<div key={paramName}>{paramName}: {String(value)}</div>
))
}
</div>
</div>
);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="h-full flex flex-col relative bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-start">
<div>
<h3 className="text-base font-medium text-gray-800">{formatNodeNameWithParams(nodeName, params)}</h3>
<div className='min-w-0'>
<h3 className="text-base font-medium text-gray-800 truncate hover:!break-words hover:!whitespace-normal hover:!overflow-visible">{formatNodeNameWithParams(nodeName, params)}</h3>
<p className="text-xs text-gray-500">{formatDate(timestamp)} • {generatedImages.length} images</p>
</div>
<button
@@ -62,6 +88,13 @@ export const HistoryItemScreen: React.FC<HistoryItemScreenProps> = ({
</button>
</div>
<div className='flex-1 min-h-0'>
<VirtualGrid
items={generatedImages}
renderItem={renderItem}
/>
</div>
{/* Image Modal */}
<ImageModal
visible={modalVisible}
@@ -73,85 +106,6 @@ export const HistoryItemScreen: React.FC<HistoryItemScreenProps> = ({
} : undefined}
onClose={closeImageModal}
/>
<div className="grid grid-cols-3 gap-4 mb-6">
{currentImages.map((image, indexOnPage) => {
const actualIndex = startIndex + indexOnPage;
return (
<div
key={actualIndex}
className={`relative rounded-lg overflow-hidden border ${selectedImageIndex === actualIndex ? 'border-pink-500 ring-2 ring-pink-300' : 'border-gray-200'}`}
onClick={(e) => handleSelectImage(actualIndex, e)}
>
{/* Zoom icon */}
<button
className="absolute top-2 right-2 bg-white bg-opacity-75 rounded-full p-0.5 text-gray-600 hover:text-gray-900 shadow-sm z-10"
onClick={(e) => openImageModal(image.url, image.params, e)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
</button>
<div className="aspect-square">
<img
src={image.url}
alt={`Generated image ${actualIndex+1}`}
className="w-full h-full object-cover"
/>
</div>
</div>
);
})}
</div>
{/* Pagination controls */}
{actualTotalPages > 1 && (
<div className="flex justify-center items-center space-x-2 mt-4 mb-4">
<button
onClick={(e) => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M8.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L4.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<div className="px-2 py-1 text-xs text-gray-800">
Page {currentPage} of {actualTotalPages}
</div>
<button
onClick={(e) => handlePageChange(currentPage + 1, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(actualTotalPages, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L8.586 10 4.293 14.293a1 1 0 000 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M11.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L15.586 10l-4.293 4.293a1 1 0 000 1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
)}
</div>
);
};
@@ -2,14 +2,15 @@
// Licensed under the MIT License.
import React from 'react';
import { app } from '../../../utils/comfyapp';
import { ImageModal } from '../modals/ImageModal';
import VirtualGrid from '../../virtualGrid';
interface GeneratedImage {
url: string;
params: { [key: string]: any };
index?: number;
}
interface ResultGalleryScreenProps {
generatedImages: GeneratedImage[];
selectedImageIndex: number | null;
@@ -33,11 +34,7 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
selectedImageIndex,
handleSelectImage,
handleApplySelected,
handlePrevious,
handleClose,
currentPage,
handlePageChange,
imagesPerPage,
notificationVisible,
modalVisible,
modalImageUrl,
@@ -45,16 +42,45 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
openImageModal,
closeImageModal
}) => {
// Calculate total pages
const actualTotalPages = Math.ceil(generatedImages.length / imagesPerPage);
// Calculate current page's image index range
const startIndex = (currentPage - 1) * imagesPerPage;
const endIndex = Math.min(startIndex + imagesPerPage, generatedImages.length);
const currentImages = generatedImages.slice(startIndex, endIndex);
const renderItem = (image: GeneratedImage, index: number) => (
<div
key={index.toString()}
className={`relative flex flex-col rounded-lg overflow-hidden border ${selectedImageIndex === image.index ? 'border-pink-500 ring-2 ring-pink-300' : 'border-gray-200'}`}
onClick={(e) => handleSelectImage(index, e)}
>
{/* Zoom icon */}
<button
className="absolute top-2 right-2 bg-white bg-opacity-75 rounded-full p-0.5 text-gray-600 hover:text-gray-900 shadow-sm z-10"
onClick={(e) => openImageModal(image.url, image.params, e)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
</button>
<div className={`aspect-square`}>
<img
src={image.url}
alt={`Generated image ${index+1}`}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2 text-xs text-gray-600 bg-white max-h-16 overflow-y-auto">
{Object.entries(image.params)
// Filter out nodeParams and other complex objects from display
.filter(([paramName, value]) =>
paramName !== 'nodeParams' &&
typeof value !== 'object'
)
.map(([paramName, value]) => (
<div key={paramName}>{paramName}: {String(value)}</div>
))
}
</div>
</div>
);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="h-full flex flex-col relative bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<div className="mb-4 border-b pb-2 flex justify-between items-center">
<div>
<h3 className="text-base font-medium text-gray-800">Generation Complete</h3>
@@ -83,106 +109,14 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
</div>
</div>
)}
{/* Image Modal */}
<ImageModal
visible={modalVisible}
imageUrl={modalImageUrl}
params={modalImageParams || {}}
onClose={closeImageModal}
/>
<div className="grid grid-cols-3 gap-4 mb-6">
{currentImages.map((image, indexOnPage) => {
const actualIndex = startIndex + indexOnPage;
return (
<div
key={actualIndex}
className={`relative rounded-lg overflow-hidden border ${selectedImageIndex === actualIndex ? 'border-pink-500 ring-2 ring-pink-300' : 'border-gray-200'}`}
onClick={(e) => handleSelectImage(actualIndex, e)}
>
{/* Zoom icon */}
<button
className="absolute top-2 right-2 bg-white bg-opacity-75 rounded-full p-0.5 text-gray-600 hover:text-gray-900 shadow-sm z-10"
onClick={(e) => openImageModal(image.url, image.params, e)}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
</svg>
</button>
<div className="aspect-square">
<img
src={image.url}
alt={`Generated image ${actualIndex+1}`}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2 text-xs text-gray-600 bg-white max-h-16 overflow-y-auto">
{Object.entries(image.params)
// Filter out nodeParams and other complex objects from display
.filter(([paramName, value]) =>
paramName !== 'nodeParams' &&
typeof value !== 'object'
)
.map(([paramName, value]) => (
<div key={paramName}>{paramName}: {String(value)}</div>
))
}
</div>
</div>
);
})}
<div className='flex-1 min-h-0'>
<VirtualGrid
items={generatedImages}
renderItem={renderItem}
/>
</div>
{/* Pagination controls */}
{actualTotalPages > 1 && (
<div className="flex justify-center items-center space-x-2 mt-4 mb-4">
<button
onClick={(e) => handlePageChange(1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M8.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L4.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(currentPage - 1, e)}
disabled={currentPage === 1}
className={`px-2 py-1 rounded ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</button>
<div className="px-2 py-1 text-xs text-gray-800">
Page {currentPage} of {actualTotalPages}
</div>
<button
onClick={(e) => handlePageChange(currentPage + 1, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
</button>
<button
onClick={(e) => handlePageChange(actualTotalPages, e)}
disabled={currentPage === actualTotalPages}
className={`px-2 py-1 rounded ${currentPage === actualTotalPages ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-100'}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L8.586 10 4.293 14.293a1 1 0 000 1.414z" clipRule="evenodd" />
<path fillRule="evenodd" d="M11.293 15.707a1 1 0 001.414 0l5-5a1 1 0 000-1.414l-5-5a1 1 0 00-1.414 1.414L15.586 10l-4.293 4.293a1 1 0 000 1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
)}
<div className="mt-6 flex justify-end">
<button
onClick={(e) => handleApplySelected(e)}
@@ -192,6 +126,14 @@ export const ResultGalleryScreen: React.FC<ResultGalleryScreenProps> = ({
Apply Selected
</button>
</div>
{/* Image Modal */}
<ImageModal
visible={modalVisible}
imageUrl={modalImageUrl}
params={modalImageParams || {}}
onClose={closeImageModal}
/>
</div>
);
};
+96
View File
@@ -0,0 +1,96 @@
import React, { useRef, useState, useLayoutEffect } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { debounce } from "lodash";
interface IProps {
items: any[]
renderItem: (item: any, index: number) => React.ReactNode;
minWidth?: number
gap?: number
}
const VirtualGrid: React.FC<IProps> = (props) => {
const {
items,
renderItem,
minWidth = 200,
gap = 16
} = props;
const parentRef = useRef<any>(null);
// 计算当前容器能放几列
const [columns, setColumns] = useState<number>(1);
const [rowCount, setRowCount] = useState<number>(0);
useLayoutEffect(() => {
if (!parentRef.current)
return;
const updateColumns = () => {
if (parentRef.current) {
const width = parentRef.current.offsetWidth;
const cols = Math.max(1, Math.floor(width / (minWidth + gap / 2)));
setColumns(cols);
setRowCount(Math.ceil(items.length / cols))
}
}
updateColumns();
const observer = new window.ResizeObserver(debounce(updateColumns, 50));
observer.observe(parentRef.current);
return () => observer.disconnect();
}, [])
// 虚拟化行
const rowVirtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => parentRef.current,
estimateSize: () => minWidth + 40, // 估算每行高度
overscan: 3,
gap
});
return (
<div
ref={parentRef}
className='h-full relative overflow-auto hide-scrollbar'
>
<div
style={{
height: rowVirtualizer.getTotalSize(),
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const rowIndex = virtualRow.index;
const start = rowIndex * columns;
const end = Math.min(start + columns, items.length);
return (
<div
key={rowIndex}
data-index={rowIndex}
ref={rowVirtualizer.measureElement}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
display: "grid",
gridTemplateColumns: `repeat(${columns}, minmax(${minWidth}px, 1fr))`,
gap: `${gap}px`
}}
>
{items.slice(start, end).map((item, i) => renderItem(item, start + i))}
</div>
);
})}
</div>
</div>
);
}
export default VirtualGrid
+11
View File
@@ -0,0 +1,11 @@
export const enum TOOLBOX_IDS {
USAGE = 'CopilotToolboxUsage',
PARAMETERS = 'CopilotToolboxParameters',
DOWNSTREAMNODES = 'CopilotToolboxDownstreamNodes'
}
export const enum TOOLBOX_LABELS {
USAGE = 'Usage',
PARAMETERS = 'Parameters',
DOWNSTREAMNODES = 'Downstream Nodes'
}
+39
View File
@@ -8,6 +8,7 @@ import { waitForApp } from "./utils/comfyapp.ts";
import "./scoped-tailwind.css";
import { app } from "./utils/comfyapp";
import "./fonts.css";
import { TOOLBOX_IDS, TOOLBOX_LABELS } from './constants/enums.ts';
const App = React.lazy(() =>
import("./App.tsx").then(({ default: App }) => ({
@@ -51,6 +52,44 @@ waitForDocumentBody()
);
},
});
}).then(() => {
app.registerExtension({
name: 'Copilot_Toolbox',
commands: [
{
id: TOOLBOX_IDS.USAGE,
label: TOOLBOX_LABELS.USAGE,
icon: 'pi pi-server',
function: () => {
// Command logic here
console.log('-call-->')
}
},
{
id: TOOLBOX_IDS.PARAMETERS,
label: TOOLBOX_LABELS.PARAMETERS,
icon: 'pi pi-book',
function: () => {
// Command logic here
console.log('-call-->')
}
},
{
id: 'Downstream_Nodes',
label: 'Downstream Nodes',
icon: 'pi pi-arrow-circle-right',
tooltip: '111',
function: () => {
// Command logic here
console.log('-call-->')
}
}
],
// Return an array of command IDs to show in the selection toolbox
// when an item is selected
getSelectionToolboxCommands: (selectedItem: any) => [TOOLBOX_IDS.USAGE, TOOLBOX_IDS.PARAMETERS, 'Downstream_Nodes']
})
console.log('111-->', app.extensionManager.getSidebarTabs());
})
// .then(() => {
// app.extensionManager.setting.set('Comfy.Sidebar.Location', 'left');
+83 -42
View File
@@ -373,6 +373,9 @@
.hidden {
display: none;
}
.aspect-\[3\/4\] {
aspect-ratio: 3/4;
}
.aspect-square {
aspect-ratio: 1 / 1;
}
@@ -415,6 +418,9 @@
.h-8 {
height: 2rem;
}
.h-\[24px\] {
height: 24px;
}
.h-\[4\.5rem\] {
height: 4.5rem;
}
@@ -451,6 +457,9 @@
.max-h-full {
max-height: 100%;
}
.min-h-0 {
min-height: 0px;
}
.min-h-\[80px\] {
min-height: 80px;
}
@@ -523,6 +532,9 @@
.w-full {
width: 100%;
}
.min-w-0 {
min-width: 0px;
}
.min-w-\[400px\] {
min-width: 400px;
}
@@ -613,6 +625,9 @@
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-\[repeat\(auto-fill\2c minmax\(120px\2c 1fr\)\)\] {
grid-template-columns: repeat(auto-fill,minmax(120px,1fr));
}
.flex-row {
flex-direction: row;
}
@@ -674,6 +689,9 @@
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.overflow-auto {
overflow: auto;
}
.overflow-hidden {
overflow: hidden;
}
@@ -686,8 +704,10 @@
.overflow-y-auto {
overflow-y: auto;
}
.scroll-smooth {
scroll-behavior: smooth;
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-ellipsis {
text-overflow: ellipsis;
@@ -765,10 +785,6 @@
--tw-border-opacity: 1;
border-color: rgb(151 151 151 / var(--tw-border-opacity, 1));
}
.border-amber-200 {
--tw-border-opacity: 1;
border-color: rgb(253 230 138 / var(--tw-border-opacity, 1));
}
.border-blue-100 {
--tw-border-opacity: 1;
border-color: rgb(219 234 254 / var(--tw-border-opacity, 1));
@@ -837,6 +853,14 @@
.border-t-transparent {
border-top-color: transparent;
}
.bg-\[\#d2d2d2\] {
--tw-bg-opacity: 1;
background-color: rgb(210 210 210 / var(--tw-bg-opacity, 1));
}
.bg-\[\#ffffff\] {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
@@ -941,13 +965,13 @@
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-amber-50 {
--tw-gradient-from: #fffbeb var(--tw-gradient-from-position);
--tw-gradient-to: rgb(255 251 235 / 0) var(--tw-gradient-to-position);
.from-gray-50 {
--tw-gradient-from: color-mix(in srgb, var(--p-text-color) 5%, transparent) var(--tw-gradient-from-position);
--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-amber-100 {
--tw-gradient-to: #fef3c7 var(--tw-gradient-to-position);
.to-gray-100 {
--tw-gradient-to: color-mix(in srgb, var(--p-text-color) 10%, transparent) var(--tw-gradient-to-position);
}
.fill-current {
fill: currentColor;
@@ -1125,21 +1149,13 @@
.leading-snug {
line-height: 1.375;
}
.text-amber-500 {
--tw-text-opacity: 1;
color: rgb(245 158 11 / var(--tw-text-opacity, 1));
}
.text-amber-600 {
--tw-text-opacity: 1;
color: rgb(217 119 6 / var(--tw-text-opacity, 1));
}
.text-amber-700 {
.text-black {
--tw-text-opacity: 1;
color: rgb(180 83 9 / var(--tw-text-opacity, 1));
}
.text-amber-800 {
--tw-text-opacity: 1;
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
color: rgb(0 0 0 / var(--tw-text-opacity, 1));
}
.text-blue-500 {
--tw-text-opacity: 1;
@@ -1352,6 +1368,30 @@
}
}
.hide-scrollbar {
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE 10+ */
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
/* Chrome/Safari/Webkit */
}
.hover\:\!overflow-visible:hover {
overflow: visible !important;
}
.hover\:\!whitespace-normal:hover {
white-space: normal !important;
}
.hover\:\!break-words:hover {
overflow-wrap: break-word !important;
}
.hover\:border-b-2:hover {
border-bottom-width: 2px;
}
@@ -1366,8 +1406,13 @@
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
}
.hover\:bg-amber-200\/50:hover {
background-color: rgb(253 230 138 / 0.5);
.hover\:\!bg-gray-200:hover {
background-color: color-mix(in srgb, var(--p-text-color) 20%, transparent) !important;
}
.hover\:bg-\[\#eee\]:hover {
--tw-bg-opacity: 1;
background-color: rgb(238 238 238 / var(--tw-bg-opacity, 1));
}
.hover\:bg-blue-100:hover {
@@ -1411,6 +1456,10 @@
background-color: color-mix(in srgb, var(--p-text-color) 5%, transparent);
}
.hover\:bg-gray-800:hover {
background-color: color-mix(in srgb, var(--p-text-color) 80%, transparent);
}
.hover\:bg-green-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
@@ -1426,16 +1475,6 @@
background-color: rgb(233 213 255 / var(--tw-bg-opacity, 1));
}
.hover\:text-amber-700:hover {
--tw-text-opacity: 1;
color: rgb(180 83 9 / var(--tw-text-opacity, 1));
}
.hover\:text-amber-800:hover {
--tw-text-opacity: 1;
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
}
.hover\:text-blue-500:hover {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
@@ -1459,6 +1498,10 @@
color: color-mix(in srgb, var(--p-text-color) 70%, transparent);
}
.hover\:text-gray-800:hover {
color: color-mix(in srgb, var(--p-text-color) 80%, transparent);
}
.hover\:text-gray-900:hover {
color: color-mix(in srgb, var(--p-text-color) 90%, transparent);
}
@@ -1522,8 +1565,8 @@
opacity: 0.5;
}
.group:hover .group-hover\:block {
display: block;
.group:hover .group-hover\:\!block {
display: block !important;
}
.group:hover .group-hover\:translate-x-1 {
@@ -1568,18 +1611,16 @@
font-weight: 500;
}
.prose-a\:text-amber-700 :is(:where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
--tw-text-opacity: 1;
color: rgb(180 83 9 / var(--tw-text-opacity, 1));
.prose-a\:text-gray-700 :is(:where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
color: color-mix(in srgb, var(--p-text-color) 70%, transparent);
}
.prose-a\:underline :is(:where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
text-decoration-line: underline;
}
.hover\:prose-a\:text-amber-800 :is(:where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))):hover {
--tw-text-opacity: 1;
color: rgb(146 64 14 / var(--tw-text-opacity, 1));
.hover\:prose-a\:text-gray-800 :is(:where(a):not(:where([class~="not-prose"],[class~="not-prose"] *))):hover {
color: color-mix(in srgb, var(--p-text-color) 80%, transparent);
}
.prose-code\:text-xs :is(:where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))) {
@@ -1725,4 +1766,4 @@
.\[\&\>\*\]\:\!my-1>* {
margin-top: 0.25rem !important;
margin-bottom: 0.25rem !important;
}
}
+9 -1
View File
@@ -34,4 +34,12 @@
background-color: transparent;
background-image: none;
}
}
}
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome/Safari/Webkit */
}
+30 -29
View File
@@ -30,6 +30,7 @@ import rehypeExternalLinks from 'rehype-external-links';
// Define the Tab type - We should import this from context to ensure consistency
import type { TabType } from '../context/ChatContext';
import { ParameterDebugInterface } from "../components/debug/ParameterDebugInterfaceV2";
interface WorkflowChatProps {
onClose?: () => void;
@@ -43,23 +44,26 @@ const Announcement = ({ message, onClose }: { message: string, onClose: () => vo
if (!message) return null;
return (
<div className="bg-gradient-to-r from-amber-50 to-amber-100 border-b border-amber-200 px-5 py-2 mt-2 relative shadow-sm">
<div
className="bg-gradient-to-r from-gray-50 to-gray-100
border border-gray-300 px-2 py-1 mt-2 ml-2 mr-2 relative shadow-sm rounded-sm"
>
<div className="flex items-center">
<div className="flex-shrink-0 mr-2">
<svg className="w-3.5 h-3.5 text-amber-600" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<div className="flex-shrink-0 mr-1">
<svg className="w-3.5 h-3.5 text-gray-700" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd"></path>
</svg>
</div>
<div className="text-amber-800 font-medium leading-relaxed pr-6 markdown-content" style={{ fontSize: '11px' }}>
<div className="text-gray-700 font-medium leading-relaxed pr-6 markdown-content" style={{ fontSize: '11px' }}>
<MemoizedReactMarkdown
rehypePlugins={[
[rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }]
]}
remarkPlugins={[remarkGfm]}
className="prose !text-[11px] prose-a:text-amber-700 prose-a:underline prose-a:font-medium hover:prose-a:text-amber-800"
className="prose !text-[11px] prose-a:text-gray-700 prose-a:underline prose-a:font-medium hover:prose-a:text-gray-800"
components={{
a: ({ node, ...props }) => (
<a {...props} className="text-amber-700 underline hover:text-amber-800 transition-colors" style={{ fontSize: '11px' }} />
<a {...props} className="text-gray-700 underline hover:text-gray-800 transition-colors" style={{ fontSize: '11px' }} />
),
p: ({ children }) => (
<span className="inline" style={{ fontSize: '11px' }}>{children}</span>
@@ -85,7 +89,7 @@ const Announcement = ({ message, onClose }: { message: string, onClose: () => vo
</div>
<button
onClick={onClose}
className="absolute right-2 top-1/2 -translate-y-1/2 text-amber-500 hover:text-amber-700 transition-colors p-1 rounded-full hover:bg-amber-200/50"
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-700 hover:text-gray-800 transition-colors p-1 rounded-full hover:bg-gray-800"
aria-label="Close announcement"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -102,11 +106,11 @@ const ParameterDebugTab = () => {
const { selectedNode, screenState } = state;
const selectedNodes = selectedNode ? selectedNode : [];
const ParameterDebugInterface = React.lazy(() =>
import("../components/debug/ParameterDebugInterfaceV2").then(module => ({
default: module.ParameterDebugInterface
}))
);
// const ParameterDebugInterface = React.lazy(() =>
// import("../components/debug/ParameterDebugInterfaceV2").then(module => ({
// default: module.ParameterDebugInterface
// }))
// );
const handleCloseParameterDebug = () => {
// Clear selected nodes and screen state
@@ -118,13 +122,11 @@ const ParameterDebugTab = () => {
return (
<div className="flex-1 flex flex-col overflow-y-auto">
<React.Suspense fallback={<div>Loading...</div>}>
<ParameterDebugInterface
selectedNodes={selectedNodes}
visible={true}
onClose={handleCloseParameterDebug}
/>
</React.Suspense>
<ParameterDebugInterface
selectedNodes={selectedNodes}
visible={true}
onClose={handleCloseParameterDebug}
/>
</div>
);
};
@@ -175,11 +177,11 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
// 使用自定义 hooks
useEffect(() => {
if (messageDivRef.current) {
messageDivRef.current.scrollTop = messageDivRef.current.scrollHeight
}
}, [messages])
// useEffect(() => {
// if (messageDivRef.current) {
// messageDivRef.current.scrollTop = messageDivRef.current.scrollHeight
// }
// }, [messages])
useEffect(() => {
if (activeTab !== 'chat') return;
@@ -643,10 +645,9 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
return (
<div
className="flex flex-col h-full w-full bg-white"
className="flex flex-col h-full w-full bg-white relative"
style={{
display: visible ? 'flex' : 'none',
height: '100%'
display: visible ? 'flex' : 'none'
}}
>
<div
@@ -687,9 +688,9 @@ export default function WorkflowChat({ onClose, visible = true, triggerUsage = f
{/* Tab content - Both tabs are mounted but only the active one is displayed */}
<div
className="flex-1 overflow-y-auto p-4 scroll-smooth h-0"
className='flex-1 p-4'
style={{ display: activeTab === 'chat' ? 'block' : 'none' }}
ref={messageDivRef}
// ref={messageDivRef}
>
<MessageList
messages={messages}