create-spatius-app 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/cli.js +9046 -0
- package/dist/cli.js.map +1 -0
- package/dist/templates.js +2856 -0
- package/dist/templates.js.map +1 -0
- package/package.json +79 -0
- package/schemas/result-v1.schema.json +129 -0
- package/schemas/result-v2.schema.json +146 -0
- package/templates/cloudflare-livekit/.editorconfig +15 -0
- package/templates/cloudflare-livekit/.prettierignore +6 -0
- package/templates/cloudflare-livekit/.prettierrc.json +4 -0
- package/templates/cloudflare-livekit/AGENTS.md +41 -0
- package/templates/cloudflare-livekit/README.md +37 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.pip +31 -0
- package/templates/cloudflare-livekit/agent/Dockerfile.uv +29 -0
- package/templates/cloudflare-livekit/agent/README.md +84 -0
- package/templates/cloudflare-livekit/agent/dockerignore +11 -0
- package/templates/cloudflare-livekit/agent/env.example +8 -0
- package/templates/cloudflare-livekit/agent/pyproject.toml +21 -0
- package/templates/cloudflare-livekit/agent/python-version +1 -0
- package/templates/cloudflare-livekit/agent/src/__init__.py +1 -0
- package/templates/cloudflare-livekit/agent/src/agent.py +97 -0
- package/templates/cloudflare-livekit/agent/src/dispatch_metadata.py +64 -0
- package/templates/cloudflare-livekit/agent/tests/test_agent_startup.py +211 -0
- package/templates/cloudflare-livekit/agent/tests/test_dispatch_metadata.py +97 -0
- package/templates/cloudflare-livekit/agent/uv.lock +2272 -0
- package/templates/cloudflare-livekit/components.json +23 -0
- package/templates/cloudflare-livekit/contracts/agent-dispatch-metadata.schema.json +40 -0
- package/templates/cloudflare-livekit/dev.vars.example +12 -0
- package/templates/cloudflare-livekit/eslint.config.mjs +41 -0
- package/templates/cloudflare-livekit/gitignore +30 -0
- package/templates/cloudflare-livekit/index.html +17 -0
- package/templates/cloudflare-livekit/npmrc +4 -0
- package/templates/cloudflare-livekit/package-lock.json +6514 -0
- package/templates/cloudflare-livekit/package.json +66 -0
- package/templates/cloudflare-livekit/playwright.config.ts +29 -0
- package/templates/cloudflare-livekit/pnpm-lock.yaml +4012 -0
- package/templates/cloudflare-livekit/pnpm-workspace.yaml +8 -0
- package/templates/cloudflare-livekit/scripts/agent-runtime.mjs +129 -0
- package/templates/cloudflare-livekit/scripts/command.mjs +52 -0
- package/templates/cloudflare-livekit/scripts/dev-agent.mjs +77 -0
- package/templates/cloudflare-livekit/scripts/dev-web.mjs +18 -0
- package/templates/cloudflare-livekit/scripts/managed-process.mjs +144 -0
- package/templates/cloudflare-livekit/scripts/process-tree.mjs +111 -0
- package/templates/cloudflare-livekit/scripts/run-agent-python.mjs +25 -0
- package/templates/cloudflare-livekit/tests/dev-agent.test.mjs +327 -0
- package/templates/cloudflare-livekit/tests/dev-fixtures.mjs +235 -0
- package/templates/cloudflare-livekit/tests/dev-supervision.test.mjs +203 -0
- package/templates/cloudflare-livekit/tests/e2e/conversation.spec.ts +376 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-avatar.ts +24 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-components.tsx +173 -0
- package/templates/cloudflare-livekit/tests/e2e/fake-livekit.ts +123 -0
- package/templates/cloudflare-livekit/tests/e2e/harness.html +12 -0
- package/templates/cloudflare-livekit/tests/e2e/harness.tsx +1 -0
- package/templates/cloudflare-livekit/tests/e2e/vite.config.ts +48 -0
- package/templates/cloudflare-livekit/tests/web/api.test.ts +64 -0
- package/templates/cloudflare-livekit/tests/web/app.test.tsx +125 -0
- package/templates/cloudflare-livekit/tests/web/audio-visualizer.test.tsx +53 -0
- package/templates/cloudflare-livekit/tests/web/avatar-session.test.ts +119 -0
- package/templates/cloudflare-livekit/tests/web/components.test.tsx +152 -0
- package/templates/cloudflare-livekit/tests/web/conversation.test.tsx +76 -0
- package/templates/cloudflare-livekit/tests/web/session-attempt.test.ts +178 -0
- package/templates/cloudflare-livekit/tsconfig.json +9 -0
- package/templates/cloudflare-livekit/tsconfig.node.json +23 -0
- package/templates/cloudflare-livekit/tsconfig.web.json +26 -0
- package/templates/cloudflare-livekit/tsconfig.worker.json +22 -0
- package/templates/cloudflare-livekit/vite.config.ts +49 -0
- package/templates/cloudflare-livekit/vitest.config.ts +11 -0
- package/templates/cloudflare-livekit/web/App.tsx +176 -0
- package/templates/cloudflare-livekit/web/api.ts +69 -0
- package/templates/cloudflare-livekit/web/assets/FONT-PROVENANCE.md +8 -0
- package/templates/cloudflare-livekit/web/assets/OFL.txt +92 -0
- package/templates/cloudflare-livekit/web/assets/inter-latin.woff2 +0 -0
- package/templates/cloudflare-livekit/web/avatar-session.ts +67 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/LICENSE +201 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/PROVENANCE.md +32 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-audio-visualizer-bar.tsx +43 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-input.tsx +78 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-chat-transcript.tsx +145 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-disconnect-button.tsx +27 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-session-provider.tsx +29 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/agent-track-toggle.tsx +51 -0
- package/templates/cloudflare-livekit/web/components/agents-ui/start-audio-button.tsx +13 -0
- package/templates/cloudflare-livekit/web/components/conversation.tsx +352 -0
- package/templates/cloudflare-livekit/web/components/icons.tsx +39 -0
- package/templates/cloudflare-livekit/web/components/transcript-panel.tsx +12 -0
- package/templates/cloudflare-livekit/web/main.tsx +17 -0
- package/templates/cloudflare-livekit/web/session-attempt.ts +110 -0
- package/templates/cloudflare-livekit/web/styles.css +571 -0
- package/templates/cloudflare-livekit/worker/dispatch-metadata.ts +22 -0
- package/templates/cloudflare-livekit/worker/index.test.ts +152 -0
- package/templates/cloudflare-livekit/worker/index.ts +61 -0
- package/templates/cloudflare-livekit/worker/session.ts +103 -0
- package/templates/cloudflare-livekit/worker-configuration.d.ts +17184 -0
- package/templates/cloudflare-livekit/wrangler.jsonc +27 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Adapted from Agents UI; see ./PROVENANCE.md.
|
|
2
|
+
import {
|
|
3
|
+
useMultibandTrackVolume,
|
|
4
|
+
type TrackReferenceOrPlaceholder,
|
|
5
|
+
} from '@livekit/components-react';
|
|
6
|
+
import type { LocalAudioTrack } from 'livekit-client';
|
|
7
|
+
|
|
8
|
+
/** Microphone-only Agents UI bar visualizer. No simulated agent-state animation. */
|
|
9
|
+
export function AgentAudioVisualizerBar({
|
|
10
|
+
audioTrack,
|
|
11
|
+
enabled,
|
|
12
|
+
}: {
|
|
13
|
+
audioTrack?: LocalAudioTrack | TrackReferenceOrPlaceholder;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
}) {
|
|
16
|
+
const bands = useMultibandTrackVolume(enabled ? audioTrack : undefined, {
|
|
17
|
+
bands: 5,
|
|
18
|
+
loPass: 100,
|
|
19
|
+
hiPass: 200,
|
|
20
|
+
});
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="user-audio"
|
|
24
|
+
role="group"
|
|
25
|
+
aria-label="Your microphone activity"
|
|
26
|
+
>
|
|
27
|
+
<span>You</span>
|
|
28
|
+
<div className="audio-bars" aria-hidden="true" data-enabled={enabled}>
|
|
29
|
+
{Array.from({ length: 5 }, (_, index) => (
|
|
30
|
+
<span
|
|
31
|
+
key={index}
|
|
32
|
+
style={{
|
|
33
|
+
height: `${Math.max(4, (enabled ? Math.min(1, Math.max(0, bands[index] ?? 0)) : 0) * 24)}px`,
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
<span className="audio-label">
|
|
39
|
+
{enabled ? 'Microphone on' : 'Microphone off'}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Extracted/adapted from AgentControlBar; see ./PROVENANCE.md.
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { Icon } from '../icons.js';
|
|
4
|
+
export function AgentChatInput({
|
|
5
|
+
disabled = false,
|
|
6
|
+
onSend,
|
|
7
|
+
}: {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
onSend: (message: string) => Promise<unknown>;
|
|
10
|
+
}) {
|
|
11
|
+
const [message, setMessage] = useState('');
|
|
12
|
+
const [isSending, setIsSending] = useState(false);
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
const sending = useRef(false);
|
|
15
|
+
const isDisabled = disabled || isSending || !message.trim();
|
|
16
|
+
const handleSend = async () => {
|
|
17
|
+
if (isDisabled || sending.current) return;
|
|
18
|
+
sending.current = true;
|
|
19
|
+
setIsSending(true);
|
|
20
|
+
setError('');
|
|
21
|
+
try {
|
|
22
|
+
await onSend(message.trim());
|
|
23
|
+
setMessage('');
|
|
24
|
+
} catch {
|
|
25
|
+
setError('Message not sent. Your text is still here — try again.');
|
|
26
|
+
} finally {
|
|
27
|
+
sending.current = false;
|
|
28
|
+
setIsSending(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return (
|
|
32
|
+
<div className="chat-input-group">
|
|
33
|
+
<form
|
|
34
|
+
className="chat-input"
|
|
35
|
+
onSubmit={(event) => {
|
|
36
|
+
event.preventDefault();
|
|
37
|
+
void handleSend();
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<label className="sr-only" htmlFor="conversation-message">
|
|
41
|
+
Message your agent
|
|
42
|
+
</label>
|
|
43
|
+
<textarea
|
|
44
|
+
id="conversation-message"
|
|
45
|
+
rows={1}
|
|
46
|
+
value={message}
|
|
47
|
+
disabled={disabled || isSending}
|
|
48
|
+
placeholder="Type a message…"
|
|
49
|
+
onChange={(event) => setMessage(event.target.value)}
|
|
50
|
+
onKeyDown={(event) => {
|
|
51
|
+
if (
|
|
52
|
+
event.key === 'Enter' &&
|
|
53
|
+
!event.shiftKey &&
|
|
54
|
+
!event.nativeEvent.isComposing &&
|
|
55
|
+
event.keyCode !== 229
|
|
56
|
+
) {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
void handleSend();
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
<button
|
|
63
|
+
className="send-button"
|
|
64
|
+
type="submit"
|
|
65
|
+
aria-label={isSending ? 'Sending message' : 'Send message'}
|
|
66
|
+
disabled={isDisabled}
|
|
67
|
+
>
|
|
68
|
+
<Icon name="send" />
|
|
69
|
+
</button>
|
|
70
|
+
</form>
|
|
71
|
+
{error && (
|
|
72
|
+
<p role="alert" className="control-error">
|
|
73
|
+
{error}
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Adapted from Agents UI; see ./PROVENANCE.md.
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import type { AgentState, ReceivedMessage } from '@livekit/components-react';
|
|
4
|
+
export type TranscriptMessage = Pick<
|
|
5
|
+
ReceivedMessage,
|
|
6
|
+
'id' | 'timestamp' | 'message'
|
|
7
|
+
> & {
|
|
8
|
+
isUser: boolean;
|
|
9
|
+
source?: 'user' | 'agent' | 'avatar';
|
|
10
|
+
senderIdentity?: string;
|
|
11
|
+
};
|
|
12
|
+
export type ConversationParticipants = {
|
|
13
|
+
agentIdentity?: string;
|
|
14
|
+
avatarIdentity?: string;
|
|
15
|
+
};
|
|
16
|
+
export function snapshotMessages(
|
|
17
|
+
messages: ReceivedMessage[],
|
|
18
|
+
participants: ConversationParticipants = {},
|
|
19
|
+
previous: readonly TranscriptMessage[] = [],
|
|
20
|
+
): TranscriptMessage[] {
|
|
21
|
+
const previousById = new Map(
|
|
22
|
+
previous.map((message) => [message.id, message]),
|
|
23
|
+
);
|
|
24
|
+
return messages.flatMap(({ id, timestamp, message, from, type }) => {
|
|
25
|
+
if (!message.trim()) return [];
|
|
26
|
+
const isUser = type === 'userTranscript' || Boolean(from?.isLocal);
|
|
27
|
+
// The SDK can lose `from` after a participant leaves. Retain attribution by
|
|
28
|
+
// stream ID so the final update and interrupted text still replace in place.
|
|
29
|
+
const prior = previousById.get(id);
|
|
30
|
+
const senderIdentity = from?.identity ?? prior?.senderIdentity;
|
|
31
|
+
const isAvatar =
|
|
32
|
+
Boolean(
|
|
33
|
+
senderIdentity && senderIdentity === participants.avatarIdentity,
|
|
34
|
+
) ||
|
|
35
|
+
Boolean(
|
|
36
|
+
participants.agentIdentity &&
|
|
37
|
+
from?.attributes?.['lk.publish_on_behalf'] ===
|
|
38
|
+
participants.agentIdentity,
|
|
39
|
+
);
|
|
40
|
+
if (
|
|
41
|
+
!isUser &&
|
|
42
|
+
senderIdentity &&
|
|
43
|
+
participants.agentIdentity &&
|
|
44
|
+
senderIdentity !== participants.agentIdentity &&
|
|
45
|
+
!isAvatar
|
|
46
|
+
)
|
|
47
|
+
return [];
|
|
48
|
+
// The avatar relays speech; service chat isn't a voice-agent response.
|
|
49
|
+
if (isAvatar && type !== 'agentTranscript') return [];
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
id,
|
|
53
|
+
timestamp,
|
|
54
|
+
message,
|
|
55
|
+
isUser,
|
|
56
|
+
senderIdentity,
|
|
57
|
+
source: isUser
|
|
58
|
+
? 'user'
|
|
59
|
+
: isAvatar
|
|
60
|
+
? 'avatar'
|
|
61
|
+
: (prior?.source ?? 'agent'),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function AgentChatTranscript({
|
|
67
|
+
messages = [],
|
|
68
|
+
agentState,
|
|
69
|
+
agentName = 'Agent',
|
|
70
|
+
}: {
|
|
71
|
+
messages?: readonly TranscriptMessage[];
|
|
72
|
+
agentState?: AgentState;
|
|
73
|
+
agentName?: string;
|
|
74
|
+
}) {
|
|
75
|
+
const viewport = useRef<HTMLDivElement>(null);
|
|
76
|
+
const atBottom = useRef(true);
|
|
77
|
+
const [showLatest, setShowLatest] = useState(false);
|
|
78
|
+
const scrollToLatest = () => {
|
|
79
|
+
const element = viewport.current;
|
|
80
|
+
if (element) element.scrollTop = element.scrollHeight;
|
|
81
|
+
atBottom.current = true;
|
|
82
|
+
setShowLatest(false);
|
|
83
|
+
};
|
|
84
|
+
useLayoutEffect(() => {
|
|
85
|
+
if (atBottom.current) scrollToLatest();
|
|
86
|
+
else setShowLatest(true);
|
|
87
|
+
}, [messages, agentState]);
|
|
88
|
+
return (
|
|
89
|
+
<div className="transcript-container">
|
|
90
|
+
<div
|
|
91
|
+
ref={viewport}
|
|
92
|
+
className="transcript-scroll"
|
|
93
|
+
role="log"
|
|
94
|
+
aria-label="Conversation transcript"
|
|
95
|
+
aria-live="polite"
|
|
96
|
+
onScroll={(event) => {
|
|
97
|
+
const element = event.currentTarget;
|
|
98
|
+
atBottom.current =
|
|
99
|
+
element.scrollHeight - element.scrollTop - element.clientHeight <
|
|
100
|
+
48;
|
|
101
|
+
if (atBottom.current) setShowLatest(false);
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{messages.length === 0 && (
|
|
105
|
+
<p className="transcript-empty">
|
|
106
|
+
Your conversation will appear here as you speak or type.
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
{messages.map(({ id, timestamp, isUser, message, source }) => (
|
|
110
|
+
<article
|
|
111
|
+
key={id}
|
|
112
|
+
className="transcript-message"
|
|
113
|
+
data-speaker={isUser ? 'user' : 'agent'}
|
|
114
|
+
data-source={source}
|
|
115
|
+
>
|
|
116
|
+
<div className="message-meta">
|
|
117
|
+
<span>{isUser ? 'You' : agentName}</span>
|
|
118
|
+
<time dateTime={new Date(timestamp).toISOString()}>
|
|
119
|
+
{new Date(timestamp).toLocaleTimeString([], {
|
|
120
|
+
hour: '2-digit',
|
|
121
|
+
minute: '2-digit',
|
|
122
|
+
})}
|
|
123
|
+
</time>
|
|
124
|
+
</div>
|
|
125
|
+
<p>{message}</p>
|
|
126
|
+
</article>
|
|
127
|
+
))}
|
|
128
|
+
{agentState === 'thinking' && (
|
|
129
|
+
<p className="thinking-indicator" role="status">
|
|
130
|
+
Thinking<span aria-hidden="true">…</span>
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
{showLatest && (
|
|
135
|
+
<button
|
|
136
|
+
className="jump-to-latest"
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={scrollToLatest}
|
|
139
|
+
>
|
|
140
|
+
Latest messages ↓
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Adapted from Agents UI; see ./PROVENANCE.md.
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { useSessionContext } from '@livekit/components-react';
|
|
4
|
+
import { Icon } from '../icons.js';
|
|
5
|
+
export function AgentDisconnectButton({
|
|
6
|
+
onClick,
|
|
7
|
+
children,
|
|
8
|
+
...props
|
|
9
|
+
}: ComponentProps<'button'>) {
|
|
10
|
+
const { end } = useSessionContext();
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
className="disconnect-button"
|
|
15
|
+
aria-label="End conversation"
|
|
16
|
+
{...props}
|
|
17
|
+
onClick={(event) => {
|
|
18
|
+
onClick?.(event);
|
|
19
|
+
// The host prevents default to include avatar disposal and preserve messages.
|
|
20
|
+
if (!event.defaultPrevented) void end().catch(() => undefined);
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
<Icon name="end" />
|
|
24
|
+
{children}
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Upstream source and adaptations: ./PROVENANCE.md.
|
|
2
|
+
import {
|
|
3
|
+
SessionProvider,
|
|
4
|
+
type UseSessionReturn,
|
|
5
|
+
RoomAudioRenderer,
|
|
6
|
+
type SessionProviderProps,
|
|
7
|
+
type RoomAudioRendererProps,
|
|
8
|
+
} from '@livekit/components-react';
|
|
9
|
+
import type { Room } from 'livekit-client';
|
|
10
|
+
export type AgentSessionProviderProps = SessionProviderProps &
|
|
11
|
+
RoomAudioRendererProps & {
|
|
12
|
+
room?: Room;
|
|
13
|
+
volume?: number;
|
|
14
|
+
muted?: boolean;
|
|
15
|
+
session: UseSessionReturn;
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
};
|
|
18
|
+
export function AgentSessionProvider({
|
|
19
|
+
session,
|
|
20
|
+
children,
|
|
21
|
+
...roomAudioRendererProps
|
|
22
|
+
}: AgentSessionProviderProps) {
|
|
23
|
+
return (
|
|
24
|
+
<SessionProvider session={session}>
|
|
25
|
+
{children}
|
|
26
|
+
<RoomAudioRenderer {...roomAudioRendererProps} />
|
|
27
|
+
</SessionProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Adapted from Agents UI; see ./PROVENANCE.md.
|
|
2
|
+
import { useState, type ComponentProps } from 'react';
|
|
3
|
+
import { Icon } from '../icons.js';
|
|
4
|
+
export type AgentTrackToggleProps = Omit<
|
|
5
|
+
ComponentProps<'button'>,
|
|
6
|
+
'onChange'
|
|
7
|
+
> & {
|
|
8
|
+
pressed?: boolean;
|
|
9
|
+
defaultPressed?: boolean;
|
|
10
|
+
pending?: boolean;
|
|
11
|
+
onPressedChange?: (pressed: boolean) => void;
|
|
12
|
+
};
|
|
13
|
+
export function AgentTrackToggle({
|
|
14
|
+
pressed,
|
|
15
|
+
defaultPressed = false,
|
|
16
|
+
pending = false,
|
|
17
|
+
onPressedChange,
|
|
18
|
+
disabled,
|
|
19
|
+
...props
|
|
20
|
+
}: AgentTrackToggleProps) {
|
|
21
|
+
const [uncontrolledPressed, setUncontrolledPressed] =
|
|
22
|
+
useState(defaultPressed);
|
|
23
|
+
const isControlled = pressed !== undefined;
|
|
24
|
+
const resolvedPressed = pressed ?? uncontrolledPressed;
|
|
25
|
+
const handlePressedChange = () => {
|
|
26
|
+
if (!isControlled) setUncontrolledPressed(!resolvedPressed);
|
|
27
|
+
onPressedChange?.(!resolvedPressed);
|
|
28
|
+
};
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className="microphone-toggle"
|
|
33
|
+
aria-label={resolvedPressed ? 'Mute microphone' : 'Enable microphone'}
|
|
34
|
+
aria-pressed={resolvedPressed}
|
|
35
|
+
aria-busy={pending}
|
|
36
|
+
data-state={resolvedPressed ? 'on' : 'off'}
|
|
37
|
+
disabled={disabled || pending}
|
|
38
|
+
onClick={handlePressedChange}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
<Icon name={resolvedPressed ? 'mic' : 'mic-off'} />
|
|
42
|
+
<span>
|
|
43
|
+
{pending
|
|
44
|
+
? 'Please wait'
|
|
45
|
+
: resolvedPressed
|
|
46
|
+
? 'Microphone on'
|
|
47
|
+
: 'Microphone off'}
|
|
48
|
+
</span>
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright LiveKit. Apache-2.0. Adapted from Agents UI; see ./PROVENANCE.md.
|
|
2
|
+
import { useStartAudio } from '@livekit/components-react';
|
|
3
|
+
import type { Room } from 'livekit-client';
|
|
4
|
+
import { Icon } from '../icons.js';
|
|
5
|
+
export function StartAudioButton({ room }: { room: Room }) {
|
|
6
|
+
const { mergedProps } = useStartAudio({ room, props: {} });
|
|
7
|
+
return (
|
|
8
|
+
<button type="button" {...mergedProps} className="audio-unlock">
|
|
9
|
+
<Icon name="audio" />
|
|
10
|
+
Enable sound
|
|
11
|
+
</button>
|
|
12
|
+
);
|
|
13
|
+
}
|