/ src / renderer / components / pages / SettingPage.vue
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>