feat: 对话增加历史记录 (#485)

This commit is contained in:
shaohuzhang1 2024-05-20 17:50:14 +08:00 committed by GitHub
parent 5a4971645d
commit a3dd967c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1121 additions and 569 deletions

View File

@ -56,6 +56,18 @@ class ChatSerializers(serializers.Serializer):
QuerySet(Chat).filter(id=self.data.get('chat_id'), application_id=self.data.get('application_id')).delete()
return True
class ClientChatHistory(serializers.Serializer):
application_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("应用id"))
client_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("客户端id"))
def page(self, current_page: int, page_size: int, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
queryset = QuerySet(Chat).filter(client_id=self.data.get('client_id'),
application_id=self.data.get('application_id'))
queryset = queryset.order_by('-create_time')
return page_search(current_page, page_size, queryset, lambda row: ChatSerializerModel(row).data)
class Query(serializers.Serializer):
abstract = serializers.CharField(required=False, error_messages=ErrMessage.char("摘要"))
history_day = serializers.IntegerField(required=True, error_messages=ErrMessage.integer("历史天数"))
@ -282,6 +294,12 @@ class ChatRecordSerializerModel(serializers.ModelSerializer):
'create_time', 'update_time']
class ChatSerializerModel(serializers.ModelSerializer):
class Meta:
model = Chat
fields = ['id', 'application_id', 'abstract', 'client_id']
class ChatRecordSerializer(serializers.Serializer):
class Operate(serializers.Serializer):
chat_id = serializers.UUIDField(required=True, error_messages=ErrMessage.uuid("对话id"))
@ -319,13 +337,16 @@ class ChatRecordSerializer(serializers.Serializer):
class Query(serializers.Serializer):
application_id = serializers.UUIDField(required=True)
chat_id = serializers.UUIDField(required=True)
order_asc = serializers.BooleanField(required=False)
def list(self, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id'))
order_by = 'create_time' if self.data.get('order_asc') is None or self.data.get(
'order_asc') else '-create_time'
return [ChatRecordSerializerModel(chat_record).data for chat_record in
QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by("create_time")]
QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by)]
@staticmethod
def reset_chat_record(chat_record):
@ -354,8 +375,10 @@ class ChatRecordSerializer(serializers.Serializer):
def page(self, current_page: int, page_size: int, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
order_by = 'create_time' if self.data.get('order_asc') is None or self.data.get(
'order_asc') else '-create_time'
page = page_search(current_page, page_size,
QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by("create_time"),
QuerySet(ChatRecord).filter(chat_id=self.data.get('chat_id')).order_by(order_by),
post_records_handler=lambda chat_record: self.reset_chat_record(chat_record))
return page

View File

@ -12,6 +12,17 @@ from application.swagger_api.application_api import ApplicationApi
from common.mixins.api_mixin import ApiMixin
class ChatClientHistoryApi(ApiMixin):
@staticmethod
def get_request_params_api():
return [openapi.Parameter(name='application_id',
in_=openapi.IN_PATH,
type=openapi.TYPE_STRING,
required=True,
description='应用id')
]
class ChatApi(ApiMixin):
@staticmethod
def get_request_body_api():
@ -80,7 +91,7 @@ class ChatApi(ApiMixin):
'problem_optimization'],
properties={
'id': openapi.Schema(type=openapi.TYPE_STRING, title="应用id",
description="应用id,修改的时候传,创建的时候不传"),
description="应用id,修改的时候传,创建的时候不传"),
'model_id': openapi.Schema(type=openapi.TYPE_STRING, title="模型id", description="模型id"),
'dataset_id_list': openapi.Schema(type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),

View File

@ -53,14 +53,17 @@ const chatButtonHtml=
const getChatContainerHtml=(protocol,host,token)=>{
return `<div id="maxkb-chat-container">
<iframe id="maxkb-chat" src=${protocol}://${host}/ui/chat/${token}></iframe>
<div class="maxkb-operate"><div class="maxkb-closeviewport maxkb-viewportnone"><svg style="vertical-align: middle;overflow: hidden;" t="1710214539671" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(100, 106, 115)" width="16" height="16"><path d="M85.333333 384c25.6 0 42.666667-17.066667 42.666667-42.666667V128h213.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666666H85.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v256c0 25.6 17.066667 42.666667 42.666666 42.666667zM938.666667 640c-25.6 0-42.666667 17.066667-42.666667 42.666667v213.333333h-213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666666h256c25.6 0 42.666667-17.066667 42.666666-42.666666v-256c0-25.6-17.066667-42.666667-42.666666-42.666667zM601.6 401.066667c4.266667 8.533333 12.8 17.066667 21.333333 21.333333 4.266667 4.266667 12.8 4.266667 17.066667 4.266667h256c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667h-153.6l226.133333-226.133333c17.066667-17.066667 17.066667-42.666667 0-59.733333-8.533333-8.533333-17.066667-12.8-29.866666-12.8s-21.333333 4.266667-29.866667 12.8L682.666667 281.6V128c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666667 42.666667v256c0 4.266667 0 12.8 4.266667 17.066667zM115.2 968.533333L341.333333 742.4V896c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666667-42.666667v-256c0-4.266667 0-12.8-4.266667-17.066667-4.266667-8.533333-12.8-17.066667-21.333333-21.333333-4.266667-4.266667-12.8-4.266667-17.066667-4.266667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h153.6l-226.133333 226.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733333s42.666667 17.066667 59.733333 0z" p-id="10189"></path></svg></div>
<iframe id="maxkb-chat" src=${protocol}://${host}/ui/chat/${token}?mode=embed></iframe>
<div class="maxkb-operate"><div class="maxkb-closeviewport maxkb-viewportnone"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.507 11.6645C7.73712 11.6645 7.94545 11.7578 8.09625 11.9086C8.24706 12.0594 8.34033 12.2677 8.34033 12.4978V16.7976C8.34033 17.0277 8.15378 17.2143 7.92366 17.2143H7.09033C6.86021 17.2143 6.67366 17.0277 6.67366 16.7976V14.5812L3.41075 17.843C3.24803 18.0057 2.98421 18.0057 2.82149 17.843L2.23224 17.2537C2.06952 17.091 2.06952 16.8272 2.23224 16.6645L5.56668 13.3311H3.19634C2.96622 13.3311 2.77967 13.1446 2.77967 12.9145V12.0811C2.77967 11.851 2.96622 11.6645 3.19634 11.6645H7.507ZM16.5991 2.1572C16.7619 1.99448 17.0257 1.99448 17.1884 2.1572L17.7777 2.74645C17.9404 2.90917 17.9404 3.17299 17.7777 3.33571L14.4432 6.66904H16.8136C17.0437 6.66904 17.2302 6.85559 17.2302 7.08571V7.91904C17.2302 8.14916 17.0437 8.33571 16.8136 8.33571H12.5029C12.2728 8.33571 12.0644 8.24243 11.9136 8.09163C11.7628 7.94082 11.6696 7.73249 11.6696 7.50237V3.20257C11.6696 2.97245 11.8561 2.7859 12.0862 2.7859H12.9196C13.1497 2.7859 13.3362 2.97245 13.3362 3.20257V5.419L16.5991 2.1572Z" fill="#646A73"/>
</svg></div>
<div class="maxkb-openviewport">
<svg t="1710150885892" style="vertical-align: middle;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(100, 106, 115)" width="16" height="16" ><path d="M85.333333 384c25.6 0 42.666667-17.066667 42.666667-42.666667V128h213.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666666H85.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v256c0 25.6 17.066667 42.666667 42.666666 42.666667zM938.666667 640c-25.6 0-42.666667 17.066667-42.666667 42.666667v213.333333h-213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666666h256c25.6 0 42.666667-17.066667 42.666666-42.666666v-256c0-25.6-17.066667-42.666667-42.666666-42.666667zM977.066667 68.266667c-4.266667-8.533333-12.8-17.066667-21.333334-21.333334-4.266667-4.266667-12.8-4.266667-17.066666-4.266666h-256c-25.6 0-42.666667 17.066667-42.666667 42.666666s17.066667 42.666667 42.666667 42.666667h153.6l-226.133334 226.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733334 8.533333 8.533333 17.066667 12.8 29.866667 12.8s21.333333-4.266667 29.866667-12.8L896 187.733333V341.333333c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666666-42.666667V85.333333c0-4.266667 0-12.8-4.266666-17.066666zM354.133333 610.133333L128 836.266667V682.666667c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666666 42.666667v256c0 4.266667 0 12.8 4.266666 17.066666 4.266667 8.533333 12.8 17.066667 21.333334 21.333334 4.266667 4.266667 12.8 4.266667 17.066666 4.266666h256c25.6 0 42.666667-17.066667 42.666667-42.666666s-17.066667-42.666667-42.666667-42.666667H187.733333l226.133334-226.133333c17.066667-17.066667 17.066667-42.666667 0-59.733334s-42.666667-17.066667-59.733334 0z" p-id="8645"></path></svg>
</div>
<div class="maxkb-chat-close"><svg style="vertical-align: middle;overflow: hidden;" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9.95317 8.73169L15.5511 3.13376C15.7138 2.97104 15.9776 2.97104 16.1403 3.13376L16.7296 3.72301C16.8923 3.88573 16.8923 4.14955 16.7296 4.31227L11.1317 9.9102L16.7296 15.5081C16.8923 15.6708 16.8923 15.9347 16.7296 16.0974L16.1403 16.6866C15.9776 16.8494 15.7138 16.8494 15.5511 16.6866L9.95317 11.0887L4.35524 16.6866C4.19252 16.8494 3.9287 16.8494 3.76598 16.6866L3.17673 16.0974C3.01401 15.9347 3.01401 15.6708 3.17673 15.5081L8.77465 9.9102L3.17673 4.31227C3.01401 4.14955 3.01401 3.88573 3.17673 3.72301L3.76598 3.13376C3.9287 2.97104 4.19252 2.97104 4.35524 3.13376L9.95317 8.73169Z" fill="#646A73"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M7.15209 11.5968C7.31481 11.4341 7.57862 11.4341 7.74134 11.5968L8.3306 12.186C8.49332 12.3487 8.49332 12.6126 8.3306 12.7753L4.99615 16.1086H7.3665C7.59662 16.1086 7.78316 16.2952 7.78316 16.5253V17.3586C7.78316 17.5887 7.59662 17.7753 7.3665 17.7753H3.05584C2.82572 17.7753 2.61738 17.682 2.46658 17.5312C2.31578 17.3804 2.2225 17.1721 2.2225 16.9419V12.6421C2.2225 12.412 2.40905 12.2255 2.63917 12.2255H3.4725C3.70262 12.2255 3.88917 12.412 3.88917 12.6421V14.8586L7.15209 11.5968ZM16.937 2.22217C17.1671 2.22217 17.3754 2.31544 17.5262 2.46625C17.677 2.61705 17.7703 2.82538 17.7703 3.0555V7.35531C17.7703 7.58543 17.5837 7.77198 17.3536 7.77198H16.5203C16.2902 7.77198 16.1036 7.58543 16.1036 7.35531V5.13888L12.8407 8.40068C12.678 8.5634 12.4142 8.5634 12.2515 8.40068L11.6622 7.81142C11.4995 7.64871 11.4995 7.38489 11.6622 7.22217L14.9966 3.88883H12.6263C12.3962 3.88883 12.2096 3.70229 12.2096 3.47217V2.63883C12.2096 2.40872 12.3962 2.22217 12.6263 2.22217H16.937Z" fill="#646A73"/>
</svg></div>
<div class="maxkb-chat-close"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9.95317 8.73169L15.5511 3.13376C15.7138 2.97104 15.9776 2.97104 16.1403 3.13376L16.7296 3.72301C16.8923 3.88573 16.8923 4.14955 16.7296 4.31227L11.1317 9.9102L16.7296 15.5081C16.8923 15.6708 16.8923 15.9347 16.7296 16.0974L16.1403 16.6866C15.9776 16.8494 15.7138 16.8494 15.5511 16.6866L9.95317 11.0887L4.35524 16.6866C4.19252 16.8494 3.9287 16.8494 3.76598 16.6866L3.17673 16.0974C3.01401 15.9347 3.01401 15.6708 3.17673 15.5081L8.77465 9.9102L3.17673 4.31227C3.01401 4.14955 3.01401 3.88573 3.17673 3.72301L3.76598 3.13376C3.9287 2.97104 4.19252 2.97104 4.35524 3.13376L9.95317 8.73169Z" fill="#646A73"/>
</svg>
</div></div>
`
}
@ -94,6 +97,7 @@ const initChat=(root)=>{
const closeviewport=root.querySelector('.maxkb-closeviewport')
const close_func=()=>{
chat_container.style['display']=chat_container.style['display']=='block'?'none':'block'
chat_button.style['display']=chat_container.style['display']=='block'?'none':'block'
}
close_icon=chat_container.querySelector('.maxkb-chat-close')
chat_button.onclick = close_func
@ -250,12 +254,12 @@ function initMaxkbStyle(root){
border: 1px solid #ffffff;
background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10);
position: fixed;bottom: 20px;right: 45px;overflow: hidden;
position: fixed;bottom: 16px;right: 16px;overflow: hidden;
}
#maxkb #maxkb-chat-container .maxkb-operate{
top: 15px;
right: 10px;
top: 18px;
right: 15px;
position: absolute;
display: flex;
align-items: center;

