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