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,5 +1,7 @@
<template> <template>
<div class="llm-config-page"> <div class="llm-config-page">
<div class="main-wrapper">
<main>
<div class="mt1em mb1em"> <div class="mt1em mb1em">
<span>大语言模型设置</span> <span>大语言模型设置</span>
</div> </div>
@@ -10,8 +12,45 @@
label-position="top" label-position="top"
class="llm-config-form" class="llm-config-form"
> >
<div v-for="(conf, index) in formContent" :key="index"> <div v-for="(conf, index) in formContent" :key="index" class="flex gap12px">
<div> <div
v-if="formContent.length > 1"
:style="{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
height: 'fit-content',
marginTop: '10px'
}"
>
<el-button
:disabled="index <= 0"
style="margin: 0"
circle
size="small"
:icon="ArrowUp"
@click="moveConfigUp(index)"
/>
<el-button
:disabled="index >= formContent.length - 1"
style="margin: 0"
circle
size="small"
:icon="ArrowDown"
@click="moveConfigDown(index)"
/>
<el-button
:disabled="1 >= formContent.length"
style="margin: 0"
circle
size="small"
:icon="Delete"
@click="removeConfig(index)"
/>
</div>
<div class="w-full">
<el-form-item prop="providerCompleteApiUrl"> <el-form-item prop="providerCompleteApiUrl">
<div <div
class="el-form-item__label flex-items-center flex-justify-between w-full pr0px" class="el-form-item__label flex-items-center flex-justify-between w-full pr0px"
@@ -35,6 +74,7 @@
</el-dropdown> </el-dropdown>
</div> </div>
<el-input <el-input
ref="firstInputRefList"
v-model="conf.providerCompleteApiUrl" v-model="conf.providerCompleteApiUrl"
:autosize="{ :autosize="{
minRows: 10, minRows: 10,
@@ -63,7 +103,7 @@
font-size-12px font-size-12px
></el-input> ></el-input>
</el-form-item> </el-form-item>
<div class="serve-weight-config"> <div v-if="formContent.length > 1" class="serve-weight-config">
<div class="flex"> <div class="flex">
<el-form-item prop="enabled"> <el-form-item prop="enabled">
<div class="el-form-item__label">启用</div> <div class="el-form-item__label">启用</div>
@@ -100,25 +140,42 @@
</el-form-item> --> </el-form-item> -->
</div> </div>
</div> </div>
<div
v-if="index !== formContent.length - 1"
class="mt6px mb20px h1px"
style="background-color: #dcdcdc"
/>
</div> </div>
</div> </div>
</el-form> </el-form>
<footer flex mt20px pb20px flex-justify-between> </main>
</div>
<footer pt10px pb10px flex flex-justify-center>
<div w480px flex flex-justify-between>
<div> <div>
<!-- <el-button type="text" @click="handleTestAvailability">测试可用性</el-button> --> <el-button type="text" @click="addConfig">添加其它服务</el-button>
</div> </div>
<div> <div>
<el-button @click="handleCancel">取消</el-button> <el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button> <el-button type="primary" @click="handleSubmit">确定</el-button>
</div> </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,14 +265,72 @@ 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;
display: flex;
flex-direction: column;
height: 100vh;
.main-wrapper {
flex: 1;
overflow: auto;
main {
margin: 0 auto; margin: 0 auto;
max-width: 480px; max-width: 480px;
} }
}
footer {
background-color: #f0f0f0;
}
}
</style> </style>
<style lang="scss"> <style lang="scss">
@@ -167,14 +167,14 @@
font-size-12px font-size-12px
/> />
</el-form-item> </el-form-item>
</div>
</div>
<div <div
v-if="index !== formContent.geekWorkExpList.length - 1" v-if="index !== formContent.geekWorkExpList.length - 1"
class="mt20px mb20px h1px" class="mt20px mb20px h1px"
style="background-color: #dcdcdc" style="background-color: #dcdcdc"
/> />
</div> </div>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<div class="el-form-item__label"> <div class="el-form-item__label">
@@ -283,14 +283,14 @@
font-size-12px font-size-12px
/> />
</el-form-item> </el-form-item>
</div>
</div>
<div <div
v-if="index !== formContent.geekProjExpList.length - 1" v-if="index !== formContent.geekProjExpList.length - 1"
class="mt20px mb20px h1px" class="mt20px mb20px h1px"
style="background-color: #dcdcdc" style="background-color: #dcdcdc"
/> />
</div> </div>
</div>
</div>
</el-form-item> </el-form-item>
</el-form> </el-form>
</main> </main>