@ -0,0 +1,9 @@ |
|||||
|
<component name="libraryTable"> |
||||
|
<library name="contour_plot"> |
||||
|
<CLASSES> |
||||
|
<root url="jar://$PROJECT_DIR$/vue-fastapi-frontend/node_modules/contour_plot/build/contour_plot.zip!/" /> |
||||
|
</CLASSES> |
||||
|
<JAVADOC /> |
||||
|
<SOURCES /> |
||||
|
</library> |
||||
|
</component> |
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="JavaScriptSettings"> |
||||
|
<option name="languageLevel" value="FLOW" /> |
||||
|
</component> |
||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" project-jdk-name="Python 3.11" project-jdk-type="Python SDK"> |
||||
|
<output url="file://$PROJECT_DIR$/out" /> |
||||
|
</component> |
||||
|
</project> |
@ -0,0 +1,8 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="ProjectModuleManager"> |
||||
|
<modules> |
||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/vue-fastapi-admin.iml" filepath="$PROJECT_DIR$/.idea/vue-fastapi-admin.iml" /> |
||||
|
</modules> |
||||
|
</component> |
||||
|
</project> |
@ -0,0 +1,6 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project version="4"> |
||||
|
<component name="VcsDirectoryMappings"> |
||||
|
<mapping directory="" vcs="Git" /> |
||||
|
</component> |
||||
|
</project> |
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<module type="JAVA_MODULE" version="4"> |
||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true"> |
||||
|
<exclude-output /> |
||||
|
<content url="file://$MODULE_DIR$" /> |
||||
|
<orderEntry type="inheritedJdk" /> |
||||
|
<orderEntry type="sourceFolder" forTests="false" /> |
||||
|
</component> |
||||
|
</module> |
@ -0,0 +1,113 @@ |
|||||
|
import json |
||||
|
import os |
||||
|
import shutil |
||||
|
from fastapi import APIRouter, Depends, Request, UploadFile, File, Form |
||||
|
from sqlalchemy.ext.asyncio import AsyncSession |
||||
|
from config.get_db import get_db |
||||
|
from module_admin.entity.vo.user_vo import CurrentUserModel |
||||
|
from module_admin.entity.vo.aichat_vo import * |
||||
|
from module_admin.service.login_service import LoginService |
||||
|
from module_admin.service.aichat_service import AiChatService |
||||
|
from utils.log_util import logger |
||||
|
from utils.response_util import ResponseUtil |
||||
|
from config.env import MinioConfig |
||||
|
from datetime import datetime |
||||
|
from utils.common_util import bytes2file_response |
||||
|
from utils.minio_util import Bucket |
||||
|
|
||||
|
aichatController = APIRouter(prefix='/aichat', dependencies=[Depends(LoginService.get_current_user)]) |
||||
|
|
||||
|
|
||||
|
@aichatController.get("/session/list/{sessionId}") |
||||
|
async def get_chat_session_list(request: Request, |
||||
|
sessionId: str, |
||||
|
query_db: AsyncSession = Depends(get_db), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
ai_session_list_result = await AiChatService.get_ai_session_list_services(query_db, sessionId, current_user) |
||||
|
logger.info('获取成功') |
||||
|
return ResponseUtil.success(data=ai_session_list_result) |
||||
|
|
||||
|
|
||||
|
@aichatController.get("/chat/list/{sessionId}") |
||||
|
async def get_chat_list(request: Request, sessionId: str, query_db: AsyncSession = Depends(get_db), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
ai_chat_list_result = await AiChatService.get_ai_chat_list_services(query_db, sessionId, current_user) |
||||
|
logger.info('获取成功') |
||||
|
return ResponseUtil.success(data=ai_chat_list_result) |
||||
|
|
||||
|
|
||||
|
@aichatController.post("/delete/session/{sessionId}") |
||||
|
async def delete_chat_session(request: Request, sessionId: str, query_db: AsyncSession = Depends(get_db), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
delete_chat_session_result = await AiChatService.delete_chat_session(query_db, sessionId, current_user) |
||||
|
logger.info(delete_chat_session_result.message) |
||||
|
return ResponseUtil.success(msg=delete_chat_session_result.message) |
||||
|
|
||||
|
|
||||
|
@aichatController.post("/add") |
||||
|
async def add_chat(request: Request, add_chat: AiChatModel, query_db: AsyncSession = Depends(get_db), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
operate_result = await AiChatService.add_chat(query_db, add_chat, current_user) |
||||
|
logger.info(operate_result.message) |
||||
|
return ResponseUtil.success(msg=operate_result.message) |
||||
|
|
||||
|
|
||||
|
@aichatController.post("/update") |
||||
|
async def update_chat(request: Request, update_chat: AiChatModel, query_db: AsyncSession = Depends(get_db), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
operate_result = await AiChatService.update_chat(query_db, update_chat) |
||||
|
logger.info(operate_result.message) |
||||
|
return ResponseUtil.success(msg=operate_result.message) |
||||
|
|
||||
|
|
||||
|
@aichatController.post("/upload") |
||||
|
async def upload_file(request: Request, sessionId: str = Form(), file: UploadFile = File(...), |
||||
|
current_user: CurrentUserModel = Depends(LoginService.get_current_user)): |
||||
|
file_extension = os.path.splitext(file.filename)[1] # 文件后缀 |
||||
|
file_prefix = os.path.splitext(file.filename)[0] # 文件前缀 |
||||
|
bucket = Bucket( |
||||
|
minio_address=MinioConfig.minio_address, |
||||
|
minio_admin=MinioConfig.minio_admin, |
||||
|
minio_password=MinioConfig.minio_password |
||||
|
) |
||||
|
file_id = datetime.now().strftime('%Y%m%d%H%M%S') |
||||
|
file_location = file_prefix + "_" + file_id + file_extension |
||||
|
with open(file_location, "wb") as buffer: |
||||
|
shutil.copyfileobj(file.file, buffer) |
||||
|
|
||||
|
# Remove the temporary file after upload (optional) |
||||
|
def remove_file(file_path): |
||||
|
os.remove(file_path) |
||||
|
|
||||
|
# Upload to MinIO |
||||
|
bucket.upload_file_to_bucket(current_user.user.user_name, '/' + sessionId + '/' + file_location, file_location) |
||||
|
|
||||
|
# Clean up |
||||
|
remove_file(file_location) |
||||
|
return ResponseUtil.success(data={"is_success": True, "message": "上传成功", "file": file_location, |
||||
|
"bucket": current_user.user.user_name}, |
||||
|
msg="上传成功") |
||||
|
|
||||
|
|
||||
|
@aichatController.post("/file/download") |
||||
|
async def download_file(request: Request, download_file: DownloadFile = Form()): |
||||
|
file_name = download_file.file |
||||
|
bucket_name = download_file.bucket |
||||
|
sessionId = download_file.sessionId |
||||
|
bucket = Bucket( |
||||
|
minio_address=MinioConfig.minio_address, |
||||
|
minio_admin=MinioConfig.minio_admin, |
||||
|
minio_password=MinioConfig.minio_password |
||||
|
) |
||||
|
|
||||
|
# Remove the temporary file after upload (optional) |
||||
|
def remove_file(file_path): |
||||
|
os.remove(file_path) |
||||
|
|
||||
|
# Upload to MinIO |
||||
|
bucket.download_file_from_bucket(bucket_name, '/' + sessionId + '/' + file_name, file_name) |
||||
|
with open(file_name, "rb") as buffer: |
||||
|
file_data = buffer.read() |
||||
|
# Clean up |
||||
|
remove_file(file_name) |
||||
|
return ResponseUtil.streaming(data=bytes2file_response(file_data)) |
@ -0,0 +1,64 @@ |
|||||
|
from sqlalchemy import desc, delete, func, select, update |
||||
|
from sqlalchemy.ext.asyncio import AsyncSession |
||||
|
from module_admin.entity.do.aichat_do import AiChatHistory |
||||
|
from module_admin.entity.do.aichat_do import AiChatSession |
||||
|
from module_admin.entity.vo.aichat_vo import AiChatModel |
||||
|
|
||||
|
|
||||
|
class AiChatDao: |
||||
|
""" |
||||
|
菜单管理模块数据库操作层 |
||||
|
""" |
||||
|
|
||||
|
@classmethod |
||||
|
async def get_ai_session_list(cls, db: AsyncSession, sessionId: str, user_id: int): |
||||
|
session_list = ( |
||||
|
await db.execute( |
||||
|
select(AiChatSession).where(AiChatSession.user == user_id, AiChatSession.sessionId != sessionId) |
||||
|
.order_by(desc(AiChatSession.time)).limit(20) |
||||
|
) |
||||
|
).scalars().all() |
||||
|
return session_list |
||||
|
|
||||
|
@classmethod |
||||
|
async def get_ai_chat_list(cls, db: AsyncSession, sessionId: str, user_id: int): |
||||
|
chat_list = ( |
||||
|
await db.execute( |
||||
|
select(AiChatHistory).where(AiChatHistory.user == user_id, AiChatHistory.sessionId == sessionId) |
||||
|
.order_by(AiChatHistory.time) |
||||
|
) |
||||
|
).scalars().all() |
||||
|
return chat_list |
||||
|
|
||||
|
@classmethod |
||||
|
async def get_ai_chat_by_id(cls, sessionId: str, db: AsyncSession, user_id: int): |
||||
|
chat_list = (await db.execute(select(AiChatSession) |
||||
|
.where(AiChatSession.user == user_id, |
||||
|
AiChatSession.sessionId == sessionId))).scalars().first() |
||||
|
return chat_list |
||||
|
|
||||
|
@classmethod |
||||
|
async def add_ai_chat_session(cls, sessionId: str, sessionName: str, time: str, db: AsyncSession, user_id: int): |
||||
|
chat_session = AiChatSession() |
||||
|
chat_session.sessionId = sessionId |
||||
|
chat_session.sessionName = sessionName |
||||
|
chat_session.time = time |
||||
|
chat_session.user = user_id |
||||
|
db.add(chat_session) |
||||
|
db.flush() |
||||
|
return chat_session |
||||
|
|
||||
|
@classmethod |
||||
|
async def add_ai_chat_history(cls, chat: AiChatHistory, db: AsyncSession): |
||||
|
db.add(chat) |
||||
|
db.flush() |
||||
|
return chat |
||||
|
|
||||
|
@classmethod |
||||
|
async def delete_chat_session(cls, db: AsyncSession, sessionId: str, user_id: int): |
||||
|
await db.execute(delete(AiChatHistory).where(AiChatHistory.sessionId == sessionId)) |
||||
|
await db.execute(delete(AiChatSession).where(AiChatSession.sessionId == sessionId)) |
||||
|
|
||||
|
@classmethod |
||||
|
async def update_ai_chat_history(cls, update_chat: AiChatModel, db: AsyncSession): |
||||
|
await db.execute(update(AiChatHistory), [dict(update_chat)]) |
@ -0,0 +1,36 @@ |
|||||
|
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text |
||||
|
from config.database import Base |
||||
|
from datetime import datetime |
||||
|
|
||||
|
|
||||
|
class AiChatHistory(Base): |
||||
|
""" |
||||
|
菜单权限表 |
||||
|
""" |
||||
|
__tablename__ = 'ai_chat_history' |
||||
|
|
||||
|
chatId = Column(String(50), primary_key=True, comment='问答id') |
||||
|
sessionId = Column(String(50), default='', comment='聊天记录id') |
||||
|
sessionName = Column(String(50), default='', comment='聊天记录标题') |
||||
|
type = Column(String(50), default='', comment='类型,问题 or 回答') |
||||
|
isEnd = Column(Boolean, default=None, comment='是否结束') |
||||
|
isStop = Column(Boolean, default=None, comment='是否停止') |
||||
|
user = Column(Integer, default=None, comment='所属用户') |
||||
|
time = Column(String(50), default=None, comment='问答时间') |
||||
|
content = Column(Text, default=None, comment='问答内容') |
||||
|
operate = Column(String(50), default=None, comment='点赞,差评等操作') |
||||
|
thumbDownReason = Column(String(255), default=None, comment='差评原因') |
||||
|
file = Column(String(255), default=None, comment='文件id集合') |
||||
|
|
||||
|
|
||||
|
class AiChatSession(Base): |
||||
|
""" |
||||
|
菜单权限表 |
||||
|
""" |
||||
|
__tablename__ = 'ai_chat_session' |
||||
|
|
||||
|
sessionId = Column(String(50), primary_key=True, comment='聊天记录id') |
||||
|
sessionName = Column(String(50), default='', comment='聊天记录标题') |
||||
|
user = Column(Integer, default=None, comment='所属用户') |
||||
|
time = Column(String(50), default=None, comment='问答时间') |
||||
|
|
@ -0,0 +1,41 @@ |
|||||
|
from pydantic import BaseModel |
||||
|
from typing import Union, Optional, List |
||||
|
|
||||
|
|
||||
|
class CrudChatModel(BaseModel): |
||||
|
is_success: bool |
||||
|
message: str |
||||
|
|
||||
|
|
||||
|
class AiChatModel(BaseModel): |
||||
|
""" |
||||
|
菜单表对应pydantic模型 |
||||
|
""" |
||||
|
chatId: Optional[str] = None |
||||
|
sessionId: Optional[str] = None |
||||
|
sessionName: Optional[str] = None |
||||
|
type: Optional[str] = None |
||||
|
isEnd: Optional[bool] = None |
||||
|
isStop: Optional[bool] = None |
||||
|
user: Optional[int] = None |
||||
|
time: Optional[str] = None |
||||
|
content: Optional[str] = None |
||||
|
operate: Optional[str] = None |
||||
|
thumbDownReason: Optional[str] = None |
||||
|
file: Optional[str] = None |
||||
|
|
||||
|
|
||||
|
class ThumbOperateModel(BaseModel): |
||||
|
chatId: str |
||||
|
operate: str |
||||
|
thumbDownReason: str |
||||
|
|
||||
|
|
||||
|
class DownloadFile(BaseModel): |
||||
|
file: str |
||||
|
bucket: str |
||||
|
sessionId: str |
||||
|
|
||||
|
|
||||
|
class AiChatListModel(BaseModel): |
||||
|
chats: List[Union[AiChatModel, None]] |
@ -0,0 +1,59 @@ |
|||||
|
import uuid |
||||
|
|
||||
|
from typing import Optional |
||||
|
from sqlalchemy.ext.asyncio import AsyncSession |
||||
|
from module_admin.entity.vo.aichat_vo import * |
||||
|
from module_admin.entity.vo.common_vo import CrudResponseModel |
||||
|
from module_admin.dao.aichat_dao import * |
||||
|
from module_admin.entity.vo.user_vo import CurrentUserModel |
||||
|
from datetime import datetime |
||||
|
from utils.common_util import CamelCaseUtil |
||||
|
|
||||
|
|
||||
|
class AiChatService: |
||||
|
""" |
||||
|
智能问答服务层 |
||||
|
""" |
||||
|
|
||||
|
@classmethod |
||||
|
async def get_ai_session_list_services(cls, result_db: AsyncSession, sessionId: str, |
||||
|
current_user: Optional[CurrentUserModel] = None): |
||||
|
ai_session_list = await AiChatDao.get_ai_session_list(result_db, sessionId, current_user.user.user_id) # 查询最新的20条 |
||||
|
return ai_session_list |
||||
|
|
||||
|
@classmethod |
||||
|
async def get_ai_chat_list_services(cls, result_db: AsyncSession, sessionId: str, |
||||
|
current_user: Optional[CurrentUserModel] = None): |
||||
|
ai_session_list = await AiChatDao.get_ai_chat_list(result_db, sessionId, current_user.user.user_id) # 查询最新的20条 |
||||
|
return CamelCaseUtil.transform_result(ai_session_list) |
||||
|
|
||||
|
@classmethod |
||||
|
async def delete_chat_session(cls, result_db: AsyncSession, sessionId: str, |
||||
|
current_user: Optional[CurrentUserModel] = None): |
||||
|
await AiChatDao.delete_chat_session(result_db, sessionId, current_user.user.user_id) |
||||
|
await result_db.commit() |
||||
|
return CrudResponseModel(is_success=True, message='删除成功') |
||||
|
|
||||
|
@classmethod |
||||
|
async def add_chat(cls, result_db: AsyncSession, add_chat: AiChatModel, |
||||
|
current_user: Optional[CurrentUserModel] = None): |
||||
|
chat_session = await AiChatDao.get_ai_chat_by_id(add_chat.sessionId, result_db, current_user.user.user_id) |
||||
|
print(chat_session) |
||||
|
add_chat.user = current_user.user.user_id |
||||
|
if add_chat.time is None: |
||||
|
add_chat.time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
||||
|
if chat_session is None: |
||||
|
await AiChatDao.add_ai_chat_session(add_chat.sessionId, add_chat.sessionName, |
||||
|
add_chat.time, result_db, current_user.user.user_id) |
||||
|
await result_db.commit() |
||||
|
chat_history = AiChatHistory(**add_chat.dict()) |
||||
|
chat_history.chatId = uuid.uuid4() |
||||
|
await AiChatDao.add_ai_chat_history(chat_history, result_db) |
||||
|
await result_db.commit() |
||||
|
return CrudResponseModel(is_success=True, message='操作成功') |
||||
|
|
||||
|
@classmethod |
||||
|
async def update_chat(cls, result_db: AsyncSession, update_chat: AiChatModel): |
||||
|
await AiChatDao.update_ai_chat_history(update_chat, result_db) |
||||
|
await result_db.commit() |
||||
|
return CrudResponseModel(is_success=True, message='操作成功') |
@ -0,0 +1,177 @@ |
|||||
|
from minio import Minio, InvalidResponseError, S3Error |
||||
|
|
||||
|
|
||||
|
# MinIO使用bucket(桶)来组织对象。 |
||||
|
# bucket类似于文件夹或目录,其中每个bucket可以容纳任意数量的对象。 |
||||
|
class Bucket: |
||||
|
|
||||
|
def __init__(self, minio_address, minio_admin, minio_password): |
||||
|
# 通过ip 账号 密码 连接minio server |
||||
|
# Http连接 将secure设置为False |
||||
|
self.minioClient = Minio(endpoint=minio_address, |
||||
|
access_key=minio_admin, |
||||
|
secret_key=minio_password, |
||||
|
secure=False) |
||||
|
|
||||
|
def create_one_bucket(self, bucket_name): |
||||
|
# 创建桶(调用make_bucket api来创建一个桶) |
||||
|
""" |
||||
|
桶命名规则:小写字母,句点,连字符和数字 允许使用 长度至少3个字符 |
||||
|
使用大写字母、下划线等会报错 |
||||
|
""" |
||||
|
try: |
||||
|
# bucket_exists:检查桶是否存在 |
||||
|
if self.minioClient.bucket_exists(bucket_name=bucket_name): |
||||
|
print("该存储桶已经存在") |
||||
|
else: |
||||
|
self.minioClient.make_bucket(bucket_name=bucket_name) |
||||
|
print(f"{bucket_name}桶创建成功") |
||||
|
except InvalidResponseError as err: |
||||
|
print(err) |
||||
|
|
||||
|
def remove_one_bucket(self, bucket_name): |
||||
|
# 删除桶(调用remove_bucket api来创建一个存储桶) |
||||
|
try: |
||||
|
if self.minioClient.bucket_exists(bucket_name=bucket_name): |
||||
|
self.minioClient.remove_bucket(bucket_name) |
||||
|
print("删除存储桶成功") |
||||
|
else: |
||||
|
print("该存储桶不存在") |
||||
|
except InvalidResponseError as err: |
||||
|
print(err) |
||||
|
|
||||
|
def upload_file_to_bucket(self, bucket_name, file_name, file_path): |
||||
|
""" |
||||
|
将文件上传到bucket |
||||
|
:param bucket_name: minio桶名称 |
||||
|
:param file_name: 存放到minio桶中的文件名字(相当于对文件进行了重命名,可以与原文件名不同) |
||||
|
file_name处可以创建新的目录(文件夹) 例如 /example/file_name |
||||
|
相当于在该桶中新建了一个example文件夹 并把文件放在其中 |
||||
|
:param file_path: 本地文件的路径 |
||||
|
""" |
||||
|
# 桶是否存在 不存在则新建 |
||||
|
check_bucket = self.minioClient.bucket_exists(bucket_name) |
||||
|
if not check_bucket: |
||||
|
self.minioClient.make_bucket(bucket_name) |
||||
|
|
||||
|
try: |
||||
|
self.minioClient.fput_object(bucket_name=bucket_name, |
||||
|
object_name=file_name, |
||||
|
file_path=file_path) |
||||
|
except FileNotFoundError as err: |
||||
|
print('upload_failed: ' + str(err)) |
||||
|
except S3Error as err: |
||||
|
print("upload_failed:", err) |
||||
|
|
||||
|
def download_file_from_bucket(self, bucket_name, minio_file_path, download_file_path): |
||||
|
""" |
||||
|
从bucket下载文件 |
||||
|
:param bucket_name: minio桶名称 |
||||
|
:param minio_file_path: 存放在minio桶中文件名字 |
||||
|
file_name处可以包含目录(文件夹) 例如 /example/file_name |
||||
|
:param download_file_path: 文件获取后存放的路径 |
||||
|
""" |
||||
|
# 桶是否存在 |
||||
|
check_bucket = self.minioClient.bucket_exists(bucket_name) |
||||
|
if check_bucket: |
||||
|
try: |
||||
|
self.minioClient.fget_object(bucket_name=bucket_name, |
||||
|
object_name=minio_file_path, |
||||
|
file_path=download_file_path) |
||||
|
except FileNotFoundError as err: |
||||
|
print('download_failed: ' + str(err)) |
||||
|
except S3Error as err: |
||||
|
print("download_failed:", err) |
||||
|
|
||||
|
def remove_object(self, bucket_name, object_name): |
||||
|
""" |
||||
|
从bucket删除文件 |
||||
|
:param bucket_name: minio桶名称 |
||||
|
:param object_name: 存放在minio桶中的文件名字 |
||||
|
object_name处可以包含目录(文件夹) 例如 /example/file_name |
||||
|
""" |
||||
|
# 桶是否存在 |
||||
|
check_bucket = self.minioClient.bucket_exists(bucket_name) |
||||
|
if check_bucket: |
||||
|
try: |
||||
|
self.minioClient.remove_object(bucket_name=bucket_name, |
||||
|
object_name=object_name) |
||||
|
except FileNotFoundError as err: |
||||
|
print('upload_failed: ' + str(err)) |
||||
|
except S3Error as err: |
||||
|
print("upload_failed:", err) |
||||
|
|
||||
|
# 获取所有的桶 |
||||
|
def get_all_bucket(self): |
||||
|
buckets = self.minioClient.list_buckets() |
||||
|
ret = [] |
||||
|
for _ in buckets: |
||||
|
ret.append(_.name) |
||||
|
return ret |
||||
|
|
||||
|
# 获取一个桶中的所有一级目录和文件 |
||||
|
def get_list_objects_from_bucket(self, bucket_name): |
||||
|
# 桶是否存在 |
||||
|
check_bucket = self.minioClient.bucket_exists(bucket_name) |
||||
|
if check_bucket: |
||||
|
# 获取到该桶中的所有目录和文件 |
||||
|
objects = self.minioClient.list_objects(bucket_name=bucket_name) |
||||
|
ret = [] |
||||
|
for _ in objects: |
||||
|
ret.append(_.object_name) |
||||
|
return ret |
||||
|
|
||||
|
# 获取桶里某个目录下的所有目录和文件 |
||||
|
def get_list_objects_from_bucket_dir(self, bucket_name, dir_name): |
||||
|
# 桶是否存在 |
||||
|
check_bucket = self.minioClient.bucket_exists(bucket_name) |
||||
|
if check_bucket: |
||||
|
# 获取到bucket_name桶中的dir_name下的所有目录和文件 |
||||
|
# prefix 获取的文件路径需包含该前缀 |
||||
|
objects = self.minioClient.list_objects(bucket_name=bucket_name, |
||||
|
prefix=dir_name, |
||||
|
recursive=True) |
||||
|
ret = [] |
||||
|
for _ in objects: |
||||
|
ret.append(_.object_name) |
||||
|
return ret |
||||
|
|
||||
|
|
||||
|
# if __name__ == "__main__": |
||||
|
# # 本地minio登录IP地址和账号密码 |
||||
|
# minio_address = "127.0.0.1:9000" |
||||
|
# minio_admin = "minioadmin" |
||||
|
# minio_password = "minioadmin" |
||||
|
# |
||||
|
# bucket = Bucket(minio_address=minio_address, |
||||
|
# minio_admin=minio_admin, |
||||
|
# minio_password=minio_password) |
||||
|
# 创建桶测试 |
||||
|
# bucket.create_one_bucket('test1') |
||||
|
|
||||
|
# 删除桶测试 |
||||
|
# bucket.remove_one_bucket('test1') |
||||
|
|
||||
|
# 上传文件测试 |
||||
|
# bucket.upload_file_to_bucket('test1', '1.jpg', './1.jpg') |
||||
|
# bucket.upload_file_to_bucket('test1', '/example/1.jpg', './1.jpg') |
||||
|
|
||||
|
# 删除文件测试 |
||||
|
# bucket.remove_object('test1', '1.jpg') |
||||
|
# bucket.remove_object('test', '/example/1.jpg') |
||||
|
|
||||
|
# 下载图像测试 |
||||
|
# bucket.download_file_from_bucket('test1', 'example/1.jpg', './1.jpg') |
||||
|
|
||||
|
# 获取所有的桶 |
||||
|
# ret = bucket.get_all_bucket() |
||||
|
# print(ret) |
||||
|
|
||||
|
# 获取一个桶中的所有一级目录和文件 |
||||
|
# ret = bucket.get_list_objects_from_bucket(bucket_name='test') |
||||
|
# print(ret) |
||||
|
|
||||
|
# 获取一个桶中的某目录下的所有文件 |
||||
|
# ret = bucket.get_list_objects_from_bucket_dir('test', 'example/') |
||||
|
# print(ret) |
||||
|
|
@ -0,0 +1,58 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
import {postStream} from '@/utils/request' |
||||
|
|
||||
|
|
||||
|
|
||||
|
export function listChatHistory(sessionId) { |
||||
|
if (sessionId === ''){ |
||||
|
sessionId = '0' |
||||
|
} |
||||
|
return request({ |
||||
|
url: '/aichat/session/list/'+sessionId, |
||||
|
method: 'get', |
||||
|
params: {} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export function getChatList(sessionId) { |
||||
|
return request({ |
||||
|
url: '/aichat/chat/list/'+sessionId, |
||||
|
method: 'get', |
||||
|
params: {} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export function postChatMessage(data) { |
||||
|
return postStream('/aichat-api/stream-chat',data) |
||||
|
} |
||||
|
|
||||
|
export function DeleteChatSession(sessionId) { |
||||
|
return request({ |
||||
|
url: '/aichat/delete/session/'+sessionId, |
||||
|
method: 'post' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export function updateChat(data) { |
||||
|
return request({ |
||||
|
url: '/aichat/update', |
||||
|
method: 'post', |
||||
|
data: data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
export async function addChat(data) { |
||||
|
return request({ |
||||
|
url: '/aichat/add', |
||||
|
method: 'post', |
||||
|
data: data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 删除菜单
|
||||
|
export function delMenu(menuId) { |
||||
|
return request({ |
||||
|
url: '/system/menu/' + menuId, |
||||
|
method: 'delete' |
||||
|
}) |
||||
|
} |
After Width: | Height: | Size: 945 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 22 KiB |
@ -1,9 +1,11 @@ |
|||||
import hasRole from './permission/hasRole' |
import hasRole from './permission/hasRole' |
||||
import hasPermi from './permission/hasPermi' |
import hasPermi from './permission/hasPermi' |
||||
import copyText from './common/copyText' |
import copyText from './common/copyText' |
||||
|
import { ClickOutside as vClickOutside } from 'element-plus' |
||||
|
|
||||
export default function directive(app){ |
export default function directive(app){ |
||||
app.directive('hasRole', hasRole) |
app.directive('hasRole', hasRole) |
||||
app.directive('hasPermi', hasPermi) |
app.directive('hasPermi', hasPermi) |
||||
app.directive('copyText', copyText) |
app.directive('copyText', copyText) |
||||
|
app.directive('click-outside', vClickOutside) |
||||
} |
} |
@ -0,0 +1,80 @@ |
|||||
|
import moment from 'moment' |
||||
|
import 'moment/dist/locale/zh-cn' |
||||
|
moment.locale('zh-cn') |
||||
|
|
||||
|
// 当天日期 YYYY-MM-DD
|
||||
|
export const nowDate = moment().format('YYYY-MM-DD') |
||||
|
|
||||
|
// 当前时间前n天
|
||||
|
export function beforeDay(n) { |
||||
|
return moment().subtract(n, 'days').format('YYYY-MM-DD') |
||||
|
} |
||||
|
|
||||
|
const getCheckDate = (timestamp) => { |
||||
|
if (!timestamp) return false |
||||
|
const dt = new Date(timestamp) |
||||
|
if (isNaN(dt.getTime())) return false |
||||
|
return dt |
||||
|
} |
||||
|
export const datetimeFormat = (timestamp) => { |
||||
|
const dt = getCheckDate(timestamp) |
||||
|
if (!dt) return timestamp |
||||
|
|
||||
|
const y = dt.getFullYear() |
||||
|
const m = (dt.getMonth() + 1 + '').padStart(2, '0') |
||||
|
const d = (dt.getDate() + '').padStart(2, '0') |
||||
|
const hh = (dt.getHours() + '').padStart(2, '0') |
||||
|
const mm = (dt.getMinutes() + '').padStart(2, '0') |
||||
|
const ss = (dt.getSeconds() + '').padStart(2, '0') |
||||
|
|
||||
|
return `${y}-${m}-${d} ${hh}:${mm}:${ss}` |
||||
|
} |
||||
|
|
||||
|
export const dateFormat = (timestamp) => { |
||||
|
const dt = getCheckDate(timestamp) |
||||
|
if (!dt) return timestamp |
||||
|
|
||||
|
const y = dt.getFullYear() |
||||
|
const m = (dt.getMonth() + 1 + '').padStart(2, '0') |
||||
|
const d = (dt.getDate() + '').padStart(2, '0') |
||||
|
|
||||
|
return `${y}-${m}-${d}` |
||||
|
} |
||||
|
|
||||
|
export function fromNowDate(time) { |
||||
|
// 拿到当前时间戳和发布时的时间戳,然后得出时间戳差
|
||||
|
const curTime = new Date() |
||||
|
const futureTime = new Date(time) |
||||
|
const timeDiff = futureTime.getTime() - curTime.getTime() |
||||
|
|
||||
|
// 单位换算
|
||||
|
const min = 60 * 1000 |
||||
|
const hour = min * 60 |
||||
|
const day = hour * 24 |
||||
|
const week = day * 7 |
||||
|
|
||||
|
// 计算发布时间距离当前时间的周、天、时、分
|
||||
|
const exceedWeek = Math.floor(timeDiff / week) |
||||
|
const exceedDay = Math.floor(timeDiff / day) |
||||
|
const exceedHour = Math.floor(timeDiff / hour) |
||||
|
const exceedMin = Math.floor(timeDiff / min) |
||||
|
|
||||
|
// 最后判断时间差到底是属于哪个区间,然后return
|
||||
|
if (exceedWeek > 0) { |
||||
|
return '' |
||||
|
} else { |
||||
|
if (exceedDay < 7 && exceedDay > 0) { |
||||
|
return exceedDay + '天后' |
||||
|
} else { |
||||
|
if (exceedHour < 24 && exceedHour > 0) { |
||||
|
return exceedHour + '小时后' |
||||
|
} else { |
||||
|
if (exceedMin < 0) { |
||||
|
return '已过期' |
||||
|
} else { |
||||
|
return '即将到期' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,55 @@ |
|||||
|
<template> |
||||
|
<template v-for="(item, index) in source" :key="index"> |
||||
|
<div v-if="item.type === 'table'" style="width: 100%; position: relative"> |
||||
|
<chatTable :data="item.content"></chatTable> |
||||
|
<el-button style="position: absolute;top: 5px;right: 0;z-index: 1;width:30px;height: 30px" @click="downLoadTable(item.content)"> |
||||
|
<el-icon><Download/></el-icon> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
<div v-else-if="item.type === 'html_image'" style="width: 100%;margin-top: 5px;height: 320px"> |
||||
|
<htmlCharts :is_large="is_large" :index="'chat-iframe-'+chatIndex+'-'+index" :data="item.content"></htmlCharts> |
||||
|
</div> |
||||
|
<div v-else-if="item.type === 'G6_ER'" style="width: 100%;height: 300px; overflow: hidden; margin-top: 5px;position: relative"> |
||||
|
<antvg6 :is_large="is_large" :g6Data="item.content" :g6Index="'g6-container-'+chatIndex+'-'+index"></antvg6> |
||||
|
<el-button style="position: absolute;top: 5px;right: 0;z-index: 1;width:30px;height: 30px" @click="fullscreenG6(item.content)"> |
||||
|
<el-icon><FullScreen/></el-icon> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
<div v-else style="width: 100%;margin-top: 5px"> |
||||
|
<markdown :markdown-string="item.content"></markdown> |
||||
|
</div> |
||||
|
</template> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import * as XLSX from 'xlsx'; |
||||
|
import markdown from './markdown.vue' |
||||
|
import antvg6 from './antv-g6.vue' |
||||
|
import chatTable from './chatTable.vue' |
||||
|
import htmlCharts from './htmlCharts.vue' |
||||
|
import {Download, FullScreen} from "@element-plus/icons-vue"; |
||||
|
import { ref, watch} from 'vue' |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
source: Array, |
||||
|
is_large: Boolean, |
||||
|
chatIndex: Number, |
||||
|
}) |
||||
|
|
||||
|
const emit = defineEmits(['fullscreenG6']) |
||||
|
function fullscreenG6(data){ |
||||
|
emit('fullscreenG6',data) |
||||
|
} |
||||
|
|
||||
|
function downLoadTable(data){ |
||||
|
const worksheet = XLSX.utils.json_to_sheet(data); |
||||
|
|
||||
|
// 创建一个新的工作簿并将工作表添加到工作簿中 |
||||
|
const workbook = XLSX.utils.book_new(); |
||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1'); |
||||
|
|
||||
|
// 生成 Excel 文件并触发下载 |
||||
|
XLSX.writeFile(workbook, '导出数据.xlsx'); |
||||
|
} |
||||
|
</script> |
||||
|
<style lang="scss" scoped> |
||||
|
</style> |
@ -0,0 +1,240 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<el-text type="info"> |
||||
|
<span class="ml-4" style="margin-left: 4px">{{ datetimeFormat(data.time) }}</span> |
||||
|
</el-text> |
||||
|
</div> |
||||
|
<div> |
||||
|
<span> |
||||
|
<el-tooltip effect="dark" content="换个答案" placement="top" popper-class="operate-tooltip"> |
||||
|
<el-button text @click="regeneration" style="padding: 0"> |
||||
|
<el-icon><RefreshRight /></el-icon> |
||||
|
</el-button> |
||||
|
</el-tooltip> |
||||
|
<el-divider direction="vertical" /> |
||||
|
<el-tooltip effect="dark" content="复制" placement="top" popper-class="operate-tooltip"> |
||||
|
<el-button text @click="copyClick(data)" style="padding: 0"> |
||||
|
<el-icon><CopyDocument /></el-icon> |
||||
|
</el-button> |
||||
|
</el-tooltip> |
||||
|
<el-divider direction="vertical" /> |
||||
|
<el-tooltip |
||||
|
effect="dark" |
||||
|
content="赞同" |
||||
|
placement="top" |
||||
|
v-if="data.operate === null || data.operate === ''" |
||||
|
popper-class="operate-tooltip" |
||||
|
> |
||||
|
<el-button text @click="thumbUp" style="padding: 0"> |
||||
|
<i class="ri-thumb-up-line"></i> |
||||
|
</el-button> |
||||
|
</el-tooltip> |
||||
|
<el-tooltip |
||||
|
effect="dark" |
||||
|
content="取消赞同" |
||||
|
placement="top" |
||||
|
v-if="data.operate !== null && data.operate === 'thumb_up'" |
||||
|
popper-class="operate-tooltip" |
||||
|
> |
||||
|
<el-button text @click="cancelThumb" style="padding: 0"> |
||||
|
<i class="ri-thumb-up-fill" style="color: orange"></i> |
||||
|
</el-button> |
||||
|
</el-tooltip> |
||||
|
<el-divider direction="vertical" v-if="data.operate === null || data.operate === ''" /> |
||||
|
<el-popover |
||||
|
:visible="visible" |
||||
|
v-if="data.operate === null || data.operate === ''" |
||||
|
placement="left" |
||||
|
popper-style="z-index:99999;" |
||||
|
:show-arrow=false |
||||
|
title="反对原因" |
||||
|
:width="260"> |
||||
|
<template #default> |
||||
|
<el-input v-model="thumbDownReason"></el-input> |
||||
|
<div style="text-align: right; margin-top: 10px"> |
||||
|
<el-button size="small" text @click="closeThumb">取消</el-button> |
||||
|
<el-button size="small" type="primary" @click="confirmReason">确定</el-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #reference> |
||||
|
<el-button text @click="showPop" style="padding: 0"> |
||||
|
<el-tooltip |
||||
|
effect="dark" |
||||
|
content="反对" |
||||
|
placement="top" |
||||
|
popper-class="operate-tooltip" |
||||
|
> |
||||
|
<i class="ri-thumb-down-line"></i> |
||||
|
</el-tooltip> |
||||
|
</el-button> |
||||
|
</template> |
||||
|
</el-popover> |
||||
|
<el-tooltip |
||||
|
effect="dark" |
||||
|
:content="'反对原因:'+data.thumbDownReason" |
||||
|
placement="top" |
||||
|
v-if="data.operate !== null && data.operate === 'thumb_down'" |
||||
|
popper-class="operate-tooltip" |
||||
|
> |
||||
|
<el-button text @click="cancelThumb" style="padding: 0"> |
||||
|
<i class="ri-thumb-down-fill" style="color: orange"></i> |
||||
|
</el-button> |
||||
|
</el-tooltip> |
||||
|
</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import { ref } from 'vue' |
||||
|
import { useRoute } from 'vue-router' |
||||
|
import Clipboard from 'vue-clipboard3' |
||||
|
import { datetimeFormat } from '@/utils/time' |
||||
|
import {updateChat} from "@/api/aichat/aichat.js"; |
||||
|
const { proxy } = getCurrentInstance(); |
||||
|
const route = useRoute() |
||||
|
const { |
||||
|
params: { id } |
||||
|
} = route |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
data: { |
||||
|
type: Object, |
||||
|
default: () => {} |
||||
|
}, |
||||
|
index: Number |
||||
|
}) |
||||
|
const thumbDownReason = ref("") |
||||
|
const visible = ref(false) |
||||
|
|
||||
|
const emit = defineEmits(['update:data', 'regeneration', 'changeThumb']) |
||||
|
|
||||
|
function regeneration() { |
||||
|
emit('regeneration') |
||||
|
} |
||||
|
|
||||
|
function closeThumb(){ |
||||
|
visible.value = false |
||||
|
thumbDownReason.value = "" |
||||
|
} |
||||
|
function showPop(){ |
||||
|
visible.value = true |
||||
|
thumbDownReason.value = props.data.thumbDownReason?props.data.thumbDownReason:'' |
||||
|
} |
||||
|
function confirmReason(){ |
||||
|
let obj = { |
||||
|
operate:'thumb_down', |
||||
|
thumbDownReason: thumbDownReason.value |
||||
|
} |
||||
|
let chat = JSON.parse(JSON.stringify(props.data)) |
||||
|
chat.operate = 'thumb_down' |
||||
|
chat.thumbDownReason = thumbDownReason.value |
||||
|
chat.content = JSON.stringify(chat.content) |
||||
|
updateChat(chat).then(res=>{ |
||||
|
visible.value = false |
||||
|
emit("changeThumb",props.index,obj) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function copyClick(data) { |
||||
|
const {toClipboard} = Clipboard() |
||||
|
let str = '' |
||||
|
let content = data.content |
||||
|
for (let i = 0; i < content.length; i++){ |
||||
|
let cont = content[i] |
||||
|
if (cont.type === 'text'){ |
||||
|
str += cont.content |
||||
|
} |
||||
|
} |
||||
|
try { |
||||
|
await toClipboard(str) |
||||
|
proxy.$modal.msgSuccess("复制成功"); |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
proxy.$modal.msgError('复制失败') |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function thumbUp(){ |
||||
|
let obj = { |
||||
|
operate:'thumb_up', |
||||
|
thumbDownReason: '' |
||||
|
} |
||||
|
let chat = JSON.parse(JSON.stringify(props.data)) |
||||
|
chat.operate = 'thumb_up' |
||||
|
chat.content = JSON.stringify(chat.content) |
||||
|
updateChat(chat).then(res=>{ |
||||
|
emit("changeThumb",props.index,obj) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function cancelThumb(){ |
||||
|
let obj = { |
||||
|
operate:'', |
||||
|
thumbDownReason: '' |
||||
|
} |
||||
|
let chat = JSON.parse(JSON.stringify(props.data)) |
||||
|
chat.operate = '' |
||||
|
chat.content = JSON.stringify(chat.content) |
||||
|
updateChat(chat).then(res=>{ |
||||
|
emit("changeThumb",props.index,obj) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function voteHandle(operate) { |
||||
|
let chat = props.data |
||||
|
let obj = { |
||||
|
operate:'', |
||||
|
thumbDownReason:'' |
||||
|
} |
||||
|
if (operate === 'thumb_up'){ |
||||
|
chat.operate = 'thumb_up' |
||||
|
chat.content = JSON.stringify(chat.content) |
||||
|
updateChat(chat).then(res=>{ |
||||
|
obj.operate = 'thumb_up' |
||||
|
emit("changeThumb",props.index,obj) |
||||
|
}) |
||||
|
} |
||||
|
if (operate === 'cancel-thumb-up'){ |
||||
|
chat.operate = '' |
||||
|
chat.content = JSON.stringify(chat.content) |
||||
|
updateChat(chat).then(res=>{ |
||||
|
obj.operate = '' |
||||
|
emit("changeThumb",props.index,obj) |
||||
|
}) |
||||
|
} |
||||
|
if(operate === 'cancel-thumb-down'){ |
||||
|
|
||||
|
} |
||||
|
emit("voteHandle",props.index,val) |
||||
|
} |
||||
|
|
||||
|
function ToPlainText(md) { |
||||
|
return ( |
||||
|
md |
||||
|
// 移除图片  |
||||
|
.replace(/!\[.*?\]\(.*?\)/g, '') |
||||
|
// 移除链接 [text](url) |
||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') |
||||
|
// 移除 标题符号 (#, ##, ###) |
||||
|
.replace(/^#{1,6}\s+/gm, '') |
||||
|
// 移除加粗 **text** 或 __text__ |
||||
|
.replace(/\*\*(.*?)\*\*/g, '$1') |
||||
|
.replace(/__(.*?)__/g, '$1') |
||||
|
// 移除斜体 *text* 或 _text_ |
||||
|
.replace(/\*(.*?)\*/g, '$1') |
||||
|
.replace(/_(.*?)_/g, '$1') |
||||
|
// 移除行内代码 `code` |
||||
|
.replace(/`(.*?)`/g, '$1') |
||||
|
// 移除代码块 ```code``` |
||||
|
.replace(/```[\s\S]*?```/g, '') |
||||
|
// 移除多余的换行符 |
||||
|
.replace(/\n{2,}/g, '\n') |
||||
|
.trim() |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
<style lang="scss"> |
||||
|
.operate-tooltip { |
||||
|
z-index: 99999 !important; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,727 @@ |
|||||
|
<template> |
||||
|
<div ref="aiChatRef" class="ai-chat"> |
||||
|
<el-scrollbar ref="scrollDiv" @scroll="handleScrollTop"> |
||||
|
<div ref="dialogScrollbar" class="ai-chat__content p-24 chat-width" style="padding: 24px;"> |
||||
|
<div class="item-content mb-16" style="margin-bottom: 16px"> |
||||
|
<div class="avatar"> |
||||
|
<img src="@/assets/logo/logo2.png" height="30px" /> |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<el-card shadow="always" class="dialog-card"> |
||||
|
<span style="font-size: 14px">您好,我是 果知小助手,您可以向我提出关于 果知的相关问题。</span> |
||||
|
</el-card> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<template v-for="(item, index) in chatList" :key="index"> |
||||
|
<!-- 问题 --> |
||||
|
<div v-if="item.type === 'question'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400"> |
||||
|
<div class="avatar"> |
||||
|
<el-avatar style="width:30px;height: 30px;background: #3370FF"> |
||||
|
<img src="@/assets/aichat/user-icon.svg" style="width: 30px" alt="" /> |
||||
|
</el-avatar> |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<div class="text break-all pre-wrap" style="word-break: break-all;white-space: pre-wrap;"> |
||||
|
{{ item.content }} |
||||
|
</div> |
||||
|
<el-tag type="success" style="margin-bottom: 5px;cursor: pointer" v-for="(fileName,index) in item.file" :disable-transitions="false" @click="downloadFile(fileName.file,fileName.bucket,item.sessionId)">{{fileName.file}}</el-tag> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 回答 --> |
||||
|
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="margin-bottom: 16px;font-weight: 400"> |
||||
|
<div class="avatar"> |
||||
|
<img src="@/assets/logo/logo2.png" height="30px" /> |
||||
|
</div> |
||||
|
<div class="content"> |
||||
|
<el-card shadow="always" class="dialog-card"> |
||||
|
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer> |
||||
|
</el-card> |
||||
|
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px"> |
||||
|
<div> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
v-if="item.isStop && !item.isEnd" |
||||
|
@click="startChat(index)" |
||||
|
link |
||||
|
>继续 |
||||
|
</el-button> |
||||
|
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link |
||||
|
>停止回答 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;"> |
||||
|
<OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</el-scrollbar> |
||||
|
<div class="ai-chat__operate p-24" style="padding: 24px"> |
||||
|
<!-- <slot name="operateBefore" />--> |
||||
|
<div class="chat-width"> |
||||
|
<el-button type="primary" link class="new-chat-button mb-8" style="margin-bottom: 8px" @click="openUpload"> |
||||
|
<el-icon><Upload /></el-icon><span class="ml-4">导入文件</span> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
<div> |
||||
|
<el-tag type="success" style="margin-bottom: 5px" v-for="(tag,index) in currentFiles" closable :disable-transitions="false" @close="removeFile(index)">{{tag.file}}</el-tag> |
||||
|
</div> |
||||
|
<div> |
||||
|
<span v-if="currentMachine.length > 0">与 </span> |
||||
|
<el-tag style="margin-bottom: 5px" size="large" v-for="tag in currentMachine" closable :disable-transitions="false" @close="removeMachine"> |
||||
|
<span style="font-size: 16px;margin-left: 10px">{{tag}}</span> |
||||
|
</el-tag> |
||||
|
<span v-if="currentMachine.length > 0"> 对话</span> |
||||
|
</div> |
||||
|
<div class="operate-textarea flex chat-width"> |
||||
|
<el-popover |
||||
|
:visible="popoverVisible" |
||||
|
placement="top" |
||||
|
:width="is_large?'400px':'860px'" |
||||
|
popper-style="max-width:860px;z-index:99999;margin-left:35px" |
||||
|
:show-arrow=false |
||||
|
:offset=1 |
||||
|
> |
||||
|
<template #reference> |
||||
|
<el-input |
||||
|
ref="quickInputRef" |
||||
|
v-model="inputValue" |
||||
|
:placeholder="'@选择机器人,Ctrl+Enter 换行,Enter发送'" |
||||
|
:autosize="{ minRows: 1, maxRows: 4 }" |
||||
|
type="textarea" |
||||
|
:maxlength="100000" |
||||
|
@input="handleInput" |
||||
|
@keydown.enter="sendChatHandle($event)" |
||||
|
/> |
||||
|
</template> |
||||
|
<template #default> |
||||
|
<div style="width: 100%" v-click-outside="clickoutside"> |
||||
|
<div style="align-items: center;display: flex"> |
||||
|
<span style="font-size: 25px;font-weight: bold">选择智能体</span> |
||||
|
<el-link :underline="false" style="font-size: 20px;margin-left: auto" @click="closeMachinePop"><i class="ri-close-large-line"></i></el-link> |
||||
|
</div> |
||||
|
<el-scrollbar max-height="200px"> |
||||
|
<div class="machineDiv" @click="chooseMachine('元数据专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">元数据专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据标准专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据标准专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据质量专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据质量专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据模型专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据模型专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据安全专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据安全专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据分析专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据分析专家</span> |
||||
|
</div> |
||||
|
<div class="machineDiv" @click="chooseMachine('数据治理管理专家','@/assets/aichat/智能体logo.jpg')" style="align-items: center;width:100%;display: flex"> |
||||
|
<el-avatar style="width: 30px;height: 30px"><img src="@/assets/aichat/智能体logo.jpg"></el-avatar> |
||||
|
<span style="font-size: 16px;margin-left: 10px">数据治理管理专家</span> |
||||
|
</div> |
||||
|
</el-scrollbar> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-popover> |
||||
|
<div class="operate flex align-center"> |
||||
|
<el-button |
||||
|
text |
||||
|
class="sent-button" |
||||
|
:disabled="isDisabledChart || loading" |
||||
|
@click="sendChatHandle" |
||||
|
> |
||||
|
<img v-show="isDisabledChart || loading" src="@/assets/aichat/icon_send.svg" alt="" /> |
||||
|
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" /> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 文件导入对话框 --> |
||||
|
<el-dialog title="文件导入" v-model="upload.open" width="400px" append-to-body> |
||||
|
<el-upload v-if="upload.open" |
||||
|
ref="uploadRef" |
||||
|
:headers="upload.headers" |
||||
|
:action="upload.url" |
||||
|
:data = "upload.data" |
||||
|
:disabled="upload.isUploading" |
||||
|
:on-progress="handleFileUploadProgress" |
||||
|
:on-success="handleFileSuccess" |
||||
|
:auto-upload="false" |
||||
|
drag |
||||
|
> |
||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon> |
||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> |
||||
|
</el-upload> |
||||
|
<template #footer> |
||||
|
<div class="dialog-footer"> |
||||
|
<el-button type="primary" @click="submitFileForm">确 定</el-button> |
||||
|
<el-button @click="upload.open = false">取 消</el-button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
<el-dialog z-index="99999999999999999999" title="ER图全览" v-if="showFullscreenG6Data" :fullscreen="true" v-model="showFullscreenG6Data" append-to-body> |
||||
|
<fullscreenG6 :g6Data="fullscreenG6Data"></fullscreenG6> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue' |
||||
|
import { useRoute } from 'vue-router' |
||||
|
import antvg6 from './antv-g6.vue' |
||||
|
import OperationButton from './OperationButton.vue' |
||||
|
import MdRenderer from '@/views/aichat/MdRenderer.vue' |
||||
|
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue' |
||||
|
import {getToken} from "@/utils/auth.js"; |
||||
|
import {postChatMessage} from "@/api/aichat/aichat.js" |
||||
|
import Cookies from "js-cookie"; |
||||
|
import {addChat} from "@/api/aichat/aichat"; |
||||
|
|
||||
|
defineOptions({ name: 'AiChat' }) |
||||
|
const route = useRoute() |
||||
|
const fullscreenG6Data = ref(null) |
||||
|
const showFullscreenG6Data = ref(false) |
||||
|
const { |
||||
|
params: { accessToken, id }, |
||||
|
query: { mode } |
||||
|
} = route |
||||
|
const props = defineProps({ |
||||
|
is_large: Boolean, |
||||
|
cookieSessionId: String, |
||||
|
data: { |
||||
|
type: Object, |
||||
|
default: () => {} |
||||
|
}, |
||||
|
record: { |
||||
|
type: Array, |
||||
|
default: () => [] |
||||
|
}, |
||||
|
chatId: { |
||||
|
type: String, |
||||
|
default: '' |
||||
|
}, // 历史记录Id |
||||
|
debug: { |
||||
|
type: Boolean, |
||||
|
default: false |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const { proxy } = getCurrentInstance(); |
||||
|
|
||||
|
const aiChatRef = ref() |
||||
|
const quickInputRef = ref() |
||||
|
const scrollDiv = ref() |
||||
|
const dialogScrollbar = ref() |
||||
|
const loading = ref(false) |
||||
|
const inputValue = ref('') |
||||
|
const chartOpenId = ref('') |
||||
|
const chatList = ref([]) |
||||
|
const answerList = ref([]) |
||||
|
|
||||
|
const popoverVisible = ref(false) |
||||
|
const currentMachine = ref([]) |
||||
|
const currentFiles = ref([]) |
||||
|
const upload = reactive({ |
||||
|
// 是否显示弹出层(用户导入) |
||||
|
open: false, |
||||
|
// 弹出层标题(用户导入) |
||||
|
title: "", |
||||
|
// 是否禁用上传 |
||||
|
isUploading: false, |
||||
|
// 设置上传的请求头部 |
||||
|
headers: { Authorization: "Bearer " + getToken() }, |
||||
|
// 上传的地址 |
||||
|
url: import.meta.env.VITE_APP_BASE_API + "/aichat/upload", |
||||
|
data: {"sessionId":Cookies.get("chatSessionId")} |
||||
|
}); |
||||
|
const isDisabledChart = computed( |
||||
|
() => !(inputValue.value.trim()) |
||||
|
) |
||||
|
|
||||
|
const emit = defineEmits(['scroll']) |
||||
|
|
||||
|
function setScrollBottom() { |
||||
|
// 将滚动条滚动到最下面 |
||||
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 滚动条距离最上面的高度 |
||||
|
*/ |
||||
|
const scrollTop = ref(0) |
||||
|
|
||||
|
const scorll = ref(true) |
||||
|
|
||||
|
const getMaxHeight = () => { |
||||
|
return dialogScrollbar.value.scrollHeight |
||||
|
} |
||||
|
const handleScrollTop = ($event) => { |
||||
|
scrollTop.value = $event.scrollTop |
||||
|
if ( |
||||
|
dialogScrollbar.value.scrollHeight - (scrollTop.value + scrollDiv.value.wrapRef.offsetHeight) <= |
||||
|
30 |
||||
|
) { |
||||
|
scorll.value = true |
||||
|
} else { |
||||
|
scorll.value = false |
||||
|
} |
||||
|
emit('scroll', { ...$event, dialogScrollbar: dialogScrollbar.value, scrollDiv: scrollDiv.value }) |
||||
|
} |
||||
|
|
||||
|
const handleScroll = () => { |
||||
|
if (!props.log && scrollDiv.value) { |
||||
|
// 内部高度小于外部高度 就需要出滚动条 |
||||
|
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value.scrollHeight) { |
||||
|
// 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随 |
||||
|
if (scorll.value) { |
||||
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/**文件上传中处理 */ |
||||
|
const handleFileUploadProgress = (event, file, fileList) => { |
||||
|
upload.isUploading = true; |
||||
|
}; |
||||
|
|
||||
|
const handleFileSuccess = (response, file, fileList) => { |
||||
|
currentFiles.value.push({file:response.data.file,bucket:response.data.bucket}) |
||||
|
upload.open = false; |
||||
|
upload.isUploading = false; |
||||
|
proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true }); |
||||
|
}; |
||||
|
|
||||
|
/** 提交上传文件 */ |
||||
|
function submitFileForm() { |
||||
|
proxy.$refs["uploadRef"].submit(); |
||||
|
} |
||||
|
|
||||
|
watch( |
||||
|
() => props.chatId, |
||||
|
(val) => { |
||||
|
if (val && val !== 'new') { |
||||
|
chartOpenId.value = val |
||||
|
} else { |
||||
|
chartOpenId.value = '' |
||||
|
} |
||||
|
}, |
||||
|
{ deep: true } |
||||
|
) |
||||
|
|
||||
|
watch( |
||||
|
() => props.record, |
||||
|
(value) => { |
||||
|
chatList.value = value |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value}) |
||||
|
|
||||
|
function regenerationChart(index){ |
||||
|
let question = chatList.value[index - 1] |
||||
|
let chat = { |
||||
|
"type":"question", |
||||
|
"content":question.content, |
||||
|
"time": formatDate(new Date()), |
||||
|
"file": question.file} |
||||
|
chatList.value.push(chat) |
||||
|
let data = { |
||||
|
"query": chat.content, |
||||
|
"user_id": Cookies.get("username"), |
||||
|
"robot": currentMachine.value.length>0?currentMachine.value[0]:"", |
||||
|
"sessionId": props.sessionId, |
||||
|
"doc": chat.file, |
||||
|
"history": [] |
||||
|
} |
||||
|
sendChatMessage(data) |
||||
|
} |
||||
|
|
||||
|
function changeThumb(index,chat){ |
||||
|
chatList.value[index].operate = chat.operate |
||||
|
chatList.value[index].thumbDownReason = chat.thumbDownReason |
||||
|
} |
||||
|
|
||||
|
function openUpload(){ |
||||
|
upload.open = true |
||||
|
} |
||||
|
function chooseMachine(val){ |
||||
|
inputValue.value = inputValue.value.slice(0,-1) |
||||
|
currentMachine.value = [val] |
||||
|
popoverVisible.value = false |
||||
|
} |
||||
|
|
||||
|
function closeMachinePop(){ |
||||
|
if (inputValue.value.endsWith('@')){ |
||||
|
inputValue.value = inputValue.value.slice(0,-1) |
||||
|
} |
||||
|
popoverVisible.value = false |
||||
|
} |
||||
|
|
||||
|
function removeMachine(){ |
||||
|
currentMachine.value = [] |
||||
|
} |
||||
|
|
||||
|
function fullscreen(data){ |
||||
|
fullscreenG6Data.value = data |
||||
|
showFullscreenG6Data.value = true |
||||
|
} |
||||
|
|
||||
|
function clickoutside() { |
||||
|
if (inputValue.value.endsWith('@')){ |
||||
|
inputValue.value = inputValue.value.slice(0,-1) |
||||
|
} |
||||
|
popoverVisible.value = false |
||||
|
} |
||||
|
|
||||
|
function removeFile(index){ |
||||
|
currentFiles.value = currentFiles.value.slice(index,1) |
||||
|
} |
||||
|
|
||||
|
function handleInput() { |
||||
|
popoverVisible.value = inputValue.value.endsWith("@"); |
||||
|
} |
||||
|
|
||||
|
function padZero(num) { |
||||
|
return num < 10 ? '0' + num : num; |
||||
|
} |
||||
|
|
||||
|
function formatDate(date) { |
||||
|
const year = date.getFullYear(); |
||||
|
const month = padZero(date.getMonth() + 1); // 月份从0开始,所以需要+1 |
||||
|
const day = padZero(date.getDate()); |
||||
|
const hours = padZero(date.getHours()); |
||||
|
const minutes = padZero(date.getMinutes()); |
||||
|
const seconds = padZero(date.getSeconds()); |
||||
|
|
||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
||||
|
} |
||||
|
|
||||
|
function downloadFile(file,bucket,sessionId){ |
||||
|
let data = {file,bucket,sessionId} |
||||
|
proxy.download("aichat/file/download", { |
||||
|
...data, |
||||
|
}, file); |
||||
|
} |
||||
|
|
||||
|
async function sendChatHandle(event) { |
||||
|
if (!event.ctrlKey) { |
||||
|
// 如果没有按下组合键ctrl,则会阻止默认事件 |
||||
|
event.preventDefault() |
||||
|
if (inputValue.value.trim() && ((chatList.value.length > 1 && chatList.value[chatList.value.length - 1].isEnd) || chatList.value.length < 2)) { |
||||
|
chatList.value.push({ |
||||
|
"type": "question", |
||||
|
"content": inputValue.value.trim(), |
||||
|
"time": formatDate(new Date()), |
||||
|
"sessionId": Cookies.get("chatSessionId"), |
||||
|
"sessionName": chatList.value.length > 0 ? chatList.value[0].content.substring(0, 20) : inputValue.value.trim().substring(0, 20), |
||||
|
"file": currentFiles.value |
||||
|
}) |
||||
|
nextTick(() => { |
||||
|
// 将滚动条滚动到最下面 |
||||
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
||||
|
}) |
||||
|
let question = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
||||
|
question.file = JSON.stringify(question.file) |
||||
|
await addChat(question) |
||||
|
inputValue.value = '' |
||||
|
let data = { |
||||
|
"query": inputValue.value.trim(), |
||||
|
"user_id": Cookies.get("username"), |
||||
|
"robot": currentMachine.value.length > 0 ? currentMachine.value[0] : "", |
||||
|
"sessionId": Cookies.get("chatSessionId"), |
||||
|
"doc": currentFiles.value, |
||||
|
"history": [] |
||||
|
} |
||||
|
sendChatMessage(data) |
||||
|
} |
||||
|
} else { |
||||
|
// 如果同时按下ctrl+回车键,则会换行 |
||||
|
inputValue.value += '\n' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function sendChatMessage(data){ |
||||
|
postChatMessage(data).then(res=>{ |
||||
|
currentFiles.value = [] |
||||
|
chatList.value.push({"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''}) |
||||
|
answerList.value.push({"index":chatList.value.length-1, "content":[], "isEnd":false}) |
||||
|
const reader = res.body.getReader() |
||||
|
const write = getWrite(reader) |
||||
|
reader.read().then(write).then(()=> { |
||||
|
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) |
||||
|
answer.content = JSON.stringify(answer.content) |
||||
|
addChat(answer) |
||||
|
}) |
||||
|
}).catch((e) => { |
||||
|
chatList.value.push({"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false}) |
||||
|
}) |
||||
|
} |
||||
|
watch( |
||||
|
chatList, |
||||
|
() => { |
||||
|
handleScroll() |
||||
|
}, |
||||
|
{ deep: true, immediate: true } |
||||
|
) |
||||
|
const getWrite = (reader) => { |
||||
|
let tempResult = ''; |
||||
|
/** |
||||
|
* 处理流式数据 |
||||
|
* @param {done, value} obj - 包含 done 和 value 属性的对象 |
||||
|
*/ |
||||
|
const write_stream = ({ done,value }) => { |
||||
|
try { |
||||
|
if (done) { |
||||
|
return |
||||
|
} |
||||
|
const decoder = new TextDecoder('utf-8'); |
||||
|
let str = decoder.decode(value, { stream: true }); |
||||
|
// 解释:数据流返回的 chunk 可能不完整,因此我们需要累积并拆分它们。 |
||||
|
tempResult += str; |
||||
|
const split = tempResult.match(/data:.*}\n\n/g); |
||||
|
if (split) { |
||||
|
str = split.join(''); |
||||
|
tempResult = tempResult.replace(str, ''); |
||||
|
} else { |
||||
|
return reader.read().then(write_stream); |
||||
|
} |
||||
|
// 如果 str 存在且以 "data:" 开头,则处理数据块。 |
||||
|
if (str && str.startsWith('data:')) { |
||||
|
split.forEach((chunkStr) => { |
||||
|
const chunk = JSON.parse(chunkStr.replace('data:', '')); |
||||
|
if (chunk.docs && chunk.docs[0].length > 0){ |
||||
|
//超链接 |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.docs[0],"type":"docs"}) |
||||
|
}else if (chunk.G6_ER && chunk.G6_ER.length > 0){ |
||||
|
//说明是ER图 |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.G6_ER,"type":"G6_ER"}) |
||||
|
}else if (chunk.html_image && chunk.html_image.length > 0){ |
||||
|
//说明是html的echarts图 |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.html_image,"type":"html_image"}) |
||||
|
}else if (chunk.table && chunk.table.length > 0){ |
||||
|
//说明是 表格 |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.table,"type":"table"}) |
||||
|
}else { |
||||
|
// 纯文本 |
||||
|
let last_answer = answerList.value[answerList.value.length - 1].content |
||||
|
chunk.choices[0].delta.content = chunk.choices[0].delta.content.replace("\n","\n\n") |
||||
|
if (last_answer.length > 0) { |
||||
|
if(last_answer[last_answer.length - 1].type === 'text'){ |
||||
|
answerList.value[answerList.value.length-1].content[last_answer.length - 1].content += chunk.choices[0].delta.content |
||||
|
}else{ |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"}) |
||||
|
} |
||||
|
}else{ |
||||
|
answerList.value[answerList.value.length - 1].content.push({"content":chunk.choices[0].delta.content,"type":"text"}) |
||||
|
} |
||||
|
} |
||||
|
if (!chatList.value[answerList.value[answerList.value.length - 1].index].isStop){ |
||||
|
chatList.value[chatList.value.length - 1].content =answerList.value[answerList.value.length - 1].content |
||||
|
} |
||||
|
nextTick(() => { |
||||
|
// 将滚动条滚动到最下面 |
||||
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
||||
|
}) |
||||
|
if (chunk.isEnd){ |
||||
|
answerList.value[answerList.value.length - 1].isEnd = true |
||||
|
answerList.value[answerList.value.length - 1].time = formatDate(new Date()) |
||||
|
if (!chatList.value[answerList.value[answerList.value.length - 1].index].isStop){ |
||||
|
chatList.value[chatList.value.length - 1].isEnd = true |
||||
|
chatList.value[chatList.value.length - 1].time = formatDate(new Date()) |
||||
|
} |
||||
|
nextTick(() => { |
||||
|
// 将滚动条滚动到最下面 |
||||
|
scrollDiv.value.setScrollTop(getMaxHeight()) |
||||
|
}) |
||||
|
return Promise.resolve(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
return Promise.reject(e); |
||||
|
} |
||||
|
return reader.read().then(write_stream); |
||||
|
}; |
||||
|
return write_stream |
||||
|
}; |
||||
|
|
||||
|
const stopChat = (index) => { |
||||
|
chatList.value[index].isStop = true |
||||
|
} |
||||
|
const startChat = (index) => { |
||||
|
chatList.value[index].isStop = false |
||||
|
} |
||||
|
|
||||
|
defineExpose({ |
||||
|
setScrollBottom |
||||
|
}) |
||||
|
|
||||
|
</script> |
||||
|
<style lang="scss" scoped> |
||||
|
.ai-chat { |
||||
|
--padding-left: 40px; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
box-sizing: border-box; |
||||
|
position: relative; |
||||
|
color: #1f2329; |
||||
|
|
||||
|
&__content { |
||||
|
height: auto; |
||||
|
padding-top: 0; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
.avatar { |
||||
|
float: left; |
||||
|
} |
||||
|
|
||||
|
.content { |
||||
|
padding-left: var(--padding-left); |
||||
|
|
||||
|
:deep(ol) { |
||||
|
margin-left: 16px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.text { |
||||
|
padding: 6px 0; |
||||
|
} |
||||
|
|
||||
|
.problem-button { |
||||
|
width: 100%; |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
background: #f5f6f7; |
||||
|
height: 46px; |
||||
|
padding: 0 12px; |
||||
|
line-height: 46px; |
||||
|
box-sizing: border-box; |
||||
|
color: #1f2329; |
||||
|
-webkit-line-clamp: 1; |
||||
|
word-break: break-all; |
||||
|
|
||||
|
&:hover { |
||||
|
background: var(--el-color-primary-light-9); |
||||
|
} |
||||
|
|
||||
|
&.disabled { |
||||
|
&:hover { |
||||
|
background: #f5f6f7; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.el-icon) { |
||||
|
color: var(--el-color-primary); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&__operate { |
||||
|
background: #f3f7f9; |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
box-sizing: border-box; |
||||
|
z-index: 10; |
||||
|
|
||||
|
&:before { |
||||
|
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%); |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
top: -16px; |
||||
|
left: 0; |
||||
|
height: 16px; |
||||
|
} |
||||
|
|
||||
|
.operate-textarea { |
||||
|
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08); |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 8px; |
||||
|
border: 1px solid #ffffff; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
&:has(.el-textarea__inner:focus) { |
||||
|
border: 1px solid var(--el-color-primary); |
||||
|
scrollbar-width: none; |
||||
|
} |
||||
|
|
||||
|
:deep(.el-textarea__inner) { |
||||
|
border-radius: 8px !important; |
||||
|
box-shadow: none; |
||||
|
resize: none; |
||||
|
padding: 12px 16px; |
||||
|
box-sizing: border-box; |
||||
|
scrollbar-width: none; |
||||
|
} |
||||
|
|
||||
|
.operate { |
||||
|
padding: 6px 10px; |
||||
|
.el-icon { |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
|
||||
|
.sent-button { |
||||
|
max-height: none; |
||||
|
.el-icon { |
||||
|
font-size: 24px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.el-loading-spinner) { |
||||
|
margin-top: -15px; |
||||
|
|
||||
|
.circular { |
||||
|
width: 31px; |
||||
|
height: 31px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.dialog-card { |
||||
|
border: none; |
||||
|
border-radius: 8px; |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-width { |
||||
|
max-width: 80%; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
.machineDiv { |
||||
|
cursor: pointer; |
||||
|
padding-top: 3px; |
||||
|
padding-bottom: 3px; |
||||
|
} |
||||
|
.machineDiv:hover { |
||||
|
background-color: #ebf1ff; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
@media only screen and (max-width: 1000px) { |
||||
|
.chat-width { |
||||
|
max-width: 100% !important; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,611 @@ |
|||||
|
<template> |
||||
|
<div :id="g6Index" style="width: 100%;height: 100%; overflow: hidden"></div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import G6 from '@antv/g6'; |
||||
|
import {watch} from "vue"; |
||||
|
const props = defineProps({ |
||||
|
is_large: Boolean, |
||||
|
g6Data: { |
||||
|
type: Array, |
||||
|
default: () => {} |
||||
|
}, |
||||
|
g6Index: String |
||||
|
}) |
||||
|
|
||||
|
function initG6(){ |
||||
|
const { |
||||
|
Util, |
||||
|
registerBehavior, |
||||
|
registerEdge, |
||||
|
registerNode |
||||
|
} = G6; |
||||
|
const isInBBox = (point, bbox) => { |
||||
|
const { |
||||
|
x, |
||||
|
y |
||||
|
} = point; |
||||
|
const { |
||||
|
minX, |
||||
|
minY, |
||||
|
maxX, |
||||
|
maxY |
||||
|
} = bbox; |
||||
|
|
||||
|
return x < maxX && x > minX && y > minY && y < maxY; |
||||
|
}; |
||||
|
const itemHeight = 20; |
||||
|
registerBehavior("dice-er-scroll", { |
||||
|
getDefaultCfg() { |
||||
|
return { |
||||
|
multiple: true, |
||||
|
}; |
||||
|
}, |
||||
|
getEvents() { |
||||
|
return { |
||||
|
itemHeight, |
||||
|
wheel: "scorll", |
||||
|
click: "click", |
||||
|
"node:mousemove": "move", |
||||
|
"node:mousedown": "mousedown", |
||||
|
"node:mouseup": "mouseup" |
||||
|
}; |
||||
|
}, |
||||
|
scorll(e) { |
||||
|
e.preventDefault(); |
||||
|
const { |
||||
|
graph |
||||
|
} = this; |
||||
|
const nodes = graph.getNodes().filter((n) => { |
||||
|
const bbox = n.getBBox(); |
||||
|
|
||||
|
return isInBBox(graph.getPointByClient(e.clientX, e.clientY), bbox); |
||||
|
}); |
||||
|
|
||||
|
const x = e.deltaX || e.movementX; |
||||
|
let y = e.deltaY || e.movementY; |
||||
|
if (!y && navigator.userAgent.indexOf('Firefox') > -1) y = (-e.wheelDelta * 125) / 3 |
||||
|
|
||||
|
if (nodes) { |
||||
|
const edgesToUpdate = new Set(); |
||||
|
nodes.forEach((node) => { |
||||
|
const model = node.getModel(); |
||||
|
if (model.attrs.length < 2) { |
||||
|
return; |
||||
|
} |
||||
|
const idx = model.startIndex || 0; |
||||
|
let startX = model.startX || 0.5; |
||||
|
let startIndex = idx + y * 0.02; |
||||
|
startX -= x; |
||||
|
if (startIndex < 0) { |
||||
|
startIndex = 0; |
||||
|
} |
||||
|
if (startX > 0) { |
||||
|
startX = 0; |
||||
|
} |
||||
|
if (startIndex > model.attrs.length - 1) { |
||||
|
startIndex = model.attrs.length - 1; |
||||
|
} |
||||
|
graph.updateItem(node, { |
||||
|
startIndex, |
||||
|
startX, |
||||
|
}); |
||||
|
node.getEdges().forEach(edge => edgesToUpdate.add(edge)) |
||||
|
}); |
||||
|
// G6 update the related edges when graph.updateItem with a node according to the new properties |
||||
|
// here you need to update the related edges manualy since the new properties { startIndex, startX } for the nodes are custom, and cannot be recognized by G6 |
||||
|
edgesToUpdate.forEach(edge => edge.refresh()) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
}, |
||||
|
click(e) { |
||||
|
const { |
||||
|
graph |
||||
|
} = this; |
||||
|
const item = e.item; |
||||
|
const shape = e.shape; |
||||
|
if (!item) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (shape.get("name") === "collapse") { |
||||
|
graph.updateItem(item, { |
||||
|
collapsed: true, |
||||
|
size: [300, 50], |
||||
|
}); |
||||
|
setTimeout(() => graph.layout(), 100); |
||||
|
} else if (shape.get("name") === "expand") { |
||||
|
graph.updateItem(item, { |
||||
|
collapsed: false, |
||||
|
size: [300, 80], |
||||
|
}); |
||||
|
setTimeout(() => graph.layout(), 100); |
||||
|
} |
||||
|
}, |
||||
|
mousedown(e) { |
||||
|
this.isMousedown = true; |
||||
|
}, |
||||
|
mouseup(e) { |
||||
|
this.isMousedown = false; |
||||
|
}, |
||||
|
move(e) { |
||||
|
if (this.isMousedown) return; |
||||
|
const name = e.shape.get("name"); |
||||
|
const item = e.item; |
||||
|
|
||||
|
if (name && name.startsWith("item")) { |
||||
|
this.graph.updateItem(item, { |
||||
|
selectedIndex: Number(name.split("-")[1]), |
||||
|
}); |
||||
|
} else { |
||||
|
this.graph.updateItem(item, { |
||||
|
selectedIndex: NaN, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registerEdge("dice-er-edge", { |
||||
|
draw(cfg, group) { |
||||
|
const edge = group.cfg.item; |
||||
|
const sourceNode = edge.getSource().getModel(); |
||||
|
const targetNode = edge.getTarget().getModel(); |
||||
|
|
||||
|
const sourceIndex = sourceNode.attrs.findIndex( |
||||
|
(e) => e.key === cfg.sourceKey |
||||
|
); |
||||
|
|
||||
|
const sourceStartIndex = sourceNode.startIndex || 0; |
||||
|
|
||||
|
let sourceY = 15; |
||||
|
|
||||
|
if (!sourceNode.collapsed && sourceIndex > sourceStartIndex - 1) { |
||||
|
sourceY = 30 + (sourceIndex - sourceStartIndex + 0.5) * itemHeight; |
||||
|
sourceY = Math.min(sourceY, 80); |
||||
|
} |
||||
|
|
||||
|
const targetIndex = targetNode.attrs.findIndex( |
||||
|
(e) => e.key === cfg.targetKey |
||||
|
); |
||||
|
|
||||
|
const targetStartIndex = targetNode.startIndex || 0; |
||||
|
|
||||
|
let targetY = 15; |
||||
|
|
||||
|
if (!targetNode.collapsed && targetIndex > targetStartIndex - 1) { |
||||
|
targetY = (targetIndex - targetStartIndex + 0.5) * itemHeight + 30; |
||||
|
targetY = Math.min(targetY, 80); |
||||
|
} |
||||
|
|
||||
|
const startPoint = { |
||||
|
...cfg.startPoint |
||||
|
}; |
||||
|
const endPoint = { |
||||
|
...cfg.endPoint |
||||
|
}; |
||||
|
|
||||
|
startPoint.y = startPoint.y + sourceY; |
||||
|
endPoint.y = endPoint.y + targetY; |
||||
|
|
||||
|
let shape; |
||||
|
if (sourceNode.id !== targetNode.id) { |
||||
|
shape = group.addShape("path", { |
||||
|
attrs: { |
||||
|
stroke: "#5B8FF9", |
||||
|
path: [ |
||||
|
["M", startPoint.x, startPoint.y], |
||||
|
[ |
||||
|
"C", |
||||
|
endPoint.x / 3 + (2 / 3) * startPoint.x, |
||||
|
startPoint.y, |
||||
|
endPoint.x / 3 + (2 / 3) * startPoint.x, |
||||
|
endPoint.y, |
||||
|
endPoint.x, |
||||
|
endPoint.y, |
||||
|
], |
||||
|
], |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: "path-shape", |
||||
|
}); |
||||
|
} else if (!sourceNode.collapsed) { |
||||
|
let gap = Math.abs((startPoint.y - endPoint.y) / 3); |
||||
|
if (startPoint["index"] === 1) { |
||||
|
gap = -gap; |
||||
|
} |
||||
|
shape = group.addShape("path", { |
||||
|
attrs: { |
||||
|
stroke: "#5B8FF9", |
||||
|
path: [ |
||||
|
["M", startPoint.x, startPoint.y], |
||||
|
[ |
||||
|
"C", |
||||
|
startPoint.x - gap, |
||||
|
startPoint.y, |
||||
|
startPoint.x - gap, |
||||
|
endPoint.y, |
||||
|
startPoint.x, |
||||
|
endPoint.y, |
||||
|
], |
||||
|
], |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: "path-shape", |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return shape; |
||||
|
}, |
||||
|
afterDraw(cfg, group) { |
||||
|
const labelCfg = cfg.labelCfg || {}; |
||||
|
const edge = group.cfg.item; |
||||
|
const sourceNode = edge.getSource().getModel(); |
||||
|
const targetNode = edge.getTarget().getModel(); |
||||
|
if (sourceNode.collapsed && targetNode.collapsed) { |
||||
|
return; |
||||
|
} |
||||
|
const path = group.find( |
||||
|
(element) => element.get("name") === "path-shape" |
||||
|
); |
||||
|
|
||||
|
const labelStyle = Util.getLabelPosition(path, 0.5, 0, 0, true); |
||||
|
const label = group.addShape("text", { |
||||
|
attrs: { |
||||
|
...labelStyle, |
||||
|
text: cfg.label || '', |
||||
|
fill: "#000", |
||||
|
textAlign: "center", |
||||
|
stroke: "#fff", |
||||
|
lineWidth: 1, |
||||
|
}, |
||||
|
}); |
||||
|
label.rotateAtStart(labelStyle.rotate); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registerNode("dice-er-box", { |
||||
|
draw(cfg, group) { |
||||
|
const width = 250; |
||||
|
const height = 96; |
||||
|
const itemCount = 10; |
||||
|
const boxStyle = { |
||||
|
stroke: "#096DD9", |
||||
|
radius: 4, |
||||
|
}; |
||||
|
|
||||
|
const { |
||||
|
attrs = [], |
||||
|
startIndex = 0, |
||||
|
selectedIndex, |
||||
|
collapsed, |
||||
|
icon, |
||||
|
} = cfg; |
||||
|
const list = attrs; |
||||
|
const afterList = list.slice( |
||||
|
Math.floor(startIndex), |
||||
|
Math.floor(startIndex + itemCount - 1) |
||||
|
); |
||||
|
const offsetY = (0.5 - (startIndex % 1)) * itemHeight + 30; |
||||
|
|
||||
|
group.addShape("rect", { |
||||
|
attrs: { |
||||
|
fill: boxStyle.stroke, |
||||
|
height: 30, |
||||
|
width, |
||||
|
radius: [boxStyle.radius, boxStyle.radius, 0, 0], |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
let fontLeft = 12; |
||||
|
|
||||
|
if (icon && icon.show !== false) { |
||||
|
group.addShape("image", { |
||||
|
attrs: { |
||||
|
x: 8, |
||||
|
y: 8, |
||||
|
height: 16, |
||||
|
width: 16, |
||||
|
...icon, |
||||
|
}, |
||||
|
}); |
||||
|
fontLeft += 18; |
||||
|
} |
||||
|
|
||||
|
group.addShape("text", { |
||||
|
attrs: { |
||||
|
y: 22, |
||||
|
x: fontLeft, |
||||
|
fill: "#fff", |
||||
|
text: cfg.label, |
||||
|
fontSize: 12, |
||||
|
fontWeight: 500, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
group.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: collapsed ? 30 : 80, |
||||
|
height: 15, |
||||
|
width, |
||||
|
fill: "#eee", |
||||
|
radius: [0, 0, boxStyle.radius, boxStyle.radius], |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: collapsed ? "expand" : "collapse", |
||||
|
}); |
||||
|
|
||||
|
group.addShape("text", { |
||||
|
attrs: { |
||||
|
x: width / 2 - 6, |
||||
|
y: (collapsed ? 30 : 80) + 12, |
||||
|
text: collapsed ? "+" : "-", |
||||
|
width, |
||||
|
fill: "#000", |
||||
|
radius: [0, 0, boxStyle.radius, boxStyle.radius], |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: collapsed ? "expand" : "collapse", |
||||
|
}); |
||||
|
|
||||
|
const keyshape = group.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: 0, |
||||
|
width, |
||||
|
height: collapsed ? 45 : height, |
||||
|
...boxStyle, |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (collapsed) { |
||||
|
return keyshape; |
||||
|
} |
||||
|
|
||||
|
const listContainer = group.addGroup({}); |
||||
|
listContainer.setClip({ |
||||
|
type: "rect", |
||||
|
attrs: { |
||||
|
x: -8, |
||||
|
y: 30, |
||||
|
width: width + 16, |
||||
|
height: 80 - 30, |
||||
|
}, |
||||
|
}); |
||||
|
listContainer.addShape({ |
||||
|
type: "rect", |
||||
|
attrs: { |
||||
|
x: 1, |
||||
|
y: 30, |
||||
|
width: width - 2, |
||||
|
height: 80 - 30, |
||||
|
fill: "#fff", |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (list.length > itemCount) { |
||||
|
const barStyle = { |
||||
|
width: 4, |
||||
|
padding: 0, |
||||
|
boxStyle: { |
||||
|
stroke: "#00000022", |
||||
|
}, |
||||
|
innerStyle: { |
||||
|
fill: "#00000022", |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
y: 30, |
||||
|
x: width - barStyle.padding - barStyle.width, |
||||
|
width: barStyle.width, |
||||
|
height: height - 30, |
||||
|
...barStyle.boxStyle, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const indexHeight = |
||||
|
afterList.length > itemCount ? |
||||
|
(afterList.length / list.length) * height : |
||||
|
10; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
y: 30 + |
||||
|
barStyle.padding + |
||||
|
(startIndex / list.length) * (height - 30), |
||||
|
x: width - barStyle.padding - barStyle.width, |
||||
|
width: barStyle.width, |
||||
|
height: Math.min(height, indexHeight), |
||||
|
...barStyle.innerStyle, |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
if (afterList) { |
||||
|
afterList.forEach((e, i) => { |
||||
|
const isSelected = |
||||
|
Math.floor(startIndex) + i === Number(selectedIndex); |
||||
|
let { |
||||
|
key = "", type |
||||
|
} = e; |
||||
|
if (type) { |
||||
|
key += " - " + type; |
||||
|
} |
||||
|
const label = key.length > 26 ? key.slice(0, 24) + "..." : key; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 1, |
||||
|
y: i * itemHeight - itemHeight / 2 + offsetY, |
||||
|
width: width - 4, |
||||
|
height: itemHeight, |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: `item-${Math.floor(startIndex) + i}-content`, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (!cfg.hideDot) { |
||||
|
listContainer.addShape("circle", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: i * itemHeight + offsetY, |
||||
|
r: 3, |
||||
|
stroke: boxStyle.stroke, |
||||
|
fill: "white", |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
}); |
||||
|
listContainer.addShape("circle", { |
||||
|
attrs: { |
||||
|
x: width, |
||||
|
y: i * itemHeight + offsetY, |
||||
|
r: 3, |
||||
|
stroke: boxStyle.stroke, |
||||
|
fill: "white", |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
listContainer.addShape("text", { |
||||
|
attrs: { |
||||
|
x: 12, |
||||
|
y: i * itemHeight + offsetY + 6, |
||||
|
text: label, |
||||
|
fontSize: 12, |
||||
|
fill: "#000", |
||||
|
fontFamily: "Avenir,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol", |
||||
|
full: e, |
||||
|
fontWeight: isSelected ? 500 : 100, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: `item-${Math.floor(startIndex) + i}`, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
return keyshape; |
||||
|
}, |
||||
|
getAnchorPoints() { |
||||
|
return [ |
||||
|
[0, 0], |
||||
|
[1, 0], |
||||
|
]; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const dataTransform = (data) => { |
||||
|
const nodes = []; |
||||
|
const edges = []; |
||||
|
|
||||
|
data.map((node) => { |
||||
|
nodes.push({ |
||||
|
...node |
||||
|
}); |
||||
|
if (node.attrs) { |
||||
|
node.attrs.forEach((attr) => { |
||||
|
if (attr.relation) { |
||||
|
attr.relation.forEach((relation) => { |
||||
|
edges.push({ |
||||
|
source: node.id, |
||||
|
target: relation.nodeId, |
||||
|
sourceKey: attr.key, |
||||
|
targetKey: relation.key, |
||||
|
label: relation.label, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
nodes, |
||||
|
edges, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const container = document.getElementById(props.g6Index); |
||||
|
const width = props.is_fullscreen?document.documentElement.scrollWidth:container.scrollWidth; |
||||
|
const height = props.is_fullscreen?document.documentElement.scrollHeight:((container.scrollHeight || 500) - 20); |
||||
|
const graph = new G6.Graph({ |
||||
|
container: container, |
||||
|
width, |
||||
|
height, |
||||
|
defaultNode: { |
||||
|
size: [300, 200], |
||||
|
type: 'dice-er-box', |
||||
|
color: '#5B8FF9', |
||||
|
style: { |
||||
|
fill: '#9EC9FF', |
||||
|
lineWidth: 3, |
||||
|
}, |
||||
|
labelCfg: { |
||||
|
style: { |
||||
|
fill: 'black', |
||||
|
fontSize: 20, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
defaultEdge: { |
||||
|
type: 'dice-er-edge', |
||||
|
style: { |
||||
|
stroke: '#e2e2e2', |
||||
|
lineWidth: 4, |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
}, |
||||
|
modes: { |
||||
|
default: ['dice-er-scroll', 'drag-node', 'drag-canvas'], |
||||
|
}, |
||||
|
layout: { |
||||
|
type: 'dagre', |
||||
|
rankdir: 'LR', |
||||
|
align: 'UL', |
||||
|
controlPoints: true, |
||||
|
nodesepFunc: () => 0.2, |
||||
|
ranksepFunc: () => 0.5, |
||||
|
}, |
||||
|
animate: true, |
||||
|
fitView: true |
||||
|
}) |
||||
|
graph.data(dataTransform(props.g6Data)) |
||||
|
graph.render(); |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
initG6(false) |
||||
|
}); |
||||
|
watch(() => props.is_large, |
||||
|
(value) =>{ |
||||
|
let g6 = document.getElementById(props.g6Index) |
||||
|
g6.innerHTML='' |
||||
|
initG6(false) |
||||
|
} |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
|
||||
|
</style> |
@ -0,0 +1,39 @@ |
|||||
|
<template> |
||||
|
<el-tooltip |
||||
|
v-bind="$attrs" |
||||
|
:disabled="!(containerWeight > contentWeight)" |
||||
|
effect="dark" |
||||
|
placement="bottom" |
||||
|
popper-class="auto-tooltip-popper" |
||||
|
> |
||||
|
<div ref="tagLabel" :class="['auto-tooltip', className]" :style="style"> |
||||
|
<slot></slot> |
||||
|
</div> |
||||
|
</el-tooltip> |
||||
|
</template> |
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, onMounted, nextTick } from 'vue' |
||||
|
defineOptions({ name: 'AutoTooltip' }) |
||||
|
const props = defineProps({ className: String, style: Object }) |
||||
|
const tagLabel = ref() |
||||
|
const containerWeight = ref(0) |
||||
|
const contentWeight = ref(0) |
||||
|
|
||||
|
onMounted(() => { |
||||
|
nextTick(() => { |
||||
|
containerWeight.value = tagLabel.value?.scrollWidth |
||||
|
contentWeight.value = tagLabel.value?.clientWidth |
||||
|
}) |
||||
|
window.addEventListener('resize', function () { |
||||
|
containerWeight.value = tagLabel.value?.scrollWidth |
||||
|
contentWeight.value = tagLabel.value?.clientWidth |
||||
|
}) |
||||
|
}) |
||||
|
</script> |
||||
|
<style lang="scss" scoped> |
||||
|
.auto-tooltip { |
||||
|
overflow: hidden; |
||||
|
white-space: nowrap; |
||||
|
text-overflow: ellipsis; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,42 @@ |
|||||
|
<template> |
||||
|
<div style="width: 100%;height: 100%"> |
||||
|
<el-table :data="tableData" style="width: 100%; margin-top: 5px"> |
||||
|
<el-table-column v-for="(value,key) in tableData[0]" :prop="key" :label="key"></el-table-column> |
||||
|
</el-table> |
||||
|
<div style="width: 100%; height: 32px"> |
||||
|
<el-pagination |
||||
|
style="position: absolute;right: 0" |
||||
|
@current-change="handleCurrentChange" |
||||
|
@size-change="handleSizeChange" |
||||
|
:current-page="currentPage" |
||||
|
:page-sizes="[10, 20, 30, 40]" |
||||
|
:page-size="pageSize" |
||||
|
layout="total, prev, pager, next" |
||||
|
:total="data.length"> |
||||
|
</el-pagination> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import {computed, ref} from "vue"; |
||||
|
const props = defineProps({ |
||||
|
data: Array |
||||
|
}) |
||||
|
const currentPage = ref(1) |
||||
|
const pageSize = ref(10) |
||||
|
function handleCurrentChange(page) { |
||||
|
currentPage.value = page; |
||||
|
} |
||||
|
function handleSizeChange(size) { |
||||
|
pageSize.value = size; |
||||
|
} |
||||
|
|
||||
|
const tableData = computed(() =>{ |
||||
|
const start = (currentPage.value - 1) * pageSize.value; |
||||
|
const end = start + pageSize.value; |
||||
|
return props.data.slice(start, end); |
||||
|
}) |
||||
|
</script> |
||||
|
<style scoped lang="scss"> |
||||
|
|
||||
|
</style> |
@ -0,0 +1,69 @@ |
|||||
|
<template> |
||||
|
<div class="common-list"> |
||||
|
<ul v-if="data.length > 0" style="list-style: none;margin: 0;padding: 0"> |
||||
|
<template v-for="(item, index) in data" :key="index"> |
||||
|
<li |
||||
|
@click.prevent="clickHandle(item, index)" |
||||
|
class="cursor" |
||||
|
:class="mouseId === item.sessionId ? 'active' : ''" |
||||
|
@mouseenter="mouseenter(item)" |
||||
|
@mouseleave="mouseleave()" |
||||
|
> |
||||
|
<slot :row="item" :index="index"> </slot> |
||||
|
</li> |
||||
|
</template> |
||||
|
</ul> |
||||
|
<slot name="empty" v-else> |
||||
|
<el-empty description="暂无数据" /> |
||||
|
</slot> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import { ref, watch } from 'vue' |
||||
|
import {getChatList} from "@/api/aichat/aichat.js"; |
||||
|
|
||||
|
defineOptions({ name: 'CommonList' }) |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
mouseId: String, |
||||
|
data: { |
||||
|
type: Array, |
||||
|
default: [] |
||||
|
}, |
||||
|
defaultActive: '' |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
const emit = defineEmits(['click', 'mouseenter', 'mouseleave']) |
||||
|
|
||||
|
function mouseenter(row) { |
||||
|
emit('mouseenter', row) |
||||
|
} |
||||
|
function mouseleave() { |
||||
|
emit('mouseleave') |
||||
|
} |
||||
|
|
||||
|
function clickHandle(row, index) { |
||||
|
emit('click', row) |
||||
|
} |
||||
|
|
||||
|
</script> |
||||
|
<style lang="scss" scoped> |
||||
|
/* 通用 ui li样式 */ |
||||
|
.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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.cursor { |
||||
|
cursor: pointer; |
||||
|
padding: 6px 16px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,604 @@ |
|||||
|
<template> |
||||
|
<div id="fullscreenG6" style="width: 100%;height: 100%; overflow: hidden"></div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import G6 from '@antv/g6'; |
||||
|
import {watch} from "vue"; |
||||
|
const props = defineProps({ |
||||
|
g6Data: { |
||||
|
type: Array, |
||||
|
default: () => {} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
function initG6(){ |
||||
|
const { |
||||
|
Util, |
||||
|
registerBehavior, |
||||
|
registerEdge, |
||||
|
registerNode |
||||
|
} = G6; |
||||
|
const isInBBox = (point, bbox) => { |
||||
|
const { |
||||
|
x, |
||||
|
y |
||||
|
} = point; |
||||
|
const { |
||||
|
minX, |
||||
|
minY, |
||||
|
maxX, |
||||
|
maxY |
||||
|
} = bbox; |
||||
|
|
||||
|
return x < maxX && x > minX && y > minY && y < maxY; |
||||
|
}; |
||||
|
const itemHeight = 20; |
||||
|
registerBehavior("dice-er-scroll", { |
||||
|
getDefaultCfg() { |
||||
|
return { |
||||
|
multiple: true, |
||||
|
}; |
||||
|
}, |
||||
|
getEvents() { |
||||
|
return { |
||||
|
itemHeight, |
||||
|
wheel: "scorll", |
||||
|
click: "click", |
||||
|
"node:mousemove": "move", |
||||
|
"node:mousedown": "mousedown", |
||||
|
"node:mouseup": "mouseup" |
||||
|
}; |
||||
|
}, |
||||
|
scorll(e) { |
||||
|
e.preventDefault(); |
||||
|
const { |
||||
|
graph |
||||
|
} = this; |
||||
|
const nodes = graph.getNodes().filter((n) => { |
||||
|
const bbox = n.getBBox(); |
||||
|
|
||||
|
return isInBBox(graph.getPointByClient(e.clientX, e.clientY), bbox); |
||||
|
}); |
||||
|
|
||||
|
const x = e.deltaX || e.movementX; |
||||
|
let y = e.deltaY || e.movementY; |
||||
|
if (!y && navigator.userAgent.indexOf('Firefox') > -1) y = (-e.wheelDelta * 125) / 3 |
||||
|
|
||||
|
if (nodes) { |
||||
|
const edgesToUpdate = new Set(); |
||||
|
nodes.forEach((node) => { |
||||
|
const model = node.getModel(); |
||||
|
if (model.attrs.length < 2) { |
||||
|
return; |
||||
|
} |
||||
|
const idx = model.startIndex || 0; |
||||
|
let startX = model.startX || 0.5; |
||||
|
let startIndex = idx + y * 0.02; |
||||
|
startX -= x; |
||||
|
if (startIndex < 0) { |
||||
|
startIndex = 0; |
||||
|
} |
||||
|
if (startX > 0) { |
||||
|
startX = 0; |
||||
|
} |
||||
|
if (startIndex > model.attrs.length - 1) { |
||||
|
startIndex = model.attrs.length - 1; |
||||
|
} |
||||
|
graph.updateItem(node, { |
||||
|
startIndex, |
||||
|
startX, |
||||
|
}); |
||||
|
node.getEdges().forEach(edge => edgesToUpdate.add(edge)) |
||||
|
}); |
||||
|
// G6 update the related edges when graph.updateItem with a node according to the new properties |
||||
|
// here you need to update the related edges manualy since the new properties { startIndex, startX } for the nodes are custom, and cannot be recognized by G6 |
||||
|
edgesToUpdate.forEach(edge => edge.refresh()) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
}, |
||||
|
click(e) { |
||||
|
const { |
||||
|
graph |
||||
|
} = this; |
||||
|
const item = e.item; |
||||
|
const shape = e.shape; |
||||
|
if (!item) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (shape.get("name") === "collapse") { |
||||
|
graph.updateItem(item, { |
||||
|
collapsed: true, |
||||
|
size: [300, 50], |
||||
|
}); |
||||
|
setTimeout(() => graph.layout(), 100); |
||||
|
} else if (shape.get("name") === "expand") { |
||||
|
graph.updateItem(item, { |
||||
|
collapsed: false, |
||||
|
size: [300, 80], |
||||
|
}); |
||||
|
setTimeout(() => graph.layout(), 100); |
||||
|
} |
||||
|
}, |
||||
|
mousedown(e) { |
||||
|
this.isMousedown = true; |
||||
|
}, |
||||
|
mouseup(e) { |
||||
|
this.isMousedown = false; |
||||
|
}, |
||||
|
move(e) { |
||||
|
if (this.isMousedown) return; |
||||
|
const name = e.shape.get("name"); |
||||
|
const item = e.item; |
||||
|
|
||||
|
if (name && name.startsWith("item")) { |
||||
|
this.graph.updateItem(item, { |
||||
|
selectedIndex: Number(name.split("-")[1]), |
||||
|
}); |
||||
|
} else { |
||||
|
this.graph.updateItem(item, { |
||||
|
selectedIndex: NaN, |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registerEdge("dice-er-edge", { |
||||
|
draw(cfg, group) { |
||||
|
const edge = group.cfg.item; |
||||
|
const sourceNode = edge.getSource().getModel(); |
||||
|
const targetNode = edge.getTarget().getModel(); |
||||
|
|
||||
|
const sourceIndex = sourceNode.attrs.findIndex( |
||||
|
(e) => e.key === cfg.sourceKey |
||||
|
); |
||||
|
|
||||
|
const sourceStartIndex = sourceNode.startIndex || 0; |
||||
|
|
||||
|
let sourceY = 15; |
||||
|
|
||||
|
if (!sourceNode.collapsed && sourceIndex > sourceStartIndex - 1) { |
||||
|
sourceY = 30 + (sourceIndex - sourceStartIndex + 0.5) * itemHeight; |
||||
|
sourceY = Math.min(sourceY, 80); |
||||
|
} |
||||
|
|
||||
|
const targetIndex = targetNode.attrs.findIndex( |
||||
|
(e) => e.key === cfg.targetKey |
||||
|
); |
||||
|
|
||||
|
const targetStartIndex = targetNode.startIndex || 0; |
||||
|
|
||||
|
let targetY = 15; |
||||
|
|
||||
|
if (!targetNode.collapsed && targetIndex > targetStartIndex - 1) { |
||||
|
targetY = (targetIndex - targetStartIndex + 0.5) * itemHeight + 30; |
||||
|
targetY = Math.min(targetY, 80); |
||||
|
} |
||||
|
|
||||
|
const startPoint = { |
||||
|
...cfg.startPoint |
||||
|
}; |
||||
|
const endPoint = { |
||||
|
...cfg.endPoint |
||||
|
}; |
||||
|
|
||||
|
startPoint.y = startPoint.y + sourceY; |
||||
|
endPoint.y = endPoint.y + targetY; |
||||
|
|
||||
|
let shape; |
||||
|
if (sourceNode.id !== targetNode.id) { |
||||
|
shape = group.addShape("path", { |
||||
|
attrs: { |
||||
|
stroke: "#5B8FF9", |
||||
|
path: [ |
||||
|
["M", startPoint.x, startPoint.y], |
||||
|
[ |
||||
|
"C", |
||||
|
endPoint.x / 3 + (2 / 3) * startPoint.x, |
||||
|
startPoint.y, |
||||
|
endPoint.x / 3 + (2 / 3) * startPoint.x, |
||||
|
endPoint.y, |
||||
|
endPoint.x, |
||||
|
endPoint.y, |
||||
|
], |
||||
|
], |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: "path-shape", |
||||
|
}); |
||||
|
} else if (!sourceNode.collapsed) { |
||||
|
let gap = Math.abs((startPoint.y - endPoint.y) / 3); |
||||
|
if (startPoint["index"] === 1) { |
||||
|
gap = -gap; |
||||
|
} |
||||
|
shape = group.addShape("path", { |
||||
|
attrs: { |
||||
|
stroke: "#5B8FF9", |
||||
|
path: [ |
||||
|
["M", startPoint.x, startPoint.y], |
||||
|
[ |
||||
|
"C", |
||||
|
startPoint.x - gap, |
||||
|
startPoint.y, |
||||
|
startPoint.x - gap, |
||||
|
endPoint.y, |
||||
|
startPoint.x, |
||||
|
endPoint.y, |
||||
|
], |
||||
|
], |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: "path-shape", |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return shape; |
||||
|
}, |
||||
|
afterDraw(cfg, group) { |
||||
|
const labelCfg = cfg.labelCfg || {}; |
||||
|
const edge = group.cfg.item; |
||||
|
const sourceNode = edge.getSource().getModel(); |
||||
|
const targetNode = edge.getTarget().getModel(); |
||||
|
if (sourceNode.collapsed && targetNode.collapsed) { |
||||
|
return; |
||||
|
} |
||||
|
const path = group.find( |
||||
|
(element) => element.get("name") === "path-shape" |
||||
|
); |
||||
|
|
||||
|
const labelStyle = Util.getLabelPosition(path, 0.5, 0, 0, true); |
||||
|
const label = group.addShape("text", { |
||||
|
attrs: { |
||||
|
...labelStyle, |
||||
|
text: cfg.label || '', |
||||
|
fill: "#000", |
||||
|
textAlign: "center", |
||||
|
stroke: "#fff", |
||||
|
lineWidth: 1, |
||||
|
}, |
||||
|
}); |
||||
|
label.rotateAtStart(labelStyle.rotate); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
registerNode("dice-er-box", { |
||||
|
draw(cfg, group) { |
||||
|
const width = 250; |
||||
|
const height = 96; |
||||
|
const itemCount = 10; |
||||
|
const boxStyle = { |
||||
|
stroke: "#096DD9", |
||||
|
radius: 4, |
||||
|
}; |
||||
|
|
||||
|
const { |
||||
|
attrs = [], |
||||
|
startIndex = 0, |
||||
|
selectedIndex, |
||||
|
collapsed, |
||||
|
icon, |
||||
|
} = cfg; |
||||
|
const list = attrs; |
||||
|
const afterList = list.slice( |
||||
|
Math.floor(startIndex), |
||||
|
Math.floor(startIndex + itemCount - 1) |
||||
|
); |
||||
|
const offsetY = (0.5 - (startIndex % 1)) * itemHeight + 30; |
||||
|
|
||||
|
group.addShape("rect", { |
||||
|
attrs: { |
||||
|
fill: boxStyle.stroke, |
||||
|
height: 30, |
||||
|
width, |
||||
|
radius: [boxStyle.radius, boxStyle.radius, 0, 0], |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
let fontLeft = 12; |
||||
|
|
||||
|
if (icon && icon.show !== false) { |
||||
|
group.addShape("image", { |
||||
|
attrs: { |
||||
|
x: 8, |
||||
|
y: 8, |
||||
|
height: 16, |
||||
|
width: 16, |
||||
|
...icon, |
||||
|
}, |
||||
|
}); |
||||
|
fontLeft += 18; |
||||
|
} |
||||
|
|
||||
|
group.addShape("text", { |
||||
|
attrs: { |
||||
|
y: 22, |
||||
|
x: fontLeft, |
||||
|
fill: "#fff", |
||||
|
text: cfg.label, |
||||
|
fontSize: 12, |
||||
|
fontWeight: 500, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
group.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: collapsed ? 30 : 80, |
||||
|
height: 15, |
||||
|
width, |
||||
|
fill: "#eee", |
||||
|
radius: [0, 0, boxStyle.radius, boxStyle.radius], |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: collapsed ? "expand" : "collapse", |
||||
|
}); |
||||
|
|
||||
|
group.addShape("text", { |
||||
|
attrs: { |
||||
|
x: width / 2 - 6, |
||||
|
y: (collapsed ? 30 : 80) + 12, |
||||
|
text: collapsed ? "+" : "-", |
||||
|
width, |
||||
|
fill: "#000", |
||||
|
radius: [0, 0, boxStyle.radius, boxStyle.radius], |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: collapsed ? "expand" : "collapse", |
||||
|
}); |
||||
|
|
||||
|
const keyshape = group.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: 0, |
||||
|
width, |
||||
|
height: collapsed ? 45 : height, |
||||
|
...boxStyle, |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (collapsed) { |
||||
|
return keyshape; |
||||
|
} |
||||
|
|
||||
|
const listContainer = group.addGroup({}); |
||||
|
listContainer.setClip({ |
||||
|
type: "rect", |
||||
|
attrs: { |
||||
|
x: -8, |
||||
|
y: 30, |
||||
|
width: width + 16, |
||||
|
height: 80 - 30, |
||||
|
}, |
||||
|
}); |
||||
|
listContainer.addShape({ |
||||
|
type: "rect", |
||||
|
attrs: { |
||||
|
x: 1, |
||||
|
y: 30, |
||||
|
width: width - 2, |
||||
|
height: 80 - 30, |
||||
|
fill: "#fff", |
||||
|
}, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (list.length > itemCount) { |
||||
|
const barStyle = { |
||||
|
width: 4, |
||||
|
padding: 0, |
||||
|
boxStyle: { |
||||
|
stroke: "#00000022", |
||||
|
}, |
||||
|
innerStyle: { |
||||
|
fill: "#00000022", |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
y: 30, |
||||
|
x: width - barStyle.padding - barStyle.width, |
||||
|
width: barStyle.width, |
||||
|
height: height - 30, |
||||
|
...barStyle.boxStyle, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const indexHeight = |
||||
|
afterList.length > itemCount ? |
||||
|
(afterList.length / list.length) * height : |
||||
|
10; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
y: 30 + |
||||
|
barStyle.padding + |
||||
|
(startIndex / list.length) * (height - 30), |
||||
|
x: width - barStyle.padding - barStyle.width, |
||||
|
width: barStyle.width, |
||||
|
height: Math.min(height, indexHeight), |
||||
|
...barStyle.innerStyle, |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
if (afterList) { |
||||
|
afterList.forEach((e, i) => { |
||||
|
const isSelected = |
||||
|
Math.floor(startIndex) + i === Number(selectedIndex); |
||||
|
let { |
||||
|
key = "", type |
||||
|
} = e; |
||||
|
if (type) { |
||||
|
key += " - " + type; |
||||
|
} |
||||
|
const label = key.length > 26 ? key.slice(0, 24) + "..." : key; |
||||
|
|
||||
|
listContainer.addShape("rect", { |
||||
|
attrs: { |
||||
|
x: 1, |
||||
|
y: i * itemHeight - itemHeight / 2 + offsetY, |
||||
|
width: width - 4, |
||||
|
height: itemHeight, |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: `item-${Math.floor(startIndex) + i}-content`, |
||||
|
draggable: true, |
||||
|
}); |
||||
|
|
||||
|
if (!cfg.hideDot) { |
||||
|
listContainer.addShape("circle", { |
||||
|
attrs: { |
||||
|
x: 0, |
||||
|
y: i * itemHeight + offsetY, |
||||
|
r: 3, |
||||
|
stroke: boxStyle.stroke, |
||||
|
fill: "white", |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
}); |
||||
|
listContainer.addShape("circle", { |
||||
|
attrs: { |
||||
|
x: width, |
||||
|
y: i * itemHeight + offsetY, |
||||
|
r: 3, |
||||
|
stroke: boxStyle.stroke, |
||||
|
fill: "white", |
||||
|
radius: 2, |
||||
|
lineWidth: 1, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
listContainer.addShape("text", { |
||||
|
attrs: { |
||||
|
x: 12, |
||||
|
y: i * itemHeight + offsetY + 6, |
||||
|
text: label, |
||||
|
fontSize: 12, |
||||
|
fill: "#000", |
||||
|
fontFamily: "Avenir,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol", |
||||
|
full: e, |
||||
|
fontWeight: isSelected ? 500 : 100, |
||||
|
cursor: "pointer", |
||||
|
}, |
||||
|
// must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type |
||||
|
name: `item-${Math.floor(startIndex) + i}`, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
return keyshape; |
||||
|
}, |
||||
|
getAnchorPoints() { |
||||
|
return [ |
||||
|
[0, 0], |
||||
|
[1, 0], |
||||
|
]; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const dataTransform = (data) => { |
||||
|
const nodes = []; |
||||
|
const edges = []; |
||||
|
|
||||
|
data.map((node) => { |
||||
|
nodes.push({ |
||||
|
...node |
||||
|
}); |
||||
|
if (node.attrs) { |
||||
|
node.attrs.forEach((attr) => { |
||||
|
if (attr.relation) { |
||||
|
attr.relation.forEach((relation) => { |
||||
|
edges.push({ |
||||
|
source: node.id, |
||||
|
target: relation.nodeId, |
||||
|
sourceKey: attr.key, |
||||
|
targetKey: relation.key, |
||||
|
label: relation.label, |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
nodes, |
||||
|
edges, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const container = document.getElementById("fullscreenG6"); |
||||
|
const width = document.documentElement.scrollWidth - 30; |
||||
|
const height = document.documentElement.scrollHeight - 30; |
||||
|
const graph = new G6.Graph({ |
||||
|
container: container, |
||||
|
width, |
||||
|
height, |
||||
|
defaultNode: { |
||||
|
size: [300, 200], |
||||
|
type: 'dice-er-box', |
||||
|
color: '#5B8FF9', |
||||
|
style: { |
||||
|
fill: '#9EC9FF', |
||||
|
lineWidth: 3, |
||||
|
}, |
||||
|
labelCfg: { |
||||
|
style: { |
||||
|
fill: 'black', |
||||
|
fontSize: 20, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
defaultEdge: { |
||||
|
type: 'dice-er-edge', |
||||
|
style: { |
||||
|
stroke: '#e2e2e2', |
||||
|
lineWidth: 4, |
||||
|
endArrow: true, |
||||
|
}, |
||||
|
}, |
||||
|
modes: { |
||||
|
default: ['dice-er-scroll', 'drag-node', 'drag-canvas'], |
||||
|
}, |
||||
|
layout: { |
||||
|
type: 'dagre', |
||||
|
rankdir: 'LR', |
||||
|
align: 'UL', |
||||
|
controlPoints: true, |
||||
|
nodesepFunc: () => 0.2, |
||||
|
ranksepFunc: () => 0.5, |
||||
|
}, |
||||
|
animate: true, |
||||
|
fitView: true |
||||
|
}) |
||||
|
graph.data(dataTransform(props.g6Data)) |
||||
|
graph.render(); |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
initG6() |
||||
|
}); |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
|
||||
|
</style> |
@ -0,0 +1,21 @@ |
|||||
|
<template> |
||||
|
<iframe :id="index" :srcDoc="data" style="width: 100%;height: 100%;border: none"></iframe> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import {watch} from "vue"; |
||||
|
const props = defineProps({ |
||||
|
is_large: Boolean, |
||||
|
data: String, |
||||
|
index: String, |
||||
|
}) |
||||
|
watch(() => props.is_large, |
||||
|
(value) =>{ |
||||
|
let iframe = document.getElementById(props.index) |
||||
|
iframe.contentWindow.location.reload() |
||||
|
} |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped lang="scss"> |
||||
|
|
||||
|
</style> |
@ -0,0 +1,259 @@ |
|||||
|
<template> |
||||
|
<div class="chat-embed layout-bg"> |
||||
|
<div class="chat-embed__header"> |
||||
|
<div class="chat-width flex align-center" style="height: 56px;align-items: center;line-height: 56px"> |
||||
|
<div class="mr-12 ml-24 flex"> |
||||
|
<el-avatar |
||||
|
shape="square" |
||||
|
:size="32" |
||||
|
style="background: none" |
||||
|
> |
||||
|
<img src="@/assets/logo/deepseek.png" alt="" /> |
||||
|
</el-avatar> |
||||
|
</div> |
||||
|
<h4 style="color: #1f2329;font-size: 16px; font-style: normal; font-weight: bold;margin: 0; -webkit-font-smoothing: antialiased">果知小助手</h4> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="chat-embed__main"> |
||||
|
<AiChat |
||||
|
ref="AiChatRef" |
||||
|
:record="currentRecordList" |
||||
|
:is_large="is_large" |
||||
|
:cookieSessionId = "sessionId" |
||||
|
class="AiChat-embed" |
||||
|
@scroll="handleScroll" |
||||
|
></AiChat> |
||||
|
</div> |
||||
|
<el-link class="chat-popover-button" :underline="false" @click.prevent.stop="showChatHistory"> |
||||
|
<i class="ri-history-line"></i> |
||||
|
</el-link> |
||||
|
<el-link style="z-index: 100; position: absolute; top: 18px; right: 120px; font-size: 20px;" :underline="false" @click.prevent.stop="newChat" :disabled="currentRecordList.length === 0"> |
||||
|
<el-icon><Plus /></el-icon> |
||||
|
</el-link> |
||||
|
<el-collapse-transition> |
||||
|
<div v-show="show" class="chat-popover w-full" style="width: 100%;" v-click-outside="clickoutside"> |
||||
|
<div class="border-b p-16-24" style="border-bottom: 1px solid #dee0e3; padding: 16px 24px; font-size: 14px"> |
||||
|
<span>历史记录</span> |
||||
|
</div> |
||||
|
<el-scrollbar max-height="300"> |
||||
|
<div class="p-8" style="padding: 8px"> |
||||
|
<common-list |
||||
|
:data="chatLogeData" |
||||
|
:mouseId="mouseId" |
||||
|
:defaultActive="sessionId" |
||||
|
@click="clickListHandle" |
||||
|
@mouseenter="mouseenter" |
||||
|
@mouseleave="mouseId = ''" |
||||
|
> |
||||
|
<template #default="{ row }"> |
||||
|
<div class="flex-between" style="display: flex; justify-content: space-between; align-items: center;"> |
||||
|
<auto-tooltip :content="row.sessionName"> |
||||
|
{{ row.sessionName }} |
||||
|
</auto-tooltip> |
||||
|
<div @click.stop v-if="mouseId === row.sessionId"> |
||||
|
<el-button style="padding: 0" link @click.stop="deleteLog(row)"> |
||||
|
<el-icon><Delete /></el-icon> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</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" style="font-weight: 400; margin-top: 8px"> |
||||
|
<span>仅显示最近 20 条对话</span> |
||||
|
</div> |
||||
|
</el-scrollbar> |
||||
|
</div> |
||||
|
</el-collapse-transition> |
||||
|
<div class="chat-popover-mask" v-show="show"></div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref, onMounted, reactive, nextTick, computed } from 'vue' |
||||
|
import { v4 as uuidv4 } from 'uuid'; |
||||
|
import Cookies from 'js-cookie' |
||||
|
import CommonList from './common-list.vue' |
||||
|
import AiChat from './aichat.vue' |
||||
|
import AutoTooltip from './auto-tooltip.vue' |
||||
|
import { useRoute } from 'vue-router' |
||||
|
import { listChatHistory, getChatList, DeleteChatSession } from "@/api/aichat/aichat"; |
||||
|
const route = useRoute() |
||||
|
const { proxy } = getCurrentInstance(); |
||||
|
const { |
||||
|
params: { accessToken } |
||||
|
} = route |
||||
|
const props = defineProps({ |
||||
|
is_large: Boolean, |
||||
|
chatDataList: Array, |
||||
|
}) |
||||
|
const AiChatRef = ref() |
||||
|
const chatLogeData = ref([]) |
||||
|
const show = ref(false) |
||||
|
const currentRecordList = ref([]) |
||||
|
const sessionId = ref(Cookies.get("chatSessionId")) // 当前历史记录Id 默认为'new' |
||||
|
const mouseId = ref('') |
||||
|
|
||||
|
function mouseenter(row) { |
||||
|
mouseId.value = row.sessionId |
||||
|
} |
||||
|
function deleteLog(row) { |
||||
|
let sessionId = row.sessionId |
||||
|
DeleteChatSession(sessionId).then(res=>{ |
||||
|
listChatHistory(sessionId).then(response =>{ |
||||
|
chatLogeData.value = response.data |
||||
|
}) |
||||
|
proxy.$modal.msgSuccess("复制成功"); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function clickoutside() { |
||||
|
show.value = false |
||||
|
} |
||||
|
|
||||
|
function showChatHistory(){ |
||||
|
show.value = true |
||||
|
listChatHistory(sessionId.value).then(response =>{ |
||||
|
chatLogeData.value = response.data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
function newChat() { |
||||
|
currentRecordList.value = [] |
||||
|
sessionId.value = uuidv4() |
||||
|
Cookies.set("chatSessionId",sessionId.value) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
function handleScroll(event) { |
||||
|
if (event.scrollTop === 0 && currentRecordList.value.length>0) { |
||||
|
const history_height = event.dialogScrollbar.offsetHeight |
||||
|
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function clickListHandle(item){ |
||||
|
getChatList(item.sessionId).then(res=>{ |
||||
|
currentRecordList.value = [] |
||||
|
let array = res.data |
||||
|
for (let i = 0; i < array.length; i++) { |
||||
|
if (array[i].type === 'answer'){ |
||||
|
array[i].content = JSON.parse(array[i].content) |
||||
|
} |
||||
|
if (array[i].type === 'question'){ |
||||
|
array[i].file = JSON.parse(array[i].file) |
||||
|
} |
||||
|
currentRecordList.value.push(array[i]) |
||||
|
AiChatRef.value.setScrollBottom() |
||||
|
} |
||||
|
show.value = false |
||||
|
sessionId.value = item.sessionId |
||||
|
Cookies.set("chatSessionId",sessionId.value) |
||||
|
}) |
||||
|
} |
||||
|
watch(() => props.chatDataList, value => currentRecordList.value = JSON.parse(JSON.stringify(value))) |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.chat-embed { |
||||
|
overflow: hidden; |
||||
|
height: 100%; |
||||
|
&__header { |
||||
|
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);; |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
left: 0; |
||||
|
top: 0; |
||||
|
z-index: 100; |
||||
|
height: 56px; |
||||
|
line-height: 56px; |
||||
|
box-sizing: border-box; |
||||
|
border-bottom: 1px solid #dee0e3; |
||||
|
} |
||||
|
&__main { |
||||
|
padding-top: 80px; |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
.new-chat-button { |
||||
|
z-index: 11; |
||||
|
} |
||||
|
// 历史对话弹出层 |
||||
|
.chat-popover { |
||||
|
position: absolute; |
||||
|
top: 56px; |
||||
|
background: #ffffff; |
||||
|
padding-bottom: 24px; |
||||
|
z-index: 2009; |
||||
|
} |
||||
|
.chat-popover-button { |
||||
|
z-index: 100; |
||||
|
position: absolute; |
||||
|
top: 18px; |
||||
|
right: 85px; |
||||
|
font-size: 20px; |
||||
|
} |
||||
|
.chat-popover-mask { |
||||
|
background-color: var(--el-overlay-color-lighter); |
||||
|
bottom: 0; |
||||
|
height: 100%; |
||||
|
left: 0; |
||||
|
overflow: auto; |
||||
|
position: absolute; |
||||
|
right: 0; |
||||
|
top: 56px; |
||||
|
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%; |
||||
|
} |
||||
|
} |
||||
|
.AiChat-embed { |
||||
|
.ai-chat__operate { |
||||
|
padding-top: 12px; |
||||
|
} |
||||
|
} |
||||
|
.chat-width { |
||||
|
max-width: 860px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
} |
||||
|
.flex { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
.mr-12 { |
||||
|
margin-right: 12px; |
||||
|
|
||||
|
} |
||||
|
.ml-24 { |
||||
|
margin-left: 24px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,23 @@ |
|||||
|
<template> |
||||
|
<div class="markdown-content" v-html="compiledMarkdown" style="font-size: 14px"></div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
import { ref, watch} from 'vue'; |
||||
|
import MarkdownIt from 'markdown-it'; |
||||
|
const props = defineProps({ |
||||
|
markdownString: String |
||||
|
}) |
||||
|
const compiledMarkdown = ref('') |
||||
|
const md = new MarkdownIt() |
||||
|
watch(() => props.markdownString, (newValue) => { |
||||
|
if (newValue){ |
||||
|
compiledMarkdown.value = md.render(newValue.replace("\n","\n\n")); |
||||
|
} |
||||
|
}, { immediate: true }); |
||||
|
</script> |
||||
|
<style lang="scss"> |
||||
|
.markdown-content a { |
||||
|
text-decoration: underline; |
||||
|
color: blue |
||||
|
} |
||||
|
</style> |