55 v-model =" dialogVisible"
66 style =" width : 600px "
77 append-to-body
8- :close-on-click-modal =" false"
9- :close-on-press-escape =" false"
8+ :close-on-click-modal =" true"
9+ :close-on-press-escape =" true"
10+ :before-close =" handleDialogClose"
1011 >
1112 <div class =" generate-prompt-dialog-bg border-r-8" >
1213 <div class =" scrollbar-height" >
2829 </p >
2930 <p v-else class =" flex align-center" >
3031 <AppIcon iconName =" app-generate-star" class =" color-primary mr-4" ></AppIcon >
31- 提示词显示在这里
32+ {{ $t('views.application.generateDialog.title') }}
3233 </p >
3334 </el-scrollbar >
34- <div v-if =" answer && !loading" >
35- <el-button type =" primary" @click =" () => emit('replace', answer)" > 替换 </el-button >
35+ <div v-if =" answer && !loading && !isStreaming && !showContinueButton " >
36+ <el-button type =" primary" @click =" () => emit('replace', answer)" > {{ $t('views.application.generateDialog.replace') }} </el-button >
3637 <el-button @click =" reAnswerClick" :disabled =" !answer || loading" :loading =" loading" >
37- 重新生成
38+ {{ $t('views.application.generateDialog.remake') }}
3839 </el-button >
3940 </div >
4041 </div >
4142
4243 <!-- 文本输入框 -->
4344
4445 <div class =" generate-prompt-operate p-16" >
45- <div class =" text-center mb-8" v-if = " loading " >
46- <el-button class =" border-primary video-stop-button" @click =" stopChat " >
46+ <div v-if = " showStopButton " class =" text-center mb-8" >
47+ <el-button class =" border-primary video-stop-button" @click =" pauseStreaming " >
4748 <app-icon iconName =" app-video-stop" class =" mr-8" ></app-icon >
48- 停止生成
49+ {{ $t('views.application.generateDialog.stop') }}
50+ </el-button >
51+ </div >
52+ <div v-if =" showContinueButton" class =" text-center mb-8" >
53+ <el-button class =" border-primary video-stop-button" @click =" continueStreaming" >
54+ <app-icon iconName =" app-video-stop" class =" mr-8" ></app-icon >
55+ {{ $t('views.application.generateDialog.continue') }}
4956 </el-button >
5057 </div >
51-
5258 <div class =" operate-textarea" >
5359 <el-input
5460 ref =" quickInputRef"
6672 <el-button
6773 text
6874 class =" sent-button"
69- :disabled =" !inputValue.trim() || loading"
75+ :disabled =" !inputValue.trim() || loading || isStreaming "
7076 @click =" handleSubmit"
7177 >
72- <img v-show =" !inputValue.trim() || loading" src =" @/assets/icon_send.svg" alt =" " />
73- <SendIcon v-show =" inputValue.trim() && !loading" />
78+ <img v-show =" !inputValue.trim() || loading || isStreaming " src =" @/assets/icon_send.svg" alt =" " />
79+ <SendIcon v-show =" inputValue.trim() && !loading && !isStreaming " />
7480 </el-button >
7581 </div >
7682 </div >
8288</template >
8389
8490<script setup lang="ts">
85- import { computed , reactive , ref , nextTick , watch } from ' vue'
91+ import { computed , onUnmounted , reactive , ref , nextTick , watch } from ' vue'
8692import { useRoute } from ' vue-router'
93+ import { MsgConfirm } from ' @/utils/message'
94+ import { t } from ' @/locales'
8795import systemGeneratePromptAPI from ' @/api/system-resource-management/application'
8896import generatePromptAPI from ' @/api/application/application'
8997import useStore from ' @/stores'
@@ -146,6 +154,71 @@ const promptTemplates = {
146154 ` ,
147155}
148156
157+ const isStreaming = ref <boolean >(false ) // 是否正在流式输出
158+ const isPaused = ref <boolean >(false ) // 是否暂停
159+ const fullContent = ref <string >(' ' ) // 完整内容缓存
160+ const currentDisplayIndex = ref <number >(0 ) // 当前显示到的字符位置
161+ let streamTimer: number | null = null // 定时器引用
162+ const isOutputComplete = ref <boolean >(false )
163+
164+
165+ // 模拟流式输出的定时器函数
166+ const startStreamingOutput = () => {
167+ if (streamTimer ) {
168+ clearInterval (streamTimer )
169+ }
170+
171+ isStreaming .value = true
172+ isPaused .value = false
173+
174+ streamTimer = setInterval (() => {
175+ if (! isPaused .value && currentDisplayIndex .value < fullContent .value .length ) {
176+ // 每次输出1-3个字符,模拟真实的流式输出
177+ const step = Math .min (3 , fullContent .value .length - currentDisplayIndex .value )
178+ currentDisplayIndex .value += step
179+
180+ // 更新显示内容
181+ const currentAnswer = chatMessages .value [chatMessages .value .length - 1 ]
182+ if (currentAnswer && currentAnswer .role === ' ai' ) {
183+ currentAnswer .content = fullContent .value .substring (0 , currentDisplayIndex .value )
184+ }
185+ } else if (loading .value === false && currentDisplayIndex .value >= fullContent .value .length ) {
186+ stopStreaming ()
187+ }
188+ }, 50 ) // 每50ms输出一次
189+ }
190+
191+ // 停止流式输出
192+ const stopStreaming = () => {
193+ if (streamTimer ) {
194+ clearInterval (streamTimer )
195+ streamTimer = null
196+ }
197+ isStreaming .value = false
198+ isPaused .value = false
199+ loading .value = false
200+ isOutputComplete .value = true
201+ }
202+
203+ const showStopButton = computed (() => {
204+ return isStreaming .value
205+ })
206+
207+
208+ // 暂停流式输出
209+ const pauseStreaming = () => {
210+ isPaused .value = true
211+ isStreaming .value = false
212+ }
213+
214+ // 继续流式输出
215+ const continueStreaming = () => {
216+ if (currentDisplayIndex .value < fullContent .value .length ) {
217+ startStreamingOutput ()
218+ }
219+ }
220+
221+
149222/**
150223 * 获取一个递归函数,处理流式数据
151224 * @param chat 每一条对话记录
@@ -154,8 +227,16 @@ const promptTemplates = {
154227 */
155228const getWrite = (reader : any ) => {
156229 let tempResult = ' '
157- const answer = reactive ({ content: ' ' , role: ' ai' })
158- chatMessages .value .push (answer )
230+ const middleAnswer = reactive ({ content: ' ' , role: ' ai' })
231+ chatMessages .value .push (middleAnswer )
232+
233+ // 初始化状态并
234+ fullContent .value = ' '
235+ currentDisplayIndex .value = 0
236+ isOutputComplete .value = false
237+
238+ let streamingStarted = false
239+
159240 /**
160241 *
161242 * @param done 是否结束
@@ -164,8 +245,8 @@ const getWrite = (reader: any) => {
164245 const write_stream = ({ done , value }: { done: boolean ; value: any }) => {
165246 try {
166247 if (done ) {
248+ // 流数据接收完成,但定时器继续运行直到显示完所有内容
167249 loading .value = false
168- // console.log('结束')
169250 return
170251 }
171252 const decoder = new TextDecoder (' utf-8' )
@@ -185,26 +266,31 @@ const getWrite = (reader: any) => {
185266 for (const index in split ) {
186267 const chunk = JSON ?.parse (split [index ].replace (' data:' , ' ' ))
187268 if (! chunk .is_end ) {
188- answer .content += chunk .content
269+ // 实时将新接收的内容添加到完整内容中
270+ fullContent .value += chunk .content
271+ if (! streamingStarted ) {
272+ streamingStarted = true
273+ startStreamingOutput ()
274+ }
189275 }
190276 if (chunk .is_end ) {
191- // 流处理成功 返回成功回调
192- loading .value = false
277+ isApiComplete .value = true
193278 return Promise .resolve ()
194279 }
195280 }
196281 }
197282 }
198283 } catch (e ) {
199284 loading .value = false
285+ stopStreaming ()
200286 return Promise .reject (e )
201287 }
202288 return reader .read ().then (write_stream )
203289 }
204290
205291 return write_stream
206292}
207-
293+ const isApiComplete = ref < boolean >( false )
208294const answer = computed (() => {
209295 const result = chatMessages .value [chatMessages .value .length - 1 ]
210296
@@ -214,6 +300,12 @@ const answer = computed(() => {
214300 return ' '
215301})
216302
303+ // 按钮状态计算
304+ const showContinueButton = computed (() => {
305+ return ! isStreaming .value && isPaused .value && currentDisplayIndex .value < fullContent .value .length
306+ })
307+
308+
217309function generatePrompt(inputValue : any ) {
218310 loading .value = true
219311 const workspaceId = user .getWorkspaceId () || ' default'
@@ -268,8 +360,12 @@ const handleSubmit = (event?: any) => {
268360 if (! originalUserInput .value ) {
269361 originalUserInput .value = inputValue .value
270362 }
271- generatePrompt (inputValue .value )
363+ if (inputValue .value ) {
364+ generatePrompt (inputValue .value )
272365 inputValue .value = ' '
366+ }
367+
368+
273369 } else {
274370 // 如果同时按下ctrl/shift/cmd/opt +enter,则会换行
275371 insertNewlineAtCursor (event )
@@ -290,11 +386,6 @@ const insertNewlineAtCursor = (event?: any) => {
290386 })
291387}
292388
293- const stopChat = () => {
294- loading .value = false
295- chatMessages .value = []
296- }
297-
298389const open = (modelId : string , applicationId : string ) => {
299390 modelID .value = modelId
300391 applicationID .value = applicationId
@@ -323,6 +414,38 @@ const handleScroll = () => {
323414 }
324415}
325416
417+ const handleDialogClose = (done : () => void ) => {
418+
419+ // 弹出 消息
420+ MsgConfirm (
421+ t (' common.tip' ),
422+ t (' views.application.generateDialog.exit' ),
423+ {
424+ confirmButtonText: t (' common.confirm' ),
425+ cancelButtonText: t (' common.cancel' ),
426+ distinguishCancelAndClose: true ,
427+ }
428+ )
429+ .then (() => {
430+ // 点击确认,清除状态
431+ stopStreaming ()
432+ chatMessages .value = []
433+ fullContent .value = ' '
434+ currentDisplayIndex .value = 0
435+ isOutputComplete .value = false
436+ done () // 真正关闭
437+ })
438+ .catch (() => {
439+ // 点击取消
440+ }
441+ )
442+ }
443+
444+ // 组件卸载时清理定时器
445+ onUnmounted (() => {
446+ stopStreaming ()
447+ })
448+
326449watch (
327450 answer ,
328451 () => {
0 commit comments