View File

@ -30,6 +30,8 @@ urlpatterns = [
path('application/<int:current_page>/<int:page_size>', views.Application.Page.as_view(), name='application_page'),
path('application/<str:application_id>/chat/open', views.ChatView.Open.as_view()),
path("application/chat/open", views.ChatView.OpenTemp.as_view()),
path("application/<str:application_id>/chat/client/<int:current_page>/<int:page_size>",
views.ChatView.ClientChatHistoryPage.as_view()),
path('application/<str:application_id>/chat/export', views.ChatView.Export.as_view(), name='export'),
path('application/<str:application_id>/chat', views.ChatView.as_view(), name='chats'),
path('application/<str:application_id>/chat/<int:current_page>/<int:page_size>', views.ChatView.Page.as_view()),

View File

@ -13,7 +13,8 @@ from rest_framework.views import APIView
from application.serializers.chat_message_serializers import ChatMessageSerializer
from application.serializers.chat_serializers import ChatSerializers, ChatRecordSerializer
from application.swagger_api.chat_api import ChatApi, VoteApi, ChatRecordApi, ImproveApi, ChatRecordImproveApi
from application.swagger_api.chat_api import ChatApi, VoteApi, ChatRecordApi, ImproveApi, ChatRecordImproveApi, \
ChatClientHistoryApi
from common.auth import TokenAuth, has_permissions
from common.constants.authentication_type import AuthenticationType
from common.constants.permission_constants import Permission, Group, Operate, \
@ -137,6 +138,28 @@ class ChatView(APIView):
data={'application_id': application_id, 'user_id': request.user.id,
'chat_id': chat_id}).delete())
class ClientChatHistoryPage(APIView):
authentication_classes = [TokenAuth]
@action(methods=['GET'], detail=False)
@swagger_auto_schema(operation_summary="分页获取客户端对话列表",
operation_id="分页获取客户端对话列表",
manual_parameters=result.get_page_request_params(
ChatClientHistoryApi.get_request_params_api()),
responses=result.get_page_api_response(ChatApi.get_response_body_api()),
tags=["应用/对话日志"]
)
@has_permissions(
ViewPermission([RoleConstants.APPLICATION_ACCESS_TOKEN],
[lambda r, keywords: Permission(group=Group.APPLICATION, operate=Operate.USE,
dynamic_tag=keywords.get('application_id'))])
)
def get(self, request: Request, application_id: str, current_page: int, page_size: int):
return result.success(ChatSerializers.ClientChatHistory(
data={'client_id': request.auth.client_id, 'application_id': application_id}).page(
current_page=current_page,
page_size=page_size))
class Page(APIView):
authentication_classes = [TokenAuth]
@ -198,7 +221,7 @@ class ChatView(APIView):
def get(self, request: Request, application_id: str, chat_id: str):
return result.success(ChatRecordSerializer.Query(
data={'application_id': application_id,
'chat_id': chat_id}).list())
'chat_id': chat_id, 'order_asc': request.query_params.get('order_asc')}).list())
class Page(APIView):
authentication_classes = [TokenAuth]
@ -219,7 +242,8 @@ class ChatView(APIView):
def get(self, request: Request, application_id: str, chat_id: str, current_page: int, page_size: int):
return result.success(ChatRecordSerializer.Query(
data={'application_id': application_id,
'chat_id': chat_id}).page(current_page, page_size))
'chat_id': chat_id, 'order_asc': request.query_params.get('order_asc')}).page(current_page,
page_size))
class Vote(APIView):
authentication_classes = [TokenAuth]

View File

