Skip to content

Commit f5e28a0

Browse files
committed
feat: implement preset setting (close: #57)
1 parent f3b5b45 commit f5e28a0

File tree

7 files changed

+368
-12
lines changed

7 files changed

+368
-12
lines changed

src/main/main.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919

2020
import { UTIOService } from './services/utio';
2121
import { store } from './store/create';
22+
import { SettingStore } from './store/setting';
2223
import { createTray } from './tray';
2324

2425
const { isProd } = env;
@@ -143,6 +144,19 @@ const initializeApp = async () => {
143144
app.on('quit', unsubscribe);
144145

145146
logger.info('initializeApp end');
147+
148+
// Check and update remote presets
149+
const settings = SettingStore.getStore();
150+
if (
151+
settings.presetSource?.type === 'remote' &&
152+
settings.presetSource.autoUpdate
153+
) {
154+
try {
155+
await SettingStore.importPresetFromUrl(settings.presetSource.url!, true);
156+
} catch (error) {
157+
logger.error('Failed to update preset:', error);
158+
}
159+
}
146160
};
147161

148162
/**
@@ -152,6 +166,34 @@ const registerIPCHandlers = () => {
152166
ipcMain.handle('utio:shareReport', async (_, params) => {
153167
await UTIOService.getInstance().shareReport(params);
154168
});
169+
170+
ipcMain.handle('utio:importPresetFromFile', async (_, yamlContent) => {
171+
await SettingStore.importPresetFromText(yamlContent);
172+
return SettingStore.getStore();
173+
});
174+
175+
ipcMain.handle('utio:importPresetFromUrl', async (_, url, autoUpdate) => {
176+
await SettingStore.importPresetFromUrl(url, autoUpdate);
177+
return SettingStore.getStore();
178+
});
179+
180+
ipcMain.handle('utio:updatePresetFromRemote', async () => {
181+
const settings = SettingStore.getStore();
182+
if (settings.presetSource?.type === 'remote' && settings.presetSource.url) {
183+
await SettingStore.importPresetFromUrl(
184+
settings.presetSource.url,
185+
settings.presetSource.autoUpdate,
186+
);
187+
return SettingStore.getStore();
188+
} else {
189+
throw new Error('No remote preset configured');
190+
}
191+
});
192+
193+
ipcMain.handle('utio:resetPreset', async () => {
194+
SettingStore.resetPreset();
195+
return SettingStore.getStore();
196+
});
155197
};
156198

157199
/**
@@ -185,3 +227,5 @@ app
185227
logger.info('app.whenReady end');
186228
})
187229
.catch(console.log);
230+
231+
// ... 保留其他代码 ...

src/main/store/setting.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import ElectronStore from 'electron-store';
6+
import yaml from 'js-yaml';
7+
import z from 'zod';
68

79
import * as env from '@main/env';
10+
import { logger } from '@main/logger';
811

912
import { LocalStore, VlmProvider } from './types';
13+
import { validatePreset } from './validate';
1014

1115
export class SettingStore {
1216
private static instance = new ElectronStore<LocalStore>({
@@ -51,4 +55,58 @@ export class SettingStore {
5155
public static openInEditor(): void {
5256
SettingStore.instance.openInEditor();
5357
}
58+
59+
public static async importPresetFromUrl(
60+
url: string,
61+
autoUpdate = false,
62+
): Promise<void> {
63+
try {
64+
const response = await fetch(url);
65+
if (!response.ok) {
66+
throw new Error(`Failed to fetch preset: ${response.status}`);
67+
}
68+
69+
const yamlText = await response.text();
70+
const preset = yaml.load(yamlText);
71+
const validatedPreset = validatePreset(preset);
72+
73+
SettingStore.setStore({
74+
...validatedPreset,
75+
presetSource: {
76+
type: 'remote',
77+
url,
78+
autoUpdate,
79+
lastUpdated: Date.now(),
80+
},
81+
});
82+
} catch (error) {
83+
logger.error(error);
84+
throw new Error(`Failed to import preset: ${error.message}`);
85+
}
86+
}
87+
88+
public static async importPresetFromText(yamlText: string): Promise<void> {
89+
try {
90+
const preset = yaml.load(yamlText);
91+
const validatedPreset = validatePreset(preset);
92+
console.log('validatedPreset', validatedPreset);
93+
94+
SettingStore.setStore({
95+
...validatedPreset,
96+
presetSource: {
97+
type: 'local',
98+
lastUpdated: Date.now(),
99+
},
100+
});
101+
} catch (error) {
102+
logger.error(error);
103+
throw new Error(`Failed to import preset: ${error.message}`);
104+
}
105+
}
106+
107+
public static resetPreset(): void {
108+
const store = SettingStore.getStore();
109+
const { presetSource, ...settings } = store;
110+
SettingStore.setStore(settings);
111+
}
54112
}

src/main/store/types.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { ComputerUseUserData, Conversation } from '@ui-tars/shared/types/data';
66

77
import { SettingStore } from './setting';
8+
import { LocalStore, PresetSource } from './validate';
89

910
export type NextAction =
1011
| { type: 'key'; text: string }
@@ -57,13 +58,4 @@ export enum VlmProvider {
5758
vLLM = 'vLLM',
5859
}
5960

60-
export type LocalStore = {
61-
language: 'zh' | 'en';
62-
vlmProvider: VlmProvider;
63-
vlmBaseUrl: string;
64-
vlmApiKey: string;
65-
vlmModelName: string;
66-
screenshotScale: number; // 0.1 ~ 1.0
67-
reportStorageBaseUrl?: string;
68-
utioBaseUrl?: string;
69-
};
61+
export type { PresetSource, LocalStore };

src/main/store/validate.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { z } from 'zod';
6+
7+
import { VlmProvider } from './types';
8+
9+
const PresetSourceSchema = z.object({
10+
type: z.enum(['local', 'remote']),
11+
url: z.string().url().optional(),
12+
autoUpdate: z.boolean().optional(),
13+
lastUpdated: z.number().optional(),
14+
});
15+
16+
export const PresetSchema = z.object({
17+
// Required fields
18+
language: z.enum(['zh', 'en']),
19+
vlmProvider: z.nativeEnum(VlmProvider),
20+
vlmBaseUrl: z.string().url(),
21+
vlmApiKey: z.string().min(1),
22+
vlmModelName: z.string().min(1),
23+
24+
// Optional fields
25+
screenshotScale: z.number().min(0.1).max(1).optional().default(1),
26+
reportStorageBaseUrl: z.string().url().optional(),
27+
utioBaseUrl: z.string().url().optional(),
28+
presetSource: PresetSourceSchema.optional(),
29+
});
30+
31+
export type PresetSource = z.infer<typeof PresetSourceSchema>;
32+
export type LocalStore = z.infer<typeof PresetSchema>;
33+
34+
export const validatePreset = (data: unknown): LocalStore => {
35+
return PresetSchema.parse(data);
36+
};

src/preload/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ const electronHandler = {
3838
utio: {
3939
shareReport: (params: UTIOPayload<'shareReport'>) =>
4040
ipcRenderer.invoke('utio:shareReport', params),
41+
importPresetFromFile: (yamlContent: string) =>
42+
ipcRenderer.invoke('utio:importPresetFromFile', yamlContent),
43+
importPresetFromUrl: (url: string, autoUpdate: boolean) =>
44+
ipcRenderer.invoke('utio:importPresetFromUrl', url, autoUpdate),
45+
updatePresetFromRemote: () =>
46+
ipcRenderer.invoke('utio:updatePresetFromRemote'),
47+
resetPreset: () => ipcRenderer.invoke('utio:resetPreset'),
4148
},
4249
};
4350

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import {
2+
Box,
3+
Button,
4+
FormControl,
5+
FormLabel,
6+
Input,
7+
Modal,
8+
ModalBody,
9+
ModalCloseButton,
10+
ModalContent,
11+
ModalFooter,
12+
ModalHeader,
13+
ModalOverlay,
14+
Switch,
15+
Tab,
16+
TabList,
17+
TabPanel,
18+
TabPanels,
19+
Tabs,
20+
Text,
21+
VStack,
22+
useToast,
23+
} from '@chakra-ui/react';
24+
import { useRef, useState } from 'react';
25+
26+
interface PresetImportProps {
27+
isOpen: boolean;
28+
onClose: () => void;
29+
}
30+
31+
export function PresetImport({ isOpen, onClose }: PresetImportProps) {
32+
const [remoteUrl, setRemoteUrl] = useState('');
33+
const [autoUpdate, setAutoUpdate] = useState(true);
34+
const fileInputRef = useRef<HTMLInputElement>(null);
35+
const toast = useToast();
36+
37+
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
38+
const file = e.target.files?.[0];
39+
if (!file) return;
40+
41+
try {
42+
// Use FileReader to read file contents
43+
const reader = new FileReader();
44+
const yamlText = await new Promise<string>((resolve, reject) => {
45+
reader.onload = () => resolve(reader.result as string);
46+
reader.onerror = () => reject(reader.error);
47+
reader.readAsText(file);
48+
});
49+
50+
// Send text content via IPC
51+
await window.electron.utio.importPresetFromFile(yamlText);
52+
53+
toast({
54+
title: 'Preset imported successfully',
55+
status: 'success',
56+
duration: 2000,
57+
});
58+
onClose();
59+
} catch (error) {
60+
toast({
61+
title: 'Failed to import preset',
62+
description: error.message,
63+
status: 'error',
64+
duration: 3000,
65+
});
66+
}
67+
};
68+
69+
const handleRemoteImport = async () => {
70+
try {
71+
await window.electron.utio.importPresetFromUrl(remoteUrl, autoUpdate);
72+
toast({
73+
title: 'Preset imported successfully',
74+
status: 'success',
75+
duration: 2000,
76+
});
77+
onClose();
78+
} catch (error) {
79+
toast({
80+
title: 'Failed to import preset',
81+
description: error.message,
82+
status: 'error',
83+
duration: 3000,
84+
});
85+
}
86+
};
87+
88+
return (
89+
<Modal isOpen={isOpen} onClose={onClose}>
90+
<ModalOverlay />
91+
<ModalContent>
92+
<ModalHeader>Import Preset</ModalHeader>
93+
<ModalCloseButton />
94+
<ModalBody>
95+
<Tabs>
96+
<TabList>
97+
<Tab>Local File</Tab>
98+
<Tab>Remote URL</Tab>
99+
</TabList>
100+
<TabPanels>
101+
<TabPanel>
102+
<VStack spacing={4}>
103+
<Text>Select a YAML file to import settings preset</Text>
104+
<input
105+
type="file"
106+
accept=".yaml,.yml"
107+
ref={fileInputRef}
108+
style={{ display: 'none' }}
109+
onChange={handleFileChange}
110+
/>
111+
<Button onClick={() => fileInputRef.current?.click()}>
112+
Choose File
113+
</Button>
114+
</VStack>
115+
</TabPanel>
116+
<TabPanel>
117+
<VStack spacing={4}>
118+
<FormControl>
119+
<FormLabel>Preset URL</FormLabel>
120+
<Input
121+
value={remoteUrl}
122+
onChange={(e) => setRemoteUrl(e.target.value)}
123+
placeholder="https://example.com/preset.yaml"
124+
/>
125+
</FormControl>
126+
<FormControl display="flex" alignItems="center">
127+
<FormLabel mb="0">Auto update on startup</FormLabel>
128+
<Switch
129+
isChecked={autoUpdate}
130+
onChange={(e) => setAutoUpdate(e.target.checked)}
131+
/>
132+
</FormControl>
133+
</VStack>
134+
</TabPanel>
135+
</TabPanels>
136+
</Tabs>
137+
</ModalBody>
138+
<ModalFooter>
139+
<Button variant="ghost" mr={3} onClick={onClose}>
140+
Cancel
141+
</Button>
142+
<Button
143+
variant="tars-ghost"
144+
onClick={handleRemoteImport}
145+
isDisabled={!remoteUrl}
146+
>
147+
Import
148+
</Button>
149+
</ModalFooter>
150+
</ModalContent>
151+
</Modal>
152+
);
153+
}

0 commit comments

Comments
 (0)