Architech #1

Merged
shiyong.qin merged 8 commits from Architech into main 2026-04-16 11:16:48 +08:00
7 changed files with 259 additions and 36 deletions
Showing only changes of commit f6a43302d2 - Show all commits

View File

@@ -4,6 +4,60 @@
## 记录 ## 记录
### [CL-20260413-1130] 2026-04-13 11:30 — 修复 SDXL 模型 404 错误:补全 Replicate 版本 hash
- **tags**: bug修复, Replicate, SDXL, 模型配置, 404
- **affected_files**:
- art-agent/backend/app/config.py
- **what**: 修复 Stable Diffusion XL 模型调用 Replicate API 返回 404 的问题
- **why**: 与之前 Kolors IP-AdapterCL-20260413-0440完全相同的根因——Replicate 对非 Black Forest Labs 官方模型需要 `owner/model:version_hash` 完整格式,短格式 `stability-ai/sdxl` 无法定位到具体版本
- **decisions**: 使用 Replicate 官方文档中的最新推荐版本 hash `39ed52f2...e08b`
- **notes**: Flux 系列black-forest-labs/flux-schnell、flux-dev不受此问题影响Replicate 对其支持短格式。后续新增模型时应默认使用完整的 `owner/model:version_hash` 格式
### [CL-20260413-1100] 2026-04-13 11:00 — 修复切换模型后 LLM 仍声称使用旧模型名
- **tags**: Agent Loop, system prompt, 模型选择, LLM 幻觉, 模型名
- **affected_files**:
- art-agent/backend/app/agent/loop.py
- **what**: System Prompt 中动态注入当前生图模型名称,防止 LLM 从对话历史幻觉旧模型名
- **why**: 用户在同一会话内切换模型(如从 InstantStyle 切到 KolorsLLM 在回复中仍声称使用旧模型。根因是 System Prompt 不包含模型信息LLM 从对话历史中的工具返回结果/文字记录推断模型名,导致幻觉
- **decisions**: 在 system prompt 末尾动态拼接当前模型名 + 强调忽略旧记录。同时在假生成重试的纠正消息中也附带模型名
- **notes**: 通过 debug 日志验证前端→后端→工具执行的模型传递链路完全正确kolors-ipadapter 一路贯穿),问题仅在 LLM 文字输出层面。修复后验证通过
### [CL-20260413-1030] 2026-04-13 10:30 — Agent Loop 假生成检测 + 自动重试机制
- **tags**: Agent Loop, DeepSeek, function calling, 防幻觉, 自动修复
- **affected_files**:
- art-agent/backend/app/agent/loop.py
- **what**: 当 LLM 返回纯文字(无 tool_calls但内容包含"已生成"等模拟生图关键词时,自动注入纠正消息强制 LLM 重新调用工具
- **why**: DeepSeek 即使在 System Prompt 中明确禁止模拟工具调用CL-0930仍会在复杂多步任务中用文字"扮演"生图过程,导致前端收不到 image_result 事件。Prompt 约束对 DeepSeek 无效,必须在代码层面硬性拦截
- **decisions**: 用正则匹配"已生成"、"生成完成"、"图片已"等关键词,命中 2 次以上且文本 >= 50 字时判定为假生成。检测到后将 LLM 的回复保留在历史中,追加一条纠正消息"你没有调用工具,请立即调用",然后 continue 回到循环让 LLM 重试。重试消耗一次迭代配额,不影响正常对话
- **notes**: 此机制主要针对 DeepSeek 的 function calling 纪律问题。GPT-4o 等模型通常不会触发。如果 DeepSeek 重试后仍然不调用工具,会在 max_iterations 用尽后正常结束
### [CL-20260413-1000] 2026-04-13 10:00 — 助手消息气泡添加复制文本按钮
- **tags**: 前端, UX, chat-messages, 复制
- **affected_files**:
- art-agent/frontend/src/components/chat/chat-messages.tsx
- **what**: 助手回复气泡下方新增复制文本按钮hover 时显示,点击复制文本内容并显示对勾反馈
- **why**: LLM 生成的 prompt 思路、风格描述等文字内容用户经常需要复制,之前只能手动选中
- **decisions**: 按钮放在气泡下方左侧而非内部,避免干扰阅读。只对 assistant 消息显示(用户自己的消息不需要复制)。使用 navigator.clipboard APIhover 显示 + 1.5s 对勾反馈
- **notes**: 仅复制纯文本内容,不含图片信息
### [CL-20260413-0930] 2026-04-13 09:30 — System Prompt 加入"禁止模拟工具调用"约束
- **tags**: prompt 工程, system prompt, DeepSeek, function calling, 防幻觉
- **affected_files**:
- art-agent/backend/app/agent/loop.py
- **what**: System Prompt 注意事项中新增"禁止模拟工具调用"硬性约束
- **why**: DeepSeek 在复杂多步任务(如一次生成 3 种不同建筑)中,有时不通过 function calling 调用 generate_image 工具,而是用文字"模拟"生成过程(如"已生成"),导致 Agent Loop 第一轮就因 tool_calls_data 为空而 yield done前端收不到 image_result 事件,用户看不到图片
- **decisions**: 在 System Prompt 中加一条行为红线,告诉 LLM 必须实际调用工具。轻量级防御(检测文字中包含"已生成"但无工具调用时发警告)暂不实现,先观察 prompt 约束的效果
- **notes**: 这是 DeepSeek function calling 纪律问题GPT-4o 等模型通常不会出现。如果 prompt 约束不够,后续可加检测机制
### [CL-20260413-0900] 2026-04-13 09:00 — 非 vision + 参考图场景:尊重用户主动指定的风格意图
- **tags**: prompt 工程, IP-Adapter, 风格, system prompt, UX
- **affected_files**:
- art-agent/backend/app/agent/loop.py
- **what**: 将"绝对禁止风格关键词"改为"不自行猜测,但保留用户明确指定的风格"
- **why**: 之前的 system prompt 和非 vision hint 中对风格关键词的禁止是绝对性的("绝对不要"、"不要添加任何"),导致 LLM 在用户主动输入了风格描述(如"赛博朋克风"、"水彩风")时也会丢弃用户意图。系统不应替用户做决定
- **decisions**: 措辞从"绝对禁止"改为"不自行猜测,但用户明确指定则保留"。两处同步修改SYSTEM_PROMPT 第 47 行 + 非 vision 模型的 hint 文本。IP-Adapter 风格迁移与用户文字风格描述可叠加不冲突
- **notes**: 这是对 CL-20260413-0540 策略的修正——当时为了防止 DeepSeek 脑补画风而加了绝对禁止,现在细化为区分 LLM 猜测 vs 用户意图
### [CL-20260413-0800] 2026-04-13 08:00 — 修复 InstantStyle ReadTimeout + 多次生成丢失参考图 ### [CL-20260413-0800] 2026-04-13 08:00 — 修复 InstantStyle ReadTimeout + 多次生成丢失参考图
- **tags**: bug修复, Replicate, InstantStyle, timeout, 参考图, Agent Loop, wait, SDK - **tags**: bug修复, Replicate, InstantStyle, timeout, 参考图, Agent Loop, wait, SDK
- **affected_files**: - **affected_files**:

View File

@@ -2,6 +2,12 @@
最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。 最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。
- [CL-20260413-1130] 修复 SDXL 模型 404 错误:与 Kolors 同因Replicate 非 Flux 官方模型需补全版本 hash
- [CL-20260413-1100] 修复切换模型后 LLM 仍声称使用旧模型名System Prompt 动态注入当前模型名,阻止 LLM 从对话历史幻觉旧模型
- [CL-20260413-1030] Agent Loop 假生成检测 + 自动重试DeepSeek 用文字模拟生图时自动注入纠正消息强制调用工具
- [CL-20260413-1000] 助手消息气泡添加复制文本按钮hover 显示、点击复制 + 对勾反馈
- [CL-20260413-0930] System Prompt 加入"禁止模拟工具调用"约束,防止 DeepSeek 用文字模拟生图而不调用工具
- [CL-20260413-0900] 非 vision + 参考图场景:不再绝对禁止风格词,改为"不自行猜测但保留用户明确指定的风格"
- [CL-20260413-0800] 修复 InstantStyle ReadTimeout + 多次生成丢失参考图wait 改 False 走纯轮询 + 不再清除 ref_image_url - [CL-20260413-0800] 修复 InstantStyle ReadTimeout + 多次生成丢失参考图wait 改 False 走纯轮询 + 不再清除 ref_image_url
- [CL-20260413-0730] 修复生图失败后 LLM 重试丢失参考图ref_image_url 改为仅在成功产出图片后才清除 - [CL-20260413-0730] 修复生图失败后 LLM 重试丢失参考图ref_image_url 改为仅在成功产出图片后才清除
- [CL-20260413-0700] 修复 InstantStyle 生图 422 + ReadTimeout`wait=300` 超限改为 `wait=60`SDK 超时自动 fallback 轮询 - [CL-20260413-0700] 修复 InstantStyle 生图 422 + ReadTimeout`wait=300` 超限改为 `wait=60`SDK 超时自动 fallback 轮询

View File

@@ -3,6 +3,36 @@
最近 ~10 次改动的摘要记录,按时间倒序排列。 最近 ~10 次改动的摘要记录,按时间倒序排列。
当 Agent 检测到当前任务与近期改动相关时自动读取。 当 Agent 检测到当前任务与近期改动相关时自动读取。
### [CL-20260413-1130] 2026-04-13 — 修复 SDXL 模型 404 错误:补全 Replicate 版本 hash
- **tags**: bug修复, Replicate, SDXL, 模型配置, 404
- **affected_files**: art-agent/backend/app/config.py
- **summary**: SDXL 调用 Replicate 404根因同 KolorsCL-20260413-0440非 Flux 官方模型需要 `owner/model:version_hash` 完整格式。将 `stability-ai/sdxl` 补全为 `stability-ai/sdxl:39ed52f2...e08b`
### [CL-20260413-1100] 2026-04-13 — 修复切换模型后 LLM 仍声称使用旧模型名
- **tags**: Agent Loop, system prompt, 模型选择, LLM 幻觉
- **affected_files**: art-agent/backend/app/agent/loop.py
- **summary**: System Prompt 不含模型信息LLM 从对话历史幻觉旧模型名。修复:在 system prompt 末尾动态注入当前模型名 + 强调忽略历史中的旧记录假生成重试消息也附带模型名。debug 日志验证传递链路正确,问题仅在 LLM 文字层。
### [CL-20260413-1030] 2026-04-13 — Agent Loop 假生成检测 + 自动重试机制
- **tags**: Agent Loop, DeepSeek, function calling, 防幻觉, 自动修复
- **affected_files**: art-agent/backend/app/agent/loop.py
- **summary**: DeepSeek 无视 prompt 约束仍用文字模拟生图。新增代码层面检测LLM 纯文字回复中命中"已生成"等关键词 >= 2 次时,自动注入纠正消息强制重试调用工具。重试消耗一次迭代配额。
### [CL-20260413-1000] 2026-04-13 — 助手消息气泡添加复制文本按钮
- **tags**: 前端, UX, chat-messages, 复制
- **affected_files**: art-agent/frontend/src/components/chat/chat-messages.tsx
- **summary**: 助手回复气泡下方新增复制文本按钮hover 显示),点击通过 clipboard API 复制文本并显示 1.5s 对勾反馈。只对 assistant 消息展示。
### [CL-20260413-0930] 2026-04-13 — System Prompt 加入"禁止模拟工具调用"约束
- **tags**: prompt 工程, system prompt, DeepSeek, function calling, 防幻觉
- **affected_files**: art-agent/backend/app/agent/loop.py
- **summary**: DeepSeek 在多步生图任务中有时不调用工具而是用文字模拟"已生成",导致前端收不到图片。在 System Prompt 注意事项中加入硬性约束:"生成图片必须调用 generate_image 工具,禁止用文字模拟"。
### [CL-20260413-0900] 2026-04-13 — 非 vision + 参考图场景:尊重用户主动指定的风格意图
- **tags**: prompt 工程, IP-Adapter, 风格, system prompt, UX
- **affected_files**: art-agent/backend/app/agent/loop.py
- **summary**: SYSTEM_PROMPT 和非 vision hint 中对风格关键词的禁止从"绝对禁止"改为"不自行猜测,但用户明确指定则保留到 prompt 中"。之前的绝对性措辞导致 LLM 即使用户主动写了"赛博朋克风"也会丢弃。修正后 IP-Adapter 风格迁移与用户文字风格可叠加。
### [CL-20260413-0800] 2026-04-13 — 修复 InstantStyle ReadTimeout + 多次生成丢失参考图 ### [CL-20260413-0800] 2026-04-13 — 修复 InstantStyle ReadTimeout + 多次生成丢失参考图
- **tags**: bug修复, Replicate, InstantStyle, timeout, 参考图, Agent Loop, wait - **tags**: bug修复, Replicate, InstantStyle, timeout, 参考图, Agent Loop, wait
- **affected_files**: art-agent/backend/app/services/image_gen.py, art-agent/backend/app/agent/loop.py - **affected_files**: art-agent/backend/app/services/image_gen.py, art-agent/backend/app/agent/loop.py

