mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-28 21:27:14 +00:00
Merge branch 'master' into feat/streaming
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
<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_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
@@ -85,8 +87,9 @@ docker compose up -d
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
||||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
||||||
| 微信 | ✅ | |
|
| 企业微信 | ✅ | |
|
||||||
| 企微对外客服 | ✅ | |
|
| 企微对外客服 | ✅ | |
|
||||||
|
| 个人微信 | ✅ | |
|
||||||
| 微信公众号 | ✅ | |
|
| 微信公众号 | ✅ | |
|
||||||
| 飞书 | ✅ | |
|
| 飞书 | ✅ | |
|
||||||
| 钉钉 | ✅ | |
|
| 钉钉 | ✅ | |
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
||||||
</a>
|
</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)
|
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class QQOfficialClient:
|
|||||||
return {'code': 0, 'message': 'success'}
|
return {'code': 0, 'message': 'success'}
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return {'error': str(e)}, 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -168,7 +168,6 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -193,7 +192,6 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -209,7 +207,7 @@ class QQOfficialClient:
|
|||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await self.logger.error(f"发送群聊消息失败:{response.json()}")
|
await self.logger.error(f'发送群聊消息失败:{response.json()}')
|
||||||
raise Exception(response.read().decode())
|
raise Exception(response.read().decode())
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
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():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
url = self.base_url + '/channels/' + channel_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -241,7 +238,6 @@ class QQOfficialClient:
|
|||||||
if not await self.check_access_token():
|
if not await self.check_access_token():
|
||||||
await self.get_access_token()
|
await self.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
url = self.base_url + '/dms/' + guild_id + '/messages'
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
headers = {
|
headers = {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class SlackClient:
|
|||||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
# 处理私信
|
# 处理私信
|
||||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||||
event = SlackEvent.from_payload(data)
|
event = SlackEvent.from_payload(data)
|
||||||
@@ -52,7 +51,7 @@ class SlackClient:
|
|||||||
return jsonify({'status': 'ok'})
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
except Exception as e:
|
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)
|
raise (e)
|
||||||
|
|
||||||
async def _handle_message(self, event: SlackEvent):
|
async def _handle_message(self, event: SlackEvent):
|
||||||
@@ -82,7 +81,7 @@ class SlackClient:
|
|||||||
self.bot_user_id = response['message']['bot_id']
|
self.bot_user_id = response['message']['bot_id']
|
||||||
return
|
return
|
||||||
except Exception as e:
|
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
|
raise e
|
||||||
|
|
||||||
async def send_message_to_one(self, text: str, user_id: str):
|
async def send_message_to_one(self, text: str, user_id: str):
|
||||||
@@ -93,7 +92,7 @@ class SlackClient:
|
|||||||
|
|
||||||
return
|
return
|
||||||
except Exception as e:
|
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
|
raise e
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
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)
|
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 = {
|
param = {'Recover': recover, 'Style': style}
|
||||||
"Recover": recover,
|
|
||||||
"Style": style
|
|
||||||
}
|
|
||||||
url = f'{self.base_url}/user/GetMyQRCode'
|
url = f'{self.base_url}/user/GetMyQRCode'
|
||||||
return post_json(base_url=url, token=self.token, data=param)
|
return post_json(base_url=url, token=self.token, data=param)
|
||||||
|
|
||||||
@@ -26,12 +23,8 @@ class UserApi:
|
|||||||
url = f'{self.base_url}/equipment/GetSafetyInfo'
|
url = f'{self.base_url}/equipment/GetSafetyInfo'
|
||||||
return post_json(base_url=url, token=self.token)
|
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 = {
|
param = {'Base64': head_img_base64}
|
||||||
"Base64": head_img_base64
|
|
||||||
}
|
|
||||||
url = f'{self.base_url}/user/UploadHeadImage'
|
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.login import LoginApi
|
||||||
from libs.wechatpad_api.api.friend import FriendApi
|
from libs.wechatpad_api.api.friend import FriendApi
|
||||||
from libs.wechatpad_api.api.message import MessageApi
|
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
|
from libs.wechatpad_api.api.chatroom import ChatRoomApi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WeChatPadClient:
|
class WeChatPadClient:
|
||||||
def __init__(self, base_url, token, logger=None):
|
def __init__(self, base_url, token, logger=None):
|
||||||
self._login_api = LoginApi(base_url, token)
|
self._login_api = LoginApi(base_url, token)
|
||||||
@@ -20,16 +16,16 @@ class WeChatPadClient:
|
|||||||
self._chatroom_api = ChatRoomApi(base_url, token)
|
self._chatroom_api = ChatRoomApi(base_url, token)
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
|
|
||||||
def get_token(self,admin_key, day: int):
|
def get_token(self, admin_key, day: int):
|
||||||
'''获取token'''
|
"""获取token"""
|
||||||
return self._login_api.get_token(admin_key, day)
|
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)
|
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)
|
return self._login_api.wake_up_login(Proxy=Proxy)
|
||||||
|
|
||||||
def log_out(self):
|
def log_out(self):
|
||||||
@@ -40,59 +36,57 @@ class WeChatPadClient:
|
|||||||
"""获取登录状态"""
|
"""获取登录状态"""
|
||||||
return self._login_api.get_login_status()
|
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):
|
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):
|
def send_app_message(self, to_wxid, app_message, type):
|
||||||
"""发送app消息"""
|
"""发送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):
|
def send_emoji_message(self, to_wxid, emoji_md5, emoji_size):
|
||||||
"""发送emoji消息"""
|
"""发送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):
|
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):
|
def get_profile(self):
|
||||||
"""获取用户信息"""
|
"""获取用户信息"""
|
||||||
return self._user_api.get_profile()
|
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):
|
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):
|
def cdn_download(self, aeskey, file_type, file_url):
|
||||||
"""cdn下载"""
|
"""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)
|
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)
|
return await self._download_api.download_url_to_base64(download_url=url)
|
||||||
|
|
||||||
def get_chatroom_member_detail(self, chatroom_name):
|
def get_chatroom_member_detail(self, chatroom_name):
|
||||||
"""查看群成员详情"""
|
"""查看群成员详情"""
|
||||||
return self._chatroom_api.get_chatroom_member_detail(chatroom_name)
|
return self._chatroom_api.get_chatroom_member_detail(chatroom_name)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import qrcode
|
import qrcode
|
||||||
|
|
||||||
|
|
||||||
def print_green(text):
|
def print_green(text):
|
||||||
print(f"\033[32m{text}\033[0m")
|
print(f'\033[32m{text}\033[0m')
|
||||||
|
|
||||||
|
|
||||||
def print_yellow(text):
|
def print_yellow(text):
|
||||||
print(f"\033[33m{text}\033[0m")
|
print(f'\033[33m{text}\033[0m')
|
||||||
|
|
||||||
|
|
||||||
def print_red(text):
|
def print_red(text):
|
||||||
print(f"\033[31m{text}\033[0m")
|
print(f'\033[31m{text}\033[0m')
|
||||||
|
|
||||||
|
|
||||||
def make_and_print_qr(url):
|
def make_and_print_qr(url):
|
||||||
"""生成并打印二维码
|
"""生成并打印二维码
|
||||||
@@ -22,10 +26,9 @@ def make_and_print_qr(url):
|
|||||||
1. 在终端打印二维码的ASCII图形
|
1. 在终端打印二维码的ASCII图形
|
||||||
2. 同时提供在线二维码生成链接作为备选
|
2. 同时提供在线二维码生成链接作为备选
|
||||||
"""
|
"""
|
||||||
print_green("请扫描下方二维码登录")
|
print_green('请扫描下方二维码登录')
|
||||||
qr = qrcode.QRCode()
|
qr = qrcode.QRCode()
|
||||||
qr.add_data(url)
|
qr.add_data(url)
|
||||||
qr.make()
|
qr.make()
|
||||||
qr.print_ascii(invert=True)
|
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:
|
if 'access_token' in data:
|
||||||
return data['access_token']
|
return data['access_token']
|
||||||
else:
|
else:
|
||||||
await self.logger.error(f"获取accesstoken失败:{response.json()}")
|
await self.logger.error(f'获取accesstoken失败:{response.json()}')
|
||||||
raise Exception(f'未获取access token: {data}')
|
raise Exception(f'未获取access token: {data}')
|
||||||
|
|
||||||
async def get_users(self):
|
async def get_users(self):
|
||||||
@@ -129,7 +129,7 @@ class WecomClient:
|
|||||||
response = await client.post(url, json=params)
|
response = await client.post(url, json=params)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.logger.error(f"发送图片失败:{data}")
|
await self.logger.error(f'发送图片失败:{data}')
|
||||||
raise Exception('Failed to send image: ' + str(e))
|
raise Exception('Failed to send image: ' + str(e))
|
||||||
|
|
||||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||||
@@ -164,7 +164,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_private_msg(user_id, agent_id, content)
|
return await self.send_private_msg(user_id, agent_id, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
await self.logger.error(f"发送消息失败:{data}")
|
await self.logger.error(f'发送消息失败:{data}')
|
||||||
raise Exception('Failed to send message: ' + str(data))
|
raise Exception('Failed to send message: ' + str(data))
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
async def handle_callback_request(self):
|
||||||
@@ -181,7 +181,7 @@ class WecomClient:
|
|||||||
echostr = request.args.get('echostr')
|
echostr = request.args.get('echostr')
|
||||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
await self.logger.error("验证失败")
|
await self.logger.error('验证失败')
|
||||||
raise Exception(f'验证失败,错误码: {ret}')
|
raise Exception(f'验证失败,错误码: {ret}')
|
||||||
return reply_echo_str
|
return reply_echo_str
|
||||||
|
|
||||||
@@ -189,10 +189,9 @@ class WecomClient:
|
|||||||
encrypt_msg = await request.data
|
encrypt_msg = await request.data
|
||||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
await self.logger.error("消息解密失败")
|
await self.logger.error('消息解密失败')
|
||||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||||
|
|
||||||
|
|
||||||
# 解析消息并处理
|
# 解析消息并处理
|
||||||
message_data = await self.get_message(xml_msg)
|
message_data = await self.get_message(xml_msg)
|
||||||
if message_data:
|
if message_data:
|
||||||
@@ -202,7 +201,7 @@ class WecomClient:
|
|||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
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
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||||
@@ -301,7 +300,7 @@ class WecomClient:
|
|||||||
except binascii.Error as e:
|
except binascii.Error as e:
|
||||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||||
else:
|
else:
|
||||||
await self.logger.error("Image对象出错")
|
await self.logger.error('Image对象出错')
|
||||||
raise ValueError('image对象出错')
|
raise ValueError('image对象出错')
|
||||||
|
|
||||||
# 设置 multipart/form-data 格式的文件
|
# 设置 multipart/form-data 格式的文件
|
||||||
@@ -325,7 +324,7 @@ class WecomClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
media_id = await self.upload_to_work(image)
|
media_id = await self.upload_to_work(image)
|
||||||
if data.get('errcode', 0) != 0:
|
if data.get('errcode', 0) != 0:
|
||||||
await self.logger.error(f"上传图片失败:{data}")
|
await self.logger.error(f'上传图片失败:{data}')
|
||||||
raise Exception('failed to upload file')
|
raise Exception('failed to upload file')
|
||||||
|
|
||||||
media_id = data.get('media_id')
|
media_id = data.get('media_id')
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class WecomCSClient:
|
|||||||
self.access_token = await self.get_access_token(self.secret)
|
self.access_token = await self.get_access_token(self.secret)
|
||||||
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
return await self.send_text_msg(open_kfid, external_userid, msgid, content)
|
||||||
if data['errcode'] != 0:
|
if data['errcode'] != 0:
|
||||||
await self.logger.error(f"发送消息失败:{data}")
|
await self.logger.error(f'发送消息失败:{data}')
|
||||||
raise Exception('Failed to send message')
|
raise Exception('Failed to send message')
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class WecomCSClient:
|
|||||||
return 'success'
|
return 'success'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.logger:
|
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:
|
else:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return f'Error processing request: {str(e)}', 400
|
return f'Error processing request: {str(e)}', 400
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
if quart.request.method == 'GET':
|
if quart.request.method == 'GET':
|
||||||
sort_by = quart.request.args.get('sort_by', 'created_at')
|
sort_by = quart.request.args.get('sort_by', 'created_at')
|
||||||
sort_order = quart.request.args.get('sort_order', 'DESC')
|
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':
|
elif quart.request.method == 'POST':
|
||||||
json_data = await quart.request.json
|
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)
|
await self.ap.user_service.reset_password(user_email, new_password)
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
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)
|
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||||
if runtime_kb is None:
|
if runtime_kb is None:
|
||||||
raise Exception('Knowledge base not found')
|
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]:
|
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
||||||
"""获取知识库文件"""
|
"""获取知识库文件"""
|
||||||
|
|||||||
@@ -82,3 +82,18 @@ class UserService:
|
|||||||
await self.ap.persistence_mgr.execute_async(
|
await self.ap.persistence_mgr.execute_async(
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ from ..types import message as platform_message
|
|||||||
from ..types import events as platform_events
|
from ..types import events as platform_events
|
||||||
from ..types import entities as platform_entities
|
from ..types import entities as platform_entities
|
||||||
|
|
||||||
|
|
||||||
# 语音功能相关异常定义
|
# 语音功能相关异常定义
|
||||||
class VoiceConnectionError(Exception):
|
class VoiceConnectionError(Exception):
|
||||||
"""语音连接基础异常"""
|
"""语音连接基础异常"""
|
||||||
|
|
||||||
def __init__(self, message: str, error_code: str = None, guild_id: int = None):
|
def __init__(self, message: str, error_code: str = None, guild_id: int = None):
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
@@ -32,8 +34,9 @@ class VoiceConnectionError(Exception):
|
|||||||
|
|
||||||
class VoicePermissionError(VoiceConnectionError):
|
class VoicePermissionError(VoiceConnectionError):
|
||||||
"""语音权限异常"""
|
"""语音权限异常"""
|
||||||
|
|
||||||
def __init__(self, message: str, missing_permissions: list = None, user_id: int = None, channel_id: int = None):
|
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.missing_permissions = missing_permissions or []
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.channel_id = channel_id
|
self.channel_id = channel_id
|
||||||
@@ -41,20 +44,22 @@ class VoicePermissionError(VoiceConnectionError):
|
|||||||
|
|
||||||
class VoiceNetworkError(VoiceConnectionError):
|
class VoiceNetworkError(VoiceConnectionError):
|
||||||
"""语音网络异常"""
|
"""语音网络异常"""
|
||||||
|
|
||||||
def __init__(self, message: str, retry_count: int = 0):
|
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.retry_count = retry_count
|
||||||
self.last_attempt = datetime.datetime.now()
|
self.last_attempt = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class VoiceConnectionStatus(Enum):
|
class VoiceConnectionStatus(Enum):
|
||||||
"""语音连接状态枚举"""
|
"""语音连接状态枚举"""
|
||||||
IDLE = "idle"
|
|
||||||
CONNECTING = "connecting"
|
IDLE = 'idle'
|
||||||
CONNECTED = "connected"
|
CONNECTING = 'connecting'
|
||||||
PLAYING = "playing"
|
CONNECTED = 'connected'
|
||||||
RECONNECTING = "reconnecting"
|
PLAYING = 'playing'
|
||||||
FAILED = "failed"
|
RECONNECTING = 'reconnecting'
|
||||||
|
FAILED = 'failed'
|
||||||
|
|
||||||
|
|
||||||
class VoiceConnectionInfo:
|
class VoiceConnectionInfo:
|
||||||
@@ -82,14 +87,14 @@ class VoiceConnectionInfo:
|
|||||||
"""
|
"""
|
||||||
self.guild_id = guild_id
|
self.guild_id = guild_id
|
||||||
self.channel_id = channel_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.connected = False
|
||||||
self.connection_time: datetime.datetime = None
|
self.connection_time: datetime.datetime = None
|
||||||
self.last_activity = datetime.datetime.now()
|
self.last_activity = datetime.datetime.now()
|
||||||
self.status = VoiceConnectionStatus.IDLE
|
self.status = VoiceConnectionStatus.IDLE
|
||||||
self.user_count = 0
|
self.user_count = 0
|
||||||
self.latency = 0.0
|
self.latency = 0.0
|
||||||
self.connection_health = "unknown"
|
self.connection_health = 'unknown'
|
||||||
self.voice_client = None
|
self.voice_client = None
|
||||||
|
|
||||||
def update_status(self, status: VoiceConnectionStatus):
|
def update_status(self, status: VoiceConnectionStatus):
|
||||||
@@ -123,16 +128,16 @@ class VoiceConnectionInfo:
|
|||||||
dict: 连接信息的字典表示
|
dict: 连接信息的字典表示
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"guild_id": self.guild_id,
|
'guild_id': self.guild_id,
|
||||||
"channel_id": self.channel_id,
|
'channel_id': self.channel_id,
|
||||||
"channel_name": self.channel_name,
|
'channel_name': self.channel_name,
|
||||||
"connected": self.connected,
|
'connected': self.connected,
|
||||||
"connection_time": self.connection_time.isoformat() if self.connection_time else None,
|
'connection_time': self.connection_time.isoformat() if self.connection_time else None,
|
||||||
"last_activity": self.last_activity.isoformat(),
|
'last_activity': self.last_activity.isoformat(),
|
||||||
"status": self.status.value,
|
'status': self.status.value,
|
||||||
"user_count": self.user_count,
|
'user_count': self.user_count,
|
||||||
"latency": self.latency,
|
'latency': self.latency,
|
||||||
"connection_health": self.connection_health
|
'connection_health': self.connection_health,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,8 +170,7 @@ class VoiceConnectionManager:
|
|||||||
self._cleanup_task = None
|
self._cleanup_task = None
|
||||||
self._monitoring_enabled = True
|
self._monitoring_enabled = True
|
||||||
|
|
||||||
async def join_voice_channel(self, guild_id: int, channel_id: int,
|
async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient:
|
||||||
user_id: int = None) -> discord.VoiceClient:
|
|
||||||
"""
|
"""
|
||||||
加入语音频道
|
加入语音频道
|
||||||
|
|
||||||
@@ -193,19 +197,11 @@ class VoiceConnectionManager:
|
|||||||
# 获取服务器和频道对象
|
# 获取服务器和频道对象
|
||||||
guild = self.bot.get_guild(guild_id)
|
guild = self.bot.get_guild(guild_id)
|
||||||
if not guild:
|
if not guild:
|
||||||
raise VoiceConnectionError(
|
raise VoiceConnectionError(f'无法找到服务器 {guild_id}', 'GUILD_NOT_FOUND', guild_id)
|
||||||
f"无法找到服务器 {guild_id}",
|
|
||||||
"GUILD_NOT_FOUND",
|
|
||||||
guild_id
|
|
||||||
)
|
|
||||||
|
|
||||||
channel = guild.get_channel(channel_id)
|
channel = guild.get_channel(channel_id)
|
||||||
if not channel or not isinstance(channel, discord.VoiceChannel):
|
if not channel or not isinstance(channel, discord.VoiceChannel):
|
||||||
raise VoiceConnectionError(
|
raise VoiceConnectionError(f'无法找到语音频道 {channel_id}', 'CHANNEL_NOT_FOUND', guild_id)
|
||||||
f"无法找到语音频道 {channel_id}",
|
|
||||||
"CHANNEL_NOT_FOUND",
|
|
||||||
guild_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证用户是否在语音频道中(如果提供了用户ID)
|
# 验证用户是否在语音频道中(如果提供了用户ID)
|
||||||
if user_id:
|
if user_id:
|
||||||
@@ -220,7 +216,7 @@ class VoiceConnectionManager:
|
|||||||
if existing_conn.connected and existing_conn.voice_client:
|
if existing_conn.connected and existing_conn.voice_client:
|
||||||
if existing_conn.channel_id == channel_id:
|
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
|
return existing_conn.voice_client
|
||||||
else:
|
else:
|
||||||
# 连接到不同频道,先断开旧连接
|
# 连接到不同频道,先断开旧连接
|
||||||
@@ -236,16 +232,16 @@ class VoiceConnectionManager:
|
|||||||
conn_info.user_count = len(channel.members)
|
conn_info.user_count = len(channel.members)
|
||||||
self.connections[guild_id] = conn_info
|
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
|
return voice_client
|
||||||
|
|
||||||
except discord.ClientException as e:
|
except discord.ClientException as e:
|
||||||
raise VoiceNetworkError(f"Discord 客户端错误: {str(e)}")
|
raise VoiceNetworkError(f'Discord 客户端错误: {str(e)}')
|
||||||
except discord.opus.OpusNotLoaded as 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:
|
except Exception as e:
|
||||||
await self.logger.error(f"连接语音频道时发生未知错误: {str(e)}")
|
await self.logger.error(f'连接语音频道时发生未知错误: {str(e)}')
|
||||||
raise VoiceConnectionError(f"连接失败: {str(e)}", "UNKNOWN_ERROR", guild_id)
|
raise VoiceConnectionError(f'连接失败: {str(e)}', 'UNKNOWN_ERROR', guild_id)
|
||||||
|
|
||||||
async def leave_voice_channel(self, guild_id: int) -> bool:
|
async def leave_voice_channel(self, guild_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -297,11 +293,11 @@ class VoiceConnectionManager:
|
|||||||
conn_info.update_status(VoiceConnectionStatus.IDLE)
|
conn_info.update_status(VoiceConnectionStatus.IDLE)
|
||||||
del self.connections[guild_id]
|
del self.connections[guild_id]
|
||||||
|
|
||||||
await self.logger.info(f"已断开语音连接: Guild {guild_id}")
|
await self.logger.info(f'已断开语音连接: Guild {guild_id}')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.logger.error(f"断开语音连接时发生错误: {str(e)}")
|
await self.logger.error(f'断开语音连接时发生错误: {str(e)}')
|
||||||
# 即使出错也要清理连接记录
|
# 即使出错也要清理连接记录
|
||||||
conn_info.update_status(VoiceConnectionStatus.FAILED)
|
conn_info.update_status(VoiceConnectionStatus.FAILED)
|
||||||
if guild_id in self.connections:
|
if guild_id in self.connections:
|
||||||
@@ -381,7 +377,7 @@ class VoiceConnectionManager:
|
|||||||
# 更新实时信息
|
# 更新实时信息
|
||||||
if conn_info.voice_client and conn_info.voice_client.is_connected():
|
if conn_info.voice_client and conn_info.voice_client.is_connected():
|
||||||
conn_info.latency = conn_info.voice_client.latency * 1000 # 转换为毫秒
|
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)
|
guild = self.bot.get_guild(guild_id)
|
||||||
@@ -435,36 +431,32 @@ class VoiceConnectionManager:
|
|||||||
# 获取用户信息
|
# 获取用户信息
|
||||||
users = []
|
users = []
|
||||||
for member in channel.members:
|
for member in channel.members:
|
||||||
users.append({
|
users.append(
|
||||||
"id": member.id,
|
{'id': member.id, 'name': member.display_name, 'status': str(member.status), 'is_bot': member.bot}
|
||||||
"name": member.display_name,
|
)
|
||||||
"status": str(member.status),
|
|
||||||
"is_bot": member.bot
|
|
||||||
})
|
|
||||||
|
|
||||||
# 获取权限信息
|
# 获取权限信息
|
||||||
bot_member = guild.me
|
bot_member = guild.me
|
||||||
permissions = channel.permissions_for(bot_member)
|
permissions = channel.permissions_for(bot_member)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"channel_id": channel_id,
|
'channel_id': channel_id,
|
||||||
"channel_name": channel.name,
|
'channel_name': channel.name,
|
||||||
"guild_id": guild_id,
|
'guild_id': guild_id,
|
||||||
"guild_name": guild.name,
|
'guild_name': guild.name,
|
||||||
"user_limit": channel.user_limit,
|
'user_limit': channel.user_limit,
|
||||||
"current_users": users,
|
'current_users': users,
|
||||||
"user_count": len(users),
|
'user_count': len(users),
|
||||||
"bitrate": channel.bitrate,
|
'bitrate': channel.bitrate,
|
||||||
"permissions": {
|
'permissions': {
|
||||||
"connect": permissions.connect,
|
'connect': permissions.connect,
|
||||||
"speak": permissions.speak,
|
'speak': permissions.speak,
|
||||||
"use_voice_activation": permissions.use_voice_activation,
|
'use_voice_activation': permissions.use_voice_activation,
|
||||||
"priority_speaker": permissions.priority_speaker
|
'priority_speaker': permissions.priority_speaker,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _validate_user_in_channel(self, guild: discord.Guild,
|
async def _validate_user_in_channel(self, guild: discord.Guild, channel: discord.VoiceChannel, user_id: int):
|
||||||
channel: discord.VoiceChannel, user_id: int):
|
|
||||||
"""
|
"""
|
||||||
验证用户是否在语音频道中
|
验证用户是否在语音频道中
|
||||||
|
|
||||||
@@ -480,19 +472,14 @@ class VoiceConnectionManager:
|
|||||||
"""
|
"""
|
||||||
member = guild.get_member(user_id)
|
member = guild.get_member(user_id)
|
||||||
if not member:
|
if not member:
|
||||||
raise VoicePermissionError(
|
raise VoicePermissionError(f'无法找到用户 {user_id}', ['member_not_found'], user_id, channel.id)
|
||||||
f"无法找到用户 {user_id}",
|
|
||||||
["member_not_found"],
|
|
||||||
user_id,
|
|
||||||
channel.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not member.voice or member.voice.channel != channel:
|
if not member.voice or member.voice.channel != channel:
|
||||||
raise VoicePermissionError(
|
raise VoicePermissionError(
|
||||||
f"用户 {member.display_name} 不在语音频道 {channel.name} 中",
|
f'用户 {member.display_name} 不在语音频道 {channel.name} 中',
|
||||||
["user_not_in_channel"],
|
['user_not_in_channel'],
|
||||||
user_id,
|
user_id,
|
||||||
channel.id
|
channel.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _validate_bot_permissions(self, channel: discord.VoiceChannel):
|
async def _validate_bot_permissions(self, channel: discord.VoiceChannel):
|
||||||
@@ -513,15 +500,15 @@ class VoiceConnectionManager:
|
|||||||
missing_permissions = []
|
missing_permissions = []
|
||||||
|
|
||||||
if not permissions.connect:
|
if not permissions.connect:
|
||||||
missing_permissions.append("connect")
|
missing_permissions.append('connect')
|
||||||
if not permissions.speak:
|
if not permissions.speak:
|
||||||
missing_permissions.append("speak")
|
missing_permissions.append('speak')
|
||||||
|
|
||||||
if missing_permissions:
|
if missing_permissions:
|
||||||
raise VoicePermissionError(
|
raise VoicePermissionError(
|
||||||
f"机器人在频道 {channel.name} 中缺少权限: {', '.join(missing_permissions)}",
|
f'机器人在频道 {channel.name} 中缺少权限: {", ".join(missing_permissions)}',
|
||||||
missing_permissions,
|
missing_permissions,
|
||||||
channel_id=channel.id
|
channel_id=channel.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def cleanup_inactive_connections(self):
|
async def cleanup_inactive_connections(self):
|
||||||
@@ -542,7 +529,7 @@ class VoiceConnectionManager:
|
|||||||
await self._disconnect_internal(guild_id)
|
await self._disconnect_internal(guild_id)
|
||||||
|
|
||||||
if cleanup_guilds:
|
if cleanup_guilds:
|
||||||
await self.logger.info(f"清理了 {len(cleanup_guilds)} 个无效的语音连接")
|
await self.logger.info(f'清理了 {len(cleanup_guilds)} 个无效的语音连接')
|
||||||
|
|
||||||
async def start_monitoring(self):
|
async def start_monitoring(self):
|
||||||
"""
|
"""
|
||||||
@@ -839,8 +826,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.bot = MyClient(intents=intents, **args)
|
self.bot = MyClient(intents=intents, **args)
|
||||||
|
|
||||||
# Voice functionality methods
|
# Voice functionality methods
|
||||||
async def join_voice_channel(self, guild_id: int, channel_id: int,
|
async def join_voice_channel(self, guild_id: int, channel_id: int, user_id: int = None) -> discord.VoiceClient:
|
||||||
user_id: int = None) -> discord.VoiceClient:
|
|
||||||
"""
|
"""
|
||||||
加入语音频道
|
加入语音频道
|
||||||
|
|
||||||
@@ -864,7 +850,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
VoiceConnectionError: 其他连接错误
|
VoiceConnectionError: 其他连接错误
|
||||||
"""
|
"""
|
||||||
if not self.voice_manager:
|
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)
|
return await self.voice_manager.join_voice_channel(guild_id, channel_id, user_id)
|
||||||
|
|
||||||
@@ -1078,7 +1064,7 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
self.voice_manager = VoiceConnectionManager(self.bot, self.logger)
|
self.voice_manager = VoiceConnectionManager(self.bot, self.logger)
|
||||||
await self.voice_manager.start_monitoring()
|
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)
|
await self.bot.start(self.config['token'], reconnect=True)
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
async def kill(self) -> bool:
|
||||||
@@ -1094,4 +1080,3 @@ class DiscordAdapter(adapter.MessagePlatformAdapter):
|
|||||||
|
|
||||||
await self.bot.close()
|
await self.bot.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
class WeChatPadMessageConverter(adapter.MessageConverter):
|
class WeChatPadMessageConverter(adapter.MessageConverter):
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: logging.Logger):
|
def __init__(self, config: dict, logger: logging.Logger):
|
||||||
self.config = config
|
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
|
self.logger = logger
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -41,9 +40,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
|||||||
|
|
||||||
for component in message_chain:
|
for component in message_chain:
|
||||||
if isinstance(component, platform_message.AtAll):
|
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):
|
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):
|
elif isinstance(component, platform_message.Plain):
|
||||||
content_list.append({'type': 'text', 'content': component.text})
|
content_list.append({'type': 'text', 'content': component.text})
|
||||||
elif isinstance(component, platform_message.Image):
|
elif isinstance(component, platform_message.Image):
|
||||||
@@ -77,9 +76,9 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
|||||||
return content_list
|
return content_list
|
||||||
|
|
||||||
async def target2yiri(
|
async def target2yiri(
|
||||||
self,
|
self,
|
||||||
message: dict,
|
message: dict,
|
||||||
bot_account_id: str,
|
bot_account_id: str,
|
||||||
) -> platform_message.MessageChain:
|
) -> platform_message.MessageChain:
|
||||||
"""外部消息转平台消息"""
|
"""外部消息转平台消息"""
|
||||||
# 数据预处理
|
# 数据预处理
|
||||||
@@ -92,8 +91,8 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
|||||||
if is_group_message:
|
if is_group_message:
|
||||||
ats_bot = self._ats_bot(message, bot_account_id)
|
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}")
|
self.logger.info(f'ats_bot: {ats_bot}; bot_account_id: {bot_account_id}; bot_wxid: {bot_wxid}')
|
||||||
if "@所有人" in content:
|
if '@所有人' in content:
|
||||||
message_list.append(platform_message.AtAll())
|
message_list.append(platform_message.AtAll())
|
||||||
if ats_bot:
|
if ats_bot:
|
||||||
message_list.append(platform_message.At(target=bot_account_id))
|
message_list.append(platform_message.At(target=bot_account_id))
|
||||||
@@ -418,12 +417,12 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
|||||||
msg_source = message.get('msg_source', '') or ''
|
msg_source = message.get('msg_source', '') or ''
|
||||||
if len(msg_source) > 0:
|
if len(msg_source) > 0:
|
||||||
msg_source_data = ET.fromstring(msg_source)
|
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:
|
if at_user_list:
|
||||||
# atuserlist格式通常是逗号分隔的用户ID列表
|
# atuserlist格式通常是逗号分隔的用户ID列表
|
||||||
at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()]
|
at_targets = [user_id.strip() for user_id in at_user_list.split(',') if user_id.strip()]
|
||||||
except Exception as e:
|
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
|
return at_targets
|
||||||
|
|
||||||
# 提取一下content前面的sender_id, 和去掉前缀的内容
|
# 提取一下content前面的sender_id, 和去掉前缀的内容
|
||||||
@@ -449,7 +448,6 @@ class WeChatPadMessageConverter(adapter.MessageConverter):
|
|||||||
|
|
||||||
|
|
||||||
class WeChatPadEventConverter(adapter.EventConverter):
|
class WeChatPadEventConverter(adapter.EventConverter):
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: logging.Logger):
|
def __init__(self, config: dict, logger: logging.Logger):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.message_converter = WeChatPadMessageConverter(config, logger)
|
self.message_converter = WeChatPadMessageConverter(config, logger)
|
||||||
@@ -460,11 +458,10 @@ class WeChatPadEventConverter(adapter.EventConverter):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def target2yiri(
|
async def target2yiri(
|
||||||
self,
|
self,
|
||||||
event: dict,
|
event: dict,
|
||||||
bot_account_id: str,
|
bot_account_id: str,
|
||||||
) -> platform_events.MessageEvent:
|
) -> platform_events.MessageEvent:
|
||||||
|
|
||||||
# 排除公众号以及微信团队消息
|
# 排除公众号以及微信团队消息
|
||||||
if (
|
if (
|
||||||
event['from_user_name']['str'].startswith('gh_')
|
event['from_user_name']['str'].startswith('gh_')
|
||||||
@@ -576,26 +573,22 @@ class WeChatPadAdapter(adapter.MessagePlatformAdapter):
|
|||||||
for msg in content_list:
|
for msg in content_list:
|
||||||
# 文本消息处理@
|
# 文本消息处理@
|
||||||
if msg['type'] == 'text' and at_targets:
|
if msg['type'] == 'text' and at_targets:
|
||||||
if "all" in at_targets:
|
if 'all' in at_targets:
|
||||||
msg['content'] = f'@所有人 {msg["content"]}'
|
msg['content'] = f'@所有人 {msg["content"]}'
|
||||||
else:
|
else:
|
||||||
at_nick_name_list = []
|
at_nick_name_list = []
|
||||||
for member in member_info:
|
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"]}')
|
at_nick_name_list.append(f'@{member["nick_name"]}')
|
||||||
msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}'
|
msg['content'] = f'{" ".join(at_nick_name_list)} {msg["content"]}'
|
||||||
|
|
||||||
# 统一消息派发
|
# 统一消息派发
|
||||||
handler_map = {
|
handler_map = {
|
||||||
'text': lambda msg: self.bot.send_text_message(
|
'text': lambda msg: self.bot.send_text_message(
|
||||||
to_wxid=target_id,
|
to_wxid=target_id, message=msg['content'], ats=['notify@all'] if 'all' in at_targets else at_targets
|
||||||
message=msg['content'],
|
|
||||||
ats= ["notify@all"] if "all" in at_targets else at_targets
|
|
||||||
),
|
),
|
||||||
'image': lambda msg: self.bot.send_image_message(
|
'image': lambda msg: self.bot.send_image_message(
|
||||||
to_wxid=target_id,
|
to_wxid=target_id, img_url=msg['image'], ats=['notify@all'] if 'all' in at_targets else at_targets
|
||||||
img_url=msg["image"],
|
|
||||||
ats = ["notify@all"] if "all" in at_targets else at_targets
|
|
||||||
),
|
),
|
||||||
'WeChatEmoji': lambda msg: self.bot.send_emoji_message(
|
'WeChatEmoji': lambda msg: self.bot.send_emoji_message(
|
||||||
to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']
|
to_wxid=target_id, emoji_md5=msg['emoji_md5'], emoji_size=msg['emoji_size']
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ metadata:
|
|||||||
name: deepseek-chat-completions
|
name: deepseek-chat-completions
|
||||||
label:
|
label:
|
||||||
en_US: DeepSeek
|
en_US: DeepSeek
|
||||||
zh_Hans: 深度求索
|
zh_Hans: DeepSeek
|
||||||
icon: deepseek.svg
|
icon: deepseek.svg
|
||||||
spec:
|
spec:
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class LocalAgentRunner(runner.RequestRunner):
|
|||||||
self.ap.logger.warning(f'Knowledge base {kb_uuid} not found')
|
self.ap.logger.warning(f'Knowledge base {kb_uuid} not found')
|
||||||
raise ValueError(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 = ''
|
final_user_message_text = ''
|
||||||
|
|
||||||
|
|||||||
@@ -123,11 +123,11 @@ class RuntimeKnowledgeBase:
|
|||||||
)
|
)
|
||||||
return wrapper.id
|
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(
|
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(
|
||||||
self.knowledge_base_entity.embedding_model_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):
|
async def delete_file(self, file_id: str):
|
||||||
# delete vector
|
# delete vector
|
||||||
|
|||||||
Generated
+53
@@ -10,8 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.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-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@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": {
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
|
"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",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.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-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@radix-ui/react-hover-card": "^1.1.13",
|
||||||
|
|||||||
+24
-15
@@ -56,6 +56,15 @@
|
|||||||
background: rgba(0, 0, 0, 0.35); /* 悬停加深 */
|
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 */
|
/* 兼容 Edge */
|
||||||
@supports (-ms-ime-align: auto) {
|
@supports (-ms-ime-align: auto) {
|
||||||
body {
|
body {
|
||||||
@@ -108,36 +117,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.08 0.002 285.823);
|
||||||
--foreground: oklch(0.985 0 0);
|
--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);
|
--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);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.92 0.004 286.32);
|
--primary: oklch(0.62 0.2 255);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(1 0 0);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.18 0.004 286.033);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--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);
|
--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);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 8%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 10%);
|
||||||
--ring: oklch(0.552 0.016 285.938);
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--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-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.62 0.2 255);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
--sidebar-accent: oklch(0.18 0.004 286.033);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--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);
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export default function BotDetailDialog({
|
|||||||
<SidebarProvider className="items-start w-full flex">
|
<SidebarProvider className="items-start w-full flex">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="none"
|
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>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
|
|||||||
@@ -6,12 +6,22 @@
|
|||||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.iconBasicInfoContainer {
|
.iconBasicInfoContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -47,6 +57,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoName {
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoDescription {
|
.basicInfoDescription {
|
||||||
@@ -58,6 +73,10 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoDescription {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoAdapterContainer {
|
.basicInfoAdapterContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -71,12 +90,20 @@
|
|||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoAdapterIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoAdapterLabel {
|
.basicInfoAdapterLabel {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoAdapterLabel {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoPipelineContainer {
|
.basicInfoPipelineContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -90,12 +117,20 @@
|
|||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoPipelineIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoPipelineLabel {
|
.basicInfoPipelineLabel {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoPipelineLabel {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.bigText {
|
.bigText {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ export default function BotForm({
|
|||||||
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
<FormLabel>{t('bots.bindPipeline')}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select onValueChange={field.onChange} {...field}>
|
<Select onValueChange={field.onChange} {...field}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t('bots.selectPipeline')}
|
placeholder={t('bots.selectPipeline')}
|
||||||
/>
|
/>
|
||||||
@@ -467,7 +467,7 @@ export default function BotForm({
|
|||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('bots.selectAdapter')} />
|
<SelectValue placeholder={t('bots.selectAdapter')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="fixed z-[1000]">
|
<SelectContent className="fixed z-[1000]">
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .botLogCardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
}
|
||||||
|
|
||||||
.listHeader {
|
.listHeader {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from '@/components/ui/hover-card';
|
} from '@/components/ui/hover-card';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { i18nObj } from '@/i18n/I18nProvider';
|
import { i18nObj } from '@/i18n/I18nProvider';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
||||||
export default function DynamicFormItemComponent({
|
export default function DynamicFormItemComponent({
|
||||||
config,
|
config,
|
||||||
@@ -132,7 +133,7 @@ export default function DynamicFormItemComponent({
|
|||||||
case DynamicFormItemType.SELECT:
|
case DynamicFormItemType.SELECT:
|
||||||
return (
|
return (
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('common.select')} />
|
<SelectValue placeholder={t('common.select')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -150,7 +151,7 @@ export default function DynamicFormItemComponent({
|
|||||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||||
return (
|
return (
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('models.selectModel')} />
|
<SelectValue placeholder={t('models.selectModel')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -267,7 +268,7 @@ export default function DynamicFormItemComponent({
|
|||||||
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
||||||
return (
|
return (
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('knowledge.selectKnowledgeBase')} />
|
<SelectValue placeholder={t('knowledge.selectKnowledgeBase')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -291,7 +292,7 @@ export default function DynamicFormItemComponent({
|
|||||||
<div key={index} className="flex gap-2 items-center">
|
<div key={index} className="flex gap-2 items-center">
|
||||||
{/* 角色选择 */}
|
{/* 角色选择 */}
|
||||||
{index === 0 ? (
|
{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
|
system
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -303,7 +304,7 @@ export default function DynamicFormItemComponent({
|
|||||||
field.onChange(newValue);
|
field.onChange(newValue);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[120px]">
|
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -315,7 +316,7 @@ export default function DynamicFormItemComponent({
|
|||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
{/* 内容输入 */}
|
{/* 内容输入 */}
|
||||||
<Input
|
<Textarea
|
||||||
className="w-[300px]"
|
className="w-[300px]"
|
||||||
value={item.content}
|
value={item.content}
|
||||||
onChange={(e) => {
|
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); */
|
/* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebarContainer {
|
||||||
|
background-color: #0a0a0b !important;
|
||||||
|
}
|
||||||
|
|
||||||
.langbotIconContainer {
|
.langbotIconContainer {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
@@ -21,32 +25,49 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.langbotIcon {
|
.langbotIcon {
|
||||||
width: 2.8rem;
|
width: 2.8rem;
|
||||||
height: 2.8rem;
|
height: 2.8rem;
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.langbotTextContainer {
|
:global(.dark) .langbotIcon {
|
||||||
display: flex;
|
box-shadow: 0 0 10px 0 rgba(255, 255, 255, 0.1);
|
||||||
flex-direction: column;
|
}
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.langbotText {
|
.langbotTextContainer {
|
||||||
font-size: 1.4rem;
|
display: flex;
|
||||||
font-weight: 500;
|
flex-direction: column;
|
||||||
}
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.langbotVersion {
|
.langbotText {
|
||||||
font-size: 0.8rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 700;
|
font-weight: 500;
|
||||||
color: #6c6c6c;
|
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 {
|
.sidebarTopContainer {
|
||||||
@@ -76,6 +97,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
/* background-color: aqua; */
|
/* background-color: aqua; */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,16 +107,40 @@
|
|||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
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 {
|
.sidebarUnselected {
|
||||||
color: #6c6c6c;
|
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 {
|
.sidebarChildIcon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: rgba(96, 149, 209, 0);
|
background-color: rgba(96, 149, 209, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebarChildName {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebarBottomContainer {
|
.sidebarBottomContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConf
|
|||||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
import { systemInfo } from '@/app/infra/http/HttpClient';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 侧边导航栏要加动画
|
// TODO 侧边导航栏要加动画
|
||||||
export default function HomeSidebar({
|
export default function HomeSidebar({
|
||||||
@@ -27,8 +39,11 @@ export default function HomeSidebar({
|
|||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
|
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
|
const [passwordChangeOpen, setPasswordChangeOpen] = useState(false);
|
||||||
|
const [languageSelectorOpen, setLanguageSelectorOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initSelect();
|
initSelect();
|
||||||
@@ -168,23 +183,113 @@ export default function HomeSidebar({
|
|||||||
}
|
}
|
||||||
name={t('common.helpDocs')}
|
name={t('common.helpDocs')}
|
||||||
/>
|
/>
|
||||||
<SidebarChild
|
|
||||||
onClick={() => {
|
<Popover
|
||||||
handleLogout();
|
open={popoverOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
// 防止语言选择器打开时关闭popover
|
||||||
|
if (!open && languageSelectorOpen) return;
|
||||||
|
setPopoverOpen(open);
|
||||||
}}
|
}}
|
||||||
isSelected={false}
|
>
|
||||||
icon={
|
<PopoverTrigger>
|
||||||
<svg
|
<SidebarChild
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
onClick={() => {}}
|
||||||
viewBox="0 0 24 24"
|
isSelected={false}
|
||||||
fill="currentColor"
|
icon={
|
||||||
>
|
<svg
|
||||||
<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>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
viewBox="0 0 24 24"
|
||||||
}
|
fill="currentColor"
|
||||||
name={t('common.logout')}
|
>
|
||||||
/>
|
<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>
|
</div>
|
||||||
|
<PasswordChangeDialog
|
||||||
|
open={passwordChangeOpen}
|
||||||
|
onOpenChange={setPasswordChangeOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
color: #585858;
|
color: #585858;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .titleText {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
.subtitleText {
|
.subtitleText {
|
||||||
margin-left: 3.2rem;
|
margin-left: 3.2rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -25,8 +29,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .subtitleText {
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
.helpLink {
|
.helpLink {
|
||||||
margin-left: 0.2rem;
|
margin-left: 0.2rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #8b8b8b;
|
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">
|
<SidebarProvider className="items-start w-full flex">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="none"
|
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>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
|
|||||||
@@ -10,12 +10,22 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoContainer {
|
.basicInfoContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -35,6 +45,11 @@
|
|||||||
.basicInfoNameText {
|
.basicInfoNameText {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoNameText {
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoDescriptionText {
|
.basicInfoDescriptionText {
|
||||||
@@ -48,6 +63,10 @@
|
|||||||
color: #b1b1b1;
|
color: #b1b1b1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoDescriptionText {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoLastUpdatedTimeContainer {
|
.basicInfoLastUpdatedTimeContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -58,11 +77,21 @@
|
|||||||
.basicInfoUpdateTimeIcon {
|
.basicInfoUpdateTimeIcon {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
height: 1.2rem;
|
height: 1.2rem;
|
||||||
|
color: #626262;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoUpdateTimeIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoUpdateTimeText {
|
.basicInfoUpdateTimeText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
color: #626262;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoUpdateTimeText {
|
||||||
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operationContainer {
|
.operationContainer {
|
||||||
@@ -86,12 +115,20 @@
|
|||||||
color: #ffcd27;
|
color: #ffcd27;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .operationDefaultBadgeIcon {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.operationDefaultBadgeText {
|
.operationDefaultBadgeText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #ffcd27;
|
color: #ffcd27;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .operationDefaultBadgeText {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.bigText {
|
.bigText {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface IKnowledgeBaseVO {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
embeddingModelUUID: string;
|
embeddingModelUUID: string;
|
||||||
|
top_k: number;
|
||||||
lastUpdatedTimeAgo: string;
|
lastUpdatedTimeAgo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
embeddingModelUUID: string;
|
embeddingModelUUID: string;
|
||||||
|
top_k: number;
|
||||||
lastUpdatedTimeAgo: string;
|
lastUpdatedTimeAgo: string;
|
||||||
|
|
||||||
constructor(props: IKnowledgeBaseVO) {
|
constructor(props: IKnowledgeBaseVO) {
|
||||||
@@ -18,6 +20,7 @@ export class KnowledgeBaseVO implements IKnowledgeBaseVO {
|
|||||||
this.name = props.name;
|
this.name = props.name;
|
||||||
this.description = props.description;
|
this.description = props.description;
|
||||||
this.embeddingModelUUID = props.embeddingModelUUID;
|
this.embeddingModelUUID = props.embeddingModelUUID;
|
||||||
|
this.top_k = props.top_k;
|
||||||
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
this.lastUpdatedTimeAgo = props.lastUpdatedTimeAgo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,12 +127,12 @@ export default function FileUploadZone({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
{isUploading
|
||||||
? t('knowledge.documentsTab.uploading')
|
? t('knowledge.documentsTab.uploading')
|
||||||
: t('knowledge.documentsTab.dragAndDrop')}
|
: t('knowledge.documentsTab.dragAndDrop')}
|
||||||
</p>
|
</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')}
|
{t('knowledge.documentsTab.supportedFormats')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,7 +77,10 @@ export const columns = (
|
|||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="bg-white dark:bg-[#2a2a2e]"
|
||||||
|
>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{t('knowledge.documentsTab.actions')}
|
{t('knowledge.documentsTab.actions')}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ const getFormSchema = (t: (key: string) => string) =>
|
|||||||
embeddingModelUUID: z
|
embeddingModelUUID: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: t('knowledge.embeddingModelUUIDRequired') }),
|
.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({
|
export default function KBForm({
|
||||||
@@ -55,6 +59,7 @@ export default function KBForm({
|
|||||||
name: '',
|
name: '',
|
||||||
description: t('knowledge.defaultDescription'),
|
description: t('knowledge.defaultDescription'),
|
||||||
embeddingModelUUID: '',
|
embeddingModelUUID: '',
|
||||||
|
top_k: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,6 +74,7 @@ export default function KBForm({
|
|||||||
form.setValue('name', val.name);
|
form.setValue('name', val.name);
|
||||||
form.setValue('description', val.description);
|
form.setValue('description', val.description);
|
||||||
form.setValue('embeddingModelUUID', val.embeddingModelUUID);
|
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,
|
name: res.base.name,
|
||||||
description: res.base.description,
|
description: res.base.description,
|
||||||
embeddingModelUUID: res.base.embedding_model_uuid,
|
embeddingModelUUID: res.base.embedding_model_uuid,
|
||||||
|
top_k: res.base.top_k || 5,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -109,6 +116,7 @@ export default function KBForm({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
embedding_model_uuid: data.embeddingModelUUID,
|
embedding_model_uuid: data.embeddingModelUUID,
|
||||||
|
top_k: data.top_k,
|
||||||
};
|
};
|
||||||
httpClient
|
httpClient
|
||||||
.updateKnowledgeBase(initKbId, updateKb)
|
.updateKnowledgeBase(initKbId, updateKb)
|
||||||
@@ -127,6 +135,7 @@ export default function KBForm({
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
embedding_model_uuid: data.embeddingModelUUID,
|
embedding_model_uuid: data.embeddingModelUUID,
|
||||||
|
top_k: data.top_k,
|
||||||
};
|
};
|
||||||
httpClient
|
httpClient
|
||||||
.createKnowledgeBase(newKb)
|
.createKnowledgeBase(newKb)
|
||||||
@@ -200,7 +209,7 @@ export default function KBForm({
|
|||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t('knowledge.selectEmbeddingModel')}
|
placeholder={t('knowledge.selectEmbeddingModel')}
|
||||||
/>
|
/>
|
||||||
@@ -226,6 +235,30 @@ export default function KBForm({
|
|||||||
</FormItem>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function KnowledgePage() {
|
|||||||
name: kb.name,
|
name: kb.name,
|
||||||
description: kb.description,
|
description: kb.description,
|
||||||
embeddingModelUUID: kb.embedding_model_uuid,
|
embeddingModelUUID: kb.embedding_model_uuid,
|
||||||
|
top_k: kb.top_k ?? 5,
|
||||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -7,6 +7,19 @@
|
|||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .homeLayoutContainer {
|
||||||
|
background-color: #0a0a0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏区域 */
|
||||||
|
.sidebar {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sidebar {
|
||||||
|
background-color: #0a0a0b;
|
||||||
|
}
|
||||||
|
|
||||||
/* 主内容区域 */
|
/* 主内容区域 */
|
||||||
.main {
|
.main {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
@@ -23,6 +36,11 @@
|
|||||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.05);
|
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 {
|
.mainContent {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
@@ -30,3 +48,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #fafafa;
|
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);
|
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.iconBasicInfoContainer {
|
.iconBasicInfoContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -39,6 +49,11 @@
|
|||||||
.basicInfoText {
|
.basicInfoText {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoText {
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.providerContainer {
|
.providerContainer {
|
||||||
@@ -56,12 +71,20 @@
|
|||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .providerIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.providerLabel {
|
.providerLabel {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .providerLabel {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.baseURLContainer {
|
.baseURLContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -77,6 +100,10 @@
|
|||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .baseURLIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.baseURLText {
|
.baseURLText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -87,6 +114,10 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .baseURLText {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.bigText {
|
.bigText {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ export default function EmbeddingForm({
|
|||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t('models.selectModelProvider')}
|
placeholder={t('models.selectModelProvider')}
|
||||||
/>
|
/>
|
||||||
@@ -492,7 +492,7 @@ export default function EmbeddingForm({
|
|||||||
updateExtraArg(index, 'type', value)
|
updateExtraArg(index, 'type', value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[120px]">
|
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('models.type')} />
|
<SelectValue placeholder={t('models.type')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -6,12 +6,22 @@
|
|||||||
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 2px 2px 0 rgba(0, 0, 0, 0.2);
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.iconBasicInfoContainer {
|
.iconBasicInfoContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -40,6 +50,11 @@
|
|||||||
.basicInfoText {
|
.basicInfoText {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoText {
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.providerContainer {
|
.providerContainer {
|
||||||
@@ -57,12 +72,20 @@
|
|||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .providerIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.providerLabel {
|
.providerLabel {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .providerLabel {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.baseURLContainer {
|
.baseURLContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -78,6 +101,10 @@
|
|||||||
color: #626262;
|
color: #626262;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .baseURLIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.baseURLText {
|
.baseURLText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -88,6 +115,10 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .baseURLText {
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
.abilitiesContainer {
|
.abilitiesContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -108,18 +139,30 @@
|
|||||||
background-color: #66baff80;
|
background-color: #66baff80;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .abilityBadge {
|
||||||
|
background-color: rgba(34, 136, 238, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.abilityIcon {
|
.abilityIcon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
color: #2288ee;
|
color: #2288ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .abilityIcon {
|
||||||
|
color: #66baff;
|
||||||
|
}
|
||||||
|
|
||||||
.abilityLabel {
|
.abilityLabel {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #2288ee;
|
color: #2288ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .abilityLabel {
|
||||||
|
color: #66baff;
|
||||||
|
}
|
||||||
|
|
||||||
.bigText {
|
.bigText {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ export default function LLMForm({
|
|||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={t('models.selectModelProvider')}
|
placeholder={t('models.selectModelProvider')}
|
||||||
/>
|
/>
|
||||||
@@ -565,7 +565,7 @@ export default function LLMForm({
|
|||||||
updateExtraArg(index, 'type', value)
|
updateExtraArg(index, 'type', value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[120px]">
|
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectValue placeholder={t('models.type')} />
|
<SelectValue placeholder={t('models.type')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export default function LLMConfigPage() {
|
|||||||
<Tabs defaultValue="llm" className="w-full">
|
<Tabs defaultValue="llm" className="w-full">
|
||||||
<div className="flex flex-row gap-0 mb-4">
|
<div className="flex flex-row gap-0 mb-4">
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
<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">
|
<TabsTrigger value="llm" className="px-6 py-4 cursor-pointer">
|
||||||
{t('llm.llmModels')}
|
{t('llm.llmModels')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -206,12 +206,14 @@ export default function LLMConfigPage() {
|
|||||||
</div>
|
</div>
|
||||||
<TabsContent value="llm">
|
<TabsContent value="llm">
|
||||||
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
|
<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>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="embedding">
|
<TabsContent value="embedding">
|
||||||
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
|
<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')}
|
{t('embedding.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function PipelineDialog({
|
|||||||
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
<SidebarProvider className="items-start w-full flex h-full min-h-0">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
collapsible="none"
|
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>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export default function AtBadge({
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
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}
|
@{targetName}
|
||||||
{!readonly && onRemove && (
|
{!readonly && onRemove && (
|
||||||
<button
|
<button
|
||||||
onClick={onRemove}
|
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" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -302,14 +302,14 @@ export default function DebugDialog({
|
|||||||
|
|
||||||
const renderContent = () => (
|
const renderContent = () => (
|
||||||
<div className="flex flex-1 h-full min-h-0">
|
<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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
||||||
sessionType === 'person'
|
sessionType === 'person'
|
||||||
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
? '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`}
|
} border-0 shadow-none`}
|
||||||
onClick={() => setSessionType('person')}
|
onClick={() => setSessionType('person')}
|
||||||
>
|
>
|
||||||
@@ -328,7 +328,7 @@ export default function DebugDialog({
|
|||||||
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
className={`w-10 h-10 justify-center rounded-md transition-none ${
|
||||||
sessionType === 'group'
|
sessionType === 'group'
|
||||||
? 'bg-[#2288ee] text-white hover:bg-[#2288ee] hover:text-white'
|
? '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`}
|
} border-0 shadow-none`}
|
||||||
onClick={() => setSessionType('group')}
|
onClick={() => setSessionType('group')}
|
||||||
>
|
>
|
||||||
@@ -345,7 +345,7 @@ export default function DebugDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col w-[10rem] h-full min-h-0">
|
<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">
|
<div className="space-y-6">
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="text-center text-muted-foreground py-12 text-lg">
|
<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',
|
'max-w-md px-5 py-3 rounded-2xl',
|
||||||
message.role === 'user'
|
message.role === 'user'
|
||||||
? 'bg-[#2288ee] text-white rounded-br-none'
|
? '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)}
|
{renderMessageContent(message)}
|
||||||
@@ -374,7 +374,7 @@ export default function DebugDialog({
|
|||||||
'text-xs mt-2',
|
'text-xs mt-2',
|
||||||
message.role === 'user'
|
message.role === 'user'
|
||||||
? 'text-white/70'
|
? 'text-white/70'
|
||||||
: 'text-gray-500',
|
: 'text-gray-500 dark:text-gray-400',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message.role === 'user'
|
{message.role === 'user'
|
||||||
@@ -389,7 +389,7 @@ export default function DebugDialog({
|
|||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-gray-600">
|
<span className="text-sm text-gray-600">
|
||||||
{t('pipelines.debugDialog.streaming')}
|
{t('pipelines.debugDialog.streaming')}
|
||||||
@@ -412,23 +412,25 @@ export default function DebugDialog({
|
|||||||
? t('pipelines.debugDialog.privateChat')
|
? t('pipelines.debugDialog.privateChat')
|
||||||
: t('pipelines.debugDialog.groupChat'),
|
: 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 && (
|
{showAtPopover && (
|
||||||
<div
|
<div
|
||||||
ref={popoverRef}
|
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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 px-4 py-1.5 rounded cursor-pointer',
|
'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}
|
onClick={handleAtSelect}
|
||||||
onMouseEnter={() => setIsHovering(true)}
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
onMouseLeave={() => setIsHovering(false)}
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
>
|
>
|
||||||
<span>
|
<span className="text-gray-800 dark:text-gray-200">
|
||||||
@webchatbot - {t('pipelines.debugDialog.atTips')}
|
@webchatbot - {t('pipelines.debugDialog.atTips')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -459,7 +461,7 @@ export default function DebugDialog({
|
|||||||
|
|
||||||
// 原有的Dialog包装
|
// 原有的Dialog包装
|
||||||
return (
|
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()}
|
{renderContent()}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,12 +10,22 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoContainer {
|
.basicInfoContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -35,6 +45,11 @@
|
|||||||
.basicInfoNameText {
|
.basicInfoNameText {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoNameText {
|
||||||
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoDescriptionText {
|
.basicInfoDescriptionText {
|
||||||
@@ -48,6 +63,10 @@
|
|||||||
color: #b1b1b1;
|
color: #b1b1b1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoDescriptionText {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
.basicInfoLastUpdatedTimeContainer {
|
.basicInfoLastUpdatedTimeContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -58,11 +77,21 @@
|
|||||||
.basicInfoUpdateTimeIcon {
|
.basicInfoUpdateTimeIcon {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
height: 1.2rem;
|
height: 1.2rem;
|
||||||
|
color: #626262;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoUpdateTimeIcon {
|
||||||
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.basicInfoUpdateTimeText {
|
.basicInfoUpdateTimeText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
color: #626262;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .basicInfoUpdateTimeText {
|
||||||
|
color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operationContainer {
|
.operationContainer {
|
||||||
@@ -86,12 +115,20 @@
|
|||||||
color: #ffcd27;
|
color: #ffcd27;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .operationDefaultBadgeIcon {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.operationDefaultBadgeText {
|
.operationDefaultBadgeText {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #ffcd27;
|
color: #ffcd27;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .operationDefaultBadgeText {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.bigText {
|
.bigText {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default function PipelineFormComponent({
|
|||||||
|
|
||||||
return (
|
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 {...form}>
|
||||||
<form
|
<form
|
||||||
id="pipeline-form"
|
id="pipeline-form"
|
||||||
@@ -456,7 +456,7 @@ export default function PipelineFormComponent({
|
|||||||
</form>
|
</form>
|
||||||
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
|
{/* 按钮栏移到 Tabs 外部,始终固定底部 */}
|
||||||
{showButtons && (
|
{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 && (
|
{isEditMode && !isDefaultPipeline && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -125,17 +125,26 @@ export default function PluginConfigPage() {
|
|||||||
value={`${sortByValue},${sortOrderValue}`}
|
value={`${sortByValue},${sortOrderValue}`}
|
||||||
onValueChange={handleSortChange}
|
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')} />
|
<SelectValue placeholder={t('pipelines.sortBy')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectItem value="created_at,DESC">
|
<SelectItem
|
||||||
|
value="created_at,DESC"
|
||||||
|
className="text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
{t('pipelines.newestCreated')}
|
{t('pipelines.newestCreated')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="updated_at,DESC">
|
<SelectItem
|
||||||
|
value="updated_at,DESC"
|
||||||
|
className="text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
{t('pipelines.recentlyEdited')}
|
{t('pipelines.recentlyEdited')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="updated_at,ASC">
|
<SelectItem
|
||||||
|
value="updated_at,ASC"
|
||||||
|
className="text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
{t('pipelines.earliestEdited')}
|
{t('pipelines.earliestEdited')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function PluginConfigPage() {
|
|||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<Tabs defaultValue="installed" className="w-full">
|
<Tabs defaultValue="installed" className="w-full">
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
<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">
|
<TabsTrigger value="installed" className="px-6 py-4 cursor-pointer">
|
||||||
{t('plugins.installed')}
|
{t('plugins.installed')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -134,7 +134,7 @@ export default function PluginConfigPage() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||||
<DialogContent className="w-[500px] p-6">
|
<DialogContent className="w-[500px] p-6 bg-white dark:bg-[#1a1a1e]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-4">
|
<DialogTitle className="flex items-center gap-4">
|
||||||
<GithubIcon className="size-6" />
|
<GithubIcon className="size-6" />
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default function PluginCardComponent({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={onCardClick}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
<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="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="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} /{' '}
|
{cardVO.author} /{' '}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
<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]">
|
<Badge variant="outline" className="text-[0.7rem]">
|
||||||
v{cardVO.version}
|
v{cardVO.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</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}
|
{cardVO.description}
|
||||||
</div>
|
</div>
|
||||||
</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="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]">
|
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||||
<svg
|
<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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
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>
|
<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>
|
</svg>
|
||||||
<div className="text-base text-black font-medium">
|
<div className="text-base text-black dark:text-[#f0f0f0] font-medium">
|
||||||
{t('plugins.eventCount', {
|
{t('plugins.eventCount', {
|
||||||
count: Object.keys(cardVO.event_handlers).length,
|
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]">
|
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||||
<svg
|
<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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
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>
|
<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>
|
</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 })}
|
{t('plugins.toolCount', { count: cardVO.tools.length })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,7 +117,9 @@ export default function PluginCardComponent({
|
|||||||
<div className="flex items-center justify-center gap-[0.4rem]">
|
<div className="flex items-center justify-center gap-[0.4rem]">
|
||||||
<svg
|
<svg
|
||||||
className={`w-[1.4rem] h-[1.4rem] cursor-pointer ${
|
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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -141,15 +141,26 @@ export default function PluginMarketComponent({
|
|||||||
value={`${sortByValue},${sortOrderValue}`}
|
value={`${sortByValue},${sortOrderValue}`}
|
||||||
onValueChange={handleSortChange}
|
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')} />
|
<SelectValue placeholder={t('plugins.sortBy')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||||
<SelectItem value="stars,DESC">{t('plugins.mostStars')}</SelectItem>
|
<SelectItem
|
||||||
<SelectItem value="created_at,DESC">
|
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')}
|
{t('plugins.recentlyAdded')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="pushed_at,DESC">
|
<SelectItem
|
||||||
|
value="pushed_at,DESC"
|
||||||
|
className="text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
{t('plugins.recentlyUpdated')}
|
{t('plugins.recentlyUpdated')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -198,7 +209,7 @@ export default function PluginMarketComponent({
|
|||||||
isActive={pageNum === nowPage}
|
isActive={pageNum === nowPage}
|
||||||
onClick={() => handlePageChange(pageNum)}
|
onClick={() => handlePageChange(pageNum)}
|
||||||
>
|
>
|
||||||
<span className="text-black select-none">
|
<span className="text-black dark:text-white select-none">
|
||||||
{pageNum}
|
{pageNum}
|
||||||
</span>
|
</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|||||||
+9
-7
@@ -16,7 +16,7 @@ export default function PluginMarketCardComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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]">
|
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||||
<svg
|
<svg
|
||||||
className="w-16 h-16 text-[#2288ee]"
|
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="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="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} /{' '}
|
{cardVO.author} /{' '}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
<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>
|
</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}
|
{cardVO.description}
|
||||||
</div>
|
</div>
|
||||||
</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="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]">
|
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||||
<svg
|
<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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
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>
|
<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>
|
</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 })}
|
{t('plugins.starCount', { count: cardVO.starCount })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
<div className="flex h-full flex-row items-center justify-center gap-[0.4rem]">
|
||||||
<svg
|
<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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
|||||||
+14
@@ -7,13 +7,27 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer {
|
||||||
|
background-color: #1f1f22;
|
||||||
|
box-shadow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContainer:hover {
|
.cardContainer:hover {
|
||||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
|
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .cardContainer:hover {
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.createCardContainer {
|
.createCardContainer {
|
||||||
font-size: 90px;
|
font-size: 90px;
|
||||||
color: #acacac;
|
color: #acacac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.dark) .createCardContainer {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export interface KnowledgeBase {
|
|||||||
description: string;
|
description: string;
|
||||||
embedding_model_uuid: string;
|
embedding_model_uuid: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
top_k?: number;
|
top_k: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRespProviderEmbeddingModels {
|
export interface ApiRespProviderEmbeddingModels {
|
||||||
@@ -155,6 +155,7 @@ export interface KnowledgeBase {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
embedding_model_uuid: string;
|
embedding_model_uuid: string;
|
||||||
|
top_k: number;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -715,6 +715,16 @@ class HttpClient {
|
|||||||
new_password: newPassword,
|
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 => {
|
const getBaseURL = (): string => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'react-photo-view/dist/react-photo-view.css';
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import I18nProvider from '@/i18n/I18nProvider';
|
import I18nProvider from '@/i18n/I18nProvider';
|
||||||
|
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'LangBot',
|
title: 'LangBot',
|
||||||
@@ -15,12 +16,14 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html>
|
<html lang="zh" suppressHydrationWarning>
|
||||||
<body className={``}>
|
<body className={``}>
|
||||||
<I18nProvider>
|
<ThemeProvider>
|
||||||
{children}
|
<I18nProvider>
|
||||||
<Toaster />
|
{children}
|
||||||
</I18nProvider>
|
<Toaster />
|
||||||
|
</I18nProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import {
|
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -26,15 +20,15 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { useRouter } from 'next/navigation';
|
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 langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||||
|
|
||||||
const formSchema = (t: (key: string) => string) =>
|
const formSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -45,7 +39,6 @@ const formSchema = (t: (key: string) => string) =>
|
|||||||
export default function Login() {
|
export default function Login() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||||
resolver: zodResolver(formSchema(t)),
|
resolver: zodResolver(formSchema(t)),
|
||||||
@@ -56,57 +49,10 @@ export default function Login() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
judgeLanguage();
|
|
||||||
getIsInitialized();
|
getIsInitialized();
|
||||||
checkIfAlreadyLoggedIn();
|
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() {
|
function getIsInitialized() {
|
||||||
httpClient
|
httpClient
|
||||||
.checkIfInited()
|
.checkIfInited()
|
||||||
@@ -155,25 +101,12 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:dark:bg-neutral-900">
|
||||||
<Card className="w-[375px]">
|
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-end mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<Select
|
<ThemeToggle />
|
||||||
value={currentLanguage}
|
<LanguageSelector />
|
||||||
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>
|
</div>
|
||||||
<img
|
<img
|
||||||
src={langbotIcon.src}
|
src={langbotIcon.src}
|
||||||
|
|||||||
@@ -8,13 +8,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import {
|
import { LanguageSelector } from '@/components/ui/language-selector';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
@@ -26,14 +20,14 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import { useRouter } from 'next/navigation';
|
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 langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '@/i18n';
|
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||||
|
|
||||||
const formSchema = (t: (key: string) => string) =>
|
const formSchema = (t: (key: string) => string) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -44,7 +38,6 @@ const formSchema = (t: (key: string) => string) =>
|
|||||||
export default function Register() {
|
export default function Register() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [currentLanguage, setCurrentLanguage] = useState<string>(i18n.language);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||||
resolver: zodResolver(formSchema(t)),
|
resolver: zodResolver(formSchema(t)),
|
||||||
@@ -55,58 +48,9 @@ export default function Register() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
judgeLanguage();
|
|
||||||
getIsInitialized();
|
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() {
|
function getIsInitialized() {
|
||||||
httpClient
|
httpClient
|
||||||
.checkIfInited()
|
.checkIfInited()
|
||||||
@@ -139,25 +83,12 @@ export default function Register() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||||
<Card className="w-[375px]">
|
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-end mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<Select
|
<ThemeToggle />
|
||||||
value={currentLanguage}
|
<LanguageSelector />
|
||||||
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>
|
</div>
|
||||||
<img
|
<img
|
||||||
src={langbotIcon.src}
|
src={langbotIcon.src}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { Mail, Lock, ArrowLeft } from 'lucide-react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { ThemeToggle } from '@/components/ui/theme-toggle';
|
||||||
|
|
||||||
const REGEXP_ONLY_DIGITS_AND_CHARS = /^[0-9a-zA-Z]+$/;
|
const REGEXP_ONLY_DIGITS_AND_CHARS = /^[0-9a-zA-Z]+$/;
|
||||||
|
|
||||||
@@ -84,17 +85,18 @@ export default function ResetPassword() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||||
<Card className="w-[375px]">
|
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
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" />
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||||
{t('resetPassword.backToLogin')}
|
{t('resetPassword.backToLogin')}
|
||||||
</Link>
|
</Link>
|
||||||
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl text-center">
|
<CardTitle className="text-2xl text-center">
|
||||||
{t('resetPassword.title')}
|
{t('resetPassword.title')}
|
||||||
|
|||||||
@@ -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:
|
secondary:
|
||||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||||
ghost:
|
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',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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)}
|
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="text-black" />
|
<ChevronLeftIcon className="text-black dark:text-white" />
|
||||||
<span className="hidden sm:block"></span>
|
<span className="hidden sm:block"></span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
);
|
);
|
||||||
@@ -94,7 +94,7 @@ function PaginationNext({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block"></span>
|
<span className="hidden sm:block"></span>
|
||||||
<ChevronRightIcon className="text-black" />
|
<ChevronRightIcon className="text-black dark:text-white" />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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: {
|
common: {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
logout: 'Logout',
|
logout: 'Logout',
|
||||||
|
accountOptions: 'Account',
|
||||||
|
account: 'Account',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
welcome: 'Welcome back to LangBot 👋',
|
welcome: 'Welcome back to LangBot 👋',
|
||||||
@@ -41,6 +43,21 @@ const enUS = {
|
|||||||
test: 'Test',
|
test: 'Test',
|
||||||
forgotPassword: 'Forgot Password?',
|
forgotPassword: 'Forgot Password?',
|
||||||
loading: 'Loading...',
|
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: {
|
notFound: {
|
||||||
title: 'Page not found',
|
title: 'Page not found',
|
||||||
@@ -253,6 +270,11 @@ const enUS = {
|
|||||||
today: 'Today',
|
today: 'Today',
|
||||||
kbName: 'Knowledge Base Name',
|
kbName: 'Knowledge Base Name',
|
||||||
kbDescription: 'Knowledge Base Description',
|
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',
|
defaultDescription: 'A knowledge base',
|
||||||
embeddingModelUUID: 'Embedding Model',
|
embeddingModelUUID: 'Embedding Model',
|
||||||
selectEmbeddingModel: 'Select Embedding Model',
|
selectEmbeddingModel: 'Select Embedding Model',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const jaJP = {
|
|||||||
common: {
|
common: {
|
||||||
login: 'ログイン',
|
login: 'ログイン',
|
||||||
logout: 'ログアウト',
|
logout: 'ログアウト',
|
||||||
|
accountOptions: 'アカウントオプション',
|
||||||
|
account: 'アカウント',
|
||||||
email: 'メールアドレス',
|
email: 'メールアドレス',
|
||||||
password: 'パスワード',
|
password: 'パスワード',
|
||||||
welcome: 'LangBot へおかえりなさい 👋',
|
welcome: 'LangBot へおかえりなさい 👋',
|
||||||
@@ -42,6 +44,21 @@ const jaJP = {
|
|||||||
test: 'テスト',
|
test: 'テスト',
|
||||||
forgotPassword: 'パスワードを忘れた?',
|
forgotPassword: 'パスワードを忘れた?',
|
||||||
loading: '読み込み中...',
|
loading: '読み込み中...',
|
||||||
|
theme: 'テーマ',
|
||||||
|
changePassword: 'パスワードを変更',
|
||||||
|
currentPassword: '現在のパスワード',
|
||||||
|
newPassword: '新しいパスワード',
|
||||||
|
confirmNewPassword: '新しいパスワードを確認',
|
||||||
|
enterCurrentPassword: '現在のパスワードを入力',
|
||||||
|
enterNewPassword: '新しいパスワードを入力',
|
||||||
|
enterConfirmPassword: '新しいパスワードを確認',
|
||||||
|
currentPasswordRequired: '現在のパスワードは必須です',
|
||||||
|
newPasswordRequired: '新しいパスワードは必須です',
|
||||||
|
confirmPasswordRequired: '新しいパスワードを確認してください',
|
||||||
|
passwordsDoNotMatch: '新しいパスワードが一致しません',
|
||||||
|
changePasswordSuccess: 'パスワードの変更に成功しました',
|
||||||
|
changePasswordFailed:
|
||||||
|
'パスワードの変更に失敗しました。現在のパスワードを確認してください',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
title: 'ページが見つかりません',
|
title: 'ページが見つかりません',
|
||||||
@@ -255,6 +272,11 @@ const jaJP = {
|
|||||||
today: '今日',
|
today: '今日',
|
||||||
kbName: '知識ベース名',
|
kbName: '知識ベース名',
|
||||||
kbDescription: '知識ベースの説明',
|
kbDescription: '知識ベースの説明',
|
||||||
|
topK: 'Top K',
|
||||||
|
topKRequired: 'Top Kは必須です',
|
||||||
|
topKMax: 'Top Kの最大値は30です',
|
||||||
|
topKdescription:
|
||||||
|
'取得する関連性の高い上位K件の文書の数。1~30の範囲で設定できます',
|
||||||
defaultDescription: '知識ベース',
|
defaultDescription: '知識ベース',
|
||||||
embeddingModelUUID: '埋め込みモデル',
|
embeddingModelUUID: '埋め込みモデル',
|
||||||
selectEmbeddingModel: '埋め込みモデルを選択',
|
selectEmbeddingModel: '埋め込みモデルを選択',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const zhHans = {
|
|||||||
common: {
|
common: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
|
accountOptions: '账户选项',
|
||||||
|
account: '账户',
|
||||||
email: '邮箱',
|
email: '邮箱',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
welcome: '欢迎回到 LangBot 👋',
|
welcome: '欢迎回到 LangBot 👋',
|
||||||
@@ -41,6 +43,20 @@ const zhHans = {
|
|||||||
test: '测试',
|
test: '测试',
|
||||||
forgotPassword: '忘记密码?',
|
forgotPassword: '忘记密码?',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
|
theme: '主题',
|
||||||
|
changePassword: '修改密码',
|
||||||
|
currentPassword: '当前密码',
|
||||||
|
newPassword: '新密码',
|
||||||
|
confirmNewPassword: '确认新密码',
|
||||||
|
enterCurrentPassword: '输入当前密码',
|
||||||
|
enterNewPassword: '输入新密码',
|
||||||
|
enterConfirmPassword: '确认新密码',
|
||||||
|
currentPasswordRequired: '当前密码不能为空',
|
||||||
|
newPasswordRequired: '新密码不能为空',
|
||||||
|
confirmPasswordRequired: '确认密码不能为空',
|
||||||
|
passwordsDoNotMatch: '两次输入的密码不一致',
|
||||||
|
changePasswordSuccess: '密码修改成功',
|
||||||
|
changePasswordFailed: '密码修改失败,请检查当前密码是否正确',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
title: '页面不存在',
|
title: '页面不存在',
|
||||||
@@ -248,6 +264,10 @@ const zhHans = {
|
|||||||
today: '今天',
|
today: '今天',
|
||||||
kbName: '知识库名称',
|
kbName: '知识库名称',
|
||||||
kbDescription: '知识库描述',
|
kbDescription: '知识库描述',
|
||||||
|
topK: '召回数量',
|
||||||
|
topKRequired: '召回数量不能为空',
|
||||||
|
topKMax: '召回数量最大值为 30',
|
||||||
|
topKdescription: '召回相关文档块的数量,取值范围为 1-30',
|
||||||
defaultDescription: '一个知识库',
|
defaultDescription: '一个知识库',
|
||||||
embeddingModelUUID: '嵌入模型',
|
embeddingModelUUID: '嵌入模型',
|
||||||
selectEmbeddingModel: '选择嵌入模型',
|
selectEmbeddingModel: '选择嵌入模型',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const zhHant = {
|
|||||||
common: {
|
common: {
|
||||||
login: '登入',
|
login: '登入',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
|
accountOptions: '帳戶選項',
|
||||||
|
account: '帳戶',
|
||||||
email: '電子郵件',
|
email: '電子郵件',
|
||||||
password: '密碼',
|
password: '密碼',
|
||||||
welcome: '歡迎回到 LangBot 👋',
|
welcome: '歡迎回到 LangBot 👋',
|
||||||
@@ -41,6 +43,20 @@ const zhHant = {
|
|||||||
test: '測試',
|
test: '測試',
|
||||||
forgotPassword: '忘記密碼?',
|
forgotPassword: '忘記密碼?',
|
||||||
loading: '載入中...',
|
loading: '載入中...',
|
||||||
|
theme: '主題',
|
||||||
|
changePassword: '修改密碼',
|
||||||
|
currentPassword: '當前密碼',
|
||||||
|
newPassword: '新密碼',
|
||||||
|
confirmNewPassword: '確認新密碼',
|
||||||
|
enterCurrentPassword: '輸入當前密碼',
|
||||||
|
enterNewPassword: '輸入新密碼',
|
||||||
|
enterConfirmPassword: '確認新密碼',
|
||||||
|
currentPasswordRequired: '當前密碼不能為空',
|
||||||
|
newPasswordRequired: '新密碼不能為空',
|
||||||
|
confirmPasswordRequired: '確認密碼不能為空',
|
||||||
|
passwordsDoNotMatch: '兩次輸入的密碼不一致',
|
||||||
|
changePasswordSuccess: '密碼修改成功',
|
||||||
|
changePasswordFailed: '密碼修改失敗,請檢查當前密碼是否正確',
|
||||||
},
|
},
|
||||||
notFound: {
|
notFound: {
|
||||||
title: '頁面不存在',
|
title: '頁面不存在',
|
||||||
@@ -236,10 +252,10 @@ const zhHant = {
|
|||||||
editKnowledgeBase: '編輯知識庫',
|
editKnowledgeBase: '編輯知識庫',
|
||||||
selectKnowledgeBase: '選擇知識庫',
|
selectKnowledgeBase: '選擇知識庫',
|
||||||
empty: '無',
|
empty: '無',
|
||||||
editDocument: '文件',
|
editDocument: '文檔',
|
||||||
description: '設定可用於提升模型回覆品質的知識庫',
|
description: '設定可用於提升模型回覆品質的知識庫',
|
||||||
metadata: '中繼資料',
|
metadata: '中繼資料',
|
||||||
documents: '文件',
|
documents: '文檔',
|
||||||
kbNameRequired: '知識庫名稱不能為空',
|
kbNameRequired: '知識庫名稱不能為空',
|
||||||
kbDescriptionRequired: '知識庫描述不能為空',
|
kbDescriptionRequired: '知識庫描述不能為空',
|
||||||
embeddingModelUUIDRequired: '嵌入模型不能為空',
|
embeddingModelUUIDRequired: '嵌入模型不能為空',
|
||||||
@@ -247,6 +263,10 @@ const zhHant = {
|
|||||||
today: '今天',
|
today: '今天',
|
||||||
kbName: '知識庫名稱',
|
kbName: '知識庫名稱',
|
||||||
kbDescription: '知識庫描述',
|
kbDescription: '知識庫描述',
|
||||||
|
topK: '召回數量 ',
|
||||||
|
topKRequired: '召回數量不能為空',
|
||||||
|
topKMax: '召回數量最大值為30',
|
||||||
|
topKdescription: '取得相關性高的上位 K 件文獻的數量,範圍為1~30',
|
||||||
defaultDescription: '一個知識庫',
|
defaultDescription: '一個知識庫',
|
||||||
embeddingModelUUID: '嵌入模型',
|
embeddingModelUUID: '嵌入模型',
|
||||||
selectEmbeddingModel: '選擇嵌入模型',
|
selectEmbeddingModel: '選擇嵌入模型',
|
||||||
@@ -259,29 +279,29 @@ const zhHant = {
|
|||||||
name: '名稱',
|
name: '名稱',
|
||||||
status: '狀態',
|
status: '狀態',
|
||||||
noResults: '暫無文件',
|
noResults: '暫無文件',
|
||||||
dragAndDrop: '拖曳檔案到此處或點擊上傳',
|
dragAndDrop: '拖曳文檔到此處或點擊上傳',
|
||||||
uploading: '上傳中...',
|
uploading: '上傳中...',
|
||||||
supportedFormats: '支援 PDF、Word、TXT、Markdown 等文件格式',
|
supportedFormats: '支援 PDF、Word、TXT、Markdown 等文檔格式',
|
||||||
uploadSuccess: '檔案上傳成功!',
|
uploadSuccess: '文檔上傳成功!',
|
||||||
uploadError: '檔案上傳失敗,請重試',
|
uploadError: '文檔上傳失敗,請重試',
|
||||||
uploadingFile: '上傳檔案中...',
|
uploadingFile: '上傳文檔中...',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
delete: '刪除檔案',
|
delete: '刪除文檔',
|
||||||
fileDeleteSuccess: '檔案刪除成功',
|
fileDeleteSuccess: '文檔刪除成功',
|
||||||
fileDeleteFailed: '檔案刪除失敗',
|
fileDeleteFailed: '文檔刪除失敗',
|
||||||
processing: '處理中',
|
processing: '處理中',
|
||||||
completed: '完成',
|
completed: '完成',
|
||||||
failed: '失敗',
|
failed: '失敗',
|
||||||
},
|
},
|
||||||
deleteKnowledgeBaseConfirmation:
|
deleteKnowledgeBaseConfirmation:
|
||||||
'您確定要刪除這個知識庫嗎?此知識庫下的所有文件將被刪除。',
|
'您確定要刪除這個知識庫嗎?此知識庫下的所有文檔將被刪除。',
|
||||||
retrieve: '檢索測試',
|
retrieve: '檢索測試',
|
||||||
retrieveTest: '檢索測試',
|
retrieveTest: '檢索測試',
|
||||||
query: '查詢',
|
query: '查詢',
|
||||||
queryPlaceholder: '輸入查詢內容...',
|
queryPlaceholder: '輸入查詢內容...',
|
||||||
distance: '距離',
|
distance: '距離',
|
||||||
content: '內容',
|
content: '內容',
|
||||||
fileName: '檔案名稱',
|
fileName: '文檔名稱',
|
||||||
noResults: '暫無結果',
|
noResults: '暫無結果',
|
||||||
retrieveError: '檢索失敗',
|
retrieveError: '檢索失敗',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user