feat: 专家经验开发完成

This commit is contained in:
li
2025-08-21 18:01:09 +08:00
parent d883080170
commit 103c2fdd45
24 changed files with 1393 additions and 275 deletions
+1
View File
@@ -16,6 +16,7 @@ from aiohttp import web
import folder_paths
from .backend.controller.conversation_api import *
from .backend.controller.llm_api import *
from .backend.controller.expert_api import *
WEB_DIRECTORY = "entry"
NODE_CLASS_MAPPINGS = {}
+192
View File
@@ -0,0 +1,192 @@
from server import PromptServer
from aiohttp import web
from typing import Dict, Any
import logging
from ..dao.expert_table import (
create_rewrite_expert,
get_rewrite_expert,
list_rewrite_experts,
update_rewrite_expert_by_id,
delete_rewrite_expert_by_id
)
# 配置日志
logger = logging.getLogger(__name__)
def validate_expert_data(data: Dict[str, Any]) -> tuple[bool, str, Dict[str, Any]]:
"""验证专家数据"""
if not data:
return False, "请求数据不能为空", {}
# 验证必填字段
if 'name' not in data or not data['name']:
return False, "名称不能为空", {}
# 验证字段类型和长度
if not isinstance(data['name'], str) or len(data['name']) > 255:
return False, "名称必须是字符串且长度不超过255个字符", {}
# 验证可选字段
validated_data = {
'name': data['name'].strip(),
'description': data.get('description'),
'content': data.get('content')
}
return True, "", validated_data
@PromptServer.instance.routes.post("/api/expert/experts")
async def create_expert(request):
"""创建新的专家记录"""
try:
data = await request.json()
if not data:
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
# 验证数据
is_valid, error_msg, validated_data = validate_expert_data(data)
if not is_valid:
return web.json_response({"success": False, "message": error_msg, "data": None}, status=400)
# 创建专家记录
expert_id = create_rewrite_expert(
name=validated_data['name'],
description=validated_data['description'],
content=validated_data['content']
)
logger.info(f"成功创建专家记录,ID: {expert_id}")
return web.json_response({"success": True, "message": "创建成功", "data": {"id": expert_id}}, status=200)
except Exception as e:
logger.error(f"创建专家记录失败: {str(e)}")
return web.json_response({"success": False, "message": f"创建失败: {str(e)}", "data": None}, status=500)
@PromptServer.instance.routes.get("/api/expert/experts")
async def get_experts(request):
"""获取所有专家记录列表"""
try:
experts = list_rewrite_experts()
logger.info(f"成功获取专家记录列表,共 {len(experts)} 条")
return web.json_response({"success": True, "message": "获取列表成功", "data": {"total": len(experts), "experts": experts}}, status=200)
except Exception as e:
logger.error(f"获取专家记录列表失败: {str(e)}")
return web.json_response({"success": False, "message": f"获取列表失败: {str(e)}", "data": None}, status=500)
@PromptServer.instance.routes.get("/api/expert/experts/{expert_id}")
async def get_expert_by_id(request):
"""根据ID获取专家记录"""
try:
expert_id = int(request.match_info['expert_id'])
expert = get_rewrite_expert(expert_id)
if not expert:
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
logger.info(f"成功获取专家记录,ID: {expert_id}")
return web.json_response({"success": True, "message": "获取记录成功", "data": expert}, status=200)
except Exception as e:
logger.error(f"获取专家记录失败,ID: {expert_id}, 错误: {str(e)}")
return web.json_response({"success": False, "message": f"获取记录失败: {str(e)}", "data": None}, status=500)
@PromptServer.instance.routes.put("/api/expert/experts/{expert_id}")
async def update_expert(request):
"""更新专家记录"""
try:
expert_id = int(request.match_info['expert_id'])
data = await request.json()
if not data:
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
# 验证数据
is_valid, error_msg, validated_data = validate_expert_data(data)
if not is_valid:
return web.json_response({"success": False, "message": error_msg, "data": None}, status=400)
# 更新专家记录
success = update_rewrite_expert_by_id(
expert_id=expert_id,
name=validated_data.get('name'),
description=validated_data.get('description'),
content=validated_data.get('content')
)
if not success:
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
logger.info(f"成功更新专家记录,ID: {expert_id}")
return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200)
except Exception as e:
logger.error(f"更新专家记录失败,ID: {expert_id}, 错误: {str(e)}")
return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500)
@PromptServer.instance.routes.delete("/api/expert/experts/{expert_id}")
async def delete_expert(request):
"""删除专家记录"""
try:
expert_id = int(request.match_info['expert_id'])
success = delete_rewrite_expert_by_id(expert_id)
if not success:
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
logger.info(f"成功删除专家记录,ID: {expert_id}")
return web.json_response({"success": True, "message": "删除成功", "data": {"id": expert_id}}, status=200)
except Exception as e:
logger.error(f"删除专家记录失败,ID: {expert_id}, 错误: {str(e)}")
return web.json_response({"success": False, "message": f"删除失败: {str(e)}", "data": None}, status=500)
@PromptServer.instance.routes.patch("/api/expert/experts/{expert_id}")
async def partial_update_expert(request):
"""部分更新专家记录"""
try:
expert_id = int(request.match_info['expert_id'])
data = await request.json()
if not data:
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
# 验证字段
update_data = {}
if 'name' in data:
if not isinstance(data['name'], str) or len(data['name']) > 255:
return web.json_response({"success": False, "message": "名称必须是字符串且长度不超过255个字符", "data": None}, status=400)
update_data['name'] = data['name'].strip()
if 'description' in data:
update_data['description'] = data['description']
if 'content' in data:
update_data['content'] = data['content']
if not update_data:
return web.json_response({"success": False, "message": "没有提供要更新的字段", "data": None}, status=400)
# 更新专家记录
success = update_rewrite_expert_by_id(
expert_id=expert_id,
**update_data
)
if not success:
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
logger.info(f"成功部分更新专家记录,ID: {expert_id}")
return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200)
except Exception as e:
logger.error(f"部分更新专家记录失败,ID: {expert_id}, 错误: {str(e)}")
return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500)
# 路由配置函数
# 所有路由已通过装饰器自动注册到 ComfyUI 的 PromptServer
+251
View File
@@ -0,0 +1,251 @@
import os
import json
from typing import Dict, Any, Optional, List
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
# 创建数据库基类
Base = declarative_base()
# 定义workflow_version表模型
class RewriteExpert(Base):
__tablename__ = 'rewrite_expert'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(255), nullable=False)
description = Column(Text, nullable=True) # 描述
content = Column(Text, nullable=True) # 内容
create_time = Column(DateTime, default=datetime.utcnow)
update_time = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
def _parse_json_or_raw(value: Optional[str]) -> Any:
if value is None:
return None
try:
return json.loads(value)
except Exception:
return value
def _format_dt(dt: Optional[datetime]) -> Optional[str]:
if dt is None:
return None
# 存储为UTC时间,序列化为ISO字符串并加Z
try:
return dt.isoformat() + 'Z'
except Exception:
return str(dt)
return {
'id': self.id,
'name': self.name,
'description': self.description,
'content': self.content,
'createTime': _format_dt(self.create_time),
'updateTime': _format_dt(self.update_time),
}
class DatabaseManager:
"""数据库管理器"""
def __init__(self, db_path: str = None):
if db_path is None:
# 默认数据库路径
current_dir = os.path.dirname(os.path.abspath(__file__))
db_dir = os.path.join(current_dir, '..', 'data')
os.makedirs(db_dir, exist_ok=True)
db_path = os.path.join(db_dir, 'rewrite_expert.db')
self.db_path = db_path
self.engine = create_engine(f'sqlite:///{db_path}', echo=False)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
# 创建表
Base.metadata.create_all(bind=self.engine)
# 轻量级表结构升级(为已有库添加新列)
self._ensure_schema()
# 首次初始化时从内置JSON写入默认数据
self._seed_initial_data()
def get_session(self):
"""获取数据库会话"""
return self.SessionLocal()
def save_rewrite_expert(self, name: str, description: str, content: str) -> int:
"""新增一条专家记录,返回新ID"""
session = self.get_session()
try:
rewrite_expert = RewriteExpert(
name=name,
description=self._serialize_field(description),
content=self._string_field(content)
)
session.add(rewrite_expert)
session.commit()
session.refresh(rewrite_expert)
return rewrite_expert.id
except Exception as e:
session.rollback()
raise e
finally:
session.close()
def _ensure_schema(self) -> None:
"""确保SQLite中存在新列(非破坏性升级)。"""
try:
with self.engine.connect() as conn:
result = conn.execute(text("PRAGMA table_info(rewrite_expert)"))
existing_columns = {row[1] for row in result}
if 'create_time' not in existing_columns:
conn.execute(text("ALTER TABLE rewrite_expert ADD COLUMN create_time TEXT"))
conn.execute(text("UPDATE rewrite_expert SET create_time = COALESCE(create_time, CURRENT_TIMESTAMP)"))
if 'update_time' not in existing_columns:
conn.execute(text("ALTER TABLE rewrite_expert ADD COLUMN update_time TEXT"))
conn.execute(text("UPDATE rewrite_expert SET update_time = COALESCE(update_time, CURRENT_TIMESTAMP)"))
except Exception:
# 忽略升级失败,后续写入由ORM默认值补齐
pass
def _seed_initial_data(self) -> None:
"""如果表为空,则从 data/workflow_rewrite_expert.json 导入初始数据。"""
try:
# 仅当表为空时导入
session = self.get_session()
try:
has_any = session.query(RewriteExpert.id).first() is not None
finally:
session.close()
if has_any:
return
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, '..', 'data', 'workflow_rewrite_expert.json')
json_path = os.path.abspath(json_path)
if not os.path.exists(json_path):
return
with open(json_path, 'r', encoding='utf-8') as f:
items = json.load(f)
if not isinstance(items, list):
return
session = self.get_session()
try:
for entry in items:
name = entry.get('name')
if not name:
continue
description = entry.get('description')
content = entry.get('content')
expert = RewriteExpert(
name=name,
description=description,
content=content
)
session.add(expert)
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
except Exception:
# 读取或导入失败不影响正常启动
pass
def get_rewrite_expert_by_id(self, expert_id: int) -> Optional[Dict[str, Any]]:
"""根据ID获取专家记录"""
session = self.get_session()
try:
expert = session.query(RewriteExpert).filter(RewriteExpert.id == expert_id).first()
return expert.to_dict() if expert else None
finally:
session.close()
def list_rewrite_experts(self) -> List[Dict[str, Any]]:
"""获取所有专家记录,按ID倒序"""
session = self.get_session()
try:
experts = session.query(RewriteExpert).order_by(RewriteExpert.id.asc()).all()
return [e.to_dict() for e in experts]
finally:
session.close()
def update_rewrite_expert(self, expert_id: int, name: Optional[str] = None, description: Optional[Any] = None, content: Optional[Any] = None) -> bool:
"""根据ID更新专家记录的部分或全部字段"""
session = self.get_session()
try:
expert = session.query(RewriteExpert).filter(RewriteExpert.id == expert_id).first()
if not expert:
return False
if name is not None:
expert.name = name
if description is not None:
expert.description = self._serialize_field(description)
if content is not None:
expert.content = self._string_field(content)
session.commit()
return True
except Exception as e:
session.rollback()
raise e
finally:
session.close()
def delete_rewrite_expert(self, expert_id: int) -> bool:
"""根据ID删除专家记录"""
session = self.get_session()
try:
expert = session.query(RewriteExpert).filter(RewriteExpert.id == expert_id).first()
if not expert:
return False
session.delete(expert)
session.commit()
return True
except Exception as e:
session.rollback()
raise e
finally:
session.close()
@staticmethod
def _serialize_field(value: Optional[Any]) -> Optional[str]:
"""将任意入参序列化为可存储的字符串。None -> None,其他 -> str/JSON"""
if value is None:
return None
if isinstance(value, str):
return value
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return str(value)
@staticmethod
def _string_field(value: Optional[Any]) -> Optional[str]:
"""确保字段以字符串形式存储(不做JSON序列化)。"""
if value is None:
return None
return value if isinstance(value, str) else str(value)
# 全局数据库管理器实例
db_manager = DatabaseManager()
# 便捷函数(对外导出)
def create_rewrite_expert(name: str, description: Any = None, content: Any = None) -> int:
return db_manager.save_rewrite_expert(name=name, description=description, content=content)
def get_rewrite_expert(expert_id: int) -> Optional[Dict[str, Any]]:
return db_manager.get_rewrite_expert_by_id(expert_id)
def list_rewrite_experts() -> List[Dict[str, Any]]:
return db_manager.list_rewrite_experts()
def update_rewrite_expert_by_id(expert_id: int, name: Optional[str] = None, description: Optional[Any] = None, content: Optional[Any] = None) -> bool:
return db_manager.update_rewrite_expert(expert_id=expert_id, name=name, description=description, content=content)
def delete_rewrite_expert_by_id(expert_id: int) -> bool:
return db_manager.delete_rewrite_expert(expert_id)
@@ -1,7 +1,7 @@
function getImportPath(filename) {
return `./${filename}`;
}
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-CnxBg5zO.js","copilot_web/vendor-markdown-BAJQc3l1.js","copilot_web/vendor-react-Dixhfmvb.js","copilot_web/message-components-CKrvYJJz.js","copilot_web/input.js","copilot_web/assets/input-Dx1yyZQD.css","copilot_web/fonts.css"].map(path => {
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["copilot_web/workflowChat-Chu9SKsZ.js","copilot_web/vendor-markdown-_DNnI6VC.js","copilot_web/vendor-react-BVt39aT-.js","copilot_web/message-components-Dl6ezfiu.js","copilot_web/input.js","copilot_web/assets/input-C0Z8FZcY.css","copilot_web/fonts.css"].map(path => {
const apiBase = window.comfyAPI?.api?.api?.api_base;
if (apiBase) {
// 有 API base 时,使用完整路径
@@ -11,4 +11,4 @@ function getImportPath(filename) {
return `./${path}`;
}
}))))=>i.map(i=>d[i]);
import{_ as a}from"./input.js";import{j as o}from"./vendor-markdown-BAJQc3l1.js";import{r as t,R as i}from"./vendor-react-Dixhfmvb.js";import{C as n}from"./message-components-CKrvYJJz.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-CnxBg5zO.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
import{_ as a}from"./input.js";import{j as o}from"./vendor-markdown-_DNnI6VC.js";import{r as t,R as i}from"./vendor-react-BVt39aT-.js";import{C as n}from"./message-components-Dl6ezfiu.js";const s={EXPLAIN_NODE:"copilot:explain-node",TOOLBOX_USAGE:"copilot:toolbox-usage",TOOLBOX_PARAMETERS:"copilot:toolbox-parameters",TOOLBOX_DOWNSTREAMNODES:"copilot:toolbox-downstreamnodes"},d=i.lazy(()=>a(()=>import(getImportPath("workflowChat-Chu9SKsZ.js")).then(e=>e.w),__vite__mapDeps([0,1,2,3,4,5,6])).then(e=>({default:e.default})));function c(){const[e,r]=t.useState(!1);return t.useEffect(()=>{const l=()=>{r(!0)};return window.addEventListener(s.EXPLAIN_NODE,l),()=>window.removeEventListener(s.EXPLAIN_NODE,l)},[]),o.jsx(n,{children:o.jsx("div",{className:"h-full w-full flex flex-col",children:o.jsx(t.Suspense,{fallback:o.jsx("div",{className:"h-full w-full flex items-center justify-center",children:"Loading..."}),children:o.jsx(d,{visible:!0,triggerUsage:e,onUsageTriggered:()=>r(!1)})})})})}const O=Object.freeze(Object.defineProperty({__proto__:null,default:c},Symbol.toStringTag,{value:"Module"}));export{O as A,s as C};
@@ -1,5 +1,5 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as s}from"./vendor-markdown-BAJQc3l1.js";import{r as i}from"./vendor-react-Dixhfmvb.js";import{u as A,B as D,R as E,h as x,a as m,W as b}from"./message-components-CKrvYJJz.js";function M({content:f,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:w}){const{dispatch:u,abortControllerRef:l}=A(),[d,_]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,N]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(f)}catch(e){return console.error("Failed to parse message content:",e),null}},[f]),h=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[r])||j,C=async()=>{if(d)return;const e=x(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
import{j as s}from"./vendor-markdown-_DNnI6VC.js";import{r as i}from"./vendor-react-BVt39aT-.js";import{u as A,B as D,R as E,i as x,a as m,W as b}from"./message-components-Dl6ezfiu.js";function M({content:f,name:k="Assistant",avatar:O,onAddMessage:y,onUpdateMessage:c,messageId:w}){const{dispatch:u,abortControllerRef:l}=A(),[d,_]=i.useState(!1),[T,G]=i.useState(""),[J,S]=i.useState(!1),[j,N]=i.useState(null),r=i.useMemo(()=>{try{return JSON.parse(f)}catch(e){return console.error("Failed to parse message content:",e),null}},[f]),h=i.useMemo(()=>{if(!r||!r.ext)return null;const e=r.ext.find(t=>t.type==="debug_start_checkpoint"||t.type==="debug_checkpoint"&&t.data?.checkpoint_type==="debug_start");return e&&e.data&&e.data.checkpoint_id?e.data.checkpoint_id:null},[r])||j,C=async()=>{if(d)return;const e=x(),t={id:e,role:"ai",content:JSON.stringify({text:`🔍 Starting workflow analysis...
`,ext:[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};y?.(t);try{await v(),u({type:"SET_LOADING",payload:!0}),await I(e)}finally{_(!1)}},v=async()=>{try{const e=await m.graphToPrompt(),t=localStorage.getItem("sessionId")||"";if(t&&e){const n=await b.saveWorkflowCheckpoint(t,e.output,e.workflow,"debug_start");if(N(n.version_id),console.log(`Saved debug start checkpoint: ${n.version_id}`),c&&r){const o=[...r.ext||[]].filter(g=>g.type!=="debug_start_checkpoint"&&!(g.type==="debug_checkpoint"&&g.data?.checkpoint_type==="debug_start"));o.push({type:"debug_start_checkpoint",data:{checkpoint_id:n.version_id,checkpoint_type:"debug_start"}});const p={id:w||x(),role:"ai",content:JSON.stringify({text:r.text,ext:o}),format:"debug_guide",name:"Assistant"};c(p)}}}catch(e){console.error("Failed to save checkpoint before debug:",e)}},I=async e=>{try{const t=await m.graphToPrompt();let n="",a=null;l&&(l.current=new AbortController);for await(const o of b.streamDebugAgent(t,l?.current?.signal||void 0))if(o.text){n=o.text,o.ext&&(a=o.ext);const p={id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!1,debugGuide:!0};c?.(p)}c?.({id:e,role:"ai",content:JSON.stringify({text:n,ext:a||[]}),format:"markdown",name:"Assistant",finished:!0,debugGuide:!0}),u({type:"SET_LOADING",payload:!1}),l.current=null}catch(t){console.error("Error calling debug agent:",t),S(!1);const a={id:e,role:"ai",content:JSON.stringify({text:`I encountered an error while analyzing your workflow: ${t.message||"Unknown error"}`,ext:[]}),format:"markdown",name:"Assistant",finished:!0};c?.(a),u({type:"SET_LOADING",payload:!1}),l.current=null}};return r?s.jsx(D,{name:k,children:s.jsxs("div",{className:"bg-gray-100 pt-4 px-4 pb-3 rounded-lg",children:[s.jsx("div",{className:"flex justify-between items-start",children:s.jsx("p",{className:"text-gray-700 text-sm flex-1",children:r.text})}),s.jsx("div",{className:"flex justify-end mt-4",children:s.jsx("button",{onClick:C,disabled:d,className:`debug-btn bg-debug-btn text-sm text-[#fff] rounded-lg px-4 py-2 ${d?"cursor-not-allowed":"cursor-pointer"}`,children:d?"Analyzing...":"Debug Errors"})}),s.jsx("div",{className:"flex justify-end mt-2",children:!!h&&s.jsx("div",{className:"ml-2 flex-shrink-0",children:s.jsx(E,{checkpointId:h,onRestore:()=>{console.log("Workflow restored from checkpoint")}})})})]})}):null}export{M as DebugGuide};
@@ -1,4 +1,4 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{j as e}from"./vendor-markdown-BAJQc3l1.js";import{b as f,c as h,B as m,p as u,R as w}from"./message-components-CKrvYJJz.js";import{r as k}from"./vendor-react-Dixhfmvb.js";import{B as j}from"./workflowChat-CnxBg5zO.js";import"./input.js";/* empty css */import"./App-zk26O2-e.js";const v=i=>{const{title:a="",isWorkflowUpdate:d=!1,className:n="",children:c}=i,[s,r]=k.useState(!1);return e.jsx(j,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${n} ${s?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{r(!s)},children:s?e.jsx(f,{className:"text-gray-700"}):e.jsx(h,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function B({content:i,name:a="Assistant",avatar:d,format:n="markdown"}){const c=()=>{let s=null,r=!1,o;try{if(o=JSON.parse(i),o.ext){let t=o.ext.find(l=>l.type==="workflow_rewrite_checkpoint"||l.type==="debug_checkpoint"&&l.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(s=t.data.checkpoint_id,r=!0):(t=o.ext.find(l=>l.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(s=t.data.version_id,r=!0):(t=o.ext.find(l=>l.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(s=t.data.checkpoint_id,r=!1))),o.ext.find(l=>l.type==="workflow_update")&&(r=!0)}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const x=r?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-gray-900",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),p=r?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(v,{title:x,isWorkflowUpdate:r,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[n==="markdown"?e.jsx(u,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),p]})}),e.jsx("div",{className:"flex justify-end mt-2",children:!!s&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(w,{checkpointId:s,onRestore:()=>{console.log(`Workflow restored from ${r?"workflow update":"debug"} checkpoint`)},title:r?`Restore to this version (Version ${s})`:`Restore checkpoint ${s}`})})})]})};return e.jsx(m,{name:a,children:c()})}export{B as DebugResult};
import{j as e}from"./vendor-markdown-_DNnI6VC.js";import{b as f,c as h,B as m,q as u,R as w}from"./message-components-Dl6ezfiu.js";import{r as k}from"./vendor-react-BVt39aT-.js";import{B as j}from"./workflowChat-Chu9SKsZ.js";import"./input.js";/* empty css */import"./App-C8-T9maK.js";const v=i=>{const{title:a="",isWorkflowUpdate:d=!1,className:n="",children:c}=i,[s,r]=k.useState(!1);return e.jsx(j,{className:"rounded-lg ",borderClassName:"rounded-lg",children:e.jsxs("div",{className:`flex flex-col ${n} ${s?"":"max-h-[200px]"}`,children:[e.jsxs("div",{className:"flex justify-between items-center pb-2 border-b border-gray-100",children:[e.jsx("div",{children:typeof a=="string"?e.jsx("h3",{className:"text-sm text-[#fff] font-medium",children:a}):a}),e.jsx("button",{onClick:()=>{r(!s)},children:s?e.jsx(f,{className:"text-gray-700"}):e.jsx(h,{className:"text-gray-700"})})]}),e.jsx("div",{className:"overflow-hidden flex-1",children:c})]})})};function B({content:i,name:a="Assistant",avatar:d,format:n="markdown"}){const c=()=>{let s=null,r=!1,o;try{if(o=JSON.parse(i),o.ext){let t=o.ext.find(l=>l.type==="workflow_rewrite_checkpoint"||l.type==="debug_checkpoint"&&l.data?.checkpoint_type==="workflow_rewrite_start");t&&t.data&&t.data.checkpoint_id?(s=t.data.checkpoint_id,r=!0):(t=o.ext.find(l=>l.type==="workflow_rewrite_complete"),t&&t.data&&t.data.version_id?(s=t.data.version_id,r=!0):(t=o.ext.find(l=>l.type==="debug_checkpoint"),t&&t.data&&t.data.checkpoint_id&&(s=t.data.checkpoint_id,r=!1))),o.ext.find(l=>l.type==="workflow_update")&&(r=!0)}}catch(t){return console.error("Failed to parse DebugResult content:",t),null}const x=r?e.jsxs("div",{className:"flex items-center text-green-500",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",fill:"currentColor",viewBox:"0 0 20 20",children:e.jsx("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z",clipRule:"evenodd"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Successfully"})]}):e.jsxs("div",{className:"flex items-center text-gray-900",children:[e.jsx("svg",{className:"w-5 h-5 mr-2",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"28108",fill:"currentColor",children:e.jsx("path",{d:"M401.048025 844.855924c0 20.341281 16.643052 36.984333 36.984333 36.984333l147.936307 0c20.341281 0 36.984333-16.643052 36.984333-36.984333L622.952998 807.872614 401.048025 807.872614 401.048025 844.855924zM512 142.159744c-142.943596 0-258.888282 115.944686-258.888282 258.888282 0 88.021729 44.011376 165.503405 110.951975 212.288964l0 83.58365c0 20.341281 16.643052 36.984333 36.984333 36.984333l221.903949 0c20.341281 0 36.984333-16.643052 36.984333-36.984333l0-83.58365c66.941622-46.784536 110.951975-124.266212 110.951975-212.288964C770.888282 258.104429 654.943596 142.159744 512 142.159744zM617.588827 552.682561l-31.621185 22.005176 0 85.248569L438.031335 659.936307l0-85.063351L406.41015 552.866756c-49.743938-34.764781-79.33079-91.350544-79.33079-151.634536 0-101.890598 83.029018-184.92064 184.92064-184.92064s184.92064 83.029018 184.92064 184.92064C696.919617 461.332017 667.332764 517.91778 617.588827 552.682561z","p-id":"28277"})}),e.jsx("h4",{className:"font-bold text-xl",children:"Workflow Updated Finished"})]}),p=r?e.jsx("div",{className:"mt-3 text-xs text-gray-700",children:"💡 If you're not satisfied with the changes, click the restore button to revert to the previous version."}):null;return e.jsxs("div",{className:"sticky top-0 left-0 w-full bg-gray-100 p-4 rounded-lg overflow-hidden",children:[e.jsx(v,{title:x,isWorkflowUpdate:r,className:"p-4",children:e.jsxs("div",{className:"prose prose-sm max-w-none",children:[n==="markdown"?e.jsx(u,{response:o||{}}):e.jsx("pre",{className:"whitespace-pre-wrap text-gray-700 text-sm leading-relaxed h-full",children:o?.text||""}),p]})}),e.jsx("div",{className:"flex justify-end mt-2",children:!!s&&e.jsx("div",{className:"ml-2 flex-shrink-0",children:e.jsx(w,{checkpointId:s,onRestore:()=>{console.log(`Workflow restored from ${r?"workflow update":"debug"} checkpoint`)},title:r?`Restore to this version (Version ${s})`:`Restore checkpoint ${s}`})})})]})};return e.jsx(m,{name:a,children:c()})}export{B as DebugResult};
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
+3 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
function getImportPath(filename) {
return `./${filename}`;
}
import{c as Wr,g as Wa}from"./vendor-react-Dixhfmvb.js";var Ya={exports:{}},rn={};/**
import{c as Wr,g as Wa}from"./vendor-react-BVt39aT-.js";var Ya={exports:{}},rn={};/**
* @license React
* react-jsx-runtime.production.js
*
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
+55
View File
@@ -0,0 +1,55 @@
const BASE = '/api/expert/experts';
const handleResponse = async (res: Response) => {
const contentType = res.headers.get('content-type') || '';
const isJson = contentType.includes('application/json');
const body = isJson ? await res.json() : await res.text();
if (!res.ok) {
const message = isJson ? body?.message || 'Request failed' : body;
throw new Error(message);
}
return body;
}
export const createRewriteExpert = async (data: any): Promise<any> => {
const res = await fetch(BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return handleResponse(res);
};
export const getRewriteExpertsList = async (): Promise<any> => {
console.log('getRewriteExpertsList--->')
const res = await fetch(BASE, { method: 'GET' });
return handleResponse(res);
};
export const getRewriteExpertById = async (expertId: number): Promise<any> => {
const res = await fetch(`${BASE}/${expertId}`, { method: 'GET' });
return handleResponse(res);
};
export const updateRewriteExpert = async (data: any): Promise<any> => {
const res = await fetch(`${BASE}/${data.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return handleResponse(res);
};
export const patchRewriteExpert = async (expertId: number, data: any): Promise<any> => {
const res = await fetch(`${BASE}/${expertId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return handleResponse(res);
};
export const deleteRewriteExpert = async (expertId: number): Promise<any> => {
const res = await fetch(`${BASE}/${expertId}`, { method: 'DELETE' });
return handleResponse(res);
};
+23 -2
View File
@@ -9,6 +9,7 @@ import { generateUUID } from '../../utils/uuid';
import { Portal } from './Portal';
import ImageLoading from '../ui/Image-Loading';
import { useChatContext } from '../../context/ChatContext';
import RewriteExpertModal from './RewriteExpertModal';
// Debug icon component
const DebugIcon = ({ className }: { className: string }) => (
@@ -17,6 +18,14 @@ const DebugIcon = ({ className }: { className: string }) => (
</svg>
);
const ExpertIcon = ({ className }: { className: string }) => (
<svg className={className} viewBox="0 0 1024 1024" fill="currentColor">
<path d="M512.82 958.87c-25.38 0-49.1-12-63.45-32.21l-20.95-29.43a8 8 0 0 0-6.41-2.79l-319.1 1.72C46.17 896.16 0 852 0 797.7V165.3c0-54.3 46.17-98.45 102.91-98.45l256.13-1.72c50 0 97.12 18.21 132.6 51.28A182.32 182.32 0 0 1 512.9 140a184.56 184.56 0 0 1 21.06-23.31c35.55-33.21 82.65-51.51 132.63-51.51h254.7c56.74 0 102.91 44.17 102.91 98.46V796c0 54.29-46.17 98.45-102.91 98.45H603.63a8 8 0 0 0-6.42 2.79l-20.94 29.43c-14.35 20.16-38.07 32.2-63.45 32.2z m-409.91-822c-18.15 0-32.91 12.76-32.91 28.45V797.7c0 15.69 14.76 28.46 32.91 28.46l319.1-1.72c25.38 0 49.1 12 63.45 32.2l20.94 29.44c1.84 2.58 5.38 2.79 6.42 2.79s4.57-0.21 6.41-2.79l21-29.44c14.35-20.16 38.06-32.2 63.45-32.2h317.61c18.15 0 32.91-12.76 32.91-28.45v-632.4c0-15.69-14.76-28.46-32.91-28.46h-254.7c-65.49 0-118.77 48.76-118.77 108.68v461a35 35 0 1 1-70 0v-461c0-28.76-12-55.82-33.9-76.19-22.48-21-52.62-32.49-84.88-32.49z">
</path>
<path d="M362.52 330.46H197.37a35 35 0 0 1 0-70h165.15a35 35 0 0 1 0 70zM298.32 482.71h-101a35 35 0 0 1 0-70h101a35 35 0 0 1 0 70zM828.16 330.46H663.02a35 35 0 0 1 0-70h165.14a35 35 0 0 1 0 70zM828.16 482.71H727.21a35 35 0 0 1 0-70h100.95a35 35 0 0 1 0 70z">
</path>
</svg>
);
interface ChatInputProps {
input: string;
loading: boolean;
@@ -63,6 +72,7 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(({
const { state, dispatch } = useChatContext();
const { messages } = state;
const [showUploadModal, setShowUploadModal] = useState(false);
const [showRewriteExpertModal, setShowRewriteExpertModal] = useState(false);
const [models, setModels] = useState<{
label: ReactNode; name: string; image_enable: boolean
}[]>([]);
@@ -238,7 +248,16 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(({
</div>
)}
<div className="w-full flex flex-row justify-end p-1">
<div className="w-full flex flex-row justify-end p-1 gap-2">
<button
type="button"
onClick={() => setShowRewriteExpertModal(true)}
className="rounded-md bg-white border-none
hover:!bg-gray-100
transition-all duration-200 active:scale-95"
title="Debug workflow">
<ExpertIcon className="h-5 w-5" />
</button>
<button
type="button"
onClick={handleDebugClick}
@@ -332,7 +351,9 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(({
title="Debug workflow">
<DebugIcon className="h-5 w-5" />
</button> */}
{
showRewriteExpertModal && <RewriteExpertModal onClose={() => setShowRewriteExpertModal(false)} />
}
{/* 上传图片模态框 */}
{showUploadModal && (
<Portal>
+3 -1
View File
@@ -18,5 +18,7 @@ export {
Settings as CogIcon,
StopCircle as StopIcon,
History as HistoryIcon,
MessageCircleWarning as WarningIcon
MessageCircleWarning as WarningIcon,
CirclePlus as CirclePlusIcon,
FilePenLine as FilePenLineIcon
} from 'lucide-react';
@@ -0,0 +1,300 @@
import { useEffect, useRef, useState } from "react";
import { createRewriteExpert, deleteRewriteExpert, getRewriteExpertsList, updateRewriteExpert } from "../../apis/rewriteExpertApi";
import { Button, ConfigProvider, Form, Space, Table, type TableProps } from 'antd';
import { Portal } from "./Portal";
import { Input } from 'antd';
import { XIcon, TrashIcon, CirclePlusIcon, FilePenLineIcon } from "./Icons";
import { debounce } from "lodash";
import Modal from '../ui/Modal';
import { app } from "../../utils/comfyapp";
const { TextArea } = Input
interface IProps {
onClose: () => void
}
type ColumnsType<T extends object> = TableProps<T>['columns'];
type DataType = {
id: number
name: string
description: string
content: string
}
const RewriteExpertModal: React.FC<IProps> = ({onClose}) => {
const [dataList, setDataList] = useState<DataType[]>([])
const [searchValue, setSearchValue] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [currentDataList, setCurrentDataList] = useState<DataType[]>([])
const [modalContent, setModalContent] = useState<string | null>(null)
const [editData, setEditData] = useState<DataType | null>(null)
const isAdd = useRef<boolean>(false)
const isDark = app.extensionManager.setting.get('Comfy.ColorPalette') === 'dark';
const onEdit = (item: DataType) => {
setEditData(item)
}
const onDelete = async (id: number) => {
if (id < 1) return
try {
const res = await deleteRewriteExpert(id)
if (!!res.success) {
setModalContent('Delete success')
queryGetRewriteExpertsList()
}
} catch (error) {
console.log('error---_>', error)
setModalContent(`Delete error: ${error}`)
}
}
const queryGetRewriteExpertsList = async () => {
setLoading(true)
try {
const res = await getRewriteExpertsList()
if (!!res.success && !!res?.data?.experts) {
setDataList(res.data.experts)
setCurrentDataList(res.data.experts)
}
} catch (error) {
setModalContent(`Get list error: ${error}`)
console.log('error---_>', error)
} finally {
setLoading(false)
}
}
const handleSetCurrentDataList = (value: string) => {
if (value === '') {
setCurrentDataList(dataList)
} else {
setCurrentDataList(dataList.filter(item => item.name.includes(value)))
}
}
const handleAddRewriteExpert = () => {
isAdd.current = true
setEditData({
id: 0,
name: '',
description: '',
content: ''
})
}
const handleSearchRewriteExpert = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value
handleSetCurrentDataList(value)
setSearchValue(value)
}
const onFinish = async (values: any) => {
let func = null
if (isAdd.current) {
func = createRewriteExpert
} else {
func = updateRewriteExpert
}
try {
const res = await func(values)
if (!!res.success) {
setModalContent(isAdd.current ? 'Add success' : 'Edit success')
queryGetRewriteExpertsList()
setEditData(null)
}
} catch (error) {
setModalContent(isAdd.current ? `Add error ${error}` : `Edit error ${error}`)
}
}
const onCancel = () => {
setEditData(null)
isAdd.current = false
}
useEffect(() => {
queryGetRewriteExpertsList()
}, [])
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
ellipsis: { showTitle: true },
width: '20%'
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
ellipsis: { showTitle: true },
width: '35%'
},
{
title: 'Content',
dataIndex: 'content',
key: 'content',
ellipsis: { showTitle: true },
width: '35%'
},
{
title: 'Action',
key: 'action',
render: (_, record) => (
<Space size="middle">
<button
onClick={() => onEdit(record)}
className="bg-transparent border-none text-gray-500 hover:!text-gray-700"
>
<FilePenLineIcon className="w-5 h-5" />
</button>
<button
onClick={() => onDelete(record.id)}
className="bg-transparent border-none text-gray-500 hover:!text-gray-700"
>
<TrashIcon className="w-5 h-5" />
</button>
</Space>
),
width: '10%'
},
];
return (
<Portal>
<div
id='comfyui-copilot-rewrite-expert-modal'
className="fixed inset-0 bg-[rgba(0,0,0,0.5)] flex items-center justify-center"
style={{
backgroundColor: 'rgba(0,0,0,0.5)'
}}
>
<div className="relative bg-white rounded-xl p-6 w-1/2 h-1/2 flex flex-col">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl text-gray-900 dark:text-white font-semibold">Set API Key</h2>
<button
onClick={onClose}
disabled={loading}
className="bg-white border-none text-gray-500 hover:!text-gray-700"
>
<XIcon className="w-5 h-5" />
</button>
</div>
<div className="flex justify-between mb-2">
<button
onClick={handleAddRewriteExpert}
className="bg-white border-none text-gray-500 hover:!text-gray-700"
>
<CirclePlusIcon className="w-5 h-5" />
</button>
<Input
placeholder="Enter search name"
allowClear
value={searchValue}
disabled={loading}
onChange={debounce(handleSearchRewriteExpert, 500)}
className="search-input w-1/4 bg-white text-[#888] placeholder-gray-500 border-gray-300"
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<ConfigProvider
theme={{
components: {
Table: {
borderColor: isDark ? '#666' : '#f0f0f0',
headerBg: isDark ? '#333' : '#fafafa',
headerColor: isDark ? '#fff' : '#fafafa',
rowHoverBg: isDark ? '#333' : '#fafafa',
},
Pagination: {
itemBg: isDark ? 'rgb(24, 24, 27)' : '#fff',
itemActiveBg: isDark ? 'rgb(24, 24, 27)' : '#fff',
colorText: isDark ? 'rgba(255,255,255,0.88)' : 'rgba(0,0,0,0.88)',
colorBgTextHover: isDark ? '#555' : 'rgba(0,0,0,0.06)',
colorPrimary: isDark ? '#aaa' : '#1677ff',
colorPrimaryHover: isDark ? '#999' : '#4096ff',
}
}
}}
>
<Table
bordered
loading={loading}
columns={columns}
dataSource={currentDataList}
rowClassName={"bg-white text-gray-800"}
/>
</ConfigProvider>
</div>
{
editData && <div className="absolute top-0 left-0 w-full h-full bg-white rounded-xl p-6 z-5">
<h2 className="text-xl text-gray-900 dark:text-white font-semibold">{`${editData?.name && editData?.name !== '' ? 'Edit' : 'Add'} Rewrite Expert`}</h2>
<Form
onFinish={onFinish}
initialValues={editData}
>
<Form.Item
name="id"
label="Id"
hidden
>
<Input
disabled
/>
</Form.Item>
<Form.Item
name="name"
label="Name"
rules={[{ required: true }]}
>
<Input
placeholder="Enter name"
/>
</Form.Item>
<Form.Item
name="description"
label="Description"
rules={[{ required: true }]}
>
<TextArea
placeholder="Enter description"
autoSize={{ maxRows: 6 }}
/>
</Form.Item>
<Form.Item
name="content"
label="Content"
rules={[{ required: true }]}
>
<TextArea
placeholder="Enter content"
autoSize={{ maxRows: 10 }}
/>
</Form.Item>
<Form.Item>
<Space align='end' size='middle'>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Button onClick={onCancel}>
Cancel
</Button>
</Space>
</Form.Item>
</Form>
</div>
}
</div>
<Modal open={!!modalContent && modalContent !== ''} onClose={() => setModalContent(null)}>
<p>{modalContent}</p>
</Modal>
</div>
</Portal>
);
}
export default RewriteExpertModal;
+1 -1
View File
@@ -41,7 +41,7 @@ const Modal: React.FC<ModalProps> = ({
onClick={e => e.stopPropagation()} // 阻止冒泡,点击内容区不关闭
>
<button
className="absolute top-2 right-4 text-gray-400 hover:text-gray-600"
className="absolute top-2 right-4 bg-transparent border-none text-gray-400 hover:text-gray-600"
onClick={onClose}
aria-label="Close"
>
+20 -1
View File
@@ -1,5 +1,6 @@
#comfyui-copilot-plugin,
#comfyui-copilot-modal {
#comfyui-copilot-modal,
#comfyui-copilot-rewrite-expert-modal {
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
@@ -409,6 +410,9 @@
.h-1 {
height: 0.25rem;
}
.h-1\/2 {
height: 50%;
}
.h-10 {
height: 2.5rem;
}
@@ -508,6 +512,9 @@
.w-1\/2 {
width: 50%;
}
.w-1\/4 {
width: 25%;
}
.w-12 {
width: 3rem;
}
@@ -1251,6 +1258,10 @@
--tw-text-opacity: 1;
color: rgb(113 163 242 / var(--tw-text-opacity, 1));
}
.text-\[\#888\] {
--tw-text-opacity: 1;
color: rgb(136 136 136 / var(--tw-text-opacity, 1));
}
.text-\[\#fff\] {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
@@ -1654,6 +1665,14 @@
animation: rotate 1s linear infinite;
}
.search-input input::-moz-placeholder {
color: #888;
}
.search-input input::placeholder {
color: #888;
}
.hover\:\!overflow-visible:hover {
overflow: visible !important;
}
+5 -2
View File
@@ -1,5 +1,6 @@
#comfyui-copilot-plugin,
#comfyui-copilot-modal {
#comfyui-copilot-modal,
#comfyui-copilot-rewrite-expert-modal {
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -200,4 +201,6 @@
.loading-spin {
animation: rotate 1s linear infinite;
}
}
.search-input input::placeholder { color: #888; }