From 0ff8eb2cf33f9cbe2ad95a222e7e8a6272c055b3 Mon Sep 17 00:00:00 2001 From: Koper Date: Tue, 8 Aug 2023 21:18:54 +0700 Subject: [PATCH] Front end API implementation (draft) --- openapi.json | 680 +++++++++++++++++++++++++++++++ www/app/components/dashboard.js | 14 +- www/app/components/transcript.js | 43 ++ www/app/components/webrtc.js | 40 +- www/app/components/websocket.js | 46 +++ www/app/page.js | 10 +- www/package.json | 1 + www/yarn.lock | 52 +++ 8 files changed, 865 insertions(+), 21 deletions(-) create mode 100644 openapi.json create mode 100644 www/app/components/transcript.js create mode 100644 www/app/components/websocket.js diff --git a/openapi.json b/openapi.json new file mode 100644 index 00000000..5bc74d33 --- /dev/null +++ b/openapi.json @@ -0,0 +1,680 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/offer": { + "post": { + "summary": "Rtc Offer", + "operationId": "rtc_offer_offer_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RtcOffer" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts": { + "get": { + "summary": "Transcripts List", + "operationId": "transcripts_list_v1_transcripts_get", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1, + "title": "Page" + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Size" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_GetTranscript_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "summary": "Transcripts Create", + "operationId": "transcripts_create_v1_transcripts_post", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTranscript" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTranscript" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts/{transcript_id}": { + "get": { + "summary": "Transcript Get", + "operationId": "transcript_get_v1_transcripts__transcript_id__get", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTranscript" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "patch": { + "summary": "Transcript Update", + "operationId": "transcript_update_v1_transcripts__transcript_id__patch", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTranscript" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTranscript" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "summary": "Transcript Delete", + "operationId": "transcript_delete_v1_transcripts__transcript_id__delete", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionStatus" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts/{transcript_id}/audio": { + "get": { + "summary": "Transcript Get Audio", + "operationId": "transcript_get_audio_v1_transcripts__transcript_id__audio_get", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts/{transcript_id}/topics": { + "get": { + "summary": "Transcript Get Topics", + "operationId": "transcript_get_topics_v1_transcripts__transcript_id__topics_get", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TranscriptTopic" + }, + "title": "Response Transcript Get Topics V1 Transcripts Transcript Id Topics Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts/{transcript_id}/events": { + "get": { + "summary": "Transcript Get Websocket Events", + "operationId": "transcript_get_websocket_events_v1_transcripts__transcript_id__events_get", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v1/transcripts/{transcript_id}/record/webrtc": { + "post": { + "summary": "Transcript Record Webrtc", + "operationId": "transcript_record_webrtc_v1_transcripts__transcript_id__record_webrtc_post", + "parameters": [ + { + "name": "transcript_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Transcript Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RtcOffer" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CreateTranscript": { + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "CreateTranscript" + }, + "DeletionStatus": { + "properties": { + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "DeletionStatus" + }, + "GetTranscript": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "status": { + "type": "string", + "title": "Status" + }, + "locked": { + "type": "boolean", + "title": "Locked" + }, + "duration": { + "type": "integer", + "title": "Duration" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "name", + "status", + "locked", + "duration", + "created_at" + ], + "title": "GetTranscript" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "Page_GetTranscript_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/GetTranscript" + }, + "type": "array", + "title": "Items" + }, + "total": { + "type": "integer", + "minimum": 0.0, + "title": "Total" + }, + "page": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Page" + }, + "size": { + "anyOf": [ + { + "type": "integer", + "minimum": 1.0 + }, + { + "type": "null" + } + ], + "title": "Size" + }, + "pages": { + "anyOf": [ + { + "type": "integer", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Pages" + } + }, + "type": "object", + "required": [ + "items", + "total", + "page", + "size" + ], + "title": "Page[GetTranscript]" + }, + "RtcOffer": { + "properties": { + "sdp": { + "type": "string", + "title": "Sdp" + }, + "type": { + "type": "string", + "title": "Type" + } + }, + "type": "object", + "required": [ + "sdp", + "type" + ], + "title": "RtcOffer" + }, + "TranscriptTopic": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "title": { + "type": "string", + "title": "Title" + }, + "summary": { + "type": "string", + "title": "Summary" + }, + "transcript": { + "type": "string", + "title": "Transcript" + }, + "timestamp": { + "type": "number", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "title", + "summary", + "transcript", + "timestamp" + ], + "title": "TranscriptTopic" + }, + "UpdateTranscript": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "locked": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Locked" + } + }, + "type": "object", + "title": "UpdateTranscript" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} \ No newline at end of file diff --git a/www/app/components/dashboard.js b/www/app/components/dashboard.js index 9c73f6f3..8c9f02f3 100644 --- a/www/app/components/dashboard.js +++ b/www/app/components/dashboard.js @@ -36,6 +36,18 @@ export function Dashboard({ } }; + const formatTime = (seconds) => { + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds % 3600) / 60); + let secs = Math.floor(seconds % 60); + + let timeString = `${hours > 0 ? hours + ":" : ""}${minutes + .toString() + .padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + + return timeString; + }; + return ( <>
@@ -68,7 +80,7 @@ export function Dashboard({ className="flex justify-between items-center cursor-pointer px-4" onClick={() => setOpenIndex(openIndex === index ? null : index)} > -
{item.timestamp}
+
{formatTime(item.timestamp)}
{item.title} { + const [response, setResponse] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const createTranscript = () => { + setLoading(true); + const url = API_URL + "/v1/transcripts/"; + const data = { + name: "Weekly All-Hands", // Hardcoded for now + }; + + console.log("Sending POST:", data); + + axios + .post(url, data) + .then((result) => { + setResponse(result.data); + setLoading(false); + console.log("Response:", result.data); + }) + .catch((err) => { + setError(err.response || err); + setLoading(false); + alert("Error: " + (err.response || err)); + console.log("Error occurred:", err.response || err); // Debugging line + }); + }; + + // You can choose when to call createTranscript, e.g., based on some dependencies + useEffect(() => { + createTranscript(); + }, []); // Empty dependencies array means this effect will run once on mount + + return { response, loading, error, createTranscript }; +}; + +export default useCreateTranscript; diff --git a/www/app/components/webrtc.js b/www/app/components/webrtc.js index 62bebb0d..9ea22792 100644 --- a/www/app/components/webrtc.js +++ b/www/app/components/webrtc.js @@ -1,35 +1,41 @@ import { useEffect, useState } from "react"; import Peer from "simple-peer"; +import axios from "axios"; -// allow customization of the WebRTC server URL from env -const WEBRTC_SERVER_URL = process.env.NEXT_PUBLIC_WEBRTC_SERVER_URL || "http://127.0.0.1:1250/offer"; +const API_URL = process.env.NEXT_PUBLIC_API_URL; -const useWebRTC = (stream) => { +const useWebRTC = (stream, transcript) => { const [data, setData] = useState({ peer: null, }); useEffect(() => { - if (!stream) { + if (!stream || !transcript) { return; } + const url = `${API_URL}/v1/transcripts/${transcript.id}/record/webrtc`; + console.log("Sending RTC Offer", url, transcript); let peer = new Peer({ initiator: true, stream: stream }); peer.on("signal", (data) => { if ("sdp" in data) { - fetch(WEBRTC_SERVER_URL, { - body: JSON.stringify({ - sdp: data.sdp, - type: data.type, - }), - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }) - .then((response) => response.json()) - .then((answer) => peer.signal(answer)) + const rtcOffer = { + sdp: data.sdp, + type: data.type, + }; + + axios + .post(url, rtcOffer, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + const answer = response.data; + console.log("Answer:", answer); + peer.signal(answer); + }) .catch((e) => { console.log("Error signaling:", e); }); @@ -76,7 +82,7 @@ const useWebRTC = (stream) => { return () => { peer.destroy(); }; - }, [stream]); + }, [stream, transcript]); return data; }; diff --git a/www/app/components/websocket.js b/www/app/components/websocket.js new file mode 100644 index 00000000..9e900867 --- /dev/null +++ b/www/app/components/websocket.js @@ -0,0 +1,46 @@ +import { useEffect } from "react"; + +export const useWebSockets = (transcript_id) => { + useEffect(() => { + if (!transcript_id) return; + + const url = `${process.env.NEXT_PUBLIC_WEBSOCKET_URL}/v1/transcripts/${transcript_id}/events`; + const ws = new WebSocket(url); + console.log("Opening websocket: ", url); + + ws.onopen = () => { + console.log("WebSocket connection opened"); + }; + + ws.onmessage = (event) => { + const message = JSON.parse(event.data); + + switch (message.event) { + case "TRANSCRIPT": + if (!message.data.text) break; + console.log("TRANSCRIPT event:", message.data.text); + break; + case "TOPIC": + console.log("TOPIC event:", message.data); + break; + case "FINAL_SUMMARY": + console.log("FINAL_SUMMARY event:", message.data.summary); + break; + default: + console.error("Unknown event:", message.event); + } + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; + + ws.onclose = () => { + console.log("WebSocket connection closed"); + }; + + return () => { + ws.close(); + }; + }, [transcript_id]); +}; diff --git a/www/app/page.js b/www/app/page.js index 128d8703..1676bd7e 100644 --- a/www/app/page.js +++ b/www/app/page.js @@ -3,14 +3,18 @@ import React, { useState } from "react"; import Recorder from "./components/record.js"; import { Dashboard } from "./components/dashboard.js"; import useWebRTC from "./components/webrtc.js"; +import useCreateTranscript from "./components/transcript.js"; +import { useWebSockets } from "./components/websocket.js"; import "../public/button.css"; const App = () => { const [stream, setStream] = useState(null); - // This is where you'd send the stream and receive the data from the server. - // transcription, summary, etc - const serverData = useWebRTC(stream); + const transcript = useCreateTranscript(); + const serverData = useWebRTC(stream, transcript.response); + const webSockets = useWebSockets(transcript.response?.id); + + console.log("serverData", serverData); const sendStopCmd = () => serverData?.peer?.send(JSON.stringify({ cmd: "STOP" })); diff --git a/www/package.json b/www/package.json index 373edc00..4565e360 100644 --- a/www/package.json +++ b/www/package.json @@ -15,6 +15,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@sentry/nextjs": "^7.61.0", "autoprefixer": "10.4.14", + "axios": "^1.4.0", "fontawesome": "^5.6.3", "jest-worker": "^29.6.2", "next": "^13.4.9", diff --git a/www/yarn.lock b/www/yarn.lock index a4cf875a..2797f71b 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -392,6 +392,11 @@ arg@^5.0.2: resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + autoprefixer@10.4.14: version "10.4.14" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz" @@ -404,6 +409,15 @@ autoprefixer@10.4.14: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -534,6 +548,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^4.0.0: version "4.1.1" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" @@ -566,6 +587,11 @@ debug@4, debug@^4.3.2: dependencies: ms "2.1.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -621,11 +647,25 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + fontawesome@^5.6.3: version "5.6.3" resolved "https://registry.npmjs.org/fontawesome/-/fontawesome-5.6.3.tgz" integrity sha512-FCc+CawwsJWWprVEg9X14yI7zI+l9YVAyhzgu70qwGeDn0tLLDH/dVfqgij72g4BBGgLGfK2qnvFGAmYUkhaWg== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fraction.js@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" @@ -889,6 +929,18 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimatch@^3.0.4: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"