Skip to content

Commit 33bb035

Browse files
committed
feat: Add web version with working settings menu and API key configuration
- Created web-compatible settings service (webSettingsService.ts) - Added WebSettingsModal component with API key input functionality - Created WebApp component for web-only functionality - Updated geminiService to work with web settings - Added web build script and configuration - Enhanced docs/index.html with modern styling and loading indicator - Added comprehensive documentation for web version - All web functionality works without Electron dependencies - Settings menu allows pasting API keys with validation - Custom prompts management in web version - Responsive design with Material-UI components
1 parent dfdbdda commit 33bb035

39 files changed

Lines changed: 24798 additions & 2139 deletions

.github/workflows/auto-version.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ jobs:
119119
run: |
120120
sudo apt-get update && sudo apt-get install -y jq
121121
COMMITS=$(git log -10 --pretty=format:"%s" | jq -R -s -c '.')
122-
PROMPT="Write concise, user-friendly release notes for these commits: $COMMITS"
122+
PROMPT="Write detailedrelease notes for these commits: $COMMITS"
123123
# Use jq to build the JSON payload safely
124124
jq -n --arg prompt "$PROMPT" \
125125
'{contents: [{parts: [{text: $prompt}]}]}' > payload.json

App.tsx

Lines changed: 475 additions & 1022 deletions
Large diffs are not rendered by default.

