@sampleapp.ai/sdk 1.0.23 → 1.0.25

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.
@@ -0,0 +1,546 @@
1
+ "use client";
2
+ /* eslint-disable @next/next/no-img-element */
3
+ import React from "react";
4
+ import { useState, useRef, useEffect, useMemo } from "react";
5
+ import { ArrowUp, Loader2, CircleStop } from "lucide-react";
6
+ import { TypingTextarea } from "./chat-bar/typing-textarea";
7
+ import { Textarea } from "./ui/textarea";
8
+ import { VoiceButton } from "./chat-bar/voice-button";
9
+ import { VoiceOverlay } from "./chat-bar/voice-overlay";
10
+ import { Button } from "./ui/button";
11
+ import { getTheme } from "../themes";
12
+ const getColorScheme = (themeName, playgroundUid) => {
13
+ // If theme is provided, use it directly
14
+ if (themeName) {
15
+ return getTheme(themeName);
16
+ }
17
+ // Use default theme
18
+ return getTheme();
19
+ };
20
+ export const ChatBar = ({ query, placeholder = "Ask anything...", setQuery, onSubmit, isStreaming, height, hasPendingEnv, playgroundUid, hasTypingAnimation = false, isSubmitting, onCancel, typingTexts = [
21
+ "Build me an AI chatbot application using OpenAI API",
22
+ "Build me an ecommerce website using Stripe API",
23
+ "Build me a price tracking tool using AgentQL API",
24
+ "Build me a movie recommendation system using Qdrant API",
25
+ "Build me a weather forecasting app using OpenWeather API",
26
+ "Build me a SMS notification system using Twilio API",
27
+ "Build me a travel booking platform using Skyscanner API",
28
+ // "Build me an inventory management system using AWS S3",
29
+ // "Build me a social media platform using Firebase",
30
+ // "Build me a project management tool using Trello API",
31
+ ], shouldFocusOnMount = true, showModelSelector = true, projectUid, theme, }) => {
32
+ const [isFocused, setIsFocused] = useState(true);
33
+ const [isRecording, setIsRecording] = useState(false);
34
+ const [isTranscribing, setIsTranscribing] = useState(false);
35
+ const [, setShowVoiceOverlay] = useState(false);
36
+ const [recordingTimeout, setRecordingTimeout] = useState(null);
37
+ const [recordingStartTime, setRecordingStartTime] = useState(null);
38
+ const mediaRecorderRef = useRef(null);
39
+ const audioChunksRef = useRef([]);
40
+ const fileInputRef = useRef(null);
41
+ const textareaRef = useRef(null);
42
+ const [isMounted, setIsMounted] = useState(false);
43
+ // Get color scheme for gradient border
44
+ const colorScheme = useMemo(() => getColorScheme(theme, playgroundUid), [theme, playgroundUid]);
45
+ useEffect(() => {
46
+ setIsMounted(true);
47
+ }, []);
48
+ useEffect(() => {
49
+ // Focus and select all text in the textarea when component mounts
50
+ if (shouldFocusOnMount && textareaRef.current && isMounted) {
51
+ textareaRef.current.focus();
52
+ textareaRef.current.select();
53
+ }
54
+ }, [isMounted, shouldFocusOnMount]);
55
+ const handleFocus = () => {
56
+ setIsFocused(true);
57
+ };
58
+ const handleBlur = () => {
59
+ setIsFocused(true);
60
+ };
61
+ // Function to adjust textarea height
62
+ const adjustTextareaHeight = () => {
63
+ const textarea = textareaRef.current;
64
+ if (textarea) {
65
+ // Reset height to auto to get the correct scrollHeight
66
+ textarea.style.height = "auto";
67
+ // Set new height based on scrollHeight, with a maximum of 200px
68
+ const newHeight = Math.min(textarea.scrollHeight, 200);
69
+ textarea.style.height = `${newHeight}px`;
70
+ }
71
+ };
72
+ // Update height whenever query changes
73
+ useEffect(() => {
74
+ adjustTextareaHeight();
75
+ }, [query]);
76
+ // Keyboard shortcuts for voice mode
77
+ useEffect(() => {
78
+ const handleKeyDown = (event) => {
79
+ // Check for Cmd+/ (Mac) or Ctrl+/ (Windows/Linux) - toggles recording
80
+ const isVoiceShortcut = (event.metaKey || event.ctrlKey) && event.key === "/";
81
+ // Check for Enter or Escape - stops recording if currently recording
82
+ const isStopRecordingKey = (event.key === "Enter" || event.key === "Escape") &&
83
+ (isRecording || isTranscribing);
84
+ if (isVoiceShortcut) {
85
+ event.preventDefault();
86
+ handleVoiceRecording();
87
+ }
88
+ else if (isStopRecordingKey) {
89
+ // Stop recording with Enter or Escape
90
+ event.preventDefault();
91
+ if (isRecording && !isTranscribing) {
92
+ // Only stop if recording (not if transcribing)
93
+ handleVoiceRecording();
94
+ }
95
+ }
96
+ };
97
+ // Add event listener
98
+ document.addEventListener("keydown", handleKeyDown);
99
+ // Cleanup function
100
+ return () => {
101
+ document.removeEventListener("keydown", handleKeyDown);
102
+ };
103
+ }, [isRecording, isTranscribing]); // Dependencies for handleVoiceRecording
104
+ // Cleanup on unmount
105
+ useEffect(() => {
106
+ return () => {
107
+ if (recordingTimeout) {
108
+ clearTimeout(recordingTimeout);
109
+ }
110
+ if (mediaRecorderRef.current && isRecording) {
111
+ mediaRecorderRef.current.stop();
112
+ }
113
+ setShowVoiceOverlay(false);
114
+ };
115
+ }, [recordingTimeout, isRecording]);
116
+ const handleVoiceOverlayDismiss = () => {
117
+ // Only allow dismissing during recording, not transcribing
118
+ if (isTranscribing)
119
+ return;
120
+ if (isRecording) {
121
+ // Stop recording if currently recording
122
+ stopRecording();
123
+ }
124
+ setShowVoiceOverlay(false);
125
+ };
126
+ const stopRecording = async () => {
127
+ if (mediaRecorderRef.current && isRecording) {
128
+ // Check minimum recording duration (1 second)
129
+ // const now = Date.now();
130
+ // const recordingDuration = recordingStartTime
131
+ // ? now - recordingStartTime
132
+ // : 0;
133
+ // if (recordingDuration < 1000) {
134
+ // toast({
135
+ // title: "Recording Too Short",
136
+ // description: "Please record for at least 1 second before stopping.",
137
+ // variant: "destructive",
138
+ // });
139
+ // return;
140
+ // }
141
+ // Immediately update UI state to prevent double-clicks
142
+ setIsRecording(false);
143
+ setIsTranscribing(true);
144
+ // Clear timeout
145
+ if (recordingTimeout) {
146
+ clearTimeout(recordingTimeout);
147
+ setRecordingTimeout(null);
148
+ }
149
+ // Wait for the audio data to be processed
150
+ return new Promise((resolve) => {
151
+ if (mediaRecorderRef.current) {
152
+ let hasData = false;
153
+ let stopTimeout; // eslint-disable-line prefer-const
154
+ // Set up data collection with timeout
155
+ mediaRecorderRef.current.ondataavailable = (event) => {
156
+ if (event.data.size > 0) {
157
+ hasData = true;
158
+ audioChunksRef.current.push(event.data);
159
+ }
160
+ };
161
+ mediaRecorderRef.current.onstop = async () => {
162
+ // Clear any pending timeout
163
+ if (stopTimeout) {
164
+ clearTimeout(stopTimeout);
165
+ }
166
+ try {
167
+ // Check if we have any audio data
168
+ if (!hasData || audioChunksRef.current.length === 0) {
169
+ throw new Error("No audio data recorded");
170
+ }
171
+ const audioBlob = new Blob(audioChunksRef.current, {
172
+ type: "audio/wav",
173
+ });
174
+ // Check if the blob has meaningful size (at least 1KB)
175
+ if (audioBlob.size < 1024) {
176
+ throw new Error("Audio recording too short");
177
+ }
178
+ // Send to backend for transcription
179
+ const formData = new FormData();
180
+ formData.append("audio", audioBlob, "recording.wav");
181
+ const response = await fetch("/api/voice/transcribe", {
182
+ method: "POST",
183
+ body: formData,
184
+ });
185
+ if (response.ok) {
186
+ const { transcription } = await response.json();
187
+ if (transcription && transcription.trim()) {
188
+ // Add transcribed text to current query
189
+ const newQuery = query
190
+ ? `${query} ${transcription}`
191
+ : transcription;
192
+ setQuery(newQuery.trim());
193
+ // Focus the textarea after transcription is added so user can press Enter immediately
194
+ setTimeout(() => {
195
+ if (textareaRef.current) {
196
+ textareaRef.current.focus();
197
+ // Move cursor to end of text
198
+ const length = textareaRef.current.value.length;
199
+ textareaRef.current.setSelectionRange(length, length);
200
+ }
201
+ }, 100); // Small delay to ensure state update completes
202
+ }
203
+ else {
204
+ // toast({
205
+ // title: "No Speech Detected",
206
+ // description:
207
+ // "Try speaking more clearly or closer to the microphone",
208
+ // variant: "destructive",
209
+ // });
210
+ }
211
+ }
212
+ else {
213
+ throw new Error("Transcription failed");
214
+ }
215
+ }
216
+ catch (error) {
217
+ console.error("Transcription error:", error);
218
+ // Provide more specific error messages
219
+ let errorMessage = "Could not convert speech to text. Please try again.";
220
+ let errorTitle = "Transcription Failed";
221
+ if (error instanceof Error) {
222
+ if (error.message.includes("No audio data") ||
223
+ error.message.includes("too short")) {
224
+ errorTitle = "Recording Too Short";
225
+ errorMessage =
226
+ "Please speak for at least 1-2 seconds before stopping.";
227
+ }
228
+ }
229
+ // toast({
230
+ // title: errorTitle,
231
+ // description: errorMessage,
232
+ // variant: "destructive",
233
+ // });
234
+ }
235
+ finally {
236
+ // Clean up
237
+ audioChunksRef.current = [];
238
+ mediaRecorderRef.current = null;
239
+ setRecordingStartTime(null);
240
+ setIsTranscribing(false);
241
+ setShowVoiceOverlay(false);
242
+ resolve();
243
+ }
244
+ };
245
+ // Set up a timeout to force stop if MediaRecorder doesn't stop properly
246
+ stopTimeout = setTimeout(() => {
247
+ console.warn("MediaRecorder stop timeout, forcing cleanup");
248
+ audioChunksRef.current = [];
249
+ mediaRecorderRef.current = null;
250
+ setRecordingStartTime(null);
251
+ setIsTranscribing(false);
252
+ setShowVoiceOverlay(false);
253
+ resolve();
254
+ }, 5000); // 5 second timeout
255
+ // Stop the recording
256
+ try {
257
+ mediaRecorderRef.current.stop();
258
+ }
259
+ catch (error) {
260
+ console.error("Error stopping MediaRecorder:", error);
261
+ // Force cleanup if stop fails
262
+ if (stopTimeout) {
263
+ clearTimeout(stopTimeout);
264
+ }
265
+ audioChunksRef.current = [];
266
+ mediaRecorderRef.current = null;
267
+ setRecordingStartTime(null);
268
+ setIsTranscribing(false);
269
+ setShowVoiceOverlay(false);
270
+ resolve();
271
+ }
272
+ }
273
+ else {
274
+ // No MediaRecorder available, just clean up
275
+ setRecordingStartTime(null);
276
+ setIsTranscribing(false);
277
+ setShowVoiceOverlay(false);
278
+ resolve();
279
+ }
280
+ });
281
+ }
282
+ };
283
+ const handleVoiceRecording = async () => {
284
+ if (isTranscribing) {
285
+ // Don't allow interaction while transcribing
286
+ return;
287
+ }
288
+ if (isRecording) {
289
+ // Manual stop
290
+ await stopRecording();
291
+ }
292
+ else {
293
+ // Start recording - automatically trigger permission request
294
+ try {
295
+ // Check if mediaDevices is supported
296
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
297
+ throw new Error("Browser doesn't support microphone access");
298
+ }
299
+ // This will automatically trigger the browser's permission prompt
300
+ const stream = await navigator.mediaDevices.getUserMedia({
301
+ audio: {
302
+ echoCancellation: true,
303
+ noiseSuppression: true,
304
+ sampleRate: 44100,
305
+ },
306
+ });
307
+ // Create MediaRecorder
308
+ const mediaRecorder = new MediaRecorder(stream, {
309
+ mimeType: MediaRecorder.isTypeSupported("audio/webm")
310
+ ? "audio/webm"
311
+ : "audio/mp4",
312
+ });
313
+ mediaRecorderRef.current = mediaRecorder;
314
+ audioChunksRef.current = [];
315
+ // Set up event listeners
316
+ mediaRecorder.ondataavailable = (event) => {
317
+ if (event.data.size > 0) {
318
+ audioChunksRef.current.push(event.data);
319
+ }
320
+ };
321
+ // Start recording
322
+ mediaRecorder.start(200); // Collect data every 200 ms
323
+ setIsRecording(true);
324
+ setRecordingStartTime(Date.now());
325
+ setShowVoiceOverlay(true);
326
+ // Set up 60 second auto-timeout
327
+ const timeout = setTimeout(async () => {
328
+ await stopRecording();
329
+ }, 60000);
330
+ setRecordingTimeout(timeout);
331
+ }
332
+ catch (error) {
333
+ console.error("Microphone access error:", error);
334
+ let errorMessage = "Please allow microphone access to use voice recording";
335
+ let errorTitle = "Microphone Access Required";
336
+ if (error instanceof Error) {
337
+ if (error.name === "NotAllowedError") {
338
+ errorMessage =
339
+ "Microphone access was denied. Please click the microphone icon in your browser's address bar and allow access, then try again.";
340
+ }
341
+ else if (error.name === "NotFoundError") {
342
+ errorTitle = "No Microphone Found";
343
+ errorMessage =
344
+ "No microphone was detected. Please connect a microphone and try again.";
345
+ }
346
+ else if (error.name === "NotReadableError") {
347
+ errorTitle = "Microphone In Use";
348
+ errorMessage =
349
+ "Your microphone is being used by another application. Please close other apps and try again.";
350
+ }
351
+ else if (error.name === "NotSupportedError") {
352
+ errorTitle = "Not Supported";
353
+ errorMessage =
354
+ "Your browser doesn't support microphone access. Please use a modern browser.";
355
+ }
356
+ }
357
+ // toast({
358
+ // title: errorTitle,
359
+ // description: errorMessage,
360
+ // variant: "destructive",
361
+ // });
362
+ }
363
+ }
364
+ };
365
+ return (React.createElement("div", null,
366
+ React.createElement(VoiceOverlay, { isRecording: isRecording, isTranscribing: isTranscribing, onDismiss: handleVoiceOverlayDismiss }),
367
+ React.createElement("div", { style: {
368
+ position: "relative",
369
+ padding: "2px",
370
+ width: "100%",
371
+ maxWidth: "100%",
372
+ marginLeft: "auto",
373
+ marginRight: "auto",
374
+ } },
375
+ React.createElement("div", { style: {
376
+ backgroundColor: colorScheme.shadow.dark,
377
+ position: "absolute",
378
+ top: "0",
379
+ right: "0",
380
+ bottom: "0",
381
+ left: "0",
382
+ borderRadius: "0.85rem",
383
+ transition: "opacity 0.3s ease-in-out",
384
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
385
+ opacity: "1",
386
+ pointerEvents: "none",
387
+ } }),
388
+ React.createElement("div", { style: {
389
+ background: colorScheme.gradient,
390
+ backgroundSize: "400% 400%",
391
+ position: "absolute",
392
+ top: "0",
393
+ right: "0",
394
+ bottom: "0",
395
+ left: "0",
396
+ borderRadius: "0.85rem",
397
+ zIndex: 1,
398
+ filter: "blur(12px)",
399
+ transition: "opacity 0.3s ease-in-out",
400
+ pointerEvents: "none",
401
+ opacity: isFocused ? "0.8" : "0.4",
402
+ animation: "gradient-bg 5s ease infinite",
403
+ } }),
404
+ React.createElement("div", { style: {
405
+ background: colorScheme.gradient,
406
+ backgroundSize: "400% 400%",
407
+ position: "absolute",
408
+ top: "0",
409
+ right: "0",
410
+ bottom: "0",
411
+ left: "0",
412
+ borderRadius: "0.85rem",
413
+ zIndex: 1,
414
+ transition: "opacity 0.3s ease-in-out",
415
+ pointerEvents: "none",
416
+ opacity: isFocused ? "1" : "0.7",
417
+ animation: "gradient-bg 5s ease infinite",
418
+ } }),
419
+ React.createElement("div", { style: {
420
+ position: "relative",
421
+ zIndex: 10,
422
+ borderRadius: "0.75rem",
423
+ backgroundColor: "rgb(24, 24, 27)", // zinc-900 equivalent
424
+ width: "100%",
425
+ paddingLeft: "0.25rem",
426
+ paddingRight: "0.25rem",
427
+ paddingTop: "0.5rem",
428
+ paddingBottom: "0.5rem",
429
+ } },
430
+ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
431
+ React.createElement("div", { style: {
432
+ position: "relative",
433
+ display: "flex",
434
+ paddingLeft: "8px",
435
+ paddingRight: "8px",
436
+ gap: "8px",
437
+ paddingTop: "4px",
438
+ paddingBottom: "4px",
439
+ } },
440
+ hasTypingAnimation && typingTexts.length > 0 ? (React.createElement(TypingTextarea, { texts: typingTexts, ref: textareaRef, style: {
441
+ flex: 1,
442
+ width: "100%",
443
+ border: "none",
444
+ boxShadow: "none",
445
+ outline: "none",
446
+ paddingLeft: "4px",
447
+ paddingRight: "4px",
448
+ resize: "none",
449
+ fontSize: "16px",
450
+ overflow: "hidden",
451
+ backgroundColor: "transparent",
452
+ display: "block",
453
+ paddingTop: "0",
454
+ paddingBottom: "0",
455
+ lineHeight: "1.5",
456
+ overflowY: "auto",
457
+ maxHeight: "192px",
458
+ marginTop: "4px",
459
+ color: "#f4f4f5",
460
+ fontFamily: "inherit",
461
+ }, value: query, onFocus: handleFocus, onBlur: handleBlur, onChange: (e) => setQuery(e.target.value), onSubmit: (e) => {
462
+ // Prevent submission if recording
463
+ if (isRecording || isTranscribing) {
464
+ e.preventDefault();
465
+ return;
466
+ }
467
+ onSubmit(e);
468
+ }, isStreaming: isStreaming, rows: 1 })) : (React.createElement(Textarea, { ref: textareaRef, placeholder: placeholder, style: {
469
+ flex: 1,
470
+ width: "100%",
471
+ border: "none",
472
+ boxShadow: "none",
473
+ outline: "none",
474
+ paddingLeft: "4px",
475
+ paddingRight: "4px",
476
+ resize: "none",
477
+ fontSize: "16px",
478
+ overflow: "hidden",
479
+ backgroundColor: "transparent",
480
+ display: "block",
481
+ paddingTop: "0",
482
+ paddingBottom: "0",
483
+ lineHeight: "1.5",
484
+ overflowY: "auto",
485
+ maxHeight: "192px",
486
+ marginTop: "4px",
487
+ color: "#f4f4f5",
488
+ fontFamily: "inherit",
489
+ }, value: query, onFocus: handleFocus, onBlur: handleBlur, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => {
490
+ if (e.key === "Enter") {
491
+ if (e.shiftKey) {
492
+ return;
493
+ }
494
+ e.preventDefault();
495
+ if (isStreaming) {
496
+ return;
497
+ }
498
+ onSubmit(e);
499
+ }
500
+ }, rows: 1 })),
501
+ React.createElement("div", null,
502
+ React.createElement(VoiceButton, { isRecording: isRecording, isTranscribing: isTranscribing, onVoiceRecording: handleVoiceRecording, playgroundUid: playgroundUid }),
503
+ React.createElement(Button, { style: {
504
+ height: "32px",
505
+ width: "32px",
506
+ borderRadius: "6px",
507
+ }, disabled: query.length === 0 || isSubmitting || hasPendingEnv, variant: isStreaming ? "outline" : "default", onClick: (e) => {
508
+ if (isStreaming && onCancel) {
509
+ onCancel();
510
+ }
511
+ else {
512
+ onSubmit(e);
513
+ }
514
+ } }, isSubmitting ? (React.createElement(Loader2, { style: {
515
+ height: "20px",
516
+ width: "20px",
517
+ animation: "spin 1s linear infinite",
518
+ } })) : isStreaming ? (React.createElement(CircleStop, { style: { height: "24px", width: "24px" } })) : (React.createElement(ArrowUp, { style: { height: "20px", width: "20px" } }))))),
519
+ React.createElement("div", { style: {
520
+ display: "flex",
521
+ alignItems: "center",
522
+ justifyContent: "space-between",
523
+ paddingLeft: "8px",
524
+ paddingRight: "8px",
525
+ } },
526
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "4px" } }, showModelSelector && (React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "8px" } })))))),
527
+ React.createElement("style", null, `
528
+ @keyframes gradient-bg {
529
+ 0%,
530
+ 100% {
531
+ background-position: 0% 50%;
532
+ }
533
+ 50% {
534
+ background-position: 100% 50%;
535
+ }
536
+ }
537
+ @keyframes spin {
538
+ from {
539
+ transform: rotate(0deg);
540
+ }
541
+ to {
542
+ transform: rotate(360deg);
543
+ }
544
+ }
545
+ `))));
546
+ };