完成后端chatmessage格式修改,可以在前端完成swipe切换显示
This commit is contained in:
@@ -210,39 +210,40 @@ class ChatHistory(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def to_chatbox_format(self) -> List[Dict[str, Any]]:
|
def to_chatbox_format(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
将聊天历史转换为适合前端chatbox显示的格式
|
将聊天历史转换为适合前端chatbox显示的格式
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
List[Dict[str, Any]]: 按floor排序的消息字典列表,每个字典包含:
|
List[Dict[str, Any]]: 按floor排序的消息字典列表,每个字典包含:
|
||||||
{
|
{
|
||||||
"name": str,
|
"name": str,
|
||||||
"is_user": bool,
|
"is_user": bool,
|
||||||
"floor": int,
|
"floor": int,
|
||||||
"mes": str,
|
"mes": str,
|
||||||
"swipes": List[str],
|
"swipes": List[str],
|
||||||
"swipe_id": int
|
"swipe_id": int
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
# 创建消息字典列表
|
# 创建消息字典列表
|
||||||
messages_list = []
|
messages_list = []
|
||||||
for msg in self.messages:
|
for msg in self.messages:
|
||||||
# 获取当前消息内容:优先从swipes数组中获取,如果不存在则使用mes
|
# 获取当前消息内容:优先从swipes数组中获取,如果不存在则使用mes
|
||||||
current_mes = msg.mes
|
current_mes = msg.mes
|
||||||
if msg.swipes and 0 <= msg.swipe_id < len(msg.swipes):
|
if msg.swipes and 0 <= msg.swipe_id < len(msg.swipes):
|
||||||
current_mes = msg.swipes[msg.swipe_id]
|
current_mes = msg.swipes[msg.swipe_id]
|
||||||
|
|
||||||
msg_dict = {
|
msg_dict = {
|
||||||
"name": msg.name,
|
"name": msg.name,
|
||||||
"is_user": msg.is_user,
|
"is_user": msg.is_user,
|
||||||
"floor": msg.floor,
|
"floor": msg.floor,
|
||||||
"mes": current_mes,
|
"mes": current_mes,
|
||||||
"swipes": msg.swipes,
|
"swipes": msg.swipes,
|
||||||
"swipe_id": msg.swipe_id
|
"swipe_id": msg.swipe_id
|
||||||
}
|
}
|
||||||
messages_list.append(msg_dict)
|
messages_list.append(msg_dict)
|
||||||
|
|
||||||
# 按floor排序
|
# 按floor排序
|
||||||
messages_list.sort(key=lambda x: x["floor"])
|
messages_list.sort(key=lambda x: x["floor"])
|
||||||
|
|
||||||
|
return messages_list
|
||||||
|
|
||||||
return messages_list
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
{"integrity": "test-uuid-1", "chat_id_hash": "hash1", "note_prompt": "", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
{"user_name": "User", "character_name": "AI Dungeon Master", "integrity": "uuid-001", "chat_id_hash": "hash-001", "note_prompt": "你是一个经验丰富的D&D地下城主。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 0,"send_date": "1700000000000", "mes": "你好,我是新来的冒险者。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "User", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "你好,我想开始一个新的冒险。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
{"name": "Guide", "is_user": false, "is_system": false, "floor": 1,"send_date": "1700000001000", "mes": "欢迎来到我们的世界!你需要什么帮助?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "欢迎,冒险者。请告诉我你想扮演什么角色?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["欢迎,冒险者。请告诉我你想扮演什么角色?", "你好,旅行者。在这个奇幻世界中,你是谁?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我想成为一名人类战士。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "明白了。作为一名人类战士,你正身处繁华的市集广场。你的下一步行动是?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{"integrity": "test-uuid-2", "chat_id_hash": "hash2", "note_prompt": "探索神秘森林", "note_interval": 5, "note_position": 0, "note_depth": 2, "note_role": 1, "extensions": {}, "timedWorldInfo": {}, "variables": {"location": "forest"}, "tainted": false, "lastInContextMessageId": -1}
|
{"user_name": "Commander", "character_name": "XCOM AI", "integrity": "uuid-003", "chat_id_hash": "hash-003", "note_prompt": "你是一名XCOM基地的中央AI,负责协助指挥官管理外星威胁。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
{"name": "System", "is_user": false, "is_system": true, "floor": 0,"send_date": "1700000002000", "mes": "你进入了一片神秘的森林,周围充满了未知的危险。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Commander", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "报告当前的外星活动情况。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 1,"send_date": "1700000003000", "mes": "我该往哪个方向走?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "警报。我们在非洲检测到异常信号,疑似外星绑架行动正在进行。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
{"name": "Ranger", "is_user": false, "is_system": false, "floor": 2,"send_date": "1700000004000", "mes": "东边有一条小路,但西边似乎有奇怪的声音。", "extra": {}, "swipes": ["东边有一条小路,但西边似乎有奇怪的声音。", "建议往东走,那边比较安全。", "小心西边,可能有野兽。"], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Commander", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "派遣布拉德福上尉带领一个小队去调查。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "收到。正在部署天火运输机。布拉德福上尉已登机。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{"integrity": "test-uuid-3", "chat_id_hash": "hash3", "note_prompt": "酒馆闲聊", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
{"user_name": "Player", "character_name": "Game Master", "integrity": "uuid-002", "chat_id_hash": "hash-002", "note_prompt": "场景:赛博朋克风格的未来城市。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 0,"send_date": "1700000005000", "mes": "有人知道关于龙的消息吗?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "我检查我的义体状态。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
{"name": "Bard", "is_user": false, "is_system": false, "floor": 1,"send_date": "1700000006000", "mes": "听说北边的山脉里有一条红龙,它守护着巨大的宝藏。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "系统自检完成。你的神经接口连接稳定,但义体排异反应指数略有上升。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
{"name": "Warrior", "is_user": false, "is_system": false, "floor": 2,"send_date": "1700000007000", "mes": "别信那些谣言,我上周去过那里,什么都没发现。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejsprocessed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我联系我的黑客朋友,问他知不知道哪里有靠谱的义体医生。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
|
{"name": "Game Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "通讯接通。你的朋友告诉你:'别去连锁店,去太平间后巷找'扳手',他收费公道。'"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{"integrity": "test-uuid-3", "chat_id_hash": "hash3", "note_prompt": "酒馆闲聊", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
{"user_name": "Player", "character_name": "Narrator", "integrity": "uuid-004", "chat_id_hash": "hash-004", "note_prompt": "这是一个文字冒险游戏,你需要描述场景并等待玩家输入。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 0,"send_date": "1700000005000", "mes": "有人知道关于龙的消息吗?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "开始游戏。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
{"name": "Bard", "is_user": false, "is_system": false, "floor": 1,"send_date": "1700000006000", "mes": "听说北边的山脉里有一条红龙,它守护着巨大的宝藏。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "当你睁开眼睛,发现自己身处一艘废弃的飞船中,应急灯闪烁着红光。你手里紧握着一个数据盘。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 3,"send_date": "1700000005000", "mes": "别信那些谣言,我上周去过那里,什么都没发现。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我打开背包看看里面有什么。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []}
|
||||||
{"name": "Warrior", "is_user": false, "is_system": false, "floor": 4,"send_date": "1700000007000", "mes": "别信那些谣言,我上周去过那里,什么都没发现。", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejsprocessed": [], "gen_started": null, "gen_finished": null}
|
{"name": "Narrator", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "背包里只有一把激光手枪,能量槽仅剩10%。还有一张写着'不要相信AI'的纸条。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null}
|
||||||
{"name": "User", "is_user": true, "is_system": false, "floor": 5,"send_date": "1700000005000", "mes": "你要是发现了,那我还去干什么?", "extra": {}, "swipes": [], "swipe_id": 0, "swipe_info": [], "title": "", "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "gen_started": null, "gen_finished": null}
|
|
||||||
|
|||||||
@@ -100,6 +100,21 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 气泡本体 */
|
||||||
|
.message .bubble {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-wrap: break-word;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
/* 新增:确保内容区域可以容纳swipe控件 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
/* 区分左右布局 */
|
/* 区分左右布局 */
|
||||||
.message.user {
|
.message.user {
|
||||||
justify-content: flex-end; /* 用户消息靠右 */
|
justify-content: flex-end; /* 用户消息靠右 */
|
||||||
@@ -352,7 +367,7 @@
|
|||||||
/* ==================== 输入区域 ==================== */
|
/* ==================== 输入区域 ==================== */
|
||||||
|
|
||||||
.chat-input-wrapper {
|
.chat-input-wrapper {
|
||||||
background-color: #fff;
|
background-color: #f9f9f9;
|
||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
height: 62px; /* 明确设置高度 */
|
height: 62px; /* 明确设置高度 */
|
||||||
@@ -478,3 +493,91 @@
|
|||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.options-button {
|
||||||
|
height: 42px;
|
||||||
|
width: 42px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-button:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
height: 42px;
|
||||||
|
width: 42px; /* 添加固定宽度,使按钮为正方形 */
|
||||||
|
padding: 0;
|
||||||
|
background-color: #1890ff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex; /* 添加 flex 布局 */
|
||||||
|
align-items: center; /* 垂直居中 */
|
||||||
|
justify-content: center; /* 水平居中 */
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 18px; /* 设置图标大小 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button:hover {
|
||||||
|
background-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button.stopping {
|
||||||
|
background-color: #ff4d4f; /* 终止状态下的背景色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button.stopping:hover {
|
||||||
|
background-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Swipe控制按钮样式 */
|
||||||
|
.swipe-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-button {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #555;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-button:hover:not(:disabled) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-button:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swipe-counter {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,179 +8,247 @@ const ChatBox = () => {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
// 从 ChatBoxStore 获取状态和方法
|
// 新增:管理每条消息的当前显示的swipe版本
|
||||||
const {
|
const [currentSwipeId, setCurrentSwipeId] = useState({});
|
||||||
messages,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
userName,
|
|
||||||
characterName,
|
|
||||||
updateMessage,
|
|
||||||
isGenerating,
|
|
||||||
sendMessage,
|
|
||||||
stopGeneration
|
|
||||||
} = useChatBoxStore();
|
|
||||||
|
|
||||||
const [inputHeight, setInputHeight] = useState(42);
|
// 从 ChatBoxStore 获取状态和方法
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
userName,
|
||||||
|
characterName,
|
||||||
|
updateMessage,
|
||||||
|
isGenerating,
|
||||||
|
sendMessage,
|
||||||
|
stopGeneration
|
||||||
|
} = useChatBoxStore();
|
||||||
|
|
||||||
// 添加输入框高度自适应处理
|
const [inputHeight, setInputHeight] = useState(42);
|
||||||
const handleInputHeight = (e) => {
|
|
||||||
const textarea = e.target;
|
|
||||||
const newHeight = Math.min(Math.max(textarea.scrollHeight, 42), 300);
|
|
||||||
setInputHeight(newHeight);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理发送或终止
|
// 添加输入框高度自适应处理
|
||||||
const handleSendOrStop = () => {
|
const handleInputHeight = (e) => {
|
||||||
if (isGenerating) {
|
const textarea = e.target;
|
||||||
stopGeneration();
|
const newHeight = Math.min(Math.max(textarea.scrollHeight, 42), 300);
|
||||||
} else {
|
setInputHeight(newHeight);
|
||||||
sendMessage(inputValue);
|
};
|
||||||
setInputValue('');
|
|
||||||
setInputHeight(42);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理键盘事件
|
// 处理发送或终止
|
||||||
const handleKeyDown = (e) => {
|
const handleSendOrStop = () => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (isGenerating) {
|
||||||
e.preventDefault();
|
stopGeneration();
|
||||||
handleSendOrStop();
|
} else {
|
||||||
}
|
sendMessage(inputValue);
|
||||||
};
|
setInputValue('');
|
||||||
|
setInputHeight(42);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendOrStop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// 处理编辑消息
|
// 处理编辑消息
|
||||||
const handleEdit = (message) => {
|
const handleEdit = (message) => {
|
||||||
setEditingId(message.floor);
|
setEditingId(message.floor);
|
||||||
setEditContent(message.mes);
|
setEditContent(message.mes);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存编辑
|
// 保存编辑
|
||||||
const handleSaveEdit = (messageId) => {
|
const handleSaveEdit = (messageId) => {
|
||||||
// 调用 store 中的 updateMessage 方法
|
// 调用 store 中的 updateMessage 方法
|
||||||
updateMessage(messageId, editContent);
|
updateMessage(messageId, editContent);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setEditContent('');
|
setEditContent('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 取消编辑
|
// 取消编辑
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setEditContent('');
|
setEditContent('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:处理swipe切换
|
||||||
|
const handleSwipeChange = (messageId, direction) => {
|
||||||
|
const message = messages.find(m => m.floor === messageId);
|
||||||
|
if (message && message.swipes && message.swipes.length > 0) {
|
||||||
|
const currentIndex = currentSwipeId[messageId] !== undefined
|
||||||
|
? currentSwipeId[messageId]
|
||||||
|
: message.swipe_id;
|
||||||
|
const newIndex = currentIndex + direction;
|
||||||
|
|
||||||
|
if (newIndex >= 0 && newIndex < message.swipes.length) {
|
||||||
|
setCurrentSwipeId(prev => ({
|
||||||
|
...prev,
|
||||||
|
[messageId]: newIndex
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染单条消息
|
// 渲染单条消息
|
||||||
const renderMessage = (message) => {
|
const renderMessage = (message) => {
|
||||||
const isUser = message.is_user;
|
const isUser = message.is_user;
|
||||||
const isEditing = editingId === message.floor;
|
const isEditing = editingId === message.floor;
|
||||||
|
|
||||||
// 根据消息类型设置显示名称
|
// 判断是否为最新消息
|
||||||
const displayName = isUser ? userName : characterName;
|
const isLatestMessage = messages.length > 0 && message.floor === messages[messages.length - 1].floor;
|
||||||
|
|
||||||
return (
|
// 根据消息类型设置显示名称
|
||||||
<div key={message.floor} className={`message ${isUser ? 'user' : 'ai'}`}>
|
const displayName = isUser ? userName : characterName;
|
||||||
<div className="message-container">
|
|
||||||
<div className="message-header">
|
|
||||||
<span className="message-name">{displayName}</span>
|
|
||||||
<span className="message-id">#{message.floor}</span>
|
|
||||||
<div className="message-toolbar">
|
|
||||||
<div className="toolbar-buttons">
|
|
||||||
<button
|
|
||||||
className="toolbar-button"
|
|
||||||
onClick={() => handleEdit(message)}
|
|
||||||
title="编辑"
|
|
||||||
>
|
|
||||||
✎
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="toolbar-button"
|
|
||||||
title="更多"
|
|
||||||
>
|
|
||||||
•••
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="message-content">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className="edit-container">
|
|
||||||
<textarea
|
|
||||||
className="edit-textarea"
|
|
||||||
value={editContent}
|
|
||||||
onChange={(e) => setEditContent(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="edit-buttons">
|
|
||||||
<button
|
|
||||||
className="cancel-button"
|
|
||||||
onClick={handleCancelEdit}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="save-button"
|
|
||||||
onClick={() => handleSaveEdit(message.floor)}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bubble">
|
|
||||||
{message.mes}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
// 确定当前显示的消息内容
|
||||||
<div className="chat-box">
|
let currentMes = message.mes;
|
||||||
<div className="chat-messages">
|
let hasSwipes = message.swipes && message.swipes.length > 0;
|
||||||
{isLoading ? (
|
let currentSwipeIndex = message.swipe_id;
|
||||||
<div className="loading">加载中...</div>
|
|
||||||
) : error ? (
|
if (hasSwipes) {
|
||||||
<div className="error">{error}</div>
|
// 如果有swipes数组
|
||||||
) : messages.length === 0 ? (
|
if (currentSwipeId[message.floor] !== undefined) {
|
||||||
<div className="loading">暂无消息</div>
|
// 如果用户已经切换过版本,使用用户选择的版本
|
||||||
) : (
|
currentSwipeIndex = currentSwipeId[message.floor];
|
||||||
messages.map(renderMessage)
|
} else {
|
||||||
)}
|
// 否则使用默认的swipe_id
|
||||||
<div ref={messagesEndRef} />
|
currentSwipeIndex = message.swipe_id;
|
||||||
</div>
|
}
|
||||||
<div className="chat-input-wrapper">
|
|
||||||
<div className="chat-input-area">
|
if (currentSwipeIndex >= 0 && currentSwipeIndex < message.swipes.length) {
|
||||||
<textarea
|
currentMes = message.swipes[currentSwipeIndex];
|
||||||
value={inputValue}
|
}
|
||||||
onChange={(e) => {
|
}
|
||||||
setInputValue(e.target.value);
|
|
||||||
handleInputHeight(e);
|
return (
|
||||||
}}
|
<div key={message.floor} className={`message ${isUser ? 'user' : 'ai'}`}>
|
||||||
onKeyDown={handleKeyDown}
|
<div className="message-container">
|
||||||
style={{ height: `${inputHeight}px` }}
|
<div className="message-header">
|
||||||
placeholder="输入消息..."
|
<span className="message-name">{displayName}</span>
|
||||||
/>
|
<span className="message-id">#{message.floor}</span>
|
||||||
|
<div className="message-toolbar">
|
||||||
|
<div className="toolbar-buttons">
|
||||||
|
<button
|
||||||
|
className="toolbar-button"
|
||||||
|
onClick={() => handleEdit(message)}
|
||||||
|
title="编辑"
|
||||||
|
>
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="toolbar-button"
|
||||||
|
title="更多"
|
||||||
|
>
|
||||||
|
•••
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="message-content">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="edit-container">
|
||||||
|
<textarea
|
||||||
|
className="edit-textarea"
|
||||||
|
value={editContent}
|
||||||
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="edit-buttons">
|
||||||
|
<button
|
||||||
|
className="cancel-button"
|
||||||
|
onClick={handleCancelEdit}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="save-button"
|
||||||
|
onClick={() => handleSaveEdit(message.floor)}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bubble">
|
||||||
|
{currentMes}
|
||||||
|
{hasSwipes && isLatestMessage && !isUser && (
|
||||||
|
<div className="swipe-controls">
|
||||||
|
<button
|
||||||
|
className="swipe-button"
|
||||||
|
onClick={() => handleSwipeChange(message.floor, -1)}
|
||||||
|
disabled={currentSwipeIndex === 0}
|
||||||
|
>
|
||||||
|
◀
|
||||||
|
</button>
|
||||||
|
<span className="swipe-counter">
|
||||||
|
{currentSwipeIndex + 1}/{message.swipes.length}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="swipe-button"
|
||||||
|
onClick={() => handleSwipeChange(message.floor, 1)}
|
||||||
|
disabled={currentSwipeIndex === message.swipes.length - 1}
|
||||||
|
>
|
||||||
|
▶
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className={`send-button ${isGenerating ? 'stopping' : ''}`}
|
|
||||||
onClick={handleSendOrStop}
|
|
||||||
disabled={!inputValue.trim() && !isGenerating}
|
|
||||||
>
|
|
||||||
{isGenerating ? '终止' : '发送'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-box">
|
||||||
|
<div className="chat-messages">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="loading">加载中...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="error">{error}</div>
|
||||||
|
) : messages.length === 0 ? (
|
||||||
|
<div className="loading">暂无消息</div>
|
||||||
|
) : (
|
||||||
|
messages.map(renderMessage)
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
<div className="chat-input-wrapper">
|
||||||
|
<button className="options-button" title="展开选项">
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
<div className="chat-input-area">
|
||||||
|
<textarea
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
handleInputHeight(e);
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={{ height: `${inputHeight}px` }}
|
||||||
|
placeholder="输入消息..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`send-button ${isGenerating ? 'stopping' : ''}`}
|
||||||
|
onClick={handleSendOrStop}
|
||||||
|
disabled={!inputValue.trim() && !isGenerating}
|
||||||
|
>
|
||||||
|
{isGenerating ? '■' : '➤'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatBox;
|
export default ChatBox;
|
||||||
|
|||||||
Reference in New Issue
Block a user