agent-starter-pack 0.13.0__py3-none-any.whl → 0.14.0__py3-none-any.whl

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.
Files changed (24) hide show
  1. {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/METADATA +11 -3
  2. {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/RECORD +22 -24
  3. agents/adk_base/notebooks/evaluating_adk_agent.ipynb +78 -71
  4. agents/agentic_rag/notebooks/evaluating_adk_agent.ipynb +78 -71
  5. llm.txt +87 -39
  6. src/base_template/Makefile +17 -2
  7. src/cli/commands/create.py +27 -5
  8. src/cli/commands/enhance.py +132 -6
  9. src/cli/commands/setup_cicd.py +91 -69
  10. src/cli/utils/cicd.py +105 -0
  11. src/cli/utils/gcp.py +19 -13
  12. src/cli/utils/logging.py +13 -1
  13. src/cli/utils/template.py +3 -0
  14. src/frontends/live_api_react/frontend/package-lock.json +9 -9
  15. src/frontends/live_api_react/frontend/src/App.tsx +12 -153
  16. src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +352 -3
  17. src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +249 -2
  18. src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +4 -1
  19. src/resources/docs/adk-cheatsheet.md +285 -38
  20. src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +0 -217
  21. src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +0 -201
  22. {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/WHEEL +0 -0
  23. {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/entry_points.txt +0 -0
  24. {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -5914,9 +5914,9 @@
5914
5914
  "license": "ISC"
5915
5915
  },
5916
5916
  "node_modules/brace-expansion": {
5917
- "version": "1.1.12",
5918
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
5919
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
5917
+ "version": "1.1.11",
5918
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
5919
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
5920
5920
  "license": "MIT",
5921
5921
  "dependencies": {
5922
5922
  "balanced-match": "^1.0.0",
@@ -9051,9 +9051,9 @@
9051
9051
  }
9052
9052
  },
9053
9053
  "node_modules/filelist/node_modules/brace-expansion": {
9054
- "version": "2.0.2",
9055
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
9056
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
9054
+ "version": "2.0.1",
9055
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
9056
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
9057
9057
  "license": "MIT",
9058
9058
  "dependencies": {
9059
9059
  "balanced-match": "^1.0.0"
@@ -16819,9 +16819,9 @@
16819
16819
  }
16820
16820
  },
16821
16821
  "node_modules/sucrase/node_modules/brace-expansion": {
16822
- "version": "2.0.2",
16823
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
16824
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
16822
+ "version": "2.0.1",
16823
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
16824
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
16825
16825
  "license": "MIT",
16826
16826
  "dependencies": {
16827
16827
  "balanced-match": "^1.0.0"
@@ -18,62 +18,32 @@ import { useRef, useState } from "react";
18
18
  import "./App.scss";
19
19
  import { LiveAPIProvider } from "./contexts/LiveAPIContext";
20
20
  import SidePanel from "./components/side-panel/SidePanel";
21
- import ControlTray from "./components/control-tray/ControlTray";
22
21
  import cn from "classnames";
23
22
 
24
- // Use relative URLs that work with integrated setup and deployments
25
- const defaultHost = window.location.host;
23
+ // In development mode (frontend on :8501), connect to backend on :8000
24
+ const isDevelopment = window.location.port === '8501';
25
+ const defaultHost = isDevelopment ? `${window.location.hostname}:8000` : window.location.host;
26
26
  const defaultUri = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${defaultHost}/`;
27
27
 
28
28
  function App() {
29
29
  const videoRef = useRef<HTMLVideoElement>(null);
30
30
  const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
31
31
  const [serverUrl, setServerUrl] = useState<string>(defaultUri);
32
- const [runId] = useState<string>(crypto.randomUUID());
33
32
  const [userId, setUserId] = useState<string>("user1");
34
33
 
35
- // Feedback state
36
- const [feedbackScore, setFeedbackScore] = useState<number>(10);
37
- const [feedbackText, setFeedbackText] = useState<string>("");
38
- const [sendFeedback, setShowFeedback] = useState(false);
39
-
40
- const submitFeedback = async () => {
41
- const feedbackUrl = new URL('feedback', serverUrl.replace('ws', 'http')).href;
42
-
43
- try {
44
- const response = await fetch(feedbackUrl, {
45
- method: 'POST',
46
- headers: {
47
- 'Content-Type': 'application/json'
48
- },
49
- body: JSON.stringify({
50
- score: feedbackScore,
51
- text: feedbackText,
52
- run_id: runId,
53
- user_id: userId,
54
- log_type: "feedback"
55
- })
56
- });
57
- if (!response.ok) {
58
- throw new Error(`Failed to submit feedback: Server returned status ${response.status} ${response.statusText}`);
59
- }
60
-
61
- // Clear feedback after successful submission
62
- setFeedbackScore(10);
63
- setFeedbackText("");
64
- setShowFeedback(false);
65
- alert("Feedback submitted successfully!");
66
- } catch (error) {
67
- console.error('Error submitting feedback:', error);
68
- alert(`Failed to submit feedback: ${error}`);
69
- }
70
- };
71
-
72
34
  return (
73
35
  <div className="App">
74
36
  <LiveAPIProvider url={serverUrl} userId={userId}>
75
37
  <div className="streaming-console">
76
- <SidePanel />
38
+ <SidePanel
39
+ videoRef={videoRef}
40
+ supportsVideo={true}
41
+ onVideoStreamChange={setVideoStream}
42
+ serverUrl={serverUrl}
43
+ userId={userId}
44
+ onServerUrlChange={setServerUrl}
45
+ onUserIdChange={setUserId}
46
+ />
77
47
  <main>
78
48
  <div className="main-app-area">
79
49
  <video
@@ -85,117 +55,6 @@ function App() {
85
55
  playsInline
86
56
  />
87
57
  </div>
88
- <ControlTray
89
- videoRef={videoRef}
90
- supportsVideo={true}
91
- onVideoStreamChange={setVideoStream}
92
- >
93
- </ControlTray>
94
- <div className="url-setup" style={{position: 'absolute', top: 0, left: 0, right: 0, pointerEvents: 'auto', zIndex: 1000, padding: '2px', marginBottom: '2px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255, 255, 255, 0.9)'}}>
95
- <div>
96
- <label htmlFor="server-url">Server URL:</label>
97
- <input
98
- id="server-url"
99
- type="text"
100
- value={serverUrl}
101
- onChange={(e) => setServerUrl(e.target.value)}
102
- placeholder="Enter server URL"
103
- style={{
104
- cursor: 'text',
105
- padding: '4px',
106
- margin: '0 4px',
107
- borderRadius: '2px',
108
- border: '1px solid #ccc',
109
- fontSize: '14px',
110
- fontFamily: 'system-ui, -apple-system, sans-serif',
111
- width: '200px'
112
- }}
113
- />
114
- <label htmlFor="user-id">User ID:</label>
115
- <input
116
- id="user-id"
117
- type="text"
118
- value={userId}
119
- onChange={(e) => setUserId(e.target.value)}
120
- placeholder="Enter user ID"
121
- style={{
122
- cursor: 'text',
123
- padding: '4px',
124
- margin: '0 4px',
125
- borderRadius: '2px',
126
- border: '1px solid #ccc',
127
- fontSize: '14px',
128
- fontFamily: 'system-ui, -apple-system, sans-serif',
129
- width: '100px'
130
- }}
131
- />
132
- </div>
133
-
134
- {/* Feedback Button */}
135
- <button
136
- onClick={() => setShowFeedback(!sendFeedback)}
137
- style={{
138
- padding: '5px 10px',
139
- margin: '10px',
140
- cursor: 'pointer'
141
- }}
142
- >
143
- {sendFeedback ? 'Hide Feedback' : 'Send Feedback'}
144
- </button>
145
- </div>
146
-
147
- {/* Feedback Overlay Section */}
148
- {sendFeedback && (
149
- <div className="feedback-section" style={{
150
- position: 'absolute',
151
- top: '50%',
152
- left: '50%',
153
- transform: 'translate(-50%, -50%)',
154
- padding: '20px',
155
- background: 'rgba(255, 255, 255, 0.95)',
156
- boxShadow: '0 0 10px rgba(0,0,0,0.2)',
157
- borderRadius: '8px',
158
- zIndex: 1001,
159
- minWidth: '300px'
160
- }}>
161
- <h3>Provide Feedback</h3>
162
- <div>
163
- <label htmlFor="feedback-score">Score (0-10): </label>
164
- <input
165
- id="feedback-score"
166
- type="number"
167
- min="0"
168
- max="10"
169
- value={feedbackScore}
170
- onChange={(e) => setFeedbackScore(Number(e.target.value))}
171
- style={{margin: '0 10px'}}
172
- />
173
- </div>
174
- <div style={{marginTop: '10px'}}>
175
- <label htmlFor="feedback-text">Comments: </label>
176
- <textarea
177
- id="feedback-text"
178
- value={feedbackText}
179
- onChange={(e) => setFeedbackText(e.target.value)}
180
- style={{
181
- width: '100%',
182
- height: '60px',
183
- margin: '5px 0'
184
- }}
185
- />
186
- </div>
187
- <button
188
- onClick={submitFeedback}
189
- style={{
190
- padding: '5px 10px',
191
- marginTop: '5px',
192
- cursor: 'pointer'
193
- }}
194
- >
195
- Submit Feedback
196
- </button>
197
- </div>
198
- )}
199
58
  </main>
200
59
  </div>
201
60
  </LiveAPIProvider>
@@ -15,11 +15,16 @@
15
15
  */
16
16
 
17
17
  import cn from "classnames";
18
- import { useEffect, useRef, useState } from "react";
18
+ import { memo, ReactNode, RefObject, useEffect, useRef, useState } from "react";
19
19
  import { RiSidebarFoldLine, RiSidebarUnfoldLine } from "react-icons/ri";
20
20
  import Select from "react-select";
21
21
  import { useLiveAPIContext } from "../../contexts/LiveAPIContext";
22
22
  import { useLoggerStore } from "../../utils/store-logger";
23
+ import { UseMediaStreamResult } from "../../hooks/use-media-stream-mux";
24
+ import { useScreenCapture } from "../../hooks/use-screen-capture";
25
+ import { useWebcam } from "../../hooks/use-webcam";
26
+ import { AudioRecorder } from "../../utils/audio-recorder";
27
+ import AudioPulse from "../audio-pulse/AudioPulse";
23
28
  import Logger, { LoggerFilterType } from "../logger/Logger";
24
29
  import "./side-panel.scss";
25
30
 
@@ -29,9 +34,58 @@ const filterOptions = [
29
34
  { value: "none", label: "All" },
30
35
  ];
31
36
 
32
- export default function SidePanel() {
33
- const { connected, client } = useLiveAPIContext();
37
+ export type SidePanelProps = {
38
+ videoRef?: RefObject<HTMLVideoElement>;
39
+ children?: ReactNode;
40
+ supportsVideo?: boolean;
41
+ onVideoStreamChange?: (stream: MediaStream | null) => void;
42
+ serverUrl?: string;
43
+ userId?: string;
44
+ onServerUrlChange?: (url: string) => void;
45
+ onUserIdChange?: (userId: string) => void;
46
+ };
47
+
48
+ type MediaStreamButtonProps = {
49
+ isStreaming: boolean;
50
+ onIcon: string;
51
+ offIcon: string;
52
+ start: () => Promise<any>;
53
+ stop: () => any;
54
+ };
55
+
56
+ const MediaStreamButton = memo(
57
+ ({ isStreaming, onIcon, offIcon, start, stop }: MediaStreamButtonProps) =>
58
+ isStreaming ? (
59
+ <button className={cn("action-button", { active: isStreaming })} onClick={stop}>
60
+ <span className="material-symbols-outlined">{onIcon}</span>
61
+ </button>
62
+ ) : (
63
+ <button className="action-button" onClick={start}>
64
+ <span className="material-symbols-outlined">{offIcon}</span>
65
+ </button>
66
+ ),
67
+ );
68
+
69
+ function SidePanel({
70
+ videoRef,
71
+ children,
72
+ onVideoStreamChange = () => {},
73
+ supportsVideo = true,
74
+ serverUrl = "ws://localhost:8000/",
75
+ userId = "user1",
76
+ onServerUrlChange = () => {},
77
+ onUserIdChange = () => {},
78
+ }: SidePanelProps) {
79
+ const { connected, client, connect, disconnect, volume } = useLiveAPIContext();
34
80
  const [open, setOpen] = useState(true);
81
+ const [connectionExpanded, setConnectionExpanded] = useState(false);
82
+
83
+ // Auto-collapse connection settings when panel is closed
84
+ useEffect(() => {
85
+ if (!open && connectionExpanded) {
86
+ setConnectionExpanded(false);
87
+ }
88
+ }, [open, connectionExpanded]);
35
89
  const loggerRef = useRef<HTMLDivElement>(null);
36
90
  const loggerLastHeightRef = useRef<number>(-1);
37
91
  const { log, logs } = useLoggerStore();
@@ -42,6 +96,135 @@ export default function SidePanel() {
42
96
  label: string;
43
97
  } | null>(null);
44
98
  const inputRef = useRef<HTMLTextAreaElement>(null);
99
+
100
+ // Control states
101
+ const videoStreams = [useWebcam(), useScreenCapture()];
102
+ const [activeVideoStream, setActiveVideoStream] = useState<MediaStream | null>(null);
103
+ const [webcam, screenCapture] = videoStreams;
104
+ const [inVolume, setInVolume] = useState(0);
105
+ const [audioRecorder] = useState(() => new AudioRecorder());
106
+ const [muted, setMuted] = useState(false);
107
+ const renderCanvasRef = useRef<HTMLCanvasElement>(null);
108
+ const connectButtonRef = useRef<HTMLButtonElement>(null);
109
+
110
+ // Feedback state (local to SidePanel)
111
+ const [feedbackScore, setFeedbackScore] = useState<number>(10);
112
+ const [feedbackText, setFeedbackText] = useState<string>("");
113
+ const [sendFeedback, setShowFeedback] = useState(false);
114
+
115
+ useEffect(() => {
116
+ if (!connected && connectButtonRef.current) {
117
+ connectButtonRef.current.focus();
118
+ }
119
+ }, [connected]);
120
+
121
+ useEffect(() => {
122
+ document.documentElement.style.setProperty(
123
+ "--volume",
124
+ `${Math.max(5, Math.min(inVolume * 200, 8))}px`,
125
+ );
126
+ }, [inVolume]);
127
+
128
+ useEffect(() => {
129
+ const onData = (base64: string) => {
130
+ client.sendRealtimeInput([
131
+ {
132
+ mimeType: "audio/pcm;rate=16000",
133
+ data: base64,
134
+ },
135
+ ]);
136
+ };
137
+ if (connected && !muted && audioRecorder) {
138
+ audioRecorder.on("data", onData).on("volume", setInVolume).start();
139
+ } else {
140
+ audioRecorder.stop();
141
+ }
142
+ return () => {
143
+ audioRecorder.off("data", onData).off("volume", setInVolume);
144
+ };
145
+ }, [connected, client, muted, audioRecorder]);
146
+
147
+ useEffect(() => {
148
+ if (videoRef && videoRef.current) {
149
+ videoRef.current.srcObject = activeVideoStream;
150
+ }
151
+
152
+ let timeoutId = -1;
153
+
154
+ function sendVideoFrame() {
155
+ const video = videoRef && videoRef.current;
156
+ const canvas = renderCanvasRef.current;
157
+
158
+ if (!video || !canvas) {
159
+ return;
160
+ }
161
+
162
+ const ctx = canvas.getContext("2d")!;
163
+ canvas.width = video.videoWidth * 0.25;
164
+ canvas.height = video.videoHeight * 0.25;
165
+ if (canvas.width + canvas.height > 0) {
166
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
167
+ const base64 = canvas.toDataURL("image/jpeg", 1.0);
168
+ const data = base64.slice(base64.indexOf(",") + 1, Infinity);
169
+ client.sendRealtimeInput([{ mimeType: "image/jpeg", data }]);
170
+ }
171
+ if (connected) {
172
+ timeoutId = window.setTimeout(sendVideoFrame, 1000 / 0.5);
173
+ }
174
+ }
175
+ if (connected && activeVideoStream !== null) {
176
+ requestAnimationFrame(sendVideoFrame);
177
+ }
178
+ return () => {
179
+ clearTimeout(timeoutId);
180
+ };
181
+ }, [connected, activeVideoStream, client, videoRef]);
182
+
183
+ //handler for swapping from one video-stream to the next
184
+ const changeStreams = (next?: UseMediaStreamResult) => async () => {
185
+ if (next) {
186
+ const mediaStream = await next.start();
187
+ setActiveVideoStream(mediaStream);
188
+ onVideoStreamChange(mediaStream);
189
+ } else {
190
+ setActiveVideoStream(null);
191
+ onVideoStreamChange(null);
192
+ }
193
+
194
+ videoStreams.filter((msr) => msr !== next).forEach((msr) => msr.stop());
195
+ };
196
+
197
+ const submitFeedback = async () => {
198
+ const feedbackUrl = new URL('feedback', serverUrl.replace('ws', 'http')).href;
199
+
200
+ try {
201
+ const response = await fetch(feedbackUrl, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json'
205
+ },
206
+ body: JSON.stringify({
207
+ score: feedbackScore,
208
+ text: feedbackText,
209
+ run_id: client.currentRunId,
210
+ user_id: userId,
211
+ log_type: "feedback"
212
+ })
213
+ });
214
+ if (!response.ok) {
215
+ throw new Error(`Failed to submit feedback: Server returned status ${response.status} ${response.statusText}`);
216
+ }
217
+
218
+ // Clear feedback after successful submission
219
+ setFeedbackScore(10);
220
+ setFeedbackText("");
221
+ setShowFeedback(false);
222
+ alert("Feedback submitted successfully!");
223
+ } catch (error) {
224
+ console.error('Error submitting feedback:', error);
225
+ alert(`Failed to submit feedback: ${error}`);
226
+ }
227
+ };
45
228
 
46
229
  //scroll the log to the bottom when new logs come in
47
230
  useEffect(() => {
@@ -74,6 +257,7 @@ export default function SidePanel() {
74
257
 
75
258
  return (
76
259
  <div className={`side-panel ${open ? "open" : ""}`}>
260
+ <canvas style={{ display: "none" }} ref={renderCanvasRef} />
77
261
  <header className="top">
78
262
  <h2>Console</h2>
79
263
  {open ? (
@@ -86,6 +270,106 @@ export default function SidePanel() {
86
270
  </button>
87
271
  )}
88
272
  </header>
273
+
274
+ {/* Connection Settings Section */}
275
+ <section className="connection-settings">
276
+ <button
277
+ className="connection-expander"
278
+ onClick={() => setConnectionExpanded(!connectionExpanded)}
279
+ >
280
+ Connection Settings
281
+ <span>{connectionExpanded ? '▼' : '▶'}</span>
282
+ </button>
283
+ {connectionExpanded && open && (
284
+ <div className="connection-content">
285
+ <div className="setting-group">
286
+ <label htmlFor="server-url">Server URL</label>
287
+ <input
288
+ id="server-url"
289
+ type="text"
290
+ value={serverUrl}
291
+ onChange={(e) => onServerUrlChange(e.target.value)}
292
+ placeholder="ws://localhost:8000/"
293
+ className="setting-input"
294
+ />
295
+ </div>
296
+ <div className="setting-group">
297
+ <label htmlFor="user-id">User ID</label>
298
+ <input
299
+ id="user-id"
300
+ type="text"
301
+ value={userId}
302
+ onChange={(e) => onUserIdChange(e.target.value)}
303
+ placeholder="user123"
304
+ className="setting-input"
305
+ />
306
+ </div>
307
+ <button
308
+ onClick={() => setShowFeedback(!sendFeedback)}
309
+ className="feedback-button"
310
+ >
311
+ {sendFeedback ? 'Hide Feedback' : 'Send Feedback'}
312
+ </button>
313
+ </div>
314
+ )}
315
+ </section>
316
+
317
+ {/* Control Tray - All buttons moved here */}
318
+ <section className="control-tray">
319
+ <nav className={cn("actions-nav", { disabled: !connected })}>
320
+ <button
321
+ ref={connectButtonRef}
322
+ className={cn("action-button connect-toggle", { connected })}
323
+ onClick={connected ? disconnect : connect}
324
+ >
325
+ <span className="material-symbols-outlined filled">
326
+ {connected ? "pause" : "play_arrow"}
327
+ </span>
328
+ </button>
329
+
330
+ <button
331
+ className={cn("action-button mic-button", { active: !muted })}
332
+ onClick={() => setMuted(!muted)}
333
+ >
334
+ {!muted ? (
335
+ <span className="material-symbols-outlined filled">mic</span>
336
+ ) : (
337
+ <span className="material-symbols-outlined filled">mic_off</span>
338
+ )}
339
+ </button>
340
+
341
+ <div className="action-button no-action outlined">
342
+ <AudioPulse volume={volume} active={connected} hover={false} />
343
+ </div>
344
+
345
+ {supportsVideo && (
346
+ <>
347
+ <MediaStreamButton
348
+ isStreaming={screenCapture.isStreaming}
349
+ start={changeStreams(screenCapture)}
350
+ stop={changeStreams()}
351
+ onIcon="cancel_presentation"
352
+ offIcon="present_to_all"
353
+ />
354
+ <MediaStreamButton
355
+ isStreaming={webcam.isStreaming}
356
+ start={changeStreams(webcam)}
357
+ stop={changeStreams()}
358
+ onIcon="videocam_off"
359
+ offIcon="videocam"
360
+ />
361
+ </>
362
+ )}
363
+ {children}
364
+ </nav>
365
+
366
+ <div className="connection-status">
367
+ <span className={cn("text-indicator", { connected })}>
368
+ {connected ? "Streaming" : "Disconnected"}
369
+ </span>
370
+ </div>
371
+ </section>
372
+
89
373
  <section className="indicators">
90
374
  <Select
91
375
  className="react-select"
@@ -156,6 +440,71 @@ export default function SidePanel() {
156
440
  </button>
157
441
  </div>
158
442
  </div>
443
+
444
+ {/* Audio Pulse Bottom Section */}
445
+ <section className="audio-pulse-bottom">
446
+ <div className="pulse-container">
447
+ <AudioPulse volume={volume} active={connected} hover={false} />
448
+ <span className="pulse-label">
449
+ {connected ? (volume > 0 ? "AI Speaking..." : "AI Ready") : "Not connected"}
450
+ </span>
451
+ </div>
452
+ </section>
453
+
454
+ {/* Feedback Overlay Section */}
455
+ {sendFeedback && (
456
+ <div className="feedback-section" style={{
457
+ position: 'absolute',
458
+ top: '50%',
459
+ left: '50%',
460
+ transform: 'translate(-50%, -50%)',
461
+ padding: '20px',
462
+ background: 'rgba(255, 255, 255, 0.95)',
463
+ boxShadow: '0 0 10px rgba(0,0,0,0.2)',
464
+ borderRadius: '8px',
465
+ zIndex: 1001,
466
+ minWidth: '300px'
467
+ }}>
468
+ <h3>Provide Feedback</h3>
469
+ <div>
470
+ <label htmlFor="feedback-score">Score (0-10): </label>
471
+ <input
472
+ id="feedback-score"
473
+ type="number"
474
+ min="0"
475
+ max="10"
476
+ value={feedbackScore}
477
+ onChange={(e) => setFeedbackScore(Number(e.target.value))}
478
+ style={{margin: '0 10px'}}
479
+ />
480
+ </div>
481
+ <div style={{marginTop: '10px'}}>
482
+ <label htmlFor="feedback-text">Comments: </label>
483
+ <textarea
484
+ id="feedback-text"
485
+ value={feedbackText}
486
+ onChange={(e) => setFeedbackText(e.target.value)}
487
+ style={{
488
+ width: '100%',
489
+ height: '60px',
490
+ margin: '5px 0'
491
+ }}
492
+ />
493
+ </div>
494
+ <button
495
+ onClick={submitFeedback}
496
+ style={{
497
+ padding: '5px 10px',
498
+ marginTop: '5px',
499
+ cursor: 'pointer'
500
+ }}
501
+ >
502
+ Submit Feedback
503
+ </button>
504
+ </div>
505
+ )}
159
506
  </div>
160
507
  );
161
508
  }
509
+
510
+ export default memo(SidePanel);