add basic llm testing window

This commit is contained in:
geekgeekrun
2025-04-22 03:20:37 +08:00
parent 954553d8d8
commit 3d148e8e01
6 changed files with 319 additions and 95 deletions
@@ -33,7 +33,10 @@ import { WriteStream } from 'node:fs'
import { hasOwn } from '@vue/shared' import { hasOwn } from '@vue/shared'
import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow' import { createLlmConfigWindow, llmConfigWindow } from '../../../window/llmConfigWindow'
import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow' import { createResumeEditorWindow, resumeEditorWindow } from '../../../window/resumeEditorWindow'
import { getValidTemplate } from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation' import {
getValidTemplate,
requestNewMessageContent
} from '../../READ_NO_REPLY_AUTO_REMINDER/boss-operation'
import { import {
autoReminderPromptTemplateFileName, autoReminderPromptTemplateFileName,
writeDefaultAutoRemindPrompt writeDefaultAutoRemindPrompt
@@ -42,6 +45,11 @@ import {
checkIsResumeContentValid, checkIsResumeContentValid,
resumeContentEnoughDetect resumeContentEnoughDetect
} from '../../../../common/utils/resume' } from '../../../../common/utils/resume'
import {
createReadNoReplyReminderLlmMockWindow,
readNoReplyReminderLlmMockWindow
} from '../../../window/readNoReplyReminderLlmMockWindow'
import { RequestSceneEnum } from '../../../features/llm-request-log'
export default function initIpc() { export default function initIpc() {
ipcMain.handle('fetch-config-file-content', async () => { ipcMain.handle('fetch-config-file-content', async () => {
@@ -539,6 +547,33 @@ export default function initIpc() {
} }
} }
}) })
ipcMain.on('test-llm-config-effect', () => {
createReadNoReplyReminderLlmMockWindow({
parent: mainWindow!,
modal: true,
show: true
})
async function requestLlm(_, requestPayload) {
return await requestNewMessageContent(requestPayload.messageList, {
requestScene: RequestSceneEnum.testing
})
}
ipcMain.handle('request-llm-for-test', requestLlm)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('request-llm-for-test')
})
async function getLlmConfigList() {
return await readConfigFile('llm.json')
}
ipcMain.handle('get-llm-config-for-test', getLlmConfigList)
readNoReplyReminderLlmMockWindow?.once('closed', () => {
ipcMain.removeHandler('get-llm-config-for-test')
})
})
ipcMain.on('close-read-no-reply-reminder-llm-mock-window', () =>
readNoReplyReminderLlmMockWindow?.close()
)
ipcMain.handle('exit-app-immediately', () => { ipcMain.handle('exit-app-immediately', () => {
app.exit(0) app.exit(0)
}) })
@@ -115,7 +115,7 @@ export const writeDefaultAutoRemindPrompt = async () => {
await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false }) await writeStorageFile(autoReminderPromptTemplateFileName, defaultPrompt, { isJson: false })
} }
export const sendGptContent = async (page: Page, chatRecords) => { export const requestNewMessageContent = async (chatRecords, { requestScene } = {}) => {
const template = await getValidTemplate() const template = await getValidTemplate()
const resumeObject = (await readConfigFile('resumes.json'))?.[0] const resumeObject = (await readConfigFile('resumes.json'))?.[0]
const resumeContent = formatResumeJsonToMarkdown(resumeObject) const resumeContent = formatResumeJsonToMarkdown(resumeObject)
@@ -148,6 +148,9 @@ export const sendGptContent = async (page: Page, chatRecords) => {
} }
console.log(chatList) console.log(chatList)
let res, llmConfig let res, llmConfig
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & {
providerApiSecret: string
} = {}
while (!res) { while (!res) {
const llmConfigList = await readConfigFile('llm.json') const llmConfigList = await readConfigFile('llm.json')
llmConfig = pickLlmConfigFromList(llmConfigList) llmConfig = pickLlmConfigFromList(llmConfigList)
@@ -155,17 +158,15 @@ export const sendGptContent = async (page: Page, chatRecords) => {
throw new Error(`CANNOT_FIND_A_USABLE_MODEL`) throw new Error(`CANNOT_FIND_A_USABLE_MODEL`)
} }
console.log(llmConfig.providerCompleteApiUrl) console.log(llmConfig.providerCompleteApiUrl)
const llmRequestRecord: Omit<LlmModelUsageRecord, 'id' | 'providerApiSecretMd5'> & { Object.assign(llmRequestRecord, {
providerApiSecret: string
} = {
providerCompleteApiUrl: llmConfig.providerCompleteApiUrl, providerCompleteApiUrl: llmConfig.providerCompleteApiUrl,
model: llmConfig.model, model: llmConfig.model,
providerApiSecret: llmConfig.providerApiSecret, providerApiSecret: llmConfig.providerApiSecret,
requestStartTime: new Date(), requestStartTime: new Date(),
hasError: false, hasError: false,
errorMessage: '', errorMessage: '',
requestScene: RequestSceneEnum.readNoReplyAutoReminder requestScene
} })
try { try {
const completion = await completes( const completion = await completes(
{ {
@@ -227,6 +228,19 @@ export const sendGptContent = async (page: Page, chatRecords) => {
} catch (err) { } catch (err) {
throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`) throw new Error(`fail to parse response. ${err?.message} ${res?.message?.content}`)
} }
return {
responseText: textToSend,
usedLlmConfig: llmConfig,
recordInfo: llmRequestRecord
}
}
export async function sendGptContent(page: Page, chatRecords) {
const textToSend = (
await requestNewMessageContent(chatRecords, {
requestScene: RequestSceneEnum.readNoReplyAutoReminder
})
).responseText
const chatInputSelector = `.chat-conversation .message-controls .chat-input` const chatInputSelector = `.chat-conversation .message-controls .chat-input`
const chatInputHandle = await page.$(chatInputSelector) const chatInputHandle = await page.$(chatInputSelector)
await chatInputHandle.click() await chatInputHandle.click()
@@ -0,0 +1,47 @@
import { BrowserWindow } from 'electron'
import path from 'path'
export let readNoReplyReminderLlmMockWindow: BrowserWindow | null = null
export function createReadNoReplyReminderLlmMockWindow(
opt?: Electron.BrowserWindowConstructorOptions
): BrowserWindow {
// Create the browser window.
if (readNoReplyReminderLlmMockWindow) {
readNoReplyReminderLlmMockWindow!.show()
}
readNoReplyReminderLlmMockWindow = new BrowserWindow({
width: 600,
height: 800,
resizable: false,
show: false,
autoHideMenuBar: true,
frame: false,
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false
},
...opt
})
readNoReplyReminderLlmMockWindow.on('ready-to-show', () => {
readNoReplyReminderLlmMockWindow!.show()
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
readNoReplyReminderLlmMockWindow.loadURL(
process.env['ELECTRON_RENDERER_URL'] + '#/readNoReplyReminderLlmMock'
)
} else {
readNoReplyReminderLlmMockWindow.loadURL(
'file://' + path.join(__dirname, '../renderer/index.html') + '#/readNoReplyReminderLlmMock'
)
}
readNoReplyReminderLlmMockWindow!.once('closed', () => {
readNoReplyReminderLlmMockWindow = null
})
return readNoReplyReminderLlmMockWindow!
}
@@ -81,7 +81,7 @@
<div> <div>
<div> <div>
<el-button size="small" type="primary" @click="handleClickEditPrompt"> <el-button size="small" type="primary" @click="handleClickEditPrompt">
使用外部编辑器编辑提示词模板 使用外部编辑器编辑提示词模板 (Markdown)
</el-button> </el-button>
<el-button <el-button
size="small" size="small"
@@ -133,6 +133,11 @@
</el-select> </el-select>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="handleTestEffectClicked"
>使用当前配置模拟已读不回复聊过程</el-button
>
</el-form-item>
</template> </template>
</div> </div>
<el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes"> <el-form-item label="跟进间隔(分钟)" prop="throttleIntervalMinutes">
@@ -256,20 +261,7 @@ watch(
} }
) )
const handleSubmit = async () => { async function checkIsCanRun() {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
rechat_content_source: formContent.value.autoReminder.rechatContentSource,
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm
})
await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
gtagRenderer('config_saved')
if (
formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
if (!(await electron.ipcRenderer.invoke('check-is-resume-content-valid'))) { if (!(await electron.ipcRenderer.invoke('check-is-resume-content-valid'))) {
gtagRenderer('cannot_launch_for_invalid_rc_dialog_show') gtagRenderer('cannot_launch_for_invalid_rc_dialog_show')
try { try {
@@ -287,7 +279,7 @@ const handleSubmit = async () => {
} catch { } catch {
gtagRenderer('invalid_rc_dialog_click_cancel') gtagRenderer('invalid_rc_dialog_click_cancel')
} }
return return false
} }
try { try {
await electron.ipcRenderer.invoke('check-if-llm-config-list-valid') await electron.ipcRenderer.invoke('check-if-llm-config-list-valid')
@@ -324,7 +316,7 @@ const handleSubmit = async () => {
message: '大模型配置检查未通过,请重试' message: '大模型配置检查未通过,请重试'
}) })
} }
return return false
} }
try { try {
await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid') await electron.ipcRenderer.invoke('check-if-auto-remind-prompt-valid')
@@ -357,6 +349,27 @@ const handleSubmit = async () => {
message: '用于生成自动提醒消息的提示词检查未通过,请重试' message: '用于生成自动提醒消息的提示词检查未通过,请重试'
}) })
} }
return false
}
return true
}
const handleSubmit = async () => {
gtagRenderer('run_read_no_reply_reminder_clicked', {
throttle_interval_minutes: formContent.value.autoReminder.throttleIntervalMinutes,
rechat_limit_day: formContent.value.autoReminder.rechatLimitDay,
rechat_content_source: formContent.value.autoReminder.rechatContentSource,
recent_message_quantity_for_llm: formContent.value.autoReminder.recentMessageQuantityForLlm
})
await formRef.value!.validate()
await electron.ipcRenderer.invoke('save-config-file-from-ui', JSON.stringify(formContent.value))
gtagRenderer('config_saved')
if (
formContent.value.autoReminder?.rechatContentSource ===
RECHAT_CONTENT_SOURCE.GEMINI_WITH_CHAT_CONTEXT
) {
if (!(await checkIsCanRun())) {
return return
} }
if (!(await electron.ipcRenderer.invoke('resume-content-enough-detect'))) { if (!(await electron.ipcRenderer.invoke('resume-content-enough-detect'))) {
@@ -455,6 +468,13 @@ const rechatLlmFallbackOptions = [
value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM value: RECHAT_LLM_FALLBACK.EXIT_REMINDER_PROGRAM
} }
] ]
async function handleTestEffectClicked() {
if (!(await checkIsCanRun())) {
return
}
electron.ipcRenderer.send('test-llm-config-effect')
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -0,0 +1,101 @@
<template>
<div class="h100vh flex flex-col">
<div
:style="{
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: `auto`,
margin: `0 auto`,
alignItems: `flex-end`
}"
>
<div class="pb20px"></div>
<div v-for="(item, index) in messageList" :key="index" class="message-item">
{{ item.text }}
</div>
<div class="pb20px"></div>
</div>
<div
:style="{
display: 'grid',
gridTemplateColumns: '100px 1fr',
height: `fit-content`,
paddingTop: `10px`,
paddingBottom: `10px`,
backgroundColor: `#f0f0f0`
}"
>
<!-- <el-select v-model="selectedLlmConfig">
<el-option v-for="(it, index) in llmConfigList" :key="index" :label="it">
<div>{{ it.model }}</div>
<div>{{ it.providerCompleteApiUrl }}</div>
</el-option>
</el-select> -->
<el-button ml20px type="text" @click="closeWindow">关闭对话框</el-button>
<el-button
:loading="isLoading"
mr10px
width-fit-content
type="primary"
@click="sendLlmGeneratedContent"
>
<template v-if="isLoading">正在生成消息请稍候...</template>
<template v-else-if="!messageList.length">发送开场白</template>
<template v-else>发送下一句提醒内容</template>
</el-button>
<div></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
type MessageItem = {
text: string
generatedBy: string
}
const messageList = ref<MessageItem[]>([])
// const llmConfigList = ref([])
// async function getLlmConfigList() {
// debugger
// llmConfigList.value = await electron.ipcRenderer.invoke('get-llm-config-for-test')
// }
// getLlmConfigList().catch(() => {})
// const selectedLlmConfig = ref()
const isLoading = ref(false)
async function sendLlmGeneratedContent() {
isLoading.value = true
try {
const response = await electron.ipcRenderer.invoke('request-llm-for-test', {
messageList: JSON.parse(JSON.stringify(messageList.value ?? []))
})
console.log(response)
messageList.value.push({
text: response.responseText,
generatedBy: response.usedLlmConfig
})
} finally {
isLoading.value = false
}
}
function closeWindow() {
electron.ipcRenderer.send(`close-read-no-reply-reminder-llm-mock-window`)
}
</script>
<style lang="scss" scoped>
.message-item {
line-height: 1.25em;
font-size: 14px;
background-color: #d1f0ef;
color: #333;
padding: 10px;
border-radius: 8px 8px 0 8px;
margin-top: 20px;
max-width: 420px;
}
</style>
@@ -31,6 +31,13 @@ const routes: Array<RouteRecordRaw> = [
title: '简历编辑' title: '简历编辑'
} }
}, },
{
path: '/readNoReplyReminderLlmMock',
component: () => import('@renderer/page/ReadNoReplyReminderLlmMock/index.vue'),
meta: {
title: '已读不回提醒器 大语言模型测试'
}
},
{ {
path: '/main-layout', path: '/main-layout',
component: () => import('@renderer/page/MainLayout/index.vue'), component: () => import('@renderer/page/MainLayout/index.vue'),