Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/components/config/Keyboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { useState, useImperativeHandle, forwardRef } from "react";
import { LocalParticipant } from "livekit-client";

interface KeyboardProps {
localParticipant: LocalParticipant;
className?: string;
}

interface KeyConfig {
label: string;
intKey: number;
strKey: string;
}

const keyConfigs: KeyConfig[] = [
{ label: "1", intKey: 1, strKey: "1" },
{ label: "2", intKey: 2, strKey: "2" },
{ label: "3", intKey: 3, strKey: "3" },
{ label: "4", intKey: 4, strKey: "4" },
{ label: "5", intKey: 5, strKey: "5" },
{ label: "6", intKey: 6, strKey: "6" },
{ label: "7", intKey: 7, strKey: "7" },
{ label: "8", intKey: 8, strKey: "8" },
{ label: "9", intKey: 9, strKey: "9" },
{ label: "*", intKey: 10, strKey: "*" },
{ label: "0", intKey: 0, strKey: "0" },
{ label: "#", intKey: 11, strKey: "#" },
];

export interface KeyboardRef {
setPressedSequence: (key: string[]) => void;
}

export const Keyboard = forwardRef<KeyboardRef, KeyboardProps>(
({ localParticipant, className = "" }, ref) => {
const [pressedKey, setPressedKey] = useState<string | null>(null);
const [pressedSequence, setPressedSequence] = useState<string[]>([]);

useImperativeHandle(ref, () => ({
setPressedSequence,
}));

const handleKeyPress = async (keyConfig: KeyConfig) => {
setPressedKey(keyConfig.label);
setPressedSequence((seq) => [...seq, keyConfig.label]);
console.log("Publishing DTMF:", keyConfig.label);

try {
await localParticipant.publishDtmf(keyConfig.intKey, keyConfig.strKey);
} catch (error) {
console.error("Failed to publish DTMF:", error);
} finally {
setTimeout(() => {
setPressedKey(null);
}, 150);
}
};

return (
<div className={`w-full ${className}`}>
<div className="flex flex-col gap-4">
<div className="mb-2 text-center text-gray-400 text-sm tracking-widest">
{pressedSequence.length > 0 ? pressedSequence.join(" ") : ""}
</div>
<div className="grid grid-cols-3 gap-2 max-w-xs mx-auto">
{keyConfigs.map((keyConfig) => (
<button
key={keyConfig.label}
onClick={() => handleKeyPress(keyConfig)}
className={`
h-14 w-14 flex items-center justify-center
text-lg font-semibold rounded-lg
border transition-all duration-150 ease-out
hover:scale-105 active:scale-95
${
pressedKey === keyConfig.label
? "bg-blue-600 border-blue-500 text-white scale-95"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-500 hover:bg-gray-700"
}
`}
disabled={pressedKey !== null}
>
{keyConfig.label}
</button>
))}
</div>
</div>
</div>
);
}
);

Keyboard.displayName = "Keyboard";
26 changes: 22 additions & 4 deletions src/components/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChatMessageType } from "@/components/chat/ChatTile";
import { ColorPicker } from "@/components/colorPicker/ColorPicker";
import { AudioInputTile } from "@/components/config/AudioInputTile";
import { ConfigurationPanelItem } from "@/components/config/ConfigurationPanelItem";
import { Keyboard, KeyboardRef } from "@/components/config/Keyboard";
import { NameValueRow } from "@/components/config/NameValueRow";
import { PlaygroundHeader } from "@/components/playground/PlaygroundHeader";
import {
Expand All @@ -28,7 +29,7 @@ import {
} from "@livekit/components-react";
import { ConnectionState, LocalParticipant, Track } from "livekit-client";
import { QRCodeSVG } from "qrcode.react";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
import tailwindTheme from "../../lib/tailwindTheme.preval";
import { EditableNameValueRow } from "@/components/config/NameValueRow";
import { AttributesInspector } from "@/components/config/AttributesInspector";
Expand Down Expand Up @@ -67,6 +68,8 @@ export default function Playground({
const [rpcPayload, setRpcPayload] = useState("");
const [showRpc, setShowRpc] = useState(false);

const keyboardRef = useRef<KeyboardRef>(null);

useEffect(() => {
if (roomState === ConnectionState.Connected) {
localParticipant.setCameraEnabled(config.settings.inputs.camera);
Expand Down Expand Up @@ -255,6 +258,16 @@ export default function Playground({
</ConfigurationPanelItem>
)}

{localParticipant && (
<ConfigurationPanelItem title="Phone Keyboard">
<Keyboard
ref={keyboardRef}
localParticipant={localParticipant}
className="px-2"
/>
</ConfigurationPanelItem>
)}

<ConfigurationPanelItem title="Room">
<div className="flex flex-col gap-2">
<EditableNameValueRow
Expand Down Expand Up @@ -550,6 +563,13 @@ export default function Playground({
),
});

const handleConnectClick = () => {
if (roomState !== ConnectionState.Disconnected) {
keyboardRef.current?.setPressedSequence([]);
}
onConnect(roomState === ConnectionState.Disconnected);
};

return (
<>
<PlaygroundHeader
Expand All @@ -559,9 +579,7 @@ export default function Playground({
height={headerHeight}
accentColor={config.settings.theme_color}
connectionState={roomState}
onConnectClicked={() =>
onConnect(roomState === ConnectionState.Disconnected)
}
onConnectClicked={handleConnectClick}
/>
<div
className={`flex gap-4 py-4 grow w-full selection:bg-${config.settings.theme_color}-900`}
Expand Down