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.
- {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/METADATA +11 -3
- {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/RECORD +22 -24
- agents/adk_base/notebooks/evaluating_adk_agent.ipynb +78 -71
- agents/agentic_rag/notebooks/evaluating_adk_agent.ipynb +78 -71
- llm.txt +87 -39
- src/base_template/Makefile +17 -2
- src/cli/commands/create.py +27 -5
- src/cli/commands/enhance.py +132 -6
- src/cli/commands/setup_cicd.py +91 -69
- src/cli/utils/cicd.py +105 -0
- src/cli/utils/gcp.py +19 -13
- src/cli/utils/logging.py +13 -1
- src/cli/utils/template.py +3 -0
- src/frontends/live_api_react/frontend/package-lock.json +9 -9
- src/frontends/live_api_react/frontend/src/App.tsx +12 -153
- src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +352 -3
- src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +249 -2
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +4 -1
- src/resources/docs/adk-cheatsheet.md +285 -38
- src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +0 -217
- src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +0 -201
- {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.13.0.dist-info → agent_starter_pack-0.14.0.dist-info}/entry_points.txt +0 -0
- {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.
|
5918
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.
|
5919
|
-
"integrity": "sha512-
|
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.
|
9055
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.
|
9056
|
-
"integrity": "sha512-
|
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.
|
16823
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.
|
16824
|
-
"integrity": "sha512-
|
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
|
-
//
|
25
|
-
const
|
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
|
33
|
-
|
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);
|