Skip to content

Commit 96376e2

Browse files
committed
Environment changes, set in Docker run
1 parent 9ad1f5f commit 96376e2

File tree

9 files changed

+120
-74
lines changed

9 files changed

+120
-74
lines changed

.env.template

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
NEXT_PUBLIC_NODE_ENV=development
2+
13
# OpenAI API Key for Text-to-Speech functionality
2-
NEXT_PUBLIC_OPENAI_API_KEY=your_openai_api_key_here
4+
API_KEY=api_key_here_if_needed
35

46
# OpenAI API Base URL (default)
57
# To use a local TTS model server, I suggest using https://github.com/remsky/Kokoro-FastAPI
6-
NEXT_PUBLIC_OPENAI_API_BASE=https://api.openai.com/v1
7-
8-
# Add other environment variables below as needed
9-
NEXT_PUBLIC_NODE_ENV=development
8+
API_BASE=https://api.openai.com/v1

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,21 @@ docker run --name openreader-webui \
4242
-v openreader_docstore:/app/docstore \
4343
richardr1126/openreader-webui:latest
4444
```
45-
> **Note:** The `openreader_docstore` volume is used to store server-side documents. You can mount a local directory instead. Or remove it if you don't need server-side documents.
45+
46+
(Optionally): Set the TTS `API_BASE` URL and/or `API_KEY` to be default for all devices
47+
```bash
48+
docker run --name openreader-webui \
49+
-e API_BASE=http://host.docker.internal:8880/v1 \
50+
-p 3003:3003 \
51+
-v openreader_docstore:/app/docstore \
52+
richardr1126/openreader-webui:latest
53+
```
54+
55+
> Requesting audio from the TTS API happens on the Next.js server not the client. So the base URL for the TTS API should be accessible and relative to the Next.js server. If it is in a Docker you may need to use `host.docker.internal` to access the host machine, instead of `localhost`.
4656
4757
Visit [http://localhost:3003](http://localhost:3003) to run the app and set your settings.
4858

49-
> Requesting audio from the TTS API happens on the Next.js server not the client. So the base URL for the TTS API should be accessible and relative to the Next.js server.
59+
> **Note:** The `openreader_docstore` volume is used to store server-side documents. You can mount a local directory instead. Or remove it if you don't need server-side documents.
5060
5161
### ⬆️ Update Docker Image
5262
```bash
@@ -64,6 +74,8 @@ services:
6474
openreader-webui:
6575
container_name: openreader-webui
6676
image: richardr1126/openreader-webui:latest
77+
environment:
78+
- API_BASE=http://host.docker.internal:8880/v1
6779
ports:
6880
- "3003:3003"
6981
volumes:

src/app/api/tts/route.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import OpenAI from 'openai';
33

