Skip to content

Commit e86782b

Browse files
committed
feat: improve document list and self-hosting clarity
- Introduce a toggleable grid/list view for the document list, enhancing organization and display flexibility. This involved updates across `DocumentList`, `DocumentFolder`, `DocumentListItem`, and `SortControls`. - Add a new `CodeBlock` component and integrate detailed Docker-based self-hosting instructions into the footer. - Enhance privacy policy popover with clearer details on Deepinfra usage and strongly recommend self-hosting for secure experience. - Implement conditional rendering and feature gating based on the `isDev` environment variable, differentiating features between the production demo and self-hosted instances. Affected components include `page.tsx`, `DocumentSettings.tsx`, `SettingsModal.tsx`, and `config.ts`. - Clarify that advanced features like audiobook export and word-by-word highlighting via `whisper.cpp` are exclusive to self-hosted setups and are disabled in the demo. - Expand the list of supported document types on the homepage to include MD and TXT. - Integrate new `ListIcon`, `GridIcon`, and `CopyIcon` to support UI enhancements. - Add a custom `xs` breakpoint in `tailwind.config.ts` for improved responsive design.
1 parent e39a5b8 commit e86782b

File tree

14 files changed

+279
-84
lines changed

14 files changed

+279
-84
lines changed

src/app/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HomeContent } from '@/components/HomeContent';
22
import { SettingsModal } from '@/components/SettingsModal';
33

4-
// Home page redesigned for fullscreen layout: hero + document area.
4+
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
55

