SettingPage.vue
1 <script setup lang="ts"> 2 import { ref, watch } from 'vue' 3 import { REASONING_EFFORT, ENABLE_THINKING } from '@/renderer/types' 4 import { useI18n } from 'vue-i18n' 5 import { useLayoutStore } from '@/renderer/store/layout' 6 import { v4 as uuidv4 } from 'uuid' 7 import { getApiToken, listenStdioProgress, removeListenStdioProgress } from '@/renderer/utils' 8 import LogoAvatar from '@/renderer/components/common/LogoAvatar.vue' 9 import ConfigJsonCard from '@/renderer/components/common/ConfigJsonCard.vue' 10 11 import type { ChatbotConfig } from '@/types/llm' 12 13 const layoutStore = useLayoutStore() 14 const { t } = useI18n() 15 const isLoading = ref(false) 16 const apiDialog = ref(false) 17 18 const stderr = ref<string[]>([]) 19 const stdout = ref<string[]>([]) 20 21 interface Props { 22 config: ChatbotConfig 23 } 24 25 interface Emits { 26 (_e: 'update:config', _value: Partial<ChatbotConfig>): void 27 (_e: 'batch:token', _apiCli: string, _apiKey: string): void 28 } 29 30 const props = defineProps<Props>() 31 const emit = defineEmits<Emits>() 32 const handleUpdate = <K extends keyof ChatbotConfig>(key: K, value: ChatbotConfig[K]) => { 33 emit('update:config', { [key]: value } as Partial<ChatbotConfig>) 34 } 35 36 const handleBatchToken = (apiCli: string, apiKey: string) => { 37 emit('batch:token', apiCli, apiKey) 38 } 39 40 // defineExpose({ 41 // updateConfig: handleUpdate 42 // }) 43 44 watch(apiDialog, (_val) => { 45 stdout.value.length = 0 46 stderr.value.length = 0 47 }) 48 49 watch( 50 () => props.config.enableExtraBody, 51 async (val) => { 52 if (val) { 53 domExtraBody.value?.scrollIntoView({ 54 behavior: 'smooth', 55 block: 'center' 56 }) 57 } 58 }, 59 { flush: 'post' } 60 ) 61 62 const domExtraBody = ref<HTMLElement | null>(null) 63 64 const handleGetApiToken = async (cli: string): Promise<void> => { 65 const handleProgress = (_event: Event, progress: string) => { 66 stdout.value.push(progress) 67 } 68 69 try { 70 isLoading.value = true 71 listenStdioProgress(handleProgress) 72 73 const token = await getApiToken(cli) 74 handleBatchToken(cli, token) 75 apiDialog.value = false 76 } catch (error: any) { 77 stderr.value.push(error.toString()) 78 console.log(error.toString()) 79 } finally { 80 isLoading.value = false 81 removeListenStdioProgress(handleProgress) 82 } 83 } 84 85 const validateNumberRange = (min: number, max: number) => { 86 return (value: string | number | null): boolean | string => { 87 if (!value && value !== 0) return true 88 89 const num = Number(value) 90 if (isNaN(num)) return t('validation.invalid-number') 91 92 if (num < min || num > max) { 93 return t('validation.number-range', { min, max }) 94 } 95 96 return true 97 } 98 } 99 </script> 100 101 <template> 102 <v-card class="mx-auto" :title="$t('setting.title-api')"> 103 <template #prepend> 104 <div class="cursor-pointer"> 105 <LogoAvatar :item="config"></LogoAvatar> 106 <v-dialog activator="parent" max-width="80vw"> 107 <template #default="{ isActive }"> 108 <v-card title="Logo"> 109 <v-divider></v-divider> 110 <v-card-text> 111 <v-text-field 112 :model-value="config.icon" 113 class="mt-2" 114 variant="outlined" 115 hide-details 116 clearable 117 @update:model-value="(v) => handleUpdate('icon', v)" 118 ></v-text-field> 119 </v-card-text> 120 <template #actions> 121 <v-btn 122 variant="plain" 123 rounded="lg" 124 icon="mdi-close-box" 125 color="error" 126 @click="isActive.value = false" 127 ></v-btn> 128 </template> 129 </v-card> 130 </template> 131 </v-dialog> 132 </div> 133 134 <!-- <v-avatar v-if="config.icon" class="cursor-pointer" rounded="lg" size="x-small"> 135 <v-img :src="config.icon"></v-img> 136 137 </v-avatar> --> 138 </template> 139 <v-divider></v-divider> 140 <v-card-text class="pt-6"> 141 <v-row class="px-2 pr-4"> 142 <v-col cols="12" md> 143 <v-text-field 144 density="compact" 145 variant="outlined" 146 :model-value="config.name" 147 :label="$t('setting.name')" 148 hide-details 149 @update:model-value="(v) => handleUpdate('name', v)" 150 @blur="!config.name && handleUpdate('name', `Chatbot ${uuidv4()}`)" 151 ></v-text-field> 152 </v-col> 153 <v-col cols="6" md="auto"> 154 <v-checkbox 155 :model-value="config.mcp" 156 :label="$t('setting.mcp')" 157 color="secondary" 158 @update:model-value="(v) => handleUpdate('mcp', Boolean(v))" 159 > 160 </v-checkbox> 161 </v-col> 162 <v-col cols="6" md="auto"> 163 <v-checkbox 164 :model-value="config.stream" 165 :label="$t('setting.stream')" 166 color="secondary" 167 @update:model-value="(v) => handleUpdate('stream', Boolean(v))" 168 > 169 </v-checkbox> 170 </v-col> 171 </v-row> 172 173 <!-- API Key --> 174 <v-text-field 175 density="compact" 176 variant="outlined" 177 :append-inner-icon="layoutStore.apiKeyShow ? 'mdi-eye-off' : 'mdi-eye'" 178 :type="layoutStore.apiKeyShow ? 'text' : 'password'" 179 :model-value="config.apiKey" 180 class="px-2 mb-6" 181 :label="$t('setting.apikey')" 182 prepend-inner-icon="mdi-key" 183 clearable 184 hide-details 185 :loading="isLoading" 186 @update:model-value="(v) => handleUpdate('apiKey', v)" 187 @click:append-inner="layoutStore.apiKeyShow = !layoutStore.apiKeyShow" 188 > 189 <template #append> 190 <v-icon-btn 191 :color="config.apiCli ? 'primary' : undefined" 192 icon="mdi-application-edit" 193 variant="plain" 194 @click="apiDialog = true" 195 > 196 </v-icon-btn> 197 </template> 198 </v-text-field> 199 200 <v-dialog v-model="apiDialog" width="auto"> 201 <v-card 202 width="50vw" 203 max-width="700" 204 prepend-icon="mdi-console" 205 :title="$t('setting.dialog')" 206 > 207 <v-divider></v-divider> 208 <v-card-text> 209 <v-textarea 210 :model-value="config.apiCli" 211 rows="1" 212 auto-grow 213 :loading="isLoading" 214 :hide-details="stderr.length == 0" 215 :error-messages="stderr.at(-1)" 216 variant="solo" 217 @update:model-value="(v) => handleUpdate('apiCli', v)" 218 > 219 </v-textarea> 220 </v-card-text> 221 <v-divider></v-divider> 222 <template #actions> 223 <v-btn 224 :disabled="!config.apiCli" 225 :text="$t('setting.exec')" 226 @click="handleGetApiToken(config.apiCli)" 227 ></v-btn> 228 </template> 229 </v-card> 230 </v-dialog> 231 232 <v-row class="pr-2"> 233 <v-switch 234 v-tooltip:start="$t('setting.auth-header')" 235 min-width="170px" 236 class="mt-1 mb-6 ml-4" 237 :label="config.authorization ? 'Authorization' : 'X-Api-Key'" 238 color="secondary" 239 base-color="primary" 240 hide-details 241 inset 242 :model-value="config.authorization" 243 @update:model-value="(v) => handleUpdate('authorization', Boolean(v))" 244 > 245 </v-switch> 246 <v-col> 247 <v-combobox 248 class="mt-3" 249 :disabled="!config.authorization" 250 density="compact" 251 :label="$t('setting.auth-prefix')" 252 :items="config.authPrefixList" 253 :model-value="config.authPrefix" 254 variant="outlined" 255 @update:model-value="(v) => handleUpdate('authPrefix', v)" 256 > 257 </v-combobox> 258 </v-col> 259 </v-row> 260 261 <!-- URL --> 262 <v-combobox 263 density="compact" 264 class="px-2" 265 :label="$t('setting.url')" 266 :items="config.urlList" 267 :model-value="config.url" 268 variant="outlined" 269 @update:model-value="(v) => handleUpdate('url', v)" 270 ></v-combobox> 271 272 <!-- Path --> 273 <v-combobox 274 density="compact" 275 class="px-2" 276 :label="$t('setting.path')" 277 :items="config.pathList" 278 :model-value="config.path" 279 variant="outlined" 280 @update:model-value="(v) => handleUpdate('path', v)" 281 ></v-combobox> 282 283 <!-- Model --> 284 <v-combobox 285 density="compact" 286 class="px-2" 287 :label="$t('setting.model')" 288 :items="config.modelList" 289 :model-value="config.model" 290 variant="outlined" 291 @update:model-value="(v) => handleUpdate('model', v)" 292 ></v-combobox> 293 </v-card-text> 294 </v-card> 295 296 <v-card class="mx-auto mt-4" :title="$t('setting.title-model')"> 297 <v-divider></v-divider> 298 <v-card-text class="pt-6"> 299 <v-row> 300 <v-combobox 301 class="px-2" 302 density="compact" 303 :label="$t('setting.max-tokens-prefix')" 304 :items="config.maxTokensPrefixList" 305 :model-value="config.maxTokensPrefix" 306 variant="outlined" 307 @update:model-value="(v) => handleUpdate('maxTokensPrefix', v)" 308 > 309 </v-combobox> 310 <v-combobox 311 class="px-2" 312 density="compact" 313 label="MaxTokenValue" 314 :model-value="config.maxTokensValue" 315 type="number" 316 single-line 317 variant="outlined" 318 @update:model-value="(v) => handleUpdate('maxTokensValue', v)" 319 > 320 </v-combobox> 321 </v-row> 322 <v-row class="my-0"> 323 <v-combobox 324 class="px-2" 325 density="compact" 326 :label="$t('setting.temperature')" 327 type="number" 328 :model-value="config.temperature" 329 variant="outlined" 330 :rules="[validateNumberRange(0, 2)]" 331 @update:model-value="(v) => handleUpdate('temperature', v)" 332 > 333 </v-combobox> 334 <v-combobox 335 class="px-2" 336 density="compact" 337 :label="$t('setting.topP')" 338 :model-value="config.topP" 339 type="number" 340 variant="outlined" 341 :rules="[validateNumberRange(0, 1)]" 342 @update:model-value="(v) => handleUpdate('topP', v)" 343 > 344 </v-combobox> 345 </v-row> 346 <v-field class="ma-2 d-inline-flex" dirty variant="outlined"> 347 <template #label> 348 <div>{{ $t('setting.reasoning-effort') }}</div> 349 </template> 350 <v-btn-toggle 351 class="mt-0" 352 color="secondary" 353 :model-value="config.reasoningEffort" 354 variant="plain" 355 @update:model-value="(v) => handleUpdate('reasoningEffort', v)" 356 > 357 <v-btn v-for="level in REASONING_EFFORT" :key="level">{{ 358 level.length > 4 ? level.slice(0, 3) : level 359 }}</v-btn> 360 </v-btn-toggle> 361 </v-field> 362 <v-field class="ma-2 ml-8 d-inline-flex" dirty variant="outlined"> 363 <template #label> 364 <div>{{ $t('setting.enable-thinking') }}</div> 365 </template> 366 <v-btn-toggle 367 class="mt-0" 368 color="secondary" 369 v-tooltip:top="$t('setting.enable-thinking-tip')" 370 :model-value="config.enableThinking" 371 variant="plain" 372 @update:model-value="(v) => handleUpdate('enableThinking', v)" 373 > 374 <v-btn v-for="level in ENABLE_THINKING" :key="level">{{ level }}</v-btn> 375 </v-btn-toggle> 376 </v-field> 377 <v-row class="px-0"> 378 <v-switch 379 min-width="200px" 380 class="ml-4" 381 :label="$t('setting.enable-extra-body')" 382 color="secondary" 383 base-color="primary" 384 hide-details 385 inset 386 :model-value="config.enableExtraBody" 387 @update:model-value="(v) => handleUpdate('enableExtraBody', Boolean(v))" 388 ></v-switch> 389 </v-row> 390 </v-card-text> 391 </v-card> 392 <div ref="domExtraBody"> 393 <v-card v-if="config.enableExtraBody" class="mx-auto mt-4" :title="$t('setting.extra-body')"> 394 <v-divider></v-divider> 395 <ConfigJsonCard v-model="config.extraBody" clearable rows="1"> </ConfigJsonCard> 396 </v-card> 397 </div> 398 </template> 399 <style scoped> 400 .cursor-pointer { 401 cursor: pointer; 402 } 403 </style>