Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
Joseph J Helfenbein
2025-01-26 06:34:04 -05:00
8 changed files with 605 additions and 345 deletions

199
README.md
View File

@@ -1,36 +1,191 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
<!-- Improved compatibility of back to top link: See: https://github.com/othneildrew/Best-README-Template/pull/73 -->
<a id="readme-top"></a>
<!--
*** Thanks for checking out the Best-README-Template. If you have a suggestion
*** that would make this better, please fork the repo and create a pull request
*** or simply open an issue with the tag "enhancement".
*** Don't forget to give the project a star!
*** Thanks again! Now go create something AMAZING! :D
-->
<!-- PROJECT SHIELDS -->
<!--
*** I'm using markdown "reference style" links for readability.
*** Reference links are enclosed in brackets [ ] instead of parentheses ( ).
*** See the bottom of this document for the declaration of the reference variables
*** for contributors-url, forks-url, etc. This is an optional, concise syntax you may use.
*** https://www.markdownguide.org/basic-syntax/#reference-style-links
-->
[![Contributors][contributors-shield]][contributors-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![Apache License][license-shield]][license-url]
<!-- PROJECT LOGO -->
<br />
<div align="center">
<a href="https://github.com/GamerBoss101/HoyaHax2025">
<img src="/public/HoyaHax2025-icon.svg" alt="Logo" width="80" height="80">
</a>
<h3 align="center">PatSafe</h3>
<p align="center">
Bridging the gap between doctors and patients for seamless post-discharge care.
<br />
<br />
<a href="https://www.patsafe.co">Visit</a>
·
<a href="https://github.com/GamerBoss101/HoyaHax2025/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
·
<a href="https://github.com/GamerBoss101/HoyaHax2025/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</p>
</div>
<!-- ABOUT THE PROJECT -->
## About the Project
PatSafe is a web-based application that connects discharged patients with their doctors for post-discharge care. It features a doctors dashboard where they can update patient information, provide medication instructions, and track patient progress. Patients can report their symptoms, recovery status, and medication adherence, which is then updated in real time for their doctor to review. Additionally, PatSafe includes an AI-powered chatbot that allows patients to ask medical questions and report symptoms, helping doctors get insights into the patients condition remotely. The chatbot also aids in tracking medication adherence and offering information for diagnoses and prescriptions.
### How does it work?
PatSafe was built using **Next.js** and **React.js** for the frontend, while the backend is powered by **MongoDB** and **Clerk** for authentication. The platform utilizes APIs for real-time updates and features such as medication tracking and symptom reporting. We integrated a chatbot powered by **Hugging Face models** for natural language processing, allowing users to interact conversationally and gather helpful medical information.
### Built With
* [![Next.js][Next.js]][Next-url]
* [![React][React.js]][React-url]
* [![Tailwind][Tailwind]][Tailwind-url]
* [![Clerk][Clerk]][Clerk-url]
* [![MongoDB][MongoDB]][MongoDB-url]
* [![HuggingFace][HuggingFace]][HuggingFace-url]
* [![OpenAI][OpenAI]][OpenAI]
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
First, run the development server:
Here are the steps to run the project locally if you want to develop your own project.
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
### Prerequisites
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
* pnpm
```sh
pnpm self-update
```
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
### Installation
## Learn More
1. Fork the repository and set it up as a project on Vercel or another hosting platform
To learn more about Next.js, take a look at the following resources:
2. Install packages
```sh
pnpm install
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
3. You can run the website locally with
```sh
npm run dev
```
or, if hosting on Vercel, with
```sh
vercel dev
```
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
<!-- LICENSE -->
## License
Distributed under the Apache 2.0 License. See `LICENSE.txt` for more information.
* [Best README Template](https://github.com/othneildrew/Best-README-Template)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/GamerBoss101/HoyaHax2025.svg?style=for-the-badge
[contributors-url]: https://github.com/GamerBoss101/HoyaHax2025/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/GamerBoss101/HoyaHax2025.svg?style=for-the-badge
[forks-url]: https://github.com/GamerBoss101/HoyaHax2025/network/members
[stars-shield]: https://img.shields.io/github/stars/GamerBoss101/HoyaHax2025.svg?style=for-the-badge
[stars-url]: https://github.com/GamerBoss101/HoyaHax2025/stargazers
[issues-shield]: https://img.shields.io/github/issues/GamerBoss101/HoyaHax2025.svg?style=for-the-badge
[issues-url]: https://github.com/GamerBoss101/HoyaHax2025/issues
[license-shield]: https://img.shields.io/github/license/GamerBoss101/HoyaHax2025.svg?style=for-the-badge
[license-url]: https://github.com/GamerBoss101/HoyaHax2025/blob/master/LICENSE.txt
[linkedin-shield]: https://img.shields.io/badge/LinkedIn-0A66C2.svg?style=for-the-badge&logo=linkedin&logoColor=white
[linkedin-url-joseph]: https://linkedin.com/in/joseph-j-helfenbein
[product-screenshot]: images/screenshot.png
[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
[Next-url]: https://nextjs.org/
[React.js]: https://img.shields.io/badge/React.js-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
[React-url]: https://reactjs.org/
[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D
[Vue-url]: https://vuejs.org/
[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white
[Angular-url]: https://angular.io/
[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00
[Svelte-url]: https://svelte.dev/
[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white
[Laravel-url]: https://laravel.com
[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
[Bootstrap-url]: https://getbootstrap.com
[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white
[JQuery-url]: https://jquery.com
[Expo]: https://img.shields.io/badge/expo-000000?style=for-the-badge&logo=expo&logoColor=white
[Expo-url]: https://expo.dev/
[Flask]: https://img.shields.io/badge/flask-4590A1?logo=flask&style=for-the-badge&logoColor=white
[Flask-url]: https://flask.palletsprojects.com/en/3.0.x/
[JavaScript]: https://img.shields.io/badge/javascript-yellow?logo=javascript&style=for-the-badge&logoColor=white
[JavaScript-url]: https://developer.oracle.com/languages/javascript.html
[ThreeJS]: https://img.shields.io/badge/three.js-black?logo=three.js&style=for-the-badge&logoColor=white
[ThreeJS-url]: https://threejs.org/
[TypeScript]: https://img.shields.io/badge/typescript-3178C6?logo=typescript&style=for-the-badge&logoColor=white
[TypeScript-url]: https://www.typescriptlang.org/
[Python]: https://img.shields.io/badge/python-3776AB?style=for-the-badge&logo=python&logoColor=white
[Python-url]: https://www.python.org/
[Amazon-RDS]: https://img.shields.io/badge/amazon%20rds-527FFF?style=for-the-badge&logo=amazon%20rds&logoColor=white
[Amazon-RDS-url]: https://aws.amazon.com/rds/
[Cloudflare]: https://img.shields.io/badge/cloudflare%20workers-F38020?style=for-the-badge&logo=cloudflare%20workers&logoColor=white
[Cloudflare-url]: https://workers.cloudflare.com/
[Vercel]: https://img.shields.io/badge/vercel-000000?logo=vercel&style=for-the-badge&logoColor=white
[Vercel-url]: https://www.vercel.com/
[Supabase]: https://img.shields.io/badge/supabase-3FCF8E?logo=supabase&style=for-the-badge&logoColor=white
[Supabase-url]: https://supabase.com/
[Clerk]: https://img.shields.io/badge/clerk-6C47FF?logo=clerk&style=for-the-badge&logoColor=white
[Clerk-url]: https://clerk.com/
[Tailwind]: https://img.shields.io/badge/tailwind%20css-06B6D4?logo=tailwindcss&style=for-the-badge&logoColor=white
[Tailwind-url]: https://tailwindcss.com/
[MongoDB]: https://img.shields.io/badge/mongodb-47A248?logo=mongodb&style=for-the-badge&logoColor=white
[MongoDB-url]: https://www.mongodb.com/
[HuggingFace]: https://img.shields.io/badge/huggingface-FFD21E?logo=huggingface&style=for-the-badge&logoColor=white
[HuggingFace-url]: https://huggingface.co/
[OpenAI]: https://img.shields.io/badge/openai%20api-412991?logo=openai&style=for-the-badge&logoColor=white
[OpenAI-url]: [https://huggingface.co/](https://openai.com/)

View File

@@ -7,17 +7,9 @@ import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Card, CardHeader, CardContent } from '@/components/ui/card';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { ChevronDown } from "lucide-react"
import { PersonForm } from './PatientForm';
import { useRouter } from 'next/navigation';
const AccountPage = () => {
const { user } = useUser();
const [userData, setUserData] = useState(null);
const [patients, setPatients] = useState([]);
useEffect(() => {
if (user) {
@@ -72,33 +64,6 @@ const AccountPage = () => {
<Button onClick={handleRoleChange} className="mb-4">
Change role to {userData.role === 'patient' ? 'caregiver' : 'patient'}
</Button>
{userData.role === 'caregiver' && (
<div>
<h2 className="text-xl font-bold mb-4">Patients</h2>
<ul className="mb-4">
{patients.map(patient => (
<Collapsible key={patient.id}>
<div className="flex items-center justify-between p-2 bg-gray-100 dark:bg-neutral-800 rounded-t-lg">
<div>
<h2 className="text-lg font-semibold">{patient.name}</h2>
<p className="text-sm text-gray-400">{patient.role}</p>
</div>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
<ChevronDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="p-4 border border-t-0 rounded-b-lg">
<PersonForm person={patient} />
</CollapsibleContent>
</Collapsible>
))}
</ul>
</div>
)}
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,33 @@
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Card, CardContent } from "@/components/ui/card"
export function AppointmentList({ appointments }) {
return (
<Card>
<CardContent>
<Table>
<TableCaption>A list of your upcoming appointments.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead>Date</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{appointments.map((appointment) => {
return (
<TableRow key={appointment.id}>
<TableCell className="font-medium">{appointment.id}</TableCell>
<TableCell>{new Date(appointment.date).toLocaleString()}</TableCell>
<TableCell>{appointment.status}</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</CardContent>
</Card>
)
}

View File

@@ -1,32 +1,35 @@
"use client"
import { PatientTable } from "./PatientTable"
import { AppointmentList } from "./AppList"
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import axios from "axios";
import { useUser } from '@clerk/nextjs';
import React from "react";
export default function Dashboard() {
const router = useRouter();
const { user } = useUser();
const [userData, setUserData] = useState(null);
const { user } = useUser();
const [userData, setUserData] = useState(null);
useEffect(() => {
if (user) {
axios.get(`/api/user?userId=${user.id}`).then(response => {
setUserData(response.data);
});
}
}, [user]);
useEffect(() => {
if (user) {
axios.get(`/api/user?userId=${user.id}`).then(response => {
setUserData(response.data);
});
}
}, [user]);
if (userData) {
if (userData) {
if (userData.role != "caregiver") {
router.push("/suite/patient/dashboard");
}
}
}
const patients = [
{ id: 1, name: "John Doe", age: 30, lastVisit: "2024-10-01" },
@@ -34,11 +37,21 @@ export default function Dashboard() {
{ id: 3, name: "Sam Johnson", age: 40, lastVisit: "2024-10-05" },
];
const appointments = [
{ id: 1, patientId: 1, date: "2025-01-27T09:00:00", status: "Scheduled" },
{ id: 2, patientId: 2, date: "2025-01-27T10:30:00", status: "Scheduled" },
{ id: 3, patientId: 3, date: "2025-01-27T14:00:00", status: "Scheduled" },
{ id: 4, patientId: 4, date: "2025-01-28T11:00:00", status: "Scheduled" },
{ id: 5, patientId: 5, date: "2025-01-28T15:30:00", status: "Scheduled" },
]
return (
<div className="container mx-auto">
<h1 className="text-3xl font-semibold mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="h-20 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<PatientTable data={patients} />
<AppointmentList appointments={appointments} />
</div>
</div>
)

View File

@@ -0,0 +1,76 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import axios from "axios";
import { useUser } from "@clerk/nextjs";
import { CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import PersonForm from "@/components/PersonForm";
import { Card } from "@/components/ui/card";
export default function Dashboard() {
const router = useRouter();
const { user } = useUser();
const [userData, setUserData] = useState(null);
const [patients, setPatients] = useState([]);
useEffect(() => {
if (user) {
axios.get(`/api/user?userId=${user.id}`).then(response => {
setUserData(response.data);
});
}
}, [user]);
if (userData) {
if (userData.role != "caregiver") {
router.push("/suite/patient/dashboard");
}
}
return (
<div className="container mx-auto">
<h1 className="text-3xl font-semibold mb-6">Patients</h1>
<div className="h-20 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardContent>
{userData.role === 'caregiver' && (
<div>
<ul className="mb-4">
{patients.map(patient => (
<Collapsible key={patient.id}>
<div className="flex items-center justify-between p-2 bg-gray-100 dark:bg-neutral-800 rounded-t-lg">
<div>
<h2 className="text-lg font-semibold">{patient.name}</h2>
<p className="text-sm text-gray-400">{patient.role}</p>
</div>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
<ChevronDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="p-4 border border-t-0 rounded-b-lg">
<PersonForm person={patient} />
</CollapsibleContent>
</Collapsible>
))}
</ul>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -1,156 +1,174 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
//import Hero1 from '@/components/Hero1'
//IMPORT THE HERO1 FUNCTION TO MAKE THE TRANSCRIBE PAGE LOOK BETTER
import React, { useState, useRef } from "react";
// import axios from "axios";
import { AlertCircle } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const AudioTranscriber: React.FC = () => {
const [file, setFile] = useState<File | null>(null);
const [transcription, setTranscription] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [recording, setRecording] = useState(false);
const [error, setError] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
const [file, setFile] = useState<File | null>(null);
const [transcription, setTranscription] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [recording, setRecording] = useState(false);
const [error, setError] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const audioChunksRef = useRef<Blob[]>([]);
// Handle file selection
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]);
console.log("File selected:", event.target.files[0].name);
}
};
// Handle file selection
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]);
console.log("File selected:", event.target.files[0].name);
}
};
// Handle file transcription
const handleTranscription = async (audioFile: File) => {
if (!audioFile) {
alert("No audio file to transcribe!");
return;
}
// Handle file transcription
const handleTranscription = async (audioFile: File) => {
if (!audioFile) {
alert("No audio file to transcribe!");
return;
}
console.log("Starting transcription for:", audioFile.name);
console.log("Starting transcription for:", audioFile.name);
const formData = new FormData();
formData.append("file", audioFile);
console.log(audioFile);
setLoading(true);
setError(null); // Clear previous errors
try {
let response = await fetch("/api/transcribe", {
method: "POST",
body: formData,
headers: {
"Content-Type": "multipart/form-data",
}
})
const formData = new FormData();
formData.append("file", audioFile);
setLoading(true);
setError(null); // Clear previous errors
try {
let response = await fetch("/api/transcribe", {
method: "POST",
body: formData,
headers: {
"Content-Type": "multipart/form-data",
}
})
response = await response.json();
console.log("Transcription response:", response);
console.log("! response:", response);
response = await response.json();
console.log("@ response:", response);
// if (response.data && response.data.transcription) {
// setTranscription(response.data.transcription);
// } else {
// setError("Unexpected response format. Check backend API.");
// console.error("Invalid response format:", response.data);
// }
} catch (error) {
console.error("Error transcribing audio:", error);
setError("Failed to transcribe audio. Please try again.");
} finally {
setLoading(false);
}
};
// if (response.data && response.data.transcription) {
// setTranscription(response.data.transcription);
// } else {
// setError("Unexpected response format. Check backend API.");
// console.error("Invalid response format:", response.data);
// }
} catch (error) {
console.error("Error transcribing audio:", error);
setError("Failed to transcribe audio. Please try again.");
} finally {
setLoading(false);
}
};
// Start recording audio
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log("Microphone access granted.");
// Start recording audio
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log("Microphone access granted.");
mediaRecorderRef.current = new MediaRecorder(stream);
audioChunksRef.current = []; // Reset audio chunks
mediaRecorderRef.current = new MediaRecorder(stream);
audioChunksRef.current = []; // Reset audio chunks
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
console.log("Audio chunk received:", event.data);
audioChunksRef.current.push(event.data);
}
};
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
console.log("Audio chunk received:", event.data);
audioChunksRef.current.push(event.data);
}
};
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/mp3" });
const audioFile = new File([audioBlob], "recording.mp3", { type: "audio/mp3" });
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/mp3" });
const audioFile = new File([audioBlob], "recording.mp3", { type: "audio/mp3" });
console.log("Recording stopped. Blob created:", audioBlob);
console.log("Recording stopped. Blob created:", audioBlob);
setFile(audioFile); // Save the recorded file
setTranscription("Processing transcription for recorded audio...");
await handleTranscription(audioFile); // Automatically transcribe
};
setFile(audioFile); // Save the recorded file
setTranscription("Processing transcription for recorded audio...");
await handleTranscription(audioFile); // Automatically transcribe
};
mediaRecorderRef.current.start();
console.log("Recording started.");
setRecording(true);
} catch (error) {
console.error("Error starting recording:", error);
setError("Failed to start recording. Please check microphone permissions.");
}
};
mediaRecorderRef.current.start();
console.log("Recording started.");
setRecording(true);
} catch (error) {
console.error("Error starting recording:", error);
setError("Failed to start recording. Please check microphone permissions.");
}
};
// Stop recording audio
const stopRecording = () => {
if (mediaRecorderRef.current) {
console.log("Stopping recording...");
mediaRecorderRef.current.stop();
setRecording(false);
}
};
// Stop recording audio
const stopRecording = () => {
if (mediaRecorderRef.current) {
console.log("Stopping recording...");
mediaRecorderRef.current.stop();
setRecording(false);
}
};
return (
<div>
<h1>
<center>
Audio Transcription
</center>
</h1>
<div>
<h2>Upload or Record Audio</h2>
return (
<section className="mx-auto my-auto bg-white dark:bg-neutral-950 h-screen w-1/2">
<h1 className="text-4xl font-bold text-center mb-8">
<center>
Audio Transcription
</center>
</h1>
<br />
<div>
<h2>Upload or Record Audio</h2>
<input type="file" accept="audio/*" onChange={handleFileChange} />
<button onClick={() => file && handleTranscription(file)} disabled={loading || !file}>
{loading ? "Transcribing..." : "Transcribe"}
</button>
</div>
<Input type="file" accept="audio/*" onChange={handleFileChange} />
<Button className="my-4" onClick={() => file && handleTranscription(file)} disabled={loading || !file}>
{loading ? "Transcribing..." : "Transcribe"}
</Button>
</div>
<div>
<h2>Record Audio</h2>
{!recording ? (
<button onClick={startRecording}>Start Recording</button>
) : (
<button onClick={stopRecording} disabled={!recording}>
Stop Recording
</button>
)}
</div>
<div>
<h2>Record Audio</h2>
{!recording ? (
<Button onClick={startRecording}>Start Recording</Button>
) : (
<Button onClick={stopRecording} disabled={!recording}>Stop Recording</Button>
)}
</div>
<div>
<h2>Transcription:</h2>
{loading ? (
<p>Processing transcription...</p>
) : transcription ? (
<p>{transcription}</p>
) : (
<p>No transcription available yet.</p>
)}
</div>
<Card className="mt-4 bg-neutral-200 dark:bg-neutral-800">
<CardHeader>
<CardTitle>Transcription Result</CardTitle>
</CardHeader>
<CardContent>
<CardContent>
{loading
? "Processing transcription..."
: transcription
? "Your transcription is ready"
: "No transcription available yet"}
</CardContent>
</CardContent>
</Card>
{error && (
<div style={{ color: "red" }}>
<strong>Error:</strong> {error}
</div>
)}
</div>
);
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
<strong>Error:</strong> {error}
</AlertDescription>
</Alert>
)}
</section>
);
};
export default AudioTranscriber;
@@ -182,179 +200,179 @@ const AudioTranscriber: React.FC = () => {
const audioChunksRef = useRef<Blob[]>([])
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0])
console.log("File selected:", event.target.files[0].name)
}
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0])
console.log("File selected:", event.target.files[0].name)
}
}
const handleTranscription = async (audioFile: File) => {
if (!audioFile) {
setError("No audio file to transcribe!")
return
}
if (!audioFile) {
setError("No audio file to transcribe!")
return
}
console.log("Starting transcription for:", audioFile.name)
console.log("Starting transcription for:", audioFile.name)
const formData = new FormData()
formData.append("file", audioFile)
const formData = new FormData()
formData.append("file", audioFile)
setLoading(true)
setError(null)
try {
const response = await axios.post("http://localhost:8000/transcribe", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
setLoading(true)
setError(null)
try {
const response = await axios.post("http://localhost:8000/transcribe", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
console.log("Transcription response:", response.data)
console.log("Transcription response:", response.data)
if (response.data && response.data.transcription) {
setTranscription(response.data.transcription)
} else {
setError("Unexpected response format. Check backend API.")
console.error("Invalid response format:", response.data)
}
} catch (error) {
console.error("Error transcribing audio:", error)
setError("Failed to transcribe audio. Please try again.")
} finally {
setLoading(false)
}
if (response.data && response.data.transcription) {
setTranscription(response.data.transcription)
} else {
setError("Unexpected response format. Check backend API.")
console.error("Invalid response format:", response.data)
}
} catch (error) {
console.error("Error transcribing audio:", error)
setError("Failed to transcribe audio. Please try again.")
} finally {
setLoading(false)
}
}
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
console.log("Microphone access granted.")
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
console.log("Microphone access granted.")
mediaRecorderRef.current = new MediaRecorder(stream)
audioChunksRef.current = []
mediaRecorderRef.current = new MediaRecorder(stream)
audioChunksRef.current = []
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
console.log("Audio chunk received:", event.data)
audioChunksRef.current.push(event.data)
}
}
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
console.log("Audio chunk received:", event.data)
audioChunksRef.current.push(event.data)
}
}
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/mp3" })
const audioFile = new File([audioBlob], "recording.mp3", { type: "audio/mp3" })
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/mp3" })
const audioFile = new File([audioBlob], "recording.mp3", { type: "audio/mp3" })
console.log("Recording stopped. Blob created:", audioBlob)
console.log("Recording stopped. Blob created:", audioBlob)
setFile(audioFile)
setTranscription("Processing transcription for recorded audio...")
await handleTranscription(audioFile)
}
setFile(audioFile)
setTranscription("Processing transcription for recorded audio...")
await handleTranscription(audioFile)
}
mediaRecorderRef.current.start()
console.log("Recording started.")
setRecording(true)
} catch (error) {
console.error("Error starting recording:", error)
setError("Failed to start recording. Please check microphone permissions.")
}
mediaRecorderRef.current.start()
console.log("Recording started.")
setRecording(true)
} catch (error) {
console.error("Error starting recording:", error)
setError("Failed to start recording. Please check microphone permissions.")
}
}
const stopRecording = () => {
if (mediaRecorderRef.current) {
console.log("Stopping recording...")
mediaRecorderRef.current.stop()
setRecording(false)
}
if (mediaRecorderRef.current) {
console.log("Stopping recording...")
mediaRecorderRef.current.stop()
setRecording(false)
}
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold text-center mb-8">Audio Transcriber</h1>
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold text-center mb-8">Audio Transcriber</h1>
<Card className="mb-8">
<CardHeader>
<CardTitle>Transcribe Audio</CardTitle>
<CardDescription>Upload an audio file or record directly to transcribe</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="upload" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload">Upload Audio</TabsTrigger>
<TabsTrigger value="record">Record Audio</TabsTrigger>
</TabsList>
<TabsContent value="upload">
<div className="space-y-4">
<Label htmlFor="audio-file">Select an audio file</Label>
<Input id="audio-file" type="file" accept="audio/*" onChange={handleFileChange} />
<Button
onClick={() => file && handleTranscription(file)}
disabled={loading || !file}
className="w-full"
>
{loading ? "Transcribing..." : "Transcribe"}
<Upload className="ml-2 h-4 w-4" />
</Button>
</div>
</TabsContent>
<TabsContent value="record">
<div className="space-y-4">
<Label>Record audio from your microphone</Label>
{!recording ? (
<Button onClick={startRecording} className="w-full">
Start Recording
<Mic className="ml-2 h-4 w-4" />
</Button>
) : (
<Button onClick={stopRecording} variant="destructive" className="w-full">
Stop Recording
<StopCircle className="ml-2 h-4 w-4" />
</Button>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<Card className="mb-8">
<CardHeader>
<CardTitle>Transcribe Audio</CardTitle>
<CardDescription>Upload an audio file or record directly to transcribe</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="upload" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload">Upload Audio</TabsTrigger>
<TabsTrigger value="record">Record Audio</TabsTrigger>
</TabsList>
<TabsContent value="upload">
<div className="space-y-4">
<Label htmlFor="audio-file">Select an audio file</Label>
<Input id="audio-file" type="file" accept="audio/*" onChange={handleFileChange} />
<Button
onClick={() => file && handleTranscription(file)}
disabled={loading || !file}
className="w-full"
>
{loading ? "Transcribing..." : "Transcribe"}
<Upload className="ml-2 h-4 w-4" />
</Button>
</div>
</TabsContent>
<TabsContent value="record">
<div className="space-y-4">
<Label>Record audio from your microphone</Label>
{!recording ? (
<Button onClick={startRecording} className="w-full">
Start Recording
<Mic className="ml-2 h-4 w-4" />
</Button>
) : (
<Button onClick={stopRecording} variant="destructive" className="w-full">
Stop Recording
<StopCircle className="ml-2 h-4 w-4" />
</Button>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Transcription Result</CardTitle>
<CardDescription>
{loading
? "Processing transcription..."
: transcription
? "Your transcription is ready"
: "No transcription available yet"}
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={transcription || ""}
readOnly
placeholder="Transcription will appear here"
className="min-h-[200px]"
/>
</CardContent>
{file && (
<CardFooter className="flex justify-between items-center">
<div className="flex items-center">
<FileAudio className="mr-2 h-4 w-4" />
<span className="text-sm text-muted-foreground">{file.name}</span>
</div>
<Button variant="outline" onClick={() => setFile(null)}>
Clear
</Button>
</CardFooter>
)}
</Card>
<Card>
<CardHeader>
<CardTitle>Transcription Result</CardTitle>
<CardDescription>
{loading
? "Processing transcription..."
: transcription
? "Your transcription is ready"
: "No transcription available yet"}
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={transcription || ""}
readOnly
placeholder="Transcription will appear here"
className="min-h-[200px]"
/>
</CardContent>
{file && (
<CardFooter className="flex justify-between items-center">
<div className="flex items-center">
<FileAudio className="mr-2 h-4 w-4" />
<span className="text-sm text-muted-foreground">{file.name}</span>
</div>
<Button variant="outline" onClick={() => setFile(null)}>
Clear
</Button>
</CardFooter>
)}
</Card>
{error && (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
{error && (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div>
)
}

View File

@@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/api/connectDB.js", "src/lib/utils.js", "src/app/(web)/account/page.jsx", "src/app/(panels)/suite/patient/account/page.jsx", "src/app/(panels)/suite/patient/dashboard/MedicationTable.jsx", "src/app/(panels)/suite/patient/dashboard/page.jsx", "src/app/api/transcribe/route.js", "src/components/ui/calendar.jsx", "src/app/(web)/page.jsx", "src/app/(web)/transcribe/page.jsx", "src/app/(web)/login/page.jsx", "src/app/(panels)/suite/layout.jsx", "src/app/(panels)/suite/doctor/dashboard/page.jsx", "src/app/(panels)/suite/patient/chat/page.jsx", "src/app/api/chat/route.js"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/api/connectDB.js", "src/lib/utils.js", "src/app/(web)/account/page.jsx", "src/app/(panels)/suite/patient/account/page.jsx", "src/app/(panels)/suite/patient/dashboard/MedicationTable.jsx", "src/app/(panels)/suite/patient/dashboard/page.jsx", "src/app/api/transcribe/route.js", "src/components/ui/calendar.jsx", "src/app/(web)/page.jsx", "src/app/(web)/transcribe/page.jsx", "src/app/(web)/login/page.jsx", "src/app/(panels)/suite/layout.jsx", "src/app/(panels)/suite/doctor/dashboard/page.jsx", "src/app/(panels)/suite/patient/chat/page.jsx", "src/app/(panels)/suite/doctor/dashboard/AppList.jsx", "src/app/(panels)/suite/doctor/patient/page.jsx"],
"exclude": ["node_modules"]
}