diff --git a/backend/alembic/versions/028_ds_oid.py b/backend/alembic/versions/028_ds_oid.py new file mode 100644 index 00000000..83afde51 --- /dev/null +++ b/backend/alembic/versions/028_ds_oid.py @@ -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 ### diff --git a/backend/alembic/versions/029_modify_chat.py b/backend/alembic/versions/029_modify_chat.py new file mode 100644 index 00000000..62b8ee9c --- /dev/null +++ b/backend/alembic/versions/029_modify_chat.py @@ -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 ### diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 3f891cbf..3d43fbcd 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -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 diff --git a/backend/apps/datasource/api/datasource.py b/backend/apps/datasource/api/datasource.py index 2c3d36e8..43a01862 100644 --- a/backend/apps/datasource/api/datasource.py +++ b/backend/apps/datasource/api/datasource.py @@ -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) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 5647f7c8..3f6a32d3 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -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() diff --git a/backend/apps/datasource/models/datasource.py b/backend/apps/datasource/models/datasource.py index ab73eaa3..4377344a 100644 --- a/backend/apps/datasource/models/datasource.py +++ b/backend/apps/datasource/models/datasource.py @@ -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] = [] diff --git a/backend/apps/mcp/mcp.py b/backend/apps/mcp/mcp.py index 3530f493..42f945e5 100644 --- a/backend/apps/mcp/mcp.py +++ b/backend/apps/mcp/mcp.py @@ -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") diff --git a/backend/apps/system/api/workspace.py b/backend/apps/system/api/workspace.py index 926a46a8..950d36a5 100644 --- a/backend/apps/system/api/workspace.py +++ b/backend/apps/system/api/workspace.py @@ -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() diff --git a/backend/apps/system/crud/user.py b/backend/apps/system/crud/user.py index 1431b971..8731aa7e 100644 --- a/backend/apps/system/crud/user.py +++ b/backend/apps/system/crud/user.py @@ -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() \ No newline at end of file + 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") diff --git a/backend/apps/system/crud/workspace.py b/backend/apps/system/crud/workspace.py new file mode 100644 index 00000000..38571fa9 --- /dev/null +++ b/backend/apps/system/crud/workspace.py @@ -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) diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py index feaa3c83..30592c31 100644 --- a/backend/apps/system/schemas/system_schema.py +++ b/backend/apps/system/schemas/system_schema.py @@ -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 diff --git a/backend/common/core/pagination.py b/backend/common/core/pagination.py index 783e3198..5423c7ec 100644 --- a/backend/common/core/pagination.py +++ b/backend/common/core/pagination.py @@ -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 diff --git a/backend/template.yaml b/backend/template.yaml index a801d88c..355a90e8 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -132,8 +132,8 @@ template: 你是一个数据分析师,你的任务是根据给定的数据进行数据预测,我将以json格式给你一组数据,你帮我预测之后的数据(一段可以展示趋势的数据,至少2个周期),用json格式返回。 ```json - 无法预测或者不支持预测的数据请直接返回:"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)" - 不需要返回原有数据,直接返回预测的部份 + 无法预测或者不支持预测的数据请直接返回(不需要返回JSON格式):"抱歉,该数据无法进行预测。(有原因则返回无法预测的原因)" + 如果可以预测,则不需要返回原有数据,直接返回预测的部份 user: | ### 请使用 i18n: {lang} 对应的语言输出你的结果 diff --git a/frontend/package.json b/frontend/package.json index 677c94c8..c06d2008 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/api/workspace.ts b/frontend/src/api/workspace.ts index b1e8c3ec..5298045b 100644 --- a/frontend/src/api/workspace.ts +++ b/frontend/src/api/workspace.ts @@ -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 }) diff --git a/frontend/src/assets/svg/icon_replace_outlined.svg b/frontend/src/assets/svg/icon_replace_outlined.svg new file mode 100644 index 00000000..b0de84d3 --- /dev/null +++ b/frontend/src/assets/svg/icon_replace_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/svg/icon_screen_outlined.svg b/frontend/src/assets/svg/icon_screen_outlined.svg new file mode 100644 index 00000000..ed5a00ba --- /dev/null +++ b/frontend/src/assets/svg/icon_screen_outlined.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/assets/svg/icon_start_outlined.svg b/frontend/src/assets/svg/icon_start_outlined.svg new file mode 100644 index 00000000..9ac68907 --- /dev/null +++ b/frontend/src/assets/svg/icon_start_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/layout/Workspace.vue b/frontend/src/components/layout/Workspace.vue index 276f8ecf..9a39955d 100644 --- a/frontend/src/components/layout/Workspace.vue +++ b/frontend/src/components/layout/Workspace.vue @@ -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) } diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index cd2dae82..10bcf900 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -85,7 +85,8 @@ "data_predict": "数据分析", "chat_search": "搜索", "thinking": "思考中", - "thinking_step": "思考过程" + "thinking_step": "思考过程", + "ask_again": "重新生成" }, "ds": { "title": "数据源", diff --git a/frontend/src/utils/markdown.ts b/frontend/src/utils/markdown.ts index 6d9fe595..817f431d 100644 --- a/frontend/src/utils/markdown.ts +++ b/frontend/src/utils/markdown.ts @@ -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 ` -
-          
-          ${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
-          
-          
- ` + return `
+                  ${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}
+                
` } catch (e) { console.error(e) - return str } } - return '
' + md.utils.escapeHtml(str) + '
' + return '
' + md.utils.escapeHtml(str) + '
' }, }) diff --git a/frontend/src/views/chat/RecommendQuestion.vue b/frontend/src/views/chat/RecommendQuestion.vue index dc705406..21f043a7 100644 --- a/frontend/src/views/chat/RecommendQuestion.vue +++ b/frontend/src/views/chat/RecommendQuestion.vue @@ -53,7 +53,6 @@ function clickQuestion(question: string): void { diff --git a/frontend/src/views/chat/answer/PredictAnswer.vue b/frontend/src/views/chat/answer/PredictAnswer.vue index bf157092..89aba684 100644 --- a/frontend/src/views/chat/answer/PredictAnswer.vue +++ b/frontend/src/views/chat/answer/PredictAnswer.vue @@ -193,6 +193,7 @@ defineExpose({ sendMessage, index: index.value, chatList: _chatList }) diff --git a/frontend/src/views/chat/component/MdComponent.vue b/frontend/src/views/chat/component/MdComponent.vue index 9f066685..ea0bdf29 100644 --- a/frontend/src/views/chat/component/MdComponent.vue +++ b/frontend/src/views/chat/component/MdComponent.vue @@ -1,6 +1,7 @@ - + diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index 8f28cb59..2f00b03a 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -117,23 +117,67 @@ @error="onChartAnswerError" >
todo
-
- - {{ t('chat.data_analysis') }} - - - {{ t('chat.data_predict') }} - +
+ {{ message.record?.error }} +
+
+
+ + + + + + + + +
+
+ {{ datetimeFormat(message.record?.create_time) }} +
@@ -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 { diff --git a/frontend/src/views/system/workspace/index.vue b/frontend/src/views/system/workspace/index.vue index 4b9b633e..09e3e446 100644 --- a/frontend/src/views/system/workspace/index.vue +++ b/frontend/src/views/system/workspace/index.vue @@ -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'),