add ui for multi llm config. dynamic adjust llm window height

This commit is contained in:
geekgeekrun
2025-04-12 14:36:37 +08:00
parent 924febd5e0
commit c5ac63b030
7 changed files with 260 additions and 129 deletions
@@ -4,6 +4,7 @@ import { createMainWindow } from '../../window/mainWindow'
import './app-menu' import './app-menu'
import initIpc from './ipc' import initIpc from './ipc'
import gtag from '../../utils/gtag' import gtag from '../../utils/gtag'
import initPublicIpc from '../../utils/initPublicIpc'
export function openSettingWindow() { export function openSettingWindow() {
// TODO: singleton lock; how can we check if there is another process should run as singleton with arguments? // TODO: singleton lock; how can we check if there is another process should run as singleton with arguments?
if (!app.requestSingleInstanceLock()) { if (!app.requestSingleInstanceLock()) {
@@ -31,6 +32,7 @@ export function openSettingWindow() {
// IPC test // IPC test
ipcMain.on('ping', () => console.log('pong')) ipcMain.on('ping', () => console.log('pong'))
initPublicIpc()
initIpc() initIpc()
app.on('activate', function () { app.on('activate', function () {
@@ -445,11 +445,7 @@ export default function initIpc() {
}) })
const defer = Promise.withResolvers() const defer = Promise.withResolvers()
async function saveLlmConfigHandler(_, configToSave) { async function saveLlmConfigHandler(_, configToSave) {
const configToPatch = await readConfigFile('llm.json') await writeConfigFile('llm.json', configToSave)
for (const k of Object.keys(configToSave)) {
configToPatch[k] = configToSave[k]
}
await writeConfigFile('llm.json', configToPatch)
defer.resolve() defer.resolve()
ipcMain.removeHandler('save-llm-config') ipcMain.removeHandler('save-llm-config')
llmConfigWindow?.close() llmConfigWindow?.close()
@@ -0,0 +1,21 @@
import { BrowserWindow, ipcMain } from 'electron'
export default function initPublicIpc() {
ipcMain.on(
'update-window-size',
(
ev,
size: {
width: number
height: number
animate?: boolean
}
) => {
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return
}
win.setSize(size.width, size.height, size.animate)
}
)
}
@@ -47,14 +47,5 @@ export const initIpc = () => {
createFirstLaunchNoticeApproveFlag() createFirstLaunchNoticeApproveFlag()
firstLaunchNoticeWindow?.close() firstLaunchNoticeWindow?.close()
}) })
ipcMain.on('update-window-size', (ev, size: {
width: number, height: number, animate?: boolean
}) => {
const win = BrowserWindow.fromWebContents(ev.sender)
if (!win) {
return
}
win.setSize(size.width, size.height, size.animate)
} )
} }
initIpc() initIpc()
@@ -11,7 +11,7 @@ export function createLlmConfigWindow(
} }
llmConfigWindow = new BrowserWindow({ llmConfigWindow = new BrowserWindow({
width: 576, width: 576,
height: 800, height: 410,
resizable: false, resizable: false,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
@@ -1,124 +1,181 @@
<template> <template>
<div class="llm-config-page"> <div class="llm-config-page">
<div class="mt1em mb1em"> <div class="main-wrapper">
<span>大语言模型设置</span> <main>
</div> <div class="mt1em mb1em">
<el-form <span>大语言模型设置</span>
ref="formRef" </div>
:model="formContent" <el-form
:rules="formRules" ref="formRef"
label-position="top" :model="formContent"
class="llm-config-form" :rules="formRules"
> label-position="top"
<div v-for="(conf, index) in formContent" :key="index"> class="llm-config-form"
<div> >
<el-form-item prop="providerCompleteApiUrl"> <div v-for="(conf, index) in formContent" :key="index" class="flex gap12px">
<div <div
class="el-form-item__label flex-items-center flex-justify-between w-full pr0px" v-if="formContent.length > 1"
:style="{ display: 'flex' }" :style="{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
height: 'fit-content',
marginTop: '10px'
}"
> >
<div>服务提供商 Base Url</div> <el-button
<el-dropdown @command="(item) => handlePresetClick(item, index)"> :disabled="index <= 0"
<el-button size="small" style="margin: 0"
>配置模板 <el-icon class="el-icon--right"><arrow-down /></el-icon circle
></el-button> size="small"
<template #dropdown> :icon="ArrowUp"
<el-dropdown-menu> @click="moveConfigUp(index)"
<el-dropdown-item />
v-for="item in llmPresetList" <el-button
:key="item.name" :disabled="index >= formContent.length - 1"
:command="item" style="margin: 0"
>{{ item.name }}</el-dropdown-item circle
> size="small"
</el-dropdown-menu> :icon="ArrowDown"
</template> @click="moveConfigDown(index)"
</el-dropdown> />
<el-button
:disabled="1 >= formContent.length"
style="margin: 0"
circle
size="small"
:icon="Delete"
@click="removeConfig(index)"
/>
</div> </div>
<el-input <div class="w-full">
v-model="conf.providerCompleteApiUrl" <el-form-item prop="providerCompleteApiUrl">
:autosize="{ <div
minRows: 10, class="el-form-item__label flex-items-center flex-justify-between w-full pr0px"
maxRows: 10 :style="{ display: 'flex' }"
}" >
font-size-12px <div>服务提供商 Base Url</div>
></el-input> <el-dropdown @command="(item) => handlePresetClick(item, index)">
</el-form-item> <el-button size="small"
<el-form-item prop="model" label="要使用的模型"> >配置模板 <el-icon class="el-icon--right"><arrow-down /></el-icon
<el-input ></el-button>
v-model="conf.model" <template #dropdown>
:autosize="{ <el-dropdown-menu>
minRows: 10, <el-dropdown-item
maxRows: 10 v-for="item in llmPresetList"
}" :key="item.name"
font-size-12px :command="item"
></el-input> >{{ item.name }}</el-dropdown-item
</el-form-item> >
<el-form-item prop="providerApiSecret" label="从服务提供商处获取的 API Secret"> </el-dropdown-menu>
<el-input </template>
v-model="conf.providerApiSecret" </el-dropdown>
:autosize="{ </div>
minRows: 10, <el-input
maxRows: 10 ref="firstInputRefList"
}" v-model="conf.providerCompleteApiUrl"
font-size-12px
></el-input>
</el-form-item>
<div class="serve-weight-config">
<div class="flex">
<el-form-item prop="enabled">
<div class="el-form-item__label">启用</div>
<el-checkbox
v-model="conf.enabled"
:autosize="{ :autosize="{
minRows: 10, minRows: 10,
maxRows: 10 maxRows: 10
}" }"
font-size-12px font-size-12px
></el-checkbox> ></el-input>
</el-form-item> </el-form-item>
<el-form-item prop="serveWeight" class="ml40px"> <el-form-item prop="model" label="要使用的模型">
<div class="el-form-item__label">权重</div> <el-input
<el-input-number v-model="conf.model"
v-model="conf.serveWeight"
:autosize="{ :autosize="{
minRows: 10, minRows: 10,
maxRows: 10 maxRows: 10
}" }"
:min="0"
:max="100"
:step="1"
step-strictly
:precision="0"
font-size-12px font-size-12px
placeholder="0 ~ 100" ></el-input>
></el-input-number>
</el-form-item> </el-form-item>
</div> <el-form-item prop="providerApiSecret" label="从服务提供商处获取的 API Secret">
<div class="flex"> <el-input
<!-- <el-form-item class="ml20px"> v-model="conf.providerApiSecret"
<el-button type="text">测试设置</el-button> :autosize="{
</el-form-item> --> minRows: 10,
maxRows: 10
}"
font-size-12px
></el-input>
</el-form-item>
<div v-if="formContent.length > 1" class="serve-weight-config">
<div class="flex">
<el-form-item prop="enabled">
<div class="el-form-item__label">启用</div>
<el-checkbox
v-model="conf.enabled"
:autosize="{
minRows: 10,
maxRows: 10
}"
font-size-12px
></el-checkbox>
</el-form-item>
<el-form-item prop="serveWeight" class="ml40px">
<div class="el-form-item__label">权重</div>
<el-input-number
v-model="conf.serveWeight"
:autosize="{
minRows: 10,
maxRows: 10
}"
:min="0"
:max="100"
:step="1"
step-strictly
:precision="0"
font-size-12px
placeholder="0 ~ 100"
></el-input-number>
</el-form-item>
</div>
<div class="flex">
<!-- <el-form-item class="ml20px">
<el-button type="text">测试设置</el-button>
</el-form-item> -->
</div>
</div>
<div
v-if="index !== formContent.length - 1"
class="mt6px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</div> </div>
</el-form>
</main>
</div>
<footer pt10px pb10px flex flex-justify-center>
<div w480px flex flex-justify-between>
<div>
<el-button type="text" @click="addConfig">添加其它服务</el-button>
</div>
<div>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div> </div>
</div>
</el-form>
<footer flex mt20px pb20px flex-justify-between>
<div>
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> -->
</div>
<div>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div> </div>
</footer> </footer>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ElForm, ElDropdown, ElDropdownMenu, ElDropdownItem, ElIcon, ElButton } from 'element-plus' import {
import { ArrowDown } from '@element-plus/icons-vue' ElForm,
import { ref, onMounted } from 'vue' ElDropdown,
ElDropdownMenu,
ElDropdownItem,
ElIcon,
ElButton,
ElInput
} from 'element-plus'
import { ArrowUp, ArrowDown, Delete } from '@element-plus/icons-vue'
import { ref, onMounted, watch, nextTick } from 'vue'
interface LlmConfigItem { interface LlmConfigItem {
providerCompleteApiUrl: string providerCompleteApiUrl: string
@@ -128,16 +185,17 @@ interface LlmConfigItem {
enabled: true enabled: true
} }
const formRef = ref<InstanceType<typeof ElForm>>() function getNewConfigItem(): LlmConfigItem {
const formContent = ref<LlmConfigItem[]>([ return {
{
providerCompleteApiUrl: '', providerCompleteApiUrl: '',
providerApiSecret: '', providerApiSecret: '',
model: '', model: '',
serveWeight: 10, serveWeight: 10,
enabled: true enabled: true
} }
]) }
const formRef = ref<InstanceType<typeof ElForm>>()
const formContent = ref<LlmConfigItem[]>([getNewConfigItem()])
const formRules = {} const formRules = {}
@@ -151,12 +209,17 @@ const handleSubmit = async () => {
onMounted(async () => { onMounted(async () => {
const savedFileContent = (await electron.ipcRenderer.invoke('fetch-config-file-content')) const savedFileContent = (await electron.ipcRenderer.invoke('fetch-config-file-content'))
?.config?.['llm.json'] ?.config?.['llm.json']
if (!savedFileContent) { if (!savedFileContent?.length) {
return return
} }
for (const k of Object.keys(formContent.value)) { const keyOfItem = Object.keys(getNewConfigItem())
formContent.value[k] = savedFileContent[k] formContent.value = savedFileContent.map((it) => {
} const conf = {}
for (const k of keyOfItem) {
conf[k] = it[k]
}
return conf
})
}) })
const llmPresetList: { const llmPresetList: {
@@ -202,13 +265,71 @@ function handlePresetClick(selected: (typeof llmPresetList)[number], index) {
} }
} }
const firstInputRefList = ref<InstanceType<typeof ElInput>[]>([])
function addConfig() {
formContent.value.push(getNewConfigItem())
nextTick(() => {
firstInputRefList.value[firstInputRefList.value.length - 1]?.focus()
})
}
function moveConfigUp(index) {
;[formContent.value[index], formContent.value[index - 1]] = [
formContent.value[index - 1],
formContent.value[index]
]
}
function moveConfigDown(index) {
;[formContent.value[index], formContent.value[index + 1]] = [
formContent.value[index + 1],
formContent.value[index]
]
}
function removeConfig(index) {
formContent.value.splice(index, 1)
}
watch(
() => formContent.value.length,
(nVal) => {
if (nVal <= 1) {
electron.ipcRenderer.send('update-window-size', {
width: window.innerWidth,
height: 360
})
} else {
electron.ipcRenderer.send('update-window-size', {
width: window.innerWidth,
height: 730
})
}
},
{
immediate: true
}
)
// function handleTestAvailability() {} // function handleTestAvailability() {}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.llm-config-page { .llm-config-page {
margin: 0 auto; margin: 0 auto;
max-width: 480px; display: flex;
flex-direction: column;
height: 100vh;
.main-wrapper {
flex: 1;
overflow: auto;
main {
margin: 0 auto;
max-width: 480px;
}
}
footer {
background-color: #f0f0f0;
}
} }
</style> </style>
@@ -167,13 +167,13 @@
font-size-12px font-size-12px
/> />
</el-form-item> </el-form-item>
<div
v-if="index !== formContent.geekWorkExpList.length - 1"
class="mt20px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</div> </div>
<div
v-if="index !== formContent.geekWorkExpList.length - 1"
class="mt20px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -283,13 +283,13 @@
font-size-12px font-size-12px
/> />
</el-form-item> </el-form-item>
<div
v-if="index !== formContent.geekProjExpList.length - 1"
class="mt20px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</div> </div>
<div
v-if="index !== formContent.geekProjExpList.length - 1"
class="mt20px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>