From 89e03dfe5850879798ad4ffde36b1b2280a05a4e Mon Sep 17 00:00:00 2001 From: 1837669410 <1837669410@qq.com> Date: Tue, 12 May 2026 21:30:19 +0800 Subject: [PATCH] Add output code language setting --- .../backend/__tests__/deepseekClient.test.ts | 1 + .../__tests__/paperAnalyzerFlow.test.ts | 19 +++++ .../backend/__tests__/promptBuilder.test.ts | 26 ++++++- .../backend/__tests__/settingsStore.test.ts | 56 ++++++++++++++- src/main/backend/paperAnalyzer.ts | 7 +- src/main/backend/promptBuilder.ts | 72 +++++++++---------- src/main/backend/settingsStore.ts | 19 +++++ src/main/main.ts | 2 +- src/main/preload.ts | 2 +- src/renderer/__tests__/App.analysis.test.tsx | 1 + src/renderer/__tests__/App.resizable.test.tsx | 1 + src/renderer/__tests__/i18n.test.ts | 1 + src/renderer/components/SettingsPanel.tsx | 27 ++++++- .../__tests__/SettingsPanel.test.tsx | 33 ++++++++- src/renderer/i18n.ts | 2 + src/renderer/types.d.ts | 5 +- 16 files changed, 223 insertions(+), 51 deletions(-) diff --git a/src/main/backend/__tests__/deepseekClient.test.ts b/src/main/backend/__tests__/deepseekClient.test.ts index 694b720..b1d5e6c 100644 --- a/src/main/backend/__tests__/deepseekClient.test.ts +++ b/src/main/backend/__tests__/deepseekClient.test.ts @@ -15,6 +15,7 @@ function mockSettings(provider = 'deepseek', model?: string, apiKey = 'test-key' provider, model: model ?? (provider === 'kimi' ? 'kimi-k2.6' : 'deepseek-v4-flash'), language: 'zh-CN', + selectedCodeLanguage: 'Python', }) } diff --git a/src/main/backend/__tests__/paperAnalyzerFlow.test.ts b/src/main/backend/__tests__/paperAnalyzerFlow.test.ts index fb489ff..ef77606 100644 --- a/src/main/backend/__tests__/paperAnalyzerFlow.test.ts +++ b/src/main/backend/__tests__/paperAnalyzerFlow.test.ts @@ -70,6 +70,7 @@ describe('analyzePaper flow', () => { provider: 'deepseek', model: 'deepseek-v4-flash', language: 'zh-CN', + selectedCodeLanguage: 'Python', }) mockedParsePDF.mockResolvedValue({ text: 'paper text', pageCount: 3 }) }) @@ -95,6 +96,24 @@ describe('analyzePaper flow', () => { expect(progress).toEqual(expect.arrayContaining(['parsing', 'summarizing', 'generating_code', 'done'])) }) + it('passes the selected output code language into the combined analysis prompt', async () => { + mockedGetActiveSettings.mockReturnValueOnce({ + apiKey: 'key', + provider: 'deepseek', + model: 'deepseek-v4-flash', + language: 'en-US', + selectedCodeLanguage: 'MATLAB', + }) + mockLlmOutput(outputWithoutCode('MATLAB summary')) + + await analyzePaper('paper.pdf', () => {}) + + const messages = mockedCallDeepSeek.mock.calls[0][0] + expect(messages[0].content).toContain('Demo Code and core code files MUST be generated in MATLAB') + expect(messages[1].content).toContain('The selected output code language is MATLAB') + expect(messages[1].content).toContain('core_code/descriptive_file_name.m') + }) + it('caches generated core code and returns hasCoreCode for valid code output', async () => { mockLlmOutput(outputWithCode()) diff --git a/src/main/backend/__tests__/promptBuilder.test.ts b/src/main/backend/__tests__/promptBuilder.test.ts index 5f55cc5..772fb5d 100644 --- a/src/main/backend/__tests__/promptBuilder.test.ts +++ b/src/main/backend/__tests__/promptBuilder.test.ts @@ -2,6 +2,22 @@ import { describe, expect, it } from 'vitest' import { buildCodePrompt, buildCombinedAnalysisPrompt, buildSummaryPrompt } from '../promptBuilder' describe('promptBuilder', () => { + it.each([ + ['Python', 'py'], + ['C', 'c'], + ['C++', 'cpp'], + ['Java', 'java'], + ['Go', 'go'], + ['Rust', 'rs'], + ['MATLAB', 'm'], + ['R', 'R'], + ])('uses the expected source extension for %s code output', (codeLanguage, extension) => { + const prompt = buildCombinedAnalysisPrompt('paper body', 'en-US', codeLanguage) + + expect(prompt.system).toContain(`generated in ${codeLanguage}`) + expect(prompt.user).toContain(`core_code/descriptive_file_name.${extension}`) + }) + it('builds Chinese summary prompts by default with README and math rules', () => { const prompt = buildSummaryPrompt('paper body') @@ -24,7 +40,7 @@ describe('promptBuilder', () => { }) it('builds code prompts with strict JSON schema and summary context', () => { - const prompt = buildCodePrompt('paper text', 'summary text', 'en-US') + const prompt = buildCodePrompt('paper text', 'summary text', 'en-US', 'Rust') expect(prompt.system).toContain('Use English') expect(prompt.user).toContain('paper text') @@ -32,6 +48,8 @@ describe('promptBuilder', () => { expect(prompt.user).toContain('Return ONLY strict JSON') expect(prompt.user).toContain('"files"') expect(prompt.user).toContain('"notApplicableReason"') + expect(prompt.user).toContain('Generate all demo/core code in Rust') + expect(prompt.user).toContain('core_code/descriptive_file_name.rs') expect(prompt.user).toContain('Every file path MUST be relative') }) @@ -47,13 +65,15 @@ describe('promptBuilder', () => { }) it('builds combined prompts with blueprint and exact code bundle constraints', () => { - const prompt = buildCombinedAnalysisPrompt('combined paper', 'en-US') + const prompt = buildCombinedAnalysisPrompt('combined paper', 'en-US', 'C++') expect(prompt.system).toContain('Use English') + expect(prompt.system).toContain('Demo Code and core code files MUST be generated in C++') expect(prompt.user).toContain('# Paper Title') expect(prompt.user).toContain('') expect(prompt.user).toContain('') - expect(prompt.user).toContain('') + expect(prompt.user).toContain('The selected output code language is C++') + expect(prompt.user).toContain('') expect(prompt.user).toContain('no more and no fewer') expect(prompt.user).toContain('Avoid generic files') }) diff --git a/src/main/backend/__tests__/settingsStore.test.ts b/src/main/backend/__tests__/settingsStore.test.ts index e0cbe7f..718ebd1 100644 --- a/src/main/backend/__tests__/settingsStore.test.ts +++ b/src/main/backend/__tests__/settingsStore.test.ts @@ -43,6 +43,7 @@ describe('settingsStore', () => { provider: 'deepseek', model: PROVIDER_SETTINGS.deepseek.defaultModel, language: 'zh-CN', + selectedCodeLanguage: 'Python', }) }) @@ -54,6 +55,7 @@ describe('settingsStore', () => { provider: 'deepseek', model: PROVIDER_SETTINGS.deepseek.defaultModel, language: 'zh-CN', + selectedCodeLanguage: 'Python', }) }) @@ -76,6 +78,25 @@ describe('settingsStore', () => { provider: 'kimi', model: 'kimi-k2.5', language: 'en-US', + selectedCodeLanguage: 'Python', + }) + }) + + it('normalizes and preserves the selected output code language', () => { + writeConfig({ + provider: 'deepseek', + selectedCodeLanguage: 'Rust', + providers: { + deepseek: { apiKey: 'deep-key', model: 'deepseek-v4-pro' }, + }, + }) + + expect(getActiveSettings()).toEqual({ + apiKey: 'deep-key', + provider: 'deepseek', + model: 'deepseek-v4-pro', + language: 'zh-CN', + selectedCodeLanguage: 'Rust', }) }) @@ -93,6 +114,21 @@ describe('settingsStore', () => { provider: 'deepseek', model: PROVIDER_SETTINGS.deepseek.defaultModel, language: 'zh-CN', + selectedCodeLanguage: 'Python', + }) + }) + + it('falls back invalid stored output code languages to Python', () => { + writeConfig({ + provider: 'deepseek', + selectedCodeLanguage: 'TypeScript', + providers: { + deepseek: { apiKey: 'deep-key', model: 'deepseek-v4-flash' }, + }, + }) + + expect(getActiveSettings()).toMatchObject({ + selectedCodeLanguage: 'Python', }) }) @@ -111,6 +147,7 @@ describe('settingsStore', () => { provider: 'kimi', model: PROVIDER_SETTINGS.kimi.defaultModel, language: 'en-US', + selectedCodeLanguage: 'Python', }) }) @@ -127,15 +164,17 @@ describe('settingsStore', () => { provider: 'glm', model: 'glm-5-turbo', language: 'en-US', + selectedCodeLanguage: 'Python', }) }) it('saves active provider settings and preserves provider-specific keys', () => { - expect(saveSettingsPatch({ apiKey: 'deep-key', language: 'en-US' })).toEqual({ + expect(saveSettingsPatch({ apiKey: 'deep-key', language: 'en-US', selectedCodeLanguage: 'Go' })).toEqual({ apiKey: 'deep-key', provider: 'deepseek', model: PROVIDER_SETTINGS.deepseek.defaultModel, language: 'en-US', + selectedCodeLanguage: 'Go', }) expect(saveSettingsPatch({ provider: 'kimi' })).toEqual({ @@ -143,6 +182,7 @@ describe('settingsStore', () => { provider: 'kimi', model: PROVIDER_SETTINGS.kimi.defaultModel, language: 'en-US', + selectedCodeLanguage: 'Go', }) expect(saveSettingsPatch({ apiKey: 'kimi-key', model: 'kimi-k2.5' })).toEqual({ @@ -150,6 +190,7 @@ describe('settingsStore', () => { provider: 'kimi', model: 'kimi-k2.5', language: 'en-US', + selectedCodeLanguage: 'Go', }) expect(saveSettingsPatch({ provider: 'deepseek' })).toEqual({ @@ -157,6 +198,19 @@ describe('settingsStore', () => { provider: 'deepseek', model: PROVIDER_SETTINGS.deepseek.defaultModel, language: 'en-US', + selectedCodeLanguage: 'Go', + }) + }) + + it('saves selected output code language patches and normalizes unsupported values', () => { + expect(saveSettingsPatch({ selectedCodeLanguage: 'MATLAB' })).toMatchObject({ + selectedCodeLanguage: 'MATLAB', + }) + + expect(readStoredConfig().selectedCodeLanguage).toBe('MATLAB') + + expect(saveSettingsPatch({ selectedCodeLanguage: 'CUDA' })).toMatchObject({ + selectedCodeLanguage: 'Python', }) }) diff --git a/src/main/backend/paperAnalyzer.ts b/src/main/backend/paperAnalyzer.ts index 5a65bfb..9088f7e 100644 --- a/src/main/backend/paperAnalyzer.ts +++ b/src/main/backend/paperAnalyzer.ts @@ -94,8 +94,11 @@ export async function analyzePaper( signal?: AbortSignal ): Promise { let language = 'zh-CN' + let selectedCodeLanguage = 'Python' try { - language = getActiveSettings().language || 'zh-CN' + const settings = getActiveSettings() + language = settings.language || 'zh-CN' + selectedCodeLanguage = settings.selectedCodeLanguage || 'Python' } catch {} const msg = (zh: string, en: string) => language === 'en-US' ? en : zh @@ -116,7 +119,7 @@ export async function analyzePaper( onProgress({ stage: 'parsing', message: msg(`PDF 解析完成 (${pageCount} 页)`, `PDF parsed (${pageCount} pages)`) }) onProgress({ stage: 'summarizing', message: msg('正在分析论文结构并生成总结...', 'Analyzing paper structure and generating summary...') }) - const analysisPrompt = buildCombinedAnalysisPrompt(text, language) + const analysisPrompt = buildCombinedAnalysisPrompt(text, language, selectedCodeLanguage) let rawOutput = '' let streamedSummary = '' let summaryClosed = false diff --git a/src/main/backend/promptBuilder.ts b/src/main/backend/promptBuilder.ts index 188b6ed..2396f67 100644 --- a/src/main/backend/promptBuilder.ts +++ b/src/main/backend/promptBuilder.ts @@ -1,3 +1,24 @@ +function getCodeExtension(selectedCodeLanguage: string): string { + switch (selectedCodeLanguage) { + case 'C': + return 'c' + case 'C++': + return 'cpp' + case 'Java': + return 'java' + case 'Go': + return 'go' + case 'Rust': + return 'rs' + case 'MATLAB': + return 'm' + case 'R': + return 'R' + default: + return 'py' + } +} + export function buildSummaryPrompt(text: string, language: string = 'zh-CN'): { system: string; user: string } { const isEn = language === 'en-US' @@ -71,8 +92,9 @@ $$` } } -export function buildCodePrompt(text: string, summaryContext: string, language: string = 'zh-CN'): { system: string; user: string } { +export function buildCodePrompt(text: string, summaryContext: string, language: string = 'zh-CN', selectedCodeLanguage: string = 'Python'): { system: string; user: string } { const isEn = language === 'en-US' + const codeExtension = getCodeExtension(selectedCodeLanguage) const system = `You are an expert AI research paper analyst. Your task is to analyze academic papers and produce structured summaries and, when applicable, extract core algorithmic implementations. @@ -97,9 +119,9 @@ ${summaryContext} Your task: 1. Identify the smallest implementable core method from the paper. 2. Implement it as multiple focused files inside a folder-style project structure. -3. Prefer Python for ML/algorithm papers. +3. Generate all demo/core code in ${selectedCodeLanguage}. Do NOT switch to Python unless the selected output code language is Python. 4. Keep files componentized and minimal: configuration, model/algorithm, loss/metrics if needed, training or inference, and an example entry point. -5. Include a requirements.txt file when dependencies are needed. +5. Use file extensions and dependency notes appropriate for ${selectedCodeLanguage}. Output format: Return ONLY strict JSON. Do not wrap it in Markdown fences. Do not add prose before or after the JSON. @@ -108,35 +130,7 @@ The JSON schema MUST be: { "files": [ { - "path": "requirements.txt", - "content": "..." - }, - { - "path": "core_code/__init__.py", - "content": "..." - }, - { - "path": "core_code/config.py", - "content": "..." - }, - { - "path": "core_code/model.py", - "content": "..." - }, - { - "path": "core_code/losses.py", - "content": "..." - }, - { - "path": "core_code/train.py", - "content": "..." - }, - { - "path": "core_code/inference.py", - "content": "..." - }, - { - "path": "core_code/example.py", + "path": "core_code/descriptive_file_name.${codeExtension}", "content": "..." } ] @@ -157,6 +151,7 @@ Rules: - If the paper uses pseudo-code, translate it to real code. - If the paper describes an architecture, implement the forward pass. - If the paper has a training procedure, implement the training loop. +- Demo Code MUST use ${selectedCodeLanguage}. - Keep code APIs, filenames, classes, and variables in English even when the UI language is Chinese.` return { @@ -165,8 +160,9 @@ Rules: } } -export function buildCombinedAnalysisPrompt(text: string, language: string = 'zh-CN'): { system: string; user: string } { +export function buildCombinedAnalysisPrompt(text: string, language: string = 'zh-CN', selectedCodeLanguage: string = 'Python'): { system: string; user: string } { const isEn = language === 'en-US' + const codeExtension = getCodeExtension(selectedCodeLanguage) const system = `You are an expert AI research paper analyst. Analyze the paper, write a README.md-style summary, decide whether core component code is needed, and generate code only when applicable. @@ -174,6 +170,7 @@ Rules: - Be precise and factual. Do not fabricate details not present in the paper. - Use ${isEn ? 'English' : 'Chinese'} for paper summary and decision reason. - Keep generated code APIs, filenames, classes, and variables in English. +- Demo Code and core code files MUST be generated in ${selectedCodeLanguage}. - Format mathematical expressions in the summary with Markdown LaTeX delimiters: inline math as $...$ and block math as $$...$$. - Follow the exact tagged output protocol. Do not add prose before, between, or after the required tags.` @@ -251,7 +248,7 @@ Use "needed": true only if the paper clearly describes at least one implementabl "minimalImplementationBoundary": "exactly what the generated code should implement, and what it should not implement", "files": [ { - "path": "core_code/descriptive_file_name.py", + "path": "core_code/descriptive_file_name.${codeExtension}", "purpose": "why this file is necessary for the minimal core contribution", "mainSymbols": ["function_or_class_name"], "mustInclude": ["specific method elements that must appear in this file"], @@ -277,6 +274,9 @@ Use "needed": true only if the paper clearly describes at least one implementabl Blueprint rules: +- The selected output code language is ${selectedCodeLanguage}. Every generated source file MUST use ${selectedCodeLanguage}. +- Do NOT switch to Python unless the selected output code language is Python. +- Choose file extensions appropriate for ${selectedCodeLanguage}. - Do NOT assume the paper is about AI, machine learning, or software engineering. Infer the domain from the paper. - Do NOT use a fixed project template. The file list must be designed from the paper's smallest implementable computational contribution. - Each file must be justified by the core contribution. If a file is not necessary for that contribution, omit it. @@ -288,7 +288,7 @@ Blueprint rules: 5. Then output code files inside these exact tags. The code bundle MUST contain exactly the files listed in P2CC_CODE_BLUEPRINT.files, no more and no fewer: - + ... @@ -301,7 +301,7 @@ Code generation rules: - Do NOT encode file contents as JSON strings. Write raw file content directly inside the file block. - Use the exact paths declared in P2CC_CODE_BLUEPRINT.files. - Do not generate extra helper files outside the blueprint. -- Prefer Python for algorithmic papers, but choose simple, dependency-light code that best fits the paper's method. +- Demo Code MUST use ${selectedCodeLanguage}; choose simple, dependency-light code that best fits the paper's method within that language. - Keep the implementation reusable as a core component, not as a full experiment reproduction project. - Use comments to explain key implementation decisions only where helpful.` diff --git a/src/main/backend/settingsStore.ts b/src/main/backend/settingsStore.ts index 017a2dd..7c67bba 100644 --- a/src/main/backend/settingsStore.ts +++ b/src/main/backend/settingsStore.ts @@ -44,11 +44,14 @@ export const PROVIDER_SETTINGS: Record } const DEFAULT_PROVIDER = 'deepseek' const DEFAULT_LANGUAGE = 'zh-CN' +const DEFAULT_CODE_LANGUAGE = 'Python' function getConfigPath(): string { return path.join(app.getPath('userData'), 'config.json') @@ -92,6 +98,12 @@ function normalizeLanguage(language: unknown): string { return language === 'en-US' ? 'en-US' : DEFAULT_LANGUAGE } +function normalizeCodeLanguage(language: unknown): string { + return typeof language === 'string' && (CODE_LANGUAGE_OPTIONS as readonly string[]).includes(language) + ? language + : DEFAULT_CODE_LANGUAGE +} + function createDefaultSettings(): StoredSettings { const providers: Record = {} @@ -105,6 +117,7 @@ function createDefaultSettings(): StoredSettings { return { provider: DEFAULT_PROVIDER, language: DEFAULT_LANGUAGE, + selectedCodeLanguage: DEFAULT_CODE_LANGUAGE, providers, } } @@ -116,6 +129,7 @@ function normalizeSettings(raw: unknown): StoredSettings { const selectedProvider = isKnownProvider(raw.provider) ? raw.provider : DEFAULT_PROVIDER settings.provider = selectedProvider settings.language = normalizeLanguage(raw.language) + settings.selectedCodeLanguage = normalizeCodeLanguage(raw.selectedCodeLanguage) if (isObject(raw.providers)) { for (const provider of Object.keys(PROVIDER_SETTINGS)) { @@ -161,6 +175,7 @@ export function getActiveSettings(): ActiveSettings { provider: settings.provider, model: providerSettings.model, language: settings.language, + selectedCodeLanguage: settings.selectedCodeLanguage, } } @@ -171,6 +186,10 @@ export function saveSettingsPatch(patch: SettingsPatch): ActiveSettings { settings.language = normalizeLanguage(patch.language) } + if (patch.selectedCodeLanguage !== undefined) { + settings.selectedCodeLanguage = normalizeCodeLanguage(patch.selectedCodeLanguage) + } + if (patch.provider !== undefined) { if (!isKnownProvider(patch.provider)) { throw new Error(`Unsupported provider: ${patch.provider}`) diff --git a/src/main/main.ts b/src/main/main.ts index 481dfe4..cac1c9f 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -49,7 +49,7 @@ ipcMain.handle('get-settings', async () => { try { return getActiveSettings() } catch { - return { apiKey: '', provider: 'deepseek', model: 'deepseek-v4-flash', language: 'zh-CN' } + return { apiKey: '', provider: 'deepseek', model: 'deepseek-v4-flash', language: 'zh-CN', selectedCodeLanguage: 'Python' } } }) diff --git a/src/main/preload.ts b/src/main/preload.ts index 93c011e..4e0f15d 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('electronAPI', { selectPDF: () => ipcRenderer.invoke('select-pdf'), - saveSettings: (settings: { apiKey?: string; provider?: string; model?: string; language?: string }) => + saveSettings: (settings: { apiKey?: string; provider?: string; model?: string; language?: string; selectedCodeLanguage?: string }) => ipcRenderer.invoke('save-settings', settings), getSettings: () => ipcRenderer.invoke('get-settings'), analyzePaper: (pdfPath: string) => ipcRenderer.invoke('analyze-paper', pdfPath), diff --git a/src/renderer/__tests__/App.analysis.test.tsx b/src/renderer/__tests__/App.analysis.test.tsx index 729f7f5..d491fe0 100644 --- a/src/renderer/__tests__/App.analysis.test.tsx +++ b/src/renderer/__tests__/App.analysis.test.tsx @@ -38,6 +38,7 @@ function createElectronMock(): ElectronMock { provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US', + selectedCodeLanguage: 'Python', }), analyzePaper: vi.fn(), cancelAnalysis: vi.fn().mockResolvedValue(true), diff --git a/src/renderer/__tests__/App.resizable.test.tsx b/src/renderer/__tests__/App.resizable.test.tsx index 62e916a..7a61d00 100644 --- a/src/renderer/__tests__/App.resizable.test.tsx +++ b/src/renderer/__tests__/App.resizable.test.tsx @@ -15,6 +15,7 @@ function installElectronMock() { provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US', + selectedCodeLanguage: 'Python', }), analyzePaper: vi.fn(), cancelAnalysis: vi.fn().mockResolvedValue(true), diff --git a/src/renderer/__tests__/i18n.test.ts b/src/renderer/__tests__/i18n.test.ts index d9e8394..5b8d5b1 100644 --- a/src/renderer/__tests__/i18n.test.ts +++ b/src/renderer/__tests__/i18n.test.ts @@ -16,6 +16,7 @@ const requiredKeys = [ 'result.completionTokens', 'result.totalTokens', 'upload.cancelAnalysis', + 'settings.outputCodeLanguage', ] describe('i18n coverage for analysis metrics', () => { diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index fb8a8d1..2b8be33 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -64,6 +64,8 @@ const PROVIDERS = [ }, ] +const CODE_LANGUAGES: CodeLanguage[] = ['Python', 'C', 'C++', 'Java', 'Go', 'Rust', 'MATLAB', 'R'] + const sectionStyle: React.CSSProperties = { background: 'var(--color-surface)', borderRadius: 'var(--radius-md)', @@ -123,21 +125,26 @@ export default function SettingsPanel({ const [apiKey, setApiKey] = useState('') const [provider, setProvider] = useState(PROVIDERS[0].value) const [model, setModel] = useState(PROVIDERS[0].models[0].value) + const [selectedCodeLanguage, setSelectedCodeLanguage] = useState('Python') const [configured, setConfigured] = useState(false) const [editing, setEditing] = useState(false) const [loading, setLoading] = useState(true) - const applySettings = (s: { apiKey: string; provider: string; model: string }) => { + const applySettings = (s: { apiKey: string; provider: string; model: string; selectedCodeLanguage?: string }) => { const nextProvider = s.provider || PROVIDERS[0].value const providerConfig = PROVIDERS.find((item) => item.value === nextProvider) || PROVIDERS[0] const nextModel = providerConfig.models.some((item) => item.value === s.model) ? s.model : providerConfig.models[0].value + const nextCodeLanguage = CODE_LANGUAGES.includes(s.selectedCodeLanguage as CodeLanguage) + ? s.selectedCodeLanguage as CodeLanguage + : 'Python' const hasKey = s.apiKey.trim().length > 0 setApiKey(s.apiKey) setProvider(providerConfig.value) setModel(nextModel) + setSelectedCodeLanguage(nextCodeLanguage) setConfigured(hasKey) setEditing(!hasKey) onApiKeyConfiguredChange(hasKey) @@ -150,7 +157,7 @@ export default function SettingsPanel({ }) }, []) - const saveConfig = async (next: { provider?: string; model?: string; apiKey?: string }) => { + const saveConfig = async (next: { provider?: string; model?: string; apiKey?: string; selectedCodeLanguage?: CodeLanguage }) => { await window.electronAPI.saveSettings(next) } @@ -170,6 +177,11 @@ export default function SettingsPanel({ await saveConfig({ model: nextModel }) } + const handleCodeLanguageChange = async (nextLanguage: CodeLanguage) => { + setSelectedCodeLanguage(nextLanguage) + await saveConfig({ selectedCodeLanguage: nextLanguage }) + } + const handleSave = async () => { const hasKey = apiKey.trim().length > 0 await saveConfig({ provider, apiKey }) @@ -219,6 +231,17 @@ export default function SettingsPanel({ ))} + + +
{configured diff --git a/src/renderer/components/__tests__/SettingsPanel.test.tsx b/src/renderer/components/__tests__/SettingsPanel.test.tsx index f69b09b..2a9c2ff 100644 --- a/src/renderer/components/__tests__/SettingsPanel.test.tsx +++ b/src/renderer/components/__tests__/SettingsPanel.test.tsx @@ -12,6 +12,7 @@ const baseSettings: Settings = { provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US', + selectedCodeLanguage: 'Python', } function installElectronAPI(settings: Settings = baseSettings) { @@ -70,6 +71,7 @@ describe('SettingsPanel', () => { provider: 'kimi', model: 'kimi-k2.5', language: 'en-US', + selectedCodeLanguage: 'Rust', }) expect(await screen.findByText('API Key configured')).toBeInTheDocument() @@ -105,6 +107,7 @@ describe('SettingsPanel', () => { provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US', + selectedCodeLanguage: 'Python', }) await screen.findByText('API Key configured') @@ -120,13 +123,15 @@ describe('SettingsPanel', () => { provider: 'unknown-provider' as Settings['provider'], model: 'unknown-model', language: 'en-US', + selectedCodeLanguage: 'TypeScript' as Settings['selectedCodeLanguage'], }) await screen.findByText('API Key configured') - const [providerSelect, modelSelect] = screen.getAllByRole('combobox') + const [providerSelect, modelSelect, codeLanguageSelect] = screen.getAllByRole('combobox') expect(providerSelect).toHaveValue('deepseek') expect(modelSelect).toHaveValue('deepseek-v4-flash') + expect(codeLanguageSelect).toHaveValue('Python') }) it('uses the default provider when stored settings omit provider', async () => { @@ -135,6 +140,7 @@ describe('SettingsPanel', () => { provider: '' as Settings['provider'], model: 'deepseek-v4-pro', language: 'en-US', + selectedCodeLanguage: 'Python', }) await screen.findByText('API Key configured') @@ -149,8 +155,8 @@ describe('SettingsPanel', () => { const api = installElectronAPI() vi.mocked(api.getSettings) .mockReset() - .mockResolvedValueOnce({ apiKey: 'sk-deep', provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US' }) - .mockResolvedValueOnce({ apiKey: '', provider: 'kimi', model: 'kimi-k2.6', language: 'en-US' }) + .mockResolvedValueOnce({ apiKey: 'sk-deep', provider: 'deepseek', model: 'deepseek-v4-flash', language: 'en-US', selectedCodeLanguage: 'Python' }) + .mockResolvedValueOnce({ apiKey: '', provider: 'kimi', model: 'kimi-k2.6', language: 'en-US', selectedCodeLanguage: 'Python' }) render( { provider: 'kimi', model: 'kimi-k2.6', language: 'en-US', + selectedCodeLanguage: 'Python', }) await screen.findByText('API Key configured') @@ -186,4 +193,24 @@ describe('SettingsPanel', () => { expect(api.saveSettings).toHaveBeenCalledWith({ model: 'kimi-k2.5' }) expect(modelSelect).toHaveValue('kimi-k2.5') }) + + it('shows and saves the selected output code language', async () => { + const user = userEvent.setup() + const { api } = renderSettingsPanel({ + apiKey: 'sk-existing', + provider: 'deepseek', + model: 'deepseek-v4-flash', + language: 'en-US', + selectedCodeLanguage: 'Go', + }) + + expect(await screen.findByText('Output Code Language')).toBeInTheDocument() + const [, , codeLanguageSelect] = screen.getAllByRole('combobox') + expect(codeLanguageSelect).toHaveValue('Go') + + await user.selectOptions(codeLanguageSelect, 'Rust') + + expect(api.saveSettings).toHaveBeenCalledWith({ selectedCodeLanguage: 'Rust' }) + expect(codeLanguageSelect).toHaveValue('Rust') + }) }) diff --git a/src/renderer/i18n.ts b/src/renderer/i18n.ts index 86b9566..958fac9 100644 --- a/src/renderer/i18n.ts +++ b/src/renderer/i18n.ts @@ -8,6 +8,7 @@ const messages: Record>> = { apiKeyConfigured: 'API Key 已配置', apiKeyMissing: 'API Key 未配置', model: '模型', + outputCodeLanguage: '输出代码语言', apiKey: 'API Key', provider: '模型供应商', save: '保存', @@ -84,6 +85,7 @@ const messages: Record>> = { apiKeyConfigured: 'API Key configured', apiKeyMissing: 'API Key missing', model: 'Model', + outputCodeLanguage: 'Output Code Language', apiKey: 'API Key', provider: 'Provider', save: 'Save', diff --git a/src/renderer/types.d.ts b/src/renderer/types.d.ts index 3b40fbd..f611cae 100644 --- a/src/renderer/types.d.ts +++ b/src/renderer/types.d.ts @@ -1,5 +1,6 @@ type Language = 'zh-CN' | 'en-US' type AnalysisStatus = 'idle' | 'parsing' | 'analyzing' | 'success' | 'error' +type CodeLanguage = 'Python' | 'C' | 'C++' | 'Java' | 'Go' | 'Rust' | 'MATLAB' | 'R' interface TokenUsage { promptTokens?: number @@ -26,8 +27,8 @@ interface AnalysisError { interface ElectronAPI { selectPDF: () => Promise - saveSettings: (settings: { apiKey?: string; provider?: string; model?: string; language?: Language }) => Promise - getSettings: () => Promise<{ apiKey: string; provider: string; model: string; language: Language }> + saveSettings: (settings: { apiKey?: string; provider?: string; model?: string; language?: Language; selectedCodeLanguage?: CodeLanguage }) => Promise + getSettings: () => Promise<{ apiKey: string; provider: string; model: string; language: Language; selectedCodeLanguage: CodeLanguage }> analyzePaper: (pdfPath: string) => Promise cancelAnalysis: () => Promise downloadCoreCode: () => Promise<{ ok: true; path: string } | { ok: false; error: string }>