feat: 更新依赖并优化组件

- 在 pyproject.toml 中添加了 colorlog、langchain-deepseek 和 langchain-together 依赖。
- 修改了 chat_router.py 中的日志记录方式。
- 重命名 tools_factory.py 中的函数以更好地描述其功能。
- 更新了配置文件以支持新的模型选择。
- 优化了前端组件的样式和功能,包括侧边栏和消息输入框的交互体验。
This commit is contained in:
Wenjie Zhang 2025-05-20 20:49:50 +08:00
parent e803a79813
commit 44fad724bf
25 changed files with 545 additions and 181 deletions

View File

@ -5,13 +5,16 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"colorlog>=6.9.0",
"dashscope>=1.23.2",
"docx2txt>=0.9",
"fastapi>=0.115.12",
"flagembedding>=1.3.4",
"langchain-community>=0.3.22",
"langchain-deepseek>=0.1.3",
"langchain-huggingface>=0.2.0",
"langchain-openai>=0.3.14",
"langchain-together>=0.3.0",
"langgraph>=0.3.34",
"langgraph-checkpoint-sqlite>=2.0.7",
"langsmith>=0.3.37",

View File

@ -213,7 +213,7 @@ async def chat_agent(agent_name: str,
try:
async for msg, metadata in agent.stream_messages(messages, config_schema=runnable_config):
logger.debug(f"msg: {msg.model_dump()}, metadata: {metadata}")
# logger.debug(f"msg: {msg.model_dump()}, metadata: {metadata}")
if isinstance(msg, AIMessageChunk):
yield make_chunk(content=msg.content,
msg=msg.model_dump(),

View File

@ -49,8 +49,8 @@ class ChatbotConfiguration(Configuration):
"configurable": True,
"options": [
"zhipu/glm-4-plus",
"siliconflow/Qwen/QwQ-32B",
"siliconflow/deepseek-ai/DeepSeek-V3",
"siliconflow/Qwen/Qwen2.5-72B-Instruct",
"siliconflow/Qwen/Qwen2.5-7B-Instruct",
],
"description": "智能体的驱动模型"
},

View File

@ -1,18 +1,9 @@
import asyncio
import uuid
from typing import Any
from datetime import datetime
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
from langchain_community.tools.tavily_search import TavilySearchResults
from src.agents.registry import State, BaseAgent
from src.agents.utils import load_chat_model
from src.agents.react.configuration import ReActConfiguration, multiply
from src.agents.registry import BaseAgent
from src.agents.react.configuration import ReActConfiguration
class ReActAgent(BaseAgent):
name = "react"

View File

@ -133,17 +133,17 @@ def calculator(a: float, b: float, operation: str) -> float:
raise ValueError(f"Invalid operation: {operation}, only support add, subtract, multiply, divide")
@tool
def get_knowledge_graph(query: Annotated[str, "The query to get knowledge graph."]):
"""Use this to get knowledge graph."""
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]):
"""Use this to query knowledge graph."""
return graph_base.query_node(query, hops=2)
_TOOLS_REGISTRY = {
"calculator": calculator,
"get_knowledge_graph": get_knowledge_graph,
"Calculator": calculator,
"QueryKnowledgeGraph": query_knowledge_graph,
}
if config.enable_web_search:
_TOOLS_REGISTRY["TavilySearchResults"] = TavilySearchResults(max_results=10)
_TOOLS_REGISTRY["WebSearchWithTavily"] = TavilySearchResults(max_results=10)

View File

@ -30,6 +30,32 @@ def select_model(model_provider=None, model_name=None):
from src.models.chat_model import OpenModel
return OpenModel(model_name)
if model_provider == "deepseek":
from langchain_deepseek import ChatDeepSeek
return OpenAIBase(
api_key=os.getenv(model_info["env"][0]),
base_url=model_info["base_url"],
model_name=model_name,
chat_open_ai=ChatDeepSeek(
model=model_name,
api_key=os.getenv(model_info["env"][0]),
base_url=model_info["base_url"],
)
)
if model_provider == "together":
from langchain_together import ChatTogether
return OpenAIBase(
api_key=os.getenv(model_info["env"][0]),
base_url=model_info["base_url"],
model_name=model_name,
chat_open_ai=ChatTogether(
model=model_name,
api_key=os.getenv(model_info["env"][0]),
base_url=model_info["base_url"],
)
)
if model_provider == "custom":
model_info = next((x for x in config.custom_models if x["custom_id"] == model_name), None)
if model_info is None:

View File

