@contentgrowth/llm-service 0.8.4 → 0.8.5

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.
@@ -200,6 +200,8 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
200
200
  const [error, setError] = (0, import_react2.useState)(null);
201
201
  const [isSupported, setIsSupported] = (0, import_react2.useState)(false);
202
202
  const recognitionRef = (0, import_react2.useRef)(null);
203
+ const isSimulatingRef = (0, import_react2.useRef)(false);
204
+ const simulationTimeoutRef = (0, import_react2.useRef)(null);
203
205
  (0, import_react2.useEffect)(() => {
204
206
  if (typeof window !== "undefined") {
205
207
  const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
@@ -214,6 +216,7 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
214
216
  setError(null);
215
217
  };
216
218
  recognition.onend = () => {
219
+ if (isSimulatingRef.current) return;
217
220
  setIsListening(false);
218
221
  if (onEnd) onEnd();
219
222
  };
@@ -235,15 +238,18 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
235
238
  recognition.onerror = (event) => {
236
239
  if (event.error === "not-allowed" && process.env.NODE_ENV === "development") {
237
240
  console.warn("Speech recognition blocked. Simulating input for development...");
241
+ isSimulatingRef.current = true;
238
242
  setError(null);
239
243
  setIsListening(true);
240
- setTimeout(() => {
244
+ simulationTimeoutRef.current = setTimeout(() => {
241
245
  const mockText = "This is a simulated voice input for testing.";
242
246
  setTranscript((prev) => prev + (prev ? " " : "") + mockText);
243
247
  if (onResult) onResult(mockText, true);
248
+ isSimulatingRef.current = false;
244
249
  setIsListening(false);
245
250
  if (onEnd) onEnd();
246
- }, 1e3);
251
+ simulationTimeoutRef.current = null;
252
+ }, 3e3);
247
253
  return;
248
254
  }
249
255
  console.error("Speech recognition error", event.error);
@@ -254,11 +260,13 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
254
260
  }
255
261
  }
256
262
  return () => {
257
- if (recognitionRef.current) {
258
- recognitionRef.current.stop();
263
+ if (isSimulatingRef.current && simulationTimeoutRef.current) {
264
+ clearTimeout(simulationTimeoutRef.current);
265
+ simulationTimeoutRef.current = null;
259
266
  }
267
+ recognitionRef.current.stop();
260
268
  };
261
- }, [onResult, onEnd]);
269
+ }, [onResult, onEnd, language]);
262
270
  const start = (0, import_react2.useCallback)(() => {
263
271
  if (recognitionRef.current && !isListening) {
264
272
  try {
@@ -270,10 +278,23 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
270
278
  }
271
279
  }, [isListening]);
272
280
  const stop = (0, import_react2.useCallback)(() => {
281
+ if (isSimulatingRef.current) {
282
+ if (simulationTimeoutRef.current) {
283
+ clearTimeout(simulationTimeoutRef.current);
284
+ simulationTimeoutRef.current = null;
285
+ }
286
+ const mockText = "This is a simulated voice input for testing.";
287
+ setTranscript((prev) => prev + (prev ? " " : "") + mockText);
288
+ if (onResult) onResult(mockText, true);
289
+ isSimulatingRef.current = false;
290
+ setIsListening(false);
291
+ if (onEnd) onEnd();
292
+ return;
293
+ }
273
294
  if (recognitionRef.current && isListening) {
274
295
  recognitionRef.current.stop();
275
296
  }
276
- }, [isListening]);
297
+ }, [isListening, onResult, onEnd]);
277
298
  const resetTranscript = (0, import_react2.useCallback)(() => {
278
299
  setTranscript("");
279
300
  }, []);
@@ -371,14 +392,23 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
371
392
  hintText,
372
393
  placeholder,
373
394
  value,
