From 6c74bef8dad85c7b9aa349ec825e8a05c186c05f Mon Sep 17 00:00:00 2001 From: "shen893514322@126.com" Date: Fri, 20 Mar 2026 18:13:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E6=96=B0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 133 bytes backend/__pycache__/main.cpython-311.pyc | Bin 0 -> 603 bytes .../api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 137 bytes backend/api/__pycache__/route.cpython-311.pyc | Bin 0 -> 1102 bytes .../core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 138 bytes .../core/__pycache__/config.cpython-311.pyc | Bin 0 -> 1988 bytes .../core/__pycache__/items.cpython-311.pyc | Bin 0 -> 1912 bytes backend/core/models/chat_history.py | 158 +++++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 139 bytes .../get_all_role_and_chat.cpython-311.pyc | Bin 0 -> 2326 bytes .../save_input_to_json.cpython-311.pyc | Bin 0 -> 5588 bytes backend/tools/get_all_role_and_chat.py | 5 +- .../chat/test/{12312.jsonl => 12331212.jsonl} | 0 .../components/RoleSelector/RoleSelector.css | 438 ++++++++++++++--- .../components/RoleSelector/RoleSelector.jsx | 423 ++++++++++++++--- .../src/components/ToolBar/ToolBar.css | 416 +--------------- .../src/components/ToolBar/ToolBar.jsx | 443 +----------------- frontend/__pycache__/app.cpython-39.pyc | Bin 0 -> 11641 bytes 18 files changed, 944 insertions(+), 939 deletions(-) create mode 100644 backend/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/__pycache__/main.cpython-311.pyc create mode 100644 backend/api/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/api/__pycache__/route.cpython-311.pyc create mode 100644 backend/core/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/core/__pycache__/config.cpython-311.pyc create mode 100644 backend/core/__pycache__/items.cpython-311.pyc create mode 100644 backend/core/models/chat_history.py create mode 100644 backend/tools/__pycache__/__init__.cpython-311.pyc create mode 100644 backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc create mode 100644 backend/tools/__pycache__/save_input_to_json.cpython-311.pyc rename data/chat/test/{12312.jsonl => 12331212.jsonl} (100%) create mode 100644 frontend/__pycache__/app.cpython-39.pyc diff --git a/backend/__pycache__/__init__.cpython-311.pyc b/backend/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9a6da1c4c670e9732b4266523610d1a70c4bd32 GIT binary patch literal 133 zcmZ3^%ge<81g>|sXM*U*AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFA4p`f&%@d z#N_PMycGTT_{_Y_lK6PNg34bUHo5sJr8%i~MXW%DAft-;fy4)9Mn=XD3^1aI87Kw- DsdF0q literal 0 HcmV?d00001 diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4432136fe5579b3d2cda1890556f8c291359a68b GIT binary patch literal 603 zcmY*UJ#Q015S_hU-`NQ!k`oGp5J5sn=fXz?O@yETga{E*3M4nD!@CZL?@Q}D0~T6Qi&W6YRL`P+qQ1LAg1x#s;D> z3L_v)UhS`H*~Ce;!0K4o*?LngF@o_mzCO7$rpUx7nY^}-_?IcVihTDzoSZoFN6yw@^yryz;tz5phSWXeaN_t2<%Lvq0RJR}iHCHu!|w zk`h7|a4o;+bGV*g^o3a;9pqbfZnmaoYi71abZG)%zoBsgjibBAw{vJr!JdJggT0{Z vABQKjnbYQ+HAZ`S&DqB2$%ki0^o#xdR?eI$b7suJr0``0*j2>KO_cQy%a@x6 literal 0 HcmV?d00001 diff --git a/backend/api/__pycache__/__init__.cpython-311.pyc b/backend/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f71c6104de8879b3e8d8e92ae3a9028dd2a60213 GIT binary patch literal 137 zcmZ3^%ge<81oswf%>>bpK?DpiLK&agfQ;!3DGb33nv8xc8H$*I{LdiCUo!fM1qJ#^ ziOJcic_~14rha^UW?p7Ve7s&k){ literal 0 HcmV?d00001 diff --git a/backend/api/__pycache__/route.cpython-311.pyc b/backend/api/__pycache__/route.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3de0bfffdcd26941652a3b1363aae786ca121115 GIT binary patch literal 1102 zcmZuu&1(};5TCcZ*?euR2?#d9q^3O>#Kd?LL=jJ-6luUiF3UFiTBDon`u0U=^-$X4 z2YByE=&2g~6TEozxPb^^Pl6YFiv%w{Id9vv*)(tG&CYxCn|bq_`J7560N3#6x8^4a z;I}x8mwaN3exR`jCYZ#67O4{grew;N)ROCRU@Ml=iqs>49kJ9_v>qiO0VM|*`E{HR z>MiW9_>$=B4(`!SRF8mQZ~27(4TPEOVCP@U zPhn4ftJI)NmRStrN`#5Yxgv1kXue`NPG!|-yr69p?Pi4uW@Tr?PY1;Y#hld2E?|rp zgL@6tAN;o9p9-0!{l~p@wV$qf$#bGqM~H>ZLWSG5rLP(c`OfH0L!ib>v{jHDsJg)Bgq=d==Vv~mBT2_@wmHinNTA(|r~($+)n2mLPZm9;801+lrTplc z;c{#W6UF$6hRtZX$!W`F5pj_dl-%;nr&v@VE*~S#kDZA&`fAW@<|~fv@}RrEjM;+f zSQ-&$jE-V=TDIA-=v|gYhnSxG9o0}Kgbd({7nT7md0`pAJufT+xb21IS3L86{zL9# zZc81iAjRJ0wsT%8-y?-SDGViv-1>5T2>3fwe`Sys#@=B<1ZB6go_3>0%6(E6L5l*t ND1epFO>iAB{0|$22$lc< literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/__init__.cpython-311.pyc b/backend/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d3b742d0c02cb2bfa460cce44a26570898d076c GIT binary patch literal 138 zcmZ3^%ge<81j3wKGC}lX5CH>>P{wCAAY(d13PUi1CZpd2X I#0(Sz0NEHD(EtDd literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/config.cpython-311.pyc b/backend/core/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..356e5ffc60d9b3277b6d3298ec2ef548e9a2f3bc GIT binary patch literal 1988 zcmah~-D}%c6u+`%$CmBbZW22m%jYOtD6!Ke&C<}0x~=ljI&rYP(TagktlPw?za}eb z>xZJj9@3FP9$IE-2N{fxQYd-c7=!%@QV`VBQ(?4Q-#ogfKJ8p7&g!(Vt8>r&-E)r4 z@jd70`o}(;FUxn^evK*MCB1rR!5#HICuv|uru#0&^%2c z2f_~QdI+|UVcKICe7-#%g6Q#~c)hI&--UP&_@%K&qN_fHTD_B)b4YT1g(UYw2l#|( z58exJd0XS~4E9NF*DV`){#|;I`6Ul#)&osH_tAHbw_}2p7p&U<*NS=5O7f8tyvw6R zagbXMjrv~_mrJ9RNiU1a8mzv2Nt6Vsq~PLam5u?=HlvcYnci2ER*5j;DGBioIEZtd zZW>Bd>k)X@5xqll9!a13hy^XVj##vT1u4y&aokc{tJRUXq$qFNdn9kG{jSxF&EMTz zTgxbU?Go}n?B@eGATcDZd^>Kp6U+wz`7K!pP{2ZAKPjIa8X*yZ$ zSyPm9WhNdM%eh#ojEiC}mMszJvn}Q$_yT=xJ;T&ESj_; zZ(x&_L`AGJ6-88VY`sz{R(+N18ZLHq!`V9~&Can`Y#jOwe5TSIOxAMp2)%-E=ye}twj5s(v)6G^if7?yz>3#$tFiJe znS2ix3jwH@z|_O%S#)SezI#g>p3tL{Ms!l6JL_EoJL$VuwZZt_px%`*x)SPAy?eOk zc;MAWPH7*f^={7S<}|vi-rHY$zm}`5JjiJup8aW4ADA@;X7%1VqjyfDqxHz(&dS}K zHgs}tN{`GKk(sBFbNi8VdgQzjIseY)+9Zgi}#_PHJ& zRWH>674%~#)XVjb_iDHGjxjY^5BF;QaXlOdb#!R=tR5YQpKX&o)I2OHY(dNw1z{r$ zvX|fg{OA2AFLnuk{_DM&p$&3Vy|}mc;+rQeqQAcU`uPu!nj(|Vu8GQMQ0@S0v#ksl zP(HUJJIT&%-#{|CQ%JOHIjeQrp#MtwO>DZ$qKu(CqM*0(Z+f6Ht*=TZr7&G(nRHJ# z(f1HGNZ1fz!@yp9&e;9D5{RUs`b=Sx7JE^G@!LoqaB4hx0Idv zJGZxQKMfw=4<6Tp9~gvUM(~soJfr#{>y%&hZ6#|{x_?+lBL*7L&`84_p-wi?+w5;5 z`{zH!IP!OHP2ZW>o>4u|oW8BPB!G$szD)8hsWl^s9OOx866UG1EXgecGT;YfWl+5#0+BtpxrH Dm~8nI literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/items.cpython-311.pyc b/backend/core/__pycache__/items.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd1b335b9d54703ad11b83c85421ee93883e153d GIT binary patch literal 1912 zcma)--ESL35Ww&3wc{jC?Zj>qm$XsUK(U|=LLgqMO0;g=qPCDa7Li4Cy7;!~sppU0 zhai!V+|o7M+;PnPS7bw!a?Hnc<*dxcYf0aQL0) z-K@-K(yU;B6TBoRH%#p>9GKmnenpc&AeEpwD&dG@m(a=4oZ|=Q9S3%ZpIS=*=z?wz z9K^#7#?`%7q z_-zfmzm9Kj;C&5z#}*&pf(^Xi-uw1CW=8|lZZn;AOjiTbVKX7Za9&*Eokr`Cv#fj? zo>!nGA5dPv6V>bYD)~ig<(hfr86eH3;3+#fwcx7DSIvc4Y-`>9+?xEkW@|Ky838hctwUFfvm5)UrO0+; zG$(P^Yv>*v&f2$^tc4X4%Z!R+u?o|b{Eg~^G5xlvYK{#M|7(04~FAbM?zPs`#qV&o%=TxX)6sCJ?{hzbxD#OgchDc01p zoqcRJ+c&}{&%rd;m&}N;kC$OeI*=U`Jp?AojUQ;B=Ae>fM(V`3PK4u;D;y_zyNh~64RJ#oD! zUh0YGj^{tpoKqho5XYi}`2S+M_h?7wdeIy2qwXQSd#Kbslf_976&qT3pG#9f<7KNW5=JlQdhP0M^zB>ye;fiT?iSe8$Z z*2+sNQ>#-q>Dk8Q@ILb#tR1RDBKdvb(ovFx)w+|W=`!^qdY+W2J?MF|?(xxs^g8u? Qt%m=!Xa4$+c{`}T0p7ZbJ^%m! literal 0 HcmV?d00001 diff --git a/backend/core/models/chat_history.py b/backend/core/models/chat_history.py new file mode 100644 index 0000000..dddb861 --- /dev/null +++ b/backend/core/models/chat_history.py @@ -0,0 +1,158 @@ +from typing import List, Dict, Any, Optional +from datetime import datetime +from pathlib import Path +from pydantic import BaseModel, Field +import json + + +class Message(BaseModel): + """消息类,代表JSONL文件中的一行消息内容""" + name: str = Field(..., description="发言者名称") + is_user: bool = Field(..., description="是否为用户消息(true=用户,false=AI/角色)") + is_system: bool = Field(False, description="是否为系统消息(系统消息在文本导出时会被排除)") + send_date: str = Field(default_factory=lambda: str(int(datetime.now().timestamp() * 1000)), + description="发送时间戳(Unix毫秒数)") + mes: str = Field(..., description="消息正文内容") + extra: Dict[str, Any] = Field(default_factory=dict, description="额外信息,包含推理内容、API、模型等") + swipes: List[str] = Field(default_factory=list, description="备选回复列表") + swipe_id: int = Field(0, description="当前选中的备选索引(0=第一条)") + swipe_info: List[Dict[str, Any]] = Field(default_factory=list, description="每个备选回复的生成信息") + title: str = Field("", description="消息标题,用于消息摘要或分支标记") + force_avatar: Optional[str] = Field(None, description="强制头像路径") + variables: List[Any] = Field(default_factory=list, description="变量值数组") + variables_initialized: List[bool] = Field(default_factory=list, description="变量初始化状态数组") + is_ejs_processed: List[bool] = Field(default_factory=list, description="EJS模板处理状态数组") + gen_started: Optional[str] = Field(None, description="生成开始时间戳(Unix毫秒数)") + gen_finished: Optional[str] = Field(None, description="生成结束时间戳(Unix毫秒数)") + + +class ChatMetadata(BaseModel): + """聊天元数据模型,代表JSONL文件的第一行内容""" + integrity: str = Field("", description="完整性校验哈希值(UUID格式)") + chat_id_hash: str = Field("", description="聊天ID哈希值") + note_prompt: str = Field("", description="笔记提示词") + note_interval: int = Field(0, description="笔记间隔(整数)") + note_position: int = Field(0, description="笔记位置(整数)") + note_depth: int = Field(0, description="笔记深度(整数)") + note_role: int = Field(0, description="笔记角色(整数,0=用户,1=助手)") + extensions: Dict[str, Any] = Field(default_factory=dict, description="扩展信息,如LittleWhiteBox等") + timedWorldInfo: Dict[str, Any] = Field(default_factory=dict, description="定时世界信息") + variables: Dict[str, Any] = Field(default_factory=dict, description="变量字典") + tainted: bool = Field(False, description="是否被污染") + lastInContextMessageId: int = Field(-1, description="上下文中最后一条消息的ID") + + +class ChatFile(BaseModel): + """聊天文件类,包含元数据和消息列表""" + user_name: str = Field("User", description="用户名") + character_name: str = Field("Assistant", description="角色名") + create_date: str = Field(default_factory=lambda: datetime.now().isoformat(), description="创建日期(ISO 8601格式)") + chat_metadata: ChatMetadata = Field(default_factory=ChatMetadata, description="聊天元数据") + messages: List[Message] = Field(default_factory=list, description="消息列表") + + class Config: + arbitrary_types_allowed = True + + +def load_chat_file_data(chat_name: str, role_name: str, base_path: Path = None) -> Dict[str, Any]: + """ + 从文件系统加载聊天原始数据 + + 参数: + chat_name: 聊天名称 + role_name: 角色名称 + base_path: 基础路径,默认为项目数据目录 + + 返回: + dict: 包含元数据和消息列表的原始数据字典 + """ + # 设置默认基础路径 + if base_path is None: + from backend.core.config import settings + base_path = settings.DATA_PATH / "chat" + + # 构建文件路径 + file_path = base_path / role_name / f"{chat_name}.jsonl" + + # 检查文件是否存在 + if not file_path.exists(): + raise FileNotFoundError(f"聊天文件不存在: {file_path}") + + # 读取文件内容 + result = { + "user_name": "User", + "character_name": role_name, + "create_date": datetime.now().isoformat(), + "chat_metadata": {}, + "messages": [] + } + + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + try: + # 解析JSON行 + line_data = json.loads(line.strip()) + + # 添加到消息列表 + result["messages"].append(line_data) + except json.JSONDecodeError: + continue + + return result + + +def create_chat_file_from_data(data: Dict[str, Any]) -> ChatFile: + """ + 从原始数据创建ChatFile对象 + + 参数: + data: 包含元数据和消息列表的原始数据字典 + + 返回: + ChatFile: 创建的聊天文件对象 + """ + # 提取元数据 + metadata = data.get("chat_metadata", {}) + chat_metadata = ChatMetadata(**metadata) + + # 处理消息列表 + messages = [] + for msg_data in data.get("messages", []): + # 转换为Message对象 + message = Message( + name=msg_data.get('name', ''), + is_user=msg_data.get('is_user', False), + send_date=msg_data.get('send_date', ''), + mes=msg_data.get('content', ''), + swipes=msg_data.get('swipes', []), + swipe_id=msg_data.get('swipes_id', 0) + ) + messages.append(message) + + # 创建并返回ChatFile对象 + return ChatFile( + user_name=data.get("user_name", "User"), + character_name=data.get("character_name", "Assistant"), + create_date=data.get("create_date", datetime.now().isoformat()), + chat_metadata=chat_metadata, + messages=messages + ) + + +def load_chat_file(chat_name: str, role_name: str, base_path: Path = None) -> ChatFile: + """ + 从文件系统加载聊天数据并创建ChatFile对象 + + 参数: + chat_name: 聊天名称 + role_name: 角色名称 + base_path: 基础路径,默认为项目数据目录 + + 返回: + ChatFile: 加载的聊天文件对象 + """ + # 加载原始数据 + data = load_chat_file_data(chat_name, role_name, base_path) + + # 创建ChatFile对象 + return create_chat_file_from_data(data) diff --git a/backend/tools/__pycache__/__init__.cpython-311.pyc b/backend/tools/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..221097a878f3184a58a928803c18e0d09bca5957 GIT binary patch literal 139 zcmZ3^%ge<81j3wKGC}lX5CH>>P{wCAAY(d13PUi1CZpd&ryk0@&FAkgB{FKt1RJ$TppmLCb#r#0x12ZEd;|B&9 JQN#=s0{{mN99jSX literal 0 HcmV?d00001 diff --git a/backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc b/backend/tools/__pycache__/get_all_role_and_chat.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e701c2208f504a89001ef05ff97445dfba42a8a GIT binary patch literal 2326 zcma)7eN0nV6u+;1Ew3L4WyKXI_*~gX(Mc993PTMW5Vt5S;!Gr28peBIv9#p9*NLVO z{s0A6Ms-Y3&~+Ion|%1k!eCUlEb;$W(!}N^BrfB%@W)~kvxNB9&TXY11`}^@&pr2i z{LZ;w=iSZD)*@)7(SZG5HA4U3$!1f%V4j@?W&|;aAsnckXe0>4C>W(f(XMP%wv&w{ zfmot#(;Bq_PNFg7MIAzoS&WKRvJ^|QRKcbSwJ6egM%_y`YM87BWTDm-U}PaAb+gOa zVsBl?E*6DER@<8ei9BxSfi|CQ5+pUp3U1EXoWukMuSN;~XPAy4JL*D0n%+;OO_J;# z;O-@7{0xSTgpW26;E!Q>>~=+* z#{n65t`)RAGa~mbM3=sc-0lV7_2Jpb=v*?C>>G^Uy^jO-1VFwvHg-ZavNb9nfVFW$iThG&``pxf_59Q!oQf9%a{7(*~J%9d7?M!yOd0|g1)HU z*T%b?4tFlbrXvFn<^KB#J-A@uOD?oqk1Zo3^YZ22*N1OZ&~=tEmOhpYJIJ zywb0ln$H1fso-)s_|jHZur)axHqPZ>ZB0(bh70n(jtf%$mKv@Q(C{2}7DLbZK`$`-dBn52LtdXTb)D62?W!#Vl=+2EqmirPJs zCLqv**M!J@KIQ5h(p2g&jfSuT?0fRVT3 zv0?YJVRz7A4jIfb^oA-wPWn|bMV==ANse*IbHy{z?d^`C>}=~wZoas^Z0eoq5*VNd za}S1c4~n@5!$q`D7gM9cw|_Ws^Tdt%@%k7d4M*a{R~>%ytr5>P&(-c>fKlcjnJS*% zvtU{-sS-=7o|w#!O(n~wlBomJx!vREULV3{IviE=t0xrkm;~!I=lfv zjJslpFj&Ku1HRf(>v%=b`0mEWN{$KkO#|s~k`=u{%&YN#G0_ME^al-p8Wv9nPA6tT zeQiizE9z^*dSm~w#BT#yOa=aUq>w(Z=LC-n(OC=7_**Nfg$o^aXDf&A42P?wMDeU3fQXkg)m5jeY@b)1I>zCL$Kiq)zYagc zHyZOhfjp-w+g*&?!5-m?VTwPkJO?DEBnTpm3dGDFMup;59rj3{=RF$I75l#q>OSb9 gV&wZo-l*XJWXd?zJYC?g3+>t;Lm+!4;mi>Be@@bqP5=M^ literal 0 HcmV?d00001 diff --git a/backend/tools/__pycache__/save_input_to_json.cpython-311.pyc b/backend/tools/__pycache__/save_input_to_json.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7aba5e7a98d8e174359c5e444f785751de132b6 GIT binary patch literal 5588 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 Dict[str, List[str]]: 读取配置目录下的所有子文件夹,并收集每个子文件夹中的 JSONL 文件 返回: - dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表 + dict: 字典结构,键是文件夹名称,值是该文件夹中的 JSONL 文件列表(仅文件名,无路径和后缀) """ result = {} @@ -33,7 +33,8 @@ def get_all_role_and_chat() -> Dict[str, List[str]]: # 遍历子文件夹中的所有文件 for file in entry.iterdir(): if file.is_file() and file.suffix == '.jsonl': - jsonl_files.append(str(file)) + # 使用 file.stem 获取不带后缀的文件名 + jsonl_files.append(file.stem) print(f" 找到文件: {file.name}") # 调试信息 # 如果该文件夹中有 JSONL 文件,则添加到结果中 diff --git a/data/chat/test/12312.jsonl b/data/chat/test/12331212.jsonl similarity index 100% rename from data/chat/test/12312.jsonl rename to data/chat/test/12331212.jsonl diff --git a/frontend-react/src/components/RoleSelector/RoleSelector.css b/frontend-react/src/components/RoleSelector/RoleSelector.css index e39d7bc..520e3ed 100644 --- a/frontend-react/src/components/RoleSelector/RoleSelector.css +++ b/frontend-react/src/components/RoleSelector/RoleSelector.css @@ -1,95 +1,419 @@ -/* ==================== 下拉框样式 ==================== */ +/* ==================== 角色选择器容器 ==================== */ -/* 角色选择器容器 */ .role-selector { width: 100%; + height: 100%; } -/* 下拉框容器 */ -.dropdown-container { - position: relative; +/* ==================== 角色面板样式 ==================== */ + +.role-panel { width: 100%; - max-width: 300px; + height: 100%; } -/* 下拉框触发器 */ -.dropdown-trigger { +.panel-controls { display: flex; justify-content: space-between; align-items: center; - padding: 8px 12px; - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; + margin-bottom: 20px; +} + +.search-box { + flex: 1; + margin-right: 16px; +} + +.search-box input { + width: 100%; + padding: 10px 16px; + border: 1px solid #e0e0e0; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s ease; +} + +.search-box input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +.add-button { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + background-color: #1890ff; + color: white; + border: none; + border-radius: 6px; cursor: pointer; font-size: 14px; + transition: all 0.2s ease; } -.dropdown-arrow { - margin-left: 8px; - font-size: 12px; +.add-button:hover { + background-color: #40a9ff; + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(24, 144, 255, 0.2); } -/* 下拉菜单 */ -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: white; - border: 1px solid #ddd; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - z-index: 10; - max-height: 200px; - overflow-y: auto; - margin-top: 5px; +.add-button .icon { + font-size: 16px; + font-weight: bold; } -/* 下拉菜单项 */ -.dropdown-item { - padding: 8px 12px; +.panel-list-container { + display: flex; + flex-direction: column; + height: calc(100% - 80px); +} + +.loading-container { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + gap: 12px; + color: #666; +} + +.loading-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid rgba(24, 144, 255, 0.2); + border-radius: 50%; + border-top-color: #1890ff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.empty-state { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: #999; +} + +/* ==================== 角色卡片网格布局 ==================== */ + +.role-cards-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +/* ==================== 角色卡片样式 ==================== */ + +.role-card { + border-radius: 8px; cursor: pointer; position: relative; - font-size: 14px; + transition: all 0.2s ease; + background-color: #fff; + border: 1px solid #e0e0e0; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } -.dropdown-item:hover { +.role-card:hover { background-color: #f5f5f5; + border-color: #1890ff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); } -.dropdown-item.selected { +.role-card.selected { + background-color: #e6f7ff; + border-color: #1890ff; + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2); +} + +.role-card-header { + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f0f0f0; +} + +.role-name { + font-size: 15px; + color: #333; + font-weight: 500; +} + +.role-card.selected .role-name { + color: #1890ff; +} + +.role-chat-count { + font-size: 12px; + color: #999; + background-color: #f0f0f0; + padding: 4px 10px; + border-radius: 12px; +} + +.role-card-actions { + display: flex; + justify-content: flex-end; + padding: 8px 12px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.role-card:hover .role-card-actions, +.role-card.clicked .role-card-actions { + opacity: 1; +} + +.action-button { + width: 28px; + height: 28px; + border-radius: 4px; + border: none; + background-color: #f5f5f5; + color: #666; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s ease; +} + +.action-button:hover { background-color: #e6f7ff; color: #1890ff; } -/* 嵌套下拉框 */ -.nested-dropdown { - position: absolute; - left: 100%; - top: 0; - width: 100%; - background-color: white; - border: 1px solid #ddd; +.edit-button:hover { + background-color: #e6f7ff; + color: #1890ff; +} + +.delete-button:hover { + background-color: #fff1f0; + color: #ff4d4f; +} + +.role-card-header input { + padding: 6px 10px; + border: 1px solid #d9d9d9; border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - z-index: 11; - max-height: 200px; + font-size: 14px; + width: 150px; +} + +.role-card-header input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +/* ==================== 聊天列表样式 ==================== */ + +.chat-list { + margin-top: 0; + border-top: 1px solid #e0e0e0; + padding: 12px; + background-color: #fafafa; + animation: slideDown 0.2s ease; + max-height: 300px; overflow-y: auto; } -.nested-dropdown .dropdown-item { - padding-left: 20px; +.chat-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + margin-bottom: 8px; } -/* 工具栏下拉框容器 */ -.toolbar-dropdown { - margin-bottom: 15px; -} - -.toolbar-dropdown label { - display: block; - margin-bottom: 5px; - font-size: 14px; +.chat-list-header span { + font-size: 13px; + font-weight: 500; color: #333; } + +.add-chat-button { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background-color: #1890ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.add-chat-button:hover { + background-color: #40a9ff; +} + +.add-chat-button .icon { + font-size: 14px; + font-weight: bold; +} + +.chat-item { + padding: 10px 12px; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + justify-content: space-between; + align-items: center; + border-radius: 4px; + margin-bottom: 4px; +} + +.chat-item:hover { + background-color: #f0f0f0; +} + +.chat-item.selected { + background-color: #e6f7ff; + color: #1890ff; +} + +.chat-name { + font-size: 13px; + color: #333; +} + +.chat-item.selected .chat-name { + color: #1890ff; + font-weight: 500; +} + +.chat-actions { + display: flex; + gap: 6px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.chat-item:hover .chat-actions { + opacity: 1; +} + +.chat-content input { + padding: 6px 10px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 13px; + width: 180px; +} + +.chat-content input:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); +} + +/* ==================== 模态框样式 ==================== */ + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal-content { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 400px; + padding: 24px; + animation: slideDown 0.3s ease; +} + +@keyframes slideDown { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.modal-content h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 500; + color: #333; +} + +.modal-content p { + margin: 0 0 24px 0; + font-size: 14px; + color: #666; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.modal-button { + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s ease; + border: none; +} + +.cancel-button { + background-color: #f5f5f5; + color: #666; +} + +.cancel-button:hover { + background-color: #e6e6e6; + color: #333; +} + +.confirm-button { + background-color: #ff4d4f; + color: white; +} + +.confirm-button:hover { + background-color: #ff7875; +} diff --git a/frontend-react/src/components/RoleSelector/RoleSelector.jsx b/frontend-react/src/components/RoleSelector/RoleSelector.jsx index a520d6b..9ad3ceb 100644 --- a/frontend-react/src/components/RoleSelector/RoleSelector.jsx +++ b/frontend-react/src/components/RoleSelector/RoleSelector.jsx @@ -1,94 +1,409 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import './RoleSelector.css'; const RoleSelector = ({ onRoleChange, onChatChange }) => { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [roleData, setRoleData] = useState({}); const [selectedRole, setSelectedRole] = useState(null); const [selectedChat, setSelectedChat] = useState(null); + const [hoveredRole, setHoveredRole] = useState(null); + const [clickedRole, setClickedRole] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [editingRole, setEditingRole] = useState(null); + const [editingChat, setEditingChat] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); + const [deleteType, setDeleteType] = useState(null); + const panelRef = useRef(null); // 获取角色和聊天数据 - useEffect(() => { - const fetchRoleData = async () => { - try { - console.log('开始获取角色数据...'); - const response = await fetch('/api/get_all_role_and_chat'); - console.log('响应状态:', response.status); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const fetchRoleData = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/tool_bar/get_all_role_and_chat', { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Expires': '0' } + }); + const data = await response.json(); + setRoleData(data); + } catch (error) { + console.error('获取角色数据失败:', error); + } finally { + setIsLoading(false); + } + }; - const data = await response.json(); - console.log('获取到的数据:', data); - setRoleData(data); - } catch (error) { - console.error('获取角色数据失败:', error); + // 组件挂载时获取数据 + useEffect(() => { + fetchRoleData(); + }, []); + + // 点击外部关闭面板 + useEffect(() => { + const handleClickOutside = (event) => { + if (panelRef.current && !panelRef.current.contains(event.target)) { + setHoveredRole(null); + setClickedRole(null); + setEditingRole(null); + setEditingChat(null); + setShowDeleteConfirm(null); } }; - fetchRoleData(); + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; }, []); // 处理角色选择 const handleRoleSelect = (role) => { setSelectedRole(role); - setSelectedChat(null); if (onRoleChange) { onRoleChange(role); } + + // 如果该角色有聊天记录,默认选择第一个 + if (roleData[role] && roleData[role].length > 0) { + const firstChat = roleData[role][0]; + setSelectedChat(firstChat); + if (onChatChange) { + onChatChange(role, firstChat); + } + } else { + setSelectedChat(null); + } }; // 处理聊天选择 const handleChatSelect = (chat) => { setSelectedChat(chat); - setIsDropdownOpen(false); + setHoveredRole(null); + setClickedRole(null); if (onChatChange) { onChatChange(selectedRole, chat); } }; + // 处理角色卡片点击 + const handleRoleCardClick = (role) => { + if (clickedRole === role) { + setClickedRole(null); + } else { + setClickedRole(role); + handleRoleSelect(role); + } + }; + + // 处理搜索 + const handleSearchChange = (e) => { + setSearchTerm(e.target.value); + }; + + // 处理角色编辑 + const handleEditRole = (e, role) => { + e.stopPropagation(); + setEditingRole(role); + }; + + // 处理角色重命名 + const handleRenameRole = (e, oldName) => { + e.stopPropagation(); + const newName = e.target.value; + if (newName && newName !== oldName) { + const newRoleData = { ...roleData }; + const chats = newRoleData[oldName]; + delete newRoleData[oldName]; + newRoleData[newName] = chats; + setRoleData(newRoleData); + + if (selectedRole === oldName) { + setSelectedRole(newName); + if (onRoleChange) { + onRoleChange(newName); + } + } + } + setEditingRole(null); + }; + + // 处理聊天编辑 + const handleEditChat = (e, chat) => { + e.stopPropagation(); + setEditingChat(chat); + }; + + // 处理聊天重命名 + const handleRenameChat = (e, oldName) => { + e.stopPropagation(); + const newName = e.target.value; + if (newName && newName !== oldName) { + const newRoleData = { ...roleData }; + const chatIndex = newRoleData[selectedRole].indexOf(oldName); + if (chatIndex !== -1) { + newRoleData[selectedRole][chatIndex] = newName; + setRoleData(newRoleData); + + if (selectedChat === oldName) { + setSelectedChat(newName); + if (onChatChange) { + onChatChange(selectedRole, newName); + } + } + } + } + setEditingChat(null); + }; + + // 处理删除确认 + const handleDeleteClick = (e, type, name) => { + e.stopPropagation(); + setDeleteType(type); + setShowDeleteConfirm(name); + }; + + // 确认删除 + const confirmDelete = () => { + if (deleteType === 'role') { + const newRoleData = { ...roleData }; + delete newRoleData[showDeleteConfirm]; + setRoleData(newRoleData); + + if (selectedRole === showDeleteConfirm) { + setSelectedRole(null); + setSelectedChat(null); + if (onRoleChange) { + onRoleChange(null); + } + } + } else if (deleteType === 'chat') { + const newRoleData = { ...roleData }; + const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm); + if (chatIndex !== -1) { + newRoleData[selectedRole].splice(chatIndex, 1); + setRoleData(newRoleData); + + if (selectedChat === showDeleteConfirm) { + setSelectedChat(null); + if (onChatChange) { + onChatChange(selectedRole, null); + } + } + } + } + setShowDeleteConfirm(null); + setDeleteType(null); + }; + + // 取消删除 + const cancelDelete = () => { + setShowDeleteConfirm(null); + setDeleteType(null); + }; + + // 处理新增角色 + const handleAddRole = () => { + const newRole = '新角色'; + const newRoleData = { ...roleData }; + newRoleData[newRole] = []; + setRoleData(newRoleData); + setEditingRole(newRole); + }; + + // 处理新增聊天 + const handleAddChat = () => { + if (!selectedRole) return; + + const newChat = '新聊天'; + const newRoleData = { ...roleData }; + newRoleData[selectedRole].push(newChat); + setRoleData(newRoleData); + setEditingChat(newChat); + }; + + // 过滤角色 + const filteredRoles = Object.keys(roleData).filter(role => + role.toLowerCase().includes(searchTerm.toLowerCase()) + ); + return ( -
-
-
setIsDropdownOpen(!isDropdownOpen)} - > - {selectedRole || '选择角色'} - +
+
+
+
+ +
+
- {isDropdownOpen && ( -
- {Object.keys(roleData).map(role => ( -
handleRoleSelect(role)} - > - {role} - {selectedRole === role && ( -
- {roleData[role].map(chat => ( -
{ - e.stopPropagation(); - handleChatSelect(chat); +
+ {isLoading ? ( +
+
+ 加载中... +
+ ) : filteredRoles.length === 0 ? ( +
+

没有找到匹配的角色

+
+ ) : ( +
+ {filteredRoles.map(role => ( +
handleRoleCardClick(role)} + onMouseEnter={() => setHoveredRole(role)} + onMouseLeave={() => setHoveredRole(null)} + > +
+ {editingRole === role ? ( + handleRenameRole(e, role)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameRole(e, role); + } }} - > - {chat} -
- ))} + onClick={(e) => e.stopPropagation()} + /> + ) : ( +
{role}
+ )} + +
+ {roleData[role] ? roleData[role].length : 0} 个聊天 +
- )} -
- ))} -
- )} + +
+ {editingRole !== role && ( + <> + + + + )} +
+ + {/* 聊天列表 */} + {(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && ( +
+
+ 聊天记录 + +
+ + {roleData[role].map(chat => ( +
{ + e.stopPropagation(); + handleChatSelect(chat); + }} + > +
+ {editingChat === chat ? ( + handleRenameChat(e, chat)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRenameChat(e, chat); + } + }} + onClick={(e) => e.stopPropagation()} + /> + ) : ( +
{chat}
+ )} +
+ +
+ {editingChat !== chat && ( + <> + + + + )} +
+
+ ))} +
+ )} +
+ ))} +
+ )} +
+ + {/* 删除确认对话框 */} + {showDeleteConfirm && ( +
+
+

确认删除

+

确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" 吗?

+
+ + +
+
+
+ )}
); }; diff --git a/frontend-react/src/components/ToolBar/ToolBar.css b/frontend-react/src/components/ToolBar/ToolBar.css index a24c855..43920cf 100644 --- a/frontend-react/src/components/ToolBar/ToolBar.css +++ b/frontend-react/src/components/ToolBar/ToolBar.css @@ -59,7 +59,22 @@ color: #fff; } -/* ==================== 弹出面板样式 ==================== */ +/* ==================== 弹出面板通用样式 ==================== */ + +.close-panel-button { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + color: #666; + padding: 5px 10px; + margin-left: auto; + transition: color 0.2s; +} + +.close-panel-button:hover { + color: #333; +} .panel-overlay { position: fixed; @@ -124,411 +139,12 @@ color: #333; } -.current-info { - display: flex; - gap: 20px; - font-size: 13px; - color: #666; -} - .panel-body { padding: 20px; overflow-y: auto; flex: 1; } -/* ==================== 角色面板样式 ==================== */ - -.role-panel { - max-width: 1200px; -} - -.panel-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; -} - -.search-box { - flex: 1; - margin-right: 16px; -} - -.search-box input { - width: 100%; - padding: 10px 16px; - border: 1px solid #e0e0e0; - border-radius: 6px; - font-size: 14px; - transition: border-color 0.2s ease; -} - -.search-box input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -.add-button { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 20px; - background-color: #1890ff; - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 14px; - transition: all 0.2s ease; -} - -.add-button:hover { - background-color: #40a9ff; - transform: translateY(-1px); - box-shadow: 0 2px 6px rgba(24, 144, 255, 0.2); -} - -.add-button .icon { - font-size: 16px; - font-weight: bold; -} - -.panel-list-container { - display: flex; - flex-direction: column; - height: calc(100% - 80px); -} - -.loading-container { - display: flex; - align-items: center; - justify-content: center; - padding: 40px; - gap: 12px; - color: #666; -} - -.loading-spinner { - display: inline-block; - width: 20px; - height: 20px; - border: 2px solid rgba(24, 144, 255, 0.2); - border-radius: 50%; - border-top-color: #1890ff; - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.empty-state { - display: flex; - align-items: center; - justify-content: center; - padding: 40px; - color: #999; -} - -/* ==================== 角色卡片网格布局 ==================== */ - -.role-cards-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; -} - -/* ==================== 角色卡片样式 ==================== */ - -.role-card { - border-radius: 8px; - cursor: pointer; - position: relative; - transition: all 0.2s ease; - background-color: #fff; - border: 1px solid #e0e0e0; - display: flex; - flex-direction: column; - overflow: hidden; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); -} - -.role-card:hover { - background-color: #f5f5f5; - border-color: #1890ff; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); -} - -.role-card.selected { - background-color: #e6f7ff; - border-color: #1890ff; - box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2); -} - -.role-card-header { - padding: 16px; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #f0f0f0; -} - -.role-name { - font-size: 15px; - color: #333; - font-weight: 500; -} - -.role-card.selected .role-name { - color: #1890ff; -} - -.role-chat-count { - font-size: 12px; - color: #999; - background-color: #f0f0f0; - padding: 4px 10px; - border-radius: 12px; -} - -.role-card-actions { - display: flex; - justify-content: flex-end; - padding: 8px 12px; - opacity: 0; - transition: opacity 0.2s ease; -} - -.role-card:hover .role-card-actions, -.role-card.clicked .role-card-actions { - opacity: 1; -} - -.action-button { - width: 28px; - height: 28px; - border-radius: 4px; - border: none; - background-color: #f5f5f5; - color: #666; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - transition: all 0.2s ease; -} - -.action-button:hover { - background-color: #e6f7ff; - color: #1890ff; -} - -.edit-button:hover { - background-color: #e6f7ff; - color: #1890ff; -} - -.delete-button:hover { - background-color: #fff1f0; - color: #ff4d4f; -} - -.role-card-header input { - padding: 6px 10px; - border: 1px solid #d9d9d9; - border-radius: 4px; - font-size: 14px; - width: 150px; -} - -.role-card-header input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -/* ==================== 聊天列表样式 ==================== */ - -.chat-list { - margin-top: 0; - border-top: 1px solid #e0e0e0; - padding: 12px; - background-color: #fafafa; - animation: slideDown 0.2s ease; - max-height: 300px; - overflow-y: auto; -} - -.chat-list-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 0; - border-bottom: 1px solid #e0e0e0; - margin-bottom: 8px; -} - -.chat-list-header span { - font-size: 13px; - font-weight: 500; - color: #333; -} - -.add-chat-button { - display: flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - background-color: #1890ff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - transition: all 0.2s ease; -} - -.add-chat-button:hover { - background-color: #40a9ff; -} - -.add-chat-button .icon { - font-size: 14px; - font-weight: bold; -} - -.chat-item { - padding: 10px 12px; - cursor: pointer; - transition: background-color 0.2s ease; - display: flex; - justify-content: space-between; - align-items: center; - border-radius: 4px; - margin-bottom: 4px; -} - -.chat-item:hover { - background-color: #f0f0f0; -} - -.chat-item.selected { - background-color: #e6f7ff; - color: #1890ff; -} - -.chat-name { - font-size: 13px; - color: #333; -} - -.chat-item.selected .chat-name { - color: #1890ff; - font-weight: 500; -} - -.chat-actions { - display: flex; - gap: 6px; - opacity: 0; - transition: opacity 0.2s ease; -} - -.chat-item:hover .chat-actions { - opacity: 1; -} - -.chat-content input { - padding: 6px 10px; - border: 1px solid #d9d9d9; - border-radius: 4px; - font-size: 13px; - width: 180px; -} - -.chat-content input:focus { - outline: none; - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); -} - -/* ==================== 模态框样式 ==================== */ - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 2000; - display: flex; - align-items: center; - justify-content: center; - animation: fadeIn 0.2s ease; -} - -.modal-content { - background-color: #fff; - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - width: 90%; - max-width: 400px; - padding: 24px; - animation: slideDown 0.3s ease; -} - -.modal-content h3 { - margin: 0 0 16px 0; - font-size: 16px; - font-weight: 500; - color: #333; -} - -.modal-content p { - margin: 0 0 24px 0; - font-size: 14px; - color: #666; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.modal-button { - padding: 8px 16px; - border-radius: 4px; - font-size: 14px; - cursor: pointer; - transition: all 0.2s ease; - border: none; -} - -.cancel-button { - background-color: #f5f5f5; - color: #666; -} - -.cancel-button:hover { - background-color: #e6e6e6; - color: #333; -} - -.confirm-button { - background-color: #ff4d4f; - color: white; -} - -.confirm-button:hover { - background-color: #ff7875; -} - /* 主内容区域 */ .main-container { margin-top: 50px; diff --git a/frontend-react/src/components/ToolBar/ToolBar.jsx b/frontend-react/src/components/ToolBar/ToolBar.jsx index 1d1eb8f..28188b9 100644 --- a/frontend-react/src/components/ToolBar/ToolBar.jsx +++ b/frontend-react/src/components/ToolBar/ToolBar.jsx @@ -1,58 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useRef } from 'react'; +import RoleSelector from '../RoleSelector/RoleSelector'; import './ToolBar.css'; -const Toolbar = ({ - onRoleChange, - onChatChange -}) => { - const [selectedRole, setSelectedRole] = useState(null); - const [selectedChat, setSelectedChat] = useState(null); +const Toolbar = () => { const [activePanel, setActivePanel] = useState(null); - const [roleData, setRoleData] = useState({}); - const [hoveredRole, setHoveredRole] = useState(null); - const [clickedRole, setClickedRole] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - const [editingRole, setEditingRole] = useState(null); - const [editingChat, setEditingChat] = useState(null); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); - const [deleteType, setDeleteType] = useState(null); const panelRef = useRef(null); - // 获取角色和聊天数据 - const fetchRoleData = async () => { - setIsLoading(true); - console.log('开始获取角色数据...'); - try { - const response = await fetch('/api/tool_bar/get_all_role_and_chat', { - method: 'GET', - headers: { - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Expires': '0' - } - }); - console.log('响应状态:', response.status); - const data = await response.json(); - console.log('获取到的数据:', data); - setRoleData(data); - } catch (error) { - console.error('获取角色数据失败:', error); - } finally { - setIsLoading(false); - } - }; - // 点击外部关闭面板 - useEffect(() => { + React.useEffect(() => { const handleClickOutside = (event) => { if (panelRef.current && !panelRef.current.contains(event.target)) { setActivePanel(null); - setHoveredRole(null); - setClickedRole(null); - setEditingRole(null); - setEditingChat(null); - setShowDeleteConfirm(null); } }; @@ -67,216 +25,15 @@ const Toolbar = ({ if (activePanel === panelName) { setActivePanel(null); } else { - if (panelName === 'role') { - fetchRoleData(); - } setActivePanel(panelName); } }; - // 处理角色选择 - const handleRoleSelect = (role) => { - setSelectedRole(role); - if (onRoleChange) { - onRoleChange(role); - } - - // 如果该角色有聊天记录,默认选择第一个 - if (roleData[role] && roleData[role].length > 0) { - const firstChat = roleData[role][0]; - setSelectedChat(firstChat); - if (onChatChange) { - onChatChange(role, firstChat); - } - } else { - setSelectedChat(null); - } - }; - - // 处理聊天选择 - const handleChatSelect = (chat) => { - setSelectedChat(chat); + // 关闭面板 + const handleClosePanel = () => { setActivePanel(null); - setHoveredRole(null); - setClickedRole(null); - if (onChatChange) { - onChatChange(selectedRole, chat); - } }; - // 处理角色卡片点击 - const handleRoleCardClick = (role) => { - if (clickedRole === role) { - setClickedRole(null); - } else { - setClickedRole(role); - handleRoleSelect(role); - } - }; - - // 处理搜索 - const handleSearchChange = (e) => { - setSearchTerm(e.target.value); - }; - - // 处理角色编辑 - const handleEditRole = (e, role) => { - e.stopPropagation(); - setEditingRole(role); - }; - - // 处理角色重命名 - const handleRenameRole = (e, oldName) => { - e.stopPropagation(); - const newName = e.target.value; - if (newName && newName !== oldName) { - // 这里应该调用API更新角色名 - console.log('重命名角色:', oldName, '->', newName); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chats = newRoleData[oldName]; - delete newRoleData[oldName]; - newRoleData[newName] = chats; - setRoleData(newRoleData); - - // 如果当前选中的是被重命名的角色,更新选中状态 - if (selectedRole === oldName) { - setSelectedRole(newName); - if (onRoleChange) { - onRoleChange(newName); - } - } - } - setEditingRole(null); - }; - - // 处理聊天编辑 - const handleEditChat = (e, chat) => { - e.stopPropagation(); - setEditingChat(chat); - }; - - // 处理聊天重命名 - const handleRenameChat = (e, oldName) => { - e.stopPropagation(); - const newName = e.target.value; - if (newName && newName !== oldName) { - // 这里应该调用API更新聊天名 - console.log('重命名聊天:', oldName, '->', newName); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chatIndex = newRoleData[selectedRole].indexOf(oldName); - if (chatIndex !== -1) { - newRoleData[selectedRole][chatIndex] = newName; - setRoleData(newRoleData); - - // 如果当前选中的是被重命名的聊天,更新选中状态 - if (selectedChat === oldName) { - setSelectedChat(newName); - if (onChatChange) { - onChatChange(selectedRole, newName); - } - } - } - } - setEditingChat(null); - }; - - // 处理删除确认 - const handleDeleteClick = (e, type, name) => { - e.stopPropagation(); - setDeleteType(type); - setShowDeleteConfirm(name); - }; - - // 确认删除 - const confirmDelete = () => { - if (deleteType === 'role') { - // 这里应该调用API删除角色 - console.log('删除角色:', showDeleteConfirm); - - // 更新本地状态 - const newRoleData = { ...roleData }; - delete newRoleData[showDeleteConfirm]; - setRoleData(newRoleData); - - // 如果删除的是当前选中的角色,清空选中状态 - if (selectedRole === showDeleteConfirm) { - setSelectedRole(null); - setSelectedChat(null); - if (onRoleChange) { - onRoleChange(null); - } - } - } else if (deleteType === 'chat') { - // 这里应该调用API删除聊天 - console.log('删除聊天:', showDeleteConfirm); - - // 更新本地状态 - const newRoleData = { ...roleData }; - const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm); - if (chatIndex !== -1) { - newRoleData[selectedRole].splice(chatIndex, 1); - setRoleData(newRoleData); - - // 如果删除的是当前选中的聊天,清空选中状态 - if (selectedChat === showDeleteConfirm) { - setSelectedChat(null); - if (onChatChange) { - onChatChange(selectedRole, null); - } - } - } - } - setShowDeleteConfirm(null); - setDeleteType(null); - }; - - // 取消删除 - const cancelDelete = () => { - setShowDeleteConfirm(null); - setDeleteType(null); - }; - - // 处理新增角色 - const handleAddRole = () => { - // 这里应该调用API创建新角色 - const newRole = '新角色'; - console.log('创建新角色:', newRole); - - // 更新本地状态 - const newRoleData = { ...roleData }; - newRoleData[newRole] = []; - setRoleData(newRoleData); - - // 自动进入编辑模式 - setEditingRole(newRole); - }; - - // 处理新增聊天 - const handleAddChat = () => { - if (!selectedRole) return; - - // 这里应该调用API创建新聊天 - const newChat = '新聊天'; - console.log('创建新聊天:', newChat); - - // 更新本地状态 - const newRoleData = { ...roleData }; - newRoleData[selectedRole].push(newChat); - setRoleData(newRoleData); - - // 自动进入编辑模式 - setEditingChat(newChat); - }; - - // 过滤角色 - const filteredRoles = Object.keys(roleData).filter(role => - role.toLowerCase().includes(searchTerm.toLowerCase()) - ); - return ( <>
@@ -332,187 +89,15 @@ const Toolbar = ({ {/* 角色管理面板 */} {activePanel === 'role' && (
-
+

角色管理

-
- 当前角色: {selectedRole || '未选择'} - 当前聊天: {selectedChat || '未选择'} -
+
-
-
-
- -
- -
- -
- {isLoading ? ( -
-
- 加载中... -
- ) : filteredRoles.length === 0 ? ( -
-

没有找到匹配的角色

-
- ) : ( -
- {filteredRoles.map(role => ( -
handleRoleCardClick(role)} - onMouseEnter={() => setHoveredRole(role)} - onMouseLeave={() => setHoveredRole(null)} - > -
- {editingRole === role ? ( - handleRenameRole(e, role)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleRenameRole(e, role); - } - }} - onClick={(e) => e.stopPropagation()} - /> - ) : ( -
{role}
- )} - -
- {roleData[role] ? roleData[role].length : 0} 个聊天 -
-
- -
- {editingRole !== role && ( - <> - - - - )} -
- - {/* 聊天列表 */} - {(hoveredRole === role || clickedRole === role) && roleData[role] && roleData[role].length > 0 && ( -
-
- 聊天记录 - -
- - {roleData[role].map(chat => ( -
{ - e.stopPropagation(); - handleChatSelect(chat); - }} - > -
- {editingChat === chat ? ( - handleRenameChat(e, chat)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleRenameChat(e, chat); - } - }} - onClick={(e) => e.stopPropagation()} - /> - ) : ( -
{chat}
- )} -
- -
- {editingChat !== chat && ( - <> - - - - )} -
-
- ))} -
- )} -
- ))} -
- )} -
-
-
-
- )} - - {/* 删除确认对话框 */} - {showDeleteConfirm && ( -
-
-

确认删除

-

确定要删除{deleteType === 'role' ? '角色' : '聊天'} "{showDeleteConfirm}" 吗?

-
- - +
@@ -523,6 +108,9 @@ const Toolbar = ({

设置

+

设置内容...

@@ -536,6 +124,9 @@ const Toolbar = ({

帮助

+

帮助内容...

diff --git a/frontend/__pycache__/app.cpython-39.pyc b/frontend/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8481713ddb25b1acbe13b0669c8d88909de26bdc GIT binary patch literal 11641 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 XVwW2*z{e#q8+aF+bAMfIcM1Os3YO$T literal 0 HcmV?d00001