feat: 支持用户输入变量
--story=1016155 --user=刘瑞斌 【应用编排】-支持设置用户输入变量 https://www.tapd.cn/57709429/s/1576480
This commit is contained in:
parent
ba023d20f0
commit
d48b51c3e0
@ -166,10 +166,10 @@ class Flow:
|
|||||||
|
|
||||||
class WorkflowManage:
|
class WorkflowManage:
|
||||||
def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler,
|
def __init__(self, flow: Flow, params, work_flow_post_handler: WorkFlowPostHandler,
|
||||||
base_to_response: BaseToResponse = SystemToResponse()):
|
base_to_response: BaseToResponse = SystemToResponse(), form_data = {}):
|
||||||
self.params = params
|
self.params = params
|
||||||
self.flow = flow
|
self.flow = flow
|
||||||
self.context = {}
|
self.context = form_data
|
||||||
self.node_context = []
|
self.node_context = []
|
||||||
self.work_flow_post_handler = work_flow_post_handler
|
self.work_flow_post_handler = work_flow_post_handler
|
||||||
self.current_node = None
|
self.current_node = None
|
||||||
|
|||||||
@ -694,6 +694,7 @@ class ApplicationSerializer(serializers.Serializer):
|
|||||||
'tts_model_id': application.tts_model_id,
|
'tts_model_id': application.tts_model_id,
|
||||||
'stt_model_enable': application.stt_model_enable,
|
'stt_model_enable': application.stt_model_enable,
|
||||||
'tts_model_enable': application.tts_model_enable,
|
'tts_model_enable': application.tts_model_enable,
|
||||||
|
'work_flow': application.work_flow,
|
||||||
'show_source': application_access_token.show_source})
|
'show_source': application_access_token.show_source})
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -855,10 +856,15 @@ class ApplicationSerializer(serializers.Serializer):
|
|||||||
nodes = instance.get('work_flow')['nodes']
|
nodes = instance.get('work_flow')['nodes']
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node['id'] == 'base-node':
|
if node['id'] == 'base-node':
|
||||||
instance['stt_model_id'] = node['properties']['node_data']['stt_model_id']
|
node_data = node['properties']['node_data']
|
||||||
instance['tts_model_id'] = node['properties']['node_data']['tts_model_id']
|
if 'stt_model_id' in node_data:
|
||||||
instance['stt_model_enable'] = node['properties']['node_data']['stt_model_enable']
|
instance['stt_model_id'] = node_data['stt_model_id']
|
||||||
instance['tts_model_enable'] = node['properties']['node_data']['tts_model_enable']
|
if 'tts_model_id' in node_data:
|
||||||
|
instance['tts_model_id'] = node_data['tts_model_id']
|
||||||
|
if 'stt_model_enable' in node_data:
|
||||||
|
instance['stt_model_enable'] = node_data['stt_model_enable']
|
||||||
|
if 'tts_model_enable' in node_data:
|
||||||
|
instance['tts_model_enable'] = node_data['tts_model_enable']
|
||||||
break
|
break
|
||||||
|
|
||||||
def speech_to_text(self, file, with_valid=True):
|
def speech_to_text(self, file, with_valid=True):
|
||||||
|
|||||||
@ -208,6 +208,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||||||
application_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid("应用id"))
|
application_id = serializers.UUIDField(required=False, allow_null=True, error_messages=ErrMessage.uuid("应用id"))
|
||||||
client_id = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端id"))
|
client_id = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端id"))
|
||||||
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端类型"))
|
client_type = serializers.CharField(required=True, error_messages=ErrMessage.char("客户端类型"))
|
||||||
|
form_data = serializers.DictField(required=False, error_messages=ErrMessage.char("全局变量"))
|
||||||
|
|
||||||
def is_valid_application_workflow(self, *, raise_exception=False):
|
def is_valid_application_workflow(self, *, raise_exception=False):
|
||||||
self.is_valid_intraday_access_num()
|
self.is_valid_intraday_access_num()
|
||||||
@ -284,6 +285,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||||||
stream = self.data.get('stream')
|
stream = self.data.get('stream')
|
||||||
client_id = self.data.get('client_id')
|
client_id = self.data.get('client_id')
|
||||||
client_type = self.data.get('client_type')
|
client_type = self.data.get('client_type')
|
||||||
|
form_data = self.data.get('form_data')
|
||||||
user_id = chat_info.application.user_id
|
user_id = chat_info.application.user_id
|
||||||
work_flow_manage = WorkflowManage(Flow.new_instance(chat_info.work_flow_version.work_flow),
|
work_flow_manage = WorkflowManage(Flow.new_instance(chat_info.work_flow_version.work_flow),
|
||||||
{'history_chat_record': chat_info.chat_record_list, 'question': message,
|
{'history_chat_record': chat_info.chat_record_list, 'question': message,
|
||||||
@ -291,7 +293,7 @@ class ChatMessageSerializer(serializers.Serializer):
|
|||||||
'stream': stream,
|
'stream': stream,
|
||||||
're_chat': re_chat,
|
're_chat': re_chat,
|
||||||
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
|
'user_id': user_id}, WorkFlowPostHandler(chat_info, client_id, client_type),
|
||||||
base_to_response)
|
base_to_response, form_data)
|
||||||
r = work_flow_manage.run()
|
r = work_flow_manage.run()
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,7 @@ class ChatView(APIView):
|
|||||||
'application_id': (request.auth.keywords.get(
|
'application_id': (request.auth.keywords.get(
|
||||||
'application_id') if request.auth.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value else None),
|
'application_id') if request.auth.client_type == AuthenticationType.APPLICATION_ACCESS_TOKEN.value else None),
|
||||||
'client_id': request.auth.client_id,
|
'client_id': request.auth.client_id,
|
||||||
|
'form_data': (request.data.get('form_data') if 'form_data' in request.data else []),
|
||||||
'client_type': request.auth.client_type}).chat()
|
'client_type': request.auth.client_type}).chat()
|
||||||
|
|
||||||
@action(methods=['GET'], detail=False)
|
@action(methods=['GET'], detail=False)
|
||||||
|
|||||||
@ -17,7 +17,9 @@
|
|||||||
class="problem-button ellipsis-2 mb-8"
|
class="problem-button ellipsis-2 mb-8"
|
||||||
:class="log ? 'disabled' : 'cursor'"
|
:class="log ? 'disabled' : 'cursor'"
|
||||||
>
|
>
|
||||||
<el-icon><EditPen /></el-icon>
|
<el-icon>
|
||||||
|
<EditPen />
|
||||||
|
</el-icon>
|
||||||
{{ item.str }}
|
{{ item.str }}
|
||||||
</div>
|
</div>
|
||||||
<MdPreview
|
<MdPreview
|
||||||
@ -33,6 +35,24 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="inputFieldList.length > 0">
|
||||||
|
<div class="avatar">
|
||||||
|
<img v-if="data.avatar" :src="data.avatar" height="30px" />
|
||||||
|
<LogoIcon v-else height="30px" />
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<el-card shadow="always" class="dialog-card">
|
||||||
|
<DynamicsForm
|
||||||
|
v-model="form_data"
|
||||||
|
:model="form_data"
|
||||||
|
label-position="left"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
:render_data="inputFieldList"
|
||||||
|
ref="dynamicsFormRef"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template v-for="(item, index) in chatList" :key="index">
|
<template v-for="(item, index) in chatList" :key="index">
|
||||||
<!-- 问题 -->
|
<!-- 问题 -->
|
||||||
<div class="item-content mb-16 lighter">
|
<div class="item-content mb-16 lighter">
|
||||||
@ -98,10 +118,12 @@
|
|||||||
v-if="item.is_stop && !item.write_ed"
|
v-if="item.is_stop && !item.write_ed"
|
||||||
@click="startChat(item)"
|
@click="startChat(item)"
|
||||||
link
|
link
|
||||||
>继续</el-button
|
>继续
|
||||||
|
</el-button
|
||||||
>
|
>
|
||||||
<el-button type="primary" v-else-if="!item.write_ed" @click="stopChat(item)" link
|
<el-button type="primary" v-else-if="!item.write_ed" @click="stopChat(item)" link
|
||||||
>停止回答</el-button
|
>停止回答
|
||||||
|
</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +138,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="float: right;" v-if="props.data.tts_model_enable">
|
<div style="float: right;" v-if="props.data.tts_model_enable">
|
||||||
<el-button :disabled="!item.write_ed" @click="playAnswerText(item.answer_text)">
|
<el-button :disabled="!item.write_ed" @click="playAnswerText(item.answer_text)">
|
||||||
<el-icon><VideoPlay /></el-icon>
|
<el-icon>
|
||||||
|
<VideoPlay />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -141,13 +165,17 @@
|
|||||||
v-if="mediaRecorderStatus"
|
v-if="mediaRecorderStatus"
|
||||||
@click="startRecording"
|
@click="startRecording"
|
||||||
>
|
>
|
||||||
<el-icon><Microphone /></el-icon>
|
<el-icon>
|
||||||
|
<Microphone />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-else
|
v-else
|
||||||
@click="stopRecording"
|
@click="stopRecording"
|
||||||
>
|
>
|
||||||
<el-icon><VideoPause /></el-icon>
|
<el-icon>
|
||||||
|
<VideoPause />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="operate">
|
<div class="operate">
|
||||||
@ -189,6 +217,9 @@ import { debounce } from 'lodash'
|
|||||||
import Recorder from 'recorder-core'
|
import Recorder from 'recorder-core'
|
||||||
import 'recorder-core/src/engine/mp3'
|
import 'recorder-core/src/engine/mp3'
|
||||||
import 'recorder-core/src/engine/mp3-engine'
|
import 'recorder-core/src/engine/mp3-engine'
|
||||||
|
import { MsgWarning } from '@/utils/message'
|
||||||
|
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||||
|
import type { FormField } from '@/components/dynamics-form/type'
|
||||||
|
|
||||||
defineOptions({ name: 'AiChat' })
|
defineOptions({ name: 'AiChat' })
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -199,7 +230,8 @@ const {
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
appId: String, // 仅分享链接有
|
appId: String, // 仅分享链接有
|
||||||
log: Boolean,
|
log: Boolean,
|
||||||
@ -234,6 +266,8 @@ const loading = ref(false)
|
|||||||
const inputValue = ref('')
|
const inputValue = ref('')
|
||||||
const chartOpenId = ref('')
|
const chartOpenId = ref('')
|
||||||
const chatList = ref<any[]>([])
|
const chatList = ref<any[]>([])
|
||||||
|
const inputFieldList = ref<FormField[]>([])
|
||||||
|
const form_data = ref<any>({})
|
||||||
|
|
||||||
const isDisabledChart = computed(
|
const isDisabledChart = computed(
|
||||||
() => !(inputValue.value.trim() && (props.appId || props.data?.name))
|
() => !(inputValue.value.trim() && (props.appId || props.data?.name))
|
||||||
@ -248,15 +282,15 @@ const prologueList = computed(() => {
|
|||||||
.reduce((pre_array: Array<any>, current: string, index: number) => {
|
.reduce((pre_array: Array<any>, current: string, index: number) => {
|
||||||
const currentObj = isMdArray(current)
|
const currentObj = isMdArray(current)
|
||||||
? {
|
? {
|
||||||
type: 'question',
|
type: 'question',
|
||||||
str: current.replace(/^-\s+/, ''),
|
str: current.replace(/^-\s+/, ''),
|
||||||
index: index
|
index: index
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: 'md',
|
type: 'md',
|
||||||
str: current,
|
str: current,
|
||||||
index: index
|
index: index
|
||||||
}
|
}
|
||||||
if (pre_array.length > 0) {
|
if (pre_array.length > 0) {
|
||||||
const pre = pre_array[pre_array.length - 1]
|
const pre = pre_array[pre_array.length - 1]
|
||||||
if (!isMdArray(current) && pre.type == 'md') {
|
if (!isMdArray(current) && pre.type == 'md') {
|
||||||
@ -286,10 +320,48 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function handleInputFieldList() {
|
||||||
|
props.data.work_flow?.nodes
|
||||||
|
.filter((v: any) => v.id === 'base-node')
|
||||||
|
.map((v: any) => {
|
||||||
|
inputFieldList.value = v.properties.input_field_list.map((v: any) => {
|
||||||
|
switch (v.type) {
|
||||||
|
case 'input':
|
||||||
|
return { field: v.variable, input_type: 'TextInput', label: v.name, required: v.is_required }
|
||||||
|
case 'select':
|
||||||
|
return {
|
||||||
|
field: v.variable,
|
||||||
|
input_type: 'SingleSelect',
|
||||||
|
label: v.name,
|
||||||
|
required: v.is_required,
|
||||||
|
option_list: v.optionList.map((o: any) => {
|
||||||
|
return { key: o, value: o }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case 'date':
|
||||||
|
return {
|
||||||
|
field: v.variable,
|
||||||
|
input_type: 'DatePicker',
|
||||||
|
label: v.name,
|
||||||
|
required: v.is_required,
|
||||||
|
attrs: {
|
||||||
|
'format': 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
'type': 'datetime'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
chartOpenId.value = ''
|
chartOpenId.value = ''
|
||||||
|
handleInputFieldList()
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
@ -327,6 +399,13 @@ const handleDebounceClick = debounce((val) => {
|
|||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
function sendChatHandle(event: any) {
|
function sendChatHandle(event: any) {
|
||||||
|
// 检查inputFieldList是否有未填写的字段
|
||||||
|
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||||
|
if (inputFieldList.value[i].required && !form_data.value[inputFieldList.value[i].field]) {
|
||||||
|
MsgWarning('请填写所有必填字段')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!event.ctrlKey) {
|
if (!event.ctrlKey) {
|
||||||
// 如果没有按下组合键ctrl,则会阻止默认事件
|
// 如果没有按下组合键ctrl,则会阻止默认事件
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -340,12 +419,14 @@ function sendChatHandle(event: any) {
|
|||||||
inputValue.value += '\n'
|
inputValue.value += '\n'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopChat = (chat: chatType) => {
|
const stopChat = (chat: chatType) => {
|
||||||
ChatManagement.stop(chat.id)
|
ChatManagement.stop(chat.id)
|
||||||
}
|
}
|
||||||
const startChat = (chat: chatType) => {
|
const startChat = (chat: chatType) => {
|
||||||
ChatManagement.write(chat.id)
|
ChatManagement.write(chat.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对话
|
* 对话
|
||||||
*/
|
*/
|
||||||
@ -398,6 +479,7 @@ function getChartOpenId(chat?: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一个递归函数,处理流式数据
|
* 获取一个递归函数,处理流式数据
|
||||||
* @param chat 每一条对话记录
|
* @param chat 每一条对话记录
|
||||||
@ -483,6 +565,7 @@ const errorWrite = (chat: any, message?: string) => {
|
|||||||
ChatManagement.updateStatus(chat.id, 500)
|
ChatManagement.updateStatus(chat.id, 500)
|
||||||
ChatManagement.close(chat.id)
|
ChatManagement.close(chat.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
|
function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
@ -513,7 +596,8 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean) {
|
|||||||
} else {
|
} else {
|
||||||
const obj = {
|
const obj = {
|
||||||
message: chat.problem_text,
|
message: chat.problem_text,
|
||||||
re_chat: re_chat || false
|
re_chat: re_chat || false,
|
||||||
|
form_data: form_data.value
|
||||||
}
|
}
|
||||||
// 对话
|
// 对话
|
||||||
applicationApi
|
applicationApi
|
||||||
@ -618,8 +702,8 @@ const handleScroll = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 定义响应式引用
|
// 定义响应式引用
|
||||||
const mediaRecorder= ref<any>(null)
|
const mediaRecorder = ref<any>(null)
|
||||||
const audioPlayer= ref<HTMLAudioElement | null>(null)
|
const audioPlayer = ref<HTMLAudioElement | null>(null)
|
||||||
const mediaRecorderStatus = ref(true)
|
const mediaRecorderStatus = ref(true)
|
||||||
|
|
||||||
|
|
||||||
@ -630,11 +714,11 @@ const startRecording = async () => {
|
|||||||
mediaRecorder.value = new Recorder({
|
mediaRecorder.value = new Recorder({
|
||||||
type: 'mp3',
|
type: 'mp3',
|
||||||
bitRate: 128,
|
bitRate: 128,
|
||||||
sampleRate: 44100,
|
sampleRate: 44100
|
||||||
})
|
})
|
||||||
|
|
||||||
mediaRecorder.value.open(() => {
|
mediaRecorder.value.open(() => {
|
||||||
mediaRecorder.value.start()
|
mediaRecorder.value.start()
|
||||||
}, (err: any) => {
|
}, (err: any) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
})
|
})
|
||||||
@ -648,13 +732,13 @@ const stopRecording = () => {
|
|||||||
if (mediaRecorder.value) {
|
if (mediaRecorder.value) {
|
||||||
mediaRecorderStatus.value = true
|
mediaRecorderStatus.value = true
|
||||||
mediaRecorder.value.stop((blob: Blob, duration: number) => {
|
mediaRecorder.value.stop((blob: Blob, duration: number) => {
|
||||||
// 测试blob是否能正常播放
|
// 测试blob是否能正常播放
|
||||||
// const link = document.createElement('a')
|
// const link = document.createElement('a')
|
||||||
// link.href = window.URL.createObjectURL(blob)
|
// link.href = window.URL.createObjectURL(blob)
|
||||||
// link.download = 'abc.mp3'
|
// link.download = 'abc.mp3'
|
||||||
// link.click()
|
// link.click()
|
||||||
|
|
||||||
uploadRecording(blob) // 上传录音文件
|
uploadRecording(blob) // 上传录音文件
|
||||||
}, (err: any) => {
|
}, (err: any) => {
|
||||||
console.error('录音失败:', err)
|
console.error('录音失败:', err)
|
||||||
})
|
})
|
||||||
@ -666,12 +750,12 @@ const uploadRecording = async (audioBlob: Blob) => {
|
|||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', audioBlob, 'recording.mp3')
|
formData.append('file', audioBlob, 'recording.mp3')
|
||||||
applicationApi.postSpeechToText(props.data.id as string, formData, loading)
|
applicationApi.postSpeechToText(props.data.id as string, formData, loading)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log('上传成功:', response.data)
|
console.log('上传成功:', response.data)
|
||||||
inputValue.value = response.data
|
inputValue.value = response.data
|
||||||
// chatMessage(null, res.data)
|
// chatMessage(null, res.data)
|
||||||
})
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传失败:', error)
|
console.error('上传失败:', error)
|
||||||
@ -697,10 +781,10 @@ const playAnswerText = (text: string) => {
|
|||||||
|
|
||||||
// 检查 audioPlayer 是否已经引用了 DOM 元素
|
// 检查 audioPlayer 是否已经引用了 DOM 元素
|
||||||
if (audioPlayer.value instanceof HTMLAudioElement) {
|
if (audioPlayer.value instanceof HTMLAudioElement) {
|
||||||
audioPlayer.value.src = url;
|
audioPlayer.value.src = url
|
||||||
audioPlayer.value.play(); // 自动播放音频
|
audioPlayer.value.play() // 自动播放音频
|
||||||
} else {
|
} else {
|
||||||
console.error("audioPlayer.value is not an instance of HTMLAudioElement");
|
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -708,6 +792,10 @@ const playAnswerText = (text: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleInputFieldList()
|
||||||
|
})
|
||||||
|
|
||||||
function setScrollBottom() {
|
function setScrollBottom() {
|
||||||
// 将滚动条滚动到最下面
|
// 将滚动条滚动到最下面
|
||||||
scrollDiv.value.setScrollTop(getMaxHeight())
|
scrollDiv.value.setScrollTop(getMaxHeight())
|
||||||
@ -751,15 +839,19 @@ defineExpose({
|
|||||||
.avatar {
|
.avatar {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-left: var(--padding-left);
|
padding-left: var(--padding-left);
|
||||||
|
|
||||||
:deep(ol) {
|
:deep(ol) {
|
||||||
margin-left: 16px !important;
|
margin-left: 16px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.problem-button {
|
.problem-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
@ -772,25 +864,30 @@ defineExpose({
|
|||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--el-color-primary-light-9);
|
background: var(--el-color-primary-light-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--app-layout-bg-color);
|
background: var(--app-layout-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-icon) {
|
:deep(.el-icon) {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__operate {
|
&__operate {
|
||||||
background: #f3f7f9;
|
background: #f3f7f9;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
|
background: linear-gradient(0deg, #f3f7f9 0%, rgba(243, 247, 249, 0) 100%);
|
||||||
content: '';
|
content: '';
|
||||||
@ -800,6 +897,7 @@ defineExpose({
|
|||||||
left: 0;
|
left: 0;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operate-textarea {
|
.operate-textarea {
|
||||||
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@ -818,16 +916,21 @@ defineExpose({
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operate {
|
.operate {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
|
|
||||||
.sent-button {
|
.sent-button {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
|
|
||||||
.el-icon {
|
.el-icon {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-loading-spinner) {
|
:deep(.el-loading-spinner) {
|
||||||
margin-top: -15px;
|
margin-top: -15px;
|
||||||
|
|
||||||
.circular {
|
.circular {
|
||||||
width: 31px;
|
width: 31px;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
@ -836,11 +939,13 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-card {
|
.dialog-card {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-width {
|
.chat-width {
|
||||||
max-width: var(--app-chat-width, 860px);
|
max-width: var(--app-chat-width, 860px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
5
ui/src/components/dynamics-form/items/DatePicker.vue
Normal file
5
ui/src/components/dynamics-form/items/DatePicker.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<el-date-picker v-bind="$attrs" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style lang="scss"></style>
|
||||||
159
ui/src/workflow/nodes/base-node/component/FieldFormDialog.vue
Normal file
159
ui/src/workflow/nodes/base-node/component/FieldFormDialog.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:title="isEdit ? '编辑变量' : '添加变量'"
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
label-position="top"
|
||||||
|
ref="fieldFormRef"
|
||||||
|
:rules="rules"
|
||||||
|
:model="form"
|
||||||
|
require-asterisk-position="right"
|
||||||
|
>
|
||||||
|
<el-form-item label="变量名" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入变量名"
|
||||||
|
maxlength="64"
|
||||||
|
show-word-limit
|
||||||
|
@blur="form.name = form.name.trim()"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="变量" prop="variable">
|
||||||
|
<el-input
|
||||||
|
v-model="form.variable"
|
||||||
|
placeholder="请输入变量"
|
||||||
|
maxlength="64"
|
||||||
|
show-word-limit
|
||||||
|
@blur="form.variable = form.variable.trim()"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="输入类型">
|
||||||
|
<el-select v-model="form.type">
|
||||||
|
<el-option label="文本框" value="input"/>
|
||||||
|
<el-option label="日期" value="date"/>
|
||||||
|
<el-option label="下拉选项" value="select"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.type === 'select'">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex-between">
|
||||||
|
选项值
|
||||||
|
<el-button link type="primary" @click="addOption()">
|
||||||
|
<el-icon class="mr-4"><Plus /></el-icon> 添加
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="w-full flex-between" :key="option" v-for="(option, $index) in form.optionList">
|
||||||
|
<input class="el-textarea__inner" v-model.lazy="form.optionList[$index]" placeholder="请输入选项值"/>
|
||||||
|
<el-button link type="primary" @click="delOption($index)">
|
||||||
|
<el-icon class="mr-4"><Remove /></el-icon> 删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否必填" @click.prevent>
|
||||||
|
<el-switch size="small" v-model="form.is_required"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="赋值方式">
|
||||||
|
<el-radio-group v-model="form.assignment_method">
|
||||||
|
<el-radio label="user_input">用户输入</el-radio>
|
||||||
|
<el-radio label="api_input">接口传参</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click.prevent="dialogVisible = false"> 取消 </el-button>
|
||||||
|
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||||
|
{{ isEdit ? '保存' : '添加' }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from 'vue'
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
import { cloneDeep, debounce } from 'lodash'
|
||||||
|
import { MsgError } from '@/utils/message'
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
const fieldFormRef = ref()
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
|
||||||
|
const form = ref<any>({
|
||||||
|
name: '',
|
||||||
|
variable: '',
|
||||||
|
type: 'input',
|
||||||
|
is_required: true,
|
||||||
|
assignment_method: 'user_input',
|
||||||
|
optionList: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive({
|
||||||
|
name: [{ required: true, message: '请输入变量名', trigger: 'blur' }],
|
||||||
|
variable: [{ required: true, message: '请输入变量', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogVisible = ref<boolean>(false)
|
||||||
|
|
||||||
|
watch(dialogVisible, (bool) => {
|
||||||
|
if (!bool) {
|
||||||
|
form.value = {
|
||||||
|
name: '',
|
||||||
|
variable: '',
|
||||||
|
type: 'input',
|
||||||
|
is_required: true,
|
||||||
|
assignment_method: 'user_input',
|
||||||
|
optionList: []
|
||||||
|
}
|
||||||
|
isEdit.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = (row: any) => {
|
||||||
|
if (row) {
|
||||||
|
form.value = cloneDeep(row)
|
||||||
|
isEdit.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
if (form.value.type === 'select' && form.value.optionList.length === 0) {
|
||||||
|
return MsgError('请添加选项值')
|
||||||
|
}
|
||||||
|
await formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
emit('refresh', form.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOption = () => {
|
||||||
|
form.value.optionList.push('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const delOption = (index: number) => {
|
||||||
|
form.value.optionList.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
defineExpose({ open, close })
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
@ -196,6 +196,53 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
<div class="flex-between">
|
||||||
|
全局变量
|
||||||
|
<el-button link type="primary" @click="openAddDialog()">
|
||||||
|
<el-icon class="mr-4"><Plus /></el-icon> 添加
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="props.nodeModel.properties.input_field_list" class="mb-16">
|
||||||
|
<el-table-column prop="name" label="变量名" />
|
||||||
|
<el-table-column prop="variable" label="变量" />
|
||||||
|
<el-table-column label="输入类型">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" class="info-tag" v-if="row.type === 'input'">文本框</el-tag>
|
||||||
|
<el-tag type="info" class="info-tag" v-if="row.type === 'date'">日期</el-tag>
|
||||||
|
<el-tag type="info" class="info-tag" v-if="row.type === 'select'">下拉选项</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="必填">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div @click.stop>
|
||||||
|
<el-switch size="small" v-model="row.is_required" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="source" label="赋值方式">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.source === 'user_input' ? '用户输入' : '接口传参' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="left" width="80">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<span class="mr-4">
|
||||||
|
<el-tooltip effect="dark" content="修改" placement="top">
|
||||||
|
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
|
||||||
|
<el-icon><EditPen /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
<el-tooltip effect="dark" content="删除" placement="top">
|
||||||
|
<el-button type="primary" text @click="deleteField($index)">
|
||||||
|
<el-icon>
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
<!-- 回复内容弹出层 -->
|
<!-- 回复内容弹出层 -->
|
||||||
<el-dialog v-model="dialogVisible" title="开场白" append-to-body>
|
<el-dialog v-model="dialogVisible" title="开场白" append-to-body>
|
||||||
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor>
|
<MdEditor v-model="cloneContent" :preview="false" :toolbars="[]" :footers="[]"></MdEditor>
|
||||||
@ -206,6 +253,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</NodeContainer>
|
</NodeContainer>
|
||||||
|
<FieldFormDialog ref="FieldFormDialogRef" @refresh="refreshFieldList" />
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { app } from '@/main'
|
import { app } from '@/main'
|
||||||
@ -218,6 +266,8 @@ import { relatedObject } from '@/utils/utils'
|
|||||||
import useStore from '@/stores'
|
import useStore from '@/stores'
|
||||||
import applicationApi from '@/api/application'
|
import applicationApi from '@/api/application'
|
||||||
import type { Provider } from '@/api/type/model'
|
import type { Provider } from '@/api/type/model'
|
||||||
|
import FieldFormDialog from './component/FieldFormDialog.vue'
|
||||||
|
import { MsgError } from '@/utils/message'
|
||||||
const { model } = useStore()
|
const { model } = useStore()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -304,9 +354,47 @@ function getTTSModel() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentIndex = ref(null)
|
||||||
|
const FieldFormDialogRef = ref()
|
||||||
|
const inputFieldList = ref<any[]>([])
|
||||||
|
|
||||||
|
function openAddDialog(data?: any, index?: any) {
|
||||||
|
if (typeof index !== 'undefined') {
|
||||||
|
currentIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldFormDialogRef.value.open(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteField(index: any) {
|
||||||
|
inputFieldList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFieldList(data: any) {
|
||||||
|
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||||
|
if (inputFieldList.value[i].variable === data.variable && currentIndex.value !== i) {
|
||||||
|
MsgError('变量已存在: ' + data.variable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentIndex.value !== null) {
|
||||||
|
inputFieldList.value.splice(currentIndex.value, 1, data)
|
||||||
|
} else {
|
||||||
|
inputFieldList.value.push(data)
|
||||||
|
}
|
||||||
|
currentIndex.value = null
|
||||||
|
FieldFormDialogRef.value.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
set(props.nodeModel, 'validate', validate)
|
set(props.nodeModel, 'validate', validate)
|
||||||
|
if (props.nodeModel.properties.input_field_list) {
|
||||||
|
props.nodeModel.properties.input_field_list.forEach((item: any) => {
|
||||||
|
inputFieldList.value.push(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
set(props.nodeModel.properties, 'input_field_list', inputFieldList)
|
||||||
getProvider()
|
getProvider()
|
||||||
getTTSModel()
|
getTTSModel()
|
||||||
getSTTModel()
|
getSTTModel()
|
||||||
|
|||||||
@ -13,6 +13,18 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-for="(item, index) in inputFieldList" :key="index"
|
||||||
|
class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter"
|
||||||
|
@mouseenter="showicon = true"
|
||||||
|
@mouseleave="showicon = false"
|
||||||
|
>
|
||||||
|
<span>{{ item.name }} {{ '{' + item.variable + '}' }}</span>
|
||||||
|
<el-tooltip effect="dark" content="复制参数" placement="top" v-if="showicon === true">
|
||||||
|
<el-button link @click="copyClick('{{' + '全局变量.' + item.variable + '}}')" style="padding: 0">
|
||||||
|
<AppIcon iconName="app-copy"></AppIcon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</NodeContainer>
|
</NodeContainer>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -27,8 +39,23 @@ const globeLabel = '{{全局变量.time}}'
|
|||||||
|
|
||||||
const showicon = ref(false)
|
const showicon = ref(false)
|
||||||
|
|
||||||
// onMounted(() => {
|
const inputFieldList = ref<any[]>([])
|
||||||
// set(props.nodeModel, 'validate', validate)
|
|
||||||
// })
|
onMounted(() => {
|
||||||
|
props.nodeModel.graphModel.nodes
|
||||||
|
.filter((v: any) => v.id === 'base-node')
|
||||||
|
.map((v: any) => {
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
props.nodeModel.properties.config.globalFields = [
|
||||||
|
{
|
||||||
|
label: '当前时间',
|
||||||
|
value: 'time'
|
||||||
|
}, ...v.properties.input_field_list.map((i: any) => {
|
||||||
|
return { label: i.name, value: i.variable }
|
||||||
|
})
|
||||||
|
]
|
||||||
|
inputFieldList.value = v.properties.input_field_list
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user