Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangjiahao
2025-07-23 16:32:03 +08:00
28 changed files with 821 additions and 331 deletions
+6
View File
@@ -14,6 +14,12 @@ FIRST_SUPERUSER_PASSWORD=123456 # Change this to your pwd
TOKEN_KEY="X-SQLBOT-TOKEN"
DEFAULT_PWD="SQLBot@123456"
ASSISTANT_TOKEN_KEY="X-SQLBOT-ASSISTANT-TOKEN"
LOG_LEVEL="INFO" # DEBUG, INFO, WARNING, ERROR
LOG_DIR="/opt/sqlbot/logs"
LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
SQL_DEBUG=False
# Postgres
POSTGRES_SERVER=localhost
-2
View File
@@ -5,7 +5,6 @@ from os.path import abspath, dirname
sys.path.insert(0, dirname(dirname(abspath(__file__))))
import os
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
@@ -16,7 +15,6 @@ config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
+18 -19
View File
@@ -1,6 +1,5 @@
import json
import sqlparse
import logging
import traceback
import warnings
from typing import Any, List, Optional, Union, Dict
@@ -35,7 +34,7 @@ from apps.system.crud.assistant import AssistantOutDs, AssistantOutDsFactory, ge
from apps.system.schemas.system_schema import AssistantOutDsSchema
from common.core.config import settings
from common.core.deps import CurrentAssistant, SessionDep, CurrentUser
from common.utils.utils import extract_nested_json
from common.utils.utils import SQLBotLogUtil, extract_nested_json
warnings.filterwarnings("ignore")
@@ -223,7 +222,7 @@ class LLMService:
res = self.llm.stream(analysis_msg)
token_usage = {}
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -273,7 +272,7 @@ class LLMService:
res = self.llm.stream(predict_msg)
token_usage = {}
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -323,7 +322,7 @@ class LLMService:
token_usage = {}
res = self.llm.stream(guess_msg)
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -390,7 +389,7 @@ class LLMService:
token_usage = {}
res = self.llm.stream(datasource_msg)
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -474,7 +473,7 @@ class LLMService:
token_usage = {}
res = self.llm.stream(self.sql_message)
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -543,7 +542,7 @@ class LLMService:
res = self.llm.stream(msg)
token_usage = {}
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -567,7 +566,7 @@ class LLMService:
# 'content': msg.content} for msg
# in
# analysis_msg]).decode())
print(full_filter_text)
SQLBotLogUtil.info(full_filter_text)
return full_filter_text
def generate_chart(self):
@@ -582,7 +581,7 @@ class LLMService:
token_usage = {}
res = self.llm.stream(self.chart_message)
for chunk in res:
print(chunk)
SQLBotLogUtil.info(chunk)
reasoning_content_chunk = ''
if 'reasoning_content' in chunk.additional_kwargs:
reasoning_content_chunk = chunk.additional_kwargs.get('reasoning_content', '')
@@ -691,7 +690,7 @@ class LLMService:
Returns:
Query results
"""
print(f"Executing SQL on ds_id {self.ds.id}: {sql}")
SQLBotLogUtil.info(f"Executing SQL on ds_id {self.ds.id}: {sql}")
return exec_sql(self.ds, sql)
@@ -717,7 +716,7 @@ def execute_sql_with_db(db: SQLDatabase, sql: str) -> str:
except Exception as e:
error_msg = f"SQL execution failed: {str(e)}"
logging.error(error_msg)
SQLBotLogUtil.exception(error_msg)
raise RuntimeError(error_msg)
@@ -741,7 +740,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
ds_res = llm_service.select_datasource()
for chunk in ds_res:
print(chunk)
SQLBotLogUtil.info(chunk)
if in_chat:
yield orjson.dumps(
{'content': chunk.get('content'), 'reasoning_content': chunk.get('reasoning_content'),
@@ -765,7 +764,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
yield orjson.dumps({'type': 'info', 'msg': 'sql generated'}).decode() + '\n\n'
# filter sql
print(full_sql_text)
SQLBotLogUtil.info(full_sql_text)
# todo row permission
sql_json_str = extract_nested_json(full_sql_text)
@@ -785,11 +784,11 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
raise Exception("SQL query is empty")
sql_result = llm_service.generate_filter(data.get('sql'), data.get('tables')) # maybe no sql and tables
print(sql_result)
SQLBotLogUtil.info(sql_result)
sql = llm_service.check_save_sql(res=sql_result)
# sql = llm_service.check_save_sql(res=full_sql_text)
print(sql)
SQLBotLogUtil.info(sql)
format_sql = sqlparse.format(sql, reindent=True)
if in_chat:
yield orjson.dumps({'content': format_sql, 'type': 'sql'}).decode() + '\n\n'
@@ -815,9 +814,9 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
yield orjson.dumps({'type': 'info', 'msg': 'chart generated'}).decode() + '\n\n'
# filter chart
print(full_chart_text)
SQLBotLogUtil.info(full_chart_text)
chart = llm_service.check_save_chart(res=full_chart_text)
print(chart)
SQLBotLogUtil.info(chart)
if in_chat:
yield orjson.dumps({'content': orjson.dumps(chart).decode(), 'type': 'chart'}).decode() + '\n\n'
else:
@@ -856,7 +855,7 @@ def run_task(llm_service: LLMService, in_chat: bool = True):
if chart['type'] != 'table':
yield '### generated chart picture\n\n'
image_url = request_picture(llm_service.record.chat_id, llm_service.record.id, chart, result)
print(image_url)
SQLBotLogUtil.info(image_url)
yield f'![{chart["type"]}]({image_url})'
except Exception as e:
traceback.print_exc()
+3 -3
View File
@@ -15,7 +15,7 @@ from apps.db.db import get_engine, get_tables, get_fields, exec_sql
from apps.db.engine import get_engine_config, get_engine_conn
from apps.db.type import db_type_relation
from common.core.deps import SessionDep, CurrentUser, Trans
from common.utils.utils import deepcopy_ignore_extra
from common.utils.utils import SQLBotLogUtil, deepcopy_ignore_extra
from .table import get_tables_by_ds_id
from ..crud.field import delete_field_by_ds_id, update_field
from ..crud.table import delete_table_by_ds_id, update_table
@@ -39,12 +39,12 @@ def check_status(session: SessionDep, ds: CoreDatasource, is_raise: bool = False
conn = get_engine(ds)
try:
with conn.connect() as connection:
print("success")
SQLBotLogUtil.info("success")
return True
except Exception as e:
SQLBotLogUtil.error(f"Datasource {ds.id} connection failed: {e}")
if is_raise:
raise HTTPException(status_code=500, detail=f'Connect Failed: {e.args}')
print("Fail:", e)
return False
+2 -2
View File
@@ -1,13 +1,13 @@
from typing import Optional
from sqlmodel import Session, select, delete as sqlmodel_delete
import logging
from apps.system.models.system_model import UserWsModel, WorkspaceModel
from apps.system.schemas.auth import CacheName, CacheNamespace
from apps.system.schemas.system_schema import BaseUserDTO, UserInfoDTO, UserWs
from common.core.deps import SessionDep
from common.core.sqlbot_cache import cache, clear_cache
from common.utils.locale import I18n
from common.utils.utils import SQLBotLogUtil
from ..models.user import UserModel
from common.core.security import verify_md5pwd
@@ -68,4 +68,4 @@ async def single_delete(session: SessionDep, id: int):
#@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def clean_user_cache(id: int):
logging.info(f"User cache for [{id}] has been cleaned")
SQLBotLogUtil.info(f"User cache for [{id}] has been cleaned")
+5 -1
View File
@@ -1,6 +1,6 @@
from typing import Optional
from fastapi import HTTPException, Request
from fastapi import Request
from fastapi.responses import JSONResponse
import jwt
from sqlmodel import Session
@@ -13,6 +13,7 @@ from apps.system.schemas.system_schema import AssistantHeader, UserInfoDTO
from common.core import security
from common.core.config import settings
from common.core.schemas import TokenPayload
from common.utils.utils import SQLBotLogUtil
from common.utils.whitelist import whiteUtils
from fastapi.security.utils import get_authorization_scheme_param
class TokenMiddleware(BaseHTTPMiddleware):
@@ -70,6 +71,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
) """
return True, session_user
except Exception as e:
SQLBotLogUtil.exception(f"Token validation error: {str(e)}", exc_info=True)
return False, e
@@ -95,4 +97,6 @@ class TokenMiddleware(BaseHTTPMiddleware):
assistant_info = AssistantHeader.model_validate(assistant_info.model_dump(exclude_unset=True))
return True, session_user, assistant_info
except Exception as e:
SQLBotLogUtil.exception(f"Assistant validation error: {str(e)}", exc_info=True)
# Return False and the exception message
return False, e
+5
View File
@@ -60,6 +60,11 @@ class Settings(BaseSettings):
TOKEN_KEY: str = "X-SQLBOT-TOKEN"
DEFAULT_PWD: str = "SQLBot@123456"
ASSISTANT_TOKEN_KEY: str = "X-SQLBOT-ASSISTANT-TOKEN"
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
LOG_DIR: str = "logs"
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
SQL_DEBUG: bool = False
@computed_field # type: ignore[prop-decorator]
@property
+4 -4
View File
@@ -1,10 +1,10 @@
import json
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.exceptions import HTTPException
from starlette.requests import Request
from common.core.config import settings
from common.utils.utils import SQLBotLogUtil
class ResponseMiddleware(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
@@ -48,7 +48,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
}
)
except Exception as e:
logging.error(f"Response processing error: {str(e)}", exc_info=True)
SQLBotLogUtil.error(f"Response processing error: {str(e)}", exc_info=True)
return JSONResponse(
status_code=500,
content={
@@ -68,6 +68,7 @@ class ResponseMiddleware(BaseHTTPMiddleware):
class exception_handler():
@staticmethod
async def http_exception_handler(request: Request, exc: HTTPException):
SQLBotLogUtil.exception(f"HTTP Exception: {exc.detail}", exc_info=True)
return JSONResponse(
status_code=exc.status_code,
content={
@@ -81,8 +82,7 @@ class exception_handler():
@staticmethod
async def global_exception_handler(request: Request, exc: Exception):
logging.error(f"Error info: {str(exc)}", exc_info=True)
SQLBotLogUtil.exception(f"Unhandled Exception: {str(exc)}", exc_info=True)
return JSONResponse(
status_code=500,
content={
+8 -11
View File
@@ -1,12 +1,13 @@
import logging
from fastapi_cache import FastAPICache
from fastapi_cache.decorator import cache as original_cache
from functools import partial, wraps
from typing import Optional, Set, Any, Dict, Tuple, Callable
from typing import Optional, Set, Any, Dict, Tuple
from inspect import Parameter, signature
from contextlib import asynccontextmanager
import asyncio
from common.utils.utils import SQLBotLogUtil
# 锁管理
_cache_locks = {}
@@ -65,7 +66,7 @@ def custom_key_builder(
base_key += f"{value}:"
except (IndexError, KeyError, AttributeError) as e:
logging.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
SQLBotLogUtil.warning(f"Failed to evaluate keyExpression '{keyExpression}': {str(e)}")
return base_key
@@ -127,19 +128,16 @@ def cache(
)
async with _get_cache_lock(cache_key):
logging.info(f"Using cache key: {cache_key}")
print(f"Using cache key: {cache_key}")
SQLBotLogUtil.info(f"Using cache key: {cache_key}")
backend = FastAPICache.get_backend()
cached_value = await backend.get(cache_key)
if cached_value is not None:
logging.info(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
print(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
SQLBotLogUtil.info(f"Cache hit for key: {cache_key}, the value is: {cached_value}")
return cached_value
result = await func(*args, **kwargs)
await backend.set(cache_key, result, expire)
logging.info(f"Cache miss for key: {cache_key}, result cached.")
print(f"Cache miss for key: {cache_key}, result cached.")
SQLBotLogUtil.info(f"Cache miss for key: {cache_key}, result cached.")
return result
return wrapper
@@ -167,8 +165,7 @@ def clear_cache(
if await FastAPICache.get_backend().get(cache_key):
await FastAPICache.clear(key=cache_key)
result = await func(*args, **kwargs)
logging.info(f"Clearing cache for key: {cache_key}")
print(f"Clearing cache for key: {cache_key}")
SQLBotLogUtil.info(f"Clearing cache for key: {cache_key}")
return result
return await func(*args, **kwargs)
+143 -3
View File
@@ -1,6 +1,10 @@
import hashlib
import inspect
import logging
from datetime import datetime, timedelta, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path
from common.core.config import settings
from typing import Optional
import jwt
@@ -10,9 +14,6 @@ from jwt.exceptions import InvalidTokenError
from common.core import security
from common.core.config import settings
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def generate_password_reset_token(email: str) -> str:
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
@@ -76,3 +77,142 @@ def extract_nested_json(text):
def string_to_numeric_hash(text: str, bits: Optional[int] = 64) -> int:
hash_bytes = hashlib.sha256(text.encode()).digest()
return int.from_bytes(hash_bytes[:bits//8], byteorder='big')
def setup_logging():
# 确保日志目录存在
log_dir = Path(settings.LOG_DIR)
log_dir.mkdir(parents=True, exist_ok=True)
# 日志格式
formatter = logging.Formatter(
f'{settings.LOG_FORMAT}'
)
# 控制台日志
console_handler = logging.StreamHandler()
console_handler.setLevel(settings.LOG_LEVEL)
console_handler.setFormatter(formatter)
# 文件日志处理器
file_handlers = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warn': logging.WARNING,
'error': logging.ERROR
}
# 主日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # 设置最低级别
# 添加控制台处理器
root_logger.addHandler(console_handler)
# 为每个级别创建文件处理器
for level_name, level in file_handlers.items():
file_path = log_dir / f"{level_name}.log"
handler = RotatingFileHandler(
file_path,
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
)
handler.setLevel(level)
handler.setFormatter(formatter)
# 添加过滤器只处理特定级别日志
if level_name == 'debug':
handler.addFilter(lambda record: record.levelno == logging.DEBUG)
elif level_name == 'info':
handler.addFilter(lambda record: record.levelno == logging.INFO)
elif level_name == 'warn':
handler.addFilter(lambda record: record.levelno == logging.WARNING)
elif level_name == 'error':
handler.addFilter(lambda record: record.levelno >= logging.ERROR)
root_logger.addHandler(handler)
# SQL日志特殊处理
if settings.LOG_LEVEL == "DEBUG" and settings.SQL_DEBUG:
sql_logger = logging.getLogger('sqlalchemy.engine')
sql_logger.setLevel(logging.DEBUG)
sql_handler = RotatingFileHandler(
log_dir / "sql.log",
maxBytes=10 * 1024 * 1024,
backupCount=2,
encoding='utf-8'
)
sql_handler.setFormatter(formatter)
sql_logger.addHandler(sql_handler)
setup_logging()
class CallerLogger(logging.Logger):
def __init__(self, logger: logging.Logger):
self.logger = logger
super().__init__(logger.name, logger.level)
def _log(self, level, msg, args, exc_info=None, extra=None, stacklevel=3):
if self.logger.isEnabledFor(level):
self.logger._log(level, msg, args, exc_info=exc_info, extra=extra, stacklevel=stacklevel)
class SQLBotLogUtil:
@staticmethod
def _get_logger() -> logging.Logger:
frame = inspect.currentframe()
try:
caller_frame = frame.f_back.f_back
module_name = caller_frame.f_globals.get('__name__', '__main__')
return CallerLogger(logging.getLogger(module_name))
finally:
del frame
@staticmethod
def debug(msg: str, *args, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.DEBUG):
logger._log(logging.DEBUG, msg, args, **kwargs)
@staticmethod
def info(msg: str, *args, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.INFO):
logger._log(logging.INFO, msg, args, **kwargs)
@staticmethod
def warning(msg: str, *args, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.WARNING):
logger._log(logging.WARNING, msg, args, **kwargs)
@staticmethod
def error(msg: str, *args, exc_info: Optional[bool] = None, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.ERROR):
logger._log(
logging.ERROR,
msg,
args,
exc_info=exc_info if exc_info is not None else True,
**kwargs
)
@staticmethod
def exception(msg: str, *args, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.ERROR):
logger._log(logging.ERROR, msg, args, exc_info=True, **kwargs)
@staticmethod
def critical(msg: str, *args, **kwargs):
logger = SQLBotLogUtil._get_logger()
if logger.isEnabledFor(logging.CRITICAL):
logger._log(logging.CRITICAL, msg, args, **kwargs)
+2 -1
View File
@@ -2,6 +2,7 @@
import re
from typing import List, Pattern
from common.core.config import settings
from common.utils.utils import SQLBotLogUtil
wlist = [
"/",
"/docs",
@@ -50,7 +51,7 @@ class WhitelistChecker:
try:
self._compiled_patterns.append(re.compile(regex_pattern))
except re.error:
print(f"Invalid regular expression pattern: {pattern}")
SQLBotLogUtil.error(f"Invalid regex pattern: {regex_pattern}")
def is_whitelisted(self, path: str) -> bool:
prefix = settings.API_V1_STR
+4 -3
View File
@@ -1,4 +1,3 @@
import logging
from fastapi.concurrency import asynccontextmanager
import sentry_sdk
from fastapi import FastAPI
@@ -18,6 +17,8 @@ import sqlbot_xpack
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from common.utils.utils import SQLBotLogUtil
def run_migrations():
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
@@ -27,10 +28,10 @@ def run_migrations():
async def lifespan(app: FastAPI):
run_migrations()
FastAPICache.init(InMemoryBackend())
print("✅ FastAPICache 初始化完成")
init_dynamic_cors(app)
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
yield
SQLBotLogUtil.info("SQLBot 应用关闭")
def custom_generate_unique_id(route: APIRoute) -> str:
tag = route.tags[0] if route.tags and len(route.tags) > 0 else ""
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.76027 7.79265L1.11652 3.24995C1.01715 3.1262 1.01715 2.92537 1.11652 2.80142C1.16423 2.74183 1.22902 2.7085 1.29631 2.7085H8.70444C8.84485 2.7085 8.95882 2.85058 8.95882 3.02578C8.95882 3.10995 8.93194 3.19037 8.88423 3.24995L5.24048 7.79287C5.10798 7.95808 4.89298 7.95808 4.76048 7.79287L4.76027 7.79265Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1.25C14.8325 1.25 18.75 5.16751 18.75 10C18.75 14.8325 14.8325 18.75 10 18.75H2.08333C1.6231 18.75 1.25 18.3769 1.25 17.9167V10C1.25 5.16751 5.16751 1.25 10 1.25ZM2.91667 17.0833H10C13.912 17.0833 17.0833 13.912 17.0833 9.99998C17.0833 6.08796 13.912 2.91665 10 2.91665C6.08798 2.91665 2.91667 6.08796 2.91667 9.99998V17.0833Z" fill="#1F2329"/>
<path d="M9.58333 6.25C9.35321 6.25 9.16667 6.43655 9.16667 6.66667V9.16667H6.66667C6.43655 9.16667 6.25 9.35321 6.25 9.58333V10.4167C6.25 10.6468 6.43655 10.8333 6.66667 10.8333H9.16667V13.3333C9.16667 13.5635 9.35321 13.75 9.58333 13.75H10.4167C10.6468 13.75 10.8333 13.5635 10.8333 13.3333V10.8333H13.3333C13.5635 10.8333 13.75 10.6468 13.75 10.4167V9.58333C13.75 9.35321 13.5635 9.16667 13.3333 9.16667H10.8333V6.66667C10.8333 6.43655 10.6468 6.25 10.4167 6.25H9.58333Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 953 B

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 1.125C13.3492 1.125 16.875 4.65076 16.875 9C16.875 13.3492 13.3492 16.875 9 16.875H1.875C1.46079 16.875 1.125 16.5392 1.125 16.125V9C1.125 4.65076 4.65076 1.125 9 1.125ZM2.625 15.375H9C12.5208 15.375 15.375 12.5208 15.375 8.99998C15.375 5.47917 12.5208 2.62498 9 2.62498C5.47918 2.62498 2.625 5.47917 2.625 8.99998V15.375Z" fill="#1CBA90"/>
<path d="M8.625 5.625C8.41789 5.625 8.25 5.79289 8.25 6V8.25H6C5.79289 8.25 5.625 8.41789 5.625 8.625V9.375C5.625 9.58211 5.79289 9.75 6 9.75H8.25V12C8.25 12.2071 8.41789 12.375 8.625 12.375H9.375C9.58211 12.375 9.75 12.2071 9.75 12V9.75H12C12.2071 9.75 12.375 9.58211 12.375 9.375V8.625C12.375 8.41789 12.2071 8.25 12 8.25H9.75V6C9.75 5.79289 9.58211 5.625 9.375 5.625H8.625Z" fill="#1CBA90"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.35433 11.2487H7.625L14.379 4.49447C14.6111 4.26241 14.6116 3.88631 14.3801 3.65363L12.3345 1.59723C12.102 1.36473 11.7251 1.36473 11.4926 1.59723L11.0727 2.01709L11.0743 2.01875L4.75898 8.38268V10.6534C4.75898 10.9822 5.02553 11.2487 5.35433 11.2487ZM11.8947 2.84338L13.1003 4.0554L12.0801 5.07666L10.8806 3.87724L11.8947 2.84338ZM11.2385 5.91901L7.12283 10.0386H7.12102L5.96904 8.88486L10.0468 4.72734L11.2385 5.91901Z" fill="#646A73"/>
<path d="M8.76346 2.31588H2.44373C2.07839 2.31588 1.78223 2.61204 1.78223 2.97737V13.5614C1.78223 13.9267 2.07839 14.2229 2.44373 14.2229H13.0277C13.393 14.2229 13.6892 13.9267 13.6892 13.5614V7.16128L12.3579 8.5006V12.898H3.11347V3.64068H7.46273L8.76346 2.31588Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4997 15.8333V17.5H2.49967V15.8333H17.4997ZM17.4997 4.16667H2.49967V17.5L2.32959 17.491C1.54496 17.4115 0.921507 16.788 0.84196 16.0034L0.833008 15.8333V4.16667C0.833008 3.24619 1.5792 2.5 2.49967 2.5H17.4997C18.4201 2.5 19.1663 3.24619 19.1663 4.16667V15.8333C19.1663 16.7538 18.4201 17.5 17.4997 17.5V4.16667Z" fill="#1F2329"/>
<path d="M6.24967 5.83333H3.74967C3.63917 5.83333 3.53319 5.87723 3.45505 5.95537C3.37691 6.03351 3.33301 6.13949 3.33301 6.25V7.08333C3.33301 7.19384 3.37691 7.29982 3.45505 7.37796C3.53319 7.4561 3.63917 7.5 3.74967 7.5H6.24967C6.30439 7.5 6.35857 7.48922 6.40913 7.46828C6.45968 7.44734 6.50561 7.41665 6.5443 7.37796C6.58299 7.33927 6.61369 7.29334 6.63462 7.24278C6.65556 7.19223 6.66634 7.13805 6.66634 7.08333V6.25C6.66634 6.19528 6.65556 6.1411 6.63462 6.09055C6.61369 6.04 6.58299 5.99406 6.5443 5.95537C6.50561 5.91668 6.45968 5.88599 6.40913 5.86505C6.35857 5.84411 6.30439 5.83333 6.24967 5.83333Z" fill="#1F2329"/>
<path d="M6.24967 8.33333H3.74967C3.63917 8.33333 3.53319 8.37723 3.45505 8.45537C3.37691 8.53351 3.33301 8.63949 3.33301 8.75V9.58333C3.33301 9.69384 3.37691 9.79982 3.45505 9.87796C3.53319 9.9561 3.63917 10 3.74967 10H6.24967C6.30439 10 6.35857 9.98922 6.40913 9.96828C6.45968 9.94734 6.50561 9.91665 6.5443 9.87796C6.58299 9.83927 6.61369 9.79334 6.63462 9.74278C6.65556 9.69223 6.66634 9.63805 6.66634 9.58333V8.75C6.66634 8.69528 6.65556 8.6411 6.63462 8.59055C6.61369 8.54 6.58299 8.49406 6.5443 8.45537C6.50561 8.41668 6.45968 8.38599 6.40913 8.36505C6.35857 8.34411 6.30439 8.33333 6.24967 8.33333Z" fill="#1F2329"/>
<path d="M7.49967 4.16667H9.16634V15.8333H7.49967V4.16667Z" fill="#1F2329"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+10 -2
View File
@@ -400,7 +400,14 @@
"filter_like": "Contains",
"filter_not_like": "Does not contain",
"filter_in": "Belongs to",
"filter_not_in": "Does not belong to"
"filter_not_in": "Does not belong to",
"enter_a_question": "Please enter a question",
"new_conversation": "New Conversation",
"send": "Send",
"stop_replying": "Stop replying",
"i_am_sqlbot": "Hello, I am SQLBot",
"predict_data_etc": "I can query data, generate charts, detect data anomalies, predict data, etc.",
"intelligent_data_query": "Hurry up and start intelligent data query~"
},
"embedded": {
"embedded_management": "Embedded management",
@@ -446,7 +453,8 @@
"edit_interface_credentials": "Edit interface credentials",
"duplicate_name": "Duplicate name",
"interface_credentials": "Interface credentials",
"no_credentials_yet": "No credentials yet"
"no_credentials_yet": "No credentials yet",
"intelligent_customer_service": "SQLBot Intelligent Customer Service"
},
"chat": {
"type": "Chart Type",
+9 -1
View File
@@ -458,7 +458,15 @@
"edit_interface_credentials": "编辑接口凭证",
"duplicate_name": "重复名称",
"interface_credentials": "接口凭证",
"no_credentials_yet": "暂无凭证"
"no_credentials_yet": "暂无凭证",
"intelligent_customer_service": "SQLBot 智能客服",
"enter_a_question": "请输入问题",
"new_conversation": "新建对话",
"send": "发送",
"stop_replying": "停止回答",
"i_am_sqlbot": "你好,我是 SQLBot",
"predict_data_etc": "我可以查询数据、生成图表、检测数据异常、预测数据等",
"intelligent_data_query": "赶快开启智能问数吧~"
},
"chat": {
"type": "图表类型",
+130 -26
View File
@@ -1,7 +1,10 @@
<script setup lang="ts">
import { Delete, EditPen, MoreFilled } from '@element-plus/icons-vue'
import icon_more_outlined from '@/assets/svg/icon_more_outlined.svg'
import icon_expand_down_filled from '@/assets/embedded/icon_expand-down_filled.svg'
import rename from '@/assets/svg/icon_rename_outlined.svg'
import delIcon from '@/assets/svg/icon_delete.svg'
import { type Chat, chatApi } from '@/api/chat.ts'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import dayjs from 'dayjs'
import { getDate } from '@/utils/utils.ts'
import { groupBy } from 'lodash-es'
@@ -48,6 +51,13 @@ const computedChatGroup = computed(() => {
return groupBy(props.chatList, groupByDate)
})
const expandMap = ref({
[t('qa.today')]: true,
[t('qa.week')]: true,
[t('qa.earlier')]: true,
[t('qa.no_time')]: true,
})
const computedChatList = computed(() => {
const _list = []
if (computedChatGroup.value[t('qa.today')]) {
@@ -169,27 +179,44 @@ function handleCommand(command: string | number | object, chat: Chat) {
<el-scrollbar ref="chatListRef">
<div class="chat-list-inner">
<div v-for="group in computedChatList" :key="group.key" class="group">
<div class="group-title">{{ group.key }}</div>
<div
class="group-title"
style="cursor: pointer"
@click="expandMap[group.key] = !expandMap[group.key]"
>
<el-icon :class="!expandMap[group.key] && 'expand'" style="margin-right: 8px" size="10">
<icon_expand_down_filled></icon_expand_down_filled>
</el-icon>
{{ group.key }}
</div>
<template v-for="chat in group.list" :key="chat.id">
<div
class="chat-list-item"
:class="{ active: currentChatId === chat.id }"
:class="{ active: currentChatId === chat.id, hide: !expandMap[group.key] }"
@click="onClickHistory(chat)"
>
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<el-button class="icon-more" link type="primary" @click.stop>
<el-dropdown trigger="click" @command="(cmd: any) => handleCommand(cmd, chat)">
<el-icon>
<MoreFilled />
<el-popover :teleported="false" popper-class="popover-card" placement="bottom">
<template #reference>
<el-icon class="more" size="16" style="margin-left: auto" @click.stop>
<icon_more_outlined></icon_more_outlined>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="EditPen" command="rename">Rename</el-dropdown-item>
<el-dropdown-item :icon="Delete" command="delete">Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-button>
</template>
<div class="content">
<div class="item" @click.stop="handleCommand('rename', chat)">
<el-icon size="16">
<rename></rename>
</el-icon>
{{ $t('dashboard.rename') }}
</div>
<div class="item" @click.stop="handleCommand('delete', chat)">
<el-icon size="16">
<delIcon></delIcon>
</el-icon>
{{ $t('dashboard.delete') }}
</div>
</div>
</el-popover>
</div>
</template>
</div>
@@ -223,6 +250,10 @@ function handleCommand(command: string | number | object, chat: Chat) {
font-size: 12px;
margin-bottom: 4px;
.expand {
transform: rotate(-90deg);
}
}
}
@@ -251,23 +282,39 @@ function handleCommand(command: string | number | object, chat: Chat) {
white-space: nowrap;
}
.icon-more {
margin-left: auto;
.more {
display: none;
&.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
min-width: unset;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
&:hover {
background-color: rgba(31, 35, 41, 0.1);
.icon-more {
display: inline-flex;
color: rgba(100, 106, 115, 1);
&:hover {
background-color: rgba(31, 35, 41, 0.1);
}
.more {
display: block;
}
}
@@ -275,6 +322,63 @@ function handleCommand(command: string | number | object, chat: Chat) {
background-color: rgba(255, 255, 255, 1);
font-weight: 500;
}
&.hide {
display: none;
}
}
}
</style>
<style lang="less">
.popover-card.popover-card.popover-card {
box-shadow: 0px 4px 8px 0px #1f23291a;
border-radius: 4px;
border: 1px solid #dee0e3;
width: 120px !important;
min-width: 120px !important;
padding: 0;
.content {
position: relative;
&::after {
position: absolute;
content: '';
top: 40px;
left: 0;
width: 100%;
height: 1px;
background: #dee0e3;
}
.item {
position: relative;
padding-left: 12px;
height: 40px;
display: flex;
align-items: center;
cursor: pointer;
.ed-icon {
margin-right: 8px;
color: #646a73;
}
&:hover {
&::after {
display: block;
}
}
&::after {
content: '';
width: 112px;
height: 32px;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1f23291a;
display: none;
}
}
}
}
</style>
+186 -252
View File
@@ -1,264 +1,198 @@
<template>
<div class="sqlbot-assistant-container">
<div class="head">
<div class="flex align-center">
<div class="head-img-div">
<img :src="AssistantGif" />
</div>
<h4>SQLBot 小助手</h4>
</div>
</div>
<chat-component v-if="!loading" ref="chatRef" class="sqlbot-chat-container" />
</div>
<div class="sqlbot-top-btn">
<el-icon style="cursor: pointer" @click="createChat">
<icon_new_chat_outlined />
</el-icon>
<el-icon style="cursor: pointer" @click="openHistory">
<history></history>
</el-icon>
</div>
<div class="sqlbot-history-container">
<el-drawer
v-model="drawer"
:append-to-body="false"
class="sqlbot-history-drawer"
modal-class="sqlbot-history-modal"
title=""
direction="ttb"
:with-header="false"
>
<div class="sqlbot-history-title">
<span>历史记录</span>
</div>
<div class="sqlbot-history-content">
<div v-if="!chatList.length" class="history-empty">
<span>暂无历史记录</span>
</div>
<div v-else class="sqlbot-chat-list-inner">
<el-scrollbar max-height="350px" class="sqlbot-chat-scroller">
<template v-for="chat in chatList" :key="chat.id">
<div
class="chat-list-item"
:class="{ 'is-active-chat': currentId === chat.id }"
@click="onClickHistory(chat)"
>
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<!-- <div class="history-operate">
<el-icon @click="EditPen(chat)"><IconOpeEdit /></el-icon>
<el-icon @click="Delete(chat)"><IconOpeDelete /></el-icon>
</div> -->
</div>
</template>
</el-scrollbar>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import ChatComponent from '@/views/chat/index.vue'
import AssistantGif from '@/assets/img/assistant.gif'
import history from '@/assets/svg/chart/history.svg'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
/* import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg' */
import { useRoute } from 'vue-router'
import { assistantApi } from '@/api/assistant'
import { useAssistantStore } from '@/stores/assistant'
<script lang="ts" setup>
import { ref } from 'vue'
import icon_sidebar_outlined from '@/assets/embedded/icon_sidebar_outlined.svg'
import icon_new_chat_outlined from '@/assets/embedded/icon_new-chat_outlined.svg'
import LOGO from '@/assets/embedded/LOGO.png'
import answer from '@/assets/embedded/answer.png'
import disable_answer from '@/assets/embedded/disable.png'
import loading_answer from '@/assets/embedded/loading.png'
import send_answer from '@/assets/embedded/send.png'
const assistantStore = useAssistantStore()
const route = useRoute()
const chatRef = ref()
const chatList = ref<Array<any>>([])
const drawer = ref(false)
const currentId = ref()
const createChat = () => {
chatRef.value?.createNewChat()
}
const openHistory = () => {
chatList.value = chatRef.value?.getHistoryList()
currentId.value = chatRef.value?.getCurrentChatId()
drawer.value = true
}
const onClickHistory = (chat: any) => {
chatRef.value?.toAssistantHistory(chat)
}
/* const EditPen = (chat: any) => {
chatRef.value?.onChatRenamed(chat)
}
const Delete = (chat: any) => {
chatRef.value?.onChatDeleted(chat.id)
} */
const validator = ref({
id: '',
valid: false,
id_match: false,
token: '',
})
const loading = ref(true)
const eventName = 'sqlbot_assistant_event'
const communicationCb = async (event: any) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== route.query.id) {
return
}
if (event.data?.busi == 'certificate') {
const certificate = event.data['certificate']
assistantStore.setType(1)
assistantStore.setCertificate(certificate)
// store certificate to pinia
}
}
}
onBeforeMount(async () => {
const assistantId = route.query.id
const online = route.query.online
console.log(online)
const now = Date.now()
assistantStore.setFlag(now)
const param = {
id: assistantId,
virtual: assistantStore.getFlag,
online,
}
validator.value = await assistantApi.validate(param)
assistantStore.setToken(validator.value.token)
assistantStore.setAssistant(true)
loading.value = false
window.addEventListener('message', communicationCb)
const readyData = {
eventName: 'sqlbot_assistant_event',
busi: 'ready',
ready: true,
messageId: assistantId,
}
window.parent.postMessage(readyData, '*')
})
onBeforeUnmount(() => {
window.removeEventListener('message', communicationCb)
})
const textareaVal = ref('')
</script>
<template>
<div class="assistant-window">
<div class="header">
<el-icon size="20">
<icon_sidebar_outlined></icon_sidebar_outlined>
</el-icon>
<img :src="LOGO" class="logo" width="30px" height="30px" alt="" />
<span class="tite">{{ $t('embedded.intelligent_customer_service') }}</span>
<el-tooltip effect="dark" :content="$t('embedded.new_conversation')" placement="top">
<el-icon class="new-chat" size="20">
<icon_new_chat_outlined></icon_new_chat_outlined>
</el-icon>
</el-tooltip>
</div>
<div class="center">
<img :src="LOGO" class="logo" width="30px" height="30px" alt="" />
<div class="i-am">{{ $t('embedded.i_am_sqlbot') }}</div>
<div class="i-can">{{ $t('embedded.predict_data_etc') }}</div>
<div class="data-query">{{ $t('embedded.intelligent_data_query') }}</div>
</div>
<div class="content">
<div class="textarea-send">
<el-input
v-model="textareaVal"
:autosize="{ minRows: 1, maxRows: 8.5848 }"
type="textarea"
:placeholder="$t('embedded.enter_a_question')"
/>
<el-tooltip :offset="10" effect="dark" :content="$t('embedded.send')" placement="top">
<img :src="disable_answer" width="32px" height="32px" alt="" />
</el-tooltip>
<el-tooltip
:offset="10"
effect="dark"
:content="$t('embedded.stop_replying')"
placement="top"
>
<img :src="answer" width="32px" height="32px" alt="" />
</el-tooltip>
<img :src="loading_answer" width="32px" height="32px" alt="" />
<img :src="send_answer" width="32px" height="32px" alt="" />
</div>
</div>
<div class="drawer-assistant"></div>
</div>
</template>
<style lang="less" scoped>
.sqlbot-assistant-container {
height: 100%;
width: 100%;
color: rgb(31, 35, 41);
.flex {
.assistant-window {
width: 460px;
height: 640px;
border-radius: 12px;
border: 1px solid #dee0e3;
box-shadow: 0px 6px 24px 0px #1f232914;
background-color: #fff;
position: relative;
.header {
background: #1cba9014;
height: 56px;
padding: 0 16px;
display: flex;
align-items: center;
position: relative;
.logo {
margin: 0 8px 0 16px;
}
.title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.ed-icon {
position: relative;
cursor: pointer;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 28px;
height: 28px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
.new-chat {
position: absolute;
right: 88px;
top: 17px;
}
}
.head {
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
position: fixed;
.center {
width: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
flex-direction: column;
.i-am {
font-weight: 600;
font-size: 24px;
line-height: 32px;
margin: 16px 0;
}
.i-can {
margin-bottom: 4px;
}
.data-query,
.i-can {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
.content {
width: calc(100% - 32px);
.textarea-send {
width: 100%;
position: relative;
.ed-textarea {
width: 100%;
}
:deep(.ed-textarea__inner) {
padding: 12px 12px 52px 12px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
border-radius: 16px;
&::placeholder {
color: #8f959e;
}
}
img {
cursor: pointer;
position: absolute;
right: 12px;
bottom: 12px;
}
}
position: absolute;
bottom: 16px;
left: 16px;
}
.drawer-assistant {
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 100;
height: 56px;
line-height: 56px;
box-sizing: border-box;
border-bottom: 1px solid #dee0e3;
.align-center {
align-items: center;
.head-img-div {
display: flex;
margin-left: 24px;
img {
width: 32px;
height: 32px;
}
}
}
}
.sqlbot-chat-container {
padding-top: 56px;
:deep(.ed-aside) {
display: none;
}
}
}
.sqlbot-top-btn {
right: 85px;
z-index: 2009;
position: fixed;
top: 16px;
display: flex;
column-gap: 16px;
// right: 16px;
font-size: 22px;
i:first-child {
color: var(--primary-color);
}
}
.sqlbot-history-container {
width: 100%;
:deep(.sqlbot-history-modal) {
top: 56px !important;
}
:deep(.sqlbot-history-title) {
border-bottom: 1px solid #dee0e3;
padding: 16px 24px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
:deep(.ed-drawer__body) {
padding: 0 !important;
}
:deep(.sqlbot-history-drawer) {
height: fit-content !important;
.sqlbot-history-content {
padding: 16px;
.history-empty {
height: 60px;
color: #8f959e !important;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.is-active-chat {
background-color: #ebf1ff;
color: #3370ff;
font-weight: 500;
}
.chat-list-item {
display: flex;
align-items: center;
padding: 10px 8px;
font-size: 14px;
color: #1f2329;
justify-content: space-between;
.history-operate {
display: none;
}
&:hover {
cursor: pointer;
border-radius: 4px;
background: rgba(31, 35, 41, 0.1);
.history-operate {
padding-right: 16px;
display: flex;
column-gap: 16px;
align-items: center;
font-size: 18px;
i {
color: #8f959e;
&:hover {
color: #8f959e99;
}
}
}
}
}
}
width: 50%;
background: #f5f6f7;
box-shadow: 0px 6px 24px 0px #1f232914;
padding: 16px;
border-right: 1px solid #dee0e3;
display: none;
}
}
</style>
+264
View File
@@ -0,0 +1,264 @@
<template>
<div class="sqlbot-assistant-container">
<div class="head">
<div class="flex align-center">
<div class="head-img-div">
<img :src="AssistantGif" />
</div>
<h4>SQLBot 小助手</h4>
</div>
</div>
<chat-component v-if="!loading" ref="chatRef" class="sqlbot-chat-container" />
</div>
<div class="sqlbot-top-btn">
<el-icon style="cursor: pointer" @click="createChat">
<icon_new_chat_outlined />
</el-icon>
<el-icon style="cursor: pointer" @click="openHistory">
<history></history>
</el-icon>
</div>
<div class="sqlbot-history-container">
<el-drawer
v-model="drawer"
:append-to-body="false"
class="sqlbot-history-drawer"
modal-class="sqlbot-history-modal"
title=""
direction="ttb"
:with-header="false"
>
<div class="sqlbot-history-title">
<span>历史记录</span>
</div>
<div class="sqlbot-history-content">
<div v-if="!chatList.length" class="history-empty">
<span>暂无历史记录</span>
</div>
<div v-else class="sqlbot-chat-list-inner">
<el-scrollbar max-height="350px" class="sqlbot-chat-scroller">
<template v-for="chat in chatList" :key="chat.id">
<div
class="chat-list-item"
:class="{ 'is-active-chat': currentId === chat.id }"
@click="onClickHistory(chat)"
>
<span class="title">{{ chat.brief ?? 'Untitled' }}</span>
<!-- <div class="history-operate">
<el-icon @click="EditPen(chat)"><IconOpeEdit /></el-icon>
<el-icon @click="Delete(chat)"><IconOpeDelete /></el-icon>
</div> -->
</div>
</template>
</el-scrollbar>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import ChatComponent from '@/views/chat/index.vue'
import AssistantGif from '@/assets/img/assistant.gif'
import history from '@/assets/svg/chart/history.svg'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
/* import IconOpeEdit from '@/assets/svg/operate/ope-edit.svg'
import IconOpeDelete from '@/assets/svg/operate/ope-delete.svg' */
import { useRoute } from 'vue-router'
import { assistantApi } from '@/api/assistant'
import { useAssistantStore } from '@/stores/assistant'
const assistantStore = useAssistantStore()
const route = useRoute()
const chatRef = ref()
const chatList = ref<Array<any>>([])
const drawer = ref(false)
const currentId = ref()
const createChat = () => {
chatRef.value?.createNewChat()
}
const openHistory = () => {
chatList.value = chatRef.value?.getHistoryList()
currentId.value = chatRef.value?.getCurrentChatId()
drawer.value = true
}
const onClickHistory = (chat: any) => {
chatRef.value?.toAssistantHistory(chat)
}
/* const EditPen = (chat: any) => {
chatRef.value?.onChatRenamed(chat)
}
const Delete = (chat: any) => {
chatRef.value?.onChatDeleted(chat.id)
} */
const validator = ref({
id: '',
valid: false,
id_match: false,
token: '',
})
const loading = ref(true)
const eventName = 'sqlbot_assistant_event'
const communicationCb = async (event: any) => {
if (event.data?.eventName === eventName) {
if (event.data?.messageId !== route.query.id) {
return
}
if (event.data?.busi == 'certificate') {
const certificate = event.data['certificate']
assistantStore.setType(1)
assistantStore.setCertificate(certificate)
// store certificate to pinia
}
}
}
onBeforeMount(async () => {
const assistantId = route.query.id
const online = route.query.online
console.log(online)
const now = Date.now()
assistantStore.setFlag(now)
const param = {
id: assistantId,
virtual: assistantStore.getFlag,
online,
}
validator.value = await assistantApi.validate(param)
assistantStore.setToken(validator.value.token)
assistantStore.setAssistant(true)
loading.value = false
window.addEventListener('message', communicationCb)
const readyData = {
eventName: 'sqlbot_assistant_event',
busi: 'ready',
ready: true,
messageId: assistantId,
}
window.parent.postMessage(readyData, '*')
})
onBeforeUnmount(() => {
window.removeEventListener('message', communicationCb)
})
</script>
<style lang="less" scoped>
.sqlbot-assistant-container {
height: 100%;
width: 100%;
color: rgb(31, 35, 41);
.flex {
display: flex;
}
.head {
background: linear-gradient(90deg, #ebf1ff 24.34%, #e5fbf8 56.18%, #f2ebfe 90.18%);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: 56px;
line-height: 56px;
box-sizing: border-box;
border-bottom: 1px solid #dee0e3;
.align-center {
align-items: center;
.head-img-div {
display: flex;
margin-left: 24px;
img {
width: 32px;
height: 32px;
}
}
}
}
.sqlbot-chat-container {
padding-top: 56px;
:deep(.ed-aside) {
display: none;
}
}
}
.sqlbot-top-btn {
right: 85px;
z-index: 2009;
position: fixed;
top: 16px;
display: flex;
column-gap: 16px;
// right: 16px;
font-size: 22px;
i:first-child {
color: var(--primary-color);
}
}
.sqlbot-history-container {
width: 100%;
:deep(.sqlbot-history-modal) {
top: 56px !important;
}
:deep(.sqlbot-history-title) {
border-bottom: 1px solid #dee0e3;
padding: 16px 24px;
font-size: 14px;
font-weight: 500;
color: #1f2329;
}
:deep(.ed-drawer__body) {
padding: 0 !important;
}
:deep(.sqlbot-history-drawer) {
height: fit-content !important;
.sqlbot-history-content {
padding: 16px;
.history-empty {
height: 60px;
color: #8f959e !important;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.is-active-chat {
background-color: #ebf1ff;
color: #3370ff;
font-weight: 500;
}
.chat-list-item {
display: flex;
align-items: center;
padding: 10px 8px;
font-size: 14px;
color: #1f2329;
justify-content: space-between;
.history-operate {
display: none;
}
&:hover {
cursor: pointer;
border-radius: 4px;
background: rgba(31, 35, 41, 0.1);
.history-operate {
padding-right: 16px;
display: flex;
column-gap: 16px;
align-items: center;
font-size: 18px;
i {
color: #8f959e;
&:hover {
color: #8f959e99;
}
}
}
}
}
}
}
}
</style>
@@ -291,10 +291,10 @@ defineExpose({
</div>
<div v-if="advancedSetting.length > 5 && configExpand" class="params-table_pagination">
<el-pagination
@current-change="handelCurrentChange"
:default-page-size="5"
layout="prev, pager, next"
:total="advancedSetting.length"
@current-change="handelCurrentChange"
/>
</div>
</div>