Browse Source

aichat升级

master
xueyinfei 4 days ago
parent
commit
62d9ef33b6
  1. 2
      vue-fastapi-backend/module_admin/service/aichat_service.py
  2. 8
      vue-fastapi-frontend/src/api/aichat/aichat.js
  3. 4
      vue-fastapi-frontend/src/layout/components/AppMain.vue
  4. 87
      vue-fastapi-frontend/src/views/aichat/Interrupt.vue
  5. 25
      vue-fastapi-frontend/src/views/aichat/OperationButton.vue
  6. 180
      vue-fastapi-frontend/src/views/aichat/aichat.vue
  7. 4
      vue-fastapi-frontend/src/views/aichat/index.vue
  8. 34
      vue-fastapi-frontend/src/views/aichat/markdown.vue

2
vue-fastapi-backend/module_admin/service/aichat_service.py

@ -38,7 +38,7 @@ class AiChatService:
@classmethod @classmethod
async def delete_chat_list(cls, result_db: AsyncSession, chatId: str, async def delete_chat_list(cls, result_db: AsyncSession, chatId: str,
current_user: Optional[CurrentUserModel] = None): current_user: Optional[CurrentUserModel] = None):
chat = AiChatDao.get_ai_chat_by_id(chatId, result_db) chat = await AiChatDao.get_ai_chat_by_id(chatId, result_db)
await AiChatDao.delete_chat_with_session_and_time(result_db, chat.sessionId, chat.time) await AiChatDao.delete_chat_with_session_and_time(result_db, chat.sessionId, chat.time)
await result_db.commit() await result_db.commit()
return CrudResponseModel(is_success=True, message='删除成功') return CrudResponseModel(is_success=True, message='删除成功')

8
vue-fastapi-frontend/src/api/aichat/aichat.js

