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",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"compromise": "^14.14.4",
|
||||||
"next": "15.1.5",
|
"next": "15.1.5",
|
||||||
"pdfjs-dist": "4.8.69",
|
"pdfjs-dist": "4.8.69",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
@ -2055,6 +2056,20 @@
|
||||||
"node": ">= 6"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -2310,6 +2325,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
|
@ -3405,6 +3429,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
|
|
@ -6084,6 +6117,12 @@
|
||||||
"node": ">=16 || 14 >=14.17"
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"compromise": "^14.14.4",
|
||||||
"next": "15.1.5",
|
"next": "15.1.5",
|
||||||
"pdfjs-dist": "4.8.69",
|
"pdfjs-dist": "4.8.69",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
import { TTSProvider } from "@/context/TTSContext";
|
||||||
|
import TTSPlayer from "@/components/TTSPlayer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
|
|
@ -29,13 +31,16 @@ export default function RootLayout({
|
||||||
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
||||||
>
|
>
|
||||||
<Providers>
|
<Providers>
|
||||||
<div className="min-h-screen bg-background py-4">
|
<TTSProvider>
|
||||||
<div className="max-w-6xl mx-auto px-2">
|
<div className="min-h-screen bg-background p-4">
|
||||||
<div className="bg-base rounded-lg shadow-lg">
|
<div className="max-w-6xl mx-auto align-center">
|
||||||
{children}
|
<div className="bg-base rounded-lg shadow-lg">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<TTSPlayer />
|
||||||
|
</TTSProvider>
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,25 @@ export function DocumentList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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>
|
<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">
|
<div className="bg-background rounded-lg shadow p-2 space-y-2">
|
||||||
{documents.map((doc) => (
|
{documents.map((doc) => (
|
||||||
<div
|
<div
|
||||||
key={doc.id}
|
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
|
<Link
|
||||||
href={`/pdf/${encodeURIComponent(doc.id)}`}
|
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">
|
<div className="flex-shrink-0">
|
||||||
<svg className="w-8 h-8 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<h3 className="font-medium text-foreground">{doc.name}</h3>
|
<h3 className="font-medium text-foreground truncate">{doc.name}</h3>
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -39,7 +39,7 @@ export function DocumentList() {
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeDocument(doc.id)}
|
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"
|
title="Remove document"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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) => (
|
{[1, 2, 3].map((index) => (
|
||||||
<div key={`skeleton_${index}`}>
|
<div key={`skeleton_${index}`}>
|
||||||
{/* Page content skeleton */}
|
{/* 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">
|
<div className="bg-gray-200 shadow-lg">
|
||||||
{/* Approximate dimensions of a PDF page */}
|
{/* Approximate dimensions of a PDF page */}
|
||||||
<div className="w-[595px] h-[842px]"></div>
|
<div className="w-[595px] h-[842px]"></div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||||
import 'react-pdf/dist/Page/TextLayer.css';
|
import 'react-pdf/dist/Page/TextLayer.css';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { PDFSkeleton } from './PDFSkeleton';
|
import { PDFSkeleton } from './PDFSkeleton';
|
||||||
|
import { useTTS } from '@/context/TTSContext';
|
||||||
|
|
||||||
// Set worker from public directory
|
// Set worker from public directory
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||||
|
|
@ -15,11 +16,42 @@ interface PDFViewerProps {
|
||||||
|
|
||||||
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
|
const { setText } = useTTS();
|
||||||
|
const [pdfText, setPdfText] = useState('');
|
||||||
|
|
||||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||||
setNumPages(numPages);
|
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 (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Document
|
<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