This commit is contained in:
dataeaseShu
2025-07-17 17:34:32 +08:00
28 changed files with 479 additions and 98 deletions
+28
View File
@@ -0,0 +1,28 @@
"""028_ds_oid
Revision ID: e96b16d3daab
Revises: b049c9f8ca5b
Create Date: 2025-07-17 14:40:48.522033
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'e96b16d3daab'
down_revision = 'b049c9f8ca5b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('core_datasource', sa.Column('oid', sa.BigInteger(), nullable=True))
op.execute('update core_datasource set oid = 1')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('core_datasource', 'oid')
# ### end Alembic commands ###
@@ -0,0 +1,54 @@
"""029_modify_chat
Revision ID: 77d4c39ec22f
Revises: e96b16d3daab
Create Date: 2025-07-17 17:05:13.392973
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '77d4c39ec22f'
down_revision = 'e96b16d3daab'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('chat', sa.Column('oid', sa.BigInteger(), nullable=True))
op.execute('update chat set oid = 1')
op.alter_column('chat', 'create_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
op.alter_column('chat_record', 'create_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
op.alter_column('chat_record', 'finish_time',
existing_type=postgresql.TIMESTAMP(timezone=True),
type_=sa.DateTime(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('chat_record', 'finish_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.alter_column('chat_record', 'create_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.alter_column('chat', 'create_time',
existing_type=sa.DateTime(),
type_=postgresql.TIMESTAMP(timezone=True),
existing_nullable=True)
op.drop_column('chat', 'oid')
# ### end Alembic commands ###
+5 -3
View File
@@ -17,7 +17,8 @@ from apps.template.filter.generator import get_permissions_template
class Chat(SQLModel, table=True):
__tablename__ = "chat"
id: Optional[int] = Field(sa_column=Column(BigInteger, Identity(always=True), primary_key=True))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
oid: Optional[int] = Field(sa_column=Column(BigInteger, nullable=True, default=1))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
brief: str = Field(max_length=64, nullable=True)
chat_type: str = Field(max_length=20, default="chat") # chat, datasource
@@ -31,8 +32,8 @@ class ChatRecord(SQLModel, table=True):
chat_id: int = Field(sa_column=Column(BigInteger, nullable=False))
ai_modal_id: Optional[int] = Field(sa_column=Column(BigInteger))
first_chat: bool = Field(sa_column=Column(Boolean, nullable=True, default=False))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=True), nullable=True))
create_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
finish_time: datetime = Field(sa_column=Column(DateTime(timezone=False), nullable=True))
create_by: int = Field(sa_column=Column(BigInteger, nullable=True))
datasource: int = Field(sa_column=Column(BigInteger, nullable=True))
engine_type: str = Field(max_length=64)
@@ -66,6 +67,7 @@ class ChatRecord(SQLModel, table=True):
analysis_record_id: int = Field(sa_column=Column(BigInteger, nullable=True))
predict_record_id: int = Field(sa_column=Column(BigInteger, nullable=True))
class CreateChat(BaseModel):
id: int = None
question: str = None
+4 -4
View File
@@ -19,8 +19,8 @@ path = "/opt/sqlbot/data/excel"
@router.get("/list")
async def datasource_list(session: SessionDep):
return get_datasource_list(session=session)
async def datasource_list(session: SessionDep, user: CurrentUser):
return get_datasource_list(session=session, user=user)
@router.post("/get/{id}")
@@ -44,8 +44,8 @@ async def choose_tables(session: SessionDep, id: int, tables: List[CoreTable]):
@router.post("/update", response_model=CoreDatasource)
async def update(session: SessionDep, ds: CoreDatasource):
return update_ds(session, ds)
async def update(session: SessionDep,user: CurrentUser, ds: CoreDatasource):
return update_ds(session,user, ds)
@router.post("/delete/{id}", response_model=CoreDatasource)
+12 -10
View File
@@ -22,10 +22,10 @@ from ..models.datasource import CoreDatasource, CreateDatasource, CoreTable, Cor
DatasourceConf, TableAndFields
def get_datasource_list(session: SessionDep):
statement = select(CoreDatasource).order_by(CoreDatasource.create_time.desc())
datasource_list = session.exec(statement).fetchall()
return datasource_list
def get_datasource_list(session: SessionDep, user: CurrentUser):
oid = user.oid if user.oid is not None else 1
return session.query(CoreDatasource).filter(CoreDatasource.oid == oid).order_by(
CoreDatasource.create_time.desc()).all()
def get_ds(session: SessionDep, id: int):
@@ -45,14 +45,15 @@ def check_status(session: SessionDep, ds: CoreDatasource):
return False
def check_name(session: SessionDep, ds: CoreDatasource):
def check_name(session: SessionDep, user: CurrentUser, ds: CoreDatasource):
if ds.id is not None:
ds_list = session.query(CoreDatasource).filter(
and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id)).all()
and_(CoreDatasource.name == ds.name, CoreDatasource.id != ds.id, CoreDatasource.oid == user.oid)).all()
if ds_list is not None and len(ds_list) > 0:
raise 'Name exist'
else:
ds_list = session.query(CoreDatasource).filter(CoreDatasource.name == ds.name).all()
ds_list = session.query(CoreDatasource).filter(
and_(CoreDatasource.name == ds.name, CoreDatasource.oid == user.oid)).all()
if ds_list is not None and len(ds_list) > 0:
raise 'Name exist'
@@ -60,10 +61,11 @@ def check_name(session: SessionDep, ds: CoreDatasource):
def create_ds(session: SessionDep, user: CurrentUser, create_ds: CreateDatasource):
ds = CoreDatasource()
deepcopy_ignore_extra(create_ds, ds)
check_name(session, ds)
check_name(session, user, ds)
ds.create_time = datetime.datetime.now()
# status = check_status(session, ds)
ds.create_by = user.id
ds.oid = user.oid if user.oid is not None else 1
ds.status = "Success"
ds.type_name = db_type_relation()[ds.type]
record = CoreDatasource(**ds.model_dump())
@@ -85,9 +87,9 @@ def chooseTables(session: SessionDep, id: int, tables: List[CoreTable]):
updateNum(session, ds)
def update_ds(session: SessionDep, ds: CoreDatasource):
def update_ds(session: SessionDep, user: CurrentUser, ds: CoreDatasource):
ds.id = int(ds.id)
check_name(session, ds)
check_name(session, user, ds)
status = check_status(session, ds)
ds.status = "Success" if status is True else "Fail"
record = session.exec(select(CoreDatasource).where(CoreDatasource.id == ds.id)).first()
@@ -18,6 +18,7 @@ class CoreDatasource(SQLModel, table=True):
create_by: int = Field(sa_column=Column(BigInteger()))
status: str = Field(max_length=64, nullable=True)
num: str = Field(max_length=256, nullable=True)
oid: int = Field(sa_column=Column(BigInteger()))
class CoreTable(SQLModel, table=True):
@@ -53,6 +54,7 @@ class CreateDatasource(BaseModel):
create_by: int = 0
status: str = ''
num: str = ''
oid: int = 1
tables: List[CoreTable] = []
+46 -17
View File
@@ -3,20 +3,29 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException
import jwt
from fastapi import HTTPException, status, APIRouter
from fastapi.responses import StreamingResponse
# from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from pydantic import ValidationError
from apps.chat.api.chat import create_chat
from apps.chat.models.chat_model import ChatMcp, CreateChat, ChatStart
from apps.chat.task.llm import LLMService, run_task
from apps.datasource.crud.datasource import get_datasource_list
from apps.system.crud.user import authenticate
from apps.system.models.system_model import AiModelDetail
from apps.system.crud.user import authenticate, get_user_info
from apps.system.schemas.system_schema import BaseUserDTO
from apps.system.schemas.system_schema import UserInfoDTO
from common.core import security
from common.core.config import settings
from common.core.deps import SessionDep, get_current_user
from common.core.schemas import Token
from common.core.deps import SessionDep
from common.core.schemas import TokenPayload, XOAuth2PasswordBearer, Token
from common.core.security import create_access_token
reusable_oauth2 = XOAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
)
router = APIRouter(tags=["mcp"], prefix="/mcp")
@@ -35,21 +44,24 @@ router = APIRouter(tags=["mcp"], prefix="/mcp")
# ))
@router.get("/ds_list", operation_id="get_datasource_list")
async def datasource_list(session: SessionDep):
return get_datasource_list(session=session)
@router.get("/model_list", operation_id="get_model_list")
async def get_model_list(session: SessionDep):
return session.query(AiModelDetail).all()
# @router.get("/ds_list", operation_id="get_datasource_list")
# async def datasource_list(session: SessionDep):
# return get_datasource_list(session=session)
#
#
# @router.get("/model_list", operation_id="get_model_list")
# async def get_model_list(session: SessionDep):
# return session.query(AiModelDetail).all()
@router.post("/mcp_start", operation_id="mcp_start")
async def mcp_start(session: SessionDep, chat: ChatStart):
user = authenticate(session=session, account=chat.username, password=chat.password)
user: BaseUserDTO = authenticate(session=session, account=chat.username, password=chat.password)
if not user:
raise HTTPException(status_code=400, detail="Incorrect account or password")
if not user.oid or user.oid == 0:
raise HTTPException(status_code=400, detail="No associated workspace, Please contact the administrator")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
user_dict = user.to_dict()
t = Token(access_token=create_access_token(
@@ -61,9 +73,26 @@ async def mcp_start(session: SessionDep, chat: ChatStart):
@router.post("/mcp_question", operation_id="mcp_question")
async def mcp_question(session: SessionDep, chat: ChatMcp):
user = await get_current_user(session, chat.token)
try:
payload = jwt.decode(
chat.token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
session_user = await get_user_info(session=session, user_id=token_data.id)
session_user = UserInfoDTO.model_validate(session_user)
if not session_user:
raise HTTPException(status_code=404, detail="User not found")
llm_service = LLMService(session, user, chat)
if session_user.status != 1:
raise HTTPException(status_code=400, detail="Inactive user")
# ask
llm_service = LLMService(session, session_user, chat)
llm_service.init_record()
return StreamingResponse(run_task(llm_service, False), media_type="text/event-stream")
+37 -14
View File
@@ -1,6 +1,8 @@
from typing import Optional
from fastapi import APIRouter, HTTPException, Query
from sqlmodel import exists, or_, select
from sqlmodel import exists, or_, select, delete as sqlmodel_delete
from apps.system.crud.user import clean_user_cache
from apps.system.crud.workspace import reset_single_user_oid, reset_user_oid
from apps.system.models.system_model import UserWsModel, WorkspaceBase, WorkspaceEditor, WorkspaceModel
from apps.system.models.user import UserModel
from apps.system.schemas.system_schema import UserWsBase, UserWsDTO, UserWsEditor, UserWsOption, WorkspaceUser
@@ -43,7 +45,7 @@ async def option_pager(
pagination=pagination,
)
@router.get("/uws/option", response_model=UserWsOption)
@router.get("/uws/option", response_model=UserWsOption | None)
async def option_user(
session: SessionDep,
current_user: CurrentUser,
@@ -60,7 +62,7 @@ async def option_user(
if keyword:
stmt = stmt.where(
or_(
UserModel.account == int(keyword),
UserModel.account == keyword,
UserModel.name == keyword,
)
)
@@ -76,8 +78,6 @@ async def pager(
oid: Optional[int] = Query(None, description="空间ID(仅admin用户生效)"),
):
if current_user.isAdmin:
if not oid:
raise RuntimeError('oid miss error')
workspace_id = oid
else:
workspace_id = current_user.oid
@@ -108,43 +108,55 @@ async def pager(
async def create(session: SessionDep, current_user: CurrentUser, creator: UserWsDTO):
if not current_user.isAdmin and current_user.weight == 0:
raise HTTPException("no permission to execute")
oid: int = creator.oid if current_user.isAdmin else current_user.oid
oid: int = creator.oid if (current_user.isAdmin and creator.oid) else current_user.oid
weight = creator.weight if (current_user.isAdmin and creator.weight) else 0
# 判断uid_list以及oid合法性
db_model_list = [
UserWsModel.model_validate({
"oid": oid,
"uid": uid,
"weight": creator.weight
"weight": weight
})
for uid in creator.uid_list
]
for uid in creator.uid_list:
await reset_single_user_oid(session, uid, oid)
await clean_user_cache(uid)
session.add_all(db_model_list)
session.commit()
@router.put("/uws")
async def edit(session: SessionDep, editor: UserWsEditor):
if not editor.oid or not editor.uid:
raise RuntimeError("param [oid, uid] miss")
raise HTTPException("param [oid, uid] miss")
db_model = session.exec(select(UserWsModel).where(UserWsModel.uid == editor.uid, UserWsModel.oid == editor.oid)).first()
if not db_model:
raise RuntimeError("uws not exist")
raise HTTPException("uws not exist")
if editor.weight == db_model.weight:
return
db_model.weight = editor.weight
session.add(db_model)
await clean_user_cache(editor.uid)
session.commit()
@router.delete("/uws")
async def delete(session: SessionDep, current_user: CurrentUser, dto: UserWsBase):
if not current_user.isAdmin and current_user.weight == 0:
raise HTTPException("no permission to execute")
oid: int = dto.oid if current_user.isAdmin else current_user.oid
oid: int = dto.oid if (current_user.isAdmin and dto.oid) else current_user.oid
db_model_list: list[UserWsModel] = session.exec(select(UserWsModel).where(UserWsModel.uid.in_(dto.uid_list), UserWsModel.oid == oid)).all()
if not db_model_list:
raise HTTPException(f"UserWsModel not found")
for db_model in db_model_list:
session.delete(db_model)
for uid in dto.uid_list:
await reset_single_user_oid(session, uid, oid, False)
await clean_user_cache(uid)
session.commit()
@router.get("", response_model=list[WorkspaceModel])
@@ -167,7 +179,7 @@ async def update(session: SessionDep, editor: WorkspaceEditor):
id = editor.id
db_model = session.get(WorkspaceModel, id)
if not db_model:
raise ValueError(f"WorkspaceModel with id {id} not found")
raise HTTPException(f"WorkspaceModel with id {id} not found")
update_data = WorkspaceModel.model_validate(editor)
db_model.sqlmodel_update(update_data)
session.add(db_model)
@@ -177,16 +189,27 @@ async def update(session: SessionDep, editor: WorkspaceEditor):
async def get_one(session: SessionDep, trans: Trans, id: int):
db_model = session.get(WorkspaceModel, id)
if not db_model:
raise ValueError(f"WorkspaceModel with id {id} not found")
raise HTTPException(f"WorkspaceModel with id {id} not found")
if db_model.name.startswith('i18n'):
db_model.name = trans(db_model.name)
return db_model
@router.delete("/{id}")
async def delete(session: SessionDep, id: int):
async def single_delete(session: SessionDep, id: int):
db_model = session.get(WorkspaceModel, id)
if not db_model:
raise ValueError(f"WorkspaceModel with id {id} not found")
raise HTTPException(f"WorkspaceModel with id {id} not found")
user_ws_list = session.exec(select(UserWsModel).where(UserWsModel.oid == id)).all()
if user_ws_list:
# clean user cache
for user_ws in user_ws_list:
await clean_user_cache(user_ws.uid)
# reset user default oid
await reset_user_oid(session, id)
# delete user_ws
session.exec(sqlmodel_delete(UserWsModel).where(UserWsModel.oid == id))
session.delete(db_model)
session.commit()
+6 -2
View File
@@ -1,7 +1,7 @@
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
@@ -64,4 +64,8 @@ async def single_delete(session: SessionDep, id: int):
del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == id)
session.exec(del_stmt)
session.delete(user_model)
session.commit()
session.commit()
@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")
+62
View File
@@ -0,0 +1,62 @@
from typing import Optional
from sqlmodel import Session, func, select, update
from apps.system.models.system_model import UserWsModel
from apps.system.models.user import UserModel
async def reset_single_user_oid(session: Session, uid: int, oid: int, add: Optional[bool] = True):
user_model = session.get(UserModel, uid)
if not user_model:
return
origin_oid = user_model.oid
if add and (not origin_oid or origin_oid == 0):
user_model.oid = oid
session.add(user_model)
if not add and origin_oid and origin_oid == oid:
user_model.oid = 0
user_ws = session.exec(select(UserWsModel).where(UserWsModel.uid == uid, UserWsModel.oid != oid)).first()
if user_ws:
user_model.oid = user_ws.oid
session.add(user_model)
async def reset_user_oid(session: Session, oid: int):
stmt = (
select(
UserModel.id,
UserModel.oid,
func.coalesce(
func.array_remove(
func.array_agg(UserWsModel.oid),
None
),
[]
).label("oid_list")
)
.join(UserWsModel, UserModel.id == UserWsModel.uid, isouter=True)
.where(UserModel.id != 1)
.group_by(UserModel.id)
)
user_filter = (
select(UserModel.id)
.join(UserWsModel, UserModel.id == UserWsModel.uid)
.where(UserWsModel.oid == oid)
.distinct()
)
stmt = stmt.where(UserModel.id.in_(user_filter))
result_user_list = session.exec(stmt)
for row in result_user_list:
result_dict = {}
for item, key in zip(row, row._fields):
result_dict[key] = item
origin_oid = result_dict['oid']
oid_list: list = list(filter(lambda x: x != oid, result_dict['oid_list']))
if origin_oid not in oid_list:
result_dict['oid'] = oid_list[0] if oid_list else 0
if result_dict['oid'] != origin_oid:
result_dict.pop("oid_list", None)
update_stmt = update(UserModel).where(UserModel.id == result_dict['id']).values(oid=result_dict['oid'])
session.exec(update_stmt)
+1 -1
View File
@@ -50,7 +50,7 @@ class UserWsBase(BaseModel):
uid_list: list[int]
oid: Optional[int] = None
class UserWsDTO(UserWsBase):
weight: int = 0
weight: Optional[int] = 0
class UserWsEditor(BaseModel):
uid: int
+1 -1
View File
@@ -14,7 +14,7 @@ class Paginator:
result_dict = {}
for item, key in zip(row, row._fields):
if isinstance(item, SQLModel):
result_dict.update(item.dict())
result_dict.update(item.model_dump())
else:
result_dict[key] = item
+2 -2
View File
@@ -132,8 +132,8 @@ template:
你是一个数据分析师,你的任务是根据给定的数据进行数据预测,我将以json格式给你一组数据,你帮我预测之后的数据(一段可以展示趋势的数据,至少2个周期),用json格式返回。
```json
无法预测或者不支持预测的数据请直接返回:"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)"
不需要返回原有数据,直接返回预测的部份
无法预测或者不支持预测的数据请直接返回(不需要返回JSON格式):"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)"
如果可以预测,则不需要返回原有数据,直接返回预测的部份
user: |
### 请使用 i18n: {lang} 对应的语言输出你的结果
+1
View File
@@ -26,6 +26,7 @@
"element-plus": "^2.10.1",
"element-plus-secondary": "^1.0.0",
"element-resize-detector": "^1.2.4",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
+1 -1
View File
@@ -11,7 +11,7 @@ export const workspaceUwsUpdate = (data: any) => request.put('/system/workspace/
export const workspaceCreate = (data: any) => request.post('/system/workspace', data)
export const workspaceUpdate = (data: any) => request.put('/system/workspace', data)
export const workspaceUwsDelete = (data: any) => request.delete('/system/workspace/uws', { data })
export const workspaceDelete = (data: any) => request.delete('/system/workspace', { data })
export const workspaceDelete = (id: any) => request.delete(`/system/workspace/${id}`)
export const workspaceList = () => request.get('/system/workspace')
export const workspaceDetail = (id: any) => request.get(`/system/workspace/${id}`)
export const uwsOption = (params: any) => request.get('system/workspace/uws/option', { params })
@@ -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="M12.8057 13.6388C11.7701 14.4894 10.4447 15 9.00003 15C5.68632 15 3.00003 12.3137 3.00003 9C3.00003 8.69123 3.02335 8.38791 3.06833 8.0917L3.77428 8.50919C4.05573 8.67564 4.40051 8.42728 4.3318 8.10761L3.79257 5.59892C3.74659 5.385 3.52799 5.25572 3.31837 5.31849L0.860228 6.05455C0.546995 6.14835 0.495468 6.57013 0.77691 6.73657L1.6979 7.28123C1.56848 7.83317 1.50003 8.40859 1.50003 9C1.50003 13.1421 4.85789 16.5 9.00003 16.5C11.0063 16.5 12.8286 15.7122 14.1745 14.4291L12.8057 13.6388Z" fill="currentColor"/>
<path d="M5.4246 4.1812C6.42315 3.43909 7.66031 3 9.00003 3C12.3137 3 15 5.68629 15 9C15 9.21238 14.989 9.42219 14.9675 9.62887L14.145 9.16094C13.8608 8.99924 13.5203 9.25334 13.5943 9.57182L14.1756 12.0711C14.2251 12.2842 14.4459 12.4098 14.6544 12.3435L17.0999 11.5663C17.4115 11.4673 17.4559 11.0447 17.1717 10.883L16.365 10.424C16.4536 9.96297 16.5 9.48691 16.5 9C16.5 4.85786 13.1422 1.5 9.00003 1.5C7.09588 1.5 5.35746 2.20961 4.03481 3.3788L5.4246 4.1812Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 3V12H15V3H3ZM1.5 2.25C1.5 1.83562 1.8375 1.5 2.25 1.5H15.75C16.1625 1.5 16.5 1.83562 16.5 2.25V12.75C16.5 13.1644 16.1625 13.5 15.75 13.5H2.25C1.8375 13.5 1.5 13.1644 1.5 12.75V2.25Z" fill="currentColor"/>
<path d="M11.5789 5.09473L12.6394 6.15523L9.0881 9.70648L7.35635 7.9751L6.15523 9.1766L5.09473 8.11573L7.35635 5.8541L9.08773 7.58548L11.5789 5.09473Z" fill="currentColor"/>
<path d="M12.75 15H5.25V16.5H12.75V15Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2276 13.6116C6.57261 13.7278 6.65136 14.0653 6.40198 14.3271C6.02398 14.7099 3.73835 15.0264 3.0596 15.1258C2.92985 15.1453 2.81023 15.1843 2.68573 15.1134C2.50685 15.0137 2.47685 14.8389 2.51098 14.606C2.60285 13.976 2.89348 12.3729 3.33823 11.2067C3.37798 11.1024 3.50885 11.0859 3.61273 11.0942C3.82873 11.0942 3.94535 11.1902 4.0241 11.4107C4.32335 12.2717 4.87648 12.9249 5.67448 13.3701C5.84923 13.4661 6.03636 13.5448 6.2276 13.6116ZM15.8674 2.2348C15.9086 2.49017 15.9379 2.7418 15.9337 3.0013C15.8962 4.2913 15.6142 5.53142 15.1459 6.73442C14.586 8.17255 13.7651 9.43742 12.6165 10.4844C12.5212 10.5707 12.492 10.6614 12.5044 10.7807C12.546 11.168 12.5749 11.9304 12.6165 12.3182C12.7076 13.2204 12.3926 13.9577 11.6587 14.4812C10.8709 15.0414 10.0537 15.5608 9.24561 16.088C8.73561 16.418 8.10936 16.1169 8.05123 15.5154C7.98486 14.8232 7.93498 13.7559 7.88548 13.0637C7.86036 12.7134 7.86898 12.7134 7.53298 12.668C6.63298 12.5525 5.93248 12.1281 5.45173 11.3619C5.20273 10.9663 5.07823 10.5294 5.03698 10.0719C5.02423 9.93205 4.96648 9.89905 4.83785 9.8908C4.11223 9.85367 3.0116 9.8083 2.28598 9.75467C1.68485 9.70967 1.37773 9.07892 1.70098 8.55167C1.92523 8.18042 2.16598 7.81817 2.40223 7.45142C2.65498 7.0558 2.9036 6.6643 3.15673 6.26867C3.62098 5.54792 4.28848 5.16055 5.14273 5.1523C5.6321 5.14817 6.49648 5.2348 6.98548 5.28805C7.08111 5.2963 7.14748 5.2843 7.20936 5.21417C8.34561 3.95305 9.74698 3.1003 11.3392 2.53142C12.2354 2.21102 13.1668 1.9995 14.1135 1.90142C14.5942 1.85155 15.0754 1.83505 15.552 1.92167C15.8134 1.97117 15.8261 1.98355 15.8674 2.2348ZM14.2687 3.39317C13.4385 3.47942 12.6307 3.66317 11.844 3.94442C10.4002 4.45967 9.24036 5.20105 8.33136 6.2098C7.92036 6.67255 7.39498 6.8293 6.82236 6.77942C6.68282 6.76378 6.54344 6.74678 6.40423 6.72842C5.99035 6.67978 5.57408 6.65436 5.15735 6.6523C4.82248 6.6553 4.6166 6.7723 4.42048 7.0768L4.04548 7.66405C3.90155 7.8898 3.75743 8.11543 3.6131 8.34092C3.92585 8.35967 4.24085 8.37767 4.55885 8.3938C4.91885 8.41705 5.60885 8.50255 5.9411 8.78005C6.32998 9.10367 6.49273 9.50942 6.53061 9.93655C6.55423 10.196 6.61686 10.3978 6.72186 10.5643C6.95473 10.9355 7.25061 11.1193 7.73361 11.1816C7.79623 11.1902 7.82511 11.1939 7.86261 11.1996C8.00661 11.222 8.12136 11.2464 8.26198 11.2963C8.54061 11.3957 8.79711 11.5625 9.00298 11.8213C9.18673 12.0523 9.28236 12.665 9.33298 12.9152C9.35586 13.0273 9.42523 13.9352 9.44286 14.1699C9.89734 13.8759 10.3457 13.5724 10.7876 13.2598C11.0715 13.0573 11.1626 12.8499 11.1251 12.4775C11.1064 12.3058 11.0265 11.0672 11.0126 10.9363C10.9511 10.346 11.1555 9.7843 11.6062 9.37555C12.5479 8.51717 13.2532 7.46192 13.7482 6.1903C14.1195 5.23705 14.3415 4.30142 14.4124 3.37967C14.3662 3.38342 14.3182 3.38792 14.2687 3.39317ZM11.8125 7.12517C11.5141 7.12517 11.228 7.00665 11.017 6.79567C10.806 6.58469 10.6875 6.29854 10.6875 6.00017C10.6875 5.70181 10.806 5.41566 11.017 5.20468C11.228 4.9937 11.5141 4.87517 11.8125 4.87517C12.1109 4.87517 12.397 4.9937 12.608 5.20468C12.819 5.41566 12.9375 5.70181 12.9375 6.00017C12.9375 6.29854 12.819 6.58469 12.608 6.79567C12.397 7.00665 12.1109 7.12517 11.8125 7.12517Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -43,6 +43,7 @@ const handleDefaultWorkspaceChange = (item: any) => {
currentWorkspace.value = { id: item.id, name: item.name }
userApi.ws_change(item.id).then(() => {
ElMessage.success(t('common.switch_success'))
location.reload()
})
emit('selectWorkspace', item)
}
+2 -1
View File
@@ -85,7 +85,8 @@
"data_predict": "数据分析",
"chat_search": "搜索",
"thinking": "思考中",
"thinking_step": "思考过程"
"thinking_step": "思考过程",
"ask_again": "重新生成"
},
"ds": {
"title": "数据源",
+9 -11
View File
@@ -2,25 +2,23 @@ import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
const md = new MarkdownIt({
html: true,
linkify: true,
html: false,
linkify: false,
breaks: true,
typographer: true,
quotes: '“”‘’',
highlight: (str: any, lang: any): string => {
if (lang && hljs.getLanguage(lang)) {
try {
return `
<pre>
<code class="hljs">
${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
</code>
</pre>
`
return `<pre class="hljs">
<code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code>
</pre>`
} catch (e) {
console.error(e)
return str
}
}
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>'
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'
},
})
@@ -53,7 +53,6 @@ function clickQuestion(question: string): void {
<style scoped lang="less">
.recommend-questions {
padding: 8px;
font-size: 14px;
font-weight: 500;
line-height: 22px;
@@ -192,6 +192,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList })
:loading="_loading"
>
<MdComponent :message="message.record?.analysis" style="margin-top: 12px" />
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
@@ -154,6 +154,11 @@ onMounted(() => {
.answer-container {
width: 100%;
line-height: 24px;
font-size: 16px;
font-weight: 400;
color: rgba(31, 35, 41, 1);
}
}
</style>
@@ -193,6 +193,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList })
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="['predict']" :loading="_loading">
<MdComponent :message="message.record?.predict_content" style="margin-top: 12px" />
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import md from '@/utils/markdown.ts'
import 'highlight.js/styles/github-dark-dimmed.css'
import 'highlight.js/styles/github-dark-dimmed.min.css'
import 'github-markdown-css/github-markdown-light.css'
import { computed } from 'vue'
const props = defineProps<{
@@ -13,7 +14,22 @@ const renderMd = computed(() => {
</script>
<template>
<div v-dompurify-html="renderMd"></div>
<div v-dompurify-html="renderMd" class="markdown-body md-render-container"></div>
</template>
<style scoped lang="less"></style>
<style lang="less">
.md-render-container {
.hljs {
overflow: auto;
padding: 1rem;
display: block;
}
//ul {
// padding-left: 16px;
//}
//ol {
// padding-left: 16px;
//}
}
</style>
+166 -24
View File
@@ -117,23 +117,67 @@
@error="onChartAnswerError"
>
<div style="height: 100px; width: 100%; background: grey">todo</div>
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickAnalysis(message.record?.id)"
>
{{ t('chat.data_analysis') }}
</el-button>
<el-button
text
type="primary"
:disabled="isTyping"
@click="clickPredict(message.record?.id)"
>
{{ t('chat.data_predict') }}
</el-button>
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<div class="tool-container">
<div class="tool-btns">
<el-tooltip effect="dark" :content="t('qa.ask_again')" placement="top">
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="askAgain(message)"
>
<el-icon size="18">
<icon_replace_outlined />
</el-icon>
</el-button>
</el-tooltip>
<template v-if="message.record?.chart">
<div class="divider"></div>
<div>
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="clickAnalysis(message.record?.id)"
>
<span class="tool-btn-inner">
<el-icon size="18">
<icon_screen_outlined />
</el-icon>
<span class="btn-text">
{{ t('chat.data_analysis') }}
</span>
</span>
</el-button>
</div>
<div>
<el-button
class="tool-btn"
text
:disabled="isTyping"
@click="clickPredict(message.record?.id)"
>
<span class="tool-btn-inner">
<el-icon size="18">
<icon_start_outlined />
</el-icon>
<span class="btn-text">
{{ t('chat.data_predict') }}
</span>
</span>
</el-button>
</div>
</template>
</div>
<div class="tool-times">
{{ datetimeFormat(message.record?.create_time) }}
</div>
</div>
<template #footer>
<RecommendQuestion
@@ -156,7 +200,20 @@
:message="message"
@finish="onAnalysisAnswerFinish"
@error="onAnalysisAnswerError"
/>
>
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<div class="tool-container">
<div class="tool-btns"></div>
<div class="tool-times">
{{ datetimeFormat(message.record?.create_time) }}
</div>
</div>
</AnalysisAnswer>
<PredictAnswer
v-if="
message?.record?.predict_record_id !== undefined &&
@@ -170,7 +227,20 @@
:message="message"
@finish="onPredictAnswerFinish"
@error="onPredictAnswerError"
/>
>
<div
v-if="message.record?.error && message.record?.error?.trim().length > 0"
class="error-container"
>
{{ message.record?.error }}
</div>
<div class="tool-container">
<div class="tool-btns"></div>
<div class="tool-times">
{{ datetimeFormat(message.record?.create_time) }}
</div>
</div>
</PredictAnswer>
</template>
</ChatRow>
</template>
@@ -233,8 +303,12 @@ import { useI18n } from 'vue-i18n'
import { endsWith, find, startsWith } from 'lodash-es'
import icon_new_chat_outlined from '@/assets/svg/icon_new_chat_outlined.svg'
import icon_sidebar_outlined from '@/assets/svg/icon_sidebar_outlined.svg'
import icon_replace_outlined from '@/assets/svg/icon_replace_outlined.svg'
import icon_screen_outlined from '@/assets/svg/icon_screen_outlined.svg'
import icon_start_outlined from '@/assets/svg/icon_start_outlined.svg'
import logo_fold from '@/assets/LOGO-fold.svg'
import logo from '@/assets/LOGO.svg'
import { datetimeFormat } from '@/utils/utils.ts'
import { useAssistantStore } from '@/stores/assistant'
const assistantStore = useAssistantStore()
@@ -536,6 +610,13 @@ function onAnalysisAnswerError() {
isTyping.value = false
}
function askAgain(message: ChatMessage) {
inputMessage.value = message.record?.question ?? ''
nextTick(() => {
sendMessage()
})
}
async function clickAnalysis(id?: number) {
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
if (baseRecord == undefined) {
@@ -750,11 +831,72 @@ defineExpose({
}
}
.analysis-container {
color: var(--ed-text-color-primary);
font-size: 12px;
line-height: 1.7692307692;
padding: 16px 22px;
.error-container {
margin-top: 12px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: rgba(31, 35, 41, 1);
}
.tool-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
min-height: 22px;
margin-top: 12px;
margin-bottom: 12px;
.tool-times {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: rgba(100, 106, 115, 1);
}
.tool-btns {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 8px;
.tool-btn {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: rgba(100, 106, 115, 1);
.tool-btn-inner {
display: flex;
flex-direction: row;
align-items: center;
}
&:hover {
background: rgba(31, 35, 41, 0.1);
}
&:active {
background: rgba(31, 35, 41, 0.1);
}
}
.btn-text {
margin-left: 4px;
}
.divider {
width: 1px;
height: 16px;
border-left: 1px solid rgba(31, 35, 41, 0.15);
}
}
}
.welcome-content-block {
@@ -236,9 +236,7 @@ const delWorkspace = (row: any) => {
customClass: 'confirm-no_icon',
autofocus: false,
}).then(() => {
workspaceDelete({
id: row.id,
}).then(() => {
workspaceDelete(row.id).then(() => {
ElMessage({
type: 'success',
message: t('dashboard.delete_success'),