@ -5,13 +5,13 @@ from src.utils import logger, get_docker_safe_url
from langchain_openai import ChatOpenAI
class OpenAIBase():
def __init__(self, api_key, base_url, model_name, **kwargs):
def __init__(self, api_key, base_url, model_name, chat_open_ai=None, **kwargs):
self.api_key = api_key
self.base_url = base_url
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.model_name = model_name
self.info = kwargs
self.chat_open_ai = ChatOpenAI(model=model_name,
self.chat_open_ai = chat_open_ai or ChatOpenAI(model=model_name,
api_key=api_key,
base_url=base_url)

View File

@ -70,6 +70,7 @@ MODEL_NAMES:
- meta-llama/Llama-3.3-70B-Instruct-Turbo
- meta-llama/Llama-3.3-70B-Instruct-Turbo-Free
- deepseek-ai/DeepSeek-R1-Distill-Llama-70B-free
- Qwen/QwQ-32B
dashscope:
name: 阿里百炼 (DashScope)

View File

@ -2,8 +2,10 @@ import logging
import os
from datetime import datetime
import pytz
from colorlog import ColoredFormatter
DATETIME = datetime.now().strftime('%Y-%m-%d-%H%M%S')
DATETIME = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d-%H%M%S')
# DATETIME = "debug" # 为了方便,调试的时候输出到 debug.log 文件
LOG_FILE = f'saves/log/project-{DATETIME}.log'
@ -18,20 +20,29 @@ def setup_logger(name, level=logging.DEBUG, console=True):
if logger.hasHandlers():
logger.handlers.clear()
# File handler for logging to a file
file_handler = logging.FileHandler(LOG_FILE)
# 文件日志(无颜色)
file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8')
file_handler.setLevel(level)
# Formatter for the logs
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
file_handler.setFormatter(formatter)
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Console handler for logging to the console (optional)
# 控制台日志(有颜色)
if console:
console_handler = logging.StreamHandler()
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
color_formatter = ColoredFormatter(
"%(log_color)s%(asctime)s - %(levelname)s - %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'bold_red',
}
)
console_handler.setFormatter(color_formatter)
logger.addHandler(console_handler)
return logger

46
uv.lock
View File

@ -425,6 +425,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
]
[[package]]
name = "colorlog"
version = "6.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
]
[[package]]
name = "cryptography"
version = "44.0.2"
@ -1282,6 +1294,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/40/aa440a7cd05f1dab5d7c91a1284eb776c3cf3eb59fa18ed39927650cfa38/langchain_core-0.3.59-py3-none-any.whl", hash = "sha256:9686baaff43f2c8175535da13faf40e6866769015e93130c3c1e4243e7244d70", size = 437656, upload-time = "2025-05-07T17:58:22.251Z" },
]
[[package]]
name = "langchain-deepseek"
version = "0.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langchain-openai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ed/7f/be5bcf99b3814214a02ac205bda66d49d55a7d5440d47223105cef5df063/langchain_deepseek-0.1.3.tar.gz", hash = "sha256:89dd6aa120fb50dcfcd3d593626d34c1c40deefe4510710d0807fcc19481adf5", size = 7860, upload-time = "2025-03-21T17:11:58.356Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/7d/51b60aa91fa77742fc461704e5a8497e856156ae878102e6942799a78915/langchain_deepseek-0.1.3-py3-none-any.whl", hash = "sha256:8588e826371b417fca65c02f4273b4061eb9815a7bfcd5eb05acaa40d603aa89", size = 7123, upload-time = "2025-03-21T17:11:57.481Z" },
]
[[package]]
name = "langchain-huggingface"
version = "0.2.0"
@ -1324,6 +1349,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/a3/3696ff2444658053c01b6b7443e761f28bb71217d82bb89137a978c5f66f/langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02", size = 32440, upload-time = "2025-04-04T14:03:50.6Z" },
]
[[package]]
name = "langchain-together"
version = "0.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "langchain-core" },
{ name = "langchain-openai" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/c4/64b92524121eaf4e805ade7ea68e71d52eab5e8fdad9b6b8e62f5a99eff4/langchain_together-0.3.0.tar.gz", hash = "sha256:c8a96377e49c065526435f766c6e1c7da3f7d054361326f079de8bd368ea76f2", size = 10247, upload-time = "2025-01-10T17:06:08.729Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/69/b3cbcf5b43acbc098c012ef75035fb0dc1e0f227f5161329ef9884a25ba4/langchain_together-0.3.0-py3-none-any.whl", hash = "sha256:4dcb4f6858c910c23d2268da1ed5f54e8cd01224ecf086dc7a8adbacdc6cb686", size = 12338, upload-time = "2025-01-10T17:06:06.678Z" },
]
[[package]]
name = "langdetect"
version = "1.0.9"
@ -4515,13 +4555,16 @@ name = "yuxi-know"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "colorlog" },
{ name = "dashscope" },
{ name = "docx2txt" },
{ name = "fastapi" },
{ name = "flagembedding" },
{ name = "langchain-community" },
{ name = "langchain-deepseek" },
{ name = "langchain-huggingface" },
{ name = "langchain-openai" },
{ name = "langchain-together" },
{ name = "langgraph" },
{ name = "langgraph-checkpoint-sqlite" },
{ name = "langsmith" },
@ -4549,13 +4592,16 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "colorlog", specifier = ">=6.9.0" },
{ name = "dashscope", specifier = ">=1.23.2" },
{ name = "docx2txt", specifier = ">=0.9" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "flagembedding", specifier = ">=1.3.4" },
{ name = "langchain-community", specifier = ">=0.3.22" },
{ name = "langchain-deepseek", specifier = ">=0.1.3" },
{ name = "langchain-huggingface", specifier = ">=0.2.0" },
{ name = "langchain-openai", specifier = ">=0.3.14" },
{ name = "langchain-together", specifier = ">=0.3.0" },
{ name = "langgraph", specifier = ">=0.3.34" },
{ name = "langgraph-checkpoint-sqlite", specifier = ">=2.0.7" },
{ name = "langsmith", specifier = ">=0.3.37" },