View File

@@ -91,6 +91,51 @@
- **chosen_alternative**: 当前资源库使用手动标签 + Prompt 文本搜索 - **chosen_alternative**: 当前资源库使用手动标签 + Prompt 文本搜索
- **deferred_reason**: 当前阶段资源量少,手动管理足够;等资源积累到一定量级后再引入自动化 - **deferred_reason**: 当前阶段资源量少,手动管理足够;等资源积累到一定量级后再引入自动化
### [context-window-management] 对话上下文管理Token 预算 + 滑动窗口 + 摘要)
- **status**: deferred
- **tags**: 上下文管理, token, 滑动窗口, 对话摘要, agent loop, 长对话
- **recorded**: 2026-04-13
- **source_chat**: [Art Agent 上下文与记忆体系讨论](memory-architecture-discussion)
- **prerequisite**: 无(随时可做,优先级高于长期记忆)
- **related_files**:
- art-agent/backend/app/agent/loop.py
- art-agent/frontend/src/lib/api.ts
- art-agent/frontend/src/lib/store.ts
- **context**: |
当前 Art Agent 对话采用"全量透传":前端把完整 messages 发给后端,后端原样拼接传给 LLM
没有 token 计数、截断或摘要。长对话会导致上下文超限、费用线性增长。
实施方案(自建,无需外部依赖):
1. Token 计数:用 tiktoken 库在构建 api_messages 时计数
2. 滑动窗口:超过预算时截断最早的消息
3. 对话摘要:被截断的消息先调 LLM 生成摘要,作为压缩上下文保留
预估工作量 1-2 天,是记忆体系的第一层基础。
- **chosen_alternative**: 当前全量透传,依赖模型自身的上下文窗口上限
- **deferred_reason**: 当前探索期对话通常较短,暂未触及上限;但随着迭代使用会很快需要
### [mem0-long-term-memory] 引入 Mem0 作为长期记忆层
- **status**: deferred
- **tags**: 长期记忆, Mem0, 用户偏好, 语义检索, 事实提取, memory layer
- **recorded**: 2026-04-13
- **source_chat**: [Art Agent 上下文与记忆体系讨论](memory-architecture-discussion)
- **prerequisite**: 第一层上下文管理context-window-management完成后
- **related_files**:
- art-agent/backend/app/agent/loop.py
- art-agent/backend/app/config.py
- **context**: |
引入 Mem0 开源版Apache 2.0)作为 Art Agent 的跨会话长期记忆层:
- Mem0 是"bolt-on"设计,保留自己的 agent loop只外挂 memory API
- 核心 APImemory.add()(存储+自动事实提取)+ memory.search()(语义检索)
- 自部署pip install mem0ai + Qdrant 向量库LLM 用 DeepSeek 降成本
- 记忆范围:用户风格偏好、历史生成反馈、常用参数等
- 与自建 Rules/Docs 静态知识系统互补Mem0 负责动态记忆Rules 负责预置知识
技术选型对比(本次讨论结论):
- Letta 不适合:它是完整 agent runtime不是 memory layer需要替换现有 loop
- Zep 可行但偏重:时序知识图谱能力强但集成复杂度更高
- Mem0 最适合:设计哲学完全对口"已有 agent + 外挂 memory",集成最简(~20 行代码)
预估工作量 3-5 天(含 Mem0 部署 + 集成 + extraction prompt 调优)。
- **chosen_alternative**: 当前无长期记忆,每次会话从零开始
- **deferred_reason**: 探索期优先做第一层上下文管理;长期记忆在用户量和使用频率增长后再引入
--- ---
## Completed / Cancelled Items ## Completed / Cancelled Items