374
- onChange
395
+ onChange,
396
+ defaultInputMode = "text"
375
397
  }, ref) => {
376
398
  var _a, _b, _c, _d;
377
399
  const [internalMessage, setInternalMessage] = (0, import_react5.useState)("");
378
- const [isVoiceActive, setIsVoiceActive] = (0, import_react5.useState)(false);
379
- const [inputMode, setInputMode] = (0, import_react5.useState)("text");
400
+ const [voiceTrigger, setVoiceTrigger] = (0, import_react5.useState)(null);
401
+ const [inputMode, setInputMode] = (0, import_react5.useState)(defaultInputMode);
402
+ const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
380
403
  const textareaRef = (0, import_react5.useRef)(null);
381
404
  const measurementRef = (0, import_react5.useRef)(null);
405
+ const voiceContainerRef = (0, import_react5.useRef)(null);
406
+ (0, import_react5.useEffect)(() => {
407
+ var _a2;
408
+ if (inputMode === "voice") {
409
+ (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
410
+ }
411
+ }, [inputMode]);
382
412
  const isControlled = value !== void 0;
383
413
  const message = isControlled ? value : internalMessage;
384
414
  const { voice: globalVoice } = useChatConfig();
@@ -396,17 +426,39 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
396
426
  }
397
427
  };
398
428
  const isInputDisabled = (currentTask == null ? void 0 : currentTask.complete) || (lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.interactive) && (((_b = lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.interactiveData) == null ? void 0 : _b.function) === "form" && !(lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.isResponseSubmitted) || ((_c = lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.interactiveData) == null ? void 0 : _c.function) === "confirm" && !(lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.isResponseSubmitted));
399
- useProactiveResize(textareaRef, measurementRef, message, isInputDisabled || isVoiceActive || inputMode === "voice");
429
+ useProactiveResize(textareaRef, measurementRef, message, isInputDisabled || !!voiceTrigger || inputMode === "voice");
430
+ const handleVoiceKeyDown = (e) => {
431
+ if (inputMode !== "voice" || isInputDisabled) return;
432
+ if (e.code !== "Space") return;
433
+ const activeElement = document.activeElement;
434
+ const isInputActive = activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement instanceof HTMLElement && activeElement.isContentEditable);
435
+ if (isInputActive) return;
436
+ e.preventDefault();
437
+ e.stopPropagation();
438
+ if (voiceTrigger === "click") return;
439
+ if (!e.repeat && !voiceTrigger) {
440
+ startRecording("space");
441
+ }
442
+ };
443
+ const handleVoiceKeyUp = (e) => {
444
+ if (inputMode !== "voice" || isInputDisabled) return;
445
+ if (e.code === "Space") {
446
+ if (voiceTrigger === "space") {
447
+ e.preventDefault();
448
+ stopRecording();
449
+ }
450
+ }
451
+ };
400
452
  const nativeSpeech = useSpeechRecognition((text) => {
401
453
  triggerChange(message + (message ? " " : "") + text);
402
454
  }, () => {
403
455
  var _a2;
404
- setIsVoiceActive(false);
456
+ setVoiceTrigger(null);
405
457
  (_a2 = voiceConfig == null ? void 0 : voiceConfig.onVoiceEnd) == null ? void 0 : _a2.call(voiceConfig);
406
458
  }, voiceConfig == null ? void 0 : voiceConfig.language);
