diff --git a/MVP/package-lock.json b/MVP/package-lock.json index d1a6d9f..7bacc2b 100644 --- a/MVP/package-lock.json +++ b/MVP/package-lock.json @@ -12,7 +12,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.2.1", - "react-mic": "^12.4.6" + "react-mic": "^12.4.6", + "react-router-dom": "^6.26.2" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -2507,6 +2508,15 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz", + "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.2.3", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", @@ -6035,6 +6045,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz", + "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz", + "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.19.2", + "react-router": "6.26.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", diff --git a/MVP/package.json b/MVP/package.json index d09ce3f..cca698a 100644 --- a/MVP/package.json +++ b/MVP/package.json @@ -14,7 +14,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.2.1", - "react-mic": "^12.4.6" + "react-mic": "^12.4.6", + "react-router-dom": "^6.26.2" }, "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/MVP/src/App.jsx b/MVP/src/App.jsx index 6460729..d952022 100644 --- a/MVP/src/App.jsx +++ b/MVP/src/App.jsx @@ -1,146 +1,24 @@ -import { useState, useRef } from 'react'; -import './App.css'; -import VoiceRecorder from './VoiceRecorder'; -import './assets/mvp.css'; -import { FaPaperclip } from 'react-icons/fa'; +import { HashRouter as Router, Routes, Route } from 'react-router-dom'; +import Home from './components/Home'; +import Report from './components/Report'; +import Header from './components/Header'; +import Footer from './components/Footer'; +import { StudentsProvider } from './contexts/Students'; function App() { - const [isRecording, setIsRecording] = useState(false); - const [audioUrl, setAudioUrl] = useState(null); - const [transcription, setTranscription] = useState(''); - const [aiResponse, setAiResponse] = useState(''); - const [isUploading, setIsUploading] = useState(false); // Track uploading state - - const fileInputRef = useRef(null); - - const handleStopRecording = async (blob) => { - const formData = new FormData(); - formData.append('file', blob.blob, 'recording.mp3'); - - setIsUploading(true); // Set uploading state to true - callAjax(formData, (response) => { - setIsUploading(false); // Reset uploading state - if (response && response.transcription) { - setTranscription(response.transcription); - setAudioUrl(URL.createObjectURL(blob.blob)); - } else { - console.error("Failed to get a transcription from the server."); - } - }); - }; - - const handleFileChange = async (event) => { - const file = event.target.files[0]; - if (file) { - console.log("Selected file: ", file); - - const formData = new FormData(); - formData.append('file', file, file.name); // Ensure filename is set - - setIsUploading(true); // Set uploading state to true - - callAjax(formData, (response) => { - setIsUploading(false); // Reset uploading state - if (response && response.transcription) { - setTranscription(response.transcription); - setAudioUrl(URL.createObjectURL(file)); // URL for the uploaded file - } else { - console.error("Failed to get a transcription from the server."); - } - }); - } - }; - - - const callAjax = (formData, callback) => { - window.clicnical_coach_jsmo_module.transcribeAudio(formData, (res) => { - if (res && res.transcription) { - if (callback) callback(res); - } else { - console.log("Unexpected response format:", res); - } - }, (err) => { - console.log("transcribeAudio error:", err); - if (callback) callback(); - }); - }; - - const handleSubmitToAI = () => { - const chatmlPayload = [ - { role: "system", content: "You are a helpful assistant." }, - { role: "user", content: transcription } - ]; - - callAI(chatmlPayload, (aiContent) => { - if (aiContent) { - setAiResponse(aiContent); - } else { - console.error("Failed to get a response from the AI."); - } - }); - }; - - const callAI = (chatmlPayload, callback) => { - window.clicnical_coach_jsmo_module.callAI(chatmlPayload, (res) => { - if (res) { - if (callback) callback(res); - } else { - console.log("Unexpected AI response format:", res); - } - }, (err) => { - console.log("callAI error:", err); - if (callback) callback(); - }); - }; - return ( -
-

Clinical Coach - MVP

-
-
- - - + + +
+
+ + } /> + } /> + +
- {isUploading && ( -

Uploading...

- )} - {audioUrl && ( -
-
- )} - {transcription && ( -
-
- "{transcription}" -
- -
- )} - {aiResponse && ( -
-
- "{aiResponse}" -
-
- )} -
-
+ + ); } diff --git a/MVP/src/assets/mvp.css b/MVP/src/assets/mvp.css index b5e62ff..668e7d1 100644 --- a/MVP/src/assets/mvp.css +++ b/MVP/src/assets/mvp.css @@ -1,3 +1,4 @@ +/* General styling for body and container */ body, html { height: 100%; margin: 0; @@ -5,68 +6,269 @@ body, html { justify-content: center; align-items: center; background-color: #f5f5f5; + overflow: hidden; /* Prevent body from scrolling */ +} + +#clinicalcoach_ui_container { + display: flex; + flex-direction: column; + min-height: 100vh; + max-height: 100vh; /* Limit to full viewport height */ + width: 100vw; + max-width: 600px; + margin: 0 auto; } #clinicalcoachmvp_container { border: 1px solid #ccc; border-radius: 8px; - padding: 20px; background-color: white; box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); - max-width: 600px; width: 100%; text-align: center; + flex-grow: 1; /* Allows it to grow vertically */ + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; /* Ensure the footer stays in place */ } -h1 { - font-size: 2rem; - margin-bottom: 20px; - color: #333; +/* Header and Footer Styling */ +header { + background-color: transparent; + padding: 1rem; + text-align: center; + border: none; + flex-shrink: 0; /* Prevents shrinking */ } -.card { +/* Footer Styling */ +footer { + position: relative; + bottom: 0; + left: 0; + right: 0; + background-color: #979DC0; /* Your desired footer color */ + color: white; + padding: 1rem; display: flex; justify-content: center; align-items: center; + border-top: 1px solid #ccc; + width: 100%; + max-width: 600px; + margin: 0 auto; + z-index: 1; + flex-shrink: 0; /* Prevents shrinking */ + overflow: visible; +} + +/* Left concave curve */ +footer::before { + content: ""; + position: absolute; + top: -30px; + left: 0; + width: 40px; + height: 20px; + background-color: transparent; + box-shadow: none; + border-right: 50px solid transparent; + border-top: 50px solid transparent; + border-bottom: 0px solid transparent; + border-lefT: 90px solid #979dc0; +} + +/* Right concave curve */ +footer::after { + content: ""; + position: absolute; + top: -30px; + right: 0; + width: 40px; + height: 20px; + box-shadow: none; + border-left: 50px solid transparent; + border-top: 50px solid transparent; + border-bottom: 0px solid transparent; + border-right: 90px solid #979dc0; +} + +/* Adjust the content area */ +.scrollable-content { + overflow-y: auto; /* Enable scrolling if content overflows */ + flex-grow: 1; + max-height: calc(100vh - 120px); /* Adjust to leave space for header and footer */ + padding: 20px; +} + +/* Footer Content - Align Record Button and Dropdown */ +.footer-content { + display: flex; flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; /* Space between heading and buttons */ + margin-top: 0; + text-align: center; } -button { +/* Create a horizontal row for the button and dropdown */ +.footer-controls { + display: flex; + flex-direction: row; /* Ensure button and dropdown are side by side */ + gap: 20px; /* Add space between button and dropdown */ + align-items: center; + justify-content: center; +} + +/* Footer Heading Styling */ +.footer-content h3 { + font-size: 1.2rem; /* Smaller size */ + font-weight: normal; /* Normal weight */ + color: #333; /* Darker color */ + margin-bottom: 10px; /* Slight spacing below heading */ +} + +/* Dropdown styling */ +.student-dropdown { + padding: 10px; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Circular button styling */ +.circle-recording-btn { background-color: #28a745; color: white; - padding: 10px 20px; + width: 100px; + height: 100px; border: none; - border-radius: 4px; - font-size: 1rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.5rem; cursor: pointer; transition: background-color 0.3s ease; + position: relative; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); } -button:hover { - background-color: #218838; +/* Pulsating animation for recording state */ +.circle-recording-btn.recording { + background-color: red; + animation: pulse 1s infinite; } -.controls { +/* Animation for recording button */ +@keyframes pulse { + 0% { + transform: scale(1); + box-shadow: 0 0 5px rgba(255, 0, 0, 0.8); + } + 50% { + transform: scale(1.05); + box-shadow: 0 0 15px rgba(255, 0, 0, 1); + } + 100% { + transform: scale(1); + box-shadow: 0 0 5px rgba(255, 0, 0, 0.8); + } +} + +.react_mic_container { + visibility: hidden; + position:absolute; + z-index:-999; +} +.home-content { + padding: 20px; +} + +.session-date { + background-color: #f0f0f0; + padding: 10px; + margin-bottom: 20px; +} + +.session-date h2 { + font-size: 1rem; + color: #666; + margin: 0; + text-align: left; +} + +.student-card { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 10px; + padding: 15px; display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 20px; +} + +.student-info { + display: flex; + flex-direction: column; align-items: center; - justify-content: center; + margin-right: 20px; } -.controls button { - margin:0; - padding:0; - vertical-align: middle; +.student-icon { + color: #666; } -.controls .attachment-button { - background-color: transparent; - margin:0 10px 10px; - cursor: pointer; +.student-name { + margin-top: 10px; font-size: 1.5rem; - color: #555; + text-align: center; +} + +.session-details { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; +} + +.session-status { + background-color: #979DC0; + padding: 10px 20px; + border-radius: 5px; + color: white; + text-align: center; + width: 100%; + max-width: 300px; +} + +.session-summary { + margin-top: 10px; + text-align: left; + width: 100%; +} + +button { + margin-top: 15px; + padding: 10px 20px; + background-color: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; +} +button:hover { + background-color: #218838; } -.controls .attachment-button:hover{ - background-color:initial; - border: initial; + +.ai-response { + margin-top: 20px; + font-style: italic; } + diff --git a/MVP/src/components/Footer.jsx b/MVP/src/components/Footer.jsx new file mode 100644 index 0000000..a4f17f4 --- /dev/null +++ b/MVP/src/components/Footer.jsx @@ -0,0 +1,89 @@ +import React, { useState, useEffect, useRef } from 'react'; +import VoiceRecorder from './VoiceRecorder'; +import { useStudents } from '../contexts/Students'; + +function Footer() { + const [isRecording, setIsRecording] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const { students, selectedStudent, selectStudent, updateTranscription } = useStudents(); + + const clinicianId = 1234; // Placeholder for clinician ID + const selectedStudentRef = useRef(null); // Create a ref to store selectedStudent + + // Keep the ref updated with the latest selectedStudent value + useEffect(() => { + selectedStudentRef.current = selectedStudent; + }, [selectedStudent]); + + const handleStudentChange = (event) => { + selectStudent(Number(event.target.value)); + }; + + const handleStopRecording = async (blob) => { + const currentSelectedStudent = selectedStudentRef.current; // Access the current ref value + if (!currentSelectedStudent) { + alert("Please select a student before recording."); + return; + } + + const formData = new FormData(); + formData.append('file', blob.blob, 'recording.mp3'); + formData.append('studentId', currentSelectedStudent.id); // Use the ref value + formData.append('clinicianId', clinicianId); + + setIsUploading(true); + callAjax(formData, (response) => { + setIsUploading(false); + + //TODO just for show for now + if (response && response.transcription) { + // Update transcription in the context + updateTranscription(currentSelectedStudent.id, response.transcription); + } + }); + }; + + const callAjax = (formData, callback) => { + window.clicnical_coach_jsmo_module.transcribeAudio(formData, (res) => { + if (res) { + if (callback) callback(res); + } else { + console.log("Unexpected response format:", res); + } + }, (err) => { + console.log("transcribeAudio error:", err); + if (callback) callback(); + }); + }; + + return ( + + ); +} + +export default Footer; diff --git a/MVP/src/components/Header.jsx b/MVP/src/components/Header.jsx new file mode 100644 index 0000000..e39ec35 --- /dev/null +++ b/MVP/src/components/Header.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useLocation, Link } from 'react-router-dom'; + +function Header() { + const location = useLocation(); // Get current route information + const { pathname } = location; // Get pathname directly + + const isHomeView = pathname === '/'; // Simplified check for home view + const isReportView = pathname.startsWith('/report'); // Simplified check for report view + + return ( +
+ {isHomeView && ( +

Clinical Coach

+ )} + {isReportView && ( + + )} +
+ ); +} + +export default Header; diff --git a/MVP/src/components/Home.jsx b/MVP/src/components/Home.jsx new file mode 100644 index 0000000..f3721d3 --- /dev/null +++ b/MVP/src/components/Home.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { useStudents } from '../contexts/Students'; +import { FaUserCircle } from 'react-icons/fa'; + +function Home() { + const { selectedStudent, aiResponse, updateAIResponse } = useStudents(); + + const handleSubmitToAI = () => { + if (!selectedStudent || !selectedStudent.transcription) { + console.error("No transcription to submit."); + return; + } + + const chatmlPayload = [ + { role: "system", content: "You are a helpful assistant." }, + { role: "user", content: selectedStudent.transcription } + ]; + + console.log("chatmlPayload", chatmlPayload); + + callAI(chatmlPayload, (aiContent) => { + console.log("aiContent callback", aiContent); + if (aiContent) { + updateAIResponse(aiContent); // Store AI response in context + } else { + console.error("Failed to get a response from the AI."); + } + }); + }; + + const callAI = (chatmlPayload, callback) => { + window.clicnical_coach_jsmo_module.callAI(chatmlPayload, (res) => { + if (res) { + if (callback) callback(res); + } else { + console.log("Unexpected AI response format:", res); + } + }, (err) => { + console.log("callAI error:", err); + if (callback) callback(); + }); + }; + + return ( +
+
+

Today, September 10, 2024

+
+ + {selectedStudent ? ( +
+
+ +

{selectedStudent.name}

+
+ +
+
+

Session Completed

+
+ + {selectedStudent.transcription && ( +
+
{selectedStudent.transcription}
+
+ )} + + +
+
+ ) : ( +

Please select a student and start recording a session.

+ )} + + {aiResponse && ( +
+ "{aiResponse}" +
+ )} +
+ ); +} + +export default Home; diff --git a/MVP/src/components/Report.jsx b/MVP/src/components/Report.jsx new file mode 100644 index 0000000..d137182 --- /dev/null +++ b/MVP/src/components/Report.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useStudents } from '../contexts/Students'; + +function Report() { + const { selectedStudent } = useStudents(); + + if (!selectedStudent) { + return

No student selected. Please go back and select a student.

; + } + + return ( +
+

Report for {selectedStudent.name}

+ {/* Display the student's report here */} +

Report content goes here...

+
+ ); +} + +export default Report; diff --git a/MVP/src/VoiceRecorder.jsx b/MVP/src/components/VoiceRecorder.jsx similarity index 83% rename from MVP/src/VoiceRecorder.jsx rename to MVP/src/components/VoiceRecorder.jsx index 20b6a71..31d2470 100644 --- a/MVP/src/VoiceRecorder.jsx +++ b/MVP/src/components/VoiceRecorder.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { ReactMic } from 'react-mic'; +import { FaMicrophone, FaStop } from 'react-icons/fa'; const VoiceRecorder = ({ setIsRecording, handleStopRecording }) => { const [record, setRecord] = useState(false); @@ -92,8 +93,6 @@ const VoiceRecorder = ({ setIsRecording, handleStopRecording }) => { const rms = Math.sqrt(sum / bufferLength); const db = 20 * Math.log10(rms); - // console.log(`Current dB level: ${db.toFixed(2)} (Threshold: ${silenceThreshold})`); - if (db < silenceThreshold) { if (silenceStartRef.current === null) { silenceStartRef.current = Date.now(); @@ -102,7 +101,7 @@ const VoiceRecorder = ({ setIsRecording, handleStopRecording }) => { } else { const silenceDurationElapsed = Date.now() - silenceStartRef.current; silenceDurationRef.current += 100; - console.log(`Silence duration elapsed: ${silenceDurationRef.current}ms`); + // console.log(`Silence duration elapsed: ${silenceDurationRef.current}ms`); if (silenceDurationRef.current >= silenceDurationMs) { console.log('Silence threshold reached, stopping recording'); stopRecording(); // Stop the recording when silence is detected @@ -136,40 +135,36 @@ const VoiceRecorder = ({ setIsRecording, handleStopRecording }) => { }, []); return ( -
+
-
+
- {audioUrl && ( -
-
- )} + {/*{audioUrl && (*/} + {/*
*/} + {/*
*/} + {/*)}*/}
); }; export default VoiceRecorder; + + + diff --git a/MVP/src/components/fileUpload.jsx b/MVP/src/components/fileUpload.jsx new file mode 100644 index 0000000..4acb858 --- /dev/null +++ b/MVP/src/components/fileUpload.jsx @@ -0,0 +1,65 @@ +import React, { useRef, useState } from 'react'; +import { FaPaperclip } from 'react-icons/fa'; + +function Footer() { + const [isUploading, setIsUploading] = useState(false); + const [transcription, setTranscription] = useState(''); + const fileInputRef = useRef(null); + + const handleFileChange = async (event) => { + const file = event.target.files[0]; + if (file) { + const formData = new FormData(); + formData.append('file', file, file.name); + + setIsUploading(true); + + callAjax(formData, (response) => { + setIsUploading(false); + if (response && response.transcription) { + setTranscription(response.transcription); + } else { + console.error("Failed to get a transcription from the server."); + } + }); + } + }; + + const callAjax = (formData, callback) => { + window.clicnical_coach_jsmo_module.transcribeAudio(formData, (res) => { + if (res && res.transcription) { + if (callback) callback(res); + } else { + console.log("Unexpected response format:", res); + } + }, (err) => { + console.log("transcribeAudio error:", err); + if (callback) callback(); + }); + }; + + return ( +
+
+ + + + {transcription && ( +
+
"{transcription}"
+
+ )} +
+
+ ); +} + +export default Footer; diff --git a/MVP/src/contexts/Students.jsx b/MVP/src/contexts/Students.jsx new file mode 100644 index 0000000..22f59ec --- /dev/null +++ b/MVP/src/contexts/Students.jsx @@ -0,0 +1,59 @@ +import React, { createContext, useState, useContext, useEffect } from 'react'; + +// Create the context +const StudentsContext = createContext(); + +// Custom hook to use the StudentsContext +export const useStudents = () => useContext(StudentsContext); + +// Provider component to wrap the app +export const StudentsProvider = ({ children }) => { + const [students, setStudents] = useState([ + { id: 1, name: "John Wick" }, + { id: 2, name: "Selina Kyle" }, + { id: 3, name: "Brock Purdy" } + ]); + + const [selectedStudent, setSelectedStudent] = useState(null); + const [aiResponse, setAiResponse] = useState(''); + + // Function to select a student + const selectStudent = (studentId) => { + const student = students.find((s) => s.id === studentId); + setSelectedStudent(student); + }; + + const updateTranscription = (studentId, transcription) => { + console.log("updateTranscription", studentId, transcription); + setStudents((prevStudents) => + prevStudents.map((student) => + student.id === studentId ? { ...student, transcription } : student + ) + ); + }; + + const updateAIResponse = (response) => { + setAiResponse(response); + }; + + // Sync selectedStudent with updated transcription + useEffect(() => { + if (selectedStudent) { + const updatedStudent = students.find((s) => s.id === selectedStudent.id); + setSelectedStudent(updatedStudent); // Re-sync selectedStudent with the updated students array + } + }, [students]); + + return ( + + {children} + + ); +}; diff --git a/MVP/src/main.jsx b/MVP/src/main.jsx index 1bb10e9..770b489 100644 --- a/MVP/src/main.jsx +++ b/MVP/src/main.jsx @@ -2,6 +2,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.jsx' import './index.css' +import './assets/mvp.css' createRoot(document.getElementById('clinicalcoach_ui_container')).render(