This commit is contained in:
dataeaseShu
2025-07-17 09:35:33 +08:00
22 changed files with 1144 additions and 483 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ async def get_tables_by_conf(session: SessionDep, ds: CoreDatasource):
@router.post("/getFields/{id}/{table_name}")
async def get_tables(session: SessionDep, id: int, table_name: str):
async def get_fields(session: SessionDep, id: int, table_name: str):
return getFields(session, id, table_name)
+1 -1
View File
@@ -263,7 +263,7 @@ def fieldEnum(session: SessionDep, id: int):
def updateNum(session: SessionDep, ds: CoreDatasource):
all_tables = get_tables(ds)
all_tables = get_tables(ds) if ds.type != 'excel' else json.loads(aes_decrypt(ds.configuration)).get('sheets')
selected_tables = get_tables_by_ds_id(session, ds.id)
num = f'{len(selected_tables)}/{len(all_tables)}'
+5 -1
View File
@@ -1,6 +1,7 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from apps.system.schemas.system_schema import BaseUserDTO
from common.core.deps import SessionDep
from ..crud.user import authenticate
from common.core.security import create_access_token
@@ -14,9 +15,12 @@ def local_login(
session: SessionDep,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
user = authenticate(session=session, account=form_data.username, password=form_data.password)
user: BaseUserDTO = authenticate(session=session, account=form_data.username, password=form_data.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()
return Token(access_token=create_access_token(
+68 -18
View File
@@ -1,8 +1,8 @@
from typing import Optional
from fastapi import APIRouter, Query
from sqlmodel import func, or_, select
from apps.system.crud.user import get_db_user, user_ws_options
from apps.system.models.system_model import UserWsModel, WorkspaceModel
from fastapi import APIRouter, HTTPException, Query
from sqlmodel import func, or_, select, delete as sqlmodel_delete
from apps.system.crud.user import get_db_user, single_delete, user_ws_options
from apps.system.models.system_model import UserWsModel
from apps.system.models.user import UserModel
from apps.system.schemas.auth import CacheName, CacheNamespace
from apps.system.schemas.system_schema import PwdEditor, UserCreator, UserEditor, UserGrid, UserLanguage, UserWs
@@ -34,14 +34,21 @@ async def pager(
stmt = (
select(
UserModel,
func.coalesce(func.string_agg(WorkspaceModel.name, ','), '').label("space_name")
func.coalesce(
func.array_remove(
func.array_agg(UserWsModel.oid),
None
),
[]
).label("oid_list")
#func.coalesce(func.string_agg(WorkspaceModel.name, ','), '').label("space_name")
)
.join(UserWsModel, UserModel.id == UserWsModel.uid, isouter=True)
.join(WorkspaceModel, UserWsModel.oid == WorkspaceModel.id, isouter=True)
#.join(WorkspaceModel, UserWsModel.oid == WorkspaceModel.id, isouter=True)
.where(UserModel.id != 1)
.group_by(UserModel.id)
.order_by(UserModel.create_time)
)
if status is not None:
stmt = stmt.where(UserModel.status == status)
@@ -67,10 +74,21 @@ async def pager(
)
)
return await paginator.get_paginated_response(
user_page = await paginator.get_paginated_response(
stmt=stmt,
pagination=pagination,
**filters)
""" for item in user_page.items:
space_name: str = item['space_name']
if space_name and 'i18n_default_workspace' in space_name:
parts = list(map(
lambda x: trans(x) if x == "i18n_default_workspace" else x,
space_name.split(',')
))
output_str = ','.join(parts)
item['space_name'] = output_str """
return user_page
@router.get("/ws")
async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Trans) -> list[UserWs]:
@@ -81,16 +99,20 @@ async def ws_options(session: SessionDep, current_user: CurrentUser, trans: Tran
async def ws_change(session: SessionDep, current_user: CurrentUser, oid: int):
ws_list: list[UserWs] = await user_ws_options(session, current_user.id)
if not any(x.id == oid for x in ws_list):
raise RuntimeError(f"oid [{oid}] is invalid!")
raise HTTPException(f"oid [{oid}] is invalid!")
user_model: UserModel = get_db_user(session = session, user_id = current_user.id)
user_model.oid = oid
session.add(user_model)
session.commit()
@router.get("/{id}", response_model=UserEditor)
async def query(session: SessionDep, id: int) -> UserEditor:
async def query(session: SessionDep, trans: Trans, id: int) -> UserEditor:
db_user: UserModel = get_db_user(session = session, user_id = id)
return db_user
u_ws_options = await user_ws_options(session, id, trans)
result = UserEditor.model_validate(db_user.model_dump())
if u_ws_options:
result.oid_list = [item.id for item in u_ws_options]
return result
@router.post("")
async def create(session: SessionDep, creator: UserCreator):
@@ -98,6 +120,19 @@ async def create(session: SessionDep, creator: UserCreator):
user_model = UserModel.model_validate(data)
#user_model.create_time = get_timestamp()
user_model.language = "zh-CN"
user_model.oid = 0
if creator.oid_list:
# need to validate oid_list
db_model_list = [
UserWsModel.model_validate({
"oid": oid,
"uid": user_model.id,
"weight": 0
})
for oid in creator.oid_list
]
session.add_all(db_model_list)
user_model.oid = creator.oid_list[0]
session.add(user_model)
session.commit()
@@ -105,22 +140,37 @@ async def create(session: SessionDep, creator: UserCreator):
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="editor.id")
async def update(session: SessionDep, editor: UserEditor):
user_model: UserModel = get_db_user(session = session, user_id = editor.id)
origin_oid: int = user_model.oid
del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == editor.id)
session.exec(del_stmt)
data = editor.model_dump(exclude_unset=True)
user_model.sqlmodel_update(data)
user_model.oid = 0
if editor.oid_list:
# need to validate oid_list
db_model_list = [
UserWsModel.model_validate({
"oid": oid,
"uid": user_model.id,
"weight": 0
})
for oid in editor.oid_list
]
session.add_all(db_model_list)
user_model.oid = origin_oid if origin_oid in editor.oid_list else editor.oid_list[0]
session.add(user_model)
session.commit()
@router.delete("/{id}")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def delete(session: SessionDep, id: int):
user_model: UserModel = get_db_user(session = session, user_id = id)
session.delete(user_model)
session.commit()
await single_delete(session, id)
@router.delete("")
async def batch_del(session: SessionDep, id_list: list[int]):
for id in id_list:
delete(session, id)
await single_delete(session, id)
@router.put("/language")
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="current_user.id")
@@ -137,7 +187,7 @@ async def langChange(session: SessionDep, current_user: CurrentUser, language: U
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
if not current_user.isAdmin:
raise RuntimeError('only for admin')
raise HTTPException('only for admin')
db_user: UserModel = get_db_user(session=session, user_id=id)
db_user.password = default_md5_pwd()
session.add(db_user)
@@ -148,7 +198,7 @@ async def pwdReset(session: SessionDep, current_user: CurrentUser, id: int):
async def pwdUpdate(session: SessionDep, current_user: CurrentUser, editor: PwdEditor):
db_user: UserModel = get_db_user(session=session, user_id=current_user.id)
if not verify_md5pwd(editor.pwd, db_user.password):
raise RuntimeError("pwd error")
raise HTTPException("pwd error")
db_user.password = md5pwd(editor.new_pwd)
session.add(db_user)
session.commit()
+12 -3
View File
@@ -1,11 +1,12 @@
from typing import Optional
from sqlmodel import Session, select
from sqlmodel import Session, select, delete as sqlmodel_delete
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.sqlbot_cache import cache
from common.core.deps import SessionDep
from common.core.sqlbot_cache import cache, clear_cache
from common.utils.locale import I18n
from ..models.user import UserModel
from common.core.security import verify_md5pwd
@@ -55,4 +56,12 @@ async def user_ws_options(session: Session, uid: int, trans: Optional[I18n] = No
return [
UserWs(id = id, name = trans(name) if name.startswith('i18n') else name)
for id, name in result.all()
]
]
@clear_cache(namespace=CacheNamespace.AUTH_INFO, cacheName=CacheName.USER_INFO, keyExpression="id")
async def single_delete(session: SessionDep, id: int):
user_model: UserModel = get_db_user(session = session, user_id = id)
del_stmt = sqlmodel_delete(UserWsModel).where(UserWsModel.uid == id)
session.exec(del_stmt)
session.delete(user_model)
session.commit()
+9 -3
View File
@@ -1,6 +1,6 @@
from typing import Optional
from fastapi import Request
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
import jwt
from sqlmodel import Session
@@ -35,7 +35,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
request.state.current_user = validator[1]
request.state.assistant = validator[2]
return await call_next(request)
return JSONResponse({"error": f"Unauthorized:[{validator[1]}]"}, status_code=401)
return JSONResponse({"msg": f"Unauthorized:[{validator[1]}]"}, status_code=401)
#validate pass
tokenkey = settings.TOKEN_KEY
token = request.headers.get(tokenkey)
@@ -43,7 +43,7 @@ class TokenMiddleware(BaseHTTPMiddleware):
if validate_pass:
request.state.current_user = data
return await call_next(request)
return JSONResponse({"error": f"Unauthorized:[{data}]"}, status_code=401)
return JSONResponse({"msg": f"Unauthorized:[{data}]"}, status_code=401)
def is_options(self, request: Request):
return request.method == "OPTIONS"
@@ -62,6 +62,12 @@ class TokenMiddleware(BaseHTTPMiddleware):
with Session(engine) as session:
session_user = await get_user_info(session = session, user_id = token_data.id)
session_user = UserInfoDTO.model_validate(session_user)
session_user = UserInfoDTO.model_validate(session_user)
""" if token_data.oid != session_user.oid:
raise HTTPException(
status_code=401,
detail="Default space has been changed, please login again!"
) """
return True, session_user
except Exception as e:
return False, e
+1 -1
View File
@@ -10,7 +10,7 @@ from common.utils.time import get_timestamp
class BaseUserPO(SQLModel):
account: str = Field(max_length=255, unique=True)
oid: int = Field(nullable=False, sa_type=BigInteger())
oid: int = Field(nullable=False, sa_type=BigInteger(), default=0)
name: str = Field(max_length=255, unique=True)
password: str = Field(default_factory=default_md5_pwd, max_length=255)
email: str = Field(max_length=255)
+2 -1
View File
@@ -30,6 +30,7 @@ class UserCreator(BaseUser):
name: str
email: str
status: int = 1
oid_list: Optional[list[int]] = None
class UserEditor(UserCreator, BaseCreatorDTO):
pass
@@ -37,7 +38,7 @@ class UserEditor(UserCreator, BaseCreatorDTO):
class UserGrid(UserEditor):
create_time: int
language: str = "zh-CN"
space_name: Optional[str] = None
#space_name: Optional[str] = None
origin: str = ''
+1
View File
@@ -24,6 +24,7 @@ export interface ChatMessage {
isTyping?: boolean
first_chat?: boolean
recommended_question?: string
index: number
}
export class ChatRecord {
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 10.5759L13.4212 5.15476C13.5514 5.02459 13.7624 5.02459 13.8926 5.15476L14.364 5.62616C14.4942 5.75634 14.4942 5.96739 14.364 6.09757L8.47145 11.9901C8.2111 12.2505 7.78899 12.2505 7.52864 11.9901L1.63608 6.09757C1.50591 5.96739 1.50591 5.75634 1.63608 5.62616L2.10749 5.15476C2.23766 5.02459 2.44872 5.02459 2.57889 5.15476L8.00004 10.5759Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00004 6.6666L13.4212 12.0878C13.5514 12.2179 13.7624 12.2179 13.8926 12.0878L14.364 11.6164C14.4942 11.4862 14.4942 11.2751 14.364 11.1449L8.47145 5.25239C8.2111 4.99204 7.78899 4.99204 7.52864 5.25239L1.63608 11.1449C1.50591 11.2751 1.50591 11.4862 1.63608 11.6164L2.10749 12.0878C2.23766 12.2179 2.44872 12.2179 2.57889 12.0878L8.00004 6.6666Z" fill="#646A73"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

+4 -2
View File
@@ -82,7 +82,9 @@
"continue_to_ask": "继续提问:",
"data_analysis": "数据预测",
"data_predict": "数据分析",
"chat_search": "搜索"
"chat_search": "搜索",
"thinking": "思考中",
"thinking_step": "思考过程"
},
"ds": {
"title": "数据源",
@@ -355,4 +357,4 @@
"back_community": "还原至社区版",
"confirm_tips": "确定还原至社区版?"
}
}
}
+1 -1
View File
@@ -121,7 +121,7 @@ const router = createRouter({
{
path: '/system',
component: LayoutDsl,
redirect: '/system/model',
redirect: '/system/user',
children: [
{
path: 'user',
-42
View File
@@ -1,42 +0,0 @@
<script setup lang="ts">
import type { ChatMessage } from '@/api/chat.ts'
defineProps<{
msg?: ChatMessage
}>()
</script>
<template>
<div class="chat-block-outer">
<div class="chat-block">
<slot>
<div class="chat-block-content">
<!-- <div v-dompurify-html="msg?.content"></div> -->
<div v-if="msg?.role === 'user'" v-html="msg.content"></div>
<div v-else v-dompurify-html="msg?.content"></div>
</div>
</slot>
</div>
<div class="chat-block-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped lang="less">
.chat-block-outer {
.chat-block {
border-radius: 2px;
background-color: white;
word-wrap: break-word;
white-space: pre-wrap;
.chat-block-content {
padding: 12px;
}
}
.chat-block-footer {
}
}
</style>
+35 -28
View File
@@ -1,8 +1,5 @@
<script setup lang="ts">
// import ChatBlock from './ChatBlock.vue'
// import ChatRecordFirst from './ChatRecordFirst.vue'
import { ChatInfo, type ChatMessage } from '@/api/chat.ts'
// import { UserFilled } from '@element-plus/icons-vue'
import logo_fold from '@/assets/LOGO-fold.svg'
withDefaults(
@@ -18,42 +15,52 @@ withDefaults(
</script>
<template>
<div class="chat-row" :class="{ 'right-to-left': msg.role === 'user' }">
<el-avatar v-if="msg.role === 'assistant'" class="ai-avatar" shape="square">
<el-icon>
<logo_fold v-if="!hideAvatar" />
</el-icon>
</el-avatar>
<div :class="{ 'row-full': msg.role === 'assistant' }">
<slot></slot>
<div class="chat-row-container">
<div class="chat-row" :class="{ 'right-to-left': msg.role === 'user' }">
<div v-if="msg.role === 'assistant'" class="ai-avatar">
<el-icon>
<logo_fold v-if="!hideAvatar" />
</el-icon>
</div>
<div :class="{ 'row-full': msg.role === 'assistant' }">
<slot></slot>
</div>
</div>
<slot name="footer"></slot>
</div>
</template>
<style scoped lang="less">
.chat-row {
.chat-row-container {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-direction: column;
gap: 8px;
padding: 20px 0 0;
max-width: 800px;
width: 100%;
max-width: 800px;
&.right-to-left {
flex-direction: row-reverse;
}
.chat-row {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 8px;
padding: 20px 0 0;
.row-full {
flex: 1;
width: 0;
}
width: 100%;
.ai-avatar {
font-size: 28px;
background: transparent;
width: 28px;
&.right-to-left {
flex-direction: row-reverse;
}
.row-full {
flex: 1;
width: 0;
}
.ai-avatar {
font-size: 28px;
background: transparent;
width: 28px;
}
}
}
</style>
+31 -11
View File
@@ -37,11 +37,16 @@ function clickQuestion(question: string): void {
<template>
<div v-if="computedQuestions.length > 0" class="recommend-questions">
<div v-if="firstChat">{{ t('qa.guess_u_ask') }}</div>
<div v-else>{{ t('qa.continue_to_ask') }}</div>
<div v-for="(question, index) in computedQuestions" :key="index">
<span class="question" @click="clickQuestion(question)">
<div v-else class="continue-ask">{{ t('qa.continue_to_ask') }}</div>
<div class="question-grid">
<div
v-for="(question, index) in computedQuestions"
:key="index"
class="question"
@click="clickQuestion(question)"
>
{{ question }}
</span>
</div>
</div>
</div>
</template>
@@ -49,19 +54,34 @@ function clickQuestion(question: string): void {
<style scoped lang="less">
.recommend-questions {
padding: 8px;
font-size: 12px;
font-weight: 400;
font-size: 14px;
font-weight: 500;
line-height: 22px;
display: flex;
flex-direction: column;
gap: 4px;
.continue-ask {
color: rgba(100, 106, 115, 1);
font-weight: 400;
}
.question-grid {
display: grid;
grid-gap: 12px;
grid-template-columns: repeat(2, 50%);
}
.question {
font-weight: 400;
cursor: pointer;
background: lightgray;
border-radius: 4px;
padding: 4px;
line-height: 20px;
background: rgba(245, 246, 247, 1);
min-height: 46px;
border-radius: 6px;
padding: 12px;
line-height: 22px;
&:hover {
background: grey;
background: rgba(31, 35, 41, 0.1);
}
}
}
@@ -0,0 +1,199 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined || currentRecord.analysis_record_id === undefined) {
error = true
}
if (error) return
try {
const response = await chatApi.analysis(currentRecord.analysis_record_id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let analysis_answer = ''
let analysis_answer_thinking = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
_loading.value = false
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'analysis-result':
analysis_answer += data.content
analysis_answer_thinking += data.reasoning_content
_currentChat.value.records[index.value].analysis = analysis_answer
_currentChat.value.records[index.value].analysis_thinking = analysis_answer_thinking
break
case 'analysis_finish':
emits('finish', currentRecord.id)
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
defineExpose({ sendMessage, index: index.value, chatList: _chatList })
</script>
<template>
<BaseAnswer
v-if="message"
:message="message"
:reasoning-name="['analysis_thinking']"
:loading="_loading"
>
<MdComponent :message="message.record?.analysis" style="margin-top: 12px" />
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,159 @@
<script setup lang="ts">
import { type ChatMessage } from '@/api/chat.ts'
import { computed, onMounted, ref } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
import icon_up_outlined from '@/assets/svg/icon_up_outlined.svg'
import icon_down_outlined from '@/assets/svg/icon_down_outlined.svg'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
message: ChatMessage
loading?: boolean
reasoningName:
| 'sql_answer'
| 'chart_answer'
| 'analysis_thinking'
| 'predict'
| Array<'sql_answer' | 'chart_answer' | 'analysis_thinking' | 'predict'>
}>(),
{
loading: false,
}
)
const { t } = useI18n()
const show = ref<boolean>(false)
const reasoningContent = computed<Array<string>>(() => {
const names: Array<'sql_answer' | 'chart_answer' | 'analysis_thinking' | 'predict'> = []
if (typeof props.reasoningName === 'string') {
names.push(props.reasoningName)
} else {
props.reasoningName.forEach((item) => {
names.push(item)
})
}
const result: Array<string> = []
names.forEach((item) => {
if (props.message?.record) {
if (props.message?.record[item]) {
result.push(props.message?.record[item] ?? '')
}
}
})
return result
})
const hasReasoning = computed<boolean>(() => {
if (reasoningContent.value.length > 0) {
for (let i = 0; i < reasoningContent.value.length; i++) {
if (reasoningContent.value[i] && reasoningContent.value[i].trim() !== '') {
return true
}
}
}
return false
})
function clickShow() {
show.value = !show.value
}
onMounted(() => {
if (props.loading) {
show.value = true
}
})
</script>
<template>
<div class="base-answer-block">
<el-button v-if="loading || hasReasoning" class="thinking-btn" @click="clickShow">
<div class="thinking-btn-inner">
<span v-if="loading">{{ t('qa.thinking') }}</span>
<span v-else>{{ t('qa.thinking_step') }}</span>
<span class="btn-icon">
<el-icon v-if="show">
<icon_up_outlined />
</el-icon>
<el-icon v-else>
<icon_down_outlined />
</el-icon>
</span>
</div>
</el-button>
<div v-if="hasReasoning && show" class="reasoning-content">
<div v-for="(reason, _index) in reasoningContent" :key="_index" class="reasoning">
<MdComponent :message="reason" />
</div>
</div>
<div class="answer-container">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="less">
.base-answer-block {
.thinking-btn {
height: 32px;
padding: 5px 12px;
--ed-button-text-color: rgba(31, 35, 41, 1);
--ed-button-hover-text-color: var(--ed-button-text-color);
--ed-button-active-text-color: var(--ed-button-text-color);
--ed-button-bg-color: rgba(255, 255, 255, 1);
--ed-button-hover-bg-color: rgba(245, 246, 247, 1);
--ed-button-active-bg-color: rgba(239, 240, 241, 1);
--ed-button-border-color: rgba(217, 220, 223, 1);
--ed-button-hover-border-color: var(--ed-button-border-color);
--ed-button-active-border-color: var(--ed-button-border-color);
--ed-button-font-weight: 400;
.thinking-btn-inner {
display: flex;
flex-direction: row;
align-items: center;
line-height: 22px;
font-weight: 400;
font-size: 14px;
}
.btn-icon {
margin-left: 4px;
}
}
.reasoning-content {
margin-top: 8px;
display: flex;
flex-direction: column;
padding-left: 9px;
border-left: 1px solid rgba(31, 35, 41, 0.15);
gap: 8px;
.reasoning {
width: 100%;
line-height: 22px;
font-weight: 400;
font-size: 14px;
color: rgba(143, 149, 158, 1);
padding-bottom: 8px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
&:last-child {
padding-bottom: unset;
border-bottom: unset;
}
}
}
.answer-container {
width: 100%;
}
}
</style>
@@ -0,0 +1,230 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
reasoningName: 'sql_answer' | 'chart_answer' | Array<'sql_answer' | 'chart_answer'>
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined) {
error = true
}
if (error) return
try {
const response = await questionApi.add({
question: currentRecord.question,
chat_id: _currentChatId.value,
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let sql_answer = ''
let chart_answer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
_loading.value = false
return
}
switch (data.type) {
case 'id':
currentRecord.id = data.id
_currentChat.value.records[index.value].id = data.id
break
case 'info':
console.log(data.msg)
break
case 'brief':
_currentChat.value.brief = data.brief
_chatList.value.forEach((c: Chat) => {
if (c.id === _currentChat.value.id) {
c.brief = _currentChat.value.brief
}
})
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'sql-result':
sql_answer += data.reasoning_content
_currentChat.value.records[index.value].sql_answer = sql_answer
break
case 'sql':
_currentChat.value.records[index.value].sql = data.content
break
case 'sql-data':
getChatData(_currentChat.value.records[index.value].id)
break
case 'chart-result':
chart_answer += data.reasoning_content
_currentChat.value.records[index.value].chart_answer = chart_answer
break
case 'chart':
_currentChat.value.records[index.value].chart = data.content
break
case 'finish':
emits('finish', currentRecord.id)
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
function getChatData(recordId?: number) {
chatApi.get_chart_data(recordId).then((response) => {
_currentChat.value.records.forEach((record) => {
if (record.id === recordId) {
record.data = response
}
})
})
}
defineExpose({ sendMessage, index: index.value })
</script>
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="reasoningName" :loading="_loading">
<slot></slot>
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,200 @@
<script setup lang="ts">
import BaseAnswer from './BaseAnswer.vue'
import { chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat.ts'
import { computed, nextTick } from 'vue'
import MdComponent from '@/views/chat/component/MdComponent.vue'
const props = withDefaults(
defineProps<{
chatList: Array<ChatInfo>
currentChatId?: number
currentChat: ChatInfo
message?: ChatMessage
loading?: boolean
}>(),
{
chatList: () => [],
currentChatId: undefined,
currentChat: () => new ChatInfo(),
message: undefined,
loading: false,
}
)
const emits = defineEmits([
'finish',
'error',
'update:loading',
'update:chatList',
'update:currentChat',
'update:currentChatId',
])
const index = computed(() => {
if (props.message?.index) {
return props.message.index
}
if (props.message?.index === 0) {
return 0
}
return -1
})
const _currentChatId = computed({
get() {
return props.currentChatId
},
set(v) {
emits('update:currentChatId', v)
},
})
const _currentChat = computed({
get() {
return props.currentChat
},
set(v) {
emits('update:currentChat', v)
},
})
const _chatList = computed({
get() {
return props.chatList
},
set(v) {
emits('update:chatList', v)
},
})
const _loading = computed({
get() {
return props.loading
},
set(v) {
emits('update:loading', v)
},
})
const sendMessage = async () => {
_loading.value = true
if (index.value < 0) {
_loading.value = false
return
}
const currentRecord: ChatRecord = _currentChat.value.records[index.value]
let error: boolean = false
if (_currentChatId.value === undefined || currentRecord.predict_record_id === undefined) {
error = true
}
if (error) return
try {
const response = await chatApi.predict(currentRecord.predict_record_id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let predict_answer = ''
let predict_content = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
_loading.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
currentRecord.error = data.content
emits('error')
break
case 'predict-result':
predict_answer += data.reasoning_content
predict_content += data.content
_currentChat.value.records[index.value].predict = predict_answer
_currentChat.value.records[index.value].predict_content = predict_content
break
case 'predict-failed':
emits('error')
break
case 'predict-success':
//currentChat.value.records[_index].predict_data = data.content
emits('finish', currentRecord.id)
break
case 'predict_finish':
_loading.value = false
break
}
await nextTick()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
emits('error')
} finally {
_loading.value = false
}
}
defineExpose({ sendMessage, index: index.value, chatList: _chatList })
</script>
<template>
<BaseAnswer v-if="message" :message="message" :reasoning-name="['predict']" :loading="_loading">
<MdComponent :message="message.record?.predict_content" style="margin-top: 12px" />
<slot name="footer"></slot>
</BaseAnswer>
</template>
<style scoped lang="less"></style>
+160 -366
View File
@@ -99,70 +99,77 @@
/>
<UserChat v-if="message.role === 'user'" :message="message" />
<template v-if="message.role === 'assistant' && !message.first_chat">
<ChatAnswer
<ChartAnswer
v-if="
message?.record?.analysis_record_id == undefined &&
message?.record?.predict_record_id == undefined
(message?.record?.analysis_record_id === undefined ||
message?.record?.analysis_record_id === null) &&
(message?.record?.predict_record_id === undefined ||
message?.record?.predict_record_id === null)
"
ref="chartAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
:reasoning-name="['sql_answer', 'chart_answer']"
@finish="onChartAnswerFinish"
@error="onChartAnswerError"
>
<template v-if="message.record?.chart">
<div style="padding: 0 22px; display: flex; justify-content: flex-end">
<el-button
text
type="primary"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
@click="clickAnalysis(message.record?.id)"
>
{{ t('chat.data_analysis') }}
</el-button>
<el-button
text
type="primary"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
@click="clickPredict(message.record?.id)"
>
{{ t('chat.data_predict') }}
</el-button>
</div>
<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>
<template #footer>
<RecommendQuestion
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
/>
</template>
</ChatAnswer>
<div
</ChartAnswer>
<AnalysisAnswer
v-if="
message?.record?.analysis_record_id != undefined ||
message?.record?.predict_record_id != undefined
message?.record?.analysis_record_id !== undefined &&
message?.record?.analysis_record_id !== null
"
class="analysis-container"
>
<template v-if="message.record?.analysis || isAnalysisTyping">
<MdComponent :message="message.record?.analysis_thinking" />
<MdComponent :message="message.record?.analysis" />
</template>
<el-divider
v-if="
(message.record?.analysis || isAnalysisTyping) &&
(message.record?.predict || isPredictTyping)
"
/>
<template v-if="message.record?.predict || isPredictTyping">
<MdComponent :message="message.record?.predict" />
<MdComponent :message="message.record?.predict_content" />
<!-- <PredictChartBlock-->
<!-- :id="message.record?.id + '-predict'"-->
<!-- :data="message.record?.predict_data ?? '[]'"-->
<!-- :message="message"-->
<!-- :chart-type="data.chartType"-->
<!-- />-->
</template>
</div>
</template>
<template v-if="message.role === 'assistant'" #footer>
<RecommendQuestion
:questions="message.recommended_question"
:first-chat="message.first_chat"
@click-question="quickAsk"
ref="analysisAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
@finish="onAnalysisAnswerFinish"
@error="onAnalysisAnswerError"
/>
<PredictAnswer
v-if="
message?.record?.predict_record_id !== undefined &&
message?.record?.predict_record_id !== null
"
ref="predictAnswerRef"
:chat-list="chatList"
:current-chat="currentChat"
:current-chat-id="currentChatId"
:loading="isTyping"
:message="message"
@finish="onPredictAnswerFinish"
@error="onPredictAnswerError"
/>
</template>
</ChatRow>
@@ -182,7 +189,7 @@
<div class="input-wrapper">
<el-input
v-model="inputMessage"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
:disabled="isTyping"
class="input-area"
type="textarea"
:rows="1"
@@ -195,7 +202,7 @@
link
type="primary"
class="input-icon"
:disabled="isTyping || isPredictTyping || isAnalysisTyping"
:disabled="isTyping"
@click="sendMessage"
>
<el-icon size="20">
@@ -213,11 +220,12 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import { Position } from '@element-plus/icons-vue'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord, questionApi } from '@/api/chat'
import { Chat, chatApi, ChatInfo, type ChatMessage, ChatRecord } from '@/api/chat'
import ChatRow from './ChatRow.vue'
import ChatAnswer from './ChatAnswer.vue'
import ChartAnswer from './answer/ChartAnswer.vue'
import AnalysisAnswer from './answer/AnalysisAnswer.vue'
import PredictAnswer from './answer/PredictAnswer.vue'
import UserChat from './chat-block/UserChat.vue'
import MdComponent from './component/MdComponent.vue'
import RecommendQuestion from './RecommendQuestion.vue'
import ChatListContainer from './ChatListContainer.vue'
import ChatCreator from '@/views/chat/ChatCreator.vue'
@@ -255,8 +263,6 @@ const chatList = ref<Array<ChatInfo>>([])
const currentChatId = ref<number | undefined>()
const currentChat = ref<ChatInfo>(new ChatInfo())
const isTyping = ref<boolean>(false)
const isAnalysisTyping = ref<boolean>(false)
const isPredictTyping = ref<boolean>(false)
const computedMessages = computed<Array<ChatMessage>>(() => {
const messages: Array<ChatMessage> = []
@@ -271,6 +277,7 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
create_time: record.create_time,
record: record,
content: record.question,
index: i,
})
}
messages.push({
@@ -280,9 +287,11 @@ const computedMessages = computed<Array<ChatMessage>>(() => {
isTyping: i === currentChat.value.records.length - 1 && isTyping.value,
first_chat: record.first_chat,
recommended_question: record.recommended_question,
index: i,
})
}
console.log(messages)
return messages
})
@@ -378,7 +387,6 @@ async function getRecommendQuestions(record_id?: number) {
while (true) {
const { done, value } = await reader.read()
if (done) {
isTyping.value = false
break
}
@@ -415,7 +423,6 @@ async function getRecommendQuestions(record_id?: number) {
type: 'error',
showClose: true,
})
isTyping.value = false
return
}
@@ -451,6 +458,19 @@ function quickAsk(question: string) {
})
}
const chartAnswerRef = ref()
async function onChartAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
await getRecommendQuestions(id)
}
function onChartAnswerError() {
loading.value = false
isTyping.value = false
}
const sendMessage = async () => {
if (!inputMessage.value.trim()) return
@@ -469,138 +489,21 @@ const sendMessage = async () => {
currentChat.value.records.push(currentRecord)
inputMessage.value = ''
let error: boolean = false
if (currentChatId.value === undefined) {
error = true
}
if (error) return
try {
const response = await questionApi.add({
question: currentRecord.question,
chat_id: currentChatId.value,
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let sql_answer = ''
let chart_answer = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (chartAnswerRef.value) {
if (chartAnswerRef.value instanceof Array) {
for (let i = 0; i < chartAnswerRef.value.length; i++) {
const _index = chartAnswerRef.value[i].index
if (index === _index) {
await chartAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
isTyping.value = false
return
}
switch (data.type) {
case 'id':
currentRecord.id = data.id
currentChat.value.records[currentChat.value.records.length - 1].id = data.id
break
case 'info':
console.log(data.msg)
break
case 'brief':
currentChat.value.brief = data.brief
chatList.value.forEach((c: Chat) => {
if (c.id === currentChat.value.id) {
c.brief = currentChat.value.brief
}
})
break
case 'error':
currentRecord.error = data.content
isTyping.value = false
break
case 'sql-result':
sql_answer += data.reasoning_content
currentChat.value.records[currentChat.value.records.length - 1].sql_answer = sql_answer
break
case 'sql':
currentChat.value.records[currentChat.value.records.length - 1].sql = data.content
break
case 'sql-data':
//currentChat.value.records[currentChat.value.records.length - 1].data = data.content
getChatData(currentChat.value.records[currentChat.value.records.length - 1].id)
break
case 'chart-result':
chart_answer += data.reasoning_content
currentChat.value.records[currentChat.value.records.length - 1].chart_answer =
chart_answer
break
case 'chart':
currentChat.value.records[currentChat.value.records.length - 1].chart = data.content
break
case 'finish':
isTyping.value = false
await getRecommendQuestions(currentRecord.id)
break
}
await nextTick()
} else {
await chartAnswerRef.value.sendMessage()
}
}
} catch (error) {
if (!currentRecord.error) {
currentRecord.error = ''
}
if (currentRecord.error.trim().length !== 0) {
currentRecord.error = currentRecord.error + '\n'
}
currentRecord.error = currentRecord.error + 'Error:' + error
console.error('Error:', error)
isTyping.value = false
} finally {
loading.value = false
}
}
function getChatData(recordId?: number) {
chatApi.get_chart_data(recordId).then((response) => {
currentChat.value.records.forEach((record) => {
if (record.id === recordId) {
record.data = response
}
})
})
}
@@ -614,12 +517,28 @@ function getChatPredictData(recordId?: number) {
})
}
const analysisAnswerRef = ref()
async function onAnalysisAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
console.log(id)
//await getRecommendQuestions(id)
}
function onAnalysisAnswerError() {
loading.value = false
isTyping.value = false
}
async function clickAnalysis(id?: number) {
const baseRecord = find(currentChat.value.records, (value) => id === value.id)
if (baseRecord == undefined) {
return
}
loading.value = true
isTyping.value = true
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = baseRecord.chat_id
@@ -627,96 +546,41 @@ async function clickAnalysis(id?: number) {
currentRecord.chart = baseRecord.chart
currentRecord.data = baseRecord.data
currentRecord.analysis_record_id = id
currentRecord.analysis = ''
currentChat.value.records.push(currentRecord)
let _index = currentChat.value.records.length - 1
isAnalysisTyping.value = true
currentChat.value.records[_index].analysis = ''
try {
const response = await chatApi.analysis(id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let analysis_answer = ''
let analysis_answer_thinking = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isAnalysisTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (analysisAnswerRef.value) {
if (analysisAnswerRef.value instanceof Array) {
for (let i = 0; i < analysisAnswerRef.value.length; i++) {
const _index = analysisAnswerRef.value[i].index
if (index === _index) {
await analysisAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
isAnalysisTyping.value = false
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
throw Error(data.content)
case 'analysis-result':
analysis_answer += data.content
analysis_answer_thinking += data.reasoning_content
currentChat.value.records[_index].analysis = analysis_answer
currentChat.value.records[_index].analysis_thinking = analysis_answer_thinking
break
case 'analysis_finish':
isAnalysisTyping.value = false
break
}
await nextTick()
} else {
await analysisAnswerRef.value.sendMessage()
}
}
} catch (error) {
console.error('Error:', error)
ElMessage({
message: error + '',
type: 'error',
showClose: true,
})
isAnalysisTyping.value = false
}
})
return
}
const predictAnswerRef = ref()
async function onPredictAnswerFinish(id: number) {
loading.value = false
isTyping.value = false
getChatPredictData(id)
//await getRecommendQuestions(id)
}
function onPredictAnswerError() {
loading.value = false
isTyping.value = false
}
async function clickPredict(id?: number) {
@@ -725,6 +589,9 @@ async function clickPredict(id?: number) {
return
}
loading.value = true
isTyping.value = true
const currentRecord = new ChatRecord()
currentRecord.create_time = new Date()
currentRecord.chat_id = baseRecord.chat_id
@@ -732,102 +599,29 @@ async function clickPredict(id?: number) {
currentRecord.chart = baseRecord.chart
currentRecord.data = baseRecord.data
currentRecord.predict_record_id = id
currentRecord.predict = ''
currentRecord.predict_data = ''
currentChat.value.records.push(currentRecord)
let _index = currentChat.value.records.length - 1
isPredictTyping.value = true
currentChat.value.records[_index].predict = ''
currentChat.value.records[_index].predict_data = ''
try {
const response = await chatApi.predict(id)
const reader = response.body.getReader()
const decoder = new TextDecoder()
let predict_answer = ''
let predict_content = ''
while (true) {
const { done, value } = await reader.read()
if (done) {
isPredictTyping.value = false
break
}
const chunk = decoder.decode(value)
let _list = [chunk]
const lines = chunk.trim().split('}\n\n{')
if (lines.length > 1) {
_list = []
for (let line of lines) {
if (!line.trim().startsWith('{')) {
line = '{' + line.trim()
nextTick(async () => {
const index = currentChat.value.records.length - 1
if (predictAnswerRef.value) {
if (predictAnswerRef.value instanceof Array) {
for (let i = 0; i < predictAnswerRef.value.length; i++) {
const _index = predictAnswerRef.value[i].index
if (index === _index) {
await predictAnswerRef.value[i].sendMessage()
break
}
if (!line.trim().endsWith('}')) {
line = line.trim() + '}'
}
_list.push(line)
}
}
// console.log(_list)
for (const str of _list) {
let data
try {
data = JSON.parse(str)
} catch (err) {
console.error('JSON string:', str)
throw err
}
if (data.code && data.code !== 200) {
ElMessage({
message: data.msg,
type: 'error',
showClose: true,
})
return
}
switch (data.type) {
case 'info':
console.log(data.msg)
break
case 'error':
throw Error(data.content)
case 'predict-result':
predict_answer += data.reasoning_content
predict_content += data.content
currentChat.value.records[_index].predict = predict_answer
currentChat.value.records[_index].predict_content = predict_content
break
case 'predict-failed':
break
case 'predict-success':
//currentChat.value.records[_index].predict_data = data.content
getChatPredictData(currentChat.value.records[_index].id)
break
case 'predict_finish':
isPredictTyping.value = false
break
}
await nextTick()
} else {
await predictAnswerRef.value.sendMessage()
}
}
} catch (error) {
console.error('Error:', error)
ElMessage({
message: error + '',
type: 'error',
showClose: true,
})
isPredictTyping.value = false
}
})
return
}
const handleCtrlEnter = (e: KeyboardEvent) => {
+19 -4
View File
@@ -66,7 +66,11 @@
<el-table-column prop="email" :label="$t('user.email')" width="280" />
<!-- <el-table-column prop="phone" :label="$t('user.phone_number')" width="280" /> -->
<!-- <el-table-column prop="user_source" :label="$t('user.user_source')" width="280" /> -->
<el-table-column prop="space_name" :label="$t('user.workspace')" width="280" />
<el-table-column prop="oid_list" :label="$t('user.workspace')" width="280">
<template #default="scope">
<span>{{ formatSpaceName(scope.row.oid_list) }}</span>
</template>
</el-table-column>
<el-table-column prop="create_time" width="180" sortable :label="$t('user.creation_time')">
<template #default="scope">
@@ -205,8 +209,8 @@
<el-form-item :label="$t('user.workspace')">
<el-select
v-model="state.form.oid"
clearable
v-model="state.form.oid_list"
multiple
:placeholder="$t('datasource.Please_select') + $t('common.empty') + $t('user.workspace')"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id" />
@@ -348,10 +352,11 @@ const state = reactive<any>({
id: '',
name: '',
account: '',
oid: '',
oid: 0,
email: '',
status: '',
phoneNumber: '',
oid_list: [],
},
pageInfo: {
currentPage: 1,
@@ -645,6 +650,16 @@ const handleCurrentChange = (val: number) => {
state.pageInfo.currentPage = val
search()
}
const formatSpaceName = (row_oid_list: Array<any>) => {
if (!row_oid_list?.length) {
return '-'
}
const wsMap: Record<string, string> = {}
options.value.forEach((option: any) => {
wsMap[option.id] = option.name
})
return row_oid_list.map((id: any) => wsMap[id]).join(',')
}
onMounted(() => {
workspaceList().then((res) => {
options.value = res || []