2025-09-09 06:09:59 +00:00
|
|
|
|
<template>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<el-dialog
|
|
|
|
|
|
align-center
|
|
|
|
|
|
:title="$t('views.application.generateDialog.generatePrompt')"
|
|
|
|
|
|
v-model="dialogVisible"
|
|
|
|
|
|
style="width: 600px"
|
|
|
|
|
|
append-to-body
|
2025-09-16 06:22:47 +00:00
|
|
|
|
:close-on-click-modal="true"
|
|
|
|
|
|
:close-on-press-escape="true"
|
|
|
|
|
|
:before-close="handleDialogClose"
|
2025-09-11 09:17:13 +00:00
|
|
|
|
>
|
|
|
|
|
|
<div class="generate-prompt-dialog-bg border-r-8">
|
|
|
|
|
|
<div class="scrollbar-height">
|
|
|
|
|
|
<!-- 生成内容 -->
|
|
|
|
|
|
<div class="p-16 pb-0 lighter">
|
2025-09-15 07:06:07 +00:00
|
|
|
|
<el-scrollbar ref="scrollDiv">
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="dialogScrollbar"
|
|
|
|
|
|
v-if="answer"
|
|
|
|
|
|
class="pre-wrap lighter"
|
|
|
|
|
|
style="max-height: calc(100vh - 400px)"
|
|
|
|
|
|
>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
{{ answer }}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
</div>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<p v-else-if="loading" shadow="always" style="margin: 0.5rem 0">
|
|
|
|
|
|
<el-icon class="is-loading color-primary mr-4"><Loading /></el-icon>
|
2025-09-26 09:44:26 +00:00
|
|
|
|
{{ $t('views.application.generateDialog.loading') }}
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<span class="dotting"></span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p v-else class="flex align-center">
|
|
|
|
|
|
<AppIcon iconName="app-generate-star" class="color-primary mr-4"></AppIcon>
|
2025-09-16 06:22:47 +00:00
|
|
|
|
{{ $t('views.application.generateDialog.title') }}
|
2025-09-11 09:17:13 +00:00
|
|
|
|
</p>
|
|
|
|
|
|
</el-scrollbar>
|
2025-09-28 07:50:21 +00:00
|
|
|
|
<div v-if="answer && !loading && !isStreaming && !showContinueButton" class="mt-8">
|
2025-09-18 06:55:03 +00:00
|
|
|
|
<el-button type="primary" @click="() => emit('replace', answer)">
|
|
|
|
|
|
{{ $t('views.application.generateDialog.replace') }}
|
|
|
|
|
|
</el-button>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<el-button @click="reAnswerClick" :disabled="!answer || loading" :loading="loading">
|
2025-09-16 06:22:47 +00:00
|
|
|
|
{{ $t('views.application.generateDialog.remake') }}
|
2025-09-11 09:17:13 +00:00
|
|
|
|
</el-button>
|
2025-09-09 06:09:59 +00:00
|
|
|
|
</div>
|
2025-09-18 06:55:03 +00:00
|
|
|
|
<div class="mt-8" v-else>
|
|
|
|
|
|
<el-button type="primary" v-if="showContinueButton" @click="continueStreaming" link>
|
|
|
|
|
|
{{ $t('views.application.generateDialog.continue') }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
</div>
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<!-- 文本输入框 -->
|
|
|
|
|
|
|
|
|
|
|
|
<div class="generate-prompt-operate p-16">
|
2025-09-16 06:22:47 +00:00
|
|
|
|
<div v-if="showStopButton" class="text-center mb-8">
|
|
|
|
|
|
<el-button class="border-primary video-stop-button" @click="pauseStreaming">
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<app-icon iconName="app-video-stop" class="mr-8"></app-icon>
|
2025-09-16 06:22:47 +00:00
|
|
|
|
{{ $t('views.application.generateDialog.stop') }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2025-09-18 06:55:03 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
<div class="operate-textarea">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
ref="quickInputRef"
|
|
|
|
|
|
v-model="inputValue"
|
|
|
|
|
|
:autosize="{ minRows: 1, maxRows: 10 }"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:placeholder="$t('views.application.generateDialog.placeholder')"
|
|
|
|
|
|
:maxlength="100000"
|
|
|
|
|
|
class="chat-operate-textarea"
|
2025-09-15 07:06:07 +00:00
|
|
|
|
@keydown.enter="handleSubmit($event)"
|
2025-09-11 09:17:13 +00:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="operate">
|
|
|
|
|
|
<div class="text-right">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
text
|
|
|
|
|
|
class="sent-button"
|
2025-09-16 06:22:47 +00:00
|
|
|
|
:disabled="!inputValue.trim() || loading || isStreaming"
|
2025-09-11 09:17:13 +00:00
|
|
|
|
@click="handleSubmit"
|
|
|
|
|
|
>
|
2025-09-18 06:55:03 +00:00
|
|
|
|
<img
|
|
|
|
|
|
v-show="!inputValue.trim() || loading || isStreaming"
|
|
|
|
|
|
src="@/assets/icon_send.svg"
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
/>
|
2025-09-16 06:22:47 +00:00
|
|
|
|
<SendIcon v-show="inputValue.trim() && !loading && !isStreaming" />
|
2025-09-11 09:17:13 +00:00
|
|
|
|
</el-button>
|
2025-09-09 06:09:59 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-09-18 06:55:03 +00:00
|
|
|
|
import { computed, onUnmounted, reactive, ref, nextTick, watch } from 'vue'
|
2025-09-15 03:42:55 +00:00
|
|
|
|
import { useRoute } from 'vue-router'
|
2025-09-16 06:22:47 +00:00
|
|
|
|
import { MsgConfirm } from '@/utils/message'
|
|
|
|
|
|
import { t } from '@/locales'
|
2025-09-15 03:42:55 +00:00
|
|
|
|
import systemGeneratePromptAPI from '@/api/system-resource-management/application'
|
2025-09-09 06:09:59 +00:00
|
|
|
|
import generatePromptAPI from '@/api/application/application'
|
2025-09-11 09:17:13 +00:00
|
|
|
|
import useStore from '@/stores'
|
2025-09-09 06:09:59 +00:00
|
|
|
|
const emit = defineEmits(['replace'])
|
|
|
|
|
|
const { user } = useStore()
|
2025-09-15 03:42:55 +00:00
|
|
|
|
const route = useRoute()
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
|
|
|
|
|
const chatMessages = ref<Array<any>>([])
|
|
|
|
|
|
|
2025-09-15 03:42:55 +00:00
|
|
|
|
const apiType = computed(() => {
|
|
|
|
|
|
if (route.path.includes('resource-management')) {
|
|
|
|
|
|
return 'systemManage'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return 'workspace'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-09-09 06:09:59 +00:00
|
|
|
|
// 原始输入
|
|
|
|
|
|
const originalUserInput = ref<string>('')
|
|
|
|
|
|
const modelID = ref('')
|
2025-09-12 09:06:38 +00:00
|
|
|
|
const applicationID = ref('')
|
2025-09-09 06:09:59 +00:00
|
|
|
|
const dialogVisible = ref(false)
|
|
|
|
|
|
const inputValue = ref<string>('')
|
|
|
|
|
|
const loading = ref<boolean>(false)
|
|
|
|
|
|
|
|
|
|
|
|
const promptTemplates = {
|
|
|
|
|
|
INIT_TEMPLATE: `
|
|
|
|
|
|
请根据用户描述生成一个完整的AI角色人设模板:
|
|
|
|
|
|
|
2025-09-28 07:50:21 +00:00
|
|
|
|
应用名称:{application_name}
|
|
|
|
|
|
应用描述:{detail}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
用户需求:{userInput}
|
|
|
|
|
|
|
2025-09-28 07:50:21 +00:00
|
|
|
|
重要说明:
|
|
|
|
|
|
1. 角色设定必须服务于"{application_name}"应用的核心功能
|
|
|
|
|
|
2. 允许用户对角色设定的具体内容进行调整和优化
|
|
|
|
|
|
3. 如果用户要求修改某个技能或部分,在保持应用主题的前提下进行相应调整
|
|
|
|
|
|
|
2025-09-09 06:09:59 +00:00
|
|
|
|
请按以下格式生成:
|
|
|
|
|
|
|
2025-09-19 02:51:59 +00:00
|
|
|
|
必须严格遵循以下规则:
|
|
|
|
|
|
1. **严格禁止输出解释、前言、额外说明**,只输出最终结果。
|
|
|
|
|
|
2. **严格使用以下格式**,不能缺少标题、不能多出其他段落。
|
2025-09-28 07:50:21 +00:00
|
|
|
|
3. **如果用户要求修改角色设定的某个部分,在保持应用核心功能的前提下进行调整**。
|
2025-09-29 06:04:29 +00:00
|
|
|
|
4. **如果用户需求与角色设定生成完全无关(如闲聊、其他话题),则主要依据应用信息生成标准角色设定,但不完全忽略用户输入,可从中提取有价值的辅助信息(如领域背景、语气风格等)作为次要参考**。
|
2025-09-19 02:51:59 +00:00
|
|
|
|
|
2025-09-15 07:06:07 +00:00
|
|
|
|
# 角色:
|
2025-09-28 01:39:55 +00:00
|
|
|
|
角色概述和主要职责的一句话描述
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
|
|
|
|
|
## 目标:
|
|
|
|
|
|
角色的工作目标,如果有多目标可以分点列出,但建议更聚焦1-2个目标
|
|
|
|
|
|
|
2025-09-28 01:39:55 +00:00
|
|
|
|
## 核心技能:
|
|
|
|
|
|
### 技能 1: [技能名称,如作品推荐/信息查询/专业分析等]
|
|
|
|
|
|
1. [执行步骤1 - 描述该技能的第一个具体操作步骤,包括条件判断和处理方式]
|
|
|
|
|
|
2. [执行步骤2 - 描述该技能的第二个具体操作步骤,包括如何获取或处理信息]
|
|
|
|
|
|
3. [执行步骤3 - 描述该技能的最终输出步骤,说明如何呈现结果]
|
|
|
|
|
|
|
|
|
|
|
|
===回复示例===
|
|
|
|
|
|
- 📋 [标识符]: <具体内容格式说明>
|
|
|
|
|
|
- 🎯 [标识符]: <具体内容格式说明>
|
|
|
|
|
|
- 💡 [标识符]: <具体内容格式说明>
|
|
|
|
|
|
===示例结束===
|
|
|
|
|
|
|
|
|
|
|
|
### 技能 2: [技能名称]
|
|
|
|
|
|
1. [执行步骤1 - 描述触发条件和初始处理方式]
|
|
|
|
|
|
2. [执行步骤2 - 描述信息获取和深化处理的具体方法]
|
|
|
|
|
|
3. [执行步骤3 - 描述最终输出的具体要求和格式]
|
|
|
|
|
|
|
|
|
|
|
|
### 技能 3: [技能名称]
|
|
|
|
|
|
- [核心能力描述 - 说明该技能的主要作用和知识基础]
|
|
|
|
|
|
- [应用方法 - 描述如何运用该技能为用户提供服务,包括具体的实施方式]
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
|
|
|
|
|
## 工作流:
|
|
|
|
|
|
1. 描述角色工作流程的第一步
|
|
|
|
|
|
2. 描述角色工作流程的第二步
|
|
|
|
|
|
3. 描述角色工作流程的第三步
|
|
|
|
|
|
|
|
|
|
|
|
## 输出格式:
|
|
|
|
|
|
如果对角色的输出格式有特定要求,可以在这里强调并举例说明想要的输出格式
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 限制:
|
2025-09-15 07:06:07 +00:00
|
|
|
|
1. **严格限制回答范围**:仅回答与角色设定相关的问题。
|
|
|
|
|
|
- 如果用户提问与角色无关,必须使用以下固定格式回复:
|
2025-09-28 07:50:21 +00:00
|
|
|
|
“对不起,我只能回答与[角色设定]相关的问题,您的问题不在服务范围内。”
|
2025-09-15 07:06:07 +00:00
|
|
|
|
- 不得提供任何与角色设定无关的回答。
|
2025-09-12 09:06:38 +00:00
|
|
|
|
2. 描述角色在互动过程中需要遵循的限制条件2
|
|
|
|
|
|
3. 描述角色在互动过程中需要遵循的限制条件3
|
2025-09-19 02:51:59 +00:00
|
|
|
|
|
|
|
|
|
|
输出时不得包含任何解释或附加说明,只能返回符合以上格式的内容。
|
2025-09-11 09:17:13 +00:00
|
|
|
|
`,
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 06:22:47 +00:00
|
|
|
|
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(() => {
|
2025-09-28 07:50:21 +00:00
|
|
|
|
if (isApiComplete.value && !isPaused.value) {
|
2025-09-28 01:39:55 +00:00
|
|
|
|
// 更新显示内容
|
|
|
|
|
|
const currentAnswer = chatMessages.value[chatMessages.value.length - 1]
|
|
|
|
|
|
if (currentAnswer && currentAnswer.role === 'ai') {
|
|
|
|
|
|
currentAnswer.content = fullContent .value
|
|
|
|
|
|
}
|
|
|
|
|
|
stopStreaming()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-16 06:22:47 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-09-18 06:55:03 +00:00
|
|
|
|
} else if (loading.value === false && currentDisplayIndex.value >= fullContent.value.length) {
|
|
|
|
|
|
stopStreaming()
|
|
|
|
|
|
}
|
2025-09-26 09:44:26 +00:00
|
|
|
|
}, 50) as any
|
2025-09-16 06:22:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 停止流式输出
|
|
|
|
|
|
const stopStreaming = () => {
|
|
|
|
|
|
if (streamTimer) {
|
|
|
|
|
|
clearInterval(streamTimer)
|
|
|
|
|
|
streamTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
isStreaming.value = false
|
2025-09-18 06:55:03 +00:00
|
|
|
|
isPaused.value = false
|
2025-09-16 06:22:47 +00:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 06:09:59 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取一个递归函数,处理流式数据
|
|
|
|
|
|
* @param chat 每一条对话记录
|
|
|
|
|
|
* @param reader 流数据
|
|
|
|
|
|
* @param stream 是否是流式数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
const getWrite = (reader: any) => {
|
|
|
|
|
|
let tempResult = ''
|
2025-09-18 06:55:03 +00:00
|
|
|
|
const middleAnswer = reactive({ content: '', role: 'ai' })
|
|
|
|
|
|
chatMessages.value.push(middleAnswer)
|
2025-09-16 06:22:47 +00:00
|
|
|
|
|
|
|
|
|
|
// 初始化状态并
|
|
|
|
|
|
fullContent.value = ''
|
|
|
|
|
|
currentDisplayIndex.value = 0
|
|
|
|
|
|
isOutputComplete.value = false
|
|
|
|
|
|
|
|
|
|
|
|
let streamingStarted = false
|
|
|
|
|
|
|
2025-09-09 06:09:59 +00:00
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param done 是否结束
|
|
|
|
|
|
* @param value 值
|
|
|
|
|
|
*/
|
|
|
|
|
|
const write_stream = ({ done, value }: { done: boolean; value: any }) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (done) {
|
2025-09-18 06:55:03 +00:00
|
|
|
|
// 流数据接收完成,但定时器继续运行直到显示完所有内容
|
2025-09-09 06:09:59 +00:00
|
|
|
|
loading.value = false
|
2025-09-28 01:39:55 +00:00
|
|
|
|
isApiComplete.value = true
|
2025-09-09 06:09:59 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const decoder = new TextDecoder('utf-8')
|
|
|
|
|
|
let str = decoder.decode(value, { stream: true })
|
|
|
|
|
|
// 这里解释一下 start 因为数据流返回流并不是按照后端chunk返回 我们希望得到的chunk是data:{xxx}\n\n 但是它获取到的可能是 data:{ -> xxx}\n\n 总而言之就是 fetch不能保证每个chunk都说以data:开始 \n\n结束
|
|
|
|
|
|
tempResult += str
|
|
|
|
|
|
const split = tempResult.match(/data:.*}\n\n/g)
|
|
|
|
|
|
if (split) {
|
|
|
|
|
|
str = split.join('')
|
|
|
|
|
|
tempResult = tempResult.replace(str, '')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return reader.read().then(write_stream)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 这里解释一下 end
|
|
|
|
|
|
if (str && str.startsWith('data:')) {
|
|
|
|
|
|
if (split) {
|
|
|
|
|
|
for (const index in split) {
|
|
|
|
|
|
const chunk = JSON?.parse(split[index].replace('data:', ''))
|
2025-09-29 10:01:56 +00:00
|
|
|
|
if (chunk.error) {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
stopStreaming()
|
|
|
|
|
|
middleAnswer.content = chunk.error
|
|
|
|
|
|
return Promise.reject(new Error(chunk.error))
|
|
|
|
|
|
}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
if (!chunk.is_end) {
|
2025-09-16 06:22:47 +00:00
|
|
|
|
// 实时将新接收的内容添加到完整内容中
|
|
|
|
|
|
fullContent.value += chunk.content
|
|
|
|
|
|
if (!streamingStarted) {
|
|
|
|
|
|
streamingStarted = true
|
|
|
|
|
|
startStreamingOutput()
|
|
|
|
|
|
}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (chunk.is_end) {
|
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
loading.value = false
|
2025-09-16 06:22:47 +00:00
|
|
|
|
stopStreaming()
|
2025-09-09 06:09:59 +00:00
|
|
|
|
return Promise.reject(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
return reader.read().then(write_stream)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return write_stream
|
|
|
|
|
|
}
|
2025-09-16 06:22:47 +00:00
|
|
|
|
const isApiComplete = ref<boolean>(false)
|
2025-09-09 06:09:59 +00:00
|
|
|
|
const answer = computed(() => {
|
|
|
|
|
|
const result = chatMessages.value[chatMessages.value.length - 1]
|
|
|
|
|
|
|
|
|
|
|
|
if (result && result.role == 'ai') {
|
|
|
|
|
|
return result.content
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-09-16 06:22:47 +00:00
|
|
|
|
// 按钮状态计算
|
|
|
|
|
|
const showContinueButton = computed(() => {
|
2025-09-18 06:55:03 +00:00
|
|
|
|
return (
|
|
|
|
|
|
!isStreaming.value && isPaused.value && currentDisplayIndex.value < fullContent.value.length
|
|
|
|
|
|
)
|
2025-09-16 06:22:47 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
2025-09-09 06:09:59 +00:00
|
|
|
|
function generatePrompt(inputValue: any) {
|
2025-09-28 01:39:55 +00:00
|
|
|
|
isApiComplete.value=false
|
2025-09-09 06:09:59 +00:00
|
|
|
|
loading.value = true
|
|
|
|
|
|
const workspaceId = user.getWorkspaceId() || 'default'
|
|
|
|
|
|
chatMessages.value.push({ content: inputValue, role: 'user' })
|
|
|
|
|
|
const requestData = {
|
|
|
|
|
|
messages: chatMessages.value,
|
2025-09-11 09:17:13 +00:00
|
|
|
|
prompt: promptTemplates.INIT_TEMPLATE,
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
2025-09-15 03:42:55 +00:00
|
|
|
|
if (apiType.value === 'workspace') {
|
2025-09-15 07:06:07 +00:00
|
|
|
|
generatePromptAPI
|
|
|
|
|
|
.generate_prompt(workspaceId, modelID.value, applicationID.value, requestData)
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (dialogScrollbar.value) {
|
|
|
|
|
|
// 将滚动条滚动到最下面
|
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
|
|
reader.read().then(getWrite(reader))
|
|
|
|
|
|
})
|
2025-09-15 03:42:55 +00:00
|
|
|
|
} else if (apiType.value === 'systemManage') {
|
2025-09-15 07:06:07 +00:00
|
|
|
|
systemGeneratePromptAPI
|
|
|
|
|
|
.generate_prompt(applicationID.value, modelID.value, requestData)
|
|
|
|
|
|
.then((response) => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (dialogScrollbar.value) {
|
|
|
|
|
|
// 将滚动条滚动到最下面
|
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const reader = response.body.getReader()
|
|
|
|
|
|
reader.read().then(getWrite(reader))
|
|
|
|
|
|
})
|
2025-09-15 03:42:55 +00:00
|
|
|
|
}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重新生成点击
|
|
|
|
|
|
const reAnswerClick = () => {
|
|
|
|
|
|
if (originalUserInput.value) {
|
2025-09-29 06:04:29 +00:00
|
|
|
|
generatePrompt(`上一次回答不满意。请针对原始问题"${originalUserInput.value}"并结合对话记录,严格按照格式规范重新生成。`)
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 07:06:07 +00:00
|
|
|
|
const quickInputRef = ref()
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = (event?: any) => {
|
|
|
|
|
|
if (!event?.ctrlKey && !event?.shiftKey && !event?.altKey && !event?.metaKey) {
|
|
|
|
|
|
// 如果没有按下组合键,则会阻止默认事件
|
|
|
|
|
|
event?.preventDefault()
|
2025-09-28 07:50:21 +00:00
|
|
|
|
if (!inputValue.value.trim() || loading.value || isStreaming.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-15 07:06:07 +00:00
|
|
|
|
if (!originalUserInput.value) {
|
|
|
|
|
|
originalUserInput.value = inputValue.value
|
|
|
|
|
|
}
|
2025-09-28 07:50:21 +00:00
|
|
|
|
if (isPaused.value || isStreaming.value) {
|
2025-09-28 01:39:55 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-09-18 06:55:03 +00:00
|
|
|
|
if (inputValue.value) {
|
2025-09-16 06:22:47 +00:00
|
|
|
|
generatePrompt(inputValue.value)
|
2025-09-18 06:55:03 +00:00
|
|
|
|
inputValue.value = ''
|
2025-09-16 06:22:47 +00:00
|
|
|
|
}
|
2025-09-15 07:06:07 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果同时按下ctrl/shift/cmd/opt +enter,则会换行
|
|
|
|
|
|
insertNewlineAtCursor(event)
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
2025-09-15 07:06:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
const insertNewlineAtCursor = (event?: any) => {
|
|
|
|
|
|
const textarea = quickInputRef.value.$el.querySelector(
|
|
|
|
|
|
'.el-textarea__inner',
|
|
|
|
|
|
) as HTMLTextAreaElement
|
|
|
|
|
|
const startPos = textarea.selectionStart
|
|
|
|
|
|
const endPos = textarea.selectionEnd
|
|
|
|
|
|
// 阻止默认行为(避免额外的换行符)
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
// 在光标处插入换行符
|
|
|
|
|
|
inputValue.value = inputValue.value.slice(0, startPos) + '\n' + inputValue.value.slice(endPos)
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
textarea.setSelectionRange(startPos + 1, startPos + 1) // 光标定位到换行后位置
|
|
|
|
|
|
})
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 09:06:38 +00:00
|
|
|
|
const open = (modelId: string, applicationId: string) => {
|
2025-09-09 06:09:59 +00:00
|
|
|
|
modelID.value = modelId
|
2025-09-12 09:06:38 +00:00
|
|
|
|
applicationID.value = applicationId
|
2025-09-09 06:09:59 +00:00
|
|
|
|
dialogVisible.value = true
|
|
|
|
|
|
originalUserInput.value = ''
|
|
|
|
|
|
chatMessages.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 07:06:07 +00:00
|
|
|
|
const scrollDiv = ref()
|
|
|
|
|
|
const dialogScrollbar = ref()
|
|
|
|
|
|
|
|
|
|
|
|
const getMaxHeight = () => {
|
|
|
|
|
|
return dialogScrollbar.value!.scrollHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理跟随滚动条
|
|
|
|
|
|
*/
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
if (scrollDiv.value) {
|
|
|
|
|
|
// 内部高度小于外部高度 就需要出滚动条
|
|
|
|
|
|
if (scrollDiv.value.wrapRef.offsetHeight < dialogScrollbar.value?.scrollHeight) {
|
|
|
|
|
|
// 如果当前滚动条距离最下面的距离在 规定距离 滚动条就跟随
|
|
|
|
|
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 06:22:47 +00:00
|
|
|
|
const handleDialogClose = (done: () => void) => {
|
2025-09-26 09:44:26 +00:00
|
|
|
|
if (answer.value) {
|
|
|
|
|
|
// 弹出 消息
|
|
|
|
|
|
MsgConfirm(t('common.tip'), t('views.application.generateDialog.exit'), {
|
|
|
|
|
|
confirmButtonText: t('common.confirm'),
|
|
|
|
|
|
cancelButtonText: t('common.cancel'),
|
|
|
|
|
|
distinguishCancelAndClose: true,
|
2025-09-18 06:55:03 +00:00
|
|
|
|
})
|
2025-09-26 09:44:26 +00:00
|
|
|
|
.then(() => {
|
|
|
|
|
|
// 点击确认,清除状态
|
|
|
|
|
|
stopStreaming()
|
|
|
|
|
|
chatMessages.value = []
|
|
|
|
|
|
fullContent.value = ''
|
|
|
|
|
|
currentDisplayIndex.value = 0
|
|
|
|
|
|
isOutputComplete.value = false
|
|
|
|
|
|
done() // 真正关闭
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
// 点击取消
|
|
|
|
|
|
})
|
2025-09-28 07:50:21 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
done()
|
2025-09-26 09:44:26 +00:00
|
|
|
|
}
|
2025-09-16 06:22:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理定时器
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopStreaming()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-09-15 07:06:07 +00:00
|
|
|
|
watch(
|
|
|
|
|
|
answer,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
handleScroll()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true, immediate: true },
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-09 06:09:59 +00:00
|
|
|
|
defineExpose({
|
2025-09-11 09:17:13 +00:00
|
|
|
|
open,
|
2025-09-09 06:09:59 +00:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.generate-prompt-dialog-bg {
|
2025-09-09 06:09:59 +00:00
|
|
|
|
background: var(--dialog-bg-gradient-color);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.generate-prompt-operate {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.operate-textarea) {
|
|
|
|
|
|
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
|
border-radius: var(--app-border-radius-large);
|
|
|
|
|
|
border: 1px solid #ffffff;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
&:has(.el-textarea__inner:focus) {
|
|
|
|
|
|
border: 1px solid var(--el-color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-textarea__inner {
|
|
|
|
|
|
border-radius: var(--app-border-radius-large) !important;
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
resize: none;
|
|
|
|
|
|
padding: 13px 16px;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
box-sizing: border-box;
|
2025-09-11 09:17:13 +00:00
|
|
|
|
min-height: 47px !important;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.operate {
|
|
|
|
|
|
padding: 6px 10px;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.el-icon {
|
|
|
|
|
|
font-size: 20px;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.sent-button {
|
|
|
|
|
|
max-height: none;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
|
|
|
|
|
.el-icon {
|
2025-09-11 09:17:13 +00:00
|
|
|
|
font-size: 24px;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.el-loading-spinner {
|
|
|
|
|
|
margin-top: -15px;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.circular {
|
|
|
|
|
|
width: 31px;
|
|
|
|
|
|
height: 31px;
|
|
|
|
|
|
}
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-11 09:17:13 +00:00
|
|
|
|
.video-stop-button {
|
|
|
|
|
|
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
2025-09-09 06:09:59 +00:00
|
|
|
|
|
2025-09-11 09:17:13 +00:00
|
|
|
|
&:hover {
|
|
|
|
|
|
background: #ffffff;
|
2025-09-09 06:09:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-11 09:17:13 +00:00
|
|
|
|
</style>
|