feat:Adjust the logic and interaction of the AI building page (#1658)
This commit is contained in:
parent
f4b922f719
commit
05205afc20
|
|
@ -60,6 +60,13 @@ export default defineService({
|
|||
post: (...args) => http?.post(...args),
|
||||
request: (...args) => http?.request(...args),
|
||||
put: (...args) => http?.put(...args),
|
||||
delete: (...args) => http?.delete(...args)
|
||||
delete: (...args) => http?.delete(...args),
|
||||
stream: (config) => {
|
||||
const streamConfig = {
|
||||
responseType: 'stream',
|
||||
...config
|
||||
}
|
||||
return http?.request(streamConfig)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<div class="build-loading-renderer">
|
||||
<img src="../assets/loading.webp" alt="loading" />
|
||||
<div class="build-loading-renderer-content">
|
||||
<div class="build-loading-renderer-content-header">页面生成中,请稍等片刻</div>
|
||||
<div class="build-loading-renderer-content-body">{{ renderContent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const renderContent = computed(() => {
|
||||
return props.content.slice(-30)
|
||||
})
|
||||
|
||||
return {
|
||||
renderContent
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.build-loading-renderer {
|
||||
display: flex;
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
&-content {
|
||||
margin-left: 16px;
|
||||
&-header {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
&-body {
|
||||
color: var(--te-chat-model-helper-text);
|
||||
font-size: 12px;
|
||||
width: 160px;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -55,7 +55,10 @@
|
|||
@item-click="handlePromptItemClick"
|
||||
></tr-prompts>
|
||||
</div>
|
||||
<tr-bubble-provider :content-renderers="contentRenderers" v-else>
|
||||
<tr-bubble-provider
|
||||
:content-renderers="aiType === AI_MODES['Chat'] ? contentRenderers : buildContentRenderers"
|
||||
v-else
|
||||
>
|
||||
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
|
||||
</tr-bubble-provider>
|
||||
</div>
|
||||
|
|
@ -144,11 +147,12 @@ import { utils } from '@opentiny/tiny-engine-utils'
|
|||
import RobotSettingPopover from './RobotSettingPopover.vue'
|
||||
import { PROMPTS } from './js/prompts'
|
||||
import * as jsonpatch from 'fast-json-patch'
|
||||
import { chatStream, checkComponentNameExists } from './js/utils'
|
||||
import { checkComponentNameExists, processSSEStream } from './js/utils'
|
||||
import McpServer from './mcp/McpServer.vue'
|
||||
import useMcpServer from './mcp/useMcp'
|
||||
import MarkdownRenderer from './mcp/MarkdownRenderer.vue'
|
||||
import LoadingRenderer from './mcp/LoadingRenderer.vue'
|
||||
import BuildLoadingRenderer from './BuildLoadingRenderer.vue'
|
||||
import { sendMcpRequest, serializeError } from './mcp/utils'
|
||||
import type { RobotMessage } from './mcp/types'
|
||||
import RobotTypeSelect from './RobotTypeSelect.vue'
|
||||
|
|
@ -395,73 +399,85 @@ export default {
|
|||
}
|
||||
|
||||
let streamContent = ''
|
||||
let lastResponseLength = 0
|
||||
const chatId = Date.now().toString()
|
||||
const currentJson = deepClone(pageState.pageSchema)
|
||||
let lastExecutionTime = 0
|
||||
const throttleDelay = 3000
|
||||
await chatStream(
|
||||
{
|
||||
requestUrl: '/app-center/api/ai/chat',
|
||||
requestData: params
|
||||
},
|
||||
{
|
||||
onData: (data) => {
|
||||
const choice = data.choices?.[0]
|
||||
if (choice && choice.delta.content) {
|
||||
if (messages.value.length === 0 || messages.value[messages.value.length - 1].role !== 'assistant') {
|
||||
messages.value.push(getAiDisplayMessage('', 'assistant', {}, chatId))
|
||||
}
|
||||
if (streamContent !== messages.value[messages.value.length - 1].content) {
|
||||
messages.value[messages.value.length - 1].content = ''
|
||||
}
|
||||
streamContent += choice.delta.content
|
||||
messages.value[messages.value.length - 1].content += choice.delta.content
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - lastExecutionTime > throttleDelay) {
|
||||
try {
|
||||
const repaired = jsonrepair(streamContent)
|
||||
const parsedJson = JSON.parse(repaired)
|
||||
const result = parsedJson.reduce((acc, patch) => {
|
||||
return jsonpatch.applyPatch(acc, [patch], false, false).newDocument
|
||||
}, currentJson)
|
||||
const editorValue = string2Obj(obj2String(result))
|
||||
|
||||
if (editorValue && checkComponentNameExists(result)) {
|
||||
setSchema(result)
|
||||
requestLoading.value = true
|
||||
getMetaApi(META_SERVICE.Http)
|
||||
.stream({
|
||||
method: 'POST',
|
||||
url: '/app-center/api/ai/chat',
|
||||
data: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${robotSettingState.selectedModel.apiKey || import.meta.env.VITE_API_TOKEN}`
|
||||
},
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
const currentResponse = progressEvent.currentTarget.responseText
|
||||
const newData = currentResponse.substring(lastResponseLength)
|
||||
lastResponseLength = currentResponse.length
|
||||
processSSEStream(newData, {
|
||||
onData: (data) => {
|
||||
const choice = data.choices?.[0]
|
||||
if (choice && choice.delta.content) {
|
||||
if (messages.value.length === 0 || messages.value[messages.value.length - 1].role !== 'assistant') {
|
||||
messages.value.push(getAiDisplayMessage('', 'assistant', {}, chatId))
|
||||
}
|
||||
if (streamContent !== messages.value[messages.value.length - 1].content) {
|
||||
messages.value[messages.value.length - 1].content = ''
|
||||
}
|
||||
streamContent += choice.delta.content
|
||||
messages.value.at(-1)!.renderContent = [{ type: 'loading', content: streamContent }]
|
||||
const currentTime = Date.now()
|
||||
if (currentTime - lastExecutionTime > throttleDelay) {
|
||||
try {
|
||||
const repaired = jsonrepair(streamContent)
|
||||
const parsedJson = JSON.parse(repaired)
|
||||
const result = parsedJson.reduce((acc, patch) => {
|
||||
return jsonpatch.applyPatch(acc, [patch], false, false).newDocument
|
||||
}, currentJson)
|
||||
const editorValue = string2Obj(obj2String(result))
|
||||
|
||||
if (editorValue && checkComponentNameExists(result)) {
|
||||
setSchema(result)
|
||||
}
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
lastExecutionTime = currentTime
|
||||
}
|
||||
} catch (error) {
|
||||
// error
|
||||
}
|
||||
lastExecutionTime = currentTime
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
messages.value[messages.value.length - 1].content = '连接失败'
|
||||
localStorage.removeItem('aiChat')
|
||||
inProcesing.value = false
|
||||
connectedFailed.value = false
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Stream error:', error)
|
||||
},
|
||||
onDone: () => {
|
||||
handleResponse(
|
||||
{
|
||||
id: chatId,
|
||||
chatMessage: {
|
||||
role: 'assistant',
|
||||
content: streamContent || '没有返回内容',
|
||||
name: 'AI'
|
||||
}
|
||||
},
|
||||
currentJson
|
||||
)
|
||||
onDone: () => {
|
||||
requestLoading.value = false
|
||||
delete messages.value.at(-1)!.renderContent
|
||||
handleResponse(
|
||||
{
|
||||
id: chatId,
|
||||
chatMessage: {
|
||||
role: 'assistant',
|
||||
content: streamContent || '没有返回内容',
|
||||
name: 'AI'
|
||||
}
|
||||
},
|
||||
currentJson
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
Authorization: `Bearer ${robotSettingState.selectedModel.apiKey || import.meta.env.VITE_API_TOKEN}`
|
||||
}
|
||||
)
|
||||
})
|
||||
.catch((error) => {
|
||||
messages.value[messages.value.length - 1].content = '连接失败'
|
||||
localStorage.removeItem('aiChat')
|
||||
requestLoading.value = false
|
||||
inProcesing.value = false
|
||||
connectedFailed.value = false
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Stream error:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -785,6 +801,10 @@ export default {
|
|||
loading: LoadingRenderer
|
||||
}
|
||||
|
||||
const buildContentRenderers: Record<string, Component> = {
|
||||
loading: BuildLoadingRenderer
|
||||
}
|
||||
|
||||
const mcpDrawerPosition = computed(() => {
|
||||
return {
|
||||
type: 'fixed',
|
||||
|
|
@ -841,7 +861,8 @@ export default {
|
|||
typeChange,
|
||||
isVisualModel,
|
||||
contentRenderers,
|
||||
mcpDrawerPosition
|
||||
mcpDrawerPosition,
|
||||
buildContentRenderers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,3 +42,25 @@ export const checkComponentNameExists = (data: any) => {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
export const processSSEStream = (data,handler) => {
|
||||
const lines = data.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const dataStr = line.substring(6).trim()
|
||||
|
||||
// 检查结束标记
|
||||
if (dataStr === '[DONE]') {
|
||||
handler.onDone()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (dataStr) {
|
||||
const data = JSON.parse(dataStr)
|
||||
handler.onData(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue