mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-07 22:36:02 +00:00
Merge branch 'master' into feat/streaming
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
@@ -85,8 +87,9 @@ docker compose up -d
|
||||
| --- | --- | --- |
|
||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
||||
| 微信 | ✅ | |
|
||||
| 企业微信 | ✅ | |
|
||||
| 企微对外客服 | ✅ | |
|
||||
| 个人微信 | ✅ | |
|
||||
| 微信公众号 | ✅ | |
|
||||
| 飞书 | ✅ | |
|
||||
| 钉钉 | ✅ | |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class QQOfficialClient:
|
||||
return {'code': 0, 'message': 'success'}
|
||||
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||
return {'error': str(e)}, 400
|
||||
|
||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||
@@ -168,7 +168,6 @@ class QQOfficialClient:
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
|
||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {
|
||||
@@ -193,7 +192,6 @@ class QQOfficialClient:
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
|
||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {
|
||||
@@ -209,7 +207,7 @@ class QQOfficialClient:
|
||||
if response.status_code == 200:
|
||||
return
|
||||
else:
|
||||
await self.logger.error(f"发送群聊消息失败:{response.json()}")
|
||||
await self.logger.error(f'发送群聊消息失败:{response.json()}')
|
||||
raise Exception(response.read().decode())
|
||||
|
||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
||||
@@ -217,7 +215,6 @@ class QQOfficialClient:
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
|
||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
||||
async with httpx.AsyncClient() as client:
|
||||
headers = {
|
||||
@@ -240,7 +237,6 @@ class QQOfficialClient:
|
||||
"""发送频道私聊消息"""
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
|
||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
||||
async with httpx.AsyncClient() as client:
|
||||
|
||||
@@ -34,7 +34,6 @@ class SlackClient:
|
||||
|
||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
# 处理私信
|
||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||
@@ -52,7 +51,7 @@ class SlackClient:
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||
raise (e)
|
||||
|
||||
async def _handle_message(self, event: SlackEvent):
|
||||
@@ -82,7 +81,7 @@ class SlackClient:
|
||||
self.bot_user_id = response['message']['bot_id']
|
||||
return
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in send_message: {e}")
|
||||
await self.logger.error(f'Error in send_message: {e}')
|
||||
raise e
|
||||
|
||||
async def send_message_to_one(self, text: str, user_id: str):
|
||||
@@ -93,7 +92,7 @@ class SlackClient:
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in send_message: {traceback.format_exc()}")
|
||||
await self.logger.error(f'Error in send_message: {traceback.format_exc()}')
|
||||
raise e
|
||||
|
||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||
|
||||
@@ -12,12 +12,9 @@ class UserApi:
|
||||
|
||||
return get_json(base_url=url, token=self.token)
|
||||
|
||||
def get_qr_code(self, recover:bool=True, style:int=8):
|
||||
def get_qr_code(self, recover: bool = True, style: int = 8):
|
||||
"""获取自己的二维码"""
|
||||
param = {
|
||||
"Recover": recover,
|
||||
"Style": style
|
||||
}
|
||||
param = {'Recover': recover, 'Style': style}
|
||||
url = f'{self.base_url}/user/GetMyQRCode'
|
||||
return post_json(base_url=url, token=self.token, data=param)
|
||||
|
||||
@@ -26,12 +23,8 @@ class UserApi:
|
||||
url = f'{self.base_url}/equipment/GetSafetyInfo'
|
||||
return post_json(base_url=url, token=self.token)
|
||||
|
||||
|
||||
|
||||
async def update_head_img(self, head_img_base64):
|
||||
async def update_head_img(self, head_img_base64):
|
||||
"""修改头像"""
|
||||
param = {
|
||||
"Base64": head_img_base64
|
||||
}
|
||||
param = {'Base64': head_img_base64}
|
||||
url = f'{self.base_url}/user/UploadHeadImage'
|
||||
return await async_request(base_url=url, token_key=self.token, json=param)
|
||||
return await async_request(base_url=url, token_key=self.token, json=param)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from libs.wechatpad_api.api.login import LoginApi
|
||||
from libs.wechatpad_api.api.friend import FriendApi
|
||||
from libs.wechatpad_api.api.message import MessageApi
|
||||
@@ -7,9 +6,6 @@ from libs.wechatpad_api.api.downloadpai import DownloadApi
|
||||
from libs.wechatpad_api.api.chatroom import ChatRoomApi
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class WeChatPadClient:
|
||||
def __init__(self, base_url, token, logger=None):
|
||||
self._login_api = LoginApi(base_url, token)
|
||||
@@ -20,16 +16,16 @@ class WeChatPadClient:
|
||||
self._chatroom_api = ChatRoomApi(base_url, token)
|
||||
self.logger = logger
|
||||
|
||||
def get_token(self,admin_key, day: int):
|
||||
'''获取token'''
|
||||
def get_token(self, admin_key, day: int):
|
||||
"""获取token"""
|
||||
return self._login_api.get_token(admin_key, day)
|
||||
|
||||
def get_login_qr(self, Proxy:str=""):
|
||||
def get_login_qr(self, Proxy: str = ''):
|
||||
"""登录二维码"""
|
||||
return self._login_api.get_login_qr(Proxy=Proxy)
|
||||
|
||||
def awaken_login(self, Proxy:str=""):
|
||||
'''唤醒登录'''
|
||||
def awaken_login(self, Proxy: str = ''):
|
||||
"""唤醒登录"""
|
||||
return self._login_api.wake_up_login(Proxy=Proxy)
|
||||
|
||||
def log_out(self):
|
||||
@@ -40,59 +36,57 @@ class WeChatPadClient:
|
||||
"""获取登录状态"""
|
||||
return self._login_api.get_login_status()
|
||||
|
||||
def send_text_message(self, to_wxid, message, ats: list=[]):
|
||||
def send_text_message(self, to_wxid, message, ats: list = []):
|
||||
"""发送文本消息"""
|
||||
return self._message_api.post_text(to_wxid, message, ats)
|
||||
return self._message_api.post_text(to_wxid, message, ats)
|
||||
|
||||
def send_image_message(self, to_wxid, img_url, ats: list=[]):
|
||||
def send_image_message(self, to_wxid, img_url, ats: list = []):
|
||||
"""发送图片消息"""
|
||||
return self._message_api.post_image(to_wxid, img_url, ats)
|
||||
return self._message_api.post_image(to_wxid, img_url, ats)
|
||||
|
||||
def send_voice_message(self, to_wxid, voice_data, voice_forma, voice_duration):
|
||||
"""发送音频消息"""
|
||||
return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration)
|
||||
return self._message_api.post_voice(to_wxid, voice_data, voice_forma, voice_duration)
|
||||
|
||||
def send_app_message(self, to_wxid, app_message, type):
|
||||
"""发送app消息"""
|
||||
return self._message_api.post_app_msg(to_wxid, app_message, type)
|
||||
return self._message_api.post_app_msg(to_wxid, app_message, type)
|
||||
|
||||
def send_emoji_message(self, to_wxid, emoji_md5, emoji_size):
|
||||
"""发送emoji消息"""
|
||||
return self._message_api.post_emoji(to_wxid,emoji_md5,emoji_size)
|
||||
return self._message_api.post_emoji(to_wxid, emoji_md5, emoji_size)
|
||||
|
||||
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
|
||||
"""撤回消息"""
|
||||
return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time)
|
||||
return self._message_api.revoke_msg(to_wxid, msg_id, new_msg_id, create_time)
|
||||
|
||||
def get_profile(self):
|
||||
"""获取用户信息"""
|
||||
return self._user_api.get_profile()
|
||||
|
||||
def get_qr_code(self, recover:bool=True, style:int=8):
|
||||
def get_qr_code(self, recover: bool = True, style: int = 8):
|
||||
"""获取用户二维码"""
|
||||
return self._user_api.get_qr_code(recover=recover, style=style)
|
||||
return self._user_api.get_qr_code(recover=recover, style=style)
|
||||
|
||||
def get_safety_info(self):
|
||||
"""获取设备信息"""
|
||||
return self._user_api.get_safety_info()
|
||||
return self._user_api.get_safety_info()
|
||||
|
||||
def update_head_img(self, head_img_base64):
|
||||
def update_head_img(self, head_img_base64):
|
||||
"""上传用户头像"""
|
||||
return self._user_api.update_head_img(head_img_base64)
|
||||
return self._user_api.update_head_img(head_img_base64)
|
||||
|
||||
def cdn_download(self, aeskey, file_type, file_url):
|
||||
"""cdn下载"""
|
||||
return self._download_api.send_download( aeskey, file_type, file_url)
|
||||
return self._download_api.send_download(aeskey, file_type, file_url)
|
||||
|
||||
def get_msg_voice(self,buf_id, length, msgid):
|
||||
def get_msg_voice(self, buf_id, length, msgid):
|
||||
"""下载语音"""
|
||||
return self._download_api.get_msg_voice(buf_id, length, msgid)
|
||||
|
||||
async def download_base64(self,url):
|
||||
async def download_base64(self, url):
|
||||
return await self._download_api.download_url_to_base64(download_url=url)
|
||||
|
||||
def get_chatroom_member_detail(self, chatroom_name):
|
||||
"""查看群成员详情"""
|
||||
return self._chatroom_api.get_chatroom_member_detail(chatroom_name)
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import qrcode
|
||||
|
||||
|
||||
def print_green(text):
|
||||
print(f"\033[32m{text}\033[0m")
|
||||
print(f'\033[32m{text}\033[0m')
|
||||
|
||||
|
||||
def print_yellow(text):
|
||||
print(f"\033[33m{text}\033[0m")
|
||||
print(f'\033[33m{text}\033[0m')
|
||||
|
||||
|
||||
def print_red(text):
|
||||
print(f"\033[31m{text}\033[0m")
|
||||
print(f'\033[31m{text}\033[0m')
|
||||
|
||||
|
||||
def make_and_print_qr(url):
|
||||
"""生成并打印二维码
|
||||
|
||||
|
||||
Args:
|
||||
url: 需要生成二维码的URL字符串
|
||||
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
|
||||
功能:
|
||||
1. 在终端打印二维码的ASCII图形
|
||||
2. 同时提供在线二维码生成链接作为备选
|
||||
"""
|
||||
print_green("请扫描下方二维码登录")
|
||||
print_green('请扫描下方二维码登录')
|
||||
qr = qrcode.QRCode()
|
||||
qr.add_data(url)
|
||||
qr.make()
|
||||
qr.print_ascii(invert=True)
|
||||
print_green(f"也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}")
|
||||
|
||||
print_green(f'也可以访问下方链接获取二维码:\nhttps://api.qrserver.com/v1/create-qr-code/?data={url}')
|
||||
|
||||
@@ -57,7 +57,7 @@ class WecomClient:
|
||||
if 'access_token' in data:
|
||||
return data['access_token']
|
||||
else:
|
||||
await self.logger.error(f"获取accesstoken失败:{response.json()}")
|
||||
await self.logger.error(f'获取accesstoken失败:{response.json()}')
|
||||
raise Exception(f'未获取access token: {data}')
|
||||
|
||||
async def get_users(self):
|
||||
@@ -129,7 +129,7 @@ class WecomClient:
|
||||
response = await client.post(url, json=params)
|
||||
data = response.json()
|
||||
except Exception as e:
|
||||
await self.logger.error(f"发送图片失败:{data}")
|
||||
await self.logger.error(f'发送图片失败:{data}')
|
||||
raise Exception('Failed to send image: ' + str(e))
|
||||
|
||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||
@@ -164,7 +164,7 @@ class WecomClient:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_private_msg(user_id, agent_id, content)
|
||||
if data['errcode'] != 0:
|
||||
await self.logger.error(f"发送消息失败:{data}")
|
||||
await self.logger.error(f'发送消息失败:{data}')
|
||||
raise Exception('Failed to send message: ' + str(data))
|
||||
|
||||
async def handle_callback_request(self):
|
||||
@@ -181,7 +181,7 @@ class WecomClient:
|
||||
echostr = request.args.get('echostr')
|
||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||
if ret != 0:
|
||||
await self.logger.error("验证失败")
|
||||
await self.logger.error('验证失败')
|
||||
raise Exception(f'验证失败,错误码: {ret}')
|
||||
return reply_echo_str
|
||||
|
||||
@@ -189,9 +189,8 @@ class WecomClient:
|
||||
encrypt_msg = await request.data
|
||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||
if ret != 0:
|
||||
await self.logger.error("消息解密失败")
|
||||
await self.logger.error('消息解密失败')
|
||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||
|
||||
|
||||
# 解析消息并处理
|
||||
message_data = await self.get_message(xml_msg)
|
||||
@@ -202,7 +201,7 @@ class WecomClient:
|
||||
|
||||
return 'success'
|
||||
except Exception as e:
|
||||
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||
return f'Error processing request: {str(e)}', 400
|
||||
|
||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||
@@ -301,7 +300,7 @@ class WecomClient:
|
||||
except binascii.Error as e:
|
||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||
else:
|
||||
await self.logger.error("Image对象出错")
|
||||
await self.logger.error('Image对象出错')
|
||||
raise ValueError('image对象出错')
|
||||
|
||||
# 设置 multipart/form-data 格式的文件
|
||||
@@ -325,7 +324,7 @@ class WecomClient:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
media_id = await self.upload_to_work(image)
|
||||
if data.get('errcode', 0) != 0:
|
||||
await self.logger.error(f"上传图片失败:{data}")
|
||||
await self.logger.error(f'上传图片失败:{data}')
|
||||
raise Exception('failed to upload file')
|
||||
|
||||
media_id = data.get('media_id')
|
||||
|
||||
@@ -187,7 +187,7 @@ class WecomCSClient:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||
if data['errcode'] != 0:
|
||||
await self.logger.error(f"发送消息失败:{data}")
|
||||
await self.logger.error(f'发送消息失败:{data}')
|
||||
raise Exception('Failed to send message')
|
||||
return data
|
||||
|
||||
@@ -227,7 +227,7 @@ class WecomCSClient:
|
||||
return 'success'
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
await self.logger.error(f"Error in handle_callback_request: {traceback.format_exc()}")
|
||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||
else:
|
||||
traceback.print_exc()
|
||||
return f'Error processing request: {str(e)}', 400
|
||||
|
||||
@@ -123,4 +123,4 @@ class RouterGroup(abc.ABC):
|
||||
|
||||
def http_status(self, status: int, code: int, msg: str) -> typing.Tuple[quart.Response, int]:
|
||||
"""返回一个指定状态码的响应"""
|
||||
return (self.fail(code, msg), status)
|
||||
return (self.fail(code, msg), status)
|
||||
|
||||
@@ -13,7 +13,9 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
if quart.request.method == 'GET':
|
||||
sort_by = quart.request.args.get('sort_by', 'created_at')
|
||||
sort_order = quart.request.args.get('sort_order', 'DESC')
|
||||
return self.success(data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)})
|
||||
return self.success(
|
||||
data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)}
|
||||
)
|
||||
elif quart.request.method == 'POST':
|
||||
json_data = await quart.request.json
|
||||
|
||||
|
||||
@@ -67,3 +67,19 @@ class UserRouterGroup(group.RouterGroup):
|
||||
await self.ap.user_service.reset_password(user_email, new_password)
|
||||
|
||||
return self.success(data={'user': user_email})
|
||||
|
||||
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(user_email: str) -> str:
|
||||
json_data = await quart.request.json
|
||||
|
||||
current_password = json_data['current_password']
|
||||
new_password = json_data['new_password']
|
||||
|
||||
try:
|
||||
await self.ap.user_service.change_password(user_email, current_password, new_password)
|
||||
except argon2.exceptions.VerifyMismatchError:
|
||||
return self.http_status(400, -1, 'Current password is incorrect')
|
||||
except ValueError as e:
|
||||
return self.http_status(400, -1, str(e))
|
||||
|
||||
return self.success(data={'user': user_email})
|
||||
|
||||
@@ -78,7 +78,9 @@ class KnowledgeService:
|
||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||
if runtime_kb is None:
|
||||
raise Exception('Knowledge base not found')
|
||||
return [result.model_dump() for result in await runtime_kb.retrieve(query)]
|
||||
return [
|
||||
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
|
||||
]
|
||||
|
||||
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
||||
"""获取知识库文件"""
|
||||
|
||||
@@ -40,7 +40,7 @@ class PipelineService:
|
||||
|
||||
async def get_pipelines(self, sort_by: str = 'created_at', sort_order: str = 'DESC') -> list[dict]:
|
||||
query = sqlalchemy.select(persistence_pipeline.LegacyPipeline)
|
||||
|
||||
|
||||
if sort_by == 'created_at':
|
||||
if sort_order == 'DESC':
|
||||
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.desc())
|
||||
@@ -51,7 +51,7 @@ class PipelineService:
|
||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc())
|
||||
else:
|
||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.asc())
|
||||
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(query)
|
||||
pipelines = result.all()
|
||||
return [
|
||||
|
||||
@@ -82,3 +82,18 @@ class UserService:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
||||
)
|
||||
|
||||
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
|
||||
ph = argon2.PasswordHasher()
|
||||
|
||||
user_obj = await self.get_user_by_email(user_email)
|
||||
if user_obj is None:
|
||||
raise ValueError('User not found')
|
||||
|
||||
ph.verify(user_obj.password, current_password)
|
||||
|
||||
hashed_password = ph.hash(new_password)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ class LifecycleControlScope(enum.Enum):
|
||||
APPLICATION = 'application'
|
||||
PLATFORM = 'platform'
|
||||
PLUGIN = 'plugin'
|
||||
PROVIDER = 'provider'
|
||||
PROVIDER = 'provider'
|
||||
|
||||
|
||||
class LauncherTypes(enum.Enum):
|
||||
|
||||
@@ -20,9 +20,11 @@ from ..types import message as platform_message
|
||||
from ..types import events as platform_events
|
||||
from ..types import entities as platform_entities
|
||||
|
||||
|
||||
# 语音功能相关异常定义
|
||||
class VoiceConnectionError(Exception):
|
||||
"""语音连接基础异常"""
|
||||
|
||||
def __init__(self, message: str, error_code: str = None, guild_id: int = None):
|
||||
super().__init__(message)
|
||||
self.error_code = error_code
|
||||
@@ -32,8 +34,9 @@ class VoiceConnectionError(Exception):
|
||||
|
||||
class VoicePermissionError(VoiceConnectionError):
|
||||
"""语音权限异常"""
|
||||
|
||||
def __init__(self, message: str, missing_permissions: list = None, user_id: int = None, channel_id: int = None):
|
||||
super().__init__(message, "PERMISSION_ERROR")
|
||||
super().__init__(message, 'PERMISSION_ERROR')
|
||||
self.missing_permissions = missing_permissions or []
|
||||
self.user_id = user_id
|
||||
self.channel_id = channel_id
|
||||
@@ -41,40 +44,42 @@ class VoicePermissionError(VoiceConnectionError):
|
||||
|
||||
class VoiceNetworkError(VoiceConnectionError):
|
||||
"""语音网络异常"""
|
||||
|
||||
def __init__(self, message: str, retry_count: int = 0):
|
||||
super().__init__(message, "NETWORK_ERROR")
|
||||
super().__init__(message, 'NETWORK_ERROR')
|
||||
self.retry_count = retry_count
|
||||
self.last_attempt = datetime.datetime.now()
|
||||
|
||||
|
||||
class VoiceConnectionStatus(Enum):
|
||||
"""语音连接状态枚举"""
|
||||
IDLE = "idle"
|
||||
CONNECTING = "connecting"
|
||||
CONNECTED = "connected"
|
||||
PLAYING = "playing"
|
||||
RECONNECTING = "reconnecting"
|
||||
FAILED = "failed"
|
||||
|
||||
IDLE = 'idle'
|
||||
CONNECTING = 'connecting'
|
||||
CONNECTED = 'connected'
|
||||
PLAYING = 'playing'
|
||||
RECONNECTING = 'reconnecting'
|
||||
FAILED = 'failed'
|
||||
|
||||
|
||||
class VoiceConnectionInfo:
|
||||
"""
|
||||
语音连接信息类
|
||||
|
||||
|
||||
用于存储和管理单个语音连接的详细信息,包括连接状态、时间戳、
|
||||
频道信息等。提供连接信息的标准化数据结构。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, guild_id: int, channel_id: int, channel_name: str = None):
|
||||
"""
|
||||
初始化语音连接信息
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
channel_id (int): 语音频道ID
|
||||
@@ -82,28 +87,28 @@ class VoiceConnectionInfo:
|
||||
"""
|
||||
self.guild_id = guild_id
|
||||
self.channel_id = channel_id
|
||||
self.channel_name = channel_name or f"Channel-{channel_id}"
|
||||
self.channel_name = channel_name or f'Channel-{channel_id}'
|
||||
self.connected = False
|
||||
self.connection_time: datetime.datetime = None
|
||||
self.last_activity = datetime.datetime.now()
|
||||
self.status = VoiceConnectionStatus.IDLE
|
||||
self.user_count = 0
|
||||
self.latency = 0.0
|
||||
self.connection_health = "unknown"
|
||||
self.connection_health = 'unknown'
|
||||
self.voice_client = None
|
||||
|
||||
|
||||
def update_status(self, status: VoiceConnectionStatus):
|
||||
"""
|
||||
更新连接状态
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
status (VoiceConnectionStatus): 新的连接状态
|
||||
"""
|
||||
self.status = status
|
||||
self.last_activity = datetime.datetime.now()
|
||||
|
||||
|
||||
if status == VoiceConnectionStatus.CONNECTED:
|
||||
self.connected = True
|
||||
if self.connection_time is None:
|
||||
@@ -112,48 +117,48 @@ class VoiceConnectionInfo:
|
||||
self.connected = False
|
||||
self.connection_time = None
|
||||
self.voice_client = None
|
||||
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
转换为字典格式
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Returns:
|
||||
dict: 连接信息的字典表示
|
||||
"""
|
||||
return {
|
||||
"guild_id": self.guild_id,
|
||||
"channel_id": self.channel_id,
|
||||
"channel_name": self.channel_name,
|
||||
"connected": self.connected,
|
||||
"connection_time": self.connection_time.isoformat() if self.connection_time else None,
|
||||
"last_activity": self.last_activity.isoformat(),
|
||||
"status": self.status.value,
|
||||
"user_count": self.user_count,
|
||||
"latency": self.latency,
|
||||
"connection_health": self.connection_health
|
||||
'guild_id': self.guild_id,
|
||||
'channel_id': self.channel_id,
|
||||
'channel_name': self.channel_name,
|
||||
'connected': self.connected,
|
||||
'connection_time': self.connection_time.isoformat() if self.connection_time else None,
|
||||
'last_activity': self.last_activity.isoformat(),
|
||||
'status': self.status.value,
|
||||
'user_count': self.user_count,
|
||||
'latency': self.latency,
|
||||
'connection_health': self.connection_health,
|
||||
}
|
||||
|
||||
|
||||
class VoiceConnectionManager:
|
||||
"""
|
||||
语音连接管理器
|
||||
|
||||
|
||||
负责管理多个服务器的语音连接,提供连接建立、断开、状态查询等功能。
|
||||
采用单例模式确保全局只有一个连接管理器实例。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, bot: discord.Client, logger: EventLogger):
|
||||
"""
|
||||
初始化语音连接管理器
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
bot (discord.Client): Discord 客户端实例
|
||||
logger (EventLogger): 事件日志记录器
|
||||
@@ -164,25 +169,24 @@ class VoiceConnectionManager:
|
||||
self._connection_lock = asyncio.Lock()
|
||||
self._cleanup_task = None
|
||||
self._monitoring_enabled = True
|
||||
|
||||
async def join_voice_channel(self, guild_id: int, channel_id: int,
|
||||
user_id: int = None) -> discord.VoiceClient:
|
||||
|
||||
async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient:
|
||||
"""
|
||||
加入语音频道
|
||||
|
||||
|
||||
验证用户权限和频道状态后,建立到指定语音频道的连接。
|
||||
支持连接复用和自动重连机制。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
channel_id (int): 语音频道ID
|
||||
user_id (int, optional): 请求用户ID,用于权限验证
|
||||
|
||||
|
||||
Returns:
|
||||
discord.VoiceClient: 语音客户端实例
|
||||
|
||||
|
||||
Raises:
|
||||
VoicePermissionError: 权限不足时抛出
|
||||
VoiceNetworkError: 网络连接失败时抛出
|
||||
@@ -193,370 +197,353 @@ class VoiceConnectionManager:
|
||||
# 获取服务器和频道对象
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
raise VoiceConnectionError(
|
||||
f"无法找到服务器 {guild_id}",
|
||||
"GUILD_NOT_FOUND",
|
||||
guild_id
|
||||
)
|
||||
|
||||
raise VoiceConnectionError(f'无法找到服务器 {guild_id}', 'GUILD_NOT_FOUND', guild_id)
|
||||
|
||||
channel = guild.get_channel(channel_id)
|
||||
if not channel or not isinstance(channel, discord.VoiceChannel):
|
||||
raise VoiceConnectionError(
|
||||
f"无法找到语音频道 {channel_id}",
|
||||
"CHANNEL_NOT_FOUND",
|
||||
guild_id
|
||||
)
|
||||
|
||||
raise VoiceConnectionError(f'无法找到语音频道 {channel_id}', 'CHANNEL_NOT_FOUND', guild_id)
|
||||
|
||||
# 验证用户是否在语音频道中(如果提供了用户ID)
|
||||
if user_id:
|
||||
await self._validate_user_in_channel(guild, channel, user_id)
|
||||
|
||||
|
||||
# 验证机器人权限
|
||||
await self._validate_bot_permissions(channel)
|
||||
|
||||
|
||||
# 检查是否已有连接
|
||||
if guild_id in self.connections:
|
||||
existing_conn = self.connections[guild_id]
|
||||
if existing_conn.connected and existing_conn.voice_client:
|
||||
if existing_conn.channel_id == channel_id:
|
||||
# 已连接到相同频道,返回现有连接
|
||||
await self.logger.info(f"复用现有语音连接: {guild.name} -> {channel.name}")
|
||||
await self.logger.info(f'复用现有语音连接: {guild.name} -> {channel.name}')
|
||||
return existing_conn.voice_client
|
||||
else:
|
||||
# 连接到不同频道,先断开旧连接
|
||||
await self._disconnect_internal(guild_id)
|
||||
|
||||
|
||||
# 建立新连接
|
||||
voice_client = await channel.connect()
|
||||
|
||||
|
||||
# 更新连接信息
|
||||
conn_info = VoiceConnectionInfo(guild_id, channel_id, channel.name)
|
||||
conn_info.voice_client = voice_client
|
||||
conn_info.update_status(VoiceConnectionStatus.CONNECTED)
|
||||
conn_info.user_count = len(channel.members)
|
||||
self.connections[guild_id] = conn_info
|
||||
|
||||
await self.logger.info(f"成功连接到语音频道: {guild.name} -> {channel.name}")
|
||||
|
||||
await self.logger.info(f'成功连接到语音频道: {guild.name} -> {channel.name}')
|
||||
return voice_client
|
||||
|
||||
|
||||
except discord.ClientException as e:
|
||||
raise VoiceNetworkError(f"Discord 客户端错误: {str(e)}")
|
||||
raise VoiceNetworkError(f'Discord 客户端错误: {str(e)}')
|
||||
except discord.opus.OpusNotLoaded as e:
|
||||
raise VoiceConnectionError(f"Opus 编码器未加载: {str(e)}", "OPUS_NOT_LOADED", guild_id)
|
||||
raise VoiceConnectionError(f'Opus 编码器未加载: {str(e)}', 'OPUS_NOT_LOADED', guild_id)
|
||||
except Exception as e:
|
||||
await self.logger.error(f"连接语音频道时发生未知错误: {str(e)}")
|
||||
raise VoiceConnectionError(f"连接失败: {str(e)}", "UNKNOWN_ERROR", guild_id)
|
||||
|
||||
await self.logger.error(f'连接语音频道时发生未知错误: {str(e)}')
|
||||
raise VoiceConnectionError(f'连接失败: {str(e)}', 'UNKNOWN_ERROR', guild_id)
|
||||
|
||||
async def leave_voice_channel(self, guild_id: int) -> bool:
|
||||
"""
|
||||
离开语音频道
|
||||
|
||||
|
||||
断开指定服务器的语音连接,清理相关资源和状态信息。
|
||||
确保音频播放停止后再断开连接。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
bool: 断开是否成功
|
||||
"""
|
||||
async with self._connection_lock:
|
||||
return await self._disconnect_internal(guild_id)
|
||||
|
||||
|
||||
async def _disconnect_internal(self, guild_id: int) -> bool:
|
||||
"""
|
||||
内部断开连接方法
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
bool: 断开是否成功
|
||||
"""
|
||||
if guild_id not in self.connections:
|
||||
return True
|
||||
|
||||
|
||||
conn_info = self.connections[guild_id]
|
||||
|
||||
|
||||
try:
|
||||
if conn_info.voice_client and conn_info.voice_client.is_connected():
|
||||
# 停止当前播放
|
||||
if conn_info.voice_client.is_playing():
|
||||
conn_info.voice_client.stop()
|
||||
|
||||
|
||||
# 等待播放完全停止
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
# 断开连接
|
||||
await conn_info.voice_client.disconnect()
|
||||
|
||||
|
||||
conn_info.update_status(VoiceConnectionStatus.IDLE)
|
||||
del self.connections[guild_id]
|
||||
|
||||
await self.logger.info(f"已断开语音连接: Guild {guild_id}")
|
||||
|
||||
await self.logger.info(f'已断开语音连接: Guild {guild_id}')
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
await self.logger.error(f"断开语音连接时发生错误: {str(e)}")
|
||||
await self.logger.error(f'断开语音连接时发生错误: {str(e)}')
|
||||
# 即使出错也要清理连接记录
|
||||
conn_info.update_status(VoiceConnectionStatus.FAILED)
|
||||
if guild_id in self.connections:
|
||||
del self.connections[guild_id]
|
||||
return False
|
||||
|
||||
|
||||
async def get_voice_client(self, guild_id: int) -> typing.Optional[discord.VoiceClient]:
|
||||
"""
|
||||
获取语音客户端
|
||||
|
||||
|
||||
返回指定服务器的语音客户端实例,如果未连接则返回 None。
|
||||
会验证连接的有效性,自动清理无效连接。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[discord.VoiceClient]: 语音客户端实例或 None
|
||||
"""
|
||||
if guild_id not in self.connections:
|
||||
return None
|
||||
|
||||
|
||||
conn_info = self.connections[guild_id]
|
||||
|
||||
|
||||
# 验证连接是否仍然有效
|
||||
if conn_info.voice_client and not conn_info.voice_client.is_connected():
|
||||
# 连接已失效,清理状态
|
||||
await self._disconnect_internal(guild_id)
|
||||
return None
|
||||
|
||||
|
||||
return conn_info.voice_client if conn_info.connected else None
|
||||
|
||||
|
||||
async def is_connected_to_voice(self, guild_id: int) -> bool:
|
||||
"""
|
||||
检查是否连接到语音频道
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
bool: 是否已连接
|
||||
"""
|
||||
if guild_id not in self.connections:
|
||||
return False
|
||||
|
||||
|
||||
conn_info = self.connections[guild_id]
|
||||
|
||||
|
||||
# 检查实际连接状态
|
||||
if conn_info.voice_client and not conn_info.voice_client.is_connected():
|
||||
# 连接已失效,清理状态
|
||||
await self._disconnect_internal(guild_id)
|
||||
return False
|
||||
|
||||
|
||||
return conn_info.connected
|
||||
|
||||
|
||||
async def get_connection_status(self, guild_id: int) -> typing.Optional[dict]:
|
||||
"""
|
||||
获取连接状态信息
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[dict]: 连接状态信息字典或 None
|
||||
"""
|
||||
if guild_id not in self.connections:
|
||||
return None
|
||||
|
||||
|
||||
conn_info = self.connections[guild_id]
|
||||
|
||||
|
||||
# 更新实时信息
|
||||
if conn_info.voice_client and conn_info.voice_client.is_connected():
|
||||
conn_info.latency = conn_info.voice_client.latency * 1000 # 转换为毫秒
|
||||
conn_info.connection_health = "good" if conn_info.latency < 100 else "poor"
|
||||
|
||||
conn_info.connection_health = 'good' if conn_info.latency < 100 else 'poor'
|
||||
|
||||
# 更新频道用户数
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if guild:
|
||||
channel = guild.get_channel(conn_info.channel_id)
|
||||
if channel and isinstance(channel, discord.VoiceChannel):
|
||||
conn_info.user_count = len(channel.members)
|
||||
|
||||
|
||||
return conn_info.to_dict()
|
||||
|
||||
|
||||
async def list_active_connections(self) -> typing.List[dict]:
|
||||
"""
|
||||
列出所有活跃连接
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Returns:
|
||||
List[dict]: 活跃连接列表
|
||||
"""
|
||||
active_connections = []
|
||||
|
||||
|
||||
for guild_id, conn_info in self.connections.items():
|
||||
if conn_info.connected:
|
||||
status = await self.get_connection_status(guild_id)
|
||||
if status:
|
||||
active_connections.append(status)
|
||||
|
||||
|
||||
return active_connections
|
||||
|
||||
|
||||
async def get_voice_channel_info(self, guild_id: int, channel_id: int) -> typing.Optional[dict]:
|
||||
"""
|
||||
获取语音频道信息
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): 服务器ID
|
||||
channel_id (int): 频道ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[dict]: 频道信息字典或 None
|
||||
"""
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if not guild:
|
||||
return None
|
||||
|
||||
|
||||
channel = guild.get_channel(channel_id)
|
||||
if not channel or not isinstance(channel, discord.VoiceChannel):
|
||||
return None
|
||||
|
||||
|
||||
# 获取用户信息
|
||||
users = []
|
||||
for member in channel.members:
|
||||
users.append({
|
||||
"id": member.id,
|
||||
"name": member.display_name,
|
||||
"status": str(member.status),
|
||||
"is_bot": member.bot
|
||||
})
|
||||
|
||||
users.append(
|
||||
{'id': member.id, 'name': member.display_name, 'status': str(member.status), 'is_bot': member.bot}
|
||||
)
|
||||
|
||||
# 获取权限信息
|
||||
bot_member = guild.me
|
||||
permissions = channel.permissions_for(bot_member)
|
||||
|
||||
|
||||
return {
|
||||
"channel_id": channel_id,
|
||||
"channel_name": channel.name,
|
||||
"guild_id": guild_id,
|
||||
"guild_name": guild.name,
|
||||
"user_limit": channel.user_limit,
|
||||
"current_users": users,
|
||||
"user_count": len(users),
|
||||
"bitrate": channel.bitrate,
|
||||
"permissions": {
|
||||
"connect": permissions.connect,
|
||||
"speak": permissions.speak,
|
||||
"use_voice_activation": permissions.use_voice_activation,
|
||||
"priority_speaker": permissions.priority_speaker
|
||||
}
|
||||
'channel_id': channel_id,
|
||||
'channel_name': channel.name,
|
||||
'guild_id': guild_id,
|
||||
'guild_name': guild.name,
|
||||
'user_limit': channel.user_limit,
|
||||
'current_users': users,
|
||||
'user_count': len(users),
|
||||
'bitrate': channel.bitrate,
|
||||
'permissions': {
|
||||
'connect': permissions.connect,
|
||||
'speak': permissions.speak,
|
||||
'use_voice_activation': permissions.use_voice_activation,
|
||||
'priority_speaker': permissions.priority_speaker,
|
||||
},
|
||||
}
|
||||
|
||||
async def _validate_user_in_channel(self, guild: discord.Guild,
|
||||
channel: discord.VoiceChannel, user_id: int):
|
||||
|
||||
async def _validate_user_in_channel(self, guild: discord.Guild, channel: discord.VoiceChannel, user_id: int):
|
||||
"""
|
||||
验证用户是否在语音频道中
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
guild: Discord 服务器对象
|
||||
channel: 语音频道对象
|
||||
user_id: 用户ID
|
||||
|
||||
|
||||
Raises:
|
||||
VoicePermissionError: 用户不在频道中时抛出
|
||||
"""
|
||||
member = guild.get_member(user_id)
|
||||
if not member:
|
||||
raise VoicePermissionError(
|
||||
f"无法找到用户 {user_id}",
|
||||
["member_not_found"],
|
||||
user_id,
|
||||
channel.id
|
||||
)
|
||||
|
||||
raise VoicePermissionError(f'无法找到用户 {user_id}', ['member_not_found'], user_id, channel.id)
|
||||
|
||||
if not member.voice or member.voice.channel != channel:
|
||||
raise VoicePermissionError(
|
||||
f"用户 {member.display_name} 不在语音频道 {channel.name} 中",
|
||||
["user_not_in_channel"],
|
||||
f'用户 {member.display_name} 不在语音频道 {channel.name} 中',
|
||||
['user_not_in_channel'],
|
||||
user_id,
|
||||
channel.id
|
||||
channel.id,
|
||||
)
|
||||
|
||||
|
||||
async def _validate_bot_permissions(self, channel: discord.VoiceChannel):
|
||||
"""
|
||||
验证机器人权限
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
|
||||
|
||||
Args:
|
||||
channel: 语音频道对象
|
||||
|
||||
|
||||
Raises:
|
||||
VoicePermissionError: 权限不足时抛出
|
||||
"""
|
||||
bot_member = channel.guild.me
|
||||
permissions = channel.permissions_for(bot_member)
|
||||
|
||||
|
||||
missing_permissions = []
|
||||
|
||||
|
||||
if not permissions.connect:
|
||||
missing_permissions.append("connect")
|
||||
missing_permissions.append('connect')
|
||||
if not permissions.speak:
|
||||
missing_permissions.append("speak")
|
||||
|
||||
missing_permissions.append('speak')
|
||||
|
||||
if missing_permissions:
|
||||
raise VoicePermissionError(
|
||||
f"机器人在频道 {channel.name} 中缺少权限: {', '.join(missing_permissions)}",
|
||||
f'机器人在频道 {channel.name} 中缺少权限: {", ".join(missing_permissions)}',
|
||||
missing_permissions,
|
||||
channel_id=channel.id
|
||||
channel_id=channel.id,
|
||||
)
|
||||
|
||||
|
||||
async def cleanup_inactive_connections(self):
|
||||
"""
|
||||
清理无效连接
|
||||
|
||||
|
||||
定期检查并清理已断开或无效的语音连接,释放资源。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
"""
|
||||
cleanup_guilds = []
|
||||
|
||||
|
||||
for guild_id, conn_info in self.connections.items():
|
||||
if not conn_info.voice_client or not conn_info.voice_client.is_connected():
|
||||
cleanup_guilds.append(guild_id)
|
||||
|
||||
|
||||
for guild_id in cleanup_guilds:
|
||||
await self._disconnect_internal(guild_id)
|
||||
|
||||
|
||||
if cleanup_guilds:
|
||||
await self.logger.info(f"清理了 {len(cleanup_guilds)} 个无效的语音连接")
|
||||
|
||||
await self.logger.info(f'清理了 {len(cleanup_guilds)} 个无效的语音连接')
|
||||
|
||||
async def start_monitoring(self):
|
||||
"""
|
||||
开始连接监控
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
"""
|
||||
if self._cleanup_task is None and self._monitoring_enabled:
|
||||
self._cleanup_task = asyncio.create_task(self._monitoring_loop())
|
||||
|
||||
|
||||
async def stop_monitoring(self):
|
||||
"""
|
||||
停止连接监控
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
"""
|
||||
self._monitoring_enabled = False
|
||||
@@ -567,11 +554,11 @@ class VoiceConnectionManager:
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._cleanup_task = None
|
||||
|
||||
|
||||
async def _monitoring_loop(self):
|
||||
"""
|
||||
监控循环
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
"""
|
||||
try:
|
||||
@@ -580,18 +567,18 @@ class VoiceConnectionManager:
|
||||
await self.cleanup_inactive_connections()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
async def disconnect_all(self):
|
||||
"""
|
||||
断开所有连接
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
"""
|
||||
async with self._connection_lock:
|
||||
guild_ids = list(self.connections.keys())
|
||||
for guild_id in guild_ids:
|
||||
await self._disconnect_internal(guild_id)
|
||||
|
||||
|
||||
await self.stop_monitoring()
|
||||
|
||||
|
||||
@@ -814,7 +801,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
||||
self.logger = logger
|
||||
|
||||
self.bot_account_id = self.config['client_id']
|
||||
|
||||
|
||||
# 初始化语音连接管理器
|
||||
self.voice_manager: VoiceConnectionManager = None
|
||||
|
||||
@@ -837,163 +824,162 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
||||
args['proxy'] = os.getenv('http_proxy')
|
||||
|
||||
self.bot = MyClient(intents=intents, **args)
|
||||
|
||||
|
||||
# Voice functionality methods
|
||||
async def join_voice_channel(self, guild_id: int, channel_id: int,
|
||||
user_id: int = None) -> discord.VoiceClient:
|
||||
async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient:
|
||||
"""
|
||||
加入语音频道
|
||||
|
||||
|
||||
为指定服务器的语音频道建立连接,支持用户权限验证和连接复用。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
channel_id (int): 语音频道ID
|
||||
user_id (int, optional): 请求用户ID,用于权限验证
|
||||
|
||||
|
||||
Returns:
|
||||
discord.VoiceClient: 语音客户端实例
|
||||
|
||||
|
||||
Raises:
|
||||
VoicePermissionError: 权限不足
|
||||
VoiceNetworkError: 网络连接失败
|
||||
VoiceConnectionError: 其他连接错误
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
raise VoiceConnectionError("语音管理器未初始化", "MANAGER_NOT_READY")
|
||||
|
||||
raise VoiceConnectionError('语音管理器未初始化', 'MANAGER_NOT_READY')
|
||||
|
||||
return await self.voice_manager.join_voice_channel(guild_id, channel_id, user_id)
|
||||
|
||||
|
||||
async def leave_voice_channel(self, guild_id: int) -> bool:
|
||||
"""
|
||||
离开语音频道
|
||||
|
||||
|
||||
断开指定服务器的语音连接,清理相关资源。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
bool: 是否成功断开连接
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return False
|
||||
|
||||
|
||||
return await self.voice_manager.leave_voice_channel(guild_id)
|
||||
|
||||
|
||||
async def get_voice_client(self, guild_id: int) -> typing.Optional[discord.VoiceClient]:
|
||||
"""
|
||||
获取语音客户端
|
||||
|
||||
|
||||
返回指定服务器的语音客户端实例,用于音频播放控制。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[discord.VoiceClient]: 语音客户端实例或 None
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return None
|
||||
|
||||
|
||||
return await self.voice_manager.get_voice_client(guild_id)
|
||||
|
||||
|
||||
async def is_connected_to_voice(self, guild_id: int) -> bool:
|
||||
"""
|
||||
检查语音连接状态
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
bool: 是否已连接到语音频道
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return False
|
||||
|
||||
|
||||
return await self.voice_manager.is_connected_to_voice(guild_id)
|
||||
|
||||
|
||||
async def get_voice_connection_status(self, guild_id: int) -> typing.Optional[dict]:
|
||||
"""
|
||||
获取语音连接详细状态
|
||||
|
||||
|
||||
返回包含连接时间、延迟、用户数等详细信息的状态字典。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[dict]: 连接状态信息或 None
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return None
|
||||
|
||||
|
||||
return await self.voice_manager.get_connection_status(guild_id)
|
||||
|
||||
|
||||
async def list_active_voice_connections(self) -> typing.List[dict]:
|
||||
"""
|
||||
列出所有活跃的语音连接
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Returns:
|
||||
List[dict]: 活跃语音连接列表
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return []
|
||||
|
||||
|
||||
return await self.voice_manager.list_active_connections()
|
||||
|
||||
|
||||
async def get_voice_channel_info(self, guild_id: int, channel_id: int) -> typing.Optional[dict]:
|
||||
"""
|
||||
获取语音频道详细信息
|
||||
|
||||
|
||||
包括频道名称、用户列表、权限信息等。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
|
||||
|
||||
Args:
|
||||
guild_id (int): Discord 服务器ID
|
||||
channel_id (int): 语音频道ID
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[dict]: 频道信息字典或 None
|
||||
"""
|
||||
if not self.voice_manager:
|
||||
return None
|
||||
|
||||
|
||||
return await self.voice_manager.get_voice_channel_info(guild_id, channel_id)
|
||||
|
||||
|
||||
async def cleanup_voice_connections(self):
|
||||
"""
|
||||
清理无效的语音连接
|
||||
|
||||
|
||||
手动触发语音连接清理,移除已断开或无效的连接。
|
||||
|
||||
|
||||
@author: @ydzat
|
||||
@version: 1.0
|
||||
@since: 2025-07-04
|
||||
@@ -1068,30 +1054,29 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
||||
async def run_async(self):
|
||||
"""
|
||||
启动 Discord 适配器
|
||||
|
||||
|
||||
初始化语音管理器并启动 Discord 客户端连接。
|
||||
|
||||
|
||||
@author: @ydzat (修改)
|
||||
"""
|
||||
async with self.bot:
|
||||
# 初始化语音管理器
|
||||
self.voice_manager = VoiceConnectionManager(self.bot, self.logger)
|
||||
await self.voice_manager.start_monitoring()
|
||||
|
||||
await self.logger.info("Discord 适配器语音功能已启用")
|
||||
|
||||
await self.logger.info('Discord 适配器语音功能已启用')
|
||||
await self.bot.start(self.config['token'], reconnect=True)
|
||||
|
||||
async def kill(self) -> bool:
|
||||
"""
|
||||
关闭 Discord 适配器
|
||||
|
||||
|
||||
清理语音连接并关闭 Discord 客户端。
|
||||
|
||||
|
||||
@author: @ydzat (修改)
|
||||
"""
|
||||
if self.voice_manager:
|
||||
await self.voice_manager.disconnect_all()
|
||||
|
||||
|
||||
await self.bot.close()
|
||||
return True
|
||||
|
||||
|
||||
@@ -29,10 +29,9 @@ import logging
|
||||
|
||||
|
||||
class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
|
||||
def __init__(self, config: dict, logger: logging.Logger):
|
||||
self.config = config
|
||||
self.bot = WeChatPadClient(self.config["wechatpad_url"],self.config["token"])
|
||||
self.bot = WeChatPadClient(self.config['wechatpad_url'], self.config['token'])
|
||||
self.logger = logger
|
||||
|
||||
@staticmethod
|
||||
@@ -41,9 +40,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
|
||||
for component in message_chain:
|
||||
if isinstance(component, platform_message.AtAll):
|
||||
content_list.append({"type": "at", "target": "all"})
|
||||
content_list.append({'type': 'at', 'target': 'all'})
|
||||
elif isinstance(component, platform_message.At):
|
||||
content_list.append({"type": "at", "target": component.target})
|
||||
content_list.append({'type': 'at', 'target': component.target})
|
||||
elif isinstance(component, platform_message.Plain):
|
||||
content_list.append({'type': 'text', 'content': component.text})
|
||||
elif isinstance(component, platform_message.Image):
|
||||
@@ -77,9 +76,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
return content_list
|
||||
|
||||
async def target2yiri(
|
||||
self,
|
||||
message: dict,
|
||||
bot_account_id: str,
|
||||
self,
|
||||
message: dict,
|
||||
bot_account_id: str,
|
||||
) -> platform_message.MessageChain:
|
||||
"""外部消息转平台消息"""
|
||||
# 数据预处理
|
||||
@@ -92,18 +91,18 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
if is_group_message:
|
||||
ats_bot = self._ats_bot(message, bot_account_id)
|
||||
|
||||
self.logger.info(f"ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}")
|
||||
if "@所有人" in content:
|
||||
self.logger.info(f'ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}')
|
||||
if '@所有人' in content:
|
||||
message_list.append(platform_message.AtAll())
|
||||
if ats_bot:
|
||||
message_list.append(platform_message.At(target=bot_account_id))
|
||||
|
||||
|
||||
# 解析@信息并生成At组件
|
||||
at_targets = self._extract_at_targets(message)
|
||||
for target_id in at_targets:
|
||||
if target_id != bot_wxid: # 避免重复添加机器人的At
|
||||
message_list.append(platform_message.At(target=target_id))
|
||||
|
||||
|
||||
content_no_preifx, _ = self._extract_content_and_sender(content)
|
||||
|
||||
msg_type = message['msg_type']
|
||||
@@ -418,14 +417,14 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
msg_source = message.get('msg_source', '') or ''
|
||||
if len(msg_source) > 0:
|
||||
msg_source_data = ET.fromstring(msg_source)
|
||||
at_user_list = msg_source_data.findtext("atuserlist") or ""
|
||||
at_user_list = msg_source_data.findtext('atuserlist') or ''
|
||||
if at_user_list:
|
||||
# atuserlist格式通常是逗号分隔的用户ID列表
|
||||
at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()]
|
||||
except Exception as e:
|
||||
self.logger.error(f"_extract_at_targets got except: {e}")
|
||||
self.logger.error(f'_extract_at_targets got except: {e}')
|
||||
return at_targets
|
||||
|
||||
|
||||
# 提取一下content前面的sender_id, 和去掉前缀的内容
|
||||
def _extract_content_and_sender(self, raw_content: str) -> Tuple[str, Optional[str]]:
|
||||
try:
|
||||
@@ -449,22 +448,20 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||
|
||||
|
||||
class WeChatPadEventConverter(adapter.EventConverter):
|
||||
|
||||
def __init__(self, config: dict, logger: logging.Logger):
|
||||
self.config = config
|
||||
self.message_converter = WeChatPadMessageConverter(config, logger)
|
||||
self.logger = logger
|
||||
|
||||
|
||||
@staticmethod
|
||||
async def yiri2target(event: platform_events.MessageEvent) -> dict:
|
||||
pass
|
||||
|
||||
async def target2yiri(
|
||||
self,
|
||||
event: dict,
|
||||
bot_account_id: str,
|
||||
self,
|
||||
event: dict,
|
||||
bot_account_id: str,
|
||||
) -> platform_events.MessageEvent:
|
||||
|
||||
# 排除公众号以及微信团队消息
|
||||
if (
|
||||
event['from_user_name']['str'].startswith('gh_')
|
||||
@@ -576,26 +573,22 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
||||
for msg in content_list:
|
||||
# 文本消息处理@
|
||||
if msg['type'] == 'text' and at_targets:
|
||||
if "all" in at_targets:
|
||||
if 'all' in at_targets:
|
||||
msg['content'] = f'@所有人 {msg["content"]}'
|
||||
else:
|
||||
at_nick_name_list = []
|
||||
for member in member_info:
|
||||
if member["user_name"] in at_targets:
|
||||
if member['user_name'] in at_targets:
|
||||
at_nick_name_list.append(f'@{member["nick_name"]}')
|
||||
msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}'
|
||||
|
||||
# 统一消息派发
|
||||
handler_map = {
|
||||
'text': lambda msg: self.bot.send_text_message(
|
||||
to_wxid=target_id,
|
||||
message=msg['content'],
|
||||
ats= ["notify@all"] if "all" in at_targets else at_targets
|
||||
to_wxid=target_id, message=msg['content'], ats=['notify@all'] if 'all' in at_targets else at_targets
|
||||
),
|
||||
'image': lambda msg: self.bot.send_image_message(
|
||||
to_wxid=target_id,
|
||||
img_url=msg["image"],
|
||||
ats = ["notify@all"] if "all" in at_targets else at_targets
|
||||
to_wxid=target_id, img_url=msg['image'], ats=['notify@all'] if 'all' in at_targets else at_targets
|
||||
),
|
||||
'WeChatEmoji': lambda msg: self.bot.send_emoji_message(
|
||||
to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: deepseek-chat-completions
|
||||
label:
|
||||
en_US: DeepSeek
|
||||
zh_Hans: 深度求索
|
||||
zh_Hans: DeepSeek
|
||||
icon: deepseek.svg
|
||||
spec:
|
||||
config:
|
||||
|
||||
@@ -65,7 +65,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
self.ap.logger.warning(f'Knowledge base {kb_uuid} not found')
|
||||
raise ValueError(f'Knowledge base {kb_uuid} not found')
|
||||
|
||||
result = await kb.retrieve(user_message_text)
|
||||
result = await kb.retrieve(user_message_text, kb.knowledge_base_entity.top_k)
|
||||
|
||||
final_user_message_text = ''
|
||||
|
||||
|
||||
@@ -123,11 +123,11 @@ class RuntimeKnowledgeBase:
|
||||
)
|
||||
return wrapper.id
|
||||
|
||||
async def retrieve(self, query: str) -> list[retriever_entities.RetrieveResultEntry]:
|
||||
async def retrieve(self, query: str, top_k: int) -> list[retriever_entities.RetrieveResultEntry]:
|
||||
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(
|
||||
self.knowledge_base_entity.embedding_model_uuid
|
||||
)
|
||||
return await self.retriever.retrieve(self.knowledge_base_entity.uuid, query, embedding_model)
|
||||
return await self.retriever.retrieve(self.knowledge_base_entity.uuid, query, embedding_model, top_k)
|
||||
|
||||
async def delete_file(self, file_id: str):
|
||||
# delete vector
|
||||
|
||||
53
web/package-lock.json
generated
53
web/package-lock.json
generated
@@ -10,8 +10,10 @@
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
@@ -1090,6 +1092,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu": {
|
||||
"version": "2.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz",
|
||||
"integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-menu": "2.1.15",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.13",
|
||||
|
||||
@@ -56,6 +56,15 @@
|
||||
background: rgba(0, 0, 0, 0.35); /* 悬停加深 */
|
||||
}
|
||||
|
||||
/* 暗黑模式下的滚动条 */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2); /* 半透明白色 */
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.35); /* 悬停加深 */
|
||||
}
|
||||
|
||||
/* 兼容 Edge */
|
||||
@supports (-ms-ime-align: auto) {
|
||||
body {
|
||||
@@ -108,36 +117,36 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--background: oklch(0.08 0.002 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card: oklch(0.12 0.004 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover: oklch(0.12 0.004 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.92 0.004 286.32);
|
||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--primary: oklch(0.62 0.2 255);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.18 0.004 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted: oklch(0.18 0.004 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent: oklch(0.18 0.004 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--border: oklch(1 0 0 / 8%);
|
||||
--input: oklch(1 0 0 / 10%);
|
||||
--ring: oklch(0.552 0.016 285.938);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar: oklch(0.1 0.003 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-primary: oklch(0.62 0.2 255);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.18 0.004 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-border: oklch(1 0 0 / 8%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function BotDetailDialog({
|
||||
<SidebarProvider className="items-start w-full flex">
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden md:flex h-[80vh] w-40 min-w-[120px] border-r bg-white"
|
||||
className="hidden md:flex h-[80vh] w-40 min-w-[120px] border-r bg-white dark:bg-black"
|
||||
>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
|
||||
@@ -6,12 +6,22 @@
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -47,6 +57,11 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoName {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.basicInfoDescription {
|
||||
@@ -58,6 +73,10 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoDescription {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.basicInfoAdapterContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -71,12 +90,20 @@
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoAdapterIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.basicInfoAdapterLabel {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoAdapterLabel {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.basicInfoPipelineContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -90,12 +117,20 @@
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoPipelineIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.basicInfoPipelineLabel {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoPipelineLabel {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.bigText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -394,7 +394,7 @@ export default function BotForm({
|
||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} {...field}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('bots.selectPipeline')}
|
||||
/>
|
||||
@@ -467,7 +467,7 @@ export default function BotForm({
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('bots.selectAdapter')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="fixed z-[1000]">
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.dark) .botLogCardContainer {
|
||||
background-color: #1f1f22;
|
||||
border: 1px solid #2a2a2e;
|
||||
}
|
||||
|
||||
.listHeader {
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '@/components/ui/hover-card';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { i18nObj } from '@/i18n/I18nProvider';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
@@ -132,7 +133,7 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.SELECT:
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('common.select')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -150,7 +151,7 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.selectModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -267,7 +268,7 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
||||
return (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('knowledge.selectKnowledgeBase')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -291,7 +292,7 @@ export default function DynamicFormItemComponent({
|
||||
<div key={index} className="flex gap-2 items-center">
|
||||
{/* 角色选择 */}
|
||||
{index === 0 ? (
|
||||
<div className="w-[120px] px-3 py-2 border rounded bg-gray-50 text-gray-500">
|
||||
<div className="w-[120px] px-3 py-2 border rounded bg-gray-50 dark:bg-[#2a292e] text-gray-500 dark:text-white dark:border-gray-600">
|
||||
system
|
||||
</div>
|
||||
) : (
|
||||
@@ -303,7 +304,7 @@ export default function DynamicFormItemComponent({
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -315,7 +316,7 @@ export default function DynamicFormItemComponent({
|
||||
</Select>
|
||||
)}
|
||||
{/* 内容输入 */}
|
||||
<Input
|
||||
<Textarea
|
||||
className="w-[300px]"
|
||||
value={item.content}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import styles from './emptyAndCreate.module.css';
|
||||
|
||||
export default function EmptyAndCreateComponent({
|
||||
title,
|
||||
subTitle,
|
||||
buttonText,
|
||||
onButtonClick,
|
||||
}: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
buttonText: string;
|
||||
onButtonClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={`${styles.emptyPageContainer}`}>
|
||||
<div className={`${styles.emptyContainer}`}>
|
||||
<div className={`${styles.emptyInfoContainer}`}>
|
||||
<div className={`${styles.emptyInfoText}`}>{title}</div>
|
||||
<div className={`${styles.emptyInfoSubText}`}>{subTitle}</div>
|
||||
</div>
|
||||
<div className={`${styles.emptyCreateButton}`} onClick={onButtonClick}>
|
||||
{buttonText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
.emptyPageContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.emptyContainer {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.emptyCreateButton {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border-radius: 20px;
|
||||
background-color: #2288ee;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.emptyCreateButton:hover {
|
||||
background-color: #1b77d2;
|
||||
}
|
||||
|
||||
.emptyInfoContainer {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #353535;
|
||||
}
|
||||
|
||||
.emptyInfoText {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.emptyInfoSubText {
|
||||
font-size: 28px;
|
||||
}
|
||||
@@ -13,6 +13,10 @@
|
||||
/* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
:global(.dark) .sidebarContainer {
|
||||
background-color: #0a0a0b !important;
|
||||
}
|
||||
|
||||
.langbotIconContainer {
|
||||
width: 200px;
|
||||
height: 70px;
|
||||
@@ -21,32 +25,49 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.langbotIcon {
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.langbotIcon {
|
||||
width: 2.8rem;
|
||||
height: 2.8rem;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.langbotTextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
:global(.dark) .langbotIcon {
|
||||
box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.langbotText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.langbotTextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.langbotVersion {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #6c6c6c;
|
||||
}
|
||||
.langbotText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .langbotText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.langbotVersion {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #6c6c6c;
|
||||
}
|
||||
|
||||
:global(.dark) .langbotVersion {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #a0a0a0 !important;
|
||||
}
|
||||
|
||||
.sidebarTopContainer {
|
||||
@@ -76,6 +97,7 @@
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
/* background-color: aqua; */
|
||||
}
|
||||
|
||||
@@ -85,16 +107,40 @@
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .sidebarSelected {
|
||||
background-color: #2288ee;
|
||||
color: white;
|
||||
box-shadow: 0 0 10px 0 rgba(34, 136, 238, 0.3);
|
||||
}
|
||||
|
||||
.sidebarUnselected {
|
||||
color: #6c6c6c;
|
||||
}
|
||||
|
||||
:global(.dark) .sidebarUnselected {
|
||||
color: #a0a0a0 !important;
|
||||
}
|
||||
|
||||
.sidebarUnselected:hover {
|
||||
background-color: rgba(34, 136, 238, 0.1);
|
||||
color: #2288ee;
|
||||
}
|
||||
|
||||
:global(.dark) .sidebarUnselected:hover {
|
||||
background-color: rgba(34, 136, 238, 0.2);
|
||||
color: #66baff;
|
||||
}
|
||||
|
||||
.sidebarChildIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(96, 149, 209, 0);
|
||||
}
|
||||
|
||||
.sidebarChildName {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.sidebarBottomContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -11,6 +11,18 @@ import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConf
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Moon, Sun, Monitor } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||
import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog';
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
export default function HomeSidebar({
|
||||
@@ -27,8 +39,11 @@ export default function HomeSidebar({
|
||||
}, [pathname]);
|
||||
|
||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [passwordChangeOpen, setPasswordChangeOpen] = useState(false);
|
||||
const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
initSelect();
|
||||
@@ -168,23 +183,113 @@ export default function HomeSidebar({
|
||||
}
|
||||
name={t('common.helpDocs')}
|
||||
/>
|
||||
<SidebarChild
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
// 防止语言选择器打开时关闭popover
|
||||
if (!open && languageSelectorOpen) return;
|
||||
setPopoverOpen(open);
|
||||
}}
|
||||
isSelected={false}
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
|
||||
</svg>
|
||||
}
|
||||
name={t('common.logout')}
|
||||
/>
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<SidebarChild
|
||||
onClick={() => {}}
|
||||
isSelected={false}
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 3C10.9 3 10 3.9 10 5C10 6.1 10.9 7 12 7C13.1 7 14 6.1 14 5C14 3.9 13.1 3 12 3ZM12 17C10.9 17 10 17.9 10 19C10 20.1 10.9 21 12 21C13.1 21 14 20.1 14 19C14 17.9 13.1 17 12 17ZM12 10C10.9 10 10 10.9 10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10Z"></path>
|
||||
</svg>
|
||||
}
|
||||
name={t('common.accountOptions')}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="end"
|
||||
className="w-auto p-4 flex flex-col gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<span className="text-sm font-medium">{t('common.theme')}</span>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={theme}
|
||||
onValueChange={(value) => {
|
||||
if (value) setTheme(value);
|
||||
}}
|
||||
className="justify-start"
|
||||
>
|
||||
<ToggleGroupItem value="light" size="sm">
|
||||
<Sun className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="dark" size="sm">
|
||||
<Moon className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="system" size="sm">
|
||||
<Monitor className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<span className="text-sm font-medium">
|
||||
{t('common.language')}
|
||||
</span>
|
||||
<LanguageSelector
|
||||
triggerClassName="w-full"
|
||||
onOpenChange={setLanguageSelectorOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<span className="text-sm font-medium">{t('common.account')}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start font-normal"
|
||||
onClick={() => {
|
||||
setPasswordChangeOpen(true);
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-4 h-4 mr-2"
|
||||
>
|
||||
<path d="M6 8V7C6 3.68629 8.68629 1 12 1C15.3137 1 18 3.68629 18 7V8H20C20.5523 8 21 8.44772 21 9V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V9C3 8.44772 3.44772 8 4 8H6ZM19 10H5V20H19V10ZM11 15.7324C10.4022 15.3866 10 14.7403 10 14C10 12.8954 10.8954 12 12 12C13.1046 12 14 12.8954 14 14C14 14.7403 13.5978 15.3866 13 15.7324V18H11V15.7324ZM8 8H16V7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7V8Z"></path>
|
||||
</svg>
|
||||
{t('common.changePassword')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start font-normal"
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-4 h-4 mr-2"
|
||||
>
|
||||
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
|
||||
</svg>
|
||||
{t('common.logout')}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<PasswordChangeDialog
|
||||
open={passwordChangeOpen}
|
||||
onOpenChange={setPasswordChangeOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
:global(.dark) .titleText {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.subtitleText {
|
||||
margin-left: 3.2rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -25,8 +29,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.dark) .subtitleText {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.helpLink {
|
||||
margin-left: 0.2rem;
|
||||
font-size: 0.8rem;
|
||||
color: #8b8b8b;
|
||||
}
|
||||
|
||||
:global(.dark) .helpLink {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
|
||||
const getFormSchema = (t: (key: string) => string) =>
|
||||
z
|
||||
.object({
|
||||
currentPassword: z
|
||||
.string()
|
||||
.min(1, { message: t('common.currentPasswordRequired') }),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(1, { message: t('common.newPasswordRequired') }),
|
||||
confirmNewPassword: z
|
||||
.string()
|
||||
.min(1, { message: t('common.confirmPasswordRequired') }),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmNewPassword, {
|
||||
message: t('common.passwordsDoNotMatch'),
|
||||
path: ['confirmNewPassword'],
|
||||
});
|
||||
|
||||
interface PasswordChangeDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function PasswordChangeDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: PasswordChangeDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const formSchema = getFormSchema(t);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmNewPassword: '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await httpClient.changePassword(
|
||||
values.currentPassword,
|
||||
values.newPassword,
|
||||
);
|
||||
toast.success(t('common.changePasswordSuccess'));
|
||||
form.reset();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
toast.error(t('common.changePasswordFailed'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.changePassword')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="currentPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.currentPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('common.enterCurrentPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.newPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('common.enterNewPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmNewPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.confirmNewPassword')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('common.enterConfirmPassword')}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -152,7 +152,7 @@ export default function KBDetailDialog({
|
||||
<SidebarProvider className="items-start w-full flex">
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden md:flex h-[80vh] w-40 min-w-[120px] border-r bg-white"
|
||||
className="hidden md:flex h-[80vh] w-40 min-w-[120px] border-r bg-white dark:bg-black"
|
||||
>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
|
||||
@@ -10,12 +10,22 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.basicInfoContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -35,6 +45,11 @@
|
||||
.basicInfoNameText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoNameText {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.basicInfoDescriptionText {
|
||||
@@ -48,6 +63,10 @@
|
||||
color: #b1b1b1;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoDescriptionText {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.basicInfoLastUpdatedTimeContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -58,11 +77,21 @@
|
||||
.basicInfoUpdateTimeIcon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoUpdateTimeIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.basicInfoUpdateTimeText {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoUpdateTimeText {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.operationContainer {
|
||||
@@ -86,12 +115,20 @@
|
||||
color: #ffcd27;
|
||||
}
|
||||
|
||||
:global(.dark) .operationDefaultBadgeIcon {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.operationDefaultBadgeText {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #ffcd27;
|
||||
}
|
||||
|
||||
:global(.dark) .operationDefaultBadgeText {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.bigText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface IKnowledgeBaseVO {
|
||||
name: string;
|
||||
description: string;
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
}
|
||||
|
||||
@@ -11,6 +12,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
name: string;
|
||||
description: string;
|
||||
embeddingModelUUID: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
|
||||
constructor(props: IKnowledgeBaseVO) {
|
||||
@@ -18,6 +20,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
||||
this.name = props.name;
|
||||
this.description = props.description;
|
||||
this.embeddingModelUUID = props.embeddingModelUUID;
|
||||
this.top_k = props.top_k;
|
||||
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +127,12 @@ export default function FileUploadZone({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-base font-medium text-gray-900">
|
||||
<p className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||||
{isUploading
|
||||
? t('knowledge.documentsTab.uploading')
|
||||
: t('knowledge.documentsTab.dragAndDrop')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
<p className="text-xs text-gray-500 mt-1 dark:text-gray-400">
|
||||
{t('knowledge.documentsTab.supportedFormats')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,10 @@ export const columns = (
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="bg-white dark:bg-[#2a2a2e]"
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{t('knowledge.documentsTab.actions')}
|
||||
</DropdownMenuLabel>
|
||||
|
||||
@@ -35,6 +35,10 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
embeddingModelUUID: z
|
||||
.string()
|
||||
.min(1, { message: t('knowledge.embeddingModelUUIDRequired') }),
|
||||
top_k: z
|
||||
.number()
|
||||
.min(1, { message: t('knowledge.topKRequired') })
|
||||
.max(30, { message: t('knowledge.topKMax') }),
|
||||
});
|
||||
|
||||
export default function KBForm({
|
||||
@@ -55,6 +59,7 @@ export default function KBForm({
|
||||
name: '',
|
||||
description: t('knowledge.defaultDescription'),
|
||||
embeddingModelUUID: '',
|
||||
top_k: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,6 +74,7 @@ export default function KBForm({
|
||||
form.setValue('name', val.name);
|
||||
form.setValue('description', val.description);
|
||||
form.setValue('embeddingModelUUID', val.embeddingModelUUID);
|
||||
form.setValue('top_k', val.top_k || 5);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -83,6 +89,7 @@ export default function KBForm({
|
||||
name: res.base.name,
|
||||
description: res.base.description,
|
||||
embeddingModelUUID: res.base.embedding_model_uuid,
|
||||
top_k: res.base.top_k || 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,6 +116,7 @@ export default function KBForm({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embedding_model_uuid: data.embeddingModelUUID,
|
||||
top_k: data.top_k,
|
||||
};
|
||||
httpClient
|
||||
.updateKnowledgeBase(initKbId, updateKb)
|
||||
@@ -127,6 +135,7 @@ export default function KBForm({
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
embedding_model_uuid: data.embeddingModelUUID,
|
||||
top_k: data.top_k,
|
||||
};
|
||||
httpClient
|
||||
.createKnowledgeBase(newKb)
|
||||
@@ -200,7 +209,7 @@ export default function KBForm({
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('knowledge.selectEmbeddingModel')}
|
||||
/>
|
||||
@@ -226,6 +235,30 @@ export default function KBForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="top_k"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('knowledge.topK')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
className="w-[180px] h-10 text-base appearance-none"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t('knowledge.topKdescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -46,6 +46,7 @@ export default function KnowledgePage() {
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
embeddingModelUUID: kb.embedding_model_uuid,
|
||||
top_k: kb.top_k ?? 5,
|
||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
:global(.dark) .homeLayoutContainer {
|
||||
background-color: #0a0a0b;
|
||||
}
|
||||
|
||||
/* 侧边栏区域 */
|
||||
.sidebar {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
:global(.dark) .sidebar {
|
||||
background-color: #0a0a0b;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main {
|
||||
background-color: #fafafa;
|
||||
@@ -23,6 +36,11 @@
|
||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .main {
|
||||
background-color: #151518;
|
||||
box-shadow: 0 0 6px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
padding: 1.5rem;
|
||||
padding-left: 2rem;
|
||||
@@ -30,3 +48,7 @@
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:global(.dark) .mainContent {
|
||||
background-color: #151518;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,22 @@
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -39,6 +49,11 @@
|
||||
.basicInfoText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoText {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.providerContainer {
|
||||
@@ -56,12 +71,20 @@
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .providerIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.providerLabel {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .providerLabel {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.baseURLContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -77,6 +100,10 @@
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .baseURLIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.baseURLText {
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
@@ -87,6 +114,10 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.dark) .baseURLText {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.bigText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -417,7 +417,7 @@ export default function EmbeddingForm({
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('models.selectModelProvider')}
|
||||
/>
|
||||
@@ -492,7 +492,7 @@ export default function EmbeddingForm({
|
||||
updateExtraArg(index, 'type', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -6,12 +6,22 @@
|
||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
padding: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.iconBasicInfoContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -40,6 +50,11 @@
|
||||
.basicInfoText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoText {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.providerContainer {
|
||||
@@ -57,12 +72,20 @@
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .providerIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.providerLabel {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .providerLabel {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.baseURLContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -78,6 +101,10 @@
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .baseURLIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.baseURLText {
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
@@ -88,6 +115,10 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.dark) .baseURLText {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.abilitiesContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -108,18 +139,30 @@
|
||||
background-color: #66baff80;
|
||||
}
|
||||
|
||||
:global(.dark) .abilityBadge {
|
||||
background-color: rgba(34, 136, 238, 0.3);
|
||||
}
|
||||
|
||||
.abilityIcon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #2288ee;
|
||||
}
|
||||
|
||||
:global(.dark) .abilityIcon {
|
||||
color: #66baff;
|
||||
}
|
||||
|
||||
.abilityLabel {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
color: #2288ee;
|
||||
}
|
||||
|
||||
:global(.dark) .abilityLabel {
|
||||
color: #66baff;
|
||||
}
|
||||
|
||||
.bigText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -432,7 +432,7 @@ export default function LLMForm({
|
||||
}}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('models.selectModelProvider')}
|
||||
/>
|
||||
@@ -565,7 +565,7 @@ export default function LLMForm({
|
||||
updateExtraArg(index, 'type', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function LLMConfigPage() {
|
||||
<Tabs defaultValue="llm" className="w-full">
|
||||
<div className="flex flex-row gap-0 mb-4">
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||
<TabsTrigger value="llm" className="px-6 py-4 cursor-pointer">
|
||||
{t('llm.llmModels')}
|
||||
</TabsTrigger>
|
||||
@@ -206,12 +206,14 @@ export default function LLMConfigPage() {
|
||||
</div>
|
||||
<TabsContent value="llm">
|
||||
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
|
||||
<p className="text-sm text-gray-500">{t('llm.description')}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('llm.description')}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="embedding">
|
||||
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('embedding.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function PipelineDialog({
|
||||
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden md:flex h-full min-h-0 w-40 border-r bg-white"
|
||||
className="hidden md:flex h-full min-h-0 w-40 border-r bg-white dark:bg-black"
|
||||
>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
|
||||
@@ -15,13 +15,13 @@ export default function AtBadge({
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 text-blue-600 hover:bg-blue-200"
|
||||
className="flex items-center gap-1 px-2 py-1 text-sm bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/60"
|
||||
>
|
||||
@{targetName}
|
||||
{!readonly && onRemove && (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="ml-1 hover:text-blue-800 focus:outline-none"
|
||||
className="ml-1 hover:text-blue-800 dark:hover:text-blue-200 focus:outline-none"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
|
||||
@@ -302,14 +302,14 @@ export default function DebugDialog({
|
||||
|
||||
const renderContent = () => (
|
||||
<div className="flex flex-1 h-full min-h-0">
|
||||
<div className="w-14 bg-white p-2 pl-0 flex-shrink-0 flex flex-col justify-start gap-2">
|
||||
<div className="w-14 bg-white dark:bg-black p-2 pl-0 flex-shrink-0 flex flex-col justify-start gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
||||
sessionType === 'person'
|
||||
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
||||
: 'bg-white text-gray-800 hover:bg-gray-100'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
} border-0 shadow-none`}
|
||||
onClick={() => setSessionType('person')}
|
||||
>
|
||||
@@ -328,7 +328,7 @@ export default function DebugDialog({
|
||||
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
||||
sessionType === 'group'
|
||||
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
||||
: 'bg-white text-gray-800 hover:bg-gray-100'
|
||||
: 'bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
} border-0 shadow-none`}
|
||||
onClick={() => setSessionType('group')}
|
||||
>
|
||||
@@ -345,7 +345,7 @@ export default function DebugDialog({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
|
||||
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white">
|
||||
<ScrollArea className="flex-1 p-6 overflow-y-auto min-h-0 bg-white dark:bg-black">
|
||||
<div className="space-y-6">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-lg">
|
||||
@@ -365,7 +365,7 @@ export default function DebugDialog({
|
||||
'max-w-md px-5 py-3 rounded-2xl',
|
||||
message.role === 'user'
|
||||
? 'bg-[#2288ee] text-white rounded-br-none'
|
||||
: 'bg-gray-100 text-gray-900 rounded-bl-none',
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-bl-none',
|
||||
)}
|
||||
>
|
||||
{renderMessageContent(message)}
|
||||
@@ -374,7 +374,7 @@ export default function DebugDialog({
|
||||
'text-xs mt-2',
|
||||
message.role === 'user'
|
||||
? 'text-white/70'
|
||||
: 'text-gray-500',
|
||||
: 'text-gray-500 dark:text-gray-400',
|
||||
)}
|
||||
>
|
||||
{message.role === 'user'
|
||||
@@ -389,7 +389,7 @@ export default function DebugDialog({
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<div className="p-4 pb-0 bg-white flex gap-2">
|
||||
<div className="p-4 pb-0 bg-white dark:bg-black flex gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">
|
||||
{t('pipelines.debugDialog.streaming')}
|
||||
@@ -412,23 +412,25 @@ export default function DebugDialog({
|
||||
? t('pipelines.debugDialog.privateChat')
|
||||
: t('pipelines.debugDialog.groupChat'),
|
||||
})}
|
||||
className="flex-1 rounded-md px-3 py-2 border border-gray-300 focus:border-[#2288ee] transition-none text-base"
|
||||
className="flex-1 rounded-md px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 focus:border-[#2288ee] transition-none text-base"
|
||||
/>
|
||||
{showAtPopover && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-white shadow-lg"
|
||||
className="absolute bottom-full left-0 mb-2 w-auto rounded-md border bg-white dark:bg-gray-800 dark:border-gray-600 shadow-lg"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
||||
isHovering ? 'bg-gray-100' : 'bg-white',
|
||||
isHovering
|
||||
? 'bg-gray-100 dark:bg-gray-700'
|
||||
: 'bg-white dark:bg-gray-800',
|
||||
)}
|
||||
onClick={handleAtSelect}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<span>
|
||||
<span className="text-gray-800 dark:text-gray-200">
|
||||
@webchatbot - {t('pipelines.debugDialog.atTips')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -459,7 +461,7 @@ export default function DebugDialog({
|
||||
|
||||
// 原有的Dialog包装
|
||||
return (
|
||||
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white">
|
||||
<DialogContent className="!max-w-[70vw] max-w-6xl h-[70vh] p-6 flex flex-col rounded-2xl shadow-2xl bg-white dark:bg-black">
|
||||
{renderContent()}
|
||||
</DialogContent>
|
||||
);
|
||||
|
||||
@@ -10,12 +10,22 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.basicInfoContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -35,6 +45,11 @@
|
||||
.basicInfoNameText {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoNameText {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.basicInfoDescriptionText {
|
||||
@@ -48,6 +63,10 @@
|
||||
color: #b1b1b1;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoDescriptionText {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.basicInfoLastUpdatedTimeContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -58,11 +77,21 @@
|
||||
.basicInfoUpdateTimeIcon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoUpdateTimeIcon {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.basicInfoUpdateTimeText {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #626262;
|
||||
}
|
||||
|
||||
:global(.dark) .basicInfoUpdateTimeText {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.operationContainer {
|
||||
@@ -86,12 +115,20 @@
|
||||
color: #ffcd27;
|
||||
}
|
||||
|
||||
:global(.dark) .operationDefaultBadgeIcon {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.operationDefaultBadgeText {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #ffcd27;
|
||||
}
|
||||
|
||||
:global(.dark) .operationDefaultBadgeText {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.bigText {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -342,7 +342,7 @@ export default function PipelineFormComponent({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="!max-w-[70vw] max-w-6xl h-full p-0 flex flex-col bg-white">
|
||||
<div className="!max-w-[70vw] max-w-6xl h-full p-0 flex flex-col bg-white dark:bg-black">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="pipeline-form"
|
||||
@@ -456,7 +456,7 @@ export default function PipelineFormComponent({
|
||||
</form>
|
||||
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
|
||||
{showButtons && (
|
||||
<div className="flex justify-end gap-2 pt-4 border-t mb-0 bg-white sticky bottom-0 z-10">
|
||||
<div className="flex justify-end gap-2 pt-4 border-t mb-0 bg-white dark:bg-black sticky bottom-0 z-10">
|
||||
{isEditMode && !isDefaultPipeline && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -125,17 +125,26 @@ export default function PluginConfigPage() {
|
||||
value={`${sortByValue},${sortOrderValue}`}
|
||||
onValueChange={handleSortChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] cursor-pointer bg-white dark:bg-gray-800">
|
||||
<SelectTrigger className="w-[180px] cursor-pointer bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('pipelines.sortBy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="created_at,DESC">
|
||||
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectItem
|
||||
value="created_at,DESC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('pipelines.newestCreated')}
|
||||
</SelectItem>
|
||||
<SelectItem value="updated_at,DESC">
|
||||
<SelectItem
|
||||
value="updated_at,DESC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('pipelines.recentlyEdited')}
|
||||
</SelectItem>
|
||||
<SelectItem value="updated_at,ASC">
|
||||
<SelectItem
|
||||
value="updated_at,ASC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('pipelines.earliestEdited')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function PluginConfigPage() {
|
||||
<div className={styles.pageContainer}>
|
||||
<Tabs defaultValue="installed" className="w-full">
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||
{t('plugins.installed')}
|
||||
</TabsTrigger>
|
||||
@@ -134,7 +134,7 @@ export default function PluginConfigPage() {
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="w-[500px] p-6">
|
||||
<DialogContent className="w-[500px] p-6 bg-white dark:bg-[#1a1a1e]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-4">
|
||||
<GithubIcon className="size-6" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function PluginCardComponent({
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer"
|
||||
className="w-[100%] h-[10rem] bg-white dark:bg-[#1f1f22] rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] dark:shadow-[0] p-[1.2rem] cursor-pointer transition-all duration-200 hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.1)] dark:hover:shadow-[0]"
|
||||
onClick={onCardClick}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
@@ -50,18 +50,20 @@ export default function PluginCardComponent({
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666]">
|
||||
<div className="text-[0.7rem] text-[#666] dark:text-[#a0a0a0]">
|
||||
{cardVO.author} /{' '}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
|
||||
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0]">
|
||||
{cardVO.name}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2">
|
||||
<div className="text-[0.8rem] text-[#666] dark:text-[#888888] line-clamp-2">
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,14 +71,14 @@ export default function PluginCardComponent({
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-black"
|
||||
className="w-[1.2rem] h-[1.2rem] text-black dark:text-[#f0f0f0]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M24 12L18.3431 17.6569L16.9289 16.2426L21.1716 12L16.9289 7.75736L18.3431 6.34315L24 12ZM2.82843 12L7.07107 16.2426L5.65685 17.6569L0 12L5.65685 6.34315L7.07107 7.75736L2.82843 12ZM9.78845 21H7.66009L14.2116 3H16.3399L9.78845 21Z"></path>
|
||||
</svg>
|
||||
<div className="text-base text-black font-medium">
|
||||
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
|
||||
{t('plugins.eventCount', {
|
||||
count: Object.keys(cardVO.event_handlers).length,
|
||||
})}
|
||||
@@ -85,14 +87,14 @@ export default function PluginCardComponent({
|
||||
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-black"
|
||||
className="w-[1.2rem] h-[1.2rem] text-black dark:text-[#f0f0f0]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
|
||||
</svg>
|
||||
<div className="text-base text-black font-medium">
|
||||
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
|
||||
{t('plugins.toolCount', { count: cardVO.tools.length })}
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +117,9 @@ export default function PluginCardComponent({
|
||||
<div className="flex items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className={`w-[1.4rem] h-[1.4rem] cursor-pointer ${
|
||||
cardVO.repository ? 'text-black' : 'text-gray-400'
|
||||
cardVO.repository
|
||||
? 'text-black dark:text-[#f0f0f0]'
|
||||
: 'text-gray-400 dark:text-gray-600'
|
||||
}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -141,15 +141,26 @@ export default function PluginMarketComponent({
|
||||
value={`${sortByValue},${sortOrderValue}`}
|
||||
onValueChange={handleSortChange}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] ml-2 cursor-pointer">
|
||||
<SelectTrigger className="w-[180px] ml-2 cursor-pointer bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('plugins.sortBy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stars,DESC">{t('plugins.mostStars')}</SelectItem>
|
||||
<SelectItem value="created_at,DESC">
|
||||
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectItem
|
||||
value="stars,DESC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('plugins.mostStars')}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="created_at,DESC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('plugins.recentlyAdded')}
|
||||
</SelectItem>
|
||||
<SelectItem value="pushed_at,DESC">
|
||||
<SelectItem
|
||||
value="pushed_at,DESC"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{t('plugins.recentlyUpdated')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -198,7 +209,7 @@ export default function PluginMarketComponent({
|
||||
isActive={pageNum === nowPage}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
<span className="text-black select-none">
|
||||
<span className="text-black dark:text-white select-none">
|
||||
{pageNum}
|
||||
</span>
|
||||
</PaginationLink>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function PluginMarketCardComponent({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem]">
|
||||
<div className="w-[100%] h-[10rem] bg-white dark:bg-[#1f1f22] rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] dark:shadow-[0] p-[1.2rem] transition-all duration-200 hover:shadow-[0px_2px_8px_0_rgba(0,0,0,0.1)] dark:hover:shadow-[0]">
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
<svg
|
||||
className="w-16 h-16 text-[#2288ee]"
|
||||
@@ -30,15 +30,17 @@ export default function PluginMarketCardComponent({
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666]">
|
||||
<div className="text-[0.7rem] text-[#666] dark:text-[#a0a0a0]">
|
||||
{cardVO.author} /{' '}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black">{cardVO.name}</div>
|
||||
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0]">
|
||||
{cardVO.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2">
|
||||
<div className="text-[0.8rem] text-[#666] dark:text-[#888888] line-clamp-2">
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,21 +48,21 @@ export default function PluginMarketCardComponent({
|
||||
<div className="w-full flex flex-row items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.2rem] h-[1.2rem] text-[#ffcd27]"
|
||||
className="w-[1.2rem] h-[1.2rem] text-[#ffcd27] dark:text-[#fbbf24]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12.0006 18.26L4.94715 22.2082L6.52248 14.2799L0.587891 8.7918L8.61493 7.84006L12.0006 0.5L15.3862 7.84006L23.4132 8.7918L17.4787 14.2799L19.054 22.2082L12.0006 18.26Z"></path>
|
||||
</svg>
|
||||
<div className="text-base text-[#ffcd27] font-medium">
|
||||
<div className="text-base text-[#ffcd27] dark:text-[#fbbf24] font-medium">
|
||||
{t('plugins.starCount', { count: cardVO.starCount })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||
<svg
|
||||
className="w-[1.4rem] h-[1.4rem] text-black cursor-pointer"
|
||||
className="w-[1.4rem] h-[1.4rem] text-black dark:text-[#f0f0f0] cursor-pointer"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -7,13 +7,27 @@
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer {
|
||||
background-color: #1f1f22;
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.cardContainer:hover {
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .cardContainer:hover {
|
||||
box-shadow: 0;
|
||||
}
|
||||
|
||||
.createCardContainer {
|
||||
font-size: 90px;
|
||||
color: #acacac;
|
||||
}
|
||||
|
||||
:global(.dark) .createCardContainer {
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface KnowledgeBase {
|
||||
description: string;
|
||||
embedding_model_uuid: string;
|
||||
created_at?: string;
|
||||
top_k?: number;
|
||||
top_k: number;
|
||||
}
|
||||
|
||||
export interface ApiRespProviderEmbeddingModels {
|
||||
@@ -155,6 +155,7 @@ export interface KnowledgeBase {
|
||||
name: string;
|
||||
description: string;
|
||||
embedding_model_uuid: string;
|
||||
top_k: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
@@ -715,6 +715,16 @@ class HttpClient {
|
||||
new_password: newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
public changePassword(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
): Promise<{ user: string }> {
|
||||
return this.post('/api/v1/user/change-password', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getBaseURL = (): string => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'react-photo-view/dist/react-photo-view.css';
|
||||
import type { Metadata } from 'next';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import I18nProvider from '@/i18n/I18nProvider';
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'LangBot',
|
||||
@@ -15,12 +16,14 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html>
|
||||
<html lang="zh" suppressHydrationWarning>
|
||||
<body className={``}>
|
||||
<I18nProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</I18nProvider>
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -8,13 +8,7 @@ import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -26,15 +20,15 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Mail, Lock, Globe } from 'lucide-react';
|
||||
import { Mail, Lock } from 'lucide-react';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
import Link from 'next/link';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
|
||||
const formSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
@@ -45,7 +39,6 @@ const formSchema = (t: (key: string) => string) =>
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
||||
|
||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||
resolver: zodResolver(formSchema(t)),
|
||||
@@ -56,57 +49,10 @@ export default function Login() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
judgeLanguage();
|
||||
getIsInitialized();
|
||||
checkIfAlreadyLoggedIn();
|
||||
}, []);
|
||||
|
||||
const judgeLanguage = () => {
|
||||
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
|
||||
setCurrentLanguage('zh-Hans');
|
||||
localStorage.setItem('langbot_language', 'zh-Hans');
|
||||
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
|
||||
setCurrentLanguage('zh-Hant');
|
||||
localStorage.setItem('langbot_language', 'zh-Hant');
|
||||
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
|
||||
setCurrentLanguage('ja-JP');
|
||||
localStorage.setItem('langbot_language', 'ja-JP');
|
||||
} else {
|
||||
setCurrentLanguage('en-US');
|
||||
localStorage.setItem('langbot_language', 'en-US');
|
||||
}
|
||||
// check if the language is already set
|
||||
const lang = localStorage.getItem('langbot_language');
|
||||
if (lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
setCurrentLanguage(lang);
|
||||
return;
|
||||
} else {
|
||||
const language = navigator.language;
|
||||
if (language) {
|
||||
let lang = 'zh-Hans';
|
||||
if (language === 'zh-CN') {
|
||||
lang = 'zh-Hans';
|
||||
} else if (language === 'zh-TW') {
|
||||
lang = 'zh-Hant';
|
||||
} else if (language === 'ja' || language === 'ja-JP') {
|
||||
lang = 'ja-JP';
|
||||
} else {
|
||||
lang = 'en-US';
|
||||
}
|
||||
i18n.changeLanguage(lang);
|
||||
setCurrentLanguage(lang);
|
||||
localStorage.setItem('langbot_language', lang);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
setCurrentLanguage(value);
|
||||
localStorage.setItem('langbot_language', value);
|
||||
};
|
||||
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
@@ -155,25 +101,12 @@ export default function Login() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[375px]">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:dark:bg-neutral-900">
|
||||
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex justify-end mb-6">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder={t('common.language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zh-Hans">简体中文</SelectItem>
|
||||
<SelectItem value="zh-Hant">繁體中文</SelectItem>
|
||||
<SelectItem value="en-US">English</SelectItem>
|
||||
<SelectItem value="ja-JP">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<ThemeToggle />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<img
|
||||
src={langbotIcon.src}
|
||||
|
||||
@@ -8,13 +8,7 @@ import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
@@ -26,14 +20,14 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Mail, Lock, Globe } from 'lucide-react';
|
||||
import { Mail, Lock } from 'lucide-react';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
|
||||
const formSchema = (t: (key: string) => string) =>
|
||||
z.object({
|
||||
@@ -44,7 +38,6 @@ const formSchema = (t: (key: string) => string) =>
|
||||
export default function Register() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
||||
|
||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||
resolver: zodResolver(formSchema(t)),
|
||||
@@ -55,58 +48,9 @@ export default function Register() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
judgeLanguage();
|
||||
getIsInitialized();
|
||||
}, []);
|
||||
|
||||
const judgeLanguage = () => {
|
||||
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
|
||||
setCurrentLanguage('zh-Hans');
|
||||
localStorage.setItem('langbot_language', 'zh-Hans');
|
||||
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
|
||||
setCurrentLanguage('zh-Hant');
|
||||
localStorage.setItem('langbot_language', 'zh-Hant');
|
||||
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
|
||||
setCurrentLanguage('ja-JP');
|
||||
localStorage.setItem('langbot_language', 'ja-JP');
|
||||
} else {
|
||||
setCurrentLanguage('en-US');
|
||||
localStorage.setItem('langbot_language', 'en-US');
|
||||
}
|
||||
// check if the language is already set
|
||||
const lang = localStorage.getItem('langbot_language');
|
||||
console.log('lang: ', lang);
|
||||
if (lang) {
|
||||
i18n.changeLanguage(lang);
|
||||
setCurrentLanguage(lang);
|
||||
} else {
|
||||
const language = navigator.language;
|
||||
if (language) {
|
||||
let lang = 'zh-Hans';
|
||||
if (language === 'zh-CN') {
|
||||
lang = 'zh-Hans';
|
||||
} else if (language === 'zh-TW') {
|
||||
lang = 'zh-Hant';
|
||||
} else if (language === 'ja' || language === 'ja-JP') {
|
||||
lang = 'ja-JP';
|
||||
} else {
|
||||
lang = 'en-US';
|
||||
}
|
||||
console.log('language: ', lang);
|
||||
i18n.changeLanguage(lang);
|
||||
setCurrentLanguage(lang);
|
||||
localStorage.setItem('langbot_language', lang);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
console.log('handleLanguageChange: ', value);
|
||||
i18n.changeLanguage(value);
|
||||
setCurrentLanguage(value);
|
||||
localStorage.setItem('langbot_language', value);
|
||||
};
|
||||
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
@@ -139,25 +83,12 @@ export default function Register() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[375px]">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex justify-end mb-6">
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder={t('common.language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zh-Hans">简体中文</SelectItem>
|
||||
<SelectItem value="zh-Hant">繁體中文</SelectItem>
|
||||
<SelectItem value="en-US">English</SelectItem>
|
||||
<SelectItem value="ja-JP">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<ThemeToggle />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<img
|
||||
src={langbotIcon.src}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Mail, Lock, ArrowLeft } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Link from 'next/link';
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||
|
||||
const REGEXP_ONLY_DIGITS_AND_CHARS = /^[0-9a-zA-Z]+$/;
|
||||
|
||||
@@ -84,17 +85,18 @@ export default function ResetPassword() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[375px]">
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
||||
className="flex items-center text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
{t('resetPassword.backToLogin')}
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<CardTitle className="text-2xl text-center">
|
||||
{t('resetPassword.title')}
|
||||
|
||||
18
web/src/components/providers/theme-provider.tsx
Normal file
18
web/src/components/providers/theme-provider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import { type ThemeProviderProps } from 'next-themes';
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const buttonVariants = cva(
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/100',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
|
||||
252
web/src/components/ui/context-menu.tsx
Normal file
252
web/src/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function ContextMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioGroup
|
||||
data-slot="context-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn(
|
||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
98
web/src/components/ui/language-selector.tsx
Normal file
98
web/src/components/ui/language-selector.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface LanguageSelectorProps {
|
||||
className?: string;
|
||||
triggerClassName?: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function LanguageSelector({
|
||||
triggerClassName,
|
||||
onOpenChange,
|
||||
}: LanguageSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
||||
|
||||
useEffect(() => {
|
||||
initializeLanguage();
|
||||
}, []);
|
||||
|
||||
const initializeLanguage = () => {
|
||||
if (i18n.language === 'zh-CN' || i18n.language === 'zh-Hans') {
|
||||
setCurrentLanguage('zh-Hans');
|
||||
localStorage.setItem('langbot_language', 'zh-Hans');
|
||||
} else if (i18n.language === 'zh-TW' || i18n.language === 'zh-Hant') {
|
||||
setCurrentLanguage('zh-Hant');
|
||||
localStorage.setItem('langbot_language', 'zh-Hant');
|
||||
} else if (i18n.language === 'ja' || i18n.language === 'ja-JP') {
|
||||
setCurrentLanguage('ja-JP');
|
||||
localStorage.setItem('langbot_language', 'ja-JP');
|
||||
} else {
|
||||
setCurrentLanguage('en-US');
|
||||
localStorage.setItem('langbot_language', 'en-US');
|
||||
}
|
||||
|
||||
const savedLanguage = localStorage.getItem('langbot_language');
|
||||
if (savedLanguage) {
|
||||
i18n.changeLanguage(savedLanguage);
|
||||
setCurrentLanguage(savedLanguage);
|
||||
} else {
|
||||
const browserLanguage = navigator.language;
|
||||
if (browserLanguage) {
|
||||
let detectedLanguage = 'zh-Hans';
|
||||
if (browserLanguage === 'zh-CN') {
|
||||
detectedLanguage = 'zh-Hans';
|
||||
} else if (browserLanguage === 'zh-TW') {
|
||||
detectedLanguage = 'zh-Hant';
|
||||
} else if (browserLanguage === 'ja' || browserLanguage === 'ja-JP') {
|
||||
detectedLanguage = 'ja-JP';
|
||||
} else {
|
||||
detectedLanguage = 'en-US';
|
||||
}
|
||||
i18n.changeLanguage(detectedLanguage);
|
||||
setCurrentLanguage(detectedLanguage);
|
||||
localStorage.setItem('langbot_language', detectedLanguage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageChange = (value: string) => {
|
||||
i18n.changeLanguage(value);
|
||||
setCurrentLanguage(value);
|
||||
localStorage.setItem('langbot_language', value);
|
||||
|
||||
// 刷新页面以应用新的语言设置
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<SelectTrigger className={triggerClassName || 'w-[140px]'}>
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder={t('common.language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="zh-Hans">简体中文</SelectItem>
|
||||
<SelectItem value="zh-Hant">繁體中文</SelectItem>
|
||||
<SelectItem value="en-US">English</SelectItem>
|
||||
<SelectItem value="ja-JP">日本語</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ function PaginationPrevious({
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="text-black" />
|
||||
<ChevronLeftIcon className="text-black dark:text-white" />
|
||||
<span className="hidden sm:block"></span>
|
||||
</PaginationLink>
|
||||
);
|
||||
@@ -94,7 +94,7 @@ function PaginationNext({
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block"></span>
|
||||
<ChevronRightIcon className="text-black" />
|
||||
<ChevronRightIcon className="text-black dark:text-white" />
|
||||
</PaginationLink>
|
||||
);
|
||||
}
|
||||
|
||||
18
web/src/components/ui/textarea.tsx
Normal file
18
web/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
23
web/src/components/ui/theme-toggle.tsx
Normal file
23
web/src/components/ui/theme-toggle.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ const enUS = {
|
||||
common: {
|
||||
login: 'Login',
|
||||
logout: 'Logout',
|
||||
accountOptions: 'Account',
|
||||
account: 'Account',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
welcome: 'Welcome back to LangBot 👋',
|
||||
@@ -41,6 +43,21 @@ const enUS = {
|
||||
test: 'Test',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
loading: 'Loading...',
|
||||
theme: 'Theme',
|
||||
changePassword: 'Change Password',
|
||||
currentPassword: 'Current Password',
|
||||
newPassword: 'New Password',
|
||||
confirmNewPassword: 'Confirm New Password',
|
||||
enterCurrentPassword: 'Enter current password',
|
||||
enterNewPassword: 'Enter new password',
|
||||
enterConfirmPassword: 'Confirm new password',
|
||||
currentPasswordRequired: 'Current password is required',
|
||||
newPasswordRequired: 'New password is required',
|
||||
confirmPasswordRequired: 'Confirm password is required',
|
||||
passwordsDoNotMatch: 'Passwords do not match',
|
||||
changePasswordSuccess: 'Password changed successfully',
|
||||
changePasswordFailed:
|
||||
'Failed to change password, please check your current password',
|
||||
},
|
||||
notFound: {
|
||||
title: 'Page not found',
|
||||
@@ -253,6 +270,11 @@ const enUS = {
|
||||
today: 'Today',
|
||||
kbName: 'Knowledge Base Name',
|
||||
kbDescription: 'Knowledge Base Description',
|
||||
topK: 'Top K',
|
||||
topKRequired: 'Top K cannot be empty',
|
||||
topKMax: 'Top K maximum value is 30',
|
||||
topKdescription:
|
||||
'Used to specify the number of relevant documents to retrieve, ranging from 1 to 30.',
|
||||
defaultDescription: 'A knowledge base',
|
||||
embeddingModelUUID: 'Embedding Model',
|
||||
selectEmbeddingModel: 'Select Embedding Model',
|
||||
|
||||
@@ -2,6 +2,8 @@ const jaJP = {
|
||||
common: {
|
||||
login: 'ログイン',
|
||||
logout: 'ログアウト',
|
||||
accountOptions: 'アカウントオプション',
|
||||
account: 'アカウント',
|
||||
email: 'メールアドレス',
|
||||
password: 'パスワード',
|
||||
welcome: 'LangBot へおかえりなさい 👋',
|
||||
@@ -42,6 +44,21 @@ const jaJP = {
|
||||
test: 'テスト',
|
||||
forgotPassword: 'パスワードを忘れた?',
|
||||
loading: '読み込み中...',
|
||||
theme: 'テーマ',
|
||||
changePassword: 'パスワードを変更',
|
||||
currentPassword: '現在のパスワード',
|
||||
newPassword: '新しいパスワード',
|
||||
confirmNewPassword: '新しいパスワードを確認',
|
||||
enterCurrentPassword: '現在のパスワードを入力',
|
||||
enterNewPassword: '新しいパスワードを入力',
|
||||
enterConfirmPassword: '新しいパスワードを確認',
|
||||
currentPasswordRequired: '現在のパスワードは必須です',
|
||||
newPasswordRequired: '新しいパスワードは必須です',
|
||||
confirmPasswordRequired: '新しいパスワードを確認してください',
|
||||
passwordsDoNotMatch: '新しいパスワードが一致しません',
|
||||
changePasswordSuccess: 'パスワードの変更に成功しました',
|
||||
changePasswordFailed:
|
||||
'パスワードの変更に失敗しました。現在のパスワードを確認してください',
|
||||
},
|
||||
notFound: {
|
||||
title: 'ページが見つかりません',
|
||||
@@ -255,6 +272,11 @@ const jaJP = {
|
||||
today: '今日',
|
||||
kbName: '知識ベース名',
|
||||
kbDescription: '知識ベースの説明',
|
||||
topK: 'Top K',
|
||||
topKRequired: 'Top Kは必須です',
|
||||
topKMax: 'Top Kの最大値は30です',
|
||||
topKdescription:
|
||||
'取得する関連性の高い上位K件の文書の数。1~30の範囲で設定できます',
|
||||
defaultDescription: '知識ベース',
|
||||
embeddingModelUUID: '埋め込みモデル',
|
||||
selectEmbeddingModel: '埋め込みモデルを選択',
|
||||
|
||||
@@ -2,6 +2,8 @@ const zhHans = {
|
||||
common: {
|
||||
login: '登录',
|
||||
logout: '退出登录',
|
||||
accountOptions: '账户选项',
|
||||
account: '账户',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
welcome: '欢迎回到 LangBot 👋',
|
||||
@@ -41,6 +43,20 @@ const zhHans = {
|
||||
test: '测试',
|
||||
forgotPassword: '忘记密码?',
|
||||
loading: '加载中...',
|
||||
theme: '主题',
|
||||
changePassword: '修改密码',
|
||||
currentPassword: '当前密码',
|
||||
newPassword: '新密码',
|
||||
confirmNewPassword: '确认新密码',
|
||||
enterCurrentPassword: '输入当前密码',
|
||||
enterNewPassword: '输入新密码',
|
||||
enterConfirmPassword: '确认新密码',
|
||||
currentPasswordRequired: '当前密码不能为空',
|
||||
newPasswordRequired: '新密码不能为空',
|
||||
confirmPasswordRequired: '确认密码不能为空',
|
||||
passwordsDoNotMatch: '两次输入的密码不一致',
|
||||
changePasswordSuccess: '密码修改成功',
|
||||
changePasswordFailed: '密码修改失败,请检查当前密码是否正确',
|
||||
},
|
||||
notFound: {
|
||||
title: '页面不存在',
|
||||
@@ -248,6 +264,10 @@ const zhHans = {
|
||||
today: '今天',
|
||||
kbName: '知识库名称',
|
||||
kbDescription: '知识库描述',
|
||||
topK: '召回数量',
|
||||
topKRequired: '召回数量不能为空',
|
||||
topKMax: '召回数量最大值为 30',
|
||||
topKdescription: '召回相关文档块的数量,取值范围为 1-30',
|
||||
defaultDescription: '一个知识库',
|
||||
embeddingModelUUID: '嵌入模型',
|
||||
selectEmbeddingModel: '选择嵌入模型',
|
||||
|
||||
@@ -2,6 +2,8 @@ const zhHant = {
|
||||
common: {
|
||||
login: '登入',
|
||||
logout: '登出',
|
||||
accountOptions: '帳戶選項',
|
||||
account: '帳戶',
|
||||
email: '電子郵件',
|
||||
password: '密碼',
|
||||
welcome: '歡迎回到 LangBot 👋',
|
||||
@@ -41,6 +43,20 @@ const zhHant = {
|
||||
test: '測試',
|
||||
forgotPassword: '忘記密碼?',
|
||||
loading: '載入中...',
|
||||
theme: '主題',
|
||||
changePassword: '修改密碼',
|
||||
currentPassword: '當前密碼',
|
||||
newPassword: '新密碼',
|
||||
confirmNewPassword: '確認新密碼',
|
||||
enterCurrentPassword: '輸入當前密碼',
|
||||
enterNewPassword: '輸入新密碼',
|
||||
enterConfirmPassword: '確認新密碼',
|
||||
currentPasswordRequired: '當前密碼不能為空',
|
||||
newPasswordRequired: '新密碼不能為空',
|
||||
confirmPasswordRequired: '確認密碼不能為空',
|
||||
passwordsDoNotMatch: '兩次輸入的密碼不一致',
|
||||
changePasswordSuccess: '密碼修改成功',
|
||||
changePasswordFailed: '密碼修改失敗,請檢查當前密碼是否正確',
|
||||
},
|
||||
notFound: {
|
||||
title: '頁面不存在',
|
||||
@@ -236,10 +252,10 @@ const zhHant = {
|
||||
editKnowledgeBase: '編輯知識庫',
|
||||
selectKnowledgeBase: '選擇知識庫',
|
||||
empty: '無',
|
||||
editDocument: '文件',
|
||||
editDocument: '文檔',
|
||||
description: '設定可用於提升模型回覆品質的知識庫',
|
||||
metadata: '中繼資料',
|
||||
documents: '文件',
|
||||
documents: '文檔',
|
||||
kbNameRequired: '知識庫名稱不能為空',
|
||||
kbDescriptionRequired: '知識庫描述不能為空',
|
||||
embeddingModelUUIDRequired: '嵌入模型不能為空',
|
||||
@@ -247,6 +263,10 @@ const zhHant = {
|
||||
today: '今天',
|
||||
kbName: '知識庫名稱',
|
||||
kbDescription: '知識庫描述',
|
||||
topK: '召回數量 ',
|
||||
topKRequired: '召回數量不能為空',
|
||||
topKMax: '召回數量最大值為30',
|
||||
topKdescription: '取得相關性高的上位 K 件文獻的數量,範圍為1~30',
|
||||
defaultDescription: '一個知識庫',
|
||||
embeddingModelUUID: '嵌入模型',
|
||||
selectEmbeddingModel: '選擇嵌入模型',
|
||||
@@ -259,29 +279,29 @@ const zhHant = {
|
||||
name: '名稱',
|
||||
status: '狀態',
|
||||
noResults: '暫無文件',
|
||||
dragAndDrop: '拖曳檔案到此處或點擊上傳',
|
||||
dragAndDrop: '拖曳文檔到此處或點擊上傳',
|
||||
uploading: '上傳中...',
|
||||
supportedFormats: '支援 PDF、Word、TXT、Markdown 等文件格式',
|
||||
uploadSuccess: '檔案上傳成功!',
|
||||
uploadError: '檔案上傳失敗,請重試',
|
||||
uploadingFile: '上傳檔案中...',
|
||||
supportedFormats: '支援 PDF、Word、TXT、Markdown 等文檔格式',
|
||||
uploadSuccess: '文檔上傳成功!',
|
||||
uploadError: '文檔上傳失敗,請重試',
|
||||
uploadingFile: '上傳文檔中...',
|
||||
actions: '操作',
|
||||
delete: '刪除檔案',
|
||||
fileDeleteSuccess: '檔案刪除成功',
|
||||
fileDeleteFailed: '檔案刪除失敗',
|
||||
delete: '刪除文檔',
|
||||
fileDeleteSuccess: '文檔刪除成功',
|
||||
fileDeleteFailed: '文檔刪除失敗',
|
||||
processing: '處理中',
|
||||
completed: '完成',
|
||||
failed: '失敗',
|
||||
},
|
||||
deleteKnowledgeBaseConfirmation:
|
||||
'您確定要刪除這個知識庫嗎?此知識庫下的所有文件將被刪除。',
|
||||
'您確定要刪除這個知識庫嗎?此知識庫下的所有文檔將被刪除。',
|
||||
retrieve: '檢索測試',
|
||||
retrieveTest: '檢索測試',
|
||||
query: '查詢',
|
||||
queryPlaceholder: '輸入查詢內容...',
|
||||
distance: '距離',
|
||||
content: '內容',
|
||||
fileName: '檔案名稱',
|
||||
fileName: '文檔名稱',
|
||||
noResults: '暫無結果',
|
||||
retrieveError: '檢索失敗',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user