Skip to content

Commit f693900

Browse files
committed
Add transcription view and move controls to bottom
1 parent cb40f0e commit f693900

File tree

4 files changed

+112
-42
lines changed

4 files changed

+112
-42
lines changed

app/page.tsx

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { CloseIcon } from "@/components/CloseIcon";
44
import { NoAgentNotification } from "@/components/NoAgentNotification";
5+
import TranscriptionView from "@/components/TranscriptionView";
6+
import useCombinedTranscriptions from "@/hooks/useCombinedTranscriptions";
57
import {
68
AgentState,
79
BarVisualizer,
@@ -18,8 +20,6 @@ import { useCallback, useEffect, useState } from "react";
1820
import type { ConnectionDetails } from "./api/connection-details/route";
1921

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

2525
const onConnectButtonClicked = useCallback(async () => {
@@ -40,6 +40,7 @@ export default function Page() {
4040
const connectionDetailsData: ConnectionDetails = await response.json();
4141

4242
await room.connect(connectionDetailsData.serverUrl, connectionDetailsData.participantToken);
43+
await room.localParticipant.setMicrophoneEnabled(true);
4344
}, [room]);
4445

4546
useEffect(() => {
@@ -53,36 +54,46 @@ export default function Page() {
5354
return (
5455
<main data-lk-theme="default" className="h-full grid content-center bg-[var(--lk-bg)]">
5556
<RoomContext.Provider value={room}>
56-
<div className="lk-room-container grid grid-rows-[2fr_1fr] items-center">
57-
<SimpleVoiceAssistant onStateChange={setAgentState} />
58-
<ControlBar onConnectButtonClicked={onConnectButtonClicked} agentState={agentState} />
59-
<RoomAudioRenderer />
60-
<NoAgentNotification state={agentState} />
57+
<div className="lk-room-container">
58+
<SimpleVoiceAssistant onConnectButtonClicked={onConnectButtonClicked} />
6159
</div>
6260
</RoomContext.Provider>
6361
</main>
6462
);
6563
}
6664

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

85-
function ControlBar(props: { onConnectButtonClicked: () => void; agentState: AgentState }) {
96+
function ControlBar(props: { onConnectButtonClicked: () => void }) {
8697
/**
8798
* Use Krisp background noise reduction when available.
8899
* Note: This is only available on Scale plan, see {@link https://livekit.io/pricing | LiveKit Pricing} for more details.
@@ -92,35 +103,32 @@ function ControlBar(props: { onConnectButtonClicked: () => void; agentState: Age
92103
krisp.setNoiseFilterEnabled(true);
93104
}, []);
94105

106+
const { state: agentState, audioTrack } = useVoiceAssistant();
107+
95108
return (
96109
<div className="relative h-[100px]">
97110
<AnimatePresence>
98-
{props.agentState === "disconnected" && (
99-
<motion.button
100-
initial={{ opacity: 0, top: 0 }}
101-
animate={{ opacity: 1 }}
102-
exit={{ opacity: 0, top: "-10px" }}
103-
transition={{ duration: 1, ease: [0.09, 1.04, 0.245, 1.055] }}
104-
className="uppercase absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-white text-black rounded-md"
105-
onClick={() => props.onConnectButtonClicked()}
106-
>
107-
Start a conversation
108-
</motion.button>
109-
)}
110-
</AnimatePresence>
111-
<AnimatePresence>
112-
{props.agentState !== "disconnected" && props.agentState !== "connecting" && (
111+
{agentState !== "disconnected" && agentState !== "connecting" && (
113112
<motion.div
114113
initial={{ opacity: 0, top: "10px" }}
115114
animate={{ opacity: 1, top: 0 }}
116115
exit={{ opacity: 0, top: "-10px" }}
117116
transition={{ duration: 0.4, ease: [0.09, 1.04, 0.245, 1.055] }}
118-
className="flex h-8 absolute left-1/2 -translate-x-1/2 justify-center"
117+
className="flex absolute w-full h-full justify-between px-8 sm:px-4"
119118
>
120-
<VoiceAssistantControlBar controls={{ leave: false }} />
121-
<DisconnectButton>
122-
<CloseIcon />
123-
</DisconnectButton>
119+
<BarVisualizer
120+
state={agentState}
121+
barCount={5}
122+
trackRef={audioTrack}
123+
className="agent-visualizer w-32 gap-2"
124+
options={{ minHeight: 12 }}
125+
/>
126+
<div className="flex items-center">
127+
<VoiceAssistantControlBar controls={{ leave: false }} />
128+
<DisconnectButton>
129+
<CloseIcon />
130+
</DisconnectButton>
131+
</div>
124132
</motion.div>
125133
)}
126134
</AnimatePresence>

components/TranscriptionView.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import useCombinedTranscriptions from "@/hooks/useCombinedTranscriptions";
2+
3+
export default function TranscriptionView() {
4+
const combinedTranscriptions = useCombinedTranscriptions();
5+
6+
return (
7+
<div className="flex flex-col gap-2 overflow-y-auto">
8+
{combinedTranscriptions.map((segment) => (
9+
<div
10+
key={segment.id}
11+
className={
12+
segment.role === "assistant"
13+
? "p-2 self-start fit-content"
14+
: "bg-gray-800 rounded-md p-2 self-end fit-content"
15+
}
16+
>
17+
{segment.text}
18+
</div>
19+
))}
20+
</div>
21+
);
22+
}

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)