66
export default function Home() {
77
return (
@@ -10,9 +10,9 @@ export default function Home() {
1010
<section className="px-4 pt-6 pb-4 md:pt-10 md:pb-6">
1111
<div className="max-w-5xl mx-auto">
1212
<h1 className="text-2xl md:text-3xl font-bold tracking-tight mb-2 text-foreground">OpenReader WebUI</h1>
13-
<p className="text-sm leading-relaxed max-w-prose text-foreground">
14-
Bring your own text-to-speech API.
15-
<span className="block font-medium">Read & listen to PDF, EPUB & HTML documents with high quality voices.</span>
13+
<p className="text-sm leading-relaxed max-w-[77ch] text-foreground">
14+
Open source document reader web app {isDev ? 'self-hosted server' : 'demo'}.
15+
<span className="block font-medium">Read & listen to PDF, EPUB, MD, and TXT documents with high quality text to speech voices.</span>
1616
</p>
1717
</div>
1818
</section>

src/components/CodeBlock.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { CopyIcon, CheckIcon } from '@/components/icons/Icons';
5+
6+
export function CodeBlock({ children }: { children: string }) {
7+
const [copied, setCopied] = useState(false);
8+
9+
const handleCopy = async () => {
10+
await navigator.clipboard.writeText(children);
11+
setCopied(true);
12+
setTimeout(() => setCopied(false), 2000);
13+
};
14+
15+
return (
16+
<div className="relative group my-2">
17+
<pre className="bg-background p-3 rounded-md overflow-x-auto text-xs text-left
18+
font-mono text-foreground border border-offbase">
19+
<code>{children}</code>
20+
</pre>
21+
<button
22+
onClick={handleCopy}
23+
className="absolute top-2 right-2 p-1.5 rounded-md bg-base hover:bg-offbase hover:text-accent
24+
transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100
25+
hover:scale-[1.05]"
26+
title="Copy to clipboard"
27+
>
28+
{copied ? <CheckIcon className="w-4 h-4" /> : <CopyIcon className="w-4 h-4" />}
29+
</button>
30+
</div>
31+
);
32+
}

src/components/DocumentSettings.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,18 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
141141
leaveTo="opacity-0 scale-95"
142142
>
143143
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
144-
{isDev && !html && <div className="space-y-2 mb-4">
144+
{!html && <div className="space-y-2 mb-4">
145145
<Button
146146
type="button"
147147
className="w-full inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
148148
font-medium text-background hover:bg-secondary-accent focus:outline-none
149149
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
150-
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-background"
150+
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-background
151+
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-[1] disabled:hover:bg-accent"
151152
onClick={() => setIsAudiobookModalOpen(true)}
153+
disabled={!isDev}
152154
>
153-
Export Audiobook
155+
Export Audiobook {!isDev && '(requires self-hosted)'}
154156
</Button>
155157
</div>}
156158

@@ -352,18 +354,18 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
352354
<input
353355
type="checkbox"
354356
checked={pdfWordHighlightEnabled && pdfHighlightEnabled}
355-
disabled={!pdfHighlightEnabled}
357+
disabled={!pdfHighlightEnabled || !isDev}
356358
onChange={(e) =>
357359
updateConfigKey('pdfWordHighlightEnabled', e.target.checked)
358360
}
359-
className="form-checkbox h-4 w-4 text-accent rounded border-muted disabled:opacity-50"
361+
className="form-checkbox h-4 w-4 text-accent rounded border-muted disabled:opacity-50 disabled:cursor-not-allowed"
360362
/>
361363
<span className="text-sm font-medium text-foreground">
362364
Word-by-word
363365
</span>
364366
</label>
365367
<p className="text-sm text-muted pl-6">
366-
Highlight individual words using audio timestamps generated by whisper.cpp
368+
Highlight individual words using audio timestamps generated by whisper.cpp {!isDev && '(requires self-hosted)'}
367369
</p>
368370
</div>
369371
</div>
@@ -389,18 +391,18 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html }: {
389391
<input
390392
type="checkbox"
391393
checked={epubWordHighlightEnabled && epubHighlightEnabled}
392-
disabled={!epubHighlightEnabled}
394+
disabled={!epubHighlightEnabled || !isDev}
393395
onChange={(e) =>
394396
updateConfigKey('epubWordHighlightEnabled', e.target.checked)
395397
}
396-
className="form-checkbox h-4 w-4 text-accent rounded border-muted disabled:opacity-50"
398+
className="form-checkbox h-4 w-4 text-accent rounded border-muted disabled:opacity-50 disabled:cursor-not-allowed"
397399
/>
398400
<span className="text-sm font-medium text-foreground">
399401
Word-by-word
400402
</span>
401403
</label>
402404
<p className="text-sm text-muted pl-6">
403-
Highlight individual words using audio timestamps generated by whisper.cpp
405+
Highlight individual words using audio timestamps generated by whisper.cpp {!isDev && '(requires self-hosted)'}
404406
</p>
405407
</div>
406408
</div>

src/components/EPUBViewer.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,18 +156,21 @@ export function EPUBViewer({ className = '' }: EPUBViewerProps) {
156156
{isTocOpen && tocRef.current && tocRef.current.length > 0 && (
157157
<div className="border-b border-offbase bg-background text-xs overflow-y-auto max-h-64 p-2">
158158
<div className="font-semibold text-muted pb-1">Skip to chapters</div>
159-
<div className="flex flex-wrap gap-1">
159+
<div className="flex flex-wrap gap-1 w-full">
160160
{tocRef.current.map((item, index) => (
161161
<button
162162
key={`${item.href}-${index}`}
163163
type="button"
164164
onClick={() => {
165-
if (item.href) {
166-
handleLocationChanged(item.href);
167-
}
165+
if (item.href) handleLocationChanged(item.href);
168166
setIsTocOpen(false);
169167
}}
170-
className="w-full px-2 py-1 rounded-md text-foreground text-center bg-base hover:bg-offbase hover:text-accent transition-colors duration-150"
168+
className="
169+
px-2 py-1 rounded-md font-medium text-foreground text-center bg-base
170+
hover:bg-offbase hover:text-accent transition-colors duration-150
171+
whitespace-nowrap
172+
flex-1 min-w-[140px]
173+
"
171174
>
172175
{item.label}
173176
</button>

src/components/Footer.tsx

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
22
import { GithubIcon } from '@/components/icons/Icons'
3+
import { CodeBlock } from '@/components/CodeBlock'
4+
5+
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
6+
37

48
export function Footer() {
59
return (
6-
<footer className="m-8 text-sm text-muted">
7-
<div className="flex flex-col items-center space-y-2">
10+
<footer className="m-8 mb-2 text-sm text-muted">
11+
<div className="flex flex-col items-center space-y-4">
812
<div className="flex flex-wrap sm:flex-nowrap items-center justify-center text-center sm:space-x-3">
913
<a
1014
href="https://github.com/richardr1126/OpenReader-WebUI"
@@ -16,15 +20,16 @@ export function Footer() {
1620
</a>
1721
<span className='w-full sm:w-fit'></span>
1822
<Popover className="flex">
19-
<PopoverButton className="hover:text-foreground transition-colors flex items-center gap-1">
23+
<PopoverButton className="font-bold hover:text-foreground transition-colors outline-none flex items-center gap-1">
2024
Privacy info
2125
</PopoverButton>
22-
<PopoverPanel anchor="top" className="bg-base p-4 rounded-lg shadow-lg w-64">
23-
<p>Documents are uploaded to your local browser cache.</p>
24-
<p className='mt-3'>Each sentence of the document you are viewing is sent to my Kokoro-FastAPI server for audio generation.</p>
25-
<p className='mt-3'>The audio is streamed back to your browser and played in real-time.</p>
26+
<PopoverPanel anchor="top" className="bg-base p-4 rounded-lg shadow-xl border border-offbase z-50">
27+
<p className='max-w-xs'>Documents are uploaded to your local browser cache.</p>
28+
<p className='mt-3 max-w-xs'>Each paragraph of the document you are viewing is sent to Deepinfra for audio generation through a Vercel backend proxy, containing a shared caching pool.</p>
29+
<p className='mt-3 max-w-xs'>The audio is streamed back to your browser and played in real-time.</p>
30+
<p className='mt-3 max-w-xs font-bold'><em>Self-hosting is the recommended way to use this app for a truly secure experience.</em></p>
2631
{/* Vercel analytics disclaimer */}
27-
<p className='mt-3'>This site uses Vercel Analytics to collect anonymous usage data to help improve the service.</p>
32+
<p className='mt-3 max-w-xs'>This site uses Vercel Analytics to collect anonymous usage data to help improve the service.</p>
2833
</PopoverPanel>
2934
</Popover>
3035
<span className='w-full sm:w-fit'></span>
@@ -34,7 +39,7 @@ export function Footer() {
3439
href="https://huggingface.co/hexgrad/Kokoro-82M"
3540
target="_blank"
3641
rel="noopener noreferrer"
37-
className="font-bold hover:text-foreground transition-colors"
42+
className="font-bold hover:text-foreground transition-colors underline decoration-dotted underline-offset-4"
3843
>
3944
hexgrad/Kokoro-82M
4045
</a>
@@ -43,12 +48,66 @@ export function Footer() {
4348
href="https://deepinfra.com/models?type=text-to-speech"
4449
target="_blank"
4550
rel="noopener noreferrer"
46-
className="font-bold hover:text-foreground transition-colors"
51+
className="font-bold hover:text-foreground transition-colors underline decoration-dotted underline-offset-4"
4752
>
4853
Deepinfra
4954
</a>
5055
</span>
5156
</div>
57+
<div className='font-medium text-center flex items-center justify-center gap-1'>
58+
<span>This is a demo app (</span>
59+
<Popover className="relative">
60+
<PopoverButton className="font-bold hover:text-foreground transition-colors outline-none">
61+
self-host
62+
</PopoverButton>
63+
<PopoverPanel anchor="top" className="bg-base p-6 rounded-xl shadow-2xl border border-offbase w-[90vw] max-w-3xl z-50 backdrop-blur-md flex flex-col gap-4">
64+
<div className="space-y-4 font-medium">
65+
<h3 className="text-lg font-bold text-foreground">Self-Hosting Instructions</h3>
66+
67+
<div>
68+
<p className="mb-2 font-medium">
69+
1. Start the <a href="https://github.com/remsky/Kokoro-FastAPI" target="_blank" rel="noopener noreferrer" className="text-muted hover:text-foreground underline decoration-dotted underline-offset-4">Kokoro-FastAPI</a> container
70+
</p>
71+
<CodeBlock>
72+
{
73+
`docker run -d \\
74+
--name kokoro-tts \\
75+
--restart unless-stopped \\
76+
-p 8880:8880 \\
77+
-e ONNX_NUM_THREADS=8 \\
78+
-e ONNX_INTER_OP_THREADS=4 \\
79+
-e ONNX_EXECUTION_MODE=parallel \\
80+
-e ONNX_OPTIMIZATION_LEVEL=all \\
81+
-e ONNX_MEMORY_PATTERN=true \\
82+
-e ONNX_ARENA_EXTEND_STRATEGY=kNextPowerOfTwo \\
83+
-e API_LOG_LEVEL=DEBUG \\
84+
ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4`
85+
}
86+
</CodeBlock>
87+
</div>
88+
89+
<div>
90+
<p className="mb-2 text-foreground font-medium">2. Start OpenReader WebUI container</p>
91+
<CodeBlock>
92+
{
93+
`docker run --name openreader-webui --rm \\
94+
-e API_BASE=http://kokoro-tts:8880/v1 \\
95+
-p 3003:3003 \\
96+
-v openreader_docstore:/app/docstore \\
97+
ghcr.io/richardr1126/openreader-webui:latest`
98+
}
99+
</CodeBlock>
100+
</div>
101+
102+
<p>
103+
Visit <a href="http://localhost:3003" target="_blank" rel="noopener noreferrer" className="text-muted hover:text-foreground transition-colors underline decoration-dotted underline-offset-4">http://localhost:3003</a> to run the app and set your settings.
104+
{' '}See the <a href="https://github.com/richardr1126/OpenReader-WebUI#readme" target="_blank" rel="noopener noreferrer" className="text-muted hover:text-foreground transition-colors underline decoration-dotted underline-offset-4">README</a> for more details.
105+
</p>
106+
</div>
107+
</PopoverPanel>
108+
</Popover>
109+
<span> for full functionality)</span>
110+
</div>
52111
</div>
53112
</footer>
54113
)

src/components/SettingsModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ export function SettingsModal() {
400400
type="password"
401401
value={localApiKey}
402402
onChange={(e) => handleInputChange('apiKey', e.target.value)}
403-
placeholder={!isDev && localTTSProvider === 'deepinfra' ? "Deepinfra free or override apikey" : "Using environment variable"}
403+
placeholder={!isDev && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
404404
className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
405405
/>
406406
</div>

src/components/doclist/DocumentFolder.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use client";
2-
31
import { useState, DragEvent } from 'react';
42
import { Button, Transition } from '@headlessui/react';
53
import { DocumentListItem } from './DocumentListItem';
@@ -16,6 +14,7 @@ interface DocumentFolderProps {
1614
onDragStart: (doc: DocumentListDocument) => void;
1715
onDragEnd: () => void;
1816
onDrop: (e: DragEvent, folderId: string) => void;
17+
viewMode: 'list' | 'grid';
1918
}
2019

2120
const ChevronIcon = ({ className = "w-4 h-4" }) => (
@@ -39,6 +38,7 @@ export function DocumentFolder({
3938
onDragStart,
4039
onDragEnd,
4140
onDrop,
41+
viewMode,
4242
}: DocumentFolderProps) {
4343
const [isHovering, setIsHovering] = useState(false);
4444
const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId && draggedDoc.id !== folder.id;
@@ -64,7 +64,7 @@ export function DocumentFolder({
6464
if (!draggedDoc || draggedDoc.folderId) return;
6565
onDrop(e, folder.id);
6666
}}
67-
className={`overflow-hidden rounded-md border border-offbase ${isDropTarget ? 'ring-2 ring-accent' : ''}`}
67+
className={`w-full overflow-hidden rounded-md border border-offbase ${isDropTarget ? 'ring-2 ring-accent' : ''}`}
6868
>
6969
<div className='flex flex-row justify-between p-0'>
7070
<div className="w-full">
@@ -104,7 +104,7 @@ export function DocumentFolder({
104104
leaveFrom="transform scale-y-100 opacity-100 max-h-[1000px]"
105105
leaveTo="transform scale-y-0 opacity-0 max-h-0"
106106
>
107-
<div id={`folder-panel-${folder.id}`} className="space-y-1 origin-top">
107+
<div id={`folder-panel-${folder.id}`} className={`${viewMode === 'grid' ? "flex flex-wrap gap-1" : "space-y-1"} w-full origin-top`}>
108108
{sortedDocuments.map(doc => (
109109
<DocumentListItem
110110
key={`${doc.type}-${doc.id}`}
@@ -114,6 +114,7 @@ export function DocumentFolder({
114114
onDragStart={onDragStart}
115115
onDragEnd={onDragEnd}
116116
isDropTarget={false}
117+
viewMode={viewMode}
117118
/>
118119
))}
119120
</div>

0 commit comments

Comments
 (0)