Add compromise, TTSPlayer template
This commit is contained in:
parent
ae8eb084c2
commit
1ce63a8fba
9 changed files with 330 additions and 13 deletions
39
package-lock.json
generated
39
package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"compromise": "^14.14.4",
|
||||
"next": "15.1.5",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -2055,6 +2056,20 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/compromise": {
|
||||
"version": "14.14.4",
|
||||
"resolved": "https://registry.npmjs.org/compromise/-/compromise-14.14.4.tgz",
|
||||
"integrity": "sha512-QdbJwronwxeqb7a5KFK/+Y5YieZ4PE1f7ai0vU58Pp4jih+soDCBMuKVbhDEPQ+6+vI3vSiG4UAAjTAXLJw1Qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"efrt": "2.7.0",
|
||||
"grad-school": "0.0.5",
|
||||
"suffix-thumb": "5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -2310,6 +2325,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/efrt": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/efrt/-/efrt-2.7.0.tgz",
|
||||
"integrity": "sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
|
|
@ -3405,6 +3429,15 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/grad-school": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/grad-school/-/grad-school-0.0.5.tgz",
|
||||
"integrity": "sha512-rXunEHF9M9EkMydTBux7+IryYXEZinRk6g8OBOGDBzo/qWJjhTxy86i5q7lQYpCLHN8Sqv1XX3OIOc7ka2gtvQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
|
|
@ -6084,6 +6117,12 @@
|
|||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/suffix-thumb": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/suffix-thumb/-/suffix-thumb-5.0.2.tgz",
|
||||
"integrity": "sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"compromise": "^14.14.4",
|
||||
"next": "15.1.5",
|
||||
"pdfjs-dist": "4.8.69",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Providers } from "./providers";
|
||||
import { TTSProvider } from "@/context/TTSContext";
|
||||
import TTSPlayer from "@/components/TTSPlayer";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
|
|
@ -29,13 +31,16 @@ export default function RootLayout({
|
|||
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
||||
>
|
||||
<Providers>
|
||||
<div className="min-h-screen bg-background py-4">
|
||||
<div className="max-w-6xl mx-auto px-2">
|
||||
<div className="bg-base rounded-lg shadow-lg">
|
||||
{children}
|
||||
<TTSProvider>
|
||||
<div className="min-h-screen bg-background p-4">
|
||||
<div className="max-w-6xl mx-auto align-center">
|
||||
<div className="bg-base rounded-lg shadow-lg">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TTSPlayer />
|
||||
</TTSProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -13,25 +13,25 @@ export function DocumentList() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full max-w-3xl mx-auto">
|
||||
<h2 className="text-xl font-semibold mb-4 text-foreground">Your Documents</h2>
|
||||
<div className="bg-background rounded-lg shadow p-2 space-y-2">
|
||||
{documents.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex items-center justify-between hover:bg-base p-1 rounded-lg transition-colors"
|
||||
className="flex items-center justify-between hover:bg-base p-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Link
|
||||
href={`/pdf/${encodeURIComponent(doc.id)}`}
|
||||
className="flex items-center space-x-4 flex-1"
|
||||
className="flex items-center space-x-4 flex-1 min-w-0"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="w-8 h-8 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground">{doc.name}</h3>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-foreground truncate">{doc.name}</h3>
|
||||
<p className="text-sm text-muted">
|
||||
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
|
|
@ -39,7 +39,7 @@ export function DocumentList() {
|
|||
</Link>
|
||||
<button
|
||||
onClick={() => removeDocument(doc.id)}
|
||||
className="p-2 text-muted hover:text-accent transition-colors"
|
||||
className="p-2 text-muted hover:text-accent transition-colors flex-shrink-0 ml-2"
|
||||
title="Remove document"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function PDFSkeleton() {
|
|||
{[1, 2, 3].map((index) => (
|
||||
<div key={`skeleton_${index}`}>
|
||||
{/* Page content skeleton */}
|
||||
<div className="flex justify-center my-4">
|
||||
<div className="flex flex-col justify-center my-4">
|
||||
<div className="bg-gray-200 shadow-lg">
|
||||
{/* Approximate dimensions of a PDF page */}
|
||||
<div className="w-[595px] h-[842px]"></div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { PDFSkeleton } from './PDFSkeleton';
|
||||
import { useTTS } from '@/context/TTSContext';
|
||||
|
||||
// Set worker from public directory
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||
|
|
@ -15,11 +16,42 @@ interface PDFViewerProps {
|
|||
|
||||
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
const { setText } = useTTS();
|
||||
const [pdfText, setPdfText] = useState('');
|
||||
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||
setNumPages(numPages);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (pdfFile) {
|
||||
// Load PDF text when file changes
|
||||
const loadPdfText = async () => {
|
||||
try {
|
||||
const pdf = await pdfjs.getDocument(pdfFile).promise;
|
||||
let fullText = '';
|
||||
|
||||
// Get text from all pages
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join(' ');
|
||||
fullText += pageText + ' ';
|
||||
}
|
||||
|
||||
setPdfText(fullText);
|
||||
setText(fullText);
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF text:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadPdfText();
|
||||
}
|
||||
}, [pdfFile, setText]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<Document
|
||||
|
|
|
|||
50
src/components/TTSPlayer.tsx
Normal file
50
src/components/TTSPlayer.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTTS } from '@/context/TTSContext';
|
||||
import { Button } from '@headlessui/react';
|
||||
import {
|
||||
PlayIcon,
|
||||
PauseIcon,
|
||||
SkipForwardIcon,
|
||||
SkipBackwardIcon,
|
||||
} from './icons/Icons';
|
||||
|
||||
export default function TTSPlayer() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const { isPlaying, togglePlay, skipForward, skipBackward } = useTTS();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 ${
|
||||
isVisible ? 'opacity-100' : 'opacity-0'
|
||||
} transition-opacity duration-300`}
|
||||
>
|
||||
<div className="bg-base dark:bg-base rounded-full shadow-lg px-2 py-1 flex items-center space-x-2 relative">
|
||||
<Button
|
||||
onClick={skipBackward}
|
||||
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"
|
||||
aria-label="Skip backward"
|
||||
>
|
||||
<SkipBackwardIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={togglePlay}
|
||||
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"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? <PauseIcon /> : <PlayIcon />}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={skipForward}
|
||||
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"
|
||||
aria-label="Skip forward"
|
||||
>
|
||||
<SkipForwardIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
src/components/icons/Icons.tsx
Normal file
110
src/components/icons/Icons.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
export const PlayIcon = () => (
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="transition-colors"
|
||||
/>
|
||||
<path
|
||||
d="M21.5 15.134c.667.385.667 1.347 0 1.732l-7.5 4.33c-.667.386-1.5-.096-1.5-.866V11.67c0-.77.833-1.252 1.5-.866l7.5 4.33z"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PauseIcon = () => (
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className="transition-colors"
|
||||
/>
|
||||
<rect
|
||||
x="12"
|
||||
y="11"
|
||||
width="2.5"
|
||||
height="10"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
<rect
|
||||
x="17.5"
|
||||
y="11"
|
||||
width="2.5"
|
||||
height="10"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SkipForwardIcon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4l10 8-10 8V4z"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
<rect
|
||||
x="16"
|
||||
y="4"
|
||||
width="2"
|
||||
height="16"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SkipBackwardIcon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 4L8 12l10 8V4z"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
<rect
|
||||
x="6"
|
||||
y="4"
|
||||
width="2"
|
||||
height="16"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
className="transition-colors"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
80
src/context/TTSContext.tsx
Normal file
80
src/context/TTSContext.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import nlp from 'compromise';
|
||||
|
||||
interface TTSContextType {
|
||||
isPlaying: boolean;
|
||||
currentText: string;
|
||||
togglePlay: () => void;
|
||||
skipForward: () => void;
|
||||
skipBackward: () => void;
|
||||
setText: (text: string) => void;
|
||||
currentSentence: string;
|
||||
}
|
||||
|
||||
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||
|
||||
export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentText, setCurrentText] = useState('');
|
||||
const [sentences, setSentences] = useState<string[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const splitIntoSentences = (text: string): string[] => {
|
||||
const doc = nlp(text);
|
||||
// Convert to array and ensure we get strings
|
||||
return doc.sentences().out('array') as string[];
|
||||
};
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
setIsPlaying(prev => !prev);
|
||||
// TODO: Implement actual TTS play/pause logic
|
||||
}, []);
|
||||
|
||||
const skipForward = useCallback(() => {
|
||||
setCurrentIndex(prev => {
|
||||
const nextIndex = Math.min(prev + 1, sentences.length - 1);
|
||||
console.log('Current sentence:', sentences[nextIndex]);
|
||||
return nextIndex;
|
||||
});
|
||||
}, [sentences]);
|
||||
|
||||
const skipBackward = useCallback(() => {
|
||||
setCurrentIndex(prev => {
|
||||
const nextIndex = Math.max(prev - 1, 0);
|
||||
console.log('Current sentence:', sentences[nextIndex]);
|
||||
return nextIndex;
|
||||
});
|
||||
}, [sentences]);
|
||||
|
||||
const setText = useCallback((text: string) => {
|
||||
setCurrentText(text);
|
||||
const newSentences = splitIntoSentences(text);
|
||||
setSentences(newSentences);
|
||||
setCurrentIndex(0);
|
||||
if (newSentences.length > 0) {
|
||||
console.log('Starting sentence:', newSentences[0]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
isPlaying,
|
||||
currentText,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
setText,
|
||||
currentSentence: sentences[currentIndex] || '',
|
||||
};
|
||||
|
||||
return <TTSContext.Provider value={value}>{children}</TTSContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTTS() {
|
||||
const context = useContext(TTSContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTTS must be used within a TTSProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Loading…
Reference in a new issue