@ -1,307 +0,0 @@
function auth(token, protocol, host) {
const XML = new XMLHttpRequest()
XML.open('POST', `${protocol}//${host}/api/application/authentication`, false)
XML.setRequestHeader('Content-Type', 'application/json')
res = XML.send(JSON.stringify({ access_token: token }))
return XML.status == 200
}
const guideHtml=`
<div class="maxkb-mask">
<div class="maxkb-content"></div>
</div>
<div class="maxkb-tips">
<div class="maxkb-close">
<svg style="vertical-align: middle;overflow: hidden;" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9.95317 8.73169L15.5511 3.13376C15.7138 2.97104 15.9776 2.97104 16.1403 3.13376L16.7296 3.72301C16.8923 3.88573 16.8923 4.14955 16.7296 4.31227L11.1317 9.9102L16.7296 15.5081C16.8923 15.6708 16.8923 15.9347 16.7296 16.0974L16.1403 16.6866C15.9776 16.8494 15.7138 16.8494 15.5511 16.6866L9.95317 11.0887L4.35524 16.6866C4.19252 16.8494 3.9287 16.8494 3.76598 16.6866L3.17673 16.0974C3.01401 15.9347 3.01401 15.6708 3.17673 15.5081L8.77465 9.9102L3.17673 4.31227C3.01401 4.14955 3.01401 3.88573 3.17673 3.72301L3.76598 3.13376C3.9287 2.97104 4.19252 2.97104 4.35524 3.13376L9.95317 8.73169Z" fill="#ffffff"></path>
</svg>
</div>
<div class="maxkb-title"> 🌟 遇见问题不再有障碍</div>
<p>你好我是你的智能小助手<br/>
点我开启高效解答模式让问题变成过去式</p>
<div class="maxkb-button">
<button>我知道了</button>
</div>
<span class="maxkb-arrow" ></span>
</div>
`
const chatButtonHtml=
`<div class="maxkb-chat-button"><svg style="vertical-align: middle;overflow: hidden;" xmlns="http://www.w3.org/2000/svg" width="48" height="56" viewBox="0 0 48 56" fill="none">
<g filter="url(#filter0_d_349_49711)">
<path d="M8 24C8 12.9543 16.9543 4 28 4H48V44H28C16.9543 44 8 35.0457 8 24Z" fill="url(#paint0_linear_349_49711)"/>
</g>
<path d="M31.6667 15.6665H28.3333V18.1665H29.1667V19.8332H24.5833C23.6629 19.8332 22.9167 20.5794 22.9167 21.4998V30.6665C22.9167 31.587 23.6629 32.3332 24.5833 32.3332H35.4167C36.3371 32.3332 37.0833 31.587 37.0833 30.6665V21.4998C37.0833 20.5794 36.3371 19.8332 35.4167 19.8332H30.8333V18.1665H31.6667V15.6665ZM25.8333 24.8332H28.3333V27.3332H25.8333V24.8332ZM34.1667 24.8332V27.3332H31.6667V24.8332H34.1667Z" fill="white"/>
<path d="M21.6667 23.9998H20V28.1665H21.6667V23.9998Z" fill="white"/>
<path d="M38.3333 23.9998H40V28.1665H38.3333V23.9998Z" fill="white"/>
<defs>
<filter id="filter0_d_349_49711" x="0" y="0" width="56" height="56" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.168627 0 0 0 0 0.372549 0 0 0 0 0.85098 0 0 0 0.24 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_349_49711"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_349_49711" result="shape"/>
</filter>
<linearGradient id="paint0_linear_349_49711" x1="48" y1="25.6667" x2="8" y2="25.6667" gradientUnits="userSpaceOnUse">
<stop stop-color="#9258F7"/>
<stop offset="1" stop-color="#3370FF"/>
</linearGradient>
</defs>
</svg>
</div>`
const getChatContainerHtml=(protocol,host,token)=>{
return `<div id="maxkb-chat-container">
<iframe id="maxkb-chat" src=${protocol}//${host}/ui/chat/${token}></iframe>
<div class="maxkb-closeviewport maxkb-viewportnone"><svg style="vertical-align: middle;overflow: hidden;" t="1710214539671" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(100, 106, 115)" width="16" height="16"><path d="M85.333333 384c25.6 0 42.666667-17.066667 42.666667-42.666667V128h213.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666666H85.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v256c0 25.6 17.066667 42.666667 42.666666 42.666667zM938.666667 640c-25.6 0-42.666667 17.066667-42.666667 42.666667v213.333333h-213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666666h256c25.6 0 42.666667-17.066667 42.666666-42.666666v-256c0-25.6-17.066667-42.666667-42.666666-42.666667zM601.6 401.066667c4.266667 8.533333 12.8 17.066667 21.333333 21.333333 4.266667 4.266667 12.8 4.266667 17.066667 4.266667h256c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667h-153.6l226.133333-226.133333c17.066667-17.066667 17.066667-42.666667 0-59.733333-8.533333-8.533333-17.066667-12.8-29.866666-12.8s-21.333333 4.266667-29.866667 12.8L682.666667 281.6V128c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666667 42.666667v256c0 4.266667 0 12.8 4.266667 17.066667zM115.2 968.533333L341.333333 742.4V896c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666667-42.666667v-256c0-4.266667 0-12.8-4.266667-17.066667-4.266667-8.533333-12.8-17.066667-21.333333-21.333333-4.266667-4.266667-12.8-4.266667-17.066667-4.266667H128c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h153.6l-226.133333 226.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733333s42.666667 17.066667 59.733333 0z" p-id="10189"></path></svg></div>
<div class="maxkb-openviewport">
<svg style="vertical-align: middle;overflow: hidden;" t="1710150885892" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="rgb(100, 106, 115)" width="16" height="16" ><path d="M85.333333 384c25.6 0 42.666667-17.066667 42.666667-42.666667V128h213.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666666H85.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v256c0 25.6 17.066667 42.666667 42.666666 42.666667zM938.666667 640c-25.6 0-42.666667 17.066667-42.666667 42.666667v213.333333h-213.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666666h256c25.6 0 42.666667-17.066667 42.666666-42.666666v-256c0-25.6-17.066667-42.666667-42.666666-42.666667zM977.066667 68.266667c-4.266667-8.533333-12.8-17.066667-21.333334-21.333334-4.266667-4.266667-12.8-4.266667-17.066666-4.266666h-256c-25.6 0-42.666667 17.066667-42.666667 42.666666s17.066667 42.666667 42.666667 42.666667h153.6l-226.133334 226.133333c-17.066667 17.066667-17.066667 42.666667 0 59.733334 8.533333 8.533333 17.066667 12.8 29.866667 12.8s21.333333-4.266667 29.866667-12.8L896 187.733333V341.333333c0 25.6 17.066667 42.666667 42.666667 42.666667s42.666667-17.066667 42.666666-42.666667V85.333333c0-4.266667 0-12.8-4.266666-17.066666zM354.133333 610.133333L128 836.266667V682.666667c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666666 42.666667v256c0 4.266667 0 12.8 4.266666 17.066666 4.266667 8.533333 12.8 17.066667 21.333334 21.333334 4.266667 4.266667 12.8 4.266667 17.066666 4.266666h256c25.6 0 42.666667-17.066667 42.666667-42.666666s-17.066667-42.666667-42.666667-42.666667H187.733333l226.133334-226.133333c17.066667-17.066667 17.066667-42.666667 0-59.733334s-42.666667-17.066667-59.733334 0z" p-id="8645"></path></svg>
</div>
<div class="chat_close"><svg style="vertical-align: middle;overflow: hidden;" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9.95317 8.73169L15.5511 3.13376C15.7138 2.97104 15.9776 2.97104 16.1403 3.13376L16.7296 3.72301C16.8923 3.88573 16.8923 4.14955 16.7296 4.31227L11.1317 9.9102L16.7296 15.5081C16.8923 15.6708 16.8923 15.9347 16.7296 16.0974L16.1403 16.6866C15.9776 16.8494 15.7138 16.8494 15.5511 16.6866L9.95317 11.0887L4.35524 16.6866C4.19252 16.8494 3.9287 16.8494 3.76598 16.6866L3.17673 16.0974C3.01401 15.9347 3.01401 15.6708 3.17673 15.5081L8.77465 9.9102L3.17673 4.31227C3.01401 4.14955 3.01401 3.88573 3.17673 3.72301L3.76598 3.13376C3.9287 2.97104 4.19252 2.97104 4.35524 3.13376L9.95317 8.73169Z" fill="#646A73"/>
</svg>
</div>`
}
/**
* 初始化引导
* @param {*} root
*/
const initGuide=(root)=>{
root.insertAdjacentHTML("beforeend",guideHtml)
const button=root.querySelector(".maxkb-button")
const close_icon=root.querySelector('.maxkb-close')
const close_func=()=>{
root.removeChild(root.querySelector('.maxkb-tips'))
root.removeChild(root.querySelector('.maxkb-mask'))
localStorage.setItem('maxkbMaskTip',true)
}
button.onclick=close_func
close_icon.onclick=close_func
}
const initChat=(root)=>{
// 添加对话icon
root.insertAdjacentHTML("beforeend",chatButtonHtml)
// 添加对话框
root.insertAdjacentHTML('beforeend',getChatContainerHtml(window.maxkbChatConfig.protocol,window.maxkbChatConfig.host,window.maxkbChatConfig.token))
// 按钮元素
const chat_button=root.querySelector('.maxkb-chat-button')
// 对话框元素
const chat_container=root.querySelector('#maxkb-chat-container')
const viewport=root.querySelector('.maxkb-openviewport')
const closeviewport=root.querySelector('.maxkb-closeviewport')
const close_func=()=>{
chat_container.style['display']=chat_container.style['display']=='block'?'none':'block'
}
close_icon=chat_container.querySelector('.maxkb-close')
chat_button.onclick = close_func
close_icon.onclick=close_func
const viewport_func=()=>{
if(chat_container.classList.contains('maxkb-enlarge')){
chat_container.classList.remove("maxkb-enlarge");
viewport.classList.remove('maxkb-viewportnone')
closeviewport.classList.add('maxkb-viewportnone')
}else{
chat_container.classList.add("maxkb-enlarge");
viewport.classList.add('maxkb-viewportnone')
closeviewport.classList.remove('maxkb-viewportnone')
}
}
viewport.onclick=viewport_func
closeviewport.onclick=viewport_func
}
/**
* 第一次进来的引导提示
*/
function initMaxkb(){
const maxkb=document.createElement('div')
const root=document.createElement('div')
root.id="maxkb"
initMaxkbStyle(maxkb)
maxkb.appendChild(root)
document.body.appendChild(maxkb)
const maxkbMaskTip=localStorage.getItem('maxkbMaskTip')
if(maxkbMaskTip==null){
initGuide(root)
}
initChat(root)
}
// 初始化全局样式
function initMaxkbStyle(root){
style=document.createElement('style')
style.type='text/css'
style.innerText= `
/* 放大 */
#maxkb .maxkb-enlarge {
width: 50%!important;
height: 100%!important;
bottom: 0!important;
right: 0 !important;
}
@media only screen and (max-width: 768px){
#maxkb .maxkb-enlarge {
width: 100%!important;
height: 100%!important;
right: 0 !important;
bottom: 0!important;
}
}
/* 引导 */
#maxkb .maxkb-mask {
position: fixed;
z-index: 999;
background-color: transparent;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
#maxkb .maxkb-mask .maxkb-content {
width: 45px;
height: 50px;
box-shadow: 1px 1px 1px 2000px rgba(0,0,0,.6);
border-radius: 50% 0 0 50%;
position: absolute;
right: 0;
bottom: 42px;
z-index: 1000;
}
#maxkb .maxkb-tips {
position: fixed;
bottom: 30px;
right: 60px;
padding: 22px 24px 24px;
border-radius: 6px;
color: #ffffff;
font-size: 14px;
background: #3370FF;
z-index: 1000;
}
#maxkb .maxkb-tips .maxkb-arrow {
position: absolute;
background: #3370FF;
width: 10px;
height: 10px;
pointer-events: none;
transform: rotate(45deg);
box-sizing: border-box;
/* left */
right: -5px;
bottom: 33px;
border-left-color: transparent;
border-bottom-color: transparent
}
#maxkb .maxkb-tips .maxkb-title {
font-size: 20px;
font-weight: 500;
margin-bottom: 8px;
}
#maxkb .maxkb-tips .maxkb-button {
text-align: right;
margin-top: 24px;
}
#maxkb .maxkb-tips .maxkb-button button {
border-radius: 4px;
background: #FFF;
padding: 3px 12px;
color: #3370FF;
cursor: pointer;
outline: none;
border: none;
}
#maxkb .maxkb-tips .maxkb-button button::after{
border: none;
}
#maxkb .maxkb-tips .maxkb-close {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
}
#maxkb-chat-container {
width: 420px;
height: 600px;
display:none;
}
@media only screen and (max-width: 768px) {
#maxkb-chat-container {
width: 100%;
height: 70%;
right: 0 !important;
}
}
#maxkb .maxkb-chat-button{
position: fixed;
bottom: 30px;
right: 0;
cursor: pointer;
}
#maxkb #maxkb-chat-container{
z-index:10000;position: relative;
border-radius: 8px;
border: 1px solid var(--N300, #DEE0E3);
background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.10);
position: fixed;bottom: 20px;right: 45px;overflow: hidden;
}
#maxkb #maxkb-chat-container .maxkb-chat-close{
position: absolute;
top: 15px;
right: 10px;
cursor: pointer;
}
#maxkb #maxkb-chat-container .maxkb-openviewport{
position: absolute;
top: 15px;
right: 50px;
cursor: pointer;
}
#maxkb #maxkb-chat-container .maxkb-closeviewport{
position: absolute;
top: 15px;
right: 50px;
cursor: pointer;
}
#maxkb #maxkb-chat-container .maxkb-viewportnone{
display:none;
}
#maxkb #maxkb-chat-container #maxkb-chat{
height:100%;
width:100%;
border: none;
}
#maxkb #maxkb-chat-container {
animation: appear .4s ease-in-out;
}
@keyframes appear {
from {
height: 0;;
}
to {
height: 600px;
}
}`
root.appendChild(style)
}
function embedChatbot() {
const t = window.maxkbChatConfig
check = auth(t.token, t.protocol, t.host)
if (t && t.token && t.protocol && t.host && check) {
// 初始化maxkb智能小助手
initMaxkb()
} else console.error('invalid parameter')
}
window.onload = embedChatbot

