diff --git a/package-lock.json b/package-lock.json index 0ca71fe..652f579 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 1e2c62a..f133f6f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6d67d8a..88612d6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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`} > -
-
-
- {children} + +
+
+
+ {children} +
-
+ + diff --git a/src/components/DocumentList.tsx b/src/components/DocumentList.tsx index 4e553db..1cf7cbb 100644 --- a/src/components/DocumentList.tsx +++ b/src/components/DocumentList.tsx @@ -13,25 +13,25 @@ export function DocumentList() { } return ( -
+

Your Documents

{documents.map((doc) => (
-
-

{doc.name}

+
+

{doc.name}

{(doc.size / 1024 / 1024).toFixed(2)} MB

@@ -39,7 +39,7 @@ export function DocumentList() { + + + + +
+
+ ); +} diff --git a/src/components/icons/Icons.tsx b/src/components/icons/Icons.tsx new file mode 100644 index 0000000..7095729 --- /dev/null +++ b/src/components/icons/Icons.tsx @@ -0,0 +1,110 @@ +export const PlayIcon = () => ( + + + + +); + +export const PauseIcon = () => ( + + + + + +); + +export const SkipForwardIcon = () => ( + + + + +); + +export const SkipBackwardIcon = () => ( + + + + +); diff --git a/src/context/TTSContext.tsx b/src/context/TTSContext.tsx new file mode 100644 index 0000000..d6948bb --- /dev/null +++ b/src/context/TTSContext.tsx @@ -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(undefined); + +export function TTSProvider({ children }: { children: React.ReactNode }) { + const [isPlaying, setIsPlaying] = useState(false); + const [currentText, setCurrentText] = useState(''); + const [sentences, setSentences] = useState([]); + 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 {children}; +} + +export function useTTS() { + const context = useContext(TTSContext); + if (context === undefined) { + throw new Error('useTTS must be used within a TTSProvider'); + } + return context; +}