Skip to content

Commit b978e8c

Browse files
committed
Better text extraction
1 parent f7fd360 commit b978e8c

File tree

4 files changed

+121
-66
lines changed

4 files changed

+121
-66
lines changed

src/components/DocumentList.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { usePDF } from '@/contexts/PDFContext';
22
import Link from 'next/link';
33
import { Dialog } from '@headlessui/react';
4-
import { Transition } from '@headlessui/react';
4+
import { Transition, TransitionChild, DialogPanel, DialogTitle } from '@headlessui/react';
55
import { Fragment, useState } from 'react';
66

77
export function DocumentList() {
@@ -102,7 +102,7 @@ export function DocumentList() {
102102
className="relative z-50"
103103
onClose={() => setIsDeleteDialogOpen(false)}
104104
>
105-
<Transition.Child
105+
<TransitionChild
106106
as={Fragment}
107107
enter="ease-out duration-300"
108108
enterFrom="opacity-0"
@@ -112,11 +112,11 @@ export function DocumentList() {
112112
leaveTo="opacity-0"
113113
>
114114
<div className="fixed inset-0 bg-black bg-opacity-25" />
115-
</Transition.Child>
115+
</TransitionChild>
116116

117117
<div className="fixed inset-0 overflow-y-auto">
118118
<div className="flex min-h-full items-center justify-center p-4 text-center">
119-
<Transition.Child
119+
<TransitionChild
120120
as={Fragment}
121121
enter="ease-out duration-300"
122122
enterFrom="opacity-0 scale-95"
@@ -125,13 +125,13 @@ export function DocumentList() {
125125
leaveFrom="opacity-100 scale-100"
126126
leaveTo="opacity-0 scale-95"
127127
>
128-
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-background p-6 text-left align-middle shadow-xl transition-all">
129-
<Dialog.Title
128+
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-background p-6 text-left align-middle shadow-xl transition-all">
129+
<DialogTitle
130130
as="h3"
131131
className="text-lg font-medium leading-6 text-foreground"
132132
>
133133
Delete Document
134-
</Dialog.Title>
134+
</DialogTitle>
135135
<div className="mt-2">
136136
<p className="text-sm text-muted">
137137
Are you sure you want to delete "{documentToDelete?.name}"? This action cannot be undone.
@@ -154,8 +154,8 @@ export function DocumentList() {
154154
Delete
155155
</button>
156156
</div>
157-
</Dialog.Panel>
158-
</Transition.Child>
157+
</DialogPanel>
158+
</TransitionChild>
159159
</div>
160160
</div>
161161
</Dialog>

src/components/SettingsModal.tsx

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

33
import { Fragment } from 'react';
4-
import { Dialog, Transition, Listbox } from '@headlessui/react';
4+
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
55
import { useTheme } from '@/contexts/ThemeContext';
66

77
interface SettingsModalProps {
@@ -22,7 +22,7 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
2222
return (
2323
<Transition appear show={isOpen} as={Fragment}>
2424
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
25-
<Transition.Child
25+
<TransitionChild
2626
as={Fragment}
2727
enter="ease-out duration-300"
2828
enterFrom="opacity-0"
@@ -32,11 +32,11 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
3232
leaveTo="opacity-0"
3333
>
3434
<div className="fixed inset-0 bg-black/25 backdrop-blur-sm" />
35-
</Transition.Child>
35+
</TransitionChild>
3636

3737
<div className="fixed inset-0 overflow-y-auto">
3838
<div className="flex min-h-full items-center justify-center p-4 text-center">
39-
<Transition.Child
39+
<TransitionChild
4040
as={Fragment}
4141
enter="ease-out duration-300"
4242
enterFrom="opacity-0 scale-95"
@@ -45,20 +45,20 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
4545
leaveFrom="opacity-100 scale-100"
4646
leaveTo="opacity-0 scale-95"
4747
>
48-
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
49-
<Dialog.Title
48+
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
49+
<DialogTitle
5050
as="h3"
5151
className="text-lg font-semibold leading-6 text-foreground"
5252
>
5353
Settings
54-
</Dialog.Title>
54+
</DialogTitle>
5555
<div className="mt-4">
5656
<div className="space-y-4">
5757
<div className="space-y-2">
5858
<label className="block text-sm font-medium text-foreground">Theme</label>
5959
<Listbox value={selectedTheme} onChange={(newTheme) => setTheme(newTheme.id)}>
6060
<div className="relative">
61-
<Listbox.Button className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
61+
<ListboxButton className="relative w-full cursor-pointer rounded-lg bg-background py-2 pl-3 pr-10 text-left text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent">
6262
<span className="block truncate">{selectedTheme.name}</span>
6363
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
6464
<svg
@@ -74,16 +74,16 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
7474
/>
7575
</svg>
7676
</span>
77-
</Listbox.Button>
77+
</ListboxButton>
7878
<Transition
7979
as={Fragment}
8080
leave="transition ease-in duration-100"
8181
leaveFrom="opacity-100"
8282
leaveTo="opacity-0"
8383
>
84-
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-background py-1 shadow-lg ring-1 ring-black/5 focus:outline-none">
84+
<ListboxOptions className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-background py-1 shadow-lg ring-1 ring-black/5 focus:outline-none">
8585
{themes.map((theme) => (
86-
<Listbox.Option
86+
<ListboxOption
8787
key={theme.id}
8888
className={({ active }) =>
8989
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
@@ -115,9 +115,9 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
115115
) : null}
116116
</>
117117
)}
118-
</Listbox.Option>
118+
</ListboxOption>
119119
))}
120-
</Listbox.Options>
120+
</ListboxOptions>
121121
</Transition>
122122
</div>
123123
</Listbox>
@@ -137,8 +137,8 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
137137
Close
138138
</button>
139139
</div>
140-
</Dialog.Panel>
141-
</Transition.Child>
140+
</DialogPanel>
141+
</TransitionChild>
142142
</div>
143143
</div>
144144
</Dialog>

src/components/TTSPlayer.tsx

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ const speedOptions = [
3434

3535
export default function TTSPlayer() {
3636
const [isVisible, setIsVisible] = useState(true);
37-
const {
38-
isPlaying,
39-
togglePlay,
40-
skipForward,
41-
skipBackward,
42-
isProcessing,
43-
speed,
37+
const {
38+
isPlaying,
39+
togglePlay,
40+
skipForward,
41+
skipBackward,
42+
isProcessing,
43+
speed,
4444
setSpeedAndRestart,
4545
voice,
4646
setVoiceAndRestart,
@@ -50,12 +50,34 @@ export default function TTSPlayer() {
5050
//console.log(availableVoices);
5151

5252
return (
53-
<div
54-
className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 ${
55-
isVisible ? 'opacity-100' : 'opacity-0'
56-
} transition-opacity duration-300`}
53+
<div
54+
className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 ${isVisible ? 'opacity-100' : 'opacity-0'
55+
} transition-opacity duration-300`}
5756
>
5857
<div className="bg-base dark:bg-base rounded-full shadow-lg px-4 py-1 flex items-center space-x-1 relative">
58+
<div className="relative">
59+
<Listbox value={speed} onChange={setSpeedAndRestart}>
60+
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
61+
<span>{speed}x</span>
62+
<ChevronUpDownIcon className="h-3 w-3" />
63+
</ListboxButton>
64+
<ListboxOptions className="absolute bottom-full mb-1 w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
65+
{speedOptions.map((option) => (
66+
<ListboxOption
67+
key={option.value}
68+
value={option.value}
69+
className={({ active, selected }) =>
70+
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''
71+
} ${selected ? 'font-medium' : ''}`
72+
}
73+
>
74+
{option.label}
75+
</ListboxOption>
76+
))}
77+
</ListboxOptions>
78+
</Listbox>
79+
</div>
80+
5981
<Button
6082
onClick={skipBackward}
6183
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none disabled:opacity-50"
@@ -64,7 +86,7 @@ export default function TTSPlayer() {
6486
>
6587
{isProcessing ? <LoadingSpinner /> : <SkipBackwardIcon />}
6688
</Button>
67-
89+
6890
<Button
6991
onClick={togglePlay}
7092
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase/80 transition-colors duration-200 focus:outline-none"
@@ -82,29 +104,6 @@ export default function TTSPlayer() {
82104
{isProcessing ? <LoadingSpinner /> : <SkipForwardIcon />}
83105
</Button>
84106

85-
<div className="relative">
86-
<Listbox value={speed} onChange={setSpeedAndRestart}>
87-
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
88-
<span>{speed}x</span>
89-
<ChevronUpDownIcon className="h-3 w-3" />
90-
</ListboxButton>
91-
<ListboxOptions className="absolute bottom-full mb-1 w-24 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
92-
{speedOptions.map((option) => (
93-
<ListboxOption
94-
key={option.value}
95-
value={option.value}
96-
className={({ active, selected }) =>
97-
`relative cursor-pointer select-none py-2 px-3 ${
98-
active ? 'bg-offbase' : ''
99-
} ${selected ? 'font-medium' : ''}`
100-
}
101-
>
102-
{option.label}
103-
</ListboxOption>
104-
))}
105-
</ListboxOptions>
106-
</Listbox>
107-
</div>
108107

109108
<div className="relative">
110109
<Listbox value={voice} onChange={setVoiceAndRestart}>
@@ -117,10 +116,8 @@ export default function TTSPlayer() {
117116
<ListboxOption
118117
key={voiceId}
119118
value={voiceId}
120-
className={({ active, selected }) =>
121-
`relative cursor-pointer select-none py-2 px-3 ${
122-
active ? 'bg-offbase' : ''
123-
} ${selected ? 'font-medium' : ''}`
119+
className={({ active, selected }) =>
120+
`relative cursor-pointer select-none py-2 px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium' : ''}`
124121
}
125122
>
126123
<span>{voiceId}</span>

src/contexts/PDFContext.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import { pdfjs } from 'react-pdf';
1515
import stringSimilarity from 'string-similarity';
1616
import nlp from 'compromise';
1717

18+
// Add the correct type import
19+
import type { TextContent, TextItem } from 'pdfjs-dist/types/src/display/api';
20+
1821
// Set worker from public directory
1922
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
2023

@@ -135,11 +138,66 @@ export function PDFProvider({ children }: { children: ReactNode }) {
135138
for (let i = 1; i <= pdf.numPages; i++) {
136139
const page = await pdf.getPage(i);
137140
const textContent = await page.getTextContent();
138-
const pageText = textContent.items.map((item: any) => item.str).join(' ');
139-
fullText += pageText + ' ';
141+
142+
// Filter out non-text items and assert proper type
143+
const textItems = textContent.items.filter((item): item is TextItem =>
144+
'str' in item && 'transform' in item
145+
);
146+
147+
// Group text items into lines based on their vertical position
148+
const tolerance = 2;
149+
const lines: TextItem[][] = [];
150+
let currentLine: TextItem[] = [];
151+
let currentY: number | null = null;
152+
153+
textItems.forEach((item) => {
154+
const y = item.transform[5];
155+
if (currentY === null) {
156+
currentY = y;
157+
currentLine.push(item);
158+
} else if (Math.abs(y - currentY) < tolerance) {
159+
currentLine.push(item);
160+
} else {
161+
lines.push(currentLine);
162+
currentLine = [item];
163+
currentY = y;
164+
}
165+
});
166+
lines.push(currentLine);
167+
168+
// Process each line to build text
169+
let pageText = '';
170+
for (const line of lines) {
171+
// Sort items horizontally within the line
172+
line.sort((a, b) => a.transform[4] - b.transform[4]);
173+
174+
let lineText = '';
175+
let prevItem: TextItem | null = null;
176+
177+
for (const item of line) {
178+
if (!prevItem) {
179+
lineText = item.str;
180+
} else {
181+
const prevEndX = prevItem.transform[4] + (prevItem.width ?? 0);
182+
const currentStartX = item.transform[4];
183+
const space = currentStartX - prevEndX;
184+
185+
// Add space if gap is significant, otherwise concatenate directly
186+
if (space > ((item.width ?? 0) * 0.3)) {
187+
lineText += ' ' + item.str;
188+
} else {
189+
lineText += item.str;
190+
}
191+
}
192+
prevItem = item;
193+
}
194+
pageText += lineText + ' ';
195+
}
196+
197+
fullText += pageText + '\n';
140198
}
141199

142-
return fullText;
200+
return fullText.replace(/\s+/g, ' ').trim();
143201
} catch (error) {
144202
console.error('Error extracting text from PDF:', error);
145203
throw new Error('Failed to extract text from PDF');

0 commit comments

Comments
 (0)