407
459
  const customRecorder = useAudioRecorder(async (blob) => {
408
460
  var _a2;
409
- setIsVoiceActive(false);
461
+ setVoiceTrigger(null);
410
462
  (_a2 = voiceConfig == null ? void 0 : voiceConfig.onVoiceEnd) == null ? void 0 : _a2.call(voiceConfig);
411
463
  if (voiceConfig == null ? void 0 : voiceConfig.onAudioCapture) {
412
464
  try {
@@ -419,8 +471,12 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
419
471
  });
420
472
  (0, import_react5.useImperativeHandle)(ref, () => ({
421
473
  focus: () => {
422
- var _a2;
423
- (_a2 = textareaRef.current) == null ? void 0 : _a2.focus();
474
+ var _a2, _b2;
475
+ if (inputMode === "voice") {
476
+ (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
477
+ } else {
478
+ (_b2 = textareaRef.current) == null ? void 0 : _b2.focus();
479
+ }
424
480
  },
425
481
  setValue: (newValue) => {
426
482
  triggerChange(newValue);
@@ -449,15 +505,15 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
449
505
  handleSubmit();
450
506
  }
451
507
  };
452
- const startRecording = async () => {
508
+ const startRecording = async (trigger) => {
453
509
  var _a2;
454
- if (isVoiceActive) return;
455
- setIsVoiceActive(true);
510
+ if (voiceTrigger) return;
511
+ setVoiceTrigger(trigger);
456
512
  (_a2 = voiceConfig == null ? void 0 : voiceConfig.onVoiceStart) == null ? void 0 : _a2.call(voiceConfig);
457
513
  if ((voiceConfig == null ? void 0 : voiceConfig.mode) === "native") {
458
514
  if (!nativeSpeech.isSupported) {
459
515
  alert("Speech recognition is not supported in this browser.");
460
- setIsVoiceActive(false);
516
+ setVoiceTrigger(null);
461
517
  return;
462
518
  }
463
519
  nativeSpeech.start();
@@ -466,7 +522,7 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
466
522
  }
467
523
  };
468
524
  const stopRecording = () => {
469
- if (!isVoiceActive) return;
525
+ if (!voiceTrigger) return;
470
526
  if ((voiceConfig == null ? void 0 : voiceConfig.mode) === "native") {
471
527
  nativeSpeech.stop();
472
528
  } else {
@@ -475,7 +531,7 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
475
531
  };
476
532
  const getPlaceholder = () => {
477
533
  if (placeholder) return placeholder;
478
- if (isVoiceActive) return "Listening...";
534
+ if (voiceTrigger) return "Listening...";
479
535
  if (currentTask == null ? void 0 : currentTask.complete) {
480
536
  return "Task completed!";
481
537
  }
@@ -507,7 +563,7 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
507
563
  {
508
564
  type: "button",
509
565
  onClick: () => {
510
- if (inputMode === "voice" && isVoiceActive) {
566
+ if (inputMode === "voice" && voiceTrigger) {
511
567
  stopRecording();
512
568
  }
513
569
  setInputMode((prev) => prev === "text" ? "voice" : "text");
@@ -523,117 +579,149 @@ var ChatInputArea = (0, import_react5.forwardRef)(({
523
579
  )
524
580
  }
525
581
  ),
526
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 flex items-center border border-gray-300 rounded-lg overflow-hidden focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 bg-white min-h-[42px] mb-1", children: [
527
- inputMode === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
528
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
529
- "span",
530
- {
531
- ref: measurementRef,
532
- className: "absolute invisible whitespace-pre-wrap p-0 m-0 text-gray-700 leading-6",
533
- style: { fontSize: "1rem" }
534
- }
535
- ),
536
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
537
- "textarea",
538
- {
539
- ref: textareaRef,
540
- value: message,
541
- onChange: (e) => {
542
- if (isControlled && onChange) {
543
- onChange(e);
544
- } else {
545
- setInternalMessage(e.target.value);
582
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
583
+ "div",
584
+ {
585
+ ref: voiceContainerRef,
586
+ tabIndex: inputMode === "voice" ? 0 : -1,
587
+ onKeyDown: handleVoiceKeyDown,
588
+ onKeyUp: handleVoiceKeyUp,
589
+ onFocus: () => setIsFocused(true),
590
+ onBlur: () => setIsFocused(false),
591
+ className: "flex-1 flex items-center border border-gray-300 rounded-lg overflow-hidden outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 bg-white min-h-[42px] mb-1",
592
+ children: [
593
+ inputMode === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
594
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
595
+ "span",
596
+ {
597
+ ref: measurementRef,
598
+ className: "absolute invisible whitespace-pre-wrap p-0 m-0 text-gray-700 leading-6",
599
+ style: { fontSize: "1rem" }
546
600
  }
547
- },
548
- onKeyDown: handleKeyDown,
549
- placeholder: getPlaceholder(),
550
- disabled: isInputDisabled || isVoiceActive,
551
- rows: 1,
552
- className: `flex-grow px-4 py-2 outline-none text-gray-700 placeholder-gray-500 disabled:bg-gray-100 resize-none leading-6 w-full ${isInputDisabled ? "cursor-not-allowed" : ""}`
553
- }
554
- )
555
- ] }),
556
- inputMode === "voice" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex-grow flex justify-center items-center p-1", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
557
- "button",
558
- {
559
- type: "button",
560
- onMouseDown: startRecording,
561
- onMouseUp: stopRecording,
562
- onTouchStart: startRecording,
563
- onTouchEnd: stopRecording,
564
- disabled: isInputDisabled,
565
- className: `flex-grow py-2 text-center font-medium rounded-md transition-colors select-none ${isVoiceActive ? "bg-blue-100 text-blue-700" : "bg-gray-50 text-gray-700 hover:bg-gray-100"}`,
566
- children: isVoiceActive ? "Release to Send" : "Hold to Talk"
567
- }
568
- ) }),
569
- (inputMode === "text" || isSending) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative mx-2 flex-shrink-0", children: [
570
- isSending && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute -inset-1", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
571
- "svg",
572
- {
573
- className: "animate-spin h-full w-full text-blue-500 opacity-75",
574
- xmlns: "http://www.w3.org/2000/svg",
575
- fill: "none",
576
- viewBox: "0 0 24 24",
577
- children: [
578
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
579
- "circle",
580
- {
581
- className: "opacity-25",
582
- cx: "12",
583
- cy: "12",
584
- r: "10",
585
- stroke: "currentColor",
586
- strokeWidth: "4"
587
- }
588
- ),
589
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
590
- "path",
591
- {
592
- className: "opacity-75",
593
- fill: "currentColor",
594
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
601
+ ),
602
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
603
+ "textarea",
604
+ {
605
+ ref: textareaRef,
606
+ value: message,
607
+ onChange: (e) => {
608
+ if (isControlled && onChange) {
609
+ onChange(e);
610
+ } else {
611
+ setInternalMessage(e.target.value);
612
+ }
613
+ },
614
+ onKeyDown: handleKeyDown,
615
+ placeholder: getPlaceholder(),
616
+ disabled: isInputDisabled || !!voiceTrigger,
617
+ rows: 1,
618
+ className: `flex-grow px-4 py-2 outline-none text-gray-700 placeholder-gray-500 disabled:bg-gray-100 resize-none leading-6 w-full ${isInputDisabled ? "cursor-not-allowed" : ""}`
619
+ }
620
+ )
621
+ ] }),
622
+ inputMode === "voice" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex-grow flex flex-col justify-center items-center p-1 relative", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
623
+ "button",
624
+ {
625
+ type: "button",
626
+ onClick: () => {
627
+ if (voiceTrigger === "click") {
628
+ stopRecording();
629
+ } else if (!voiceTrigger) {
630
+ startRecording("click");
595
631
  }
596
- )
597
- ]
598
- }
599
- ) }),
600
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
601
- "button",
602
- {
603
- type: "button",
604
- onClick: (e) => {
605
- if (isSending && onStop) {
606
- e.preventDefault();
607
- onStop();
608
- } else {
609
- handleSubmit();
632
+ },
633
+ disabled: isInputDisabled || voiceTrigger === "space",
634
+ className: `w-full py-2 text-center font-medium rounded-md transition-all select-none flex items-center justify-center gap-2 ${voiceTrigger ? "bg-red-50 text-red-600 animate-pulse border border-red-200" : "bg-gray-50 text-gray-700 hover:bg-gray-100"} ${voiceTrigger === "space" ? "opacity-90 cursor-default" : ""}`,
635
+ children: voiceTrigger ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
636
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-2 h-2 rounded-full bg-red-500 animate-ping mr-2" }),
637
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { children: [
638
+ "Listening... ",
639
+ voiceTrigger === "space" ? "(Release Space to send)" : "Tap to send"
640
+ ] })
641
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: "Tap to Talk" })
642
+ }
643
+ ) }),
644
+ (inputMode === "text" || isSending) && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative mx-2 flex-shrink-0", children: [
645
+ isSending && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute -inset-1", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
646
+ "svg",
647
+ {
648
+ className: "animate-spin h-full w-full text-blue-500 opacity-75",
649
+ xmlns: "http://www.w3.org/2000/svg",
650
+ fill: "none",
651
+ viewBox: "0 0 24 24",
652
+ children: [
653
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
654
+ "circle",
655
+ {
656
+ className: "opacity-25",
657
+ cx: "12",
658
+ cy: "12",
659
+ r: "10",
660
+ stroke: "currentColor",
661
+ strokeWidth: "4"
662
+ }
663
+ ),
664
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
665
+ "path",
666
+ {
667
+ className: "opacity-75",
668
+ fill: "currentColor",
669
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
670
+ }
671
+ )
672
+ ]
610
673
  }
