Skip to content

Commit dede667

Browse files
committed
feat(web): 添加消息复制功能并优化反馈组件图标
1 parent abf4414 commit dede667

File tree

7 files changed

+99
-34
lines changed

7 files changed

+99
-34
lines changed

docs/latest/changelog/roadmap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接 (2/4)
1212
- chat_model 的 call 需要异步
1313
- 优化chunk逻辑,移除 QA 分割,集成到普通分块中
14+
- 考虑修改附件的处理逻辑,考虑使用文件系统,将附件解析后放到文件系统中,智能体按需读取,用户可以使用 @ 来引用附件,例如 @file:reports.md 相较于现在的处理逻辑感觉会更加自然一点。在此之前可能还要考虑文件系统的后端支持问题
15+
- skills 如何实现还需要继续调研
1416

1517
### Bugs
1618
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
19+
- 部分 local 的 mcp server 无法正常加载,但是建议在项目外部启动 mcp 服务器,然后通过 sse 的方式使用。
1720
- DeepSeek 官方接口适配会出现问题
21+
- 部分推理后端与 langchain 的适配有问题:
1822
- 目前的知识库的图片存在公开访问风险
1923
- 工具传递给模型的时候,使用英文,但部分模型不支持中文函数名(如gpt-4o-mini)
2024
- 首页加载的问题

src/agents/chatbot/tools.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313

1414
@tool
15-
async def text_to_img_qwen(text: str) -> str:
16-
"""(用来测试文件存储)使用模型生成图片, 会返回图片的URL"""
15+
async def text_to_img_demo(text: str) -> str:
16+
"""【测试用】使用模型生成图片, 会返回图片的URL"""
1717

1818
url = "https://api.siliconflow.cn/v1/images/generations"
1919

@@ -27,13 +27,13 @@ async def text_to_img_qwen(text: str) -> str:
2727
response = requests.post(url, json=payload, headers=headers)
2828
response_json = response.json()
2929
except Exception as e:
30-
logger.error(f"Failed to generate image with Kolors: {e}")
30+
logger.error(f"Failed to generate image with: {e}")
3131
raise ValueError(f"Image generation failed: {e}")
3232

3333
try:
3434
image_url = response_json["images"][0]["url"]
3535
except (KeyError, IndexError, TypeError) as e:
36-
logger.error(f"Failed to parse image URL from Kolors response: {e}, {response_json=}")
36+
logger.error(f"Failed to parse image URL from response: {e}, {response_json=}")
3737
raise ValueError(f"Image URL extraction failed: {e}")
3838

3939
# 2. Upload to MinIO (Simplified)
@@ -51,6 +51,6 @@ async def text_to_img_qwen(text: str) -> str:
5151
def get_tools() -> list[Any]:
5252
"""获取所有可运行的工具(给大模型使用)"""
5353
tools = get_buildin_tools()
54-
tools.append(text_to_img_qwen)
54+
tools.append(text_to_img_demo)
5555
tools.extend(get_mysql_tools())
5656
return tools

web/src/assets/css/base.dark.css

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,12 @@
107107
--color-text-tertiary: rgba(255, 255, 255, 0.45);
108108

109109
/* Shadow System - 深色模式阴影系统 */
110-
--shadow-0: rgba(0, 0, 0, 0.1);
111-
--shadow-1: rgba(0, 0, 0, 0.2);
112-
--shadow-2: rgba(0, 0, 0, 0.3);
113-
--shadow-3: rgba(0, 0, 0, 0.4);
114-
--shadow-4: rgba(0, 0, 0, 0.5);
115-
--shadow-5: rgba(0, 0, 0, 0.6);
110+
--shadow-0: rgba(255, 255, 255, 0.02);
111+
--shadow-1: rgba(255, 255, 255, 0.05);
112+
--shadow-2: rgba(255, 255, 255, 0.08);
113+
--shadow-3: rgba(255, 255, 255, 0.12);
114+
--shadow-4: rgba(255, 255, 255, 0.16);
115+
--shadow-5: rgba(255, 255, 255, 0.20);
116116

117117

118118
--color-trans-light: rgba(0, 0, 0, 0.85);

