Skip to content

Commit 6054e8b

Browse files
authored
Add transcription view and move controls to bottom (#88)
1 parent 1bcfdf3 commit 6054e8b

File tree

4 files changed

+123
-43
lines changed

4 files changed

+123
-43
lines changed

app/page.tsx

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { CloseIcon } from "@/components/CloseIcon";
44
import { NoAgentNotification } from "@/components/NoAgentNotification";
5+
import TranscriptionView from "@/components/TranscriptionView";
56
import {
6-
AgentState,
77
BarVisualizer,
88
DisconnectButton,
99
RoomAudioRenderer,
@@ -18,8 +18,6 @@ import { useCallback, useEffect, useState } from "react";
1818
import type { ConnectionDetails } from "./api/connection-details/route";
1919

2020
export default function Page() {
21-
const [agentState, setAgentState] = useState<AgentState>("disconnected");
22-
2321
const [room] = useState(new Room());
2422

2523
const onConnectButtonClicked = useCallback(async () => {
@@ -54,36 +52,46 @@ export default function Page() {
5452
return (
5553
<main data-lk-theme="default" className="h-full grid content-center bg-[var(--lk-bg)]">
5654
<RoomContext.Provider value={room}>
57-
<div className="lk-room-container grid grid-rows-[2fr_1fr] items-center">
58-
<SimpleVoiceAssistant onStateChange={setAgentState} />
59-
<ControlBar onConnectButtonClicked={onConnectButtonClicked} agentState={agentState} />
60-
<RoomAudioRenderer />
61-
<NoAgentNotification state={agentState} />
55+
<div className="lk-room-container max-h-[90vh]">
56+
<SimpleVoiceAssistant onConnectButtonClicked={onConnectButtonClicked} />
6257
</div>
6358
</RoomContext.Provider>
6459
</main>
6560
);
6661
}
6762

68-
function SimpleVoiceAssistant(props: { onStateChange: (state: AgentState) => void }) {
69-
const { state, audioTrack } = useVoiceAssistant();
70-
useEffect(() => {
71-
props.onStateChange(state);
72-
}, [props, state]);
63+
function SimpleVoiceAssistant(props: { onConnectButtonClicked: () => void }) {
64+
const { state: agentState } = useVoiceAssistant();
7365
return (
74-
<div className="h-[300px] max-w-[90vw] mx-auto">
75-
<BarVisualizer
76-
state={state}
77-
barCount={5}
78-
trackRef={audioTrack}
79-
className="agent-visualizer"
80-
options={{ minHeight: 24 }}
81-
/>
82-
</div>
66+
<>
67+
<AnimatePresence>
68+
{agentState === "disconnected" && (
69+
<motion.button
70+
initial={{ opacity: 0, top: 0 }}
71+
animate={{ opacity: 1 }}
72+
exit={{ opacity: 0, top: "-10px" }}
73+
transition={{ duration: 1, ease: [0.09, 1.04, 0.245, 1.055] }}
74+
className="uppercase absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-white text-black rounded-md"
75+
onClick={() => props.onConnectButtonClicked()}
76+
>
77+
Start a conversation
78+
</motion.button>
79+
)}
80+
<div className="w-3/4 lg:w-1/2 mx-auto h-full">
81+
<TranscriptionView />
82+
</div>
83+
</AnimatePresence>
84+
85+
<RoomAudioRenderer />
86+
<NoAgentNotification state={agentState} />
87+
<div className="fixed bottom-0 w-full px-4 py-2">
88+
<ControlBar />
89+
</div>
90+
</>
8391
);
8492
}
8593

86-
function ControlBar(props: { onConnectButtonClicked: () => void; agentState: AgentState }) {
94+
function ControlBar() {
8795
/**
8896
* Use Krisp background noise reduction when available.
8997
* Note: This is only available on Scale plan, see {@link https://livekit.io/pricing | LiveKit Pricing} for more details.
@@ -93,35 +101,32 @@ function ControlBar(props: { onConnectButtonClicked: () => void; agentState: Age
93101
krisp.setNoiseFilterEnabled(true);
94102
}, []);
95103

104+
const { state: agentState, audioTrack } = useVoiceAssistant();
105+
96106
return (
97107
<div className="relative h-[100px]">
98108
<AnimatePresence>
99-
{props.agentState === "disconnected" && (
100-
<motion.button
101-
initial={{ opacity: 0, top: 0 }}
102-
animate={{ opacity: 1 }}
103-
exit={{ opacity: 0, top: "-10px" }}
104-
transition={{ duration: 1, ease: [0.09, 1.04, 0.245, 1.055] }}
105-
className="uppercase absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-white text-black rounded-md"
106-
onClick={() => props.onConnectButtonClicked()}
107-
>
108-
Start a conversation
109-
</motion.button>
110-
)}
111-
</AnimatePresence>
112-
<AnimatePresence>
113-
{props.agentState !== "disconnected" && props.agentState !== "connecting" && (
109+
{agentState !== "disconnected" && agentState !== "connecting" && (
114110
<motion.div
115111
initial={{ opacity: 0, top: "10px" }}
116112
animate={{ opacity: 1, top: 0 }}
117113
exit={{ opacity: 0, top: "-10px" }}
118114
transition={{ duration: 0.4, ease: [0.09, 1.04, 0.245, 1.055] }}
119-
className="flex h-8 absolute left-1/2 -translate-x-1/2 justify-center"
115+
className="flex absolute w-full h-full justify-between px-8 sm:px-4"
120116
>
121-
<VoiceAssistantControlBar controls={{ leave: false }} />
122-
<DisconnectButton>
123-
<CloseIcon />
124-
</DisconnectButton>
117+
<BarVisualizer
118+
state={agentState}
119+
barCount={5}
120+
trackRef={audioTrack}
121+
className="agent-visualizer w-24 gap-2"
122+
options={{ minHeight: 12 }}
123+
/>
124+
<div className="flex items-center">
125+
<VoiceAssistantControlBar controls={{ leave: false }} />
126+
<DisconnectButton>
127+
<CloseIcon />
128+
</DisconnectButton>
129+
</div>
125130
</motion.div>
126131
)}
127132
</AnimatePresence>

components/TranscriptionView.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import useCombinedTranscriptions from "@/hooks/useCombinedTranscriptions";
2+
import * as React from "react";
3+
4+
export default function TranscriptionView() {
5+
const combinedTranscriptions = useCombinedTranscriptions();
6+
7+
// scroll to bottom when new transcription is added
8+
React.useEffect(() => {
9+
const transcription = combinedTranscriptions[combinedTranscriptions.length - 1];
10+
if (transcription) {
11+
const transcriptionElement = document.getElementById(transcription.id);
12+
if (transcriptionElement) {
13+
transcriptionElement.scrollIntoView({ behavior: "smooth" });
14+
}
15+
}
16+
}, [combinedTranscriptions]);
17+
18+
return (
19+
<div className="h-full flex flex-col gap-2 overflow-y-auto">
20+
{combinedTranscriptions.map((segment) => (
21+
<div
22+
id={segment.id}
23+
key={segment.id}
24+
className={
25+
segment.role === "assistant"
26+
? "p-2 self-start fit-content"
27+
: "bg-gray-800 rounded-md p-2 self-end fit-content"
28+
}
29+
>
30+
{segment.text}
31+
</div>
32+
))}
33+
</div>
34+
);
35+
}

hooks/useCombinedTranscriptions.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useTrackTranscription, useVoiceAssistant } from "@livekit/components-react";
2+
import { useMemo } from "react";
3+
import useLocalMicTrack from "./useLocalMicTrack";
4+
5+
export default function useCombinedTranscriptions() {
6+
const { agentTranscriptions } = useVoiceAssistant();
7+
8+
const micTrackRef = useLocalMicTrack();
9+
const { segments: userTranscriptions } = useTrackTranscription(micTrackRef);
10+
11+
const combinedTranscriptions = useMemo(() => {
12+
return [
13+
...agentTranscriptions.map((val) => {
14+
return { ...val, role: "assistant" };
15+
}),
16+
...userTranscriptions.map((val) => {
17+
return { ...val, role: "user" };
18+
}),
19+
].sort((a, b) => a.firstReceivedTime - b.firstReceivedTime);
20+
}, [agentTranscriptions, userTranscriptions]);
21+
22+
return combinedTranscriptions;
23+
}

hooks/useLocalMicTrack.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TrackReferenceOrPlaceholder, useLocalParticipant } from "@livekit/components-react";
2+
import { Track } from "livekit-client";
3+
import { useMemo } from "react";
4+
5+
export default function useLocalMicTrack() {
6+
const { microphoneTrack, localParticipant } = useLocalParticipant();
7+
8+
const micTrackRef: TrackReferenceOrPlaceholder = useMemo(() => {
9+
return {
10+
participant: localParticipant,
11+
source: Track.Source.Microphone,
12+
publication: microphoneTrack,
13+
};
14+
}, [localParticipant, microphoneTrack]);
15+
16+
return micTrackRef;
17+
}

0 commit comments

Comments
 (0)