View File

@@ -1,13 +1,14 @@
"""Agent Loop 主循环LLM 对话 -> 工具调用 -> 结果回传 -> 继续。""" """Agent Loop 主循环LLM 对话 -> 工具调用 -> 结果回传 -> 继续。"""
import json import json
import re
import os import os
from typing import AsyncGenerator, Optional from typing import AsyncGenerator, Optional
from openai import AsyncOpenAI from openai import AsyncOpenAI
from app.agent.tools import TOOL_DEFINITIONS, execute_tool from app.agent.tools import TOOL_DEFINITIONS, execute_tool
from app.config import get_llm_model, get_llm_max_iterations from app.config import get_llm_model, get_llm_max_iterations, get_image_model_config
from app.services.image_gen import to_data_uri from app.services.image_gen import to_data_uri
@@ -44,12 +45,13 @@ SYSTEM_PROMPT = """\
- 尽量详细描述:主体内容、颜色方案、光照、构图、材质等 - 尽量详细描述:主体内容、颜色方案、光照、构图、材质等
- 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button" - 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button"
- 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征 - 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你的 prompt 中**绝对不要猜测或指定任何画风/艺术风格关键词**(如 pixel art、watercolor、oil painting 等),只描述画面内容(主体、构图、光照等),把风格完全交给参考图来决定 - 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你不要自行猜测画风/艺术风格关键词(如 pixel art、watercolor、oil painting 等),把风格交给参考图来决定。但如果用户在消息中明确指定了风格(如"赛博朋克风""水彩风"等),应保留并翻译到 prompt 中——尊重用户的主动意图
## 注意事项 ## 注意事项
- 用中文和用户交流 - 用中文和用户交流
- 生成图片后简要说明你使用的 prompt 思路 - 生成图片后简要说明你使用的 prompt 思路
- 主动建议迭代方向 - 主动建议迭代方向
- **禁止模拟工具调用**:生成图片时必须实际调用 generate_image 工具,绝不能用文字描述"已生成"或假装工具已执行。如果需要生成多张图片,每张都必须单独调用工具
""" """
@@ -68,8 +70,18 @@ async def run_agent_loop(
- done: 完成 - done: 完成
- error: 错误 - error: 错误
""" """
# 构建消息列表 # 构建消息列表:将当前生图模型信息注入 system prompt避免 LLM 猜测模型名
api_messages = [{"role": "system", "content": SYSTEM_PROMPT}] model_config = get_image_model_config(image_model)
current_model_name = model_config.get('name', '未知')
current_model_id = model_config.get('id', '未知')
model_hint = (
f"\n\n## 当前生图模型(重要)\n"
f"本次对话用户选择的生图模型是 **{current_model_name}**"
f"model_id: {current_model_id})。\n"
f"**注意**:对话历史中可能包含之前使用其他模型的记录,忽略那些旧模型名。"
f"本次生成使用的是 {current_model_name},在回复中只能使用这个名称。"
)
api_messages = [{"role": "system", "content": SYSTEM_PROMPT + model_hint}]
# 检测当前 LLM 是否支持 vision多模态图片输入 # 检测当前 LLM 是否支持 vision多模态图片输入
llm_model = get_llm_model().lower() llm_model = get_llm_model().lower()
@@ -96,9 +108,9 @@ async def run_agent_loop(
"【系统提示:用户上传了一张参考图,已自动传递给图片生成工具的 IP-Adapter。" "【系统提示:用户上传了一张参考图,已自动传递给图片生成工具的 IP-Adapter。"
"IP-Adapter 会从参考图中提取风格并融合到生成结果中。" "IP-Adapter 会从参考图中提取风格并融合到生成结果中。"
"你无法看到这张参考图,因此在生成 prompt 时:\n" "你无法看到这张参考图,因此在生成 prompt 时:\n"
"1. 描述画面内容(主体、构图、光照、材质等)\n" "1. 描述画面内容(主体、构图、光照、材质等)\n"
"2. 不要猜测或添加任何画风/艺术风格关键词(如 pixel art、watercolor、cartoon 等)\n" "2. 不要自行猜测画风/艺术风格——但如果用户明确指定了风格,保留到 prompt 中\n"
"3. 风格完全由参考图通过 IP-Adapter 决定】" "3. 用户未指定风格时,风格完全由参考图通过 IP-Adapter 决定】"
) )
api_messages.append({"role": "user", "content": hint}) api_messages.append({"role": "user", "content": hint})
continue continue
@@ -150,8 +162,25 @@ async def run_agent_loop(
finish_reason = chunk.choices[0].finish_reason if chunk.choices else None finish_reason = chunk.choices[0].finish_reason if chunk.choices else None
# 如果没有工具调用,对话结束 # 如果没有工具调用:检测 LLM 是否在用文字模拟生图
if not tool_calls_data: if not tool_calls_data:
if _looks_like_fake_generation(collected_text):
# LLM 用文字假装调用了工具,丢弃这段文字,注入纠正消息强制重试
yield {
"type": "text_delta",
"data": {"text": "\n\n[系统:检测到未调用生图工具,正在重试...]\n"},
}
api_messages.append({"role": "assistant", "content": collected_text})
api_messages.append({
"role": "user",
"content": (
"你刚才没有调用 generate_image 工具,只是用文字描述了生成过程。"
"请立即调用 generate_image 工具来实际生成图片。"
"不要解释,直接调用工具。"
f"当前使用的生图模型是 {model_config.get('name', '未知')}"
),
})
continue
yield {"type": "done", "data": {}} yield {"type": "done", "data": {}}
return return
@@ -225,3 +254,19 @@ async def run_agent_loop(
# 后续工具调用仍需参考图InstantStyle 等模型必须有 style_image # 后续工具调用仍需参考图InstantStyle 等模型必须有 style_image
yield {"type": "done", "data": {}} yield {"type": "done", "data": {}}
# ─── 假生成检测 ──────────────────────────────────────────
_FAKE_GEN_PATTERNS = re.compile(
r"已生成|生成完成|开始生成|正在生成|图片已|prompt.*?设计思路",
re.IGNORECASE,
)
def _looks_like_fake_generation(text: str) -> bool:
"""判断 LLM 的纯文字回复是否在假装已经调用了生图工具。"""
if not text or len(text) < 50:
return False
matches = _FAKE_GEN_PATTERNS.findall(text)
return len(matches) >= 2

View File

@@ -59,7 +59,7 @@ IMAGE_MODELS: dict[str, dict[str, Any]] = {
"id": "sdxl", "id": "sdxl",
"name": "Stable Diffusion XL", "name": "Stable Diffusion XL",
"provider": "replicate", "provider": "replicate",
"model_id": "stability-ai/sdxl", "model_id": "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b",
"description": "经典 SDXL支持 negative prompt", "description": "经典 SDXL支持 negative prompt",
"supports_ref_image": False, "supports_ref_image": False,
"num_images_param": "num_outputs", "num_images_param": "num_outputs",

View File

@@ -1,8 +1,43 @@
"use client"; "use client";
import { useState, useCallback } from "react";
import type { ChatMessage, ImageAsset } from "@/lib/types"; import type { ChatMessage, ImageAsset } from "@/lib/types";
import { ImageGrid } from "./image-grid"; import { ImageGrid } from "./image-grid";
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
// fallback 不做处理
}
}, [text]);
return (
<button
onClick={handleCopy}
className="p-1 rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]
hover:bg-[var(--bg-secondary)] transition-colors cursor-pointer"
title="复制文本"
>
{copied ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
)}
</button>
);
}
interface ChatMessagesProps { interface ChatMessagesProps {
messages: ChatMessage[]; messages: ChatMessage[];
isLoading: boolean; isLoading: boolean;
@@ -21,35 +56,43 @@ export function ChatMessages({
return ( return (
<div className="flex-1 overflow-y-auto px-3 md:px-4 py-4 md:py-6 space-y-4 md:space-y-6"> <div className="flex-1 overflow-y-auto px-3 md:px-4 py-4 md:py-6 space-y-4 md:space-y-6">
{messages.map((msg) => ( {messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}> <div key={msg.id} className={`group/msg flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
<div <div className="flex flex-col items-start gap-0.5">
className={`max-w-[90%] md:max-w-[80%] rounded-2xl px-3.5 md:px-4 py-2.5 md:py-3 ${ <div
msg.role === "user" className={`max-w-[90vw] md:max-w-[80vw] rounded-2xl px-3.5 md:px-4 py-2.5 md:py-3 ${
? "bg-[var(--accent)] text-white" msg.role === "user"
: "bg-[var(--bg-tertiary)] text-[var(--text-primary)]" ? "bg-[var(--accent)] text-white"
}`} : "bg-[var(--bg-tertiary)] text-[var(--text-primary)]"
> }`}
{msg.refImageUrl && ( >
<div className="mb-2"> {msg.refImageUrl && (
<img <div className="mb-2">
src={msg.refImageUrl} <img
alt="参考图" src={msg.refImageUrl}
className="max-w-[160px] max-h-[120px] rounded-lg object-cover border border-white/20" alt="参考图"
/> className="max-w-[160px] max-h-[120px] rounded-lg object-cover border border-white/20"
<span className="text-[10px] opacity-70 mt-1 block"></span> />
<span className="text-[10px] opacity-70 mt-1 block"></span>
</div>
)}
<div className="whitespace-pre-wrap text-sm leading-relaxed">{msg.content}</div>
{msg.images && msg.images.length > 0 && (
<>
<ImageGrid images={msg.images} />
{msg.modelName && (
<div className="mt-1.5 text-[10px] text-[var(--text-secondary)] opacity-70">
{msg.modelName}
</div>
)}
</>
)}
</div>
{msg.content && (
<div className={`opacity-0 group-hover/msg:opacity-100 transition-opacity
${msg.role === "user" ? "mr-1 self-end" : "ml-1"}`}>
<CopyButton text={msg.content} />
</div> </div>
)} )}
<div className="whitespace-pre-wrap text-sm leading-relaxed">{msg.content}</div>
{msg.images && msg.images.length > 0 && (
<>
<ImageGrid images={msg.images} />
{msg.modelName && (
<div className="mt-1.5 text-[10px] text-[var(--text-secondary)] opacity-70">
{msg.modelName}
</div>
)}
</>
)}
</div> </div>
</div> </div>
))} ))}