611
- },
612
- disabled: (currentTask == null ? void 0 : currentTask.complete) || isSending && !onStop || isInputDisabled || isVoiceActive,
613
- className: `relative z-10 text-white rounded-full p-2 transition-colors duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed ${isSending && onStop ? "bg-red-500 hover:bg-red-600" : "bg-blue-600 hover:bg-blue-700"}`,
614
- title: isSending && onStop ? "Stop generating" : "Send message",
615
- children: isSending ? onStop ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.StopIcon, { className: "h-5 w-5" }) : (
616
- // AND we show the overlay spinner outside?
617
- // Actually `ChatInput.tsx` lines 117-140 are `isLoading && (...)`. It is always shown when loading.
618
- // So we have a spinner ring AROUND the button (absolute -inset-1).
619
- // AND potentially a spinner INSIDE the button if no onStop?
620
- // In my case, I will stick to:
621
- // If onStop: Show StopIcon. Button is Red.
622
- // If !onStop: Show Spinner inside? Or just let the outer ring do the work?
623
- // Legacy `Spinner` component usage inside button suggests double spinner if we are not careful.
624
- // But usually `onStop` is provided for streaming.
625
- // If I look at the screenshot, it shows a RED button (with stop icon) and a BLUE ring around it.
626
- // That matches: Red button (bg-red-500) + Blue Spinner Ring (text-blue-500).
627
- // So I will replicate that structure.
628
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.StopIcon, { className: "h-5 w-5" })
629
- ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.PaperAirplaneIcon, { className: "h-5 w-5" })
630
- }
631
- )
632
- ] })
633
- ] })
674
+ ) }),
675
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
676
+ "button",
677
+ {
678
+ type: "button",
679
+ onClick: (e) => {
680
+ if (isSending && onStop) {
681
+ e.preventDefault();
682
+ onStop();
683
+ } else {
684
+ handleSubmit();
685
+ }
686
+ },
687
+ disabled: (currentTask == null ? void 0 : currentTask.complete) || isSending && !onStop || isInputDisabled || !!voiceTrigger,
688
+ className: `relative z-10 text-white rounded-full p-2 transition-colors duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed ${isSending && onStop ? "bg-red-500 hover:bg-red-600" : "bg-blue-600 hover:bg-blue-700"}`,
689
+ title: isSending && onStop ? "Stop generating" : "Send message",
690
+ children: isSending ? onStop ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.StopIcon, { className: "h-5 w-5" }) : (
691
+ // AND we show the overlay spinner outside?
692
+ // Actually `ChatInput.tsx` lines 117-140 are `isLoading && (...)`. It is always shown when loading.
693
+ // So we have a spinner ring AROUND the button (absolute -inset-1).
694
+ // AND potentially a spinner INSIDE the button if no onStop?
695
+ // In my case, I will stick to:
696
+ // If onStop: Show StopIcon. Button is Red.
697
+ // If !onStop: Show Spinner inside? Or just let the outer ring do the work?
698
+ // Legacy `Spinner` component usage inside button suggests double spinner if we are not careful.
699
+ // But usually `onStop` is provided for streaming.
700
+ // If I look at the screenshot, it shows a RED button (with stop icon) and a BLUE ring around it.
701
+ // That matches: Red button (bg-red-500) + Blue Spinner Ring (text-blue-500).
702
+ // So I will replicate that structure.
703
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.StopIcon, { className: "h-5 w-5" })
704
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_outline.PaperAirplaneIcon, { className: "h-5 w-5" })
705
+ }
706
+ )
707
+ ] })
708
+ ]
709
+ }
710
+ )
634
711
  ] }),
635
712
  inputHint && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-sm text-red-500 bg-red-50 py-1 px-4 rounded-lg mt-1", children: inputHint }),
636
- hintText && inputMode === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-gray-500 ml-12 mb-2 mt-1", children: hintText })
713
+ hintText && inputMode === "text" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-xs text-gray-500 ml-12 mb-2 mt-1", children: hintText }),
714
+ inputMode === "voice" && !voiceTrigger && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
715
+ "p",
716
+ {
717
+ className: "text-[10px] text-gray-400 font-medium ml-12 text-center -mt-1 mb-1 cursor-pointer hover:text-gray-600 transition-colors",
718
+ onClick: () => {
719
+ var _a2;
720
+ return (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
721
+ },
722
+ children: isFocused ? "Click to talk or hold space to talk" : "Tap to talk or click here to focus and push space to talk"
723
+ }
724
+ )
637
725
  ] });
638
726
  });
639
727
  ChatInputArea.displayName = "ChatInputArea";