View File

@ -23,6 +23,7 @@
"echarts-gl": "^2.0.9",
"highlight.js": "^11.11.1",
"less": "^4.3.0",
"lucide-vue-next": "^0.511.0",
"marked": "^15.0.11",
"marked-highlight": "^2.2.1",
"md-editor-v3": "^5.5.1",

View File

@ -41,6 +41,9 @@ importers:
less:
specifier: ^4.3.0
version: 4.3.0
lucide-vue-next:
specifier: ^0.511.0
version: 0.511.0(vue@3.5.14)
marked:
specifier: ^15.0.11
version: 15.0.11
@ -1644,6 +1647,11 @@ packages:
peerDependencies:
vue: '>=3.0.1'
lucide-vue-next@0.511.0:
resolution: {integrity: sha512-VSv0F3pHniGN7JMMzDcLFNMQbl8381+shNnHwV8hi+El7xl2ZL8qdNuzPoiBViKk8mTKK5K3ZDfmE/wEcTZVIQ==}
peerDependencies:
vue: '>=3.0.1'
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@ -4008,6 +4016,10 @@ snapshots:
dependencies:
vue: 3.5.14
lucide-vue-next@0.511.0(vue@3.5.14):
dependencies:
vue: 3.5.14
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0

View File

@ -19,10 +19,10 @@
<div class="header__left">
<slot name="header-left" class="nav-btn"></slot>
<div class="toggle-sidebar nav-btn" v-if="!state.isSidebarOpen" @click="toggleSidebar">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
<PanelLeftOpen size="20" color="var(--gray-800)"/>
</div>
<div class="newchat nav-btn" @click="createNewChat" :disabled="state.isProcessingRequest || state.creatingNewChat">
<PlusCircleOutlined /> <span class="text" :class="{'hide-text': isMediumContainer}">新对话</span>
<MessageSquarePlus size="20" color="var(--gray-800)"/> <span class="text" :class="{'hide-text': isMediumContainer}">新对话</span>
</div>
</div>
<div class="header__center">
@ -87,7 +87,7 @@
@keydown="handleKeyDown"
/>
<div class="bottom-actions">
<p class="note">请注意辨别内容的可靠性</p>
<p class="note" @click="getAgentHistory">请注意辨别内容的可靠性</p>
</div>
</div>
</div>
@ -96,7 +96,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, watch, nextTick, computed, onUnmounted } from 'vue';
import { ref, reactive, onMounted, watch, nextTick, computed, onUnmounted, toRaw } from 'vue';
import {
RobotOutlined, SendOutlined, LoadingOutlined,
ThunderboltOutlined, ReloadOutlined, CheckCircleOutlined,
@ -107,6 +107,7 @@ import MessageInputComponent from '@/components/MessageInputComponent.vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
import { chatApi, threadApi } from '@/apis/auth_api'
import { PanelLeftOpen, MessageSquarePlus } from 'lucide-vue-next';
// propsagentId
const props = defineProps({
@ -458,6 +459,9 @@ const handleStreamResponseError = (error) => {
//
const processResponseChunk = async (data) => {
// console.log(":", data.msg);
// if (data.msg.additional_kwargs?.tool_calls) {
// console.log("tool_calls", data.msg.additional_kwargs.tool_calls);
// }
if (data.status === 'init') {
//
state.waitingServerResponse = false;
@ -465,12 +469,6 @@ const processResponseChunk = async (data) => {
onGoingConv.msgChunks[data.request_id] = [data.msg];
} else if (data.status === 'loading') {
// lastConv,undefined
const lastConv = convs.value.length > 0 ? convs.value[convs.value.length - 1] : null;
if (lastConv && lastConv.status === 'finished') {
await getAgentHistory();
}
if (data.msg.id) {
if (!onGoingConv.msgChunks[data.msg.id]) {
onGoingConv.msgChunks[data.msg.id] = []
@ -478,7 +476,7 @@ const processResponseChunk = async (data) => {
onGoingConv.msgChunks[data.msg.id].push(data.msg)
}
} else if (data.status === 'finished') {
await getAgentHistory();
// await getAgentHistory();
}
// await scrollToBottom();
};
@ -698,12 +696,19 @@ const handleKeyDown = (e) => {
}
};
const mergeMessageChunk = (chunks) => {
if (chunks.length === 0) return null;
// chunk
const result = {...chunks[0]};
for (const c of chunks) {
if (c.additional_kwargs?.tool_calls) {
console.warn("chunks", toRaw(c))
break;
}
}
// chunk
const result = JSON.parse(JSON.stringify(chunks[0]));
result.content = result.content || '';
// chunks
@ -716,7 +721,7 @@ const mergeMessageChunk = (chunks) => {
// chunk key, value, result[key] result
for (const key in chunk) {
if (!result[key]) {
result[key] = chunk[key]
result[key] = JSON.parse(JSON.stringify(chunk[key]));
}
}
@ -727,7 +732,7 @@ const mergeMessageChunk = (chunks) => {
for (const toolCall of chunk.additional_kwargs.tool_calls) {
const existingToolCall = result.additional_kwargs.tool_calls.find(
t => t.id === toolCall.id
t => (t.id === toolCall.id || t.index === toolCall.index)
);
if (existingToolCall) {
@ -735,7 +740,7 @@ const mergeMessageChunk = (chunks) => {
existingToolCall.function.arguments += toolCall.function.arguments;
} else {
// tool call
result.additional_kwargs.tool_calls.push({...toolCall});
result.additional_kwargs.tool_calls.push(JSON.parse(JSON.stringify(toolCall)));
}
}
}
@ -743,6 +748,9 @@ const mergeMessageChunk = (chunks) => {
if (result.type === 'AIMessageChunk') {
result.type = 'ai'
if (result.additional_kwargs?.tool_calls) {
result.tool_calls = result.additional_kwargs.tool_calls;
}
}
return result;
}

View File

@ -35,19 +35,19 @@
<span v-if="!toolCall.tool_call_result">
<LoadingOutlined /> &nbsp;
<span>正在调用工具: </span>
<span class="tool-name">{{ toolCall.name }}</span>
<span class="tool-name">{{ toolCall.name || toolCall.function.name }}</span>
</span>
<span v-else>
<ThunderboltOutlined /> 工具 <span class="tool-name">{{ toolCall.name }}</span> 执行完成
<ThunderboltOutlined /> 工具 <span class="tool-name">{{ toolCall.name || toolCall.function.name }}</span> 执行完成
</span>
</div>
<div class="tool-content" v-show="expandedToolCalls.has(toolCall.id)">
<div class="tool-params" v-if="toolCall.args">
<div class="tool-params" v-if="toolCall.args || toolCall.function.arguments">
<div class="tool-params-header">
参数:
</div>
<div class="tool-params-content">
<pre>{{ toolCall.args }}</pre>
<pre>{{ toolCall.args || toolCall.function.arguments }}</pre>
</div>
</div>
<div class="tool-params" v-if="toolCall.tool_call_result && toolCall.tool_call_result.content">
@ -302,6 +302,11 @@ const toggleToolCall = (toolCallId) => {
border-radius: 4px;
padding: 8px;
overflow-x: auto;
color: var(--gray-800);
pre {
margin: 0;
}
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<div class="chat" ref="chatContainer">
<div class="chat" ref="chatContainer" :class="{ 'refs-sidebar-open': refsSidebarVisible && refsSidebarPinned }">
<div class="chat-header">
<div class="header__left">
<div
@ -8,20 +8,26 @@
@click="state.isSidebarOpen = true"
>
<a-tooltip title="展开侧边栏" placement="right">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
<PanelLeftOpen size="20" color="var(--gray-800)"/>
</a-tooltip>
</div>
<div class="newchat nav-btn nav-btn-icon-only" @click="$emit('newconv')">
<a-tooltip title="新建对话" placement="right">
<PlusCircleOutlined />
<MessageSquarePlus size="20" color="var(--gray-800)"/>
</a-tooltip>
</div>
<ModelSelectorComponent class="nav-btn borderless" @select-model="handleModelSelect" />
<ModelSelectorComponent
class="nav-btn borderless max-width"
@select-model="handleModelSelect"
:model_name="configStore.config?.model_name"
:model_provider="configStore.config?.model_provider"
/>
</div>
<div class="header__right">
<div class="nav-btn text" @click="opts.showPanel = !opts.showPanel">
<component :is="opts.showPanel ? FolderOpenOutlined : FolderOutlined" /> <span class="text">选项</span>
<Ellipsis />
<!-- <span class="text">选项</span> -->
</div>
<div v-if="opts.showPanel" class="my-panal r0 top100 swing-in-top-fwd" ref="panel">
<div class="flex-center" @click="meta.summary_title = !meta.summary_title">
@ -128,6 +134,7 @@
:visible="refsSidebarVisible"
:latestRefs="currentRefs"
@update:visible="refsSidebarVisible = $event"
@pin-change="handleRefsPinChange"
/>
</div>
</template>
@ -135,31 +142,12 @@
<script setup>
import { reactive, ref, onMounted, toRefs, nextTick, onUnmounted, watch, computed } from 'vue'
import {
SendOutlined,
MenuOutlined,
FormOutlined,
LoadingOutlined,
BookOutlined,
BookFilled,
CompassOutlined,
ArrowUpOutlined,
CompassFilled,
GoldenFilled,
GoldOutlined,
SettingOutlined,
SettingFilled,
PlusCircleOutlined,
FolderOutlined,
FolderOpenOutlined,
GlobalOutlined,
FileTextOutlined,
BulbOutlined,
CaretRightOutlined,
DeploymentUnitOutlined,
PauseOutlined,
ReloadOutlined,
CopyOutlined
} from '@ant-design/icons-vue'
import { Ellipsis, PanelLeftOpen, MessageSquarePlus } from 'lucide-vue-next'
import { onClickOutside } from '@vueuse/core'
import { useConfigStore } from '@/stores/config'
import { useUserStore } from '@/stores/user'
@ -219,6 +207,14 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
const refsSidebarVisible = ref(false)
const currentRefs = ref({})
//
const refsSidebarPinned = ref(false)
//
const handleRefsPinChange = (pinned) => {
refsSidebarPinned.value = pinned
}
// refs
const handleOpenRefs = ({ type, refs }) => {
console.log('ChatComponent handleOpenRefs called with type:', type);
@ -759,6 +755,14 @@ const findLastIndex = (array, predicate) => {
}
</script>
<style lang="less">
.chat {
--refs-sidebar-floating-width: 700px;
--refs-sidebar-pinned-width: min(40%, 450px);
}
</style>
<style lang="less" scoped>
.chat {
position: relative;
@ -772,6 +776,11 @@ const findLastIndex = (array, predicate) => {
box-sizing: border-box;
flex: 5 5 200px;
overflow-y: scroll;
transition: padding-right 0.3s ease;
&.refs-sidebar-open {
padding-right: var(--refs-sidebar-pinned-width); /* 与侧边栏固定时的宽度一致 */
}
.chat-header {
user-select: none;
@ -1124,6 +1133,18 @@ const findLastIndex = (array, predicate) => {
}
// Scrollable menu styles moved to ModelSelectorComponent
@media (max-width: 768px) {
&.refs-sidebar-open {
padding-right: 280px; /* 中等屏幕上固定时的宽度 */
}
}
@media (max-width: 480px) {
&.refs-sidebar-open {
padding-right: 0; /* 小屏幕上不调整内容区域 */
}
}
</style>
<!-- Global styles for dropdown moved to ModelSelectorComponent -->

View File

@ -4,14 +4,14 @@
<div class="header-title">{{ currentAgentId }}</div>
<div class="header-actions">
<div class="toggle-sidebar nav-btn" v-if="isSidebarOpen" @click="toggleCollapse">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
<PanelLeftClose size="20" color="var(--gray-800)"/>
</div>
</div>
</div>
<div class="conversation-list-top">
<a-button type="text" @click="createNewChat" class="new-chat-btn">
<PlusCircleOutlined /> 新对话
</a-button>
<button type="text" @click="createNewChat" class="new-chat-btn">
新对话
</button>
</div>
<div class="conversation-list">
<a-spin v-if="loading" />
@ -55,12 +55,12 @@
<script setup>
import { ref, computed, onMounted, watch, h } from 'vue';
import {
PlusCircleOutlined,
DeleteOutlined,
EditOutlined,
MoreOutlined
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
import { PanelLeftClose, MessageSquarePlus } from 'lucide-vue-next';
const props = defineProps({
currentAgentId: {
@ -214,7 +214,9 @@ const toggleCollapse = () => {
border-radius: 16px;
background-color: var(--gray-300);
color: var(--main-color);
border: none;
transition: all 0.2s ease;
font-weight: 500;
&:hover {
background-color: var(--gray-400);

View File

@ -1,5 +1,5 @@
<template>
<div class="input-box" :class="customClasses">
<div class="input-box" :class="customClasses" @click="focusInput">
<div class="input-area">
<a-textarea
ref="inputRef"
@ -111,6 +111,12 @@ const handleSendOrStop = () => {
emit('send');
};
//
const focusInput = () => {
if (inputRef.value && !props.disabled) {
inputRef.value.focus();
}
};
// Wait for component to mount before setting up onStartTyping
onMounted(() => {
@ -147,13 +153,12 @@ onMounted(() => {
display: flex;
align-items: flex-end;
gap: 8px;
margin-bottom: 4px;
}
.user-input {
flex: 1;
min-height: 44px;
padding: 0.5rem 0;
padding: 0;
background-color: transparent;
border: none;
margin: 0;
@ -180,7 +185,6 @@ onMounted(() => {
.input-options {
display: flex;
padding: 8px 0 0;
margin-top: 6px;
border-top: 1px solid var(--gray-100);
.options__left,

View File

@ -1,11 +1,11 @@
<template>
<a-dropdown>
<a class="model-select" @click.prevent>
<BulbOutlined />
<a-tooltip :title="configStore.config?.model_name" placement="right">
<span class="model-text text"> {{ configStore.config?.model_name }} </span>
<!-- <BulbOutlined /> -->
<a-tooltip :title="model_name" placement="right">
<span class="model-text text"> {{ model_name }} </span>
</a-tooltip>
<span class="text" style="color: #aaa;">{{ configStore.config?.model_provider }} </span>
<span class="text" style="color: #aaa;">{{ model_provider }} </span>
</a>
<template #overlay>
<a-menu class="scrollable-menu">
@ -29,6 +29,17 @@ import { computed } from 'vue'
import { BulbOutlined } from '@ant-design/icons-vue'
import { useConfigStore } from '@/stores/config'
const props = defineProps({
model_name: {
type: String,
default: ''
},
model_provider: {
type: String,
default: ''
}
});
const configStore = useConfigStore()
const emit = defineEmits(['select-model'])
@ -50,7 +61,6 @@ const handleSelectModel = (provider, name) => {
<style lang="less" scoped>
.model-select {
max-width: 350px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -67,6 +77,10 @@ const handleSelectModel = (provider, name) => {
border: none;
}
&.max-width {
max-width: 380px;
}
.model-text {
overflow: hidden;
text-overflow: ellipsis;

View File

@ -123,6 +123,7 @@ const regenerateMessage = () => {
.refs {
display: flex;
margin-bottom: 20px;
margin-top: 10px;
color: var(--gray-500);
font-size: 13px;
gap: 10px;

View File

@ -1,17 +1,19 @@
<template>
<div>
<a-drawer
:open="visible"
title="参考信息"
:width="700"
:contentWrapperStyle="{ maxWidth: '100%'}"
placement="right"
class="refs-sidebar"
rootClassName="root"
@close="$emit('update:visible', false)"
@afterOpenChange="handleAfterVisibleChange"
>
<a-tabs v-model:activeKey="activeTab">
<div class="refs-sidebar-container" :class="{ 'visible': visible, 'pinned': isPinned }">
<div class="overlay" v-if="visible && !isPinned" @click="$emit('update:visible', false)"></div>
<div class="refs-sidebar">
<div class="sidebar-header">
<h3>参考信息</h3>
<div class="sidebar-controls">
<a-button type="text" @click="togglePin" class="pin-button">
<PushpinOutlined :class="{ 'pinned': isPinned }" />
</a-button>
<a-button type="text" @click="$emit('update:visible', false)">
<CloseOutlined />
</a-button>
</div>
</div>
<a-tabs v-model:activeKey="activeTab" class="refs-tabs">
<!-- 关系图 -->
<a-tab-pane key="graph" tab="关系图" :disabled="!hasGraphData">
<div v-if="hasGraphData" class="graph-container">
@ -24,23 +26,22 @@
<!-- 网页搜索 -->
<a-tab-pane key="webSearch" tab="网页搜索" :disabled="!hasWebSearchData">
<div v-if="hasWebSearchData" class="results-list">
<div v-for="result in latestRefs.web_search.results" :key="result.url" class="result-item">
<div class="result-meta">
<div class="score-info">
<span>
<strong>相关度</strong>
<a-progress :percent="getPercent(result.score)"/>
</span>
</div>
<div v-if="hasWebSearchData" class="web-results-list">
<div v-for="result in latestRefs.web_search.results" :key="result.url" class="web-result-card">
<div class="card-header">
<h3 class="result-title">
<a :href="result.url" target="_blank">{{ result.title }}</a>
</h3>
</div>
<div class="result-content">
<h3 class="result-title">{{ result.title }}</h3>
<div class="result-url">
<a :href="result.url" target="_blank">{{ result.url }}</a>
</div>
<div class="card-body">
<div class="result-text">{{ result.content }}</div>
</div>
<div class="card-footer">
<div class="score-info">
<span>相关度{{ getPercent(result.score) }}%</span>
<span>来源{{ getDomain(result.url) }}</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-data">
@ -89,7 +90,7 @@
</div>
</a-tab-pane>
</a-tabs>
</a-drawer>
</div>
</div>
</template>
@ -98,6 +99,8 @@ import { ref, computed, reactive, watch, nextTick } from 'vue'
import {
FileOutlined,
ClockCircleOutlined,
CloseOutlined,
PushpinOutlined
} from '@ant-design/icons-vue'
import GraphContainer from './GraphContainer.vue'
@ -112,12 +115,21 @@ const props = defineProps({
}
})
const emit = defineEmits(['update:visible'])
const emit = defineEmits(['update:visible', 'pin-change'])
//
const activeTab = ref('graph')
const activeFiles = ref([])
//
const isPinned = ref(false)
//
const togglePin = () => {
isPinned.value = !isPinned.value
emit('pin-change', isPinned.value)
}
//
const hasGraphData = computed(() => {
try {
@ -217,6 +229,10 @@ const getPercent = (value) => {
return parseFloat((value * 100).toFixed(2))
}
const getDomain = (url) => {
return url.split('/')[2]
}
//
const formatDate = (timestamp) => {
return new Date(timestamp * 1000).toLocaleString()
@ -258,25 +274,21 @@ const setActiveTab = (tab) => {
// ref
const graphContainerRef = ref(null);
//
const handleAfterVisibleChange = (visible) => {
console.log('RefsSidebar afterOpenChange fired, visible:', visible, 'activeTab:', activeTab.value);
//
watch(() => props.visible, (visible) => {
if (!visible) return;
//
nextTick(() => {
if (activeTab.value === 'graph' && hasGraphData.value) {
console.log('Refreshing graph after drawer opened');
console.log('Refreshing graph after sidebar becomes visible');
//
if (graphContainerRef.value && graphContainerRef.value.refreshGraph) {
graphContainerRef.value.refreshGraph();
}
} else {
console.log('Current active tab is', activeTab.value, 'no need to refresh graph');
}
});
};
});
//
watch(activeTab, (newTab) => {
@ -309,7 +321,109 @@ defineExpose({
</script>
<style lang="less" scoped>
.refs-sidebar {
.refs-sidebar-container {
position: fixed;
top: 0;
right: calc(-1 * var(--refs-sidebar-floating-width)); /* 初始状态隐藏在右侧 - 使用更大宽度 */
width: var(--refs-sidebar-floating-width); /* 未固定时的宽度 */
height: 100vh;
background: white;
z-index: 1000;
transition: right 0.3s ease, width 0.3s ease; /* 添加宽度过渡效果 */
display: flex;
flex-direction: column;
user-select: text;
&::before {
z-index: 999; // z-index
}
//
&::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
z-index: -1;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
&.visible:not(.pinned)::before {
opacity: 1;
pointer-events: auto;
cursor: pointer;
}
&.visible {
right: 0; /* 显示时移动到可见区域 */
}
&.pinned {
width: var(--refs-sidebar-pinned-width); /* 固定时的宽度 */
box-shadow: none;
border-left: 1px solid var(--gray-200);
}
&.visible:not(.pinned) {
box-shadow: -2px 0 15px rgba(0, 0, 0, 0.15);
.refs-sidebar {
background-color: white;
}
}
.refs-sidebar {
height: 100%;
display: flex;
flex-direction: column;
// padding: 0 16px;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-200);
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.sidebar-controls {
display: flex;
align-items: center;
.pin-button {
margin-right: 8px;
.pinned {
color: var(--main-color);
transform: rotate(45deg);
}
}
}
}
.refs-tabs {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 0 16px;
}
.ant-tabs-content {
flex: 1;
overflow-y: auto;
}
.empty-data {
display: flex;
justify-content: center;
@ -317,7 +431,7 @@ defineExpose({
height: 200px;
color: #999;
font-size: 14px;
background-color: #f9f9f9;
background-color: var(--gray-100);
border-radius: 4px;
}
@ -333,7 +447,7 @@ defineExpose({
display: flex;
justify-content: space-between;
padding: 12px 16px;
background-color: #f5f5f5;
background-color: var(--gray-100);
border-radius: 4px;
margin-bottom: 16px;
@ -343,9 +457,19 @@ defineExpose({
}
}
// LESS mixin
.result-text-mixin() {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
color: var(--gray-800);
}
//
.results-list {
.result-item {
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--gray-200);
padding: 16px 0;
&:last-child {
@ -369,7 +493,7 @@ defineExpose({
strong {
margin-right: 8px;
white-space: nowrap;
color: #666;
color: var(--gray-700);
}
.ant-progress {
@ -378,7 +502,7 @@ defineExpose({
margin-inline: 10px;
.ant-progress-bg {
background-color: #666;
background-color: var(--gray-500);
}
}
}
@ -386,7 +510,7 @@ defineExpose({
.result-id {
font-size: 12px;
color: #999;
color: var(--gray-600);
margin-bottom: 8px;
}
}
@ -396,31 +520,110 @@ defineExpose({
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
color: var(--gray-800);
}
.result-url {
font-size: 12px;
color: #0563e7;
color: var(--main-color);
margin-bottom: 8px;
a {
color: #0563e7;
color: var(--main-color);
word-break: break-all;
}
}
.result-text {
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
background-color: #f9f9f9;
.result-text-mixin();
background-color: var(--gray-100);
padding: 12px;
border-radius: 4px;
border: 1px solid #e8e8e8;
border: 1px solid var(--gray-200);
}
}
}
//
.web-results-list {
display: flex;
flex-direction: column;
gap: 16px;
.web-result-card {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid var(--gray-200);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.card-header {
padding: 16px 16px 0;
.result-title {
margin-bottom: 8px;
line-height: 1.4;
}
}
.card-body {
padding: 0 16px;
.result-text {
.result-text-mixin();
margin-bottom: 16px;
max-height: 200px;
overflow-y: auto;
}
}
.card-footer {
background-color: var(--gray-100);
padding: 12px 16px;
border-top: 1px solid var(--gray-200);
.score-info {
display: flex;
justify-content: space-between;
span {
font-size: 13px;
color: var(--gray-600);
}
}
}
}
}
@media (max-width: 768px) {
width: 500px; /* 中等屏幕上未固定时的宽度 */
right: -500px;
&.pinned {
width: 280px; /* 中等屏幕上固定时的宽度 */
}
}
@media (max-width: 480px) {
width: 100%; /* 小屏幕上的宽度 */
right: -100%;
&.pinned {
width: 100%; /* 小屏幕上固定时也是全宽 */
}
}
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
</style>

View File

@ -3,7 +3,7 @@
<a-dropdown :trigger="['click']" v-if="userStore.isLoggedIn">
<div class="user-info-dropdown" :data-align="showRole ? 'left' : 'center'">
<div class="user-avatar">
<UserOutlined />
<CircleUser />
<!-- <div class="user-role-badge" :class="userRoleClass"></div> -->
</div>
<div v-if="showRole">{{ userStore.username }}</div>
@ -18,13 +18,13 @@
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="logout">
<LogoutOutlined /> 退出登录
<LogoutOutlined /> &nbsp;退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button v-else type="link" @click="goToLogin">
<UserOutlined /> 登录
<UserRoundCheck /> 登录
</a-button>
</div>
</template>
@ -35,6 +35,7 @@ import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
import { UserOutlined, LogoutOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { CircleUser, LogOut } from 'lucide-vue-next';
const router = useRouter();
const userStore = useUserStore();
@ -94,6 +95,7 @@ const goToLogin = () => {
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-800);
// margin-bottom: 16px;
}

View File

@ -2,29 +2,12 @@
import { ref, reactive, KeepAlive, onMounted, computed } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import {
MessageOutlined,
MessageFilled,
SettingOutlined,
SettingFilled,
BookOutlined,
BookFilled,
GithubOutlined,
FolderOutlined,
FolderFilled,
GoldOutlined,
GoldFilled,
ToolFilled,
ToolOutlined,
BugOutlined,
ProjectFilled,
ProjectOutlined,
StarFilled,
StarOutlined,
ExclamationCircleOutlined,
RobotOutlined,
RobotFilled,
ApiOutlined,
} from '@ant-design/icons-vue'
import { Bot, Waypoints, LibraryBig, MessageSquareMore, Settings } from 'lucide-vue-next';
import { useConfigStore } from '@/stores/config'
import { useDatabaseStore } from '@/stores/database'
import DebugComponent from '@/components/DebugComponent.vue'
@ -82,24 +65,24 @@ console.log(route)
const mainList = [{
name: '对话',
path: '/chat',
icon: MessageOutlined,
activeIcon: MessageFilled,
icon: MessageSquareMore,
activeIcon: MessageSquareMore,
}, {
name: '智能体',
path: '/agent',
icon: ToolOutlined,
activeIcon: ToolFilled,
icon: Bot,
activeIcon: Bot,
}, {
name: '图谱',
path: '/graph',
icon: ProjectOutlined,
activeIcon: ProjectFilled,
icon: Waypoints,
activeIcon: Waypoints,
// hidden: !configStore.config.enable_knowledge_graph,
}, {
name: '知识库',
path: '/database',
icon: BookOutlined,
activeIcon: BookFilled,
icon: LibraryBig,
activeIcon: LibraryBig,
// hidden: !configStore.config.enable_knowledge_base,
}
]
@ -145,7 +128,7 @@ const mainList = [{
v-show="!item.hidden"
class="nav-item"
active-class="active">
<component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" />
<component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" size="22"/>
<span class="text">{{item.name}}</span>
</RouterLink>
@ -162,7 +145,7 @@ const mainList = [{
<div class="github nav-item">
<a-tooltip placement="right">
<template #title>GitHub</template>
<template #title>欢迎 Star</template>
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank" class="github-link">
<GithubOutlined class="icon" style="color: #222;"/>
<span v-if="githubStars > 0" class="github-stars">
@ -191,7 +174,7 @@ const mainList = [{
<RouterLink class="nav-item setting" to="/setting" active-class="active">
<a-tooltip placement="right">
<template #title>设置</template>
<component class="icon" :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
<Settings />
</a-tooltip>
</RouterLink>
</div>
@ -484,6 +467,13 @@ div.header, #app-router-view {
.icon {
margin-right: 8px;
font-size: 15px; //
border: none;
outline: none;
&:focus, &:active {
border: none;
outline: none;
}
}
.text {

View File

@ -75,8 +75,17 @@
class="config-item"
>
<p v-if="value.description" class="description">{{ value.description }}</p>
<div v-if="key === 'model'" class="agent-model">
<p><small>注意部分模型对于 Tool Calling 的支持不稳定建议采用{{ value.options }} </small></p>
<ModelSelectorComponent
@select-model="handleModelChange"
:model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''"
:model_provider="agentConfig[key] ? agentConfig[key].split('/')[0] : ''"
/>
</div>
<a-switch
v-if="typeof agentConfig[key] === 'boolean'"
v-else-if="typeof agentConfig[key] === 'boolean'"
v-model:checked="agentConfig[key]"
/>
<a-textarea
@ -155,6 +164,7 @@ import {
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import { useUserStore } from '@/stores/user';
import { chatApi } from '@/apis/auth_api';
import { systemConfigApi } from '@/apis/admin_api';
@ -285,6 +295,10 @@ const loadAgentConfig = async () => {
}
};
const handleModelChange = (data) => {
agentConfig.value.model = `${data.provider}/${data.name}`;
}
//
const saveConfig = async () => {
if (!selectedAgentId.value) {
@ -537,6 +551,10 @@ const toggleSidebar = () => {
text-overflow: ellipsis;
}
.agent-model {
width: 100%;
}
.content {
flex: 1;
overflow: hidden;

View File

@ -5,7 +5,7 @@
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
<span class="header-title">Yuxi-Know</span>
<div class="action close" @click="state.isSidebarOpen = false">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
<PanelLeftClose size="20" color="var(--gray-800)"/>
</div>
</div>
<div class="conversation-list">
@ -14,7 +14,7 @@
:key="index"
:class="{ active: curConvId === index }"
@click="goToConversation(index)">
<div class="conversation__title"><CommentOutlined /> &nbsp;{{ state.title }}</div>
<div class="conversation__title">{{ state.title }}</div>
<div class="conversation__delete" @click.stop="delConv(index)"><DeleteOutlined /></div>
</div>
</div>
@ -29,8 +29,9 @@
<script setup>
import { reactive, ref, watch, onMounted } from 'vue'
import { FormOutlined, MenuOutlined, DeleteOutlined, CommentOutlined } from '@ant-design/icons-vue'
import { DeleteOutlined } from '@ant-design/icons-vue'
import ChatComponent from '@/components/ChatComponent.vue'
import { MessageSquareMore, PanelLeftClose } from 'lucide-vue-next'
const convs = reactive(JSON.parse(localStorage.getItem('chat-convs')) || [
{

View File

@ -21,7 +21,11 @@
<div class="section">
<div class="card card-select">
<span class="label">对话模型</span>
<ModelSelectorComponent @select-model="handleChatModelSelect" />
<ModelSelectorComponent
@select-model="handleChatModelSelect"
:model_name="configStore.config?.model_name"
:model_provider="configStore.config?.model_provider"
/>
</div>
<div class="card card-select">
<span class="label">{{ items?.embed_model.des }}</span>