View File

@ -1,5 +1,5 @@
import { Result } from '@/request/Result'
import { get, post, del, put, exportExcel } from '@/request/index'
import { get, del, put, exportExcel } from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import { type Ref } from 'vue'
@ -64,11 +64,12 @@ const getChatRecordLog: (
application_id: String,
chart_id: String,
page: pageRequest,
loading?: Ref<boolean>
) => Promise<Result<any>> = (application_id, chart_id, page, loading) => {
loading?: Ref<boolean>,
order_asc?: boolean
) => Promise<Result<any>> = (application_id, chart_id, page, loading, order_asc) => {
return get(
`${prefix}/${application_id}/chat/${chart_id}/chat_record/${page.current_page}/${page.page_size}`,
undefined,
{ order_asc: order_asc !== undefined ? order_asc : true },
loading
)
}
@ -173,6 +174,18 @@ const getRecordDetail: (
)
}
const getChatLogClient: (
application_id: String,
page: pageRequest,
loading?: Ref<boolean>
) => Promise<Result<any>> = (application_id, page, loading) => {
return get(
`${prefix}/${application_id}/chat/client/${page.current_page}/${page.page_size}`,
null,
loading
)
}
export default {
getChatLog,
delChatLog,
@ -181,5 +194,6 @@ export default {
getMarkRecord,
getRecordDetail,
delMarkRecord,
exportChatLog
exportChatLog,
getChatLogClient
}

View File

@ -41,7 +41,7 @@
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { ref } from 'vue'
import { copyClick } from '@/utils/clipboard'
import EditContentDialog from '@/views/log/component/EditContentDialog.vue'
import EditMarkDialog from '@/views/log/component/EditMarkDialog.vue'
@ -65,7 +65,6 @@ const EditContentDialogRef = ref()
const EditMarkDialogRef = ref()
const buttonData = ref(props.data)
const loading = ref(false)
function editContent(data: any) {
EditContentDialogRef.value.open(data)

View File

@ -56,7 +56,7 @@
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { ref } from 'vue'
import { copyClick } from '@/utils/clipboard'
import applicationApi from '@/api/application'
const props = defineProps({
@ -68,7 +68,7 @@ const props = defineProps({
type: String,
default: ''
},
chartId: {
chatId: {
type: String,
default: ''
},
@ -86,8 +86,8 @@ function regeneration() {
function voteHandle(val: string) {
applicationApi
.putChatVote(props.applicationId, props.chartId, props.data.record_id, val, loading)
.then((res) => {
.putChatVote(props.applicationId, props.chatId, props.data.record_id, val, loading)
.then(() => {
buttonData.value['vote_status'] = val
emit('update:data', buttonData.value)
})

View File

@ -2,7 +2,7 @@
<div ref="aiChatRef" class="ai-chat" :class="log ? 'chart-log' : ''">
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop">
<div ref="dialogScrollbar" class="ai-chat__content p-24">
<div class="item-content mb-16" v-if="!props.available || props.data?.prologue">
<div class="item-content mb-16" v-if="!props.available || (props.data?.prologue && !log)">
<div class="avatar">
<AppAvatar class="avatar-gradient">
<img src="@/assets/icon_robot.svg" style="width: 75%" alt="" />
@ -73,9 +73,7 @@
<el-card v-else shadow="always" class="dialog-card">
<MdRenderer :source="item.answer_text"></MdRenderer>
<div
v-if="(id && item.write_ed) || (props.data?.show_source && item.write_ed) || log"
>
<div v-if="showSource(item)">
<el-divider> <el-text type="info">知识来源</el-text> </el-divider>
<div class="mb-8">
<el-space wrap>
@ -132,7 +130,7 @@
<OperationButton
:data="item"
:applicationId="appId"
:chartId="chartOpenId"
:chatId="chartOpenId"
@regeneration="regenerationChart(item)"
/>
</div>
@ -187,7 +185,6 @@ import { randomId } from '@/utils/utils'
import useStore from '@/stores'
import MdRenderer from '@/components/markdown-renderer/MdRenderer.vue'
import { MdPreview } from 'md-editor-v3'
import { MsgError } from '@/utils/message'
import { debounce } from 'lodash'
defineOptions({ name: 'AiChat' })
const route = useRoute()
@ -209,8 +206,15 @@ const props = defineProps({
available: {
type: Boolean,
default: true
}
},
chatId: {
type: String,
default: ''
} // Id
})
const emit = defineEmits(['refresh', 'scroll'])
const { application } = useStore()
const ParagraphSourceDialogRef = ref()
@ -249,6 +253,18 @@ const prologueList = computed(() => {
return arr
})
watch(
() => props.chatId,
(val) => {
if (val && val !== 'new') {
chartOpenId.value = val
} else {
chartOpenId.value = ''
}
},
{ deep: true }
)
watch(
() => props.data,
() => {
@ -260,15 +276,25 @@ watch(
watch(
() => props.record,
(value) => {
if (props.log) {
chatList.value = value
}
chatList.value = value
},
{
immediate: true
}
)
function showSource(row: any) {
if (props.log) {
return true
} else if (row.write_ed) {
if (id || props.data?.show_source) {
return true
}
} else {
return false
}
}
function openParagraph(row: any, id?: string) {
ParagraphSourceDialogRef.value.open(row, id)
}
@ -446,7 +472,7 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
})
}
if (!chartOpenId.value) {
getChartOpenId(chat).catch((e) => {
getChartOpenId(chat).catch(() => {
errorWrite(chat)
})
} else {
@ -464,7 +490,7 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
.then(() => {
chatMessage(chat)
})
.catch((err) => {
.catch(() => {
errorWrite(chat)
})
} else if (response.status === 460) {
@ -487,6 +513,9 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
}
})
.then(() => {
if (props.chatId === 'new') {
emit('refresh', chartOpenId.value)
}
return (id || props.data?.show_source) && getSourceDetail(chat)
})
.finally(() => {
@ -535,6 +564,7 @@ const handleScrollTop = ($event: any) => {
} else {
scorll.value = false
}
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value })
}
const handleScroll = () => {
@ -549,6 +579,11 @@ const handleScroll = () => {
}
}
function setScrollBottom() {
//
scrollDiv.value.setScrollTop(getMaxHeight())
}
watch(
chatList,
() => {
@ -556,6 +591,10 @@ watch(
},
{ deep: true, immediate: true }
)
defineExpose({
setScrollBottom
})
</script>
<style lang="scss" scoped>
.ai-chat {

View File

@ -4,18 +4,20 @@
<template v-for="(item, index) in data" :key="index">
<li
@click.prevent="clickHandle(item, index)"
:class="current === index ? 'active' : ''"
:class="current === item[props.valueKey] ? 'active' : ''"
class="cursor"
>
<slot :row="item" :index="index"> </slot>
</li>
</template>
</ul>
<el-empty description="暂无数据" v-else />
<slot name="empty" v-else>
<el-empty description="暂无数据" />
</slot>
</div>
</template>
<script setup lang="ts">
import { ref, watch, useSlots } from 'vue'
import { ref, watch } from 'vue'
defineOptions({ name: 'CommonList' })
@ -23,29 +25,29 @@ const props = withDefaults(
defineProps<{
data: Array<any>
defaultActive?: string
valueKey?: string //
}>(),
{
data: () => [],
defaultActive: ''
defaultActive: '',
valueKey: 'id'
}
)
const current = ref<Number | String>(0)
watch(
() => props.defaultActive,
(val) => {
if (val) {
current.value = props.data.findIndex((v) => v.id === val)
}
current.value = val
},
{ immediate: true }
)
const emit = defineEmits(['click'])
const current = ref(0)
function clickHandle(row: any, index: number) {
current.value = index
current.value = row[props.valueKey]
emit('click', row)
}
</script>
@ -54,10 +56,12 @@ function clickHandle(row: any, index: number) {
.common-list {
li {
padding: 10px 16px;
font-weight: 400;
&.active {
background: var(--el-color-primary-light-9);
border-radius: 4px;
color: var(--el-color-primary);
font-weight: 500;
}
}
}

View File

@ -823,5 +823,55 @@ export const iconMap: any = {
)
])
}
},
'app-chat-record': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 16 16',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M11.3333 7.33334C11.3333 6.96515 11.6318 6.66667 12 6.66667H14.6667C15.0349 6.66667 15.3333 6.96515 15.3333 7.33334V12.6667C15.3333 13.0349 15.0349 13.3333 14.6667 13.3333H13.2761L12.4714 14.1381C12.2111 14.3984 11.7889 14.3984 11.5286 14.1381L10.7239 13.3333H7.33334C6.96515 13.3333 6.66667 13.0349 6.66667 12.6667V10C6.66667 9.63182 6.96515 9.33334 7.33334 9.33334H11.3333V7.33334ZM12.6667 8.00001V10C12.6667 10.3682 12.3682 10.6667 12 10.6667H8.00001V12H11C11.1768 12 11.3464 12.0702 11.4714 12.1953L12 12.7239L12.5286 12.1953C12.6536 12.0702 12.8232 12 13 12H14V8.00001H12.6667Z',
fill: 'currentColor'
}),
h('path', {
d: 'M1.33334 1.33333C0.965149 1.33333 0.666672 1.63181 0.666672 1.99999V10C0.666672 10.3682 0.965149 10.6667 1.33334 10.6667H2.72386L3.86193 11.8047C4.12228 12.0651 4.54439 12.0651 4.80474 11.8047L5.94281 10.6667H12C12.3682 10.6667 12.6667 10.3682 12.6667 10V1.99999C12.6667 1.63181 12.3682 1.33333 12 1.33333H1.33334ZM4.66667 5.99999C4.66667 6.36818 4.36819 6.66666 4.00001 6.66666C3.63182 6.66666 3.33334 6.36818 3.33334 5.99999C3.33334 5.6318 3.63182 5.33333 4.00001 5.33333C4.36819 5.33333 4.66667 5.6318 4.66667 5.99999ZM7.33334 5.99999C7.33334 6.36818 7.03486 6.66666 6.66667 6.66666C6.29848 6.66666 6 6.36818 6 5.99999C6 5.6318 6.29848 5.33333 6.66667 5.33333C7.03486 5.33333 7.33334 5.6318 7.33334 5.99999ZM10 5.99999C10 6.36818 9.70153 6.66666 9.33334 6.66666C8.96515 6.66666 8.66667 6.36818 8.66667 5.99999C8.66667 5.6318 8.96515 5.33333 9.33334 5.33333C9.70153 5.33333 10 5.6318 10 5.99999Z',
fill: 'currentColor'
})
]
)
])
}
},
'app-history-outlined': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg'
},
[
h('path', {
d: 'M18.6667 10.0001C18.6667 14.6025 14.9358 18.3334 10.3334 18.3334C7.68359 18.3334 5.32266 17.0967 3.79633 15.1689L5.12054 14.1563C6.3421 15.6864 8.22325 16.6667 10.3334 16.6667C14.0153 16.6667 17 13.682 17 10.0001C17 6.31818 14.0153 3.33341 10.3334 3.33341C7.03005 3.33341 4.28786 5.73596 3.75889 8.88897H4.3469C4.70187 8.88897 4.9136 9.28459 4.7167 9.57995L3.32493 11.6676C3.14901 11.9315 2.76125 11.9315 2.58533 11.6676L1.19356 9.57995C0.996651 9.28459 1.20838 8.88897 1.56336 8.88897H2.07347C2.61669 4.8119 6.10774 1.66675 10.3334 1.66675C14.9358 1.66675 18.6667 5.39771 18.6667 10.0001Z',
fill: 'currentColor'
}),
h('path', {
d: 'M10.8334 9.7223V7.11119C10.8334 6.86573 10.6344 6.66675 10.3889 6.66675H9.61115C9.36569 6.66675 9.16671 6.86573 9.16671 7.11119V10.9445C9.16671 11.19 9.36569 11.389 9.61115 11.389H13.1667C13.4122 11.389 13.6112 11.19 13.6112 10.9445V10.1667C13.6112 9.92129 13.4122 9.7223 13.1667 9.7223H10.8334Z',
fill: 'currentColor'
})
]
)
])
}
}
}