@ -26,6 +26,14 @@ export function postChatMessage(data,signal) {
return postStream('/aichat-api/datagov_assist',data,signal) return postStream('/aichat-api/datagov_assist',data,signal)
} }
export function cancelJob(data){
return request({
url: '/aichat-api/cancel_job',
method: 'post',
data: data
})
}
export function DeleteChatSession(sessionId) { export function DeleteChatSession(sessionId) {
return request({ return request({
url: '/default-api/aichat/delete/session/'+sessionId, url: '/default-api/aichat/delete/session/'+sessionId,

4
vue-fastapi-frontend/src/layout/components/AppMain.vue

@ -124,10 +124,10 @@ function closeChatDiv(){
cursor: pointer; cursor: pointer;
max-height: 500px; max-height: 500px;
max-width: 500px; max-width: 500px;
z-index: 10000; z-index: 1000;
} }
.ai_chat_div { .ai_chat_div {
z-index: 10000; z-index: 1000;
border-radius: 8px; border-radius: 8px;
border: 1px solid #ffffff; border: 1px solid #ffffff;
background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1; background: linear-gradient(188deg, rgba(235, 241, 255, 0.20) 39.6%, rgba(231, 249, 255, 0.20) 94.3%), #EFF0F1;

87
vue-fastapi-frontend/src/views/aichat/Interrupt.vue

@ -1,21 +1,21 @@
<template> <template>
<template v-if="formData && formData.block"> <template v-if="source && source.block">
<el-form :rules="formRules" ref="dynamicForm" > <el-form ref="dynamicForm" :model="formData" :rules="formRules">
<template v-for="(item, index) in formData.block"> <template v-for="(item, index) in source.block">
<el-form-item :key="'span'+ index" v-if="item.d_type === 'text' && item.ct_type === 'span'"> <el-form-item :key="'span'+ index" v-if="item.d_type === 'text' && item.ct_type === 'span'">
<span style="font-size: 14px">{{item.default_value}}</span> <span style="font-size: 14px">{{item.default_value}}</span>
</el-form-item> </el-form-item>
<el-form-item v-else :key="'comp'+ index" :label="item.name" :prop="'field_' + index" :rules="item.required ? [{ required: true, message: `${item.name}必填` }] : []"> <el-form-item v-else :key="'comp'+ index" :label="item.name" :prop="item.name" :rules="getValidationRules(item)">
<el-date-picker v-if="item.d_type === 'date' && item.ct_type === 'datePicker'" v-model="item.default_value" type="date" placeholder="请输入日期" value-format="YYYY-MM-DD" :disabled="item.read_only"/> <el-date-picker popper-class="custom-dropdown" v-if="item.d_type === 'date' && item.ct_type === 'datePicker'" v-model="formData[item.name]" type="date" placeholder="请输入日期" value-format="YYYY-MM-DD" :disabled="item.read_only || !isLastChat"/>
<el-date-picker v-if="item.d_type === 'date' && item.ct_type === 'dateRangePicker'" v-model="item.default_value" type="daterange" range-separator="" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" :disabled="item.read_only"/> <el-date-picker popper-class="custom-dropdown" v-if="item.d_type === 'date' && item.ct_type === 'dateRangePicker'" v-model="formData[item.name]" type="daterange" range-separator="" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" :disabled="item.read_only || !isLastChat"/>
<el-input v-if="item.d_type === 'text' && item.ct_type === 'input'" v-model="item.default_value" :disabled="item.read_only"></el-input> <el-input v-if="item.d_type === 'text' && item.ct_type === 'input'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat"></el-input>
<el-radio-group v-if="item.d_type === 'enum' && item.ct_type === 'radioGroup'" v-model="item.default_value" :disabled="item.read_only"> <el-radio-group v-if="item.d_type === 'enum' && item.ct_type === 'radioGroup'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat">
<el-radio v-for="radio in item.options" :value="radio">{{radio}}</el-radio> <el-radio v-for="radio in item.options" :value="radio">{{radio}}</el-radio>
</el-radio-group> </el-radio-group>
<el-checkbox-group v-if="item.d_type === 'enum' && item.ct_type === 'checkboxGroup'" v-model="item.default_value" :disabled="item.read_only"> <el-checkbox-group v-if="item.d_type === 'enum' && item.ct_type === 'checkboxGroup'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat">
<el-checkbox v-for="checkItem in item.options" :label="checkItem" :value="checkItem" /> <el-checkbox v-for="checkItem in item.options" :label="checkItem" :value="checkItem" />
</el-checkbox-group> </el-checkbox-group>
<el-select v-if="item.d_type === 'enum' && item.ct_type === 'select'" v-model="item.default_value" :disabled="item.read_only" :filterable="item.allow_create" :allow-create="item.allow_create"> <el-select class="interrupt-select" v-if="item.d_type === 'enum' && item.ct_type === 'select'" v-model="formData[item.name]" :disabled="item.read_only || !isLastChat" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-option <el-option
v-for="selectItem in item.options" v-for="selectItem in item.options"
:key="selectItem" :key="selectItem"
@ -23,7 +23,7 @@
:value="selectItem" :value="selectItem"
/> />
</el-select> </el-select>
<el-select v-if="item.d_type === 'enum' && item.ct_type === 'multiselect'" multiple v-model="item.default_value" :disabled="item.read_only" :filterable="item.allow_create" :allow-create="item.allow_create"> <el-select popper-class="custom-dropdown" v-if="item.d_type === 'enum' && item.ct_type === 'multiselect'" multiple v-model="formData[item.name]" :disabled="item.read_only || !isLastChat" :filterable="item.allow_create" :allow-create="item.allow_create">
<el-option <el-option
v-for="selectItem in item.options" v-for="selectItem in item.options"
:key="selectItem" :key="selectItem"
@ -34,29 +34,53 @@
</el-form-item> </el-form-item>
</template> </template>
<el-form-item> <el-form-item>
<el-button :disabled="!isLastChat" v-for="item in source.action" :type="item.style" @click="click(item)">{{item.label}}</el-button> <div style="text-align: center;width: 100%">
<el-button :disabled="!isLastChat" v-for="item in source.action" :type="item.style" @click="click(item)">
<el-icon v-if="!isLastChat && action === item.action"><CircleCheckFilled /></el-icon>
{{item.label}}
</el-button>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
</template> </template>
<script setup> <script setup>
import { ref, nextTick, computed, watch, reactive, onMounted } from 'vue'
const emit = defineEmits(['processAuth']) const emit = defineEmits(['processAuth'])
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const props = defineProps({ const props = defineProps({
source: Object, source: Object,
checkpointer: Object, checkpointer: Object,
action: String,
chatId: String, chatId: String,
isLastChat: Boolean isLastChat: Boolean
}) })
const formData = ref({}) const formData = reactive({});
const formRules = ref({}) const formRules = computed(() => {
const rules = {};
onMounted(()=>{ props.source.block.forEach((item) => {
formData.value = props.source rules[item.name] = getValidationRules(item);
}) });
return rules;
});
function getValidationRules(item) {
const baseRules = [];
//
if (item.required) {
baseRules.push({
required: true,
message: `${item.name}必填`,
// trigger: 'blur'
});
}
return [...baseRules];
}
watch(() => props.source?.block, (blocks) => {
if (!blocks) return;
//
blocks.forEach((item) => {
formData[item.name] = item.default_value
});
}, { immediate: true });
function click(item){ function click(item){
if (item.style === 'primary'){ if (item.style === 'primary'){
@ -64,25 +88,28 @@ function click(item){
if (valid) { if (valid) {
let data = { let data = {
'chatId':props.chatId, 'chatId':props.chatId,
'checkpointer': props.style.checkpointer, 'checkpointer': props.checkpointer,
'interrupt': formData.value, 'interrupt': props.source,
'action': item.action 'action': item.action,
'formData': formData
} }
emit('processAuth',data) emit('processAuth',data)
} }
}) })
}else{ }else{
let data = { let data = {
'checkpointer': props.style.checkpointer, 'checkpointer': props.checkpointer,
'block': formData.value.block, 'block': props.source.block,
'action': item.action 'interrupt': props.source,
'action': item.action,
'formData': formData
} }
emit('processAuth',data) emit('processAuth',data)
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
:deep(.interrupt-select .el-select-dropdown){
z-index: 2000 !important;
}
</style> </style>

25
vue-fastapi-frontend/src/views/aichat/OperationButton.vue

@ -6,12 +6,12 @@
</div> </div>
<div> <div>
<span> <span>
<el-tooltip effect="dark" content="换个答案" placement="top" popper-class="operate-tooltip"> <!-- <el-tooltip effect="dark" content="换个答案" placement="top" popper-class="operate-tooltip">-->
<el-button text @click="regeneration" style="padding: 0"> <!-- <el-button text @click="regeneration" style="padding: 0">-->
<el-icon><RefreshRight /></el-icon> <!-- <el-icon><RefreshRight /></el-icon>-->
</el-button> <!-- </el-button>-->
</el-tooltip> <!-- </el-tooltip>-->
<el-divider direction="vertical" /> <!-- <el-divider direction="vertical" />-->
<el-tooltip effect="dark" content="复制" placement="top" popper-class="operate-tooltip"> <el-tooltip effect="dark" content="复制" placement="top" popper-class="operate-tooltip">
<el-button text @click="copyClick(data)" style="padding: 0"> <el-button text @click="copyClick(data)" style="padding: 0">
<el-icon><CopyDocument /></el-icon> <el-icon><CopyDocument /></el-icon>
@ -45,7 +45,7 @@
:visible="visible" :visible="visible"
v-if="data.operate === null || data.operate === ''" v-if="data.operate === null || data.operate === ''"
placement="left" placement="left"
popper-style="z-index:99999;" popper-style="z-index:2000;"
:show-arrow=false :show-arrow=false
title="反对原因" title="反对原因"
:width="260"> :width="260">
@ -105,11 +105,12 @@ const props = defineProps({
const thumbDownReason = ref("") const thumbDownReason = ref("")
const visible = ref(false) const visible = ref(false)
const emit = defineEmits(['update:data', 'regeneration', 'changeThumb']) const emit = defineEmits(['update:data', 'changeThumb'])
// const emit = defineEmits(['update:data', 'regeneration', 'changeThumb'])
function regeneration() { // function regeneration() {
emit('regeneration') // emit('regeneration')
} // }
function closeThumb(){ function closeThumb(){
visible.value = false visible.value = false
@ -235,6 +236,6 @@ function ToPlainText(md) {
</script> </script>
<style lang="scss"> <style lang="scss">
.operate-tooltip { .operate-tooltip {
z-index: 99999 !important; z-index: 2000 !important;
} }
</style> </style>

180
vue-fastapi-frontend/src/views/aichat/aichat.vue

@ -30,7 +30,7 @@
</div> </div>
<!-- 回答 --> <!-- 回答 -->
<div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="font-weight: 400"> <div v-if="item.type === 'answer'" class="item-content mb-16 lighter" style="font-weight: 400">
<el-popconfirm popper-style="z-index:99999" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)"> <el-popconfirm popper-style="z-index:2000" title="确定要回到这一步吗?" @confirm="confirmReturn(item,index)">
<template #reference> <template #reference>
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px"> <div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i> <i class="ri-bookmark-2-fill returnToHereIcon"></i>
@ -46,27 +46,44 @@
<!-- </div>--> <!-- </div>-->
<div class="content"> <div class="content">
<MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer> <MdRenderer :is_large="is_large" :chatIndex="index" :source="item.content" @fullscreenG6="fullscreen"></MdRenderer>
<Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt> <Interrupt :isLastChat="index === (chatList.length - 1)" :chatId="item.chatId" :action="item.action" :source="item.interrupt" :checkpointer="item.checkpointer" @processAuth="processAuth"></Interrupt>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<el-button
type="primary"
v-if="item.isStop && !item.isEnd"
@click="startChat(index)"
link
>重新生成
</el-button>
<el-button type="primary" v-else-if="!item.isEnd" @click="stopChat(index)" link
>停止回答
</el-button>
</div>
</div>
<div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;"> <div v-if="item.isEnd" class="flex-between" style="display: flex; justify-content: space-between; align-items: center;">
<OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" /> <OperationButton :data="item" :index="index" @changeThumb="changeThumb" />
<!-- <OperationButton :data="item" :index="index" @regeneration="regenerationChart(index)" @changeThumb="changeThumb" />-->
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<div class="flex-between mt-8" style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px">
<div>
<div v-if="currentJobId !== ''">
<i class="ri-loader-2-line loading-icon"></i>
<span style="font-size: 14px;color: #409EFF">回复中...</span>
</div>
<!-- <el-button-->
<!-- type="primary"-->
<!-- v-if="chatList.length>0 && chatList[chatList.length-1].isStop && !chatList[chatList.length-1].isEnd"-->
<!-- @click="startChat(chatList.length-1)"-->
<!-- link-->
<!-- >重新生成-->
<!-- </el-button>-->
<!-- <el-button type="primary" v-else-if="chatList.length>0 && !chatList[chatList.length-1].isEnd" @click="stopChat(chatList.length-1)" link-->
<!-- >停止回答-->
<!-- </el-button>-->
<el-button type="primary" v-if="currentJobId !== ''" link @click="stopChat(chatList.length-1)"
>停止回答
</el-button>
</div>
</div>
<div v-if="currentError !== ''">
<div class="returnToHere" style="margin-top: 5px;margin-bottom: 5px">
<i class="ri-bookmark-2-fill returnToHereIcon"></i>
<el-divider class="returnToHereDivider">
</el-divider>
</div>
<span style="font-size: 14px">{{currentError}}</span>
<el-link :underline="false" icon="Refresh" type="primary" @click="refreshModal">重试</el-link>
</div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div class="ai-chat__operate p-24" style="padding: 24px"> <div class="ai-chat__operate p-24" style="padding: 24px">
@ -91,7 +108,7 @@
:visible="popoverVisible" :visible="popoverVisible"
placement="top" placement="top"
:width="is_large?'400px':'860px'" :width="is_large?'400px':'860px'"
popper-style="max-width:860px;z-index:99999;margin-left:35px" popper-style="max-width:860px;z-index:2000;margin-left:35px"
:show-arrow=false :show-arrow=false
:offset=1 :offset=1
> >
@ -161,7 +178,7 @@
<img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" /> <img v-show="!isDisabledChart && !loading" src="@/assets/aichat/icon_send_colorful.svg" alt="" />
</el-button> </el-button>
<el-popover <el-popover
popper-style="z-index:99999;width:300px" popper-style="z-index:2000;width:300px"
title="自动审批配置" title="自动审批配置"
placement="left-end" placement="left-end"
trigger="click" trigger="click"
@ -199,7 +216,7 @@
</div> </div>
</div> </div>
<!-- 文件导入对话框 --> <!-- 文件导入对话框 -->
<el-dialog :z-index="99999" title="文件导入" v-model="upload.open" width="400px" append-to-body> <el-dialog :z-index="2000" title="文件导入" v-model="upload.open" width="400px" append-to-body>
<el-upload v-if="upload.open" <el-upload v-if="upload.open"
ref="uploadRef" ref="uploadRef"
:headers="upload.headers" :headers="upload.headers"
@ -233,7 +250,7 @@ import OperationButton from './OperationButton.vue'
import MdRenderer from '@/views/aichat/MdRenderer.vue' import MdRenderer from '@/views/aichat/MdRenderer.vue'
import fullscreenG6 from '@/views/aichat/fullscreenG6.vue' import fullscreenG6 from '@/views/aichat/fullscreenG6.vue'
import {getToken} from "@/utils/auth.js"; import {getToken} from "@/utils/auth.js";
import {addChat, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js" import {addChat, cancelJob, DeleteChatList, postChatMessage, updateChatProcessData} from "@/api/aichat/aichat.js"
import cache from "@/plugins/cache.js"; import cache from "@/plugins/cache.js";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import Interrupt from "@/views/aichat/Interrupt.vue"; import Interrupt from "@/views/aichat/Interrupt.vue";
@ -279,8 +296,8 @@ const inputValue = ref('')
const currentQuestion = ref({}) const currentQuestion = ref({})
const chartOpenId = ref('') const chartOpenId = ref('')
const chatList = ref([]) const chatList = ref([])
const answerList = ref([])
const controller = ref(null) const controller = ref(null)
const currentChatData = ref({})
const autoProcess = ref({ const autoProcess = ref({
checkAll: false, checkAll: false,
isIndeterminate: false, isIndeterminate: false,
@ -290,6 +307,9 @@ const autoProcess = ref({
const popoverVisible = ref(false) const popoverVisible = ref(false)
const currentMachine = ref([]) const currentMachine = ref([])
const currentFiles = ref([]) const currentFiles = ref([])
const currentError = ref('')
const currentJobId = ref('')
const lastQuestion = ref({})
const upload = reactive({ const upload = reactive({
// //
open: false, open: false,
@ -415,24 +435,30 @@ watch(
watch(() => props.cookieSessionId, value => upload.data = {sessionId:value}) watch(() => props.cookieSessionId, value => upload.data = {sessionId:value})
function regenerationChart(index){ // function regenerationChart(index){
let question = chatList.value[index - 1] //
let chat = { // if (currentQuestion.value.query){
"chatId":uuidv4(), // sendChatMessage(currentQuestion.value)
"type":"question", // }else{
"content":question.content, // // let question = chatList.value[index - 1]
"time": formatDate(new Date()), // // let chat = {
"file": question.file} // // "chatId":uuidv4(),
chatList.value.push(chat) // // "type":"question",
let data = { // // "content":question.content,
"query": chat.content, // // "time": formatDate(new Date()),
"user_id": cache.local.get("username"), // // "file": question.file}
"robot": currentMachine.value.length>0?currentMachine.value[0]:"", // // chatList.value.push(chat)
"session_id": Cookies.get("chatSessionId"), // // let data = {
"doc": chat.file, // // "query": chat.content,
} // // "user_id": cache.local.get("username"),
sendChatMessage(data) // // "robot": currentMachine.value.length>0?currentMachine.value[0]:"",
} // // "session_id": Cookies.get("chatSessionId"),
// // "doc": chat.file,
// // }
// sendChatMessage(data)
// }
//
// }
function changeThumb(index,chat){ function changeThumb(index,chat){
chatList.value[index].operate = chat.operate chatList.value[index].operate = chat.operate
@ -503,6 +529,13 @@ function downloadFile(file,bucket,sessionId){
} }
function processAuth(data){ function processAuth(data){
for (let i = 0; i < data.interrupt.block.length; i++) {
for (let key in data.formData){
if (key && key === data.interrupt.block[i].name){
data.interrupt.block[i].default_value = data.formData[key]
}
}
}
let updateData = { let updateData = {
chatId: data.chatId, chatId: data.chatId,
action: data.action, action: data.action,
@ -517,10 +550,15 @@ function processAuth(data){
"resume": true, "resume": true,
"block": data.interrupt.block "block": data.interrupt.block
} }
currentChatData.value = reqData
sendChatMessage(reqData) sendChatMessage(reqData)
}) })
} }
function refreshModal(){
sendChatMessage(currentChatData.value)
}
async function sendChatHandle(event) { async function sendChatHandle(event) {
if (!event.ctrlKey) { if (!event.ctrlKey) {
// ctrl // ctrl
@ -570,8 +608,11 @@ function sendChatMessage(data){
controller.value = new AbortController() controller.value = new AbortController()
postChatMessage(data,{signal:controller.value.signal}).then(res=>{ postChatMessage(data,{signal:controller.value.signal}).then(res=>{
if (res.status !== 200){ if (res.status !== 200){
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常,错误码:"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''}) // chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":":"+res.status}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":'',"thumbDownReason":''})
currentError.value = "服务异常,错误码:"+res.status +",请联系管理员!"
}else { }else {
currentError.value = ''
currentChatData.value = {}
currentFiles.value = [] currentFiles.value = []
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''}) chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[],"isEnd":false,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName, "operate":'',"thumbDownReason":''})
const reader = res.body.getReader() const reader = res.body.getReader()
@ -579,7 +620,7 @@ function sendChatMessage(data){
reader.read().then(write).then(()=> { reader.read().then(write).then(()=> {
let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1])) let answer = JSON.parse(JSON.stringify(chatList.value[chatList.value.length - 1]))
answer.content = JSON.stringify(answer.content) answer.content = JSON.stringify(answer.content)
answer.interrupt = JSON.stringify(answer.interrupt)? answer.interrupt: null answer.interrupt = answer.interrupt ? JSON.stringify(answer.interrupt): null
answer.checkpointer = JSON.stringify(answer.checkpointer) answer.checkpointer = JSON.stringify(answer.checkpointer)
addChat(answer) addChat(answer)
}).then(()=>{ }).then(()=>{
@ -631,7 +672,7 @@ function sendChatMessage(data){
}) })
} }
}).catch((e) => { }).catch((e) => {
chatList.value.push({"chatId":uuidv4(),"type":"answer","content":[{"type":"text","content":"服务异常"}],"isEnd":true,"isStop":false,"sessionId":chatList.value[0].sessionId,"sessionName":chatList.value[0].sessionName,"operate":"","thumbDownReason":""}) currentError.value = "服务异常,请联系系统管理员!"
}) })
} }
@ -653,13 +694,11 @@ watch(
const getWrite = (reader) => { const getWrite = (reader) => {
// 1tempResult // 1tempResult
let tempResult = ''; let tempResult = '';
const write_stream = ({ done, value }) => { const write_stream = ({ done, value }) => {
if (done) return; if (done) return;
const decoder = new TextDecoder('utf-8'); const decoder = new TextDecoder('utf-8');
let str = decoder.decode(value, { stream: true }); let str = decoder.decode(value, { stream: true });
tempResult += str; tempResult += str;
// 2使 // 2使
const split = tempResult.match(/data:.*?}\r\n/g); const split = tempResult.match(/data:.*?}\r\n/g);
@ -684,6 +723,7 @@ const getWrite = (reader) => {
}; };
const processChunk = (chunk) => { const processChunk = (chunk) => {
currentJobId.value = chunk.job_id;
const lastMsg = chatList.value[chatList.value.length - 1]; const lastMsg = chatList.value[chatList.value.length - 1];
// 5 // 5
if (chunk.docs?.length) { if (chunk.docs?.length) {
@ -700,12 +740,13 @@ const getWrite = (reader) => {
// 6 // 6
// const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n"); // const text = chunk.choices[0].delta.content.replace(/\n/g, "\n\n");
// //
const content = chunk.choices[0].delta.content; const text = chunk.choices[0].delta.content;
const isNewParagraph = content.startsWith('\n\n'); // const isNewParagraph = content.startsWith('\n\n');
// //
const text = isNewParagraph // const text = content;
? content.replace(/\n{2,}/g, '\n\n') // const text = isNewParagraph
: content.replace(/\n/g, ' '); // ? content.replace(/\n{2,}/g, '\n\n')
// : content.replace(/\n/g, '\n\n');
const lastContent = lastMsg.content[lastMsg.content.length - 1]; const lastContent = lastMsg.content[lastMsg.content.length - 1];
if (lastContent?.type === "text") { if (lastContent?.type === "text") {
lastContent.content += text; lastContent.content += text;
@ -719,6 +760,7 @@ const getWrite = (reader) => {
// 7 // 7
if (chunk.checkpointer) { if (chunk.checkpointer) {
lastMsg.checkpointer = chunk.checkpointer lastMsg.checkpointer = chunk.checkpointer
currentJobId.value = ''
lastMsg.isEnd = true; lastMsg.isEnd = true;
lastMsg.time = formatDate(new Date()); lastMsg.time = formatDate(new Date());
} }
@ -729,6 +771,9 @@ const getWrite = (reader) => {
}; };
const stopChat = (index) => { const stopChat = (index) => {
if (currentJobId.value !== ''){
cancelJob({job_id:currentJobId.value})
}
chatList.value[index].isStop = true chatList.value[index].isStop = true
if (controller.value !== null){ if (controller.value !== null){
controller.value.abort() controller.value.abort()
@ -736,11 +781,12 @@ const stopChat = (index) => {
let answer = JSON.parse(JSON.stringify(chatList.value[index])) let answer = JSON.parse(JSON.stringify(chatList.value[index]))
answer.content = JSON.stringify(answer.content) answer.content = JSON.stringify(answer.content)
addChat(answer) addChat(answer)
currentJobId.value = ''
} }
const startChat = (index) => { // const startChat = (index) => {
regenerationChart(index) // regenerationChart(index)
} // }
defineExpose({ defineExpose({
setScrollBottom setScrollBottom
@ -945,5 +991,33 @@ defineExpose({
:deep(.el-divider__line) { :deep(.el-divider__line) {
border-top-color: var(--divider-border-color) !important; border-top-color: var(--divider-border-color) !important;
} }
/* 定义旋转动画 */
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 应用动画到图标 */
//.ri-loader-2-line {
// display: inline-block; /* transform */
// animation: rotate 1s linear infinite; /* 1 */
// transform-origin: 50% 50%;
//}
/* 可选:添加渐隐效果 */
//.ri-loader-2-line.fade {
// opacity: 0.7;
// transition: opacity 0.3s;
//}
.loading-icon {
display: inline-block; /* 确保 transform 生效 */
animation: rotate 1s linear infinite; /* 1秒一圈,匀速旋转,无限循环 */
transform-origin: 50% 50%;
color: #409EFF; /* 修改颜色 */
//animation: rotate 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
/* 使用非匀速旋转增加动感 */
}
</style> </style>

4
vue-fastapi-frontend/src/views/aichat/index.vue

@ -226,7 +226,7 @@ onMounted(
top: 56px; top: 56px;
background: #ffffff; background: #ffffff;
padding-bottom: 24px; padding-bottom: 24px;
z-index: 2009; z-index: 1009;
} }
.chat-popover-button { .chat-popover-button {
z-index: 100; z-index: 100;
@ -244,7 +244,7 @@ onMounted(
position: absolute; position: absolute;
right: 0; right: 0;
top: 56px; top: 56px;
z-index: 2008; z-index: 1008;
} }
.gradient-divider { .gradient-divider {
position: relative; position: relative;

34
vue-fastapi-frontend/src/views/aichat/markdown.vue

@ -8,10 +8,19 @@ const props = defineProps({
markdownString: String markdownString: String
}) })
const compiledMarkdown = ref('') const compiledMarkdown = ref('')
const md = new MarkdownIt() const md = new MarkdownIt({
breaks: true, // <br>
html: true, // HTML
//
configure: (md) => {
md.renderer.rules.hardbreak = (tokens, idx) => {
return '<br class="custom-break">';
};
}
})
watch(() => props.markdownString, (newValue) => { watch(() => props.markdownString, (newValue) => {
if (newValue){ if (newValue){
compiledMarkdown.value = md.render(newValue.replace("\n","\n\n")); compiledMarkdown.value = md.render(newValue);
} }
}, { immediate: true }); }, { immediate: true });
</script> </script>
@ -20,4 +29,25 @@ watch(() => props.markdownString, (newValue) => {
text-decoration: underline; text-decoration: underline;
color: blue color: blue
} }
.markdown-content {
//
line-height: 1.8;
//
p {
margin: 0; //
//margin-bottom: 0.5em; //
}
// <br>
br {
display: block;
content: ''; //
margin-top: 0.5em; //
}
//
word-break: break-word;
overflow-wrap: break-word;
}
</style> </style>
Loading…
Cancel
Save