44
export async function POST(req: NextRequest) {
55
try {
6-
// Get API credentials from headers
7-
const openApiKey = req.headers.get('x-openai-key');
8-
const openApiBaseUrl = req.headers.get('x-openai-base-url');
6+
// Get API credentials from headers or fall back to environment variables
7+
const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none';
8+
const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE;
99
const { text, voice, speed } = await req.json();
1010
console.log('Received TTS request:', text, voice, speed);
1111

12-
if (!openApiKey || !openApiBaseUrl) {
13-
return NextResponse.json({ error: 'Missing API credentials' }, { status: 401 });
12+
if (!openApiKey) {
13+
return NextResponse.json({ error: 'Missing OpenAI API key' }, { status: 401 });
1414
}
1515

1616
if (!text || !voice || !speed) {
@@ -20,7 +20,7 @@ export async function POST(req: NextRequest) {
2020
// Initialize OpenAI client with abort signal
2121
const openai = new OpenAI({
2222
apiKey: openApiKey,
23-
baseURL: openApiBaseUrl,
23+
baseURL: openApiBaseUrl || 'https://api.openai.com/v1',
2424
});
2525

2626
// Request audio from OpenAI and pass along the abort signal

src/app/api/tts/voices/route.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,12 @@ const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova'
44

55
export async function GET(req: NextRequest) {
66
try {
7-
// Get API credentials from headers
8-
const openApiKey = req.headers.get('x-openai-key');
9-
const openApiBaseUrl = req.headers.get('x-openai-base-url');
10-
11-
if (!openApiKey || !openApiBaseUrl) {
12-
return NextResponse.json({ error: 'Missing API credentials' }, { status: 401 });
13-
}
7+
// Get API credentials from headers or fall back to environment variables
8+
const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none';
9+
const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE;
1410

1511
// Request voices from OpenAI
16-
const response = await fetch(`${openApiBaseUrl}/audio/voices`, {
12+
const response = await fetch(`${openApiBaseUrl || 'https://api.openai.com/v1'}/audio/voices`, {
1713
headers: {
1814
'Authorization': `Bearer ${openApiKey}`,
1915
'Content-Type': 'application/json',

src/components/SettingsModal.tsx

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ export function SettingsModal() {
9191
setShowClearServerConfirm(false);
9292
};
9393

94+
const handleInputChange = (type: 'apiKey' | 'baseUrl', value: string) => {
95+
if (type === 'apiKey') {
96+
setLocalApiKey(value === '' ? '' : value);
97+
} else {
98+
setLocalBaseUrl(value === '' ? '' : value);
99+
}
100+
};
101+
94102
return (
95103
<Button
96104
onClick={() => setIsOpen(true)}
@@ -183,23 +191,35 @@ export function SettingsModal() {
183191
</div>
184192

185193
<div className="space-y-2">
186-
<label className="block text-sm font-medium text-foreground">OpenAI API Key</label>
187-
<Input
188-
type="password"
189-
value={localApiKey}
190-
onChange={(e) => setLocalApiKey(e.target.value)}
191-
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
192-
/>
194+
<label className="block text-sm font-medium text-foreground">
195+
OpenAI API Key
196+
{localApiKey && <span className="ml-2 text-xs text-accent">(Overriding env)</span>}
197+
</label>
198+
<div className="flex gap-2">
199+
<Input
200+
type="password"
201+
value={localApiKey}
202+
onChange={(e) => handleInputChange('apiKey', e.target.value)}
203+
placeholder="Using environment variable"
204+
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
205+
/>
206+
</div>
193207
</div>
194208

195209
<div className="space-y-2">
196-
<label className="block text-sm font-medium text-foreground">OpenAI API Base URL</label>
197-
<Input
198-
type="text"
199-
value={localBaseUrl}
200-
onChange={(e) => setLocalBaseUrl(e.target.value)}
201-
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
202-
/>
210+
<label className="block text-sm font-medium text-foreground">
211+
OpenAI API Base URL
212+
{localBaseUrl && <span className="ml-2 text-xs text-accent">(Overriding env)</span>}
213+
</label>
214+
<div className="flex gap-2">
215+
<Input
216+
type="text"
217+
value={localBaseUrl}
218+
onChange={(e) => handleInputChange('baseUrl', e.target.value)}
219+
placeholder="Using environment variable"
220+
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
221+
/>
222+
</div>
203223
</div>
204224

205225
{isDev && <div className="space-y-2">
@@ -256,7 +276,20 @@ export function SettingsModal() {
256276
</div>
257277
</div>
258278

259-
<div className="mt-6 flex justify-end">
279+
<div className="mt-6 flex justify-end gap-2">
280+
<Button
281+
type="button"
282+
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
283+
font-medium text-foreground hover:bg-background/90 focus:outline-none
284+
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
285+
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent"
286+
onClick={async () => {
287+
setLocalApiKey('');
288+
setLocalBaseUrl('');
289+
}}
290+
>
291+
Reset
292+
</Button>
260293
<Button
261294
type="button"
262295
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
@@ -265,8 +298,8 @@ export function SettingsModal() {
265298
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-background"
266299
onClick={async () => {
267300
await updateConfig({
268-
apiKey: localApiKey,
269-
baseUrl: localBaseUrl
301+
apiKey: localApiKey || '',
302+
baseUrl: localBaseUrl || '',
270303
});
271304
setIsOpen(false);
272305
}}

src/contexts/ConfigContext.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
4-
import { getItem, indexedDBService, setItem } from '@/utils/indexedDB';
4+
import { getItem, indexedDBService, setItem, removeItem } from '@/utils/indexedDB';
55

66
/** Represents the possible view types for document display */
77
export type ViewType = 'single' | 'dual' | 'scroll';
@@ -60,7 +60,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
6060
await indexedDBService.init();
6161
setIsDBReady(true);
6262

63-
// Now load config
63+
// Load config from IndexedDB
6464
const cachedApiKey = await getItem('apiKey');
6565
const cachedBaseUrl = await getItem('baseUrl');
6666
const cachedViewType = await getItem('viewType');
@@ -69,34 +69,24 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
6969
const cachedSkipBlank = await getItem('skipBlank');
7070
const cachedEpubTheme = await getItem('epubTheme');
7171

72-
if (cachedApiKey) console.log('Cached API key found:', cachedApiKey);
73-
if (cachedBaseUrl) console.log('Cached base URL found:', cachedBaseUrl);
74-
if (cachedViewType) console.log('Cached view type found:', cachedViewType);
75-
if (cachedVoiceSpeed) console.log('Cached voice speed found:', cachedVoiceSpeed);
76-
if (cachedVoice) console.log('Cached voice found:', cachedVoice);
77-
if (cachedSkipBlank) console.log('Cached skip blank found:', cachedSkipBlank);
78-
if (cachedEpubTheme) console.log('Cached EPUB theme found:', cachedEpubTheme);
79-
80-
// If not in cache, use env variables
81-
const defaultApiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY || '1234567890';
82-
const defaultBaseUrl = process.env.NEXT_PUBLIC_OPENAI_API_BASE || 'https://api.openai.com/v1';
83-
84-
// Set the values
85-
setApiKey(cachedApiKey || defaultApiKey);
86-
setBaseUrl(cachedBaseUrl || defaultBaseUrl);
72+
// Only set API key and base URL if they were explicitly saved by the user
73+
if (cachedApiKey) {
74+
console.log('Using cached API key');
75+
setApiKey(cachedApiKey);
76+
}
77+
if (cachedBaseUrl) {
78+
console.log('Using cached base URL');
79+
setBaseUrl(cachedBaseUrl);
80+
}
81+
82+
// Set the other values with defaults
8783
setViewType((cachedViewType || 'single') as ViewType);
8884
setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1'));
8985
setVoice(cachedVoice || 'af_sarah');
9086
setSkipBlank(cachedSkipBlank === 'false' ? false : true);
9187
setEpubTheme(cachedEpubTheme === 'true');
9288

93-
// If not in cache, save to cache
94-
if (!cachedApiKey) {
95-
await setItem('apiKey', defaultApiKey);
96-
}
97-
if (!cachedBaseUrl) {
98-
await setItem('baseUrl', defaultBaseUrl);
99-
}
89+
// Only save non-sensitive settings by default
10090
if (!cachedViewType) {
10191
await setItem('viewType', 'single');
10292
}
@@ -119,18 +109,31 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
119109

120110
/**
121111
* Updates multiple configuration values simultaneously
122-
* @param {Partial<{apiKey: string; baseUrl: string}>} newConfig - Object containing new config values
112+
* Only saves API credentials if they are explicitly set
123113
*/
124114
const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => {
125115
try {
126-
if (newConfig.apiKey !== undefined) {
127-
await setItem('apiKey', newConfig.apiKey);
128-
setApiKey(newConfig.apiKey);
116+
if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') {
117+
// Only save API key to IndexedDB if it's different from env default
118+
await setItem('apiKey', newConfig.apiKey!);
119+
setApiKey(newConfig.apiKey!);
120+
}
121+
if (newConfig.baseUrl !== undefined || newConfig.baseUrl !== '') {
122+
// Only save base URL to IndexedDB if it's different from env default
123+
await setItem('baseUrl', newConfig.baseUrl!);
124+
setBaseUrl(newConfig.baseUrl!);
125+
}
126+
127+
// Delete completely if '' is passed
128+
if (newConfig.apiKey === '') {
129+
await removeItem('apiKey');
130+
setApiKey('');
129131
}
130-
if (newConfig.baseUrl !== undefined) {
131-
await setItem('baseUrl', newConfig.baseUrl);
132-
setBaseUrl(newConfig.baseUrl);
132+
if (newConfig.baseUrl === '') {
133+
await removeItem('baseUrl');
134+
setBaseUrl('');
133135
}
136+
134137
} catch (error) {
135138
console.error('Error updating config:', error);
136139
throw error;

src/contexts/TTSContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
383383
* Initializes configuration and fetches available voices
384384
*/
385385
useEffect(() => {
386-
if (!configIsLoading && openApiKey && openApiBaseUrl) {
386+
if (!configIsLoading) {
387387
fetchVoices();
388388
updateVoiceAndSpeed();
389389
}

src/hooks/audio/useVoiceManagement.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ export function useVoiceManagement(apiKey: string | undefined, baseUrl: string |
1414
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
1515

1616
const fetchVoices = useCallback(async () => {
17-
if (!apiKey || !baseUrl) return;
18-
1917
try {
18+
console.log('Fetching voices...');
2019
const response = await fetch('/api/tts/voices', {
2120
headers: {
22-
'x-openai-key': apiKey,
23-
'x-openai-base-url': baseUrl,
21+
'x-openai-key': apiKey || '',
22+
'x-openai-base-url': baseUrl || '',
2423
'Content-Type': 'application/json',
2524
},
2625
});

src/utils/indexedDB.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,10 @@ export async function setItem(key: string, value: string): Promise<void> {
638638
return indexedDBService.setConfigItem(key, value);
639639
}
640640

641+
export async function removeItem(key: string): Promise<void> {
642+
return indexedDBService.removeConfigItem(key);
643+
}
644+
641645
// Add these helper functions before the final export
642646
export async function getLastDocumentLocation(docId: string): Promise<string | null> {
643647
const key = `lastLocation_${docId}`;

0 commit comments

Comments
 (0)