WebApp.tsx

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import React, { useState, useCallback, useEffect, useRef } from 'react';
2+
import { generateChatResponses } from './services/geminiService';
3+
import {
4+
Button, Select, MenuItem, Checkbox, FormControlLabel, Snackbar, AppBar, Toolbar, Typography, IconButton, Container, Paper, Box, Modal, CssBaseline
5+
} from '@mui/material';
6+
import type { SavedMessageSet } from './types';
7+
import WebSettingsModal from './src/components/WebSettingsModal';
8+
import { webSettingsService, type WebAppConfig } from './services/webSettingsService';
9+
import { ThemeProvider, createTheme } from '@mui/material/styles';
10+
11+
type CustomPromptType = string | { prompt: string; label?: string; type?: string };
12+
13+
// Minimal ErrorBoundary for runtime safety
14+
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
15+
constructor(props: { children: React.ReactNode }) {
16+
super(props);
17+
this.state = { hasError: false };
18+
}
19+
static getDerivedStateFromError() { return { hasError: true }; }
20+
render() { return this.state.hasError ? <div>Something went wrong.</div> : this.props.children; }
21+
}
22+
23+
function WebApp() {
24+
// --- State ---
25+
const [selectedCustomPrompt] = useState<CustomPromptType>('');
26+
const hotkeyHints = [
27+
{ key: 'Ctrl+Alt+S', label: 'Screenshot' },
28+
{ key: 'Ctrl+Alt+G', label: 'Generate' },
29+
{ key: 'Ctrl+Alt+F', label: 'Funnier' },
30+
{ key: 'Ctrl+Alt+N', label: 'New Image' }
31+
];
32+
const [autoGenerate, setAutoGenerate] = useState(() => {
33+
const saved = localStorage.getItem('autoGenerate');
34+
return saved !== null ? JSON.parse(saved) : true;
35+
});
36+
const [notification, setNotification] = useState<string | undefined>(undefined);
37+
const [isDragging, setIsDragging] = useState(false);
38+
const [showSettings, setShowSettings] = useState(false);
39+
const [customButtonLabel, setCustomButtonLabel] = useState('Custom Action');
40+
const [customActionEnabled, setCustomActionEnabled] = useState(true);
41+
const fileInputRef = useRef<HTMLInputElement>(null);
42+
43+
// Add missing state for screenshotUrl, responses, error, isLoading
44+
const [screenshotUrl, setScreenshotUrl] = useState<string | undefined>(undefined);
45+
const [responses, setResponses] = useState<any>(undefined);
46+
const [error, setError] = useState<string | undefined>(undefined);
47+
const [isLoading, setIsLoading] = useState(false);
48+
49+
// --- Handlers ---
50+
const triggerFileInput = useCallback(() => {
51+
if (fileInputRef.current) fileInputRef.current.click();
52+
}, []);
53+
54+
const handleGeneration = useCallback(
55+
async (
56+
makeItFunnier = false,
57+
imageUrl?: string,
58+
customPromptId?: string,
59+
messageCount = 5
60+
) => {
61+
setIsLoading(true);
62+
setError(undefined);
63+
setResponses(undefined);
64+
const funninessLevel = makeItFunnier ? 2 : 1;
65+
const customPrompt = customPromptId || (typeof selectedCustomPrompt === 'string' ? selectedCustomPrompt : selectedCustomPrompt.prompt);
66+
const imageToUse = imageUrl ?? screenshotUrl ?? undefined;
67+
try {
68+
const response = await generateChatResponses(imageToUse || '', makeItFunnier, customPrompt, messageCount);
69+
setResponses(response);
70+
} catch (err: any) {
71+
setError(err?.message || 'Failed to generate chat messages');
72+
} finally {
73+
setIsLoading(false);
74+
}
75+
}, [selectedCustomPrompt, screenshotUrl]);
76+
77+
const processImageFile = useCallback(
78+
(fileOrUrl: File | string | undefined, _autoGen: boolean) => {
79+
let imageUrl: string | undefined = undefined;
80+
if (!fileOrUrl) {
81+
setError('No image selected');
82+
return;
83+
}
84+
if (typeof fileOrUrl === 'string') {
85+
imageUrl = fileOrUrl;
86+
} else {
87+
imageUrl = URL.createObjectURL(fileOrUrl);
88+
}
89+
setScreenshotUrl(imageUrl);
90+
setError(undefined);
91+
setResponses(undefined);
92+
}, []);
93+
94+
const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
95+
const file = event.target.files?.[0];
96+
if (file) {
97+
processImageFile(file, autoGenerate);
98+
}
99+
}, [processImageFile, autoGenerate]);
100+
101+
const handleDrop = useCallback((event: React.DragEvent) => {
102+
event.preventDefault();
103+
setIsDragging(false);
104+
const files = event.dataTransfer.files;
105+
if (files.length > 0) {
106+
processImageFile(files[0], autoGenerate);
107+
}
108+
}, [processImageFile, autoGenerate]);
109+
110+
const handleDragOver = useCallback((event: React.DragEvent) => {
111+
event.preventDefault();
112+
setIsDragging(true);
113+
}, []);
114+
115+
const handleDragLeave = useCallback((event: React.DragEvent) => {
116+
event.preventDefault();
117+
setIsDragging(false);
118+
}, []);
119+
120+
const resetSelection = useCallback(() => {
121+
setScreenshotUrl(undefined);
122+
setResponses(undefined);
123+
setError(undefined);
124+
}, []);
125+
126+
const handleConfigUpdate = useCallback((config: WebAppConfig) => {
127+
if (typeof config.customActionEnabled === 'boolean') {
128+
setCustomActionEnabled(config.customActionEnabled);
129+
}
130+
if (typeof config.customButtonLabel === 'string') {
131+
setCustomButtonLabel(config.customButtonLabel);
132+
}
133+
}, []);
134+
135+
// Load config on mount
136+
useEffect(() => {
137+
const loadConfig = async () => {
138+
try {
139+
const config = await webSettingsService.getConfig();
140+
if (typeof config.customActionEnabled === 'boolean') {
141+
setCustomActionEnabled(config.customActionEnabled);
142+
}
143+
if (typeof config.customButtonLabel === 'string') {
144+
setCustomButtonLabel(config.customButtonLabel);
145+
}
146+
} catch (error) {
147+
console.error('Failed to load config:', error);
148+
}
149+
};
150+
loadConfig();
151+
}, []);
152+
153+
// Save autoGenerate to localStorage
154+
useEffect(() => {
155+
localStorage.setItem('autoGenerate', JSON.stringify(autoGenerate));
156+
}, [autoGenerate]);
157+
158+
const theme = createTheme({
159+
palette: {
160+
mode: 'dark',
161+
primary: {
162+
main: '#00bcd4',
163+
},
164+
secondary: {
165+
main: '#f50057',
166+
},
167+
},
168+
});
169+
170+
return (
171+
<ErrorBoundary>
172+
<ThemeProvider theme={theme}>
173+
<CssBaseline />
174+
<Box sx={{ flexGrow: 1 }}>
175+
<AppBar position="static" sx={{ backgroundColor: 'grey.900' }}>
176+
<Toolbar>
177+
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
178+
New World Chat AI
179+
</Typography>
180+
<IconButton
181+
color="inherit"
182+
title="Settings"
183+
onClick={() => setShowSettings(true)}
184+
>
185+
<span role="img" aria-label="settings">⚙️</span>
186+
</IconButton>
187+
</Toolbar>
188+
</AppBar>
189+
190+
<Container maxWidth="lg" sx={{ mt: 4 }}>
191+
<Paper
192+
elevation={3}
193+
sx={{
194+
p: 3,
195+
backgroundColor: 'grey.800',
196+
border: isDragging ? '2px dashed #00bcd4' : '2px dashed transparent',
197+
transition: 'border 0.2s ease-in-out'
198+
}}
199+
onDrop={handleDrop}
200+
onDragOver={handleDragOver}
201+
onDragLeave={handleDragLeave}
202+
>
203+
<Box sx={{ textAlign: 'center', mb: 3 }}>
204+
<Typography variant="h5" component="h1" gutterBottom>
205+
New World Chat AI
206+
</Typography>
207+
<Typography variant="body1" color="text.secondary" gutterBottom>
208+
Upload an image or drag and drop to generate chat messages
209+
</Typography>
210+
</Box>
211+
212+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }}>
213+
<input
214+
ref={fileInputRef}
215+
type="file"
216+
accept="image/*"
217+
onChange={handleFileChange}
218+
style={{ display: 'none' }}
219+
aria-label="Select image file"
220+
/>
221+
222+
<Button
223+
variant="contained"
224+
onClick={triggerFileInput}
225+
sx={{ mb: 2 }}
226+
>
227+
📸 Select Image
228+
</Button>
229+
230+
{screenshotUrl && (
231+
<Box sx={{ mt: 2, textAlign: 'center' }}>
232+
<img
233+
src={screenshotUrl}
234+
alt="Screenshot"
235+
style={{ maxWidth: '100%', maxHeight: '300px', borderRadius: '8px' }}
236+
/>
237+
<Box sx={{ mt: 2, display: 'flex', gap: 1, justifyContent: 'center', flexWrap: 'wrap' }}>
238+
<Button
239+
variant="contained"
240+
onClick={() => handleGeneration()}
241+
disabled={isLoading}
242+
>
243+
{isLoading ? 'Generating...' : 'Generate Chat Messages'}
244+
</Button>
245+
<Button
246+
variant="outlined"
247+
onClick={() => handleGeneration(true)}
248+
disabled={isLoading}
249+
>
250+
Make it Funnier
251+
</Button>
252+
<Button
253+
variant="outlined"
254+
onClick={resetSelection}
255+
>
256+
New Image
257+
</Button>
258+
</Box>
259+
</Box>
260+
)}
261+
262+
{error && (
263+
<Typography color="error" sx={{ mt: 2 }}>
264+
{error}
265+
</Typography>
266+
)}
267+
268+
{responses && responses.chatMessages && (
269+
<Box sx={{ mt: 3, width: '100%' }}>
270+
<Typography variant="h6" gutterBottom>
271+
Generated Messages:
272+
</Typography>
273+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
274+
{responses.chatMessages.map((message: any, index: number) => (
275+
<Paper
276+
key={index}
277+
sx={{
278+
p: 2,
279+
backgroundColor: 'grey.700',
280+
cursor: 'pointer',
281+
'&:hover': { backgroundColor: 'grey.600' }
282+
}}
283+
onClick={() => {
284+
navigator.clipboard.writeText(message.text || message.message || message);
285+
setNotification('Message copied to clipboard!');
286+
}}
287+
>
288+
<Typography variant="body1">
289+
{message.text || message.message || message}
290+
</Typography>
291+
</Paper>
292+
))}
293+
</Box>
294+
</Box>
295+
)}
296+
297+
<Box sx={{ mt: 3, textAlign: 'center' }}>
298+
<Typography variant="body2" color="text.secondary">
299+
Hotkeys: {hotkeyHints.map(hint => `${hint.key} - ${hint.label}`).join(', ')}
300+
</Typography>
301+
</Box>
302+
303+
<FormControlLabel
304+
control={
305+
<Checkbox
306+
id="auto-generate-web"
307+
checked={autoGenerate}
308+
onChange={(e) => setAutoGenerate(e.target.checked)}
309+
/>
310+
}
311+
label="Auto-generate on paste"
312+
/>
313+
</Box>
314+
</Paper>
315+
316+
{/* Settings Modal */}
317+
<WebSettingsModal
318+
isOpen={showSettings}
319+
onClose={() => setShowSettings(false)}
320+
onConfigUpdate={handleConfigUpdate}
321+
autoGenerate={autoGenerate}
322+
setAutoGenerate={setAutoGenerate}
323+
customButtonLabel={customButtonLabel}
324+
setCustomButtonLabel={setCustomButtonLabel}
325+
customActionEnabled={customActionEnabled}
326+
setCustomActionEnabled={setCustomActionEnabled}
327+
/>
328+
329+
{/* Notification */}
330+
<Snackbar
331+
open={!!notification}
332+
autoHideDuration={3000}
333+
onClose={() => setNotification(undefined)}
334+
message={notification}
335+
/>
336+
</Container>
337+
</Box>
338+
</ThemeProvider>
339+
</ErrorBoundary>
340+
);
341+
}
342+
343+
export default WebApp;

build/config.gypi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@
414414
"v8_use_siphash": 1,
415415
"want_separate_host_toolset": 1,
416416
"nodedir": "C:\\Users\\lukas\\.electron-gyp\\29.4.6",
417+
"python": "C:\\Program Files\\Python313\\python.exe",
417418
"standalone_static_library": 1,
418419
"msbuild_path": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\MSBuild\\Current\\Bin\\MSBuild.exe",
419420
"build_from_source": "true",

0 commit comments

Comments
 (0)