feat(web): 添加消息复制功能并优化反馈组件图标
This commit is contained in:
parent
abf4414a0f
commit
dede6679db
@ -11,10 +11,14 @@
|
||||
- 文档解析部分的 markdown 中的图片替换为内部可访问的链接 (2/4)
|
||||
- chat_model 的 call 需要异步
|
||||
- 优化chunk逻辑,移除 QA 分割,集成到普通分块中
|
||||
- 考虑修改附件的处理逻辑,考虑使用文件系统,将附件解析后放到文件系统中,智能体按需读取,用户可以使用 @ 来引用附件,例如 @file:reports.md 相较于现在的处理逻辑感觉会更加自然一点。在此之前可能还要考虑文件系统的后端支持问题
|
||||
- skills 如何实现还需要继续调研
|
||||
|
||||
### Bugs
|
||||
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
|
||||
- 部分 local 的 mcp server 无法正常加载,但是建议在项目外部启动 mcp 服务器,然后通过 sse 的方式使用。
|
||||
- DeepSeek 官方接口适配会出现问题
|
||||
- 部分推理后端与 langchain 的适配有问题:
|
||||
- 目前的知识库的图片存在公开访问风险
|
||||
- 工具传递给模型的时候,使用英文,但部分模型不支持中文函数名(如gpt-4o-mini)
|
||||
- 首页加载的问题
|
||||
|
||||
@ -12,8 +12,8 @@ from src.utils import logger
|
||||
|
||||
|
||||
@tool
|
||||
async def text_to_img_qwen(text: str) -> str:
|
||||
"""(用来测试文件存储)使用模型生成图片, 会返回图片的URL"""
|
||||
async def text_to_img_demo(text: str) -> str:
|
||||
"""【测试用】使用模型生成图片, 会返回图片的URL"""
|
||||
|
||||
url = "https://api.siliconflow.cn/v1/images/generations"
|
||||
|
||||
@ -27,13 +27,13 @@ async def text_to_img_qwen(text: str) -> str:
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
response_json = response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate image with Kolors: {e}")
|
||||
logger.error(f"Failed to generate image with: {e}")
|
||||
raise ValueError(f"Image generation failed: {e}")
|
||||
|
||||
try:
|
||||
image_url = response_json["images"][0]["url"]
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
logger.error(f"Failed to parse image URL from Kolors response: {e}, {response_json=}")
|
||||
logger.error(f"Failed to parse image URL from response: {e}, {response_json=}")
|
||||
raise ValueError(f"Image URL extraction failed: {e}")
|
||||
|
||||
# 2. Upload to MinIO (Simplified)
|
||||
@ -51,6 +51,6 @@ async def text_to_img_qwen(text: str) -> str:
|
||||
def get_tools() -> list[Any]:
|
||||
"""获取所有可运行的工具(给大模型使用)"""
|
||||
tools = get_buildin_tools()
|
||||
tools.append(text_to_img_qwen)
|
||||
tools.append(text_to_img_demo)
|
||||
tools.extend(get_mysql_tools())
|
||||
return tools
|
||||
|
||||
@ -107,12 +107,12 @@
|
||||
--color-text-tertiary: rgba(255, 255, 255, 0.45);
|
||||
|
||||
/* Shadow System - 深色模式阴影系统 */
|
||||
--shadow-0: rgba(0, 0, 0, 0.1);
|
||||
--shadow-1: rgba(0, 0, 0, 0.2);
|
||||
--shadow-2: rgba(0, 0, 0, 0.3);
|
||||
--shadow-3: rgba(0, 0, 0, 0.4);
|
||||
--shadow-4: rgba(0, 0, 0, 0.5);
|
||||
--shadow-5: rgba(0, 0, 0, 0.6);
|
||||
--shadow-0: rgba(255, 255, 255, 0.02);
|
||||
--shadow-1: rgba(255, 255, 255, 0.05);
|
||||
--shadow-2: rgba(255, 255, 255, 0.08);
|
||||
--shadow-3: rgba(255, 255, 255, 0.12);
|
||||
--shadow-4: rgba(255, 255, 255, 0.16);
|
||||
--shadow-5: rgba(255, 255, 255, 0.20);
|
||||
|
||||
|
||||
--color-trans-light: rgba(0, 0, 0, 0.85);
|
||||
|
||||
@ -684,7 +684,8 @@ const resetConfig = async () => {
|
||||
background-color: var(--gray-25);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 1px 1px var(--shadow-2);
|
||||
border: 1px solid var(--gray-100);
|
||||
// box-shadow: 0px 0px 2px var(--shadow-3);
|
||||
|
||||
:deep(label.form_item_model) {
|
||||
font-weight: 600;
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
</div>
|
||||
<div class="message-box" :class="[message.type, customClasses]">
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="message.type === 'human'" class="message-copy-btn human-copy" @click="copyToClipboard(message.content)" :class="{ 'is-copied': isCopied }">
|
||||
<Check v-if="isCopied" size="14" />
|
||||
<Copy v-else size="14" />
|
||||
</div>
|
||||
<p v-if="message.type === 'human'" class="message-text">{{ message.content }}</p>
|
||||
|
||||
<p v-else-if="message.type === 'system'" class="message-text-system">{{ message.content }}</p>
|
||||
@ -102,7 +106,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { CaretRightOutlined, ThunderboltOutlined, LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import RefsComponent from '@/components/RefsComponent.vue'
|
||||
import { Loader, CircleCheckBig } from 'lucide-vue-next';
|
||||
import { Loader, CircleCheckBig, Copy, Check } from 'lucide-vue-next';
|
||||
import { ToolResultRenderer } from '@/components/ToolCallingResult'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
@ -150,6 +154,21 @@ const editorRef = ref()
|
||||
|
||||
const emit = defineEmits(['retry', 'retryStoppedMessage', 'openRefs']);
|
||||
|
||||
// 复制状态
|
||||
const isCopied = ref(false);
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
isCopied.value = true;
|
||||
setTimeout(() => {
|
||||
isCopied.value = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 推理面板展开状态
|
||||
const reasoningActiveKey = ref(['hide']);
|
||||
const expandedToolCalls = ref(new Set()); // 展开的工具调用集合
|
||||
@ -321,6 +340,38 @@ const toggleToolCall = (toolCallId) => {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.message-copy-btn {
|
||||
cursor: pointer;
|
||||
color: var(--gray-400);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
&.is-copied {
|
||||
color: var(--color-success-500);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.human-copy {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.message-copy-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text-system {
|
||||
max-width: 100%;
|
||||
margin-bottom: 0;
|
||||
|
||||
@ -264,7 +264,7 @@ onUnmounted(() => {
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0px 4px var(--shadow-3);
|
||||
box-shadow: 0 0 4px 0px var(--shadow-2);
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
@ -272,10 +272,10 @@ onUnmounted(() => {
|
||||
border-radius: 6px;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
background: var(--color-trans-light);
|
||||
|
||||
&:hover, &:focus, &-focused {
|
||||
background: #fff;
|
||||
background: var(--main-0);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,7 @@
|
||||
@click="likeThisResponse(msg)"
|
||||
:title="feedbackState.hasSubmitted && feedbackState.rating === 'like' ? '已点赞' : '点赞'"
|
||||
>
|
||||
<LikeFilled v-if="feedbackState.rating === 'like'" />
|
||||
<LikeOutlined v-else />
|
||||
<ThumbsUp size="12" :fill="feedbackState.rating === 'like' ? 'currentColor' : 'none'" />
|
||||
</span>
|
||||
<span
|
||||
class="item btn"
|
||||
@ -17,23 +16,24 @@
|
||||
@click="dislikeThisResponse(msg)"
|
||||
:title="feedbackState.hasSubmitted && feedbackState.rating === 'dislike' ? '已点踩' : '点踩'"
|
||||
>
|
||||
<DislikeFilled v-if="feedbackState.rating === 'dislike'" />
|
||||
<DislikeOutlined v-else />
|
||||
<ThumbsDown size="12" :fill="feedbackState.rating === 'dislike' ? 'currentColor' : 'none'" />
|
||||
</span>
|
||||
<!-- 模型名称 -->
|
||||
<span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)">
|
||||
<BulbOutlined /> {{ getModelName(msg) }}
|
||||
<Bot size="12" /> {{ getModelName(msg) }}
|
||||
</span>
|
||||
<!-- 复制 -->
|
||||
<span
|
||||
v-if="showKey('copy')"
|
||||
class="item btn" @click="copyText(msg.content)" title="复制"><CopyOutlined />
|
||||
class="item btn" @click="copyText(msg.content)" title="复制">
|
||||
<Check v-if="isCopied" size="12" />
|
||||
<Copy v-else size="12" />
|
||||
</span>
|
||||
|
||||
<!-- 重试 -->
|
||||
<span
|
||||
v-if="showKey('regenerate')"
|
||||
class="item btn" @click="regenerateMessage()" title="重新生成"><ReloadOutlined />
|
||||
class="item btn" @click="regenerateMessage()" title="重新生成"><RotateCcw size="12" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,14 +63,13 @@ import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
CopyOutlined,
|
||||
BulbOutlined,
|
||||
ReloadOutlined,
|
||||
LikeOutlined,
|
||||
LikeFilled,
|
||||
DislikeOutlined,
|
||||
DislikeFilled,
|
||||
} from '@ant-design/icons-vue'
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Bot,
|
||||
Copy,
|
||||
Check,
|
||||
RotateCcw,
|
||||
} from 'lucide-vue-next'
|
||||
import { agentApi } from '@/apis'
|
||||
|
||||
const emit = defineEmits(['retry', 'openRefs']);
|
||||
@ -110,12 +109,19 @@ const showKey = (key) => {
|
||||
return props.showRefs.includes(key)
|
||||
}
|
||||
|
||||
// 复制状态
|
||||
const isCopied = ref(false)
|
||||
|
||||
// 定义 copy 方法
|
||||
const copyText = async (text) => {
|
||||
if (isSupported) {
|
||||
try {
|
||||
await copy(text)
|
||||
message.success('文本已复制到剪贴板')
|
||||
isCopied.value = true
|
||||
setTimeout(() => {
|
||||
isCopied.value = false
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
message.error('复制失败,请手动复制')
|
||||
@ -255,11 +261,16 @@ const cancelDislike = () => {
|
||||
.item {
|
||||
background: var(--gray-50);
|
||||
color: var(--gray-700);
|
||||
padding: 2px 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
line-height: 1;
|
||||
|
||||
&.btn {
|
||||
cursor: pointer;
|
||||
@ -272,8 +283,6 @@ const cancelDislike = () => {
|
||||
|
||||
// Disabled state - when feedback has been submitted
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-50);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user