mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
layout and design improvements
This commit is contained in:
@@ -4,20 +4,19 @@ import {
|
||||
useFiefUserinfo,
|
||||
} from "@fief/fief/nextjs/react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function UserInfo() {
|
||||
const isAuthenticated = useFiefIsAuthenticated();
|
||||
const userinfo = useFiefUserinfo();
|
||||
|
||||
return !isAuthenticated ? (
|
||||
<span className="hover:underline font-thin px-2">
|
||||
<span className="hover:underline underline-offset-2 decoration-[.5px] font-light px-2">
|
||||
<Link href="/login">Log in or create account</Link>
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-thin px-2">
|
||||
<span className="font-light px-2">
|
||||
{userinfo?.email} (
|
||||
<span className="hover:underline">
|
||||
<span className="hover:underline underline-offset-2 decoration-[.5px]">
|
||||
<Link href="/logout">Log out</Link>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import UserInfo from "./(auth)/userInfo";
|
||||
import { ErrorProvider } from "./(errors)/errorContext";
|
||||
import ErrorMessage from "./(errors)/errorMessage";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
|
||||
|
||||
@@ -63,11 +64,11 @@ export default function RootLayout({ children }) {
|
||||
{/*TODO lvh or svh ? */}
|
||||
<div
|
||||
id="container"
|
||||
className="flex flex-col items-center min-h-[100svh] p-2 md:p-4"
|
||||
className="items-center h-[100svh] p-2 md:p-4 grid grid-rows-layout gap-2 md:gap-4"
|
||||
>
|
||||
<header className="flex justify-between items-center w-full mb-4 md:mb-4">
|
||||
<header className="flex justify-between items-center w-full">
|
||||
{/* Logo on the left */}
|
||||
<div className="flex">
|
||||
<Link href="/" className="flex">
|
||||
<Image
|
||||
src="/reach.png"
|
||||
width={16}
|
||||
@@ -81,7 +82,7 @@ export default function RootLayout({ children }) {
|
||||
Capture The Signal, Not The Noise
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{/* Text link on the right */}
|
||||
<UserInfo />
|
||||
</header>
|
||||
|
||||
@@ -8,13 +8,9 @@ button {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
/* Visual */
|
||||
/* background-color: #3e68ff; */
|
||||
/* color: #fff; */
|
||||
border-radius: 8px;
|
||||
/* box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18); */
|
||||
/* Size */
|
||||
padding: 0.4em 1em;
|
||||
min-width: 10ch;
|
||||
min-height: 44px;
|
||||
/* Text */
|
||||
text-align: center;
|
||||
|
||||
@@ -12,18 +12,6 @@ body {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.temp-transcription {
|
||||
background: rgb(151 190 255);
|
||||
border-radius: 5px;
|
||||
border: solid 1px #808080;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.temp-transcription h2 {
|
||||
font-weight: bold;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.Dropdown-placeholder {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
@@ -31,8 +19,6 @@ body {
|
||||
top: 47% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.audio-source-dropdown .Dropdown-control {
|
||||
max-width: 200px;
|
||||
}
|
||||
.Dropdown-control.Dropdown-disabled {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import getApi from "../../lib/getApi";
|
||||
import useTranscript from "../useTranscript";
|
||||
import useTopics from "../useTopics";
|
||||
import useWaveform from "../useWaveform";
|
||||
import { Dashboard } from "../dashboard";
|
||||
import { TopicList } from "../topicList";
|
||||
import Recorder from "../recorder";
|
||||
import { Topic } from "../webSocketTypes";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "../../styles/button.css";
|
||||
import FinalSummary from "../finalSummary";
|
||||
|
||||
type TranscriptDetails = {
|
||||
params: {
|
||||
@@ -34,34 +35,37 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{transcript?.loading === true ||
|
||||
waveform?.loading == true ||
|
||||
topics?.loading == true ? (
|
||||
<Modal
|
||||
title="Loading"
|
||||
text={"Loading transcript..." + transcript.loading}
|
||||
{transcript?.loading === true ||
|
||||
waveform?.loading == true ||
|
||||
topics?.loading == true ? (
|
||||
<Modal
|
||||
title="Loading"
|
||||
text={"Loading transcript..." + transcript.loading}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Recorder
|
||||
topics={topics?.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
waveform={waveform?.waveform}
|
||||
isPastMeeting={true}
|
||||
transcriptId={transcript?.response?.id}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Recorder
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
|
||||
<TopicList
|
||||
topics={topics?.topics || []}
|
||||
useActiveTopic={useActiveTopic}
|
||||
waveform={waveform?.waveform}
|
||||
isPastMeeting={true}
|
||||
transcriptId={transcript?.response?.id}
|
||||
/>
|
||||
|
||||
<Dashboard
|
||||
transcriptionText={""}
|
||||
finalSummary={{ summary: transcript?.response?.longSummary }}
|
||||
topics={topics?.topics || []}
|
||||
disconnected={false}
|
||||
useActiveTopic={useActiveTopic}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<section className="relative w-full h-auto max-h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
|
||||
<div className="py-2 h-auto">
|
||||
{transcript?.response?.longSummary && (
|
||||
<FinalSummary text={transcript?.response?.longSummary} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faMicrophone } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Dropdown, { Option } from "react-dropdown";
|
||||
import "react-dropdown/style.css";
|
||||
|
||||
const AudioInputsDropdown: React.FC<{
|
||||
audioDevices?: Option[];
|
||||
audioDevices: Option[];
|
||||
disabled: boolean;
|
||||
setDeviceId: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
disabled?: boolean;
|
||||
}> = (props) => {
|
||||
}> = ({ audioDevices, disabled, setDeviceId }) => {
|
||||
const [ddOptions, setDdOptions] = useState<Option[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.audioDevices) {
|
||||
setDdOptions(props.audioDevices);
|
||||
props.setDeviceId(
|
||||
props.audioDevices.length > 0 ? props.audioDevices[0].value : null,
|
||||
);
|
||||
if (audioDevices) {
|
||||
setDdOptions(audioDevices);
|
||||
setDeviceId(audioDevices.length > 0 ? audioDevices[0].value : null);
|
||||
}
|
||||
}, [props.audioDevices]);
|
||||
}, [audioDevices]);
|
||||
|
||||
const handleDropdownChange = (option: Option) => {
|
||||
props.setDeviceId(option.value);
|
||||
setDeviceId(option.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
options={ddOptions}
|
||||
onChange={handleDropdownChange}
|
||||
value={ddOptions[0]}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
);
|
||||
if (audioDevices?.length > 0) {
|
||||
return (
|
||||
<div className="flex w-full items-center">
|
||||
<FontAwesomeIcon icon={faMicrophone} className="p-2" />
|
||||
<Dropdown
|
||||
options={ddOptions}
|
||||
onChange={handleDropdownChange}
|
||||
value={ddOptions[0]}
|
||||
className="flex-grow"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default AudioInputsDropdown;
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChevronRight,
|
||||
faChevronDown,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatTime } from "../lib/time";
|
||||
import ScrollToBottom from "./scrollToBottom";
|
||||
import DisconnectedIndicator from "./disconnectedIndicator";
|
||||
import LiveTrancription from "./liveTranscription";
|
||||
import FinalSummary from "./finalSummary";
|
||||
import { Topic, FinalSummary as FinalSummaryType } from "./webSocketTypes";
|
||||
|
||||
type DashboardProps = {
|
||||
transcriptionText: string;
|
||||
finalSummary: FinalSummaryType;
|
||||
topics: Topic[];
|
||||
disconnected: boolean;
|
||||
useActiveTopic: [
|
||||
Topic | null,
|
||||
React.Dispatch<React.SetStateAction<Topic | null>>,
|
||||
];
|
||||
};
|
||||
|
||||
export function Dashboard({
|
||||
transcriptionText,
|
||||
finalSummary,
|
||||
topics,
|
||||
disconnected,
|
||||
useActiveTopic,
|
||||
}: DashboardProps) {
|
||||
const [activeTopic, setActiveTopic] = useActiveTopic;
|
||||
const [autoscrollEnabled, setAutoscrollEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoscrollEnabled) scrollToBottom();
|
||||
console.log(topics);
|
||||
}, [topics.length]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const topicsDiv = document.getElementById("topics-div");
|
||||
|
||||
if (!topicsDiv)
|
||||
console.error("Could not find topics div to scroll to bottom");
|
||||
else topicsDiv.scrollTop = topicsDiv.scrollHeight;
|
||||
};
|
||||
|
||||
const handleScroll = (e) => {
|
||||
const bottom =
|
||||
e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
|
||||
if (!bottom && autoscrollEnabled) {
|
||||
setAutoscrollEnabled(false);
|
||||
} else if (bottom && !autoscrollEnabled) {
|
||||
setAutoscrollEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-4 grid grid-cols-1 lg:grid-cols-2 gap-2 lg:gap-4 grid-rows-2 lg:grid-rows-1 h-outer-dashboard md:h-outer-dashboard-md lg:h-outer-dashboard-lg">
|
||||
{/* Topic Section */}
|
||||
<section className="relative w-full h-auto max-h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
|
||||
{topics.length > 0 ? (
|
||||
<>
|
||||
<ScrollToBottom
|
||||
visible={!autoscrollEnabled}
|
||||
hasFinalSummary={finalSummary ? true : false}
|
||||
handleScrollBottom={scrollToBottom}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="topics-div"
|
||||
className="overflow-y-auto h-auto max-h-full"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{topics.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-b-2 last:border-none px-2 md:px-4 py-2 hover:bg-blue-400/20"
|
||||
role="button"
|
||||
onClick={() =>
|
||||
setActiveTopic(activeTopic?.id == item.id ? null : item)
|
||||
}
|
||||
>
|
||||
<div className="flex justify-between items-center rounded-lg md:rounded-xl text-l md:text-xl font-bold">
|
||||
<p>
|
||||
<span className="font-light text-slate-500 pr-1">
|
||||
[{formatTime(item.timestamp)}]
|
||||
</span>
|
||||
|
||||
<span className="pr-1">{item.title}</span>
|
||||
</p>
|
||||
<FontAwesomeIcon
|
||||
className="transform transition-transform duration-200"
|
||||
icon={
|
||||
activeTopic?.id == item.id
|
||||
? faChevronDown
|
||||
: faChevronRight
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{activeTopic?.id == item.id && (
|
||||
<div className="p-2 mt-2 -mb-2 h-fit">
|
||||
{item.transcript}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 p-4">
|
||||
Discussion topics will appear here after you start recording. It may
|
||||
take up to 5 minutes of conversation for the first topic to appear.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="relative w-full h-auto max-h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
|
||||
{finalSummary.summary ? (
|
||||
<FinalSummary text={finalSummary.summary} />
|
||||
) : (
|
||||
<LiveTrancription text={transcriptionText} />
|
||||
)}
|
||||
</section>
|
||||
|
||||
{disconnected && <DisconnectedIndicator />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ type FinalSummaryProps = {
|
||||
export default function FinalSummary(props: FinalSummaryProps) {
|
||||
return (
|
||||
<div className="overflow-y-auto h-auto max-h-full">
|
||||
<h2 className="text-lg">Final Summary</h2>
|
||||
<h2 className="text-xl font-bold">Final Summary</h2>
|
||||
<p>{props.text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ type LiveTranscriptionProps = {
|
||||
export default function LiveTrancription(props: LiveTranscriptionProps) {
|
||||
return (
|
||||
<div className="text-center p-4">
|
||||
<p className="text-lg md:text-xl font-bold">
|
||||
<p className="text-lg md:text-xl font-bold line-clamp-4">
|
||||
{/* Nous allons prendre quelques appels téléphoniques et répondre à quelques questions */}
|
||||
{props.text}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Recorder from "../recorder";
|
||||
import { Dashboard } from "../dashboard";
|
||||
import { TopicList } from "../topicList";
|
||||
import useWebRTC from "../useWebRTC";
|
||||
import useTranscript from "../useTranscript";
|
||||
import { useWebSockets } from "../useWebSockets";
|
||||
@@ -10,11 +10,15 @@ import "../../styles/button.css";
|
||||
import { Topic } from "../webSocketTypes";
|
||||
import getApi from "../../lib/getApi";
|
||||
import AudioInputsDropdown from "../audioInputsDropdown";
|
||||
import LiveTrancription from "../liveTranscription";
|
||||
import DisconnectedIndicator from "../disconnectedIndicator";
|
||||
|
||||
const TranscriptCreate = () => {
|
||||
const [stream, setStream] = useState<MediaStream | null>(null);
|
||||
const [disconnected, setDisconnected] = useState<boolean>(false);
|
||||
const useActiveTopic = useState<Topic | null>(null);
|
||||
const [deviceId, setDeviceId] = useState<string | null>(null);
|
||||
const [recordStarted, setRecordStarted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_ENV === "development") {
|
||||
@@ -40,13 +44,18 @@ const TranscriptCreate = () => {
|
||||
} = useAudioDevice();
|
||||
|
||||
const getCurrentStream = async () => {
|
||||
return audioDevices.length
|
||||
? await getAudioStream(audioDevices[0].value)
|
||||
: null;
|
||||
setRecordStarted(true);
|
||||
return deviceId ? await getAudioStream(deviceId) : null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (audioDevices.length > 0) {
|
||||
setDeviceId[audioDevices[0].value];
|
||||
}
|
||||
}, [audioDevices]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<>
|
||||
{permissionOk ? (
|
||||
<>
|
||||
<Recorder
|
||||
@@ -60,14 +69,28 @@ const TranscriptCreate = () => {
|
||||
useActiveTopic={useActiveTopic}
|
||||
isPastMeeting={false}
|
||||
/>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 grid-rows-2 lg:grid-rows-1 gap-2 lg:gap-4 h-full">
|
||||
<TopicList
|
||||
topics={webSockets.topics}
|
||||
useActiveTopic={useActiveTopic}
|
||||
/>
|
||||
<div className="h-full flex flex-col">
|
||||
<section className="mb-2">
|
||||
<AudioInputsDropdown
|
||||
setDeviceId={setDeviceId}
|
||||
audioDevices={audioDevices}
|
||||
disabled={recordStarted}
|
||||
/>
|
||||
</section>
|
||||
<section className="w-full h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
|
||||
<div className="py-2 h-auto">
|
||||
<LiveTrancription text={webSockets.transcriptText} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Dashboard
|
||||
transcriptionText={webSockets.transcriptText}
|
||||
finalSummary={webSockets.finalSummary}
|
||||
topics={webSockets.topics}
|
||||
disconnected={disconnected}
|
||||
useActiveTopic={useActiveTopic}
|
||||
/>
|
||||
{disconnected && <DisconnectedIndicator />}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -87,7 +110,7 @@ const TranscriptCreate = () => {
|
||||
: "Please grant permission to continue."}
|
||||
</p>
|
||||
<button
|
||||
className="mt-4 bg-black/40 hover:bg-black/60 text-white font-bold py-2 px-4 rounded m-auto"
|
||||
className="mt-4 bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded m-auto"
|
||||
onClick={requestPermission}
|
||||
disabled={permissionDenied}
|
||||
>
|
||||
@@ -98,7 +121,7 @@ const TranscriptCreate = () => {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -213,10 +213,7 @@ export default function Recorder(props: RecorderProps) {
|
||||
return (
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex-grow items-end relative">
|
||||
<div
|
||||
ref={waveformRef}
|
||||
className="flex-grow shadow-lg md:shadow-xl rounded-2xl h-20"
|
||||
></div>
|
||||
<div ref={waveformRef} className="flex-grow rounded-2xl h-20"></div>
|
||||
<div className="absolute right-2 bottom-0">
|
||||
{isRecording && (
|
||||
<div className="inline-block bg-red-500 rounded-full w-2 h-2 my-auto mr-1 animate-ping"></div>
|
||||
@@ -229,8 +226,10 @@ export default function Recorder(props: RecorderProps) {
|
||||
<>
|
||||
<button
|
||||
className={`${
|
||||
isPlaying ? "bg-orange-400" : "bg-green-400"
|
||||
} ml-2 md:ml:4 text-white`}
|
||||
isPlaying
|
||||
? "bg-orange-400 hover:bg-orange-500"
|
||||
: "bg-green-400 hover:bg-green-500"
|
||||
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
|
||||
id="play-btn"
|
||||
onClick={handlePlayClick}
|
||||
disabled={isRecording}
|
||||
@@ -244,7 +243,7 @@ export default function Recorder(props: RecorderProps) {
|
||||
className="text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2"
|
||||
href={`${process.env.NEXT_PUBLIC_API_URL}/v1/transcripts/${props.transcriptId}/audio/mp3`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<FontAwesomeIcon icon={faDownload} className="h-5 w-auto" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -254,7 +253,7 @@ export default function Recorder(props: RecorderProps) {
|
||||
title="Download recording"
|
||||
className="invisible text-center cursor-pointer text-blue-400 hover:text-blue-700 ml-2 md:ml:4 p-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
<FontAwesomeIcon icon={faDownload} className="h-5 w-auto" />
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
@@ -262,8 +261,10 @@ export default function Recorder(props: RecorderProps) {
|
||||
{!hasRecorded && (
|
||||
<button
|
||||
className={`${
|
||||
isRecording ? "bg-red-400" : "bg-blue-400"
|
||||
} text-white ml-2 md:ml:4`}
|
||||
isRecording
|
||||
? "bg-red-400 hover:bg-red-500"
|
||||
: "bg-blue-400 hover:bg-blue-500"
|
||||
} text-white ml-2 md:ml:4 md:h-[78px] md:min-w-[100px] text-lg`}
|
||||
onClick={handleRecClick}
|
||||
disabled={isPlaying}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type ScrollToBottomProps = {
|
||||
visible: boolean;
|
||||
hasFinalSummary: boolean;
|
||||
handleScrollBottom: () => void;
|
||||
};
|
||||
|
||||
export default function ScrollToBottom(props: ScrollToBottomProps) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute right-5 w-10 h-10 ${
|
||||
className={`absolute left-0 w-10 h-10 ${
|
||||
props.visible ? "flex" : "hidden"
|
||||
} ${
|
||||
props.hasFinalSummary ? "top-[49%]" : "bottom-1"
|
||||
} justify-center items-center text-2xl cursor-pointer opacity-70 hover:opacity-100 transition-opacity duration-200 animate-bounce rounded-xl border-slate-400 bg-[#3c82f638] text-[#3c82f6ed]`}
|
||||
} top-[49%] text-2xl cursor-pointer opacity-70 hover:opacity-100 transition-opacity duration-200 animate-bounce rounded-xl text-blue-400`}
|
||||
onClick={() => {
|
||||
props.handleScrollBottom();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
⬇
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
101
www/app/transcripts/topicList.tsx
Normal file
101
www/app/transcripts/topicList.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChevronRight,
|
||||
faChevronDown,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatTime } from "../lib/time";
|
||||
import ScrollToBottom from "./scrollToBottom";
|
||||
import { Topic } from "./webSocketTypes";
|
||||
|
||||
type TopicListProps = {
|
||||
topics: Topic[];
|
||||
useActiveTopic: [
|
||||
Topic | null,
|
||||
React.Dispatch<React.SetStateAction<Topic | null>>,
|
||||
];
|
||||
};
|
||||
|
||||
export function TopicList({ topics, useActiveTopic }: TopicListProps) {
|
||||
const [activeTopic, setActiveTopic] = useActiveTopic;
|
||||
const [autoscrollEnabled, setAutoscrollEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoscrollEnabled) scrollToBottom();
|
||||
console.log(topics);
|
||||
}, [topics.length]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const topicsDiv = document.getElementById("topics-div");
|
||||
|
||||
if (!topicsDiv)
|
||||
console.error("Could not find topics div to scroll to bottom");
|
||||
else topicsDiv.scrollTop = topicsDiv.scrollHeight;
|
||||
};
|
||||
|
||||
const handleScroll = (e) => {
|
||||
const bottom =
|
||||
e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
|
||||
if (!bottom && autoscrollEnabled) {
|
||||
setAutoscrollEnabled(false);
|
||||
} else if (bottom && !autoscrollEnabled) {
|
||||
setAutoscrollEnabled(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="relative w-full h-full bg-blue-400/20 rounded-lg md:rounded-xl px-2 md:px-4 flex flex-col justify-center align-center">
|
||||
{topics.length > 0 ? (
|
||||
<>
|
||||
<ScrollToBottom
|
||||
visible={!autoscrollEnabled}
|
||||
handleScrollBottom={scrollToBottom}
|
||||
/>
|
||||
|
||||
<div
|
||||
id="topics-div"
|
||||
className="overflow-y-auto py-2 h-full"
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{topics.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border-b border-blue-300 last:border-none p-2 hover:bg-blue-400/20"
|
||||
role="button"
|
||||
onClick={() =>
|
||||
setActiveTopic(activeTopic?.id == item.id ? null : item)
|
||||
}
|
||||
>
|
||||
<div className="flex justify-between items-center rounded-lg md:rounded-xl text-lg md:text-xl font-bold">
|
||||
<p>
|
||||
<span className="font-light font-mono text-slate-500 pr-1 text-base md:text-lg">
|
||||
[{formatTime(item.timestamp)}]
|
||||
</span>
|
||||
|
||||
<span className="pr-1">{item.title}</span>
|
||||
</p>
|
||||
<FontAwesomeIcon
|
||||
className="transform transition-transform duration-200 ml-2"
|
||||
icon={
|
||||
activeTopic?.id == item.id
|
||||
? faChevronDown
|
||||
: faChevronRight
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{activeTopic?.id == item.id && (
|
||||
<div className="px-2">{item.transcript}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 p-4">
|
||||
Discussion topics will appear here after you start recording. It may
|
||||
take up to 5 minutes of conversation for the first topic to appear.
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
// 8 margin main container Top + 40 height header + 16 margin bottom header + 80 recorder
|
||||
const dashboardStart = 144;
|
||||
|
||||
// 8 margin main container Top + 40 height header + 16 margin bottom header + 80 recorder
|
||||
const dashboardStartMd = 144;
|
||||
|
||||
// 16 margin main container Top + 64 height header + 16 margin bottom header + 80 recorder
|
||||
const dashboardStartLg = 176;
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
@@ -17,10 +8,8 @@ module.exports = {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
height: {
|
||||
"outer-dashboard": `calc(100svh - ${dashboardStart}px)`,
|
||||
"outer-dashboard-md": `calc(100svh - ${dashboardStartMd + 34}px)`,
|
||||
"outer-dashboard-lg": `calc(100svh - ${dashboardStartLg}px)`,
|
||||
gridTemplateRows: {
|
||||
layout: "auto auto minmax(0, 1fr)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user