Architech #1
@@ -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-Adapter(CL-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 切到 Kolors),LLM 在回复中仍声称使用旧模型。根因是 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 API,hover 显示 + 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 + 多次生成丢失参考图
|
||||
- **tags**: bug修复, Replicate, InstantStyle, timeout, 参考图, Agent Loop, wait, SDK
|
||||
- **affected_files**:
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
最近 ~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-0730] 修复生图失败后 LLM 重试丢失参考图:ref_image_url 改为仅在成功产出图片后才清除
|
||||
- [CL-20260413-0700] 修复 InstantStyle 生图 422 + ReadTimeout:`wait=300` 超限改为 `wait=60`,SDK 超时自动 fallback 轮询
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
最近 ~10 次改动的摘要记录,按时间倒序排列。
|
||||
当 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,根因同 Kolors(CL-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 + 多次生成丢失参考图
|
||||
- **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
|
||||
|
||||
@@ -91,6 +91,51 @@
|
||||
- **chosen_alternative**: 当前资源库使用手动标签 + Prompt 文本搜索
|
||||
- **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
|
||||
- 核心 API:memory.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
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""Agent Loop 主循环:LLM 对话 -> 工具调用 -> 结果回传 -> 继续。"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -44,12 +45,13 @@ SYSTEM_PROMPT = """\
|
||||
- 尽量详细描述:主体内容、颜色方案、光照、构图、材质等
|
||||
- 如果用户要求游戏 UI 元素,添加相关关键词如 "game UI", "icon", "button" 等
|
||||
- 如果你能直接看到参考图(图片内容),可以在 prompt 中描述参考图的风格特征
|
||||
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你的 prompt 中**绝对不要猜测或指定任何画风/艺术风格关键词**(如 pixel art、watercolor、oil painting 等),只描述画面内容(主体、构图、光照等),把风格完全交给参考图来决定
|
||||
- 如果你无法看到参考图(只收到了文字提示说有参考图),参考图会由生图工具的 IP-Adapter 自动处理风格融合。此时你不要自行猜测画风/艺术风格关键词(如 pixel art、watercolor、oil painting 等),把风格交给参考图来决定。但如果用户在消息中明确指定了风格(如"赛博朋克风"、"水彩风"等),应保留并翻译到 prompt 中——尊重用户的主动意图
|
||||
|
||||
## 注意事项
|
||||
- 用中文和用户交流
|
||||
- 生成图片后简要说明你使用的 prompt 思路
|
||||
- 主动建议迭代方向
|
||||
- **禁止模拟工具调用**:生成图片时必须实际调用 generate_image 工具,绝不能用文字描述"已生成"或假装工具已执行。如果需要生成多张图片,每张都必须单独调用工具
|
||||
"""
|
||||
|
||||
|
||||
@@ -68,8 +70,18 @@ async def run_agent_loop(
|
||||
- done: 完成
|
||||
- error: 错误
|
||||
"""
|
||||
# 构建消息列表
|
||||
api_messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
# 构建消息列表:将当前生图模型信息注入 system prompt,避免 LLM 猜测模型名
|
||||
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_model = get_llm_model().lower()
|
||||
@@ -96,9 +108,9 @@ async def run_agent_loop(
|
||||
"【系统提示:用户上传了一张参考图,已自动传递给图片生成工具的 IP-Adapter。"
|
||||
"IP-Adapter 会从参考图中提取风格并融合到生成结果中。"
|
||||
"你无法看到这张参考图,因此在生成 prompt 时:\n"
|
||||
"1. 只描述画面内容(主体、构图、光照、材质等)\n"
|
||||
"2. 不要猜测或添加任何画风/艺术风格关键词(如 pixel art、watercolor、cartoon 等)\n"
|
||||
"3. 风格完全由参考图通过 IP-Adapter 决定】"
|
||||
"1. 描述画面内容(主体、构图、光照、材质等)\n"
|
||||
"2. 不要自行猜测画风/艺术风格——但如果用户明确指定了风格,保留到 prompt 中\n"
|
||||
"3. 用户未指定风格时,风格完全由参考图通过 IP-Adapter 决定】"
|
||||
)
|
||||
api_messages.append({"role": "user", "content": hint})
|
||||
continue
|
||||
@@ -150,8 +162,25 @@ async def run_agent_loop(
|
||||
|
||||
finish_reason = chunk.choices[0].finish_reason if chunk.choices else None
|
||||
|
||||
# 如果没有工具调用,对话结束
|
||||
# 如果没有工具调用:检测 LLM 是否在用文字模拟生图
|
||||
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": {}}
|
||||
return
|
||||
|
||||
@@ -225,3 +254,19 @@ async def run_agent_loop(
|
||||
# 后续工具调用仍需参考图(InstantStyle 等模型必须有 style_image)
|
||||
|
||||
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
|
||||
|
||||
@@ -59,7 +59,7 @@ IMAGE_MODELS: dict[str, dict[str, Any]] = {
|
||||
"id": "sdxl",
|
||||
"name": "Stable Diffusion XL",
|
||||
"provider": "replicate",
|
||||
"model_id": "stability-ai/sdxl",
|
||||
"model_id": "stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b",
|
||||
"description": "经典 SDXL,支持 negative prompt",
|
||||
"supports_ref_image": False,
|
||||
"num_images_param": "num_outputs",
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import type { ChatMessage, ImageAsset } from "@/lib/types";
|
||||
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 {
|
||||
messages: ChatMessage[];
|
||||
isLoading: boolean;
|
||||
@@ -21,35 +56,43 @@ export function ChatMessages({
|
||||
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">
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div
|
||||
className={`max-w-[90%] md:max-w-[80%] rounded-2xl px-3.5 md:px-4 py-2.5 md:py-3 ${
|
||||
msg.role === "user"
|
||||
? "bg-[var(--accent)] text-white"
|
||||
: "bg-[var(--bg-tertiary)] text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{msg.refImageUrl && (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={msg.refImageUrl}
|
||||
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>
|
||||
<div key={msg.id} className={`group/msg flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<div
|
||||
className={`max-w-[90vw] md:max-w-[80vw] rounded-2xl px-3.5 md:px-4 py-2.5 md:py-3 ${
|
||||
msg.role === "user"
|
||||
? "bg-[var(--accent)] text-white"
|
||||
: "bg-[var(--bg-tertiary)] text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{msg.refImageUrl && (
|
||||
<div className="mb-2">
|
||||
<img
|
||||
src={msg.refImageUrl}
|
||||
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>
|
||||
</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 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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user