feat: conflict dist/copilot_web
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
## 📚 许可证
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 236 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 181 KiB |
Vendored
-14
@@ -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
@@ -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
@@ -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=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" 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 |
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-38
File diff suppressed because one or more lines are too long
-146
File diff suppressed because one or more lines are too long
-306
File diff suppressed because one or more lines are too long
-20
File diff suppressed because one or more lines are too long
-4
@@ -1,4 +0,0 @@
|
||||
function getImportPath(filename) {
|
||||
return `./${filename}`;
|
||||
}
|
||||
|
||||
-145
File diff suppressed because one or more lines are too long
+1
-1
@@ -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]
|
||||
|
||||
Generated
+1186
-2
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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}
|
||||
|
||||
Reference in New Issue
Block a user