View File

@ -0,0 +1,151 @@
import { nextTick } from 'vue'
import { throttle } from 'lodash-unified'
import { getScrollContainer } from 'element-plus/es/utils/index'
import type { App } from 'vue'
export const SCOPE = 'InfiniteScrollUP'
export const CHECK_INTERVAL = 50
export const DEFAULT_DELAY = 200
export const DEFAULT_DISTANCE = 0
const attributes = {
delay: {
type: Number,
default: DEFAULT_DELAY
},
distance: {
type: Number,
default: DEFAULT_DISTANCE
},
disabled: {
type: Boolean,
default: false
},
immediate: {
type: Boolean,
default: true
}
}
type Attrs = typeof attributes
type ScrollOptions = { [K in keyof Attrs]: Attrs[K]['default'] }
type InfiniteScrollCallback = () => void
type InfiniteScrollEl = HTMLElement & {
[SCOPE]: {
container: HTMLElement | Window
containerEl: HTMLElement
instance: any
delay: number // export for test
lastScrollTop: number
cb: InfiniteScrollCallback
onScroll: () => void
observer?: MutationObserver
}
}
const getScrollOptions = (el: HTMLElement, instance: any): ScrollOptions => {
return Object.entries(attributes).reduce((acm: any, [name, option]) => {
const { type, default: defaultValue } = option
const attrVal: any = el.getAttribute(`infinite-scroll-up-${name}`)
let value = instance[attrVal] ?? attrVal ?? defaultValue
value = value === 'false' ? false : value
value = type(value)
acm[name] = Number.isNaN(value) ? defaultValue : value
return acm
}, {} as ScrollOptions)
}
const destroyObserver = (el: InfiniteScrollEl) => {
const { observer } = el[SCOPE]
if (observer) {
observer.disconnect()
delete el[SCOPE].observer
}
}
const handleScroll = (el: InfiniteScrollEl, cb: InfiniteScrollCallback) => {
const { container, containerEl, instance, observer, lastScrollTop } = el[SCOPE]
const { disabled } = getScrollOptions(el, instance)
const { scrollTop } = containerEl
el[SCOPE].lastScrollTop = scrollTop
// trigger only if full check has done and not disabled and scroll down
if (observer || disabled || scrollTop > 0) return
if (scrollTop == 0) {
cb.call(instance)
}
}
function checkFull(el: InfiniteScrollEl, cb: InfiniteScrollCallback) {
const { containerEl, instance } = el[SCOPE]
const { disabled } = getScrollOptions(el, instance)
if (disabled || containerEl.clientHeight == 0) return
if (containerEl.scrollTop <= 0) {
cb.call(instance)
} else {
destroyObserver(el)
}
}
const InfiniteScroll = {
async mounted(el: any, binding: any) {
const { instance, value: cb } = binding
// ensure parentNode mounted
await nextTick()
const { delay, immediate } = getScrollOptions(el, instance)
const container = getScrollContainer(el, true)
const containerEl = container === window ? document.documentElement : (container as HTMLElement)
const onScroll = throttle(handleScroll.bind(null, el, cb), delay)
if (!container) return
el[SCOPE] = {
instance,
container,
containerEl,
delay,
cb,
onScroll,
lastScrollTop: containerEl.scrollTop
}
if (immediate) {
const observer = new MutationObserver(throttle(checkFull.bind(null, el, cb), CHECK_INTERVAL))
el[SCOPE].observer = observer
observer.observe(el, { childList: true, subtree: true })
checkFull(el, cb)
}
container.addEventListener('scroll', onScroll)
},
unmounted(el: any) {
if (!el[SCOPE]) return
const { container, onScroll } = el[SCOPE]
container?.removeEventListener('scroll', onScroll)
destroyObserver(el)
},
async updated(el: any) {
if (!el[SCOPE]) {
await nextTick()
} else {
const { containerEl, cb, observer } = el[SCOPE]
if (containerEl.clientHeight && observer) {
checkFull(el, cb)
}
}
}
}
export default {
install: (app: App) => {
app.directive('infinite-scroll-up', InfiniteScroll)
}
}

