Skip to content

Commit e3483a4

Browse files
committed
feat(flow): add shared AI form components
1 parent 53e8e71 commit e3483a4

File tree

11 files changed

+518
-6
lines changed

11 files changed

+518
-6
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template>
2+
<el-autocomplete
3+
v-model="inputValue"
4+
:fetch-suggestions="fetchSuggestions"
5+
clearable
6+
popper-class="is-wider"
7+
/>
8+
</template>
9+
10+
<script setup lang="ts">
11+
import { ElAutocomplete } from 'element-plus'
12+
import { computed } from 'vue'
13+
14+
const props = withDefaults(
15+
defineProps<{
16+
modelValue?: string
17+
options: string[]
18+
filterable?: boolean
19+
}>(),
20+
{
21+
filterable: true,
22+
},
23+
)
24+
25+
const emit = defineEmits<{
26+
(e: 'update:modelValue', value: string): void
27+
}>()
28+
29+
const inputValue = computed({
30+
get() {
31+
return props.modelValue
32+
},
33+
set(val: string) {
34+
emit('update:modelValue', val ?? '')
35+
},
36+
})
37+
38+
const fetchSuggestions = (queryString: string, cb: any) => {
39+
if (!queryString) {
40+
cb(props.options)
41+
}
42+
let options = props.options
43+
if (props.filterable) {
44+
options = options.filter((value) => value.includes(queryString))
45+
}
46+
const ret = options.map((value) => ({ value, label: value }))
47+
cb(ret)
48+
}
49+
</script>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<template>
2+
<el-input v-model="inputValue">
3+
<template #suffix>
4+
<el-button link @click.stop.prevent="showDialog = true">
5+
<el-icon><FullScreen /></el-icon>
6+
</el-button>
7+
</template>
8+
</el-input>
9+
<el-dialog
10+
v-model="showDialog"
11+
append-to-body
12+
width="700"
13+
destroy-on-close
14+
:title="title"
15+
@open="handleDialogOpen"
16+
>
17+
<el-input ref="DialogInputRef" v-model="dialogInputValue" :rows="20" type="textarea" />
18+
<template #footer>
19+
<el-button type="primary" @click="submit">
20+
{{ t('flow.confirm') }}
21+
</el-button>
22+
</template>
23+
</el-dialog>
24+
</template>
25+
26+
<script lang="ts" setup>
27+
import { ElDialog, ElInput, ElButton, ElIcon } from 'element-plus'
28+
import { FullScreen } from '@element-plus/icons-vue'
29+
import { waitAMoment } from '@emqx/shared-ui-utils'
30+
import { computed, ref } from 'vue'
31+
import { useFlowLocale } from '../flow/composables/useFlowLocale'
32+
33+
const props = defineProps<{
34+
modelValue?: string
35+
title: string
36+
}>()
37+
38+
const emit = defineEmits<{
39+
(e: 'update:modelValue', val: string): void
40+
}>()
41+
42+
const { t } = useFlowLocale()
43+
44+
const inputValue = computed({
45+
get() {
46+
return props.modelValue
47+
},
48+
set(val: string) {
49+
emit('update:modelValue', val)
50+
},
51+
})
52+
53+
const showDialog = ref(false)
54+
const dialogInputValue = ref('')
55+
const handleDialogOpen = async () => {
56+
dialogInputValue.value = props.modelValue ?? ''
57+
await waitAMoment(200)
58+
DialogInputRef.value?.focus?.()
59+
}
60+
61+
const DialogInputRef = ref()
62+
63+
const submit = () => {
64+
emit('update:modelValue', dialogInputValue.value)
65+
showDialog.value = false
66+
}
67+
</script>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<template>
2+
<el-form
3+
ref="FormCom"
4+
label-width="164px"
5+
class="provider-form"
6+
label-position="right"
7+
:rules="rules"
8+
:model="record"
9+
:validate-on-rule-change="false"
10+
@submit.prevent
11+
>
12+
<CustomFormItem prop="input" :label="t('flow.input')" :readonly="readonly">
13+
<el-autocomplete
14+
v-model="record.input"
15+
:fetch-suggestions="getFieldList"
16+
clearable
17+
popper-class="is-wider"
18+
/>
19+
</CustomFormItem>
20+
<CustomFormItem prop="system_prompt" :readonly="readonly">
21+
<template #label>
22+
{{ t('flow.systemPrompt') }}
23+
<component :is="tipComponent" :content="t('flow.systemPromptDesc')" />
24+
</template>
25+
<InputWithTextEditDialog v-model="record.system_prompt" :title="t('flow.systemPrompt')" />
26+
</CustomFormItem>
27+
<CustomFormItem prop="base_url" :readonly="readonly">
28+
<template #label>
29+
{{ t('flow.baseURL') }}
30+
<component
31+
:is="tipComponent"
32+
:content="
33+
isGemini
34+
? t('flow.geminiBaseUrlTips')
35+
: t('flow.baseURLDesc', { url: defaultBaseURL, name: title })
36+
"
37+
/>
38+
</template>
39+
<el-input v-model="baseUrlProxy" :placeholder="defaultBaseURL"> </el-input>
40+
</CustomFormItem>
41+
<CustomFormItem prop="api_key" :label="t('flow.apiKey')" :readonly="readonly">
42+
<CustomInputPassword v-model="record.api_key" />
43+
</CustomFormItem>
44+
<CustomFormItem prop="model" :label="t('flow.model')" :readonly="readonly">
45+
<InputWithOptions v-model="record.model" :options="modelOpts" :filterable="false" />
46+
</CustomFormItem>
47+
<template v-if="isAnthropicProfile(record)">
48+
<CustomFormItem prop="max_tokens" :label="t('flow.maxTokens')" :readonly="readonly">
49+
<component :is="inputNumberComponent" v-model="record.max_tokens" />
50+
</CustomFormItem>
51+
<CustomFormItem
52+
prop="anthropic_version"
53+
:label="t('flow.anthropicVersion')"
54+
:readonly="readonly"
55+
>
56+
<el-select v-model="record.anthropic_version">
57+
<el-option v-for="item in anthropicVersionOpts" :key="item" :label="item" :value="item" />
58+
</el-select>
59+
</CustomFormItem>
60+
</template>
61+
<CustomFormItem prop="alias" :readonly="readonly">
62+
<template #label>
63+
{{ t('flow.aiOutputAlias') }}
64+
<component
65+
:is="tipComponent"
66+
:content="`${t('flow.aiOutputAliasDesc')}<br />${t('flow.aliasDesc')}`"
67+
:is-html="true"
68+
:desc-marked="true"
69+
/>
70+
</template>
71+
<el-input v-model="record.alias" />
72+
</CustomFormItem>
73+
<slot name="advancedSettings"></slot>
74+
</el-form>
75+
</template>
76+
77+
<script setup lang="ts">
78+
import { ElForm, ElAutocomplete, ElSelect, ElOption, ElInput, type FormRules } from 'element-plus'
79+
import {
80+
GEMINI_DEFAULT_BASE_URL,
81+
ProcessingType,
82+
ANTHROPIC_VERSION_MAP,
83+
AIProviderType,
84+
} from '@emqx/shared-ui-constants'
85+
import { Component, computed, ref } from 'vue'
86+
import aiModels from './aiModels.json'
87+
import { useFlowLocale } from '../../composables/useFlowLocale'
88+
import useFlowNode from '../../composables/useFlowNode'
89+
import CustomFormItem from '../../../common/CustomFormItem.vue'
90+
import CustomInputPassword from '../../../common/CustomInputPassword.vue'
91+
import InputWithOptions from '../../../common/InputWithOptions.vue'
92+
import InputWithTextEditDialog from '../../../common/InputWithTextEditDialog.vue'
93+
import type { Node } from '@vue-flow/core'
94+
import type { AIAnthropicConfig, AIConfig } from '../../types'
95+
96+
const defaultModelOptsMap = new Map([
97+
[ProcessingType.AIOpenAI, aiModels.openai],
98+
[ProcessingType.AIAnthropic, aiModels.anthropic],
99+
[ProcessingType.AIGemini, aiModels.gemini],
100+
])
101+
102+
const props = defineProps<{
103+
modelValue: AIConfig
104+
nodeSpecificType: ProcessingType
105+
readonly: boolean
106+
availableFields: Array<{ value: string }>
107+
rules: FormRules
108+
inputNumberComponent: Component
109+
tipComponent: Component
110+
modelOptsMap?: Map<ProcessingType, Array<string>>
111+
nodes?: Array<Node>
112+
}>()
113+
const emit = defineEmits<{
114+
(e: 'update:modelValue', val: AIConfig): void
115+
}>()
116+
117+
const { t } = useFlowLocale()
118+
const { getCommonTypeLabel } = useFlowNode()
119+
120+
const FormCom = ref()
121+
122+
const record = computed({
123+
get() {
124+
return props.modelValue
125+
},
126+
set(val) {
127+
emit('update:modelValue', val)
128+
},
129+
})
130+
131+
const getFieldList = (queryString: string, cb: any) => {
132+
if (!queryString) {
133+
cb(props.availableFields)
134+
}
135+
const ret = props.availableFields.filter(({ value }) => value.includes(queryString))
136+
cb(ret)
137+
}
138+
139+
const anthropicVersionOpts = Object.values(ANTHROPIC_VERSION_MAP)
140+
141+
const isAnthropicProfile = (profile: AIConfig): profile is AIAnthropicConfig => {
142+
return profile.type === AIProviderType.Anthropic
143+
}
144+
145+
const modelOpts = computed(
146+
() => (props.modelOptsMap ?? defaultModelOptsMap).get(props.nodeSpecificType) ?? [],
147+
)
148+
const isGemini = computed(() => props.nodeSpecificType === ProcessingType.AIGemini)
149+
const baseUrlProxy = computed({
150+
get() {
151+
const { base_url } = record.value
152+
if (isGemini.value && base_url === GEMINI_DEFAULT_BASE_URL) {
153+
return ''
154+
}
155+
return base_url
156+
},
157+
set(val: string) {
158+
if (isGemini.value) {
159+
if (!val) {
160+
record.value.base_url = GEMINI_DEFAULT_BASE_URL
161+
} else {
162+
record.value.base_url = val
163+
}
164+
} else {
165+
record.value.base_url = val
166+
}
167+
},
168+
})
169+
const defaultBaseURL = computed(() =>
170+
isGemini.value ? GEMINI_DEFAULT_BASE_URL : `https://api.${props.modelValue.type}.com/v1`,
171+
)
172+
const title = computed(() => getCommonTypeLabel(props.nodeSpecificType))
173+
174+
defineExpose({ validate: () => FormCom.value.validate(), defaultBaseURL })
175+
</script>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<template>
2+
<CustomFormItem prop="transport_options.connect_timeout" :readonly="readonly">
3+
<template #label>
4+
{{ t('flow.connectTimeout') }}
5+
</template>
6+
<component :is="inputWithUnitComponent" v-model="transportOptions.connect_timeout" />
7+
</CustomFormItem>
8+
<CustomFormItem prop="transport_options.recv_timeout" :readonly="readonly">
9+
<template #label>
10+
{{ t('flow.recvTimeout') }}
11+
</template>
12+
<component :is="inputWithUnitComponent" v-model="transportOptions.recv_timeout" />
13+
</CustomFormItem>
14+
<CustomFormItem prop="transport_options.checkout_timeout" :readonly="readonly">
15+
<template #label>
16+
{{ t('flow.checkoutTimeout') }}
17+
<component :is="tipComponent" :content="t('flow.checkoutTimeoutDesc')" />
18+
</template>
19+
<component :is="inputWithUnitComponent" v-model="transportOptions.checkout_timeout" />
20+
</CustomFormItem>
21+
<CustomFormItem prop="transport_options.max_connections" :readonly="readonly">
22+
<template #label>
23+
{{ t('flow.maxConn') }}
24+
<component :is="tipComponent" :content="t('flow.maxConnectionsDesc')" />
25+
</template>
26+
<component :is="inputNumberComponent" v-model="transportOptions.max_connections" />
27+
</CustomFormItem>
28+
</template>
29+
30+
<script lang="ts" setup>
31+
import { Component, computed } from 'vue'
32+
import CustomFormItem from '../../../common/CustomFormItem.vue'
33+
import { useFlowLocale } from '../../composables/useFlowLocale'
34+
import { AITransportOptions } from '../../types'
35+
36+
const { t } = useFlowLocale()
37+
38+
const props = defineProps<{
39+
modelValue: AITransportOptions
40+
readonly: boolean
41+
inputWithUnitComponent: Component
42+
inputNumberComponent: Component
43+
tipComponent: Component
44+
}>()
45+
46+
const emit = defineEmits<{
47+
(e: 'update:modelValue', val: AITransportOptions): void
48+
}>()
49+
50+
const transportOptions = computed({
51+
get: () => props.modelValue,
52+
set: (val) => emit('update:modelValue', val),
53+
})
54+
</script>

0 commit comments

Comments
 (0)