feat: 专家经验开发完成
This commit is contained in:
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -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
-1
@@ -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};
|
||||
-71
File diff suppressed because one or more lines are too long
+4
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+46
-36
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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
|
||||
*
|
||||
+20
File diff suppressed because one or more lines are too long
-20
File diff suppressed because one or more lines are too long
+463
File diff suppressed because one or more lines are too long
-132
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
Reference in New Issue
Block a user