View File

@ -5,7 +5,7 @@ const settingRouter = {
path: '/setting',
name: 'setting',
meta: { icon: 'Setting', title: '系统设置', permission: 'SETTING:READ' },
redirect: (to: any) => {
redirect: () => {
if (hasPermission(new Role('ADMIN'), 'AND')) {
return '/user'
}

View File

@ -10,16 +10,7 @@ export const routes: Array<RouteRecordRaw> = [
name: 'home',
component: () => import('@/layout/app-layout/index.vue'),
redirect: '/application',
children: [
// TODO 待处理
// {
// path: '/first',
// name: 'first',
// meta: { icon: 'House', title: '首页' },
// component: () => import('@/views/first/index.vue')
// },
...rolesRoutes
]
children: [...rolesRoutes]
},
{

View File

@ -18,6 +18,36 @@ const useLogStore = defineStore({
reject(error)
})
})
},
async asyncChatRecordLog(
id: string,
chatId: string,
page: pageRequest,
loading?: Ref<boolean>,
order_asc?: boolean
) {
return new Promise((resolve, reject) => {
logApi
.getChatRecordLog(id, chatId, page, loading, order_asc)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncGetChatLogClient(id: string, page: pageRequest, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
logApi
.getChatLogClient(id, page, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
}
}
})

View File

@ -184,6 +184,9 @@ h4 {
.p-24 {
padding: calc(var(--app-base-px) * 3);
}
.p-16-24 {
padding: calc(var(--app-base-px) * 2) calc(var(--app-base-px) * 3);
}
.pt-0 {
padding-top: 0;

View File

@ -1,6 +1,4 @@
import { h } from 'vue'
import { ElMessageBox, ElMessage, ElIcon } from 'element-plus'
import { WarningFilled } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
export const MsgSuccess = (message: string) => {
ElMessage.success({
@ -52,21 +50,3 @@ export const MsgConfirm = (title: string, description: string, options?: any) =>
}
return ElMessageBox.confirm(description, title, defaultOptions)
}
// export const MsgConfirm = ({ title, description }: any, options?: any) => {
// const message: any = h('div', { class: 'app-confirm' }, [
// h('h4', { class: 'app-confirm-title flex align-center' }, [
// h(ElIcon, { class: 'icon' }, [h(WarningFilled)]),
// h('span', { class: 'ml-16' }, title)
// ]),
// h('div', { class: 'app-confirm-description mt-8' }, description)
// ])
// const defaultOptions: Object = {
// showCancelButton: true,
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// ...options
// }
// return ElMessageBox({ message, ...defaultOptions })
// }

View File

@ -92,7 +92,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, computed } from 'vue'
import { ref, onMounted, reactive } from 'vue'
import applicationApi from '@/api/application'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { isAppIcon } from '@/utils/application'
@ -122,7 +122,7 @@ function searchHandle() {
}
function getAccessToken(id: string) {
application.asyncGetAccessToken(id, loading).then((res: any) => {
window.open(application.location + res?.data?.access_token)
window.open(application.location + res?.data?.access_token + '?mode=pc')
})
}

View File

@ -0,0 +1,106 @@
<template>
<div class="chat" v-loading="loading">
<div class="chat__header">
<div class="chat-width">
<h2 class="ml-24">{{ applicationDetail?.name }}</h2>
</div>
</div>
<div class="chat__main chat-width">
<AiChat
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
></AiChat>
</div>
<div class="chat__footer"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application'
import useStore from '@/stores'
const route = useRoute()
const {
params: { accessToken }
} = route as any
const { application, user } = useStore()
const loading = ref(false)
const applicationDetail = ref<any>({})
const applicationAvailable = ref<boolean>(true)
function getAccessToken(token: string) {
application
.asyncAppAuthentication(token, loading)
.then(() => {
getProfile()
})
.catch(() => {
applicationAvailable.value = false
})
}
function getProfile() {
applicationApi
.getProfile(loading)
.then((res) => {
applicationDetail.value = res.data
})
.catch(() => {
applicationAvailable.value = false
})
}
onMounted(() => {
user.changeUserType(2)
getAccessToken(accessToken)
})
</script>
<style lang="scss">
.chat {
background-color: var(--app-layout-bg-color);
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 24px);
height: calc(100vh - var(--app-header-height) - 24px);
overflow: hidden;
}
&__footer {
background: #f3f7f9;
height: 80px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
border-radius: 8px !important;
&:before {
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
content: '';
position: absolute;
width: 100%;
top: -16px;
left: 0;
height: 16px;
}
}
.chat-width {
max-width: var(--app-chat-width, 860px);
margin: 0 auto;
}
}
</style>

View File

@ -0,0 +1,285 @@
<template>
<div class="chat-embed" v-loading="loading" @click="closePopover($event)">
<div class="chat-embed__header">
<div class="chat-width">
<h4 class="ml-24">{{ applicationDetail?.name }}</h4>
</div>
</div>
<div class="chat-embed__main chat-width">
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
></AiChat>
</div>
<el-button type="primary" link class="new-chat-button" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4"></span>
</el-button>
<!-- 历史记录弹出层 -->
<div @click.prevent.stop="show = !show" class="chat-popover-button cursor color-secondary">
<AppIcon iconName="app-history-outlined"></AppIcon>
</div>
<el-collapse-transition>
<div v-show="show" class="chat-popover w-full" id="chat-popover">
<div class="border-b p-16-24">
<span>历史记录</span>
</div>
<el-scrollbar>
<div class="p-8">
<common-list
:data="chatLogeData"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
>
<template #default="{ row }">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
</div>
</el-collapse-transition>
<div class="chat-popover-mask" v-show="show"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application'
import useStore from '@/stores'
const route = useRoute()
const {
params: { accessToken }
} = route as any
const { application, user, log } = useStore()
const AiChatRef = ref()
const loading = ref(false)
const left_loading = ref(false)
const applicationDetail = ref<any>({})
const applicationAvailable = ref<boolean>(true)
const chatLogeData = ref<any[]>([])
const show = ref(false)
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const currentRecordList = ref<any>([])
const currentChatId = ref('new') // Id 'new'
function handleScroll(event: any) {
if (
currentChatId.value !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.total > currentRecordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
function closePopover(event: any) {
const popover = document.getElementById('chat-popover')
if (popover) {
if (!popover.contains(event.target)) {
show.value = false
}
}
}
function newChat() {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = 'new'
}
function getAccessToken(token: string) {
application
.asyncAppAuthentication(token, loading)
.then(() => {
getProfile()
})
.catch(() => {
applicationAvailable.value = false
})
}
function getProfile() {
applicationApi
.getProfile(loading)
.then((res) => {
applicationDetail.value = res.data
getChatLog(applicationDetail.value.id)
})
.catch(() => {
applicationAvailable.value = false
})
}
function getChatLog(id: string) {
const page = {
current_page: 1,
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogeData.value = res.data.records
})
}
function getChatRecord() {
return log
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,
paginationConfig,
loading,
false
)
.then((res: any) => {
paginationConfig.total = res.data.total
const list = res.data.records
list.map((v: any) => {
v['write_ed'] = true
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
const clickListHandle = (item: any) => {
if (item.id !== currentChatId.value) {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = item.id
if (currentChatId.value !== 'new') {
getChatRecord()
}
show.value = false
}
}
function refresh(id: string) {
getChatLog(applicationDetail.value.id)
currentChatId.value = id
}
onMounted(() => {
user.changeUserType(2)
getAccessToken(accessToken)
})
</script>
<style lang="scss">
.chat-embed {
background-color: var(--app-layout-bg-color);
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 24px);
height: calc(100vh - var(--app-header-height) - 24px);
overflow: hidden;
}
.new-chat-button {
position: absolute;
bottom: 84px;
left: 18px;
z-index: 11;
}
//
.chat-popover {
position: absolute;
top: var(--app-header-height);
background: #ffffff;
padding-bottom: 24px;
z-index: 2009;
}
.chat-popover-button {
z-index: 2009;
position: absolute;
top: 16px;
right: 85px;
font-size: 22px;
}
.chat-popover-mask {
background-color: var(--el-overlay-color-lighter);
bottom: 0;
height: 100%;
left: 0;
overflow: auto;
position: fixed;
right: 0;
top: var(--app-header-height);
z-index: 2008;
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.chat-width {
max-width: var(--app-chat-width, 860px);
margin: 0 auto;
}
}
</style>

View File

@ -1,144 +1,23 @@
<template>
<div class="chat" v-loading="loading">
<div class="chat__header">
<div class="chat-width">
<h2 class="ml-24">{{ applicationDetail?.name }}</h2>
</div>
</div>
<div class="chat__main chat-width">
<AiChat
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
></AiChat>
</div>
<div class="chat__footer"></div>
</div>
<component :is="currentTemplate" :key="route.fullPath" />
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import applicationApi from '@/api/application'
import useStore from '@/stores'
import { onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
const components: any = import.meta.glob('@/views/chat/**/index.vue', {
eager: true
})
const route = useRoute()
const {
params: { accessToken }
query: { mode }
} = route as any
const { application, user, log } = useStore()
const loading = ref(false)
const applicationDetail = ref<any>({})
const applicationAvailable = ref<boolean>(true)
const chatLogeData = ref<any[]>([])
function getAccessToken(token: string) {
application
.asyncAppAuthentication(token, loading)
.then((res) => {
getProfile()
})
.catch(() => {
applicationAvailable.value = false
})
}
function getProfile() {
applicationApi
.getProfile(loading)
.then((res) => {
applicationDetail.value = res.data
})
.catch(() => {
applicationAvailable.value = false
})
}
function getChatLog(id: string) {
const page = {
current_page: 1,
page_size: 20
}
const param = {
history_day: 183
}
log.asyncGetChatLog(id, page, param, loading).then((res: any) => {
chatLogeData.value = res.data.records
})
}
onMounted(() => {
user.changeUserType(2)
getAccessToken(accessToken)
const currentTemplate = computed(() => {
const name = `/src/views/chat/${mode || 'pc'}/index.vue`
return components[name].default
})
</script>
<style lang="scss">
.chat {
background-color: var(--app-layout-bg-color);
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 24px);
height: calc(100vh - var(--app-header-height) - 24px);
overflow: hidden;
}
&__footer {
background: #f3f7f9;
height: 80px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
border-radius: 8px !important;
&:before {
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
content: '';
position: absolute;
width: 100%;
top: -16px;
left: 0;
height: 16px;
}
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.chat-width {
max-width: var(--app-chat-width, 860px);
margin: 0 auto;
}
}
</style>
onMounted(() => {})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,279 @@
<template>
<div class="chat-pc" v-loading="loading">
<div class="chat-pc__header">
<h4 class="ml-24">{{ applicationDetail?.name }}</h4>
</div>
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<el-button class="add-button w-full primary" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4"></span>
</el-button>
<p class="mt-20 mb-8">历史记录</p>
</div>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:data="chatLogeData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
>
<template #default="{ row }">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">暂无历史记录</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogeData.length" class="gradient-divider lighter mt-8">
<span>仅显示最近 20 条对话</span>
</div>
</el-scrollbar>
</div>
</div>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4>{{ currentChatName }}</h4>
<span v-if="currentRecordList.length" class="flex align-center">
<AppIcon iconName="app-chat-record" class="info mr-8" style="font-size: 16px"></AppIcon>
<span class="lighter"> {{ paginationConfig.total }} 条提问 </span>
</span>
</div>
<div class="right-height">
<!-- 对话 -->
<AiChat
ref="AiChatRef"
v-model:data="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
></AiChat>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application'
import useStore from '@/stores'
const route = useRoute()
const {
params: { accessToken }
} = route as any
const { application, user, log } = useStore()
const newObj = {
id: 'new',
abstract: '新建对话'
}
const AiChatRef = ref()
const loading = ref(false)
const left_loading = ref(false)
const applicationDetail = ref<any>({})
const applicationAvailable = ref<boolean>(true)
const chatLogeData = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const currentRecordList = ref<any>([])
const currentChatId = ref('new') // Id 'new'
const currentChatName = ref('新建对话')
function handleScroll(event: any) {
if (
currentChatId.value !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.total > currentRecordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
function getAccessToken(token: string) {
application
.asyncAppAuthentication(token, loading)
.then(() => {
getProfile()
})
.catch(() => {
applicationAvailable.value = false
})
}
function getProfile() {
applicationApi
.getProfile(loading)
.then((res) => {
applicationDetail.value = res.data
getChatLog(applicationDetail.value.id)
})
.catch(() => {
applicationAvailable.value = false
})
}
function newChat() {
if (!chatLogeData.value.some((v) => v.id === 'new')) {
paginationConfig.current_page = 1
currentRecordList.value = []
chatLogeData.value.unshift(newObj)
} else {
paginationConfig.current_page = 1
currentRecordList.value = []
}
currentChatId.value = 'new'
currentChatName.value = '新建对话'
}
function getChatLog(id: string) {
const page = {
current_page: 1,
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogeData.value = res.data.records
})
}
function getChatRecord() {
return log
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,
paginationConfig,
loading,
false
)
.then((res: any) => {
paginationConfig.total = res.data.total
const list = res.data.records
list.map((v: any) => {
v['write_ed'] = true
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
const clickListHandle = (item: any) => {
if (item.id !== currentChatId.value) {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = item.id
currentChatName.value = item.abstract
if (currentChatId.value !== 'new') {
getChatRecord()
}
}
}
function refresh(id: string) {
getChatLog(applicationDetail.value.id)
currentChatId.value = id
}
onMounted(() => {
user.changeUserType(2)
getAccessToken(accessToken)
})
</script>
<style lang="scss">
.chat-pc {
background-color: var(--app-layout-bg-color);
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__left {
padding-top: calc(var(--app-header-height) - 8px);
background: #ffffff;
width: 280px;
.add-button {
border: 1px solid var(--el-color-primary);
}
.left-height {
height: calc(100vh - var(--app-header-height) - 135px);
}
}
&__right {
width: calc(100% - 280px);
padding-top: calc(var(--app-header-height));
overflow: hidden;
position: relative;
.right-header {
background: #ffffff;
}
.right-height {
height: calc(100vh - var(--app-header-height) * 2 - 24px);
overflow: scroll;
}
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.chat-width {
max-width: var(--app-chat-width, 860px);
margin: 0 auto;
}
}
</style>

View File

@ -35,8 +35,6 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { MsgSuccess } from '@/utils/message'
import useStore from '@/stores'
const { dataset } = useStore()

View File

@ -83,7 +83,7 @@
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import type { FormInstance } from 'element-plus'
import documentApi from '@/api/document'
import { MsgSuccess } from '@/utils/message'
import { hitHandlingMethod } from '../utils'
@ -156,14 +156,14 @@ const open = (row: any, list: Array<string>) => {
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
await formEl.validate((valid) => {
if (valid) {
if (isImport.value) {
const obj = {
source_url_list: form.value.source_url.split('\n'),
selector: form.value.selector
}
documentApi.postWebDocument(id, obj, loading).then((res: any) => {
documentApi.postWebDocument(id, obj, loading).then(() => {
MsgSuccess('导入成功')
emit('refresh')
dialogVisible.value = false
@ -178,7 +178,7 @@ const submit = async (formEl: FormInstance | undefined) => {
selector: form.value.selector
}
}
documentApi.putDocument(id, documentId.value, obj, loading).then((res) => {
documentApi.putDocument(id, documentId.value, obj, loading).then(() => {
MsgSuccess('设置成功')
emit('refresh')
dialogVisible.value = false
@ -190,7 +190,7 @@ const submit = async (formEl: FormInstance | undefined) => {
directly_return_similarity: form.value.directly_return_similarity || 0.9,
id_list: documentList.value
}
documentApi.batchEditHitHandling(id, obj, loading).then((res: any) => {
documentApi.batchEditHitHandling(id, obj, loading).then(() => {
MsgSuccess('设置成功')
emit('refresh')
dialogVisible.value = false

View File

@ -243,11 +243,11 @@ const { common, dataset, document } = useStore()
const storeKey = 'documents'
onBeforeRouteUpdate((to: any, from: any) => {
onBeforeRouteUpdate(() => {
common.savePage(storeKey, null)
common.saveCondition(storeKey, null)
})
onBeforeRouteLeave((to: any, from: any) => {
onBeforeRouteLeave((to: any) => {
if (to.name !== 'Paragraph') {
common.savePage(storeKey, null)
common.saveCondition(storeKey, null)
@ -351,7 +351,7 @@ function refreshDocument(row: any) {
confirmButtonClass: 'danger'
})
.then(() => {
documentApi.putDocumentRefresh(row.dataset_id, row.id).then((res) => {
documentApi.putDocumentRefresh(row.dataset_id, row.id).then(() => {
getList()
})
})
@ -365,7 +365,7 @@ function refreshDocument(row: any) {
.catch(() => {})
}
} else {
documentApi.putDocumentRefresh(row.dataset_id, row.id).then((res) => {
documentApi.putDocumentRefresh(row.dataset_id, row.id).then(() => {
getList()
})
}

View File

@ -29,7 +29,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ref, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import logApi from '@/api/log'
import { type chatType } from '@/api/type/application'
@ -43,7 +43,7 @@ const props = withDefaults(
/**
* 对话 记录id
*/
chartId: string
chatId: string
currentAbstract: string
/**
* 下一条
@ -61,7 +61,7 @@ const props = withDefaults(
{}
)
const emit = defineEmits(['update:chartId', 'update:currentAbstract', 'refresh'])
const emit = defineEmits(['update:chatId', 'update:currentAbstract', 'refresh'])
const route = useRoute()
const {
@ -84,8 +84,8 @@ function closeHandle() {
}
function getChatRecord() {
if (props.chartId && visible.value) {
logApi.getChatRecordLog(id as string, props.chartId, paginationConfig, loading).then((res) => {
if (props.chatId && visible.value) {
logApi.getChatRecordLog(id as string, props.chatId, paginationConfig, loading).then((res) => {
paginationConfig.total = res.data.total
recordList.value = [...recordList.value, ...res.data.records]
})
@ -93,7 +93,7 @@ function getChatRecord() {
}
watch(
() => props.chartId,
() => props.chatId,
() => {
recordList.value = []
paginationConfig.total = 0
@ -104,7 +104,7 @@ watch(
watch(visible, (bool) => {
if (!bool) {
emit('update:chartId', '')
emit('update:chatId', '')
emit('update:currentAbstract', '')
emit('refresh')
}

View File

@ -92,13 +92,6 @@ import useStore from '@/stores'
const { application, document } = useStore()
const props = defineProps({
chartId: {
type: String,
default: ''
}
})
const route = useRoute()
const {
params: { id }
@ -147,7 +140,6 @@ watch(dialogVisible, (bool) => {
}
})
function changeDataset(id: string) {
form.value.document_id = ''
getDocument(id)
@ -176,7 +168,7 @@ const open = (data: any) => {
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
await formEl.validate((valid) => {
if (valid) {
const obj = {
title: form.value.title,

View File

@ -121,7 +121,7 @@
:next="nextChatRecord"
:pre="preChatRecord"
ref="ChatRecordRef"
v-model:chartId="currentChatId"
v-model:chatId="currentChatId"
v-model:currentAbstract="currentAbstract"
:application="detail"
:pre_disable="pre_disable"
@ -131,11 +131,11 @@
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, watch, computed } from 'vue'
import { ref, onMounted, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import ChatRecordDrawer from './component/ChatRecordDrawer.vue'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import logApi from '@/api/log'
import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
@ -253,7 +253,7 @@ const preChatRecord = () => {
return
}
paginationConfig.current_page = paginationConfig.current_page - 1
getList().then((ok) => {
getList().then(() => {
index = paginationConfig.page_size - 1
currentChatId.value = tableData.value[index].id
currentAbstract.value = tableData.value[index].abstract

View File

@ -12,16 +12,6 @@
<span class="lighter" v-else>{{ form.title || '-' }}</span>
</el-form-item>
<el-form-item label="分段内容" prop="content">
<!-- <el-input
v-if="isEdit"
v-model="form.content"
placeholder="请输入分段内容"
maxlength="4096"
show-word-limit
:rows="8"
type="textarea"
>
</el-input>-->
<MarkdownEditor
v-if="isEdit"
v-model="form.content"
@ -39,12 +29,11 @@
:modelValue="form.content"
class="maxkb-md"
/>
<!-- <span v-else class="break-all lighter">{{ form.content }}</span> -->
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted, watch, nextTick } from 'vue'
import { ref, reactive, onUnmounted, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { MdPreview } from 'md-editor-v3'
import MarkdownEditor from '@/components/markdown-editor/index.vue'

View File

@ -3,7 +3,13 @@
<div class="template-manage flex main-calc-height">
<div class="template-manage__left p-8 border-r">
<h4 class="p-16" style="padding-bottom: 8px">供应商</h4>
<common-list :data="provider_list" v-loading="loading" @click="clickListHandle">
<common-list
:data="provider_list"
v-loading="loading"
@click="clickListHandle"
value-key="provider"
default-active=""
>
<template #default="{ row, index }">
<div class="flex" v-if="index === 0">
<AppIcon