web/src/components/AgentConfigSidebar.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,8 @@ const resetConfig = async () => {
684684
background-color: var(--gray-25);
685685
padding: 12px;
686686
border-radius: 8px;
687-
box-shadow: 0px 1px 1px var(--shadow-2);
687+
border: 1px solid var(--gray-100);
688+
// box-shadow: 0px 0px 2px var(--shadow-3);
688689
689690
:deep(label.form_item_model) {
690691
font-weight: 600;

web/src/components/AgentMessageComponent.vue

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
</div>
55
<div class="message-box" :class="[message.type, customClasses]">
66
<!-- 用户消息 -->
7+
<div v-if="message.type === 'human'" class="message-copy-btn human-copy" @click="copyToClipboard(message.content)" :class="{ 'is-copied': isCopied }">
8+
<Check v-if="isCopied" size="14" />
9+
<Copy v-else size="14" />
10+
</div>
711
<p v-if="message.type === 'human'" class="message-text">{{ message.content }}</p>
812

913
<p v-else-if="message.type === 'system'" class="message-text-system">{{ message.content }}</p>
@@ -102,7 +106,7 @@
102106
import { computed, ref } from 'vue';
103107
import { CaretRightOutlined, ThunderboltOutlined, LoadingOutlined } from '@ant-design/icons-vue';
104108
import RefsComponent from '@/components/RefsComponent.vue'
105-
import { Loader, CircleCheckBig } from 'lucide-vue-next';
109+
import { Loader, CircleCheckBig, Copy, Check } from 'lucide-vue-next';
106110
import { ToolResultRenderer } from '@/components/ToolCallingResult'
107111
import { useAgentStore } from '@/stores/agent'
108112
import { useInfoStore } from '@/stores/info'
@@ -150,6 +154,21 @@ const editorRef = ref()
150154
151155
const emit = defineEmits(['retry', 'retryStoppedMessage', 'openRefs']);
152156
157+
// 复制状态
158+
const isCopied = ref(false);
159+
160+
const copyToClipboard = async (text) => {
161+
try {
162+
await navigator.clipboard.writeText(text);
163+
isCopied.value = true;
164+
setTimeout(() => {
165+
isCopied.value = false;
166+
}, 2000);
167+
} catch (err) {
168+
console.error('Failed to copy: ', err);
169+
}
170+
};
171+
153172
// 推理面板展开状态
154173
const reasoningActiveKey = ref(['hide']);
155174
const expandedToolCalls = ref(new Set()); // 展开的工具调用集合
@@ -321,6 +340,38 @@ const toggleToolCall = (toolCallId) => {
321340
white-space: pre-line;
322341
}
323342
343+
.message-copy-btn {
344+
cursor: pointer;
345+
color: var(--gray-400);
346+
transition: all 0.2s ease;
347+
display: flex;
348+
align-items: center;
349+
justify-content: center;
350+
opacity: 0;
351+
flex-shrink: 0;
352+
353+
&:hover {
354+
color: var(--main-color);
355+
}
356+
357+
&.is-copied {
358+
color: var(--color-success-500);
359+
opacity: 1;
360+
}
361+
362+
&.human-copy {
363+
position: absolute;
364+
left: -28px;
365+
bottom: 8px;
366+
}
367+
}
368+
369+
&:hover {
370+
.message-copy-btn {
371+
opacity: 1;
372+
}
373+
}
374+
324375
.message-text-system {
325376
max-width: 100%;
326377
margin-bottom: 0;

web/src/components/KnowledgeGraphSection.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,18 +264,18 @@ onUnmounted(() => {
264264
backdrop-filter: blur(4px);
265265
padding: 2px;
266266
border-radius: 8px;
267-
box-shadow: 0 0px 4px var(--shadow-3);
267+
box-shadow: 0 0 4px 0px var(--shadow-2);
268268
}
269269
270270
:deep(.ant-input-affix-wrapper) {
271271
padding: 4px 11px;
272272
border-radius: 6px;
273273
border-color: transparent;
274274
box-shadow: none;
275-
background: rgba(255, 255, 255, 0.6);
275+
background: var(--color-trans-light);
276276
277277
&:hover, &:focus, &-focused {
278-
background: #fff;
278+
background: var(--main-0);
279279
border-color: var(--primary-color);
280280
}
281281

web/src/components/RefsComponent.vue

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,32 @@
88
@click="likeThisResponse(msg)"
99
:title="feedbackState.hasSubmitted && feedbackState.rating === 'like' ? '已点赞' : '点赞'"
1010
>
11-
<LikeFilled v-if="feedbackState.rating === 'like'" />
12-
<LikeOutlined v-else />
11+
<ThumbsUp size="12" :fill="feedbackState.rating === 'like' ? 'currentColor' : 'none'" />
1312
</span>
1413
<span
1514
class="item btn"
1615
:class="{ 'disabled': feedbackState.hasSubmitted }"
1716
@click="dislikeThisResponse(msg)"
1817
:title="feedbackState.hasSubmitted && feedbackState.rating === 'dislike' ? '已点踩' : '点踩'"
1918
>
20-
<DislikeFilled v-if="feedbackState.rating === 'dislike'" />
21-
<DislikeOutlined v-else />
19+
<ThumbsDown size="12" :fill="feedbackState.rating === 'dislike' ? 'currentColor' : 'none'" />
2220
</span>
2321
<!-- 模型名称 -->
2422
<span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)">
25-
<BulbOutlined /> {{ getModelName(msg) }}
23+
<Bot size="12" /> {{ getModelName(msg) }}
2624
</span>
2725
<!-- 复制 -->
2826
<span
2927
v-if="showKey('copy')"
30-
class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined />
28+
class="item btn" @click="copyText(msg.content)" title="复制">
29+
<Check v-if="isCopied" size="12" />
30+
<Copy v-else size="12" />
3131
</span>
3232

3333
<!-- 重试 -->
3434
<span
3535
v-if="showKey('regenerate')"
36-
class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined />
36+
class="item btn" @click="regenerateMessage()" title="重新生成"><RotateCcw size="12" />
3737
</span>
3838
</div>
3939
</div>
@@ -63,14 +63,13 @@ import { ref, computed, reactive, onMounted } from 'vue'
6363
import { useClipboard } from '@vueuse/core'
6464
import { message } from 'ant-design-vue'
6565
import {
66-
CopyOutlined,
67-
BulbOutlined,
68-
ReloadOutlined,
69-
LikeOutlined,
70-
LikeFilled,
71-
DislikeOutlined,
72-
DislikeFilled,
73-
} from '@ant-design/icons-vue'
66+
ThumbsUp,
67+
ThumbsDown,
68+
Bot,
69+
Copy,
70+
Check,
71+
RotateCcw,
72+
} from 'lucide-vue-next'
7473
import { agentApi } from '@/apis'
7574
7675
const emit = defineEmits(['retry', 'openRefs']);
@@ -110,12 +109,19 @@ const showKey = (key) => {
110109
return props.showRefs.includes(key)
111110
}
112111
112+
// 复制状态
113+
const isCopied = ref(false)
114+
113115
// 定义 copy 方法
114116
const copyText = async (text) => {
115117
if (isSupported) {
116118
try {
117119
await copy(text)
118120
message.success('文本已复制到剪贴板')
121+
isCopied.value = true
122+
setTimeout(() => {
123+
isCopied.value = false
124+
}, 2000)
119125
} catch (error) {
120126
console.error('复制失败:', error)
121127
message.error('复制失败,请手动复制')
@@ -255,11 +261,16 @@ const cancelDislike = () => {
255261
.item {
256262
background: var(--gray-50);
257263
color: var(--gray-700);
258-
padding: 2px 8px;
264+
padding: 6px 8px;
259265
border-radius: 8px;
260266
font-size: 13px;
261267
user-select: none;
262268
transition: all 0.2s ease;
269+
display: inline-flex;
270+
align-items: center;
271+
justify-content: center;
272+
gap: 4px;
273+
line-height: 1;
263274
264275
&.btn {
265276
cursor: pointer;
@@ -272,8 +283,6 @@ const cancelDislike = () => {
272283
273284
// Disabled state - when feedback has been submitted
274285
&.disabled {
275-
cursor: not-allowed;
276-
opacity: 0.7;
277286
278287
&:hover {
279288
background: var(--gray-50);

0 commit comments

Comments
 (0)