Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ui/src/locales/lang/en-US/views/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export default {
label: 'Generate',
generatePrompt: 'Generate Prompt',
placeholder: 'Please enter the prompt topic',
title: 'The prompt is displayed here',
remake: 'Regenerate',
stop: 'Stop Generating',
continue: 'Continue Generating',
replace: 'Replace',
exit: 'Are you sure you want to exit and discard the AI-generated content?',
},
dialog: {
addKnowledge: 'Add Related Knowledge',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code appears to be part of a configuration object used for localization in a web application or similar project. The export statement exports an object with various keys, each containing human-readable strings that will be displayed to users.

Here's a brief summary of the corrections needed:

  1. Typo: There might be a typo in the key 'remake'. It should likely be 'regenerate'.

  2. Consistency: Ensure consistency throughout the translation entries. For example, both 'generatePrompt' should use consistent terminology and formatting (e.g., 'prompt', no extra spaces after it).

Here’s the corrected version of the block:

@@ -135,6 +135,9 @@ export default {
     label: 'Generate',
     generatePrompt: 'Generate Prompt',
     placeholder: 'Please enter the prompt topic',
+    title: 'The prompt is displayed here',
+    regenerate: 'Regenerate',
+    stopGenerating: 'Stop Generating',
     continueGenerating: 'Continue Generating',
     replaceContent: 'Replace Content',
     exitConfirmation: 'Are you sure you want to exit and discard the AI-generated content?',
   },

In this revised version:

  • 'replace' has been replaced with 'replaceContent' which seems more natural if the intention is to change the generated text.
  • A new key 'exitConfirmation' has been added to confirm whether the user wants to cancel their action while still having access to the AI-generated content. This improves clarity as it provides options on how to handle changes versus abandoning them entirely.

Expand Down
6 changes: 6 additions & 0 deletions ui/src/locales/lang/zh-CN/views/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ export default {
label: '生成',
generatePrompt: '生成提示词',
placeholder: '请输入提示词主题',
title: '提示词显示在这里',
remake: '重新生成',
stop: '停止生成',
continue: '继续生成',
replace: '替换',
exit: '确认退出并舍弃 AI 生成的内容吗?',
},
dialog: {
addKnowledge: '添加关联知识库',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your code looks mostly correct, but there are a few improvements that could be made:

  1. Consistent String Formatting: Consider using string interpolation (template literals) to make the string formatting more consistent across different parts of the object.

  2. Error Handling for placeholder:

    • Add a check to ensure that placeholder is not empty or null before setting it.
    • Provide default values if necessary.
  3. Optimization Suggestions:

    • Ensure that all strings are unique to avoid namespace conflicts within different contexts.

Here's an example of how you can improve the readability and robustness of the strings.js file while maintaining the existing functionality:

export default {
  common: {
    cancel: '取消',
    confirm: '确认',
  },
  
  settings: {
    saveButtonLabel: {
      type: 'save_settings_dialog_save_button_label'
    }
  // other settings...
}

// Example of adding error handling for placeholder
const placeholder = '请输入提示词主题';
if (!placeholder || typeof placeholder !== 'string') {
  throw new Error('Placeholder must be a non-empty string');
}

This way, when someone tries to set an invalid value for placeholder, TypeScript will catch it during compilation, providing better debugging experience. Remember to replace placeholders like {type: "save_settings_dialog_save_button_label"} with actual labels or keys from your application's localization setup.

Expand Down
6 changes: 6 additions & 0 deletions ui/src/locales/lang/zh-Hant/views/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export default {
label: '生成',
generatePrompt: '生成提示詞',
placeholder: '請輸入提示詞主題',
title: '提示詞顯示在這裡',
remake: '重新生成',
stop: '停止生成',
continue: '繼續生成',
replace: '替換',
exit: '確認退出並捨棄 AI 生成的內容嗎?',
},
dialog: {
addKnowledge: '新增關聯知識庫',
Expand Down
177 changes: 150 additions & 27 deletions ui/src/views/application/component/GeneratePromptDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
v-model="dialogVisible"
style="width: 600px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
:close-on-click-modal="true"
:close-on-press-escape="true"
:before-close="handleDialogClose"
>
<div class="generate-prompt-dialog-bg border-r-8">
<div class="scrollbar-height">
Expand All @@ -28,27 +29,32 @@
</p>
<p v-else class="flex align-center">
<AppIcon iconName="app-generate-star" class="color-primary mr-4"></AppIcon>
提示词显示在这里
{{ $t('views.application.generateDialog.title') }}
</p>
</el-scrollbar>
<div v-if="answer && !loading">
<el-button type="primary" @click="() => emit('replace', answer)"> 替换 </el-button>
<div v-if="answer && !loading && !isStreaming && !showContinueButton">
<el-button type="primary" @click="() => emit('replace', answer)"> {{ $t('views.application.generateDialog.replace') }} </el-button>
<el-button @click="reAnswerClick" :disabled="!answer || loading" :loading="loading">
重新生成
{{ $t('views.application.generateDialog.remake') }}
</el-button>
</div>
</div>

<!-- 文本输入框 -->

<div class="generate-prompt-operate p-16">
<div class="text-center mb-8" v-if="loading">
<el-button class="border-primary video-stop-button" @click="stopChat">
<div v-if="showStopButton" class="text-center mb-8">
<el-button class="border-primary video-stop-button" @click="pauseStreaming">
<app-icon iconName="app-video-stop" class="mr-8"></app-icon>
停止生成
{{ $t('views.application.generateDialog.stop') }}
</el-button>
</div>
<div v-if="showContinueButton" class="text-center mb-8">
<el-button class="border-primary video-stop-button" @click="continueStreaming">
<app-icon iconName="app-video-stop" class="mr-8"></app-icon>
{{ $t('views.application.generateDialog.continue') }}
</el-button>
</div>

<div class="operate-textarea">
<el-input
ref="quickInputRef"
Expand All @@ -66,11 +72,11 @@
<el-button
text
class="sent-button"
:disabled="!inputValue.trim() || loading"
:disabled="!inputValue.trim() || loading || isStreaming"
@click="handleSubmit"
>
<img v-show="!inputValue.trim() || loading" src="@/assets/icon_send.svg" alt="" />
<SendIcon v-show="inputValue.trim() && !loading" />
<img v-show="!inputValue.trim() || loading || isStreaming" src="@/assets/icon_send.svg" alt="" />
<SendIcon v-show="inputValue.trim() && !loading && !isStreaming" />
</el-button>
</div>
</div>
Expand All @@ -82,8 +88,10 @@
</template>

<script setup lang="ts">
import { computed, reactive, ref, nextTick, watch } from 'vue'
import { computed, onUnmounted,reactive, ref, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import { MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
import systemGeneratePromptAPI from '@/api/system-resource-management/application'
import generatePromptAPI from '@/api/application/application'
import useStore from '@/stores'
Expand Down Expand Up @@ -146,6 +154,71 @@ const promptTemplates = {
`,
}

const isStreaming = ref<boolean>(false) // 是否正在流式输出
const isPaused = ref<boolean>(false) // 是否暂停
const fullContent = ref<string>('') // 完整内容缓存
const currentDisplayIndex = ref<number>(0) // 当前显示到的字符位置
let streamTimer: number | null = null // 定时器引用
const isOutputComplete = ref<boolean>(false)


// 模拟流式输出的定时器函数
const startStreamingOutput = () => {
if (streamTimer) {
clearInterval(streamTimer)
}

isStreaming.value = true
isPaused.value = false

streamTimer = setInterval(() => {
if (!isPaused.value && currentDisplayIndex.value < fullContent.value.length) {
// 每次输出1-3个字符,模拟真实的流式输出
const step = Math.min(3, fullContent.value.length - currentDisplayIndex.value)
currentDisplayIndex.value += step

// 更新显示内容
const currentAnswer = chatMessages.value[chatMessages.value.length - 1]
if (currentAnswer && currentAnswer.role === 'ai') {
currentAnswer.content = fullContent.value.substring(0, currentDisplayIndex.value)
}
} else if (loading.value === false && currentDisplayIndex.value >= fullContent.value.length) {
stopStreaming()
}
}, 50) // 每50ms输出一次
}

// 停止流式输出
const stopStreaming = () => {
if (streamTimer) {
clearInterval(streamTimer)
streamTimer = null
}
isStreaming.value = false
isPaused.value = false
loading.value = false
isOutputComplete.value = true
}

const showStopButton = computed(() => {
return isStreaming.value
})


// 暂停流式输出
const pauseStreaming = () => {
isPaused.value = true
isStreaming.value = false
}

// 继续流式输出
const continueStreaming = () => {
if (currentDisplayIndex.value < fullContent.value.length) {
startStreamingOutput()
}
}


/**
* 获取一个递归函数,处理流式数据
* @param chat 每一条对话记录
Expand All @@ -154,8 +227,16 @@ const promptTemplates = {
*/
const getWrite = (reader: any) => {
let tempResult = ''
const answer = reactive({ content: '', role: 'ai' })
chatMessages.value.push(answer)
const middleAnswer = reactive({ content: '', role: 'ai' })
chatMessages.value.push(middleAnswer )

// 初始化状态并
fullContent.value = ''
currentDisplayIndex.value = 0
isOutputComplete.value = false

let streamingStarted = false

/**
*
* @param done 是否结束
Expand All @@ -164,8 +245,8 @@ const getWrite = (reader: any) => {
const write_stream = ({ done, value }: { done: boolean; value: any }) => {
try {
if (done) {
// 流数据接收完成,但定时器继续运行直到显示完所有内容
loading.value = false
// console.log('结束')
return
}
const decoder = new TextDecoder('utf-8')
Expand All @@ -185,26 +266,31 @@ const getWrite = (reader: any) => {
for (const index in split) {
const chunk = JSON?.parse(split[index].replace('data:', ''))
if (!chunk.is_end) {
answer.content += chunk.content
// 实时将新接收的内容添加到完整内容中
fullContent.value += chunk.content
if (!streamingStarted) {
streamingStarted = true
startStreamingOutput()
}
}
if (chunk.is_end) {
// 流处理成功 返回成功回调
loading.value = false
isApiComplete.value = true
return Promise.resolve()
}
}
}
}
} catch (e) {
loading.value = false
stopStreaming()
return Promise.reject(e)
}
return reader.read().then(write_stream)
}

return write_stream
}

const isApiComplete = ref<boolean>(false)
const answer = computed(() => {
const result = chatMessages.value[chatMessages.value.length - 1]

Expand All @@ -214,6 +300,12 @@ const answer = computed(() => {
return ''
})

// 按钮状态计算
const showContinueButton = computed(() => {
return !isStreaming.value && isPaused.value && currentDisplayIndex.value < fullContent.value.length
})


function generatePrompt(inputValue: any) {
loading.value = true
const workspaceId = user.getWorkspaceId() || 'default'
Expand Down Expand Up @@ -268,8 +360,12 @@ const handleSubmit = (event?: any) => {
if (!originalUserInput.value) {
originalUserInput.value = inputValue.value
}
generatePrompt(inputValue.value)
if (inputValue.value) {
generatePrompt(inputValue.value)
inputValue.value = ''
}


} else {
// 如果同时按下ctrl/shift/cmd/opt +enter,则会换行
insertNewlineAtCursor(event)
Expand All @@ -290,11 +386,6 @@ const insertNewlineAtCursor = (event?: any) => {
})
}

const stopChat = () => {
loading.value = false
chatMessages.value = []
}

const open = (modelId: string, applicationId: string) => {
modelID.value = modelId
applicationID.value = applicationId
Expand Down Expand Up @@ -323,6 +414,38 @@ const handleScroll = () => {
}
}

const handleDialogClose = (done: () => void) => {

// 弹出 消息
MsgConfirm(
t('common.tip'),
t('views.application.generateDialog.exit'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
distinguishCancelAndClose: true,
}
)
.then(() => {
// 点击确认,清除状态
stopStreaming()
chatMessages.value = []
fullContent.value = ''
currentDisplayIndex.value = 0
isOutputComplete.value = false
done() // 真正关闭
})
.catch(() => {
// 点击取消
}
)
}

// 组件卸载时清理定时器
onUnmounted(() => {
stopStreaming()
})

watch(
answer,
() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code looks generally correct but lacks some critical optimizations and security considerations. Here are my observations:

  1. Security Concerns:

    • The MsgConfirm function call uses a translation method ($t) which suggests that translations are missing or incorrectly formatted in your Vue.js project. Ensure all necessary translations are correctly set up and loaded.
  2. Component Unmounting Issue:

    • The cleanup of the streamTimer inside the onUnmounted hook is good practice to prevent memory leaks during component unmounting. However, it’s worth noting that the timer might be stopped multiple times due to early exits before receiving complete responses.
  3. Stream Handling Issues:

    • The flow when streaming data can lead to edge cases where partial messages might still be cached or processed. Consider adding checks within the if (!split[index].replace('data:', '')) {} block to ensure only valid chunks are handled.
  4. Loading State Management:

    • Currently, loading state management isn't fully clear since there aren’t direct updates or reset operations after handling API requests. This might cause inconsistencies especially when generating long answers slowly.

Suggested Improvements

  1. Error Logging & Cleanup:

    • Add logging for errors occurring during message processing to help debug any issues with incomplete responses.
    const handleWrite = ({ done, value }: { done: boolean; value: any }) => {
      try {
        if (done) {
          loading.value = false;
          console.info('Flow successfully completed');
        } else {
          await processMessage(value);
        }
      } catch (error) {
        loading.value = false;
        isApiComplete.value = true;
        console.error('Processing failed', error);
    
        MsgErr(t('common.flowFailed'));
        stopStreaming();
      }
    
      return reader.read().then(handleWrite);
    };
  2. Optimization for Streaming Start:

    • Instead of starting the streaming immediately upon receiving each chunk, consider checking whether the entire response has been received based on specific indicators like "EOS" markers. Delay starting the interval until such an indicator is encountered.
startStreamingOutput() {
  if(isPaused.value || streamTimer) {
    return;
  }
  isStreaming.value = true;

  streamTimer = setInterval(async () => {
    if(!isPaused.value && currentDisplayIndex.value < fullContent.value.length){
      const step = Math.min(3, fullContent.value.length - currentDisplayIndex.value);
      currentDisplayIndex.value += step;

      // Update displayed content
      const currentAnswer = chatMessages.value[chatMessages.value.length - 1];
      if(currentAnswer && currentAnswer.role === 'ai'){
        currentAnswer.content = fullContent.value.substring(0, currentDisplayIndex.value);
      }

      // Check if we have reached the end gracefully
      const lastChunk = fullContentvalue.split('\n').pop();      
      if(lastChunk && /END_OF_STREAM/.test(lastChunk)){
        clearInterval(this.streamTimer);
        this.streamTimer = null;
        this.isStreaming = false;
        this.loading = false;  // Optional depending on how you want to finalize the dialog UI        
      }
    }
    }, 50); // Adjust polling interval as needed            
}

This ensures smoother transitions between short chunks and completes the display when appropriate, avoiding abrupt stops in large outputs.

By implementing these suggested improvements, your component should become more robust and easier to maintain.

Feel free to further clarify any concerns or adjust solutions as per your app's specific requirements!

Expand Down
3 changes: 2 additions & 1 deletion ui/src/workflow/nodes/ai-chat-node/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,8 @@ const openGeneratePromptDialog = (modelId: string) => {
}
}
const replace = (v: any) => {
set(props.nodeModel.properties.node_data.model_setting, 'system', v)
console.log(props.nodeModel.properties.node_data.model_setting)
set(props.nodeModel.properties.node_data, 'system', v)
}
const openReasoningParamSettingDialog = () => {
ReasoningParamSettingDialogRef.value?.open(chat_data.value.model_setting)
Expand Down
Loading