add instruction window

This commit is contained in:
2025-12-10 16:48:27 +08:00
parent 974cf0994f
commit 944deec482
4 changed files with 87 additions and 18 deletions

View File

@@ -20,9 +20,10 @@ export interface PhoneSimulatorProps {
onDisconnect: () => void; onDisconnect: () => void;
phoneMode?: "normal" | "capture"; phoneMode?: "normal" | "capture";
onCapture?: (image: File) => void; onCapture?: (image: File) => void;
capturePrompt?: string;
} }
export function PhoneSimulator({ onConnect, onDisconnect, phoneMode = "normal", onCapture }: PhoneSimulatorProps) { export function PhoneSimulator({ onConnect, onDisconnect, phoneMode = "normal", onCapture, capturePrompt }: PhoneSimulatorProps) {
const { config, setUserSettings } = useConfig(); const { config, setUserSettings } = useConfig();
const { setToastMessage } = useToast(); const { setToastMessage } = useToast();
const room = useRoomContext(); const room = useRoomContext();
@@ -517,6 +518,15 @@ export function PhoneSimulator({ onConnect, onDisconnect, phoneMode = "normal",
{/* Center Focus Indicator */} {/* Center Focus Indicator */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 border border-white/50 rounded-sm"></div> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 border border-white/50 rounded-sm"></div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 bg-white/50 rounded-full"></div> <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 bg-white/50 rounded-full"></div>
{/* Prompt Display */}
{capturePrompt && (
<div className="absolute top-32 left-0 w-full px-6 text-center z-20">
<div className="inline-block bg-black/60 backdrop-blur-md text-white px-4 py-3 rounded-2xl text-sm font-medium shadow-lg border border-white/10 max-w-full break-words">
{capturePrompt}
</div>
</div>
)}
</div> </div>
)} )}

View File

@@ -66,6 +66,7 @@ export default function Playground({
const tracks = useTracks(); const tracks = useTracks();
const room = useRoomContext(); const room = useRoomContext();
const [phoneMode, setPhoneMode] = useState<"normal" | "capture">("normal"); const [phoneMode, setPhoneMode] = useState<"normal" | "capture">("normal");
const [capturePrompt, setCapturePrompt] = useState<string>("");
const [rpcMethod, setRpcMethod] = useState(""); const [rpcMethod, setRpcMethod] = useState("");
const [rpcPayload, setRpcPayload] = useState(""); const [rpcPayload, setRpcPayload] = useState("");
@@ -106,7 +107,17 @@ export default function Playground({
localParticipant.registerRpcMethod( localParticipant.registerRpcMethod(
'enterImageCaptureMode', 'enterImageCaptureMode',
async () => { async (data: RpcInvocationData) => {
if (data.payload) {
try {
const payload = JSON.parse(data.payload);
if (payload.prompt) {
setCapturePrompt(payload.prompt);
}
} catch (e) {
console.error("Failed to parse enterImageCaptureMode payload", e);
}
}
setPhoneMode("capture"); setPhoneMode("capture");
return JSON.stringify({ success: true }); return JSON.stringify({ success: true });
} }
@@ -116,6 +127,7 @@ export default function Playground({
'exitImageCaptureMode', 'exitImageCaptureMode',
async () => { async () => {
setPhoneMode("normal"); setPhoneMode("normal");
setCapturePrompt("");
return JSON.stringify({ success: true }); return JSON.stringify({ success: true });
} }
); );
@@ -315,6 +327,24 @@ export default function Playground({
voiceAssistant.agent, voiceAssistant.agent,
]); ]);
const instructionsContent = (
<ConfigurationPanelItem title="Instructions">
<textarea
className="w-full bg-gray-950 text-white text-sm p-3 rounded-md border border-gray-800 focus:border-gray-600 focus:outline-none transition-colors resize-none disabled:opacity-50 disabled:cursor-not-allowed"
style={{ minHeight: "80px" }}
rows={3}
placeholder="Enter system instructions for the agent..."
value={config.settings.instructions}
onChange={(e) => {
const newSettings = { ...config.settings };
newSettings.instructions = e.target.value;
setUserSettings(newSettings);
}}
disabled={roomState !== ConnectionState.Disconnected}
/>
</ConfigurationPanelItem>
);
const handleRpcCall = useCallback(async () => { const handleRpcCall = useCallback(async () => {
if (!voiceAssistant.agent || !room) { if (!voiceAssistant.agent || !room) {
throw new Error("No agent or room available"); throw new Error("No agent or room available");
@@ -599,6 +629,7 @@ export default function Playground({
onConnect={() => onConnect(true)} onConnect={() => onConnect(true)}
onDisconnect={() => onConnect(false)} onDisconnect={() => onConnect(false)}
phoneMode={phoneMode} phoneMode={phoneMode}
capturePrompt={capturePrompt}
onCapture={async (content: File) => { onCapture={async (content: File) => {
if (localParticipant) { if (localParticipant) {
await localParticipant.sendFile(content, { topic: "image" }); await localParticipant.sendFile(content, { topic: "image" });
@@ -616,6 +647,19 @@ export default function Playground({
title: "Chat", title: "Chat",
content: chatTileContent, content: chatTileContent,
}); });
mobileTabs.push({
title: "Instructions",
content: (
<PlaygroundTile
padding={false}
backgroundColor="gray-950"
className="h-full w-full grow items-start overflow-y-auto"
childrenClassName="h-full grow items-start"
>
{instructionsContent}
</PlaygroundTile>
),
});
} }
mobileTabs.push({ mobileTabs.push({
@@ -672,6 +716,7 @@ export default function Playground({
onConnect={() => onConnect(true)} onConnect={() => onConnect(true)}
onDisconnect={() => onConnect(false)} onDisconnect={() => onConnect(false)}
phoneMode={phoneMode} phoneMode={phoneMode}
capturePrompt={capturePrompt}
onCapture={async (content: File) => { onCapture={async (content: File) => {
if (localParticipant) { if (localParticipant) {
await localParticipant.sendFile(content, { topic: "image" }); await localParticipant.sendFile(content, { topic: "image" });
@@ -683,12 +728,22 @@ export default function Playground({
</div> </div>
{config.settings.chat && ( {config.settings.chat && (
<PlaygroundTile <div className="flex flex-col h-full grow basis-1/4 hidden lg:flex gap-4">
title="Chat" <PlaygroundTile
className="h-full grow basis-1/4 hidden lg:flex" padding={false}
> backgroundColor="gray-950"
{chatTileContent} className="h-auto w-full flex-none min-h-0"
</PlaygroundTile> childrenClassName="items-start"
>
{instructionsContent}
</PlaygroundTile>
<PlaygroundTile
title="Chat"
className="w-full grow min-h-0"
>
{chatTileContent}
</PlaygroundTile>
</div>
)} )}
<PlaygroundTile <PlaygroundTile
padding={false} padding={false}

View File

@@ -40,6 +40,7 @@ export type UserSettings = {
participant_id: string; participant_id: string;
participant_name: string; participant_name: string;
agent_name?: string; agent_name?: string;
instructions?: string;
metadata?: string; metadata?: string;
attributes?: AttributeItem[]; attributes?: AttributeItem[];
}; };
@@ -67,6 +68,7 @@ const defaultConfig: AppConfig = {
room_name: "", room_name: "",
participant_id: "", participant_id: "",
participant_name: "", participant_name: "",
instructions: "",
metadata: "", metadata: "",
attributes: [], attributes: [],
}, },
@@ -140,6 +142,7 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
room_name: "", room_name: "",
participant_id: "", participant_id: "",
participant_name: "", participant_name: "",
instructions: "",
} as UserSettings; } as UserSettings;
}, [appConfig]); }, [appConfig]);

View File

@@ -72,19 +72,19 @@ export const ConnectionProvider = ({
if (config.settings.metadata) { if (config.settings.metadata) {
body.metadata = config.settings.metadata; body.metadata = config.settings.metadata;
} }
const attributes: Record<string, string> = {};
if (config.settings.instructions) {
attributes.instructions = config.settings.instructions;
}
const attributesArray = Array.isArray(config.settings.attributes) const attributesArray = Array.isArray(config.settings.attributes)
? config.settings.attributes ? config.settings.attributes
: []; : [];
if (attributesArray?.length) { attributesArray.forEach((attr) => {
const attributes = attributesArray.reduce( if (attr.key) {
(acc, attr) => { attributes[attr.key] = attr.value;
if (attr.key) { }
acc[attr.key] = attr.value; });
} if (Object.keys(attributes).length > 0) {
return acc;
},
{} as Record<string, string>,
);
body.attributes = attributes; body.attributes = attributes;
} }
const { accessToken } = await fetch(`/api/token`, { const { accessToken } = await fetch(`/api/token`, {
@@ -111,6 +111,7 @@ export const ConnectionProvider = ({
config.settings.participant_id, config.settings.participant_id,
config.settings.metadata, config.settings.metadata,
config.settings.attributes, config.settings.attributes,
config.settings.instructions,
generateToken, generateToken,
setToastMessage, setToastMessage,
], ],