Skip to content

Commit 05205af

Browse files
authored
feat:Adjust the logic and interaction of the AI building page (#1658)
1 parent f4b922f commit 05205af

File tree

4 files changed

+171
-63
lines changed

4 files changed

+171
-63
lines changed

packages/common/composable/http/index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export default defineService({
6060
post: (...args) => http?.post(...args),
6161
request: (...args) => http?.request(...args),
6262
put: (...args) => http?.put(...args),
63-
delete: (...args) => http?.delete(...args)
63+
delete: (...args) => http?.delete(...args),
64+
stream: (config) => {
65+
const streamConfig = {
66+
responseType: 'stream',
67+
...config
68+
}
69+
return http?.request(streamConfig)
70+
}
6471
})
6572
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div class="build-loading-renderer">
3+
<img src="../assets/loading.webp" alt="loading" />
4+
<div class="build-loading-renderer-content">
5+
<div class="build-loading-renderer-content-header">页面生成中,请稍等片刻</div>
6+
<div class="build-loading-renderer-content-body">{{ renderContent }}</div>
7+
</div>
8+
</div>
9+
</template>
10+
11+
<script lang="ts">
12+
import { computed } from 'vue'
13+
14+
export default {
15+
props: {
16+
content: {
17+
type: String,
18+
required: true
19+
}
20+
},
21+
setup(props) {
22+
const renderContent = computed(() => {
23+
return props.content.slice(-30)
24+
})
25+
26+
return {
27+
renderContent
28+
}
29+
}
30+
}
31+
</script>
32+
33+
<style lang="less">
34+
.build-loading-renderer {
35+
display: flex;
36+
img {
37+
width: 20px;
38+
height: 20px;
39+
}
40+
&-content {
41+
margin-left: 16px;
42+
&-header {
43+
font-size: 14px;
44+
font-weight: 700;
45+
margin-bottom: 10px;
46+
}
47+
&-body {
48+
color: var(--te-chat-model-helper-text);
49+
font-size: 12px;
50+
width: 160px;
51+
height: 30px;
52+
overflow: hidden;
53+
text-overflow: ellipsis;
54+
white-space: nowrap;
55+
}
56+
}
57+
}
58+
</style>

packages/plugins/robot/src/Main.vue

Lines changed: 83 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
@item-click="handlePromptItemClick"
5656
></tr-prompts>
5757
</div>
58-
<tr-bubble-provider :content-renderers="contentRenderers" v-else>
58+
<tr-bubble-provider
59+
:content-renderers="aiType === AI_MODES['Chat'] ? contentRenderers : buildContentRenderers"
60+
v-else
61+
>
5962
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
6063
</tr-bubble-provider>
6164
</div>
@@ -144,11 +147,12 @@ import { utils } from '@opentiny/tiny-engine-utils'
144147
import RobotSettingPopover from './RobotSettingPopover.vue'
145148
import { PROMPTS } from './js/prompts'
146149
import * as jsonpatch from 'fast-json-patch'
147-
import { chatStream, checkComponentNameExists } from './js/utils'
150+
import { checkComponentNameExists, processSSEStream } from './js/utils'
148151
import McpServer from './mcp/McpServer.vue'
149152
import useMcpServer from './mcp/useMcp'
150153
import MarkdownRenderer from './mcp/MarkdownRenderer.vue'
151154
import LoadingRenderer from './mcp/LoadingRenderer.vue'
155+
import BuildLoadingRenderer from './BuildLoadingRenderer.vue'
152156
import { sendMcpRequest, serializeError } from './mcp/utils'
153157
import type { RobotMessage } from './mcp/types'
154158
import RobotTypeSelect from './RobotTypeSelect.vue'
@@ -395,73 +399,85 @@ export default {
395399
}
396400
397401
let streamContent = ''
402+
let lastResponseLength = 0
398403
const chatId = Date.now().toString()
399404
const currentJson = deepClone(pageState.pageSchema)
400405
let lastExecutionTime = 0
401406
const throttleDelay = 3000
402-
await chatStream(
403-
{
404-
requestUrl: '/app-center/api/ai/chat',
405-
requestData: params
406-
},
407-
{
408-
onData: (data) => {
409-
const choice = data.choices?.[0]
410-
if (choice && choice.delta.content) {
411-
if (messages.value.length === 0 || messages.value[messages.value.length - 1].role !== 'assistant') {
412-
messages.value.push(getAiDisplayMessage('', 'assistant', {}, chatId))
413-
}
414-
if (streamContent !== messages.value[messages.value.length - 1].content) {
415-
messages.value[messages.value.length - 1].content = ''
416-
}
417-
streamContent += choice.delta.content
418-
messages.value[messages.value.length - 1].content += choice.delta.content
419-
const currentTime = Date.now()
420-
if (currentTime - lastExecutionTime > throttleDelay) {
421-
try {
422-
const repaired = jsonrepair(streamContent)
423-
const parsedJson = JSON.parse(repaired)
424-
const result = parsedJson.reduce((acc, patch) => {
425-
return jsonpatch.applyPatch(acc, [patch], false, false).newDocument
426-
}, currentJson)
427-
const editorValue = string2Obj(obj2String(result))
428-
429-
if (editorValue && checkComponentNameExists(result)) {
430-
setSchema(result)
431-
}
432-
} catch (error) {
433-
// error
434-
}
435-
lastExecutionTime = currentTime
436-
}
437-
}
438-
},
439-
onError: (error) => {
440-
messages.value[messages.value.length - 1].content = '连接失败'
441-
localStorage.removeItem('aiChat')
442-
inProcesing.value = false
443-
connectedFailed.value = false
444-
// eslint-disable-next-line no-console
445-
console.error('Stream error:', error)
407+
requestLoading.value = true
408+
getMetaApi(META_SERVICE.Http)
409+
.stream({
410+
method: 'POST',
411+
url: '/app-center/api/ai/chat',
412+
data: params,
413+
headers: {
414+
'Content-Type': 'application/json',
415+
Accept: 'text/event-stream',
416+
Authorization: `Bearer ${robotSettingState.selectedModel.apiKey || import.meta.env.VITE_API_TOKEN}`
446417
},
447-
onDone: () => {
448-
handleResponse(
449-
{
450-
id: chatId,
451-
chatMessage: {
452-
role: 'assistant',
453-
content: streamContent || '没有返回内容',
454-
name: 'AI'
418+
onDownloadProgress: (progressEvent) => {
419+
const currentResponse = progressEvent.currentTarget.responseText
420+
const newData = currentResponse.substring(lastResponseLength)
421+
lastResponseLength = currentResponse.length
422+
processSSEStream(newData, {
423+
onData: (data) => {
424+
const choice = data.choices?.[0]
425+
if (choice && choice.delta.content) {
426+
if (messages.value.length === 0 || messages.value[messages.value.length - 1].role !== 'assistant') {
427+
messages.value.push(getAiDisplayMessage('', 'assistant', {}, chatId))
428+
}
429+
if (streamContent !== messages.value[messages.value.length - 1].content) {
430+
messages.value[messages.value.length - 1].content = ''
431+
}
432+
streamContent += choice.delta.content
433+
messages.value.at(-1)!.renderContent = [{ type: 'loading', content: streamContent }]
434+
const currentTime = Date.now()
435+
if (currentTime - lastExecutionTime > throttleDelay) {
436+
try {
437+
const repaired = jsonrepair(streamContent)
438+
const parsedJson = JSON.parse(repaired)
439+
const result = parsedJson.reduce((acc, patch) => {
440+
return jsonpatch.applyPatch(acc, [patch], false, false).newDocument
441+
}, currentJson)
442+
const editorValue = string2Obj(obj2String(result))
443+
444+
if (editorValue && checkComponentNameExists(result)) {
445+
setSchema(result)
446+
}
447+
} catch (error) {
448+
// error
449+
}
450+
lastExecutionTime = currentTime
451+
}
455452
}
456453
},
457-
currentJson
458-
)
454+
onDone: () => {
455+
requestLoading.value = false
456+
delete messages.value.at(-1)!.renderContent
457+
handleResponse(
458+
{
459+
id: chatId,
460+
chatMessage: {
461+
role: 'assistant',
462+
content: streamContent || '没有返回内容',
463+
name: 'AI'
464+
}
465+
},
466+
currentJson
467+
)
468+
}
469+
})
459470
}
460-
},
461-
{
462-
Authorization: `Bearer ${robotSettingState.selectedModel.apiKey || import.meta.env.VITE_API_TOKEN}`
463-
}
464-
)
471+
})
472+
.catch((error) => {
473+
messages.value[messages.value.length - 1].content = '连接失败'
474+
localStorage.removeItem('aiChat')
475+
requestLoading.value = false
476+
inProcesing.value = false
477+
connectedFailed.value = false
478+
// eslint-disable-next-line no-console
479+
console.error('Stream error:', error)
480+
})
465481
}
466482
}
467483
@@ -785,6 +801,10 @@ export default {
785801
loading: LoadingRenderer
786802
}
787803
804+
const buildContentRenderers: Record<string, Component> = {
805+
loading: BuildLoadingRenderer
806+
}
807+
788808
const mcpDrawerPosition = computed(() => {
789809
return {
790810
type: 'fixed',
@@ -841,7 +861,8 @@ export default {
841861
typeChange,
842862
isVisualModel,
843863
contentRenderers,
844-
mcpDrawerPosition
864+
mcpDrawerPosition,
865+
buildContentRenderers
845866
}
846867
}
847868
}

packages/plugins/robot/src/js/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,25 @@ export const checkComponentNameExists = (data: any) => {
4242

4343
return true
4444
}
45+
46+
export const processSSEStream = (data,handler) => {
47+
const lines = data.split('\n')
48+
49+
for (const line of lines) {
50+
if (line.startsWith('data: ')) {
51+
const dataStr = line.substring(6).trim()
52+
53+
// 检查结束标记
54+
if (dataStr === '[DONE]') {
55+
handler.onDone()
56+
57+
return
58+
}
59+
60+
if (dataStr) {
61+
const data = JSON.parse(dataStr)
62+
handler.onData(data)
63+
}
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)