@contentgrowth/llm-service 0.8.9 → 0.9.1

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.
@@ -236,6 +236,7 @@ declare const useSpeechRecognition: (onResult?: (text: string, isFinal: boolean)
236
236
 
237
237
  interface AudioRecorderHook {
238
238
  isRecording: boolean;
239
+ isSimulated: boolean;
239
240
  start: () => Promise<void>;
240
241
  stop: () => void;
241
242
  blob: Blob | null;
@@ -57,7 +57,7 @@ var MessageBubble = ({
57
57
  )
58
58
  ] }) });
59
59
  }
60
- return /* @__PURE__ */ jsxs("div", { className: `flex items-start gap-3 my-4 ${isUser ? "justify-end" : "justify-start"}`, children: [
60
+ return /* @__PURE__ */ jsxs("div", { className: `flex items-start gap-3 my-1 ${isUser ? "justify-end" : "justify-start"}`, children: [
61
61
  !isUser && /* @__PURE__ */ jsx3("div", { className: "flex-shrink-0 h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center text-white", children: /* @__PURE__ */ jsx3(SparklesIcon, { className: "h-5 w-5" }) }),
62
62
  /* @__PURE__ */ jsxs(
63
63
  "div",
@@ -148,8 +148,8 @@ function ChatHeader({
148
148
  }
149
149
 
150
150
  // src/ui/react/components/ChatInputArea.tsx
151
- import { useState as useState3, useRef as useRef3, useImperativeHandle, forwardRef, useEffect as useEffect3, useCallback as useCallback3 } from "react";
152
- import { StopIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
151
+ import { useState as useState3, useRef as useRef3, useImperativeHandle, forwardRef, useEffect as useEffect3, useCallback as useCallback3, useLayoutEffect } from "react";
152
+ import { MicrophoneIcon, StopIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
153
153
 
154
154
  // src/ui/react/hooks/useSpeechRecognition.ts
155
155
  import { useState, useEffect, useCallback, useRef } from "react";
@@ -277,12 +277,23 @@ var useSpeechRecognition = (onResult, onEnd, language = "en-US") => {
277
277
  import { useState as useState2, useRef as useRef2, useCallback as useCallback2 } from "react";
278
278
  var useAudioRecorder = (onStop) => {
279
279
  const [isRecording, setIsRecording] = useState2(false);
280
+ const [isSimulated, setIsSimulated] = useState2(false);
280
281
  const [blob, setBlob] = useState2(null);
281
282
  const [error, setError] = useState2(null);
282
283
  const mediaRecorderRef = useRef2(null);
283
284
  const chunksRef = useRef2([]);
284
285
  const start = useCallback2(async () => {
285
286
  try {
287
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
288
+ if (process.env.NODE_ENV === "development") {
289
+ console.warn("[useAudioRecorder] MediaDevices not available. Entering simulation mode...");
290
+ setIsRecording(true);
291
+ setIsSimulated(true);
292
+ setError(null);
293
+ return;
294
+ }
295
+ throw new Error("Media devices not available. Ensure you are using HTTPS or localhost.");
296
+ }
286
297
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
287
298
  const mediaRecorder = new MediaRecorder(stream);
288
299
  mediaRecorderRef.current = mediaRecorder;
@@ -310,12 +321,21 @@ var useAudioRecorder = (onStop) => {
310
321
  }
311
322
  }, [onStop]);
312
323
  const stop = useCallback2(() => {
324
+ if (isSimulated) {
325
+ setIsRecording(false);
326
+ setIsSimulated(false);
327
+ const simulatedBlob = new Blob(["simulated speech"], { type: "audio/simulated" });
328
+ setBlob(simulatedBlob);
329
+ if (onStop) onStop(simulatedBlob);
330
+ return;
331
+ }
313
332
  if (mediaRecorderRef.current && mediaRecorderRef.current.state !== "inactive") {
314
333
  mediaRecorderRef.current.stop();
315
334
  }
316
- }, []);
335
+ }, [isSimulated, onStop]);
317
336
  return {
318
337
  isRecording,
338
+ isSimulated,
319
339
  start,
320
340
  stop,
321
341
  blob,
@@ -345,7 +365,7 @@ function useProactiveResize(textareaRef, measurementRef, value, disabled) {
345
365
  }
346
366
 
347
367
  // src/ui/react/components/ChatInputArea.tsx
348
- import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
368
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
349
369
  var ChatInputArea = forwardRef(({
350
370
  onSubmit,
351
371
  isSending,
@@ -364,21 +384,24 @@ var ChatInputArea = forwardRef(({
364
384
  var _a, _b, _c, _d;
365
385
  const [internalMessage, setInternalMessage] = useState3("");
366
386
  const [voiceTrigger, setVoiceTrigger] = useState3(null);
367
- const [inputMode, setInputMode] = useState3(defaultInputMode);
387
+ const [isTranscribing, setIsTranscribing] = useState3(false);
388
+ const [voiceError, setVoiceError] = useState3(null);
368
389
  const [isFocused, setIsFocused] = useState3(false);
369
390
  const textareaRef = useRef3(null);
370
391
  const measurementRef = useRef3(null);
371
- const voiceContainerRef = useRef3(null);
372
- useEffect3(() => {
373
- var _a2;
374
- if (inputMode === "voice") {
375
- (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
376
- }
377
- }, [inputMode]);
392
+ const pendingSelectionRef = useRef3(null);
378
393
  const isControlled = value !== void 0;
379
394
  const message = isControlled ? value : internalMessage;
380
395
  const messageRef = useRef3(message);
381
396
  messageRef.current = message;
397
+ useLayoutEffect(() => {
398
+ if (pendingSelectionRef.current && textareaRef.current) {
399
+ const { start, end } = pendingSelectionRef.current;
400
+ textareaRef.current.focus();
401
+ textareaRef.current.setSelectionRange(start, end);
402
+ pendingSelectionRef.current = null;
403
+ }
404
+ }, [message]);
382
405
  const onChangeRef = useRef3(onChange);
383
406
  useEffect3(() => {
384
407
  onChangeRef.current = onChange;
@@ -391,6 +414,7 @@ var ChatInputArea = forwardRef(({
391
414
  voiceConfigRef.current = voiceConfig;
392
415
  }, [voiceConfig]);
393
416
  const triggerChange = useCallback3((newValue) => {
417
+ setVoiceError(null);
394
418
  if (isControlled && onChangeRef.current) {
395
419
  const syntheticEvent = {
396
420
  target: { value: newValue },
@@ -402,34 +426,30 @@ var ChatInputArea = forwardRef(({
402
426
  }
403
427
  }, [isControlled]);
404
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));
405
- useProactiveResize(textareaRef, measurementRef, message, isInputDisabled || !!voiceTrigger || inputMode === "voice");
406
- const handleVoiceKeyDown = (e) => {
407
- if (inputMode !== "voice" || isInputDisabled) return;
408
- if (e.code !== "Space") return;
409
- const activeElement = document.activeElement;
410
- const isInputActive = activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement instanceof HTMLElement && activeElement.isContentEditable);
411
- if (isInputActive) return;
412
- e.preventDefault();
413
- e.stopPropagation();
414
- if (voiceTrigger === "click") return;
415
- if (!e.repeat && !voiceTrigger) {
416
- startRecording("space");
417
- }
418
- };
419
- const handleVoiceKeyUp = (e) => {
420
- if (inputMode !== "voice" || isInputDisabled) return;
421
- if (e.code === "Space") {
422
- if (voiceTrigger === "space") {
423
- e.preventDefault();
424
- stopRecording();
425
- }
429
+ useProactiveResize(textareaRef, measurementRef, message, isInputDisabled || !!voiceTrigger);
430
+ const insertTextAtCursor = useCallback3((text) => {
431
+ const textarea = textareaRef.current;
432
+ const currentVal = messageRef.current || "";
433
+ if (!textarea) {
434
+ triggerChange(currentVal + (currentVal ? " " : "") + text);
435
+ return;
426
436
  }
427
- };
437
+ const start = textarea.selectionStart;
438
+ const end = textarea.selectionEnd;
439
+ const before = currentVal.substring(0, start);
440
+ const after = currentVal.substring(end);
441
+ const prefix = start > 0 && !/\s$/.test(before) ? " " : "";
442
+ const newText = before + prefix + text + after;
443
+ const selectionStart = start + prefix.length;
444
+ const selectionEnd = selectionStart + text.length;
445
+ pendingSelectionRef.current = { start: selectionStart, end: selectionEnd };
446
+ triggerChange(newText);
447
+ }, [triggerChange]);
428
448
  const handleVoiceResult = useCallback3((text, isFinal) => {
429
449
  if (isFinal) {
430
- triggerChange(messageRef.current + (messageRef.current ? " " : "") + text);
450
+ insertTextAtCursor(text);
431
451
  }
432
- }, []);
452
+ }, [insertTextAtCursor]);
433
453
  const handleVoiceEnd = useCallback3(() => {
434
454
  var _a2, _b2;
435
455
  setVoiceTrigger(null);
@@ -439,24 +459,34 @@ var ChatInputArea = forwardRef(({
439
459
  const customRecorder = useAudioRecorder(async (blob) => {
440
460
  var _a2, _b2, _c2;
441
461
  setVoiceTrigger(null);
462
+ setIsTranscribing(true);
463
+ setVoiceError(null);
442
464
  (_b2 = (_a2 = voiceConfigRef.current) == null ? void 0 : _a2.onVoiceEnd) == null ? void 0 : _b2.call(_a2);
465
+ if (blob.type === "audio/simulated") {
466
+ console.log("[ChatInputArea] Handling simulated audio capture");
467
+ await new Promise((resolve) => setTimeout(resolve, 1500));
468
+ insertTextAtCursor("This is a simulated transcription for development testing.");
469
+ setIsTranscribing(false);
470
+ return;
471
+ }
443
472
  if ((_c2 = voiceConfigRef.current) == null ? void 0 : _c2.onAudioCapture) {
444
473
  try {
445
474
  const text = await voiceConfigRef.current.onAudioCapture(blob);
446
- if (text) triggerChange(messageRef.current + (messageRef.current ? " " : "") + text);
475
+ if (text) insertTextAtCursor(text);
447
476
  } catch (e) {
448
477
  console.error("[ChatInputArea] Audio capture failed", e);
478
+ setVoiceError(e.message || "Transcription failed");
479
+ } finally {
480
+ setIsTranscribing(false);
449
481
  }
482
+ } else {
483
+ setIsTranscribing(false);
450
484
  }
451
485
  });
452
486
  useImperativeHandle(ref, () => ({
453
487
  focus: () => {
454
- var _a2, _b2;
455
- if (inputMode === "voice") {
456
- (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
457
- } else {
458
- (_b2 = textareaRef.current) == null ? void 0 : _b2.focus();
459
- }
488
+ var _a2;
489
+ (_a2 = textareaRef.current) == null ? void 0 : _a2.focus();
460
490
  },
461
491
  setValue: (newValue) => {
462
492
  triggerChange(newValue);
@@ -487,8 +517,9 @@ var ChatInputArea = forwardRef(({
487
517
  };
488
518
  const startRecording = async (trigger) => {
489
519
  var _a2;
490
- if (voiceTrigger) return;
520
+ if (voiceTrigger || isTranscribing) return;
491
521
  setVoiceTrigger(trigger);
522
+ setVoiceError(null);
492
523
  (_a2 = voiceConfig == null ? void 0 : voiceConfig.onVoiceStart) == null ? void 0 : _a2.call(voiceConfig);
493
524
  if ((voiceConfig == null ? void 0 : voiceConfig.mode) === "native") {
494
525
  if (!nativeSpeech.isSupported) {
@@ -500,6 +531,10 @@ var ChatInputArea = forwardRef(({
500
531
  } else {
501
532
  await customRecorder.start();
502
533
  }
534
+ setTimeout(() => {
535
+ var _a3;
536
+ return (_a3 = textareaRef.current) == null ? void 0 : _a3.focus();
537
+ }, 0);
503
538
  };
504
539
  const stopRecording = () => {
505
540
  if (!voiceTrigger) return;
@@ -512,9 +547,7 @@ var ChatInputArea = forwardRef(({
512
547
  const getPlaceholder = () => {
513
548
  if (placeholder) return placeholder;
514
549
  if (voiceTrigger) return "Listening...";
515
- if (currentTask == null ? void 0 : currentTask.complete) {
516
- return "Task completed!";
517
- }
550
+ if (currentTask == null ? void 0 : currentTask.complete) return "Task completed!";
518
551
  if ((lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.interactive) && (lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.interactiveData) && !(lastInteractiveMessage == null ? void 0 : lastInteractiveMessage.isResponseSubmitted)) {
519
552
  const interactiveType = lastInteractiveMessage.interactiveData.function;
520
553
  switch (interactiveType) {
@@ -543,85 +576,61 @@ var ChatInputArea = forwardRef(({
543
576
  {
544
577
  type: "button",
545
578
  onClick: () => {
546
- if (inputMode === "voice" && voiceTrigger) {
579
+ if (voiceTrigger) {
547
580
  stopRecording();
581
+ } else {
582
+ startRecording("click");
548
583
  }
549
- setInputMode((prev) => prev === "text" ? "voice" : "text");
550
584
  },
551
- className: "mb-1 p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors flex-shrink-0 border border-gray-300 bg-white",
552
- title: inputMode === "text" ? "Switch to Voice" : "Switch to Keyboard",
553
- children: inputMode === "text" ? (
554
- // Voice Icon (Waveform)
555
- /* @__PURE__ */ jsx5("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-5 h-5 text-gray-600", children: /* @__PURE__ */ jsx5("path", { d: "M11.25 4.532A.75.75 0 0 1 12 5.25v13.5a.75.75 0 0 1-1.5 0V5.25a.75.75 0 0 1 .75-.718ZM7.5 8.25a.75.75 0 0 1 .75.75v5.25a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm9 0a.75.75 0 0 1 .75.75v5.25a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75ZM3.75 10.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Zm16.5 0a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.75.75 0 0 1 .75-.75Z" }) })
556
- ) : (
557
- // Keyboard Icon (Filled)
558
- /* @__PURE__ */ jsx5("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-5 h-5 text-gray-600", children: /* @__PURE__ */ jsx5("path", { fillRule: "evenodd", d: "M3 6a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6Zm4.5 3a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1-.75-.75V9Zm6 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1-.75-.75V9Zm6 0a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1-.75-.75V9Zm-12 4.5a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1-.75-.75v-1.5Zm6 0a.75.75 0 0 1 .75-.75h6.75a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-.75.75h-6.75a.75.75 0 0 1-.75-.75v-1.5Z", clipRule: "evenodd" }) })
559
- )
585
+ className: `mb-1 p-2 rounded-full transition-all duration-300 flex-shrink-0 border ${voiceTrigger ? "text-white border-orange-400 bg-orange-500 scale-110 shadow-lg animate-pulse" : "text-gray-500 border-gray-300 bg-white hover:text-gray-700 hover:bg-gray-100"}`,
586
+ title: voiceTrigger ? "Stop Recording" : "Start Voice Input",
587
+ children: /* @__PURE__ */ jsx5(MicrophoneIcon, { className: "w-5 h-5" })
560
588
  }
561
589
  ),
562
590
  /* @__PURE__ */ jsxs3(
563
591
  "div",
564
592
  {
565
- ref: voiceContainerRef,
566
- tabIndex: inputMode === "voice" ? 0 : -1,
567
- onKeyDown: handleVoiceKeyDown,
568
- onKeyUp: handleVoiceKeyUp,
569
- onFocus: () => setIsFocused(true),
570
- onBlur: () => setIsFocused(false),
571
- 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",
593
+ tabIndex: -1,
594
+ className: `flex-1 flex items-center border border-gray-300 rounded-lg overflow-hidden outline-none bg-white min-h-[42px] mb-1 transition-all ${voiceTrigger ? "ring-2 ring-orange-100 border-orange-300" : "focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500"}`,
572
595
  children: [
573
- inputMode === "text" && /* @__PURE__ */ jsxs3(Fragment, { children: [
574
- /* @__PURE__ */ jsx5(
575
- "span",
576
- {
577
- ref: measurementRef,
578
- className: "absolute invisible whitespace-pre-wrap p-0 m-0 text-gray-700 leading-6",
579
- style: { fontSize: "1rem" }
580
- }
581
- ),
582
- /* @__PURE__ */ jsx5(
583
- "textarea",
584
- {
585
- ref: textareaRef,
586
- value: message,
587
- onChange: (e) => {
588
- if (isControlled && onChange) {
589
- onChange(e);
590
- } else {
591
- setInternalMessage(e.target.value);
592
- }
593
- },
594
- onKeyDown: handleKeyDown,
595
- placeholder: getPlaceholder(),
596
- disabled: isInputDisabled || !!voiceTrigger,
597
- rows: 1,
598
- 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" : ""}`
599
- }
600
- )
601
- ] }),
602
- inputMode === "voice" && /* @__PURE__ */ jsx5("div", { className: "flex-grow flex flex-col justify-center items-center p-1 relative", children: /* @__PURE__ */ jsx5(
603
- "button",
596
+ /* @__PURE__ */ jsx5(
597
+ "span",
598
+ {
599
+ ref: measurementRef,
600
+ className: "absolute invisible whitespace-pre-wrap p-0 m-0 text-gray-700 leading-6",
601
+ style: { fontSize: "1rem" }
602
+ }
603
+ ),
604
+ /* @__PURE__ */ jsx5(
605
+ "textarea",
604
606
  {
605
- type: "button",
606
- onClick: () => {
607
- if (voiceTrigger === "click") {
608
- stopRecording();
609
- } else if (!voiceTrigger) {
610
- startRecording("click");
607
+ ref: textareaRef,
608
+ value: message,
609
+ onChange: (e) => {
610
+ if (isControlled && onChange) {
611
+ onChange(e);
612
+ } else {
613
+ setInternalMessage(e.target.value);
611
614
  }
612
615
  },
613
- disabled: isInputDisabled || voiceTrigger === "space",
614
- 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" : ""}`,
615
- children: voiceTrigger ? /* @__PURE__ */ jsxs3(Fragment, { children: [
616
- /* @__PURE__ */ jsx5("div", { className: "w-2 h-2 rounded-full bg-red-500 animate-ping mr-2" }),
617
- /* @__PURE__ */ jsxs3("span", { children: [
618
- "Listening... ",
619
- voiceTrigger === "space" ? "(Release Space to send)" : "Tap to send"
620
- ] })
621
- ] }) : /* @__PURE__ */ jsx5("span", { children: "Tap to Talk" })
616
+ onKeyDown: handleKeyDown,
617
+ onFocus: () => {
618
+ setIsFocused(true);
619
+ setVoiceError(null);
620
+ },
621
+ onBlur: () => setIsFocused(false),
622
+ placeholder: getPlaceholder(),
623
+ disabled: isInputDisabled,
624
+ readOnly: !!voiceTrigger || isTranscribing,
625
+ rows: 1,
626
+ className: `flex-grow px-4 py-2 outline-none text-gray-700 placeholder-gray-500 resize-none leading-6 w-full ${isInputDisabled ? "bg-gray-100 cursor-not-allowed" : "bg-transparent"} ${voiceTrigger || isTranscribing ? "cursor-default" : ""}`
622
627
  }
623
- ) }),
624
- (inputMode === "text" || isSending) && /* @__PURE__ */ jsxs3("div", { className: "relative mx-2 flex-shrink-0", children: [
628
+ ),
629
+ isTranscribing && /* @__PURE__ */ jsx5("div", { className: "flex-shrink-0 animate-spin mr-2", children: /* @__PURE__ */ jsxs3("svg", { className: "w-4 h-4 text-orange-500", viewBox: "0 0 24 24", children: [
630
+ /* @__PURE__ */ jsx5("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4", fill: "none" }),
631
+ /* @__PURE__ */ jsx5("path", { className: "opacity-75", fill: "currentColor", 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" })
632
+ ] }) }),
633
+ /* @__PURE__ */ jsxs3("div", { className: "relative mx-2 flex-shrink-0", children: [
625
634
  isSending && /* @__PURE__ */ jsx5("div", { className: "absolute -inset-1", children: /* @__PURE__ */ jsxs3(
626
635
  "svg",
627
636
  {
@@ -664,24 +673,10 @@ var ChatInputArea = forwardRef(({
664
673
  handleSubmit();
665
674
  }
666
675
  },
667
- disabled: (currentTask == null ? void 0 : currentTask.complete) || isSending && !onStop || isInputDisabled || !!voiceTrigger,
676
+ disabled: (currentTask == null ? void 0 : currentTask.complete) || isSending && !onStop || isInputDisabled,
668
677
  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"}`,
669
678
  title: isSending && onStop ? "Stop generating" : "Send message",
670
- children: isSending ? onStop ? /* @__PURE__ */ jsx5(StopIcon, { className: "h-5 w-5" }) : (
671
- // AND we show the overlay spinner outside?
672
- // Actually `ChatInput.tsx` lines 117-140 are `isLoading && (...)`. It is always shown when loading.
673
- // So we have a spinner ring AROUND the button (absolute -inset-1).
674
- // AND potentially a spinner INSIDE the button if no onStop?
675
- // In my case, I will stick to:
676
- // If onStop: Show StopIcon. Button is Red.
677
- // If !onStop: Show Spinner inside? Or just let the outer ring do the work?
678
- // Legacy `Spinner` component usage inside button suggests double spinner if we are not careful.
679
- // But usually `onStop` is provided for streaming.
680
- // If I look at the screenshot, it shows a RED button (with stop icon) and a BLUE ring around it.
681
- // That matches: Red button (bg-red-500) + Blue Spinner Ring (text-blue-500).
682
- // So I will replicate that structure.
683
- /* @__PURE__ */ jsx5(StopIcon, { className: "h-5 w-5" })
684
- ) : /* @__PURE__ */ jsx5(PaperAirplaneIcon, { className: "h-5 w-5" })
679
+ children: isSending ? onStop ? /* @__PURE__ */ jsx5(StopIcon, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx5("div", { className: "w-5 h-5" }) : /* @__PURE__ */ jsx5(PaperAirplaneIcon, { className: "h-5 w-5" })
685
680
  }
686
681
  )
687
682
  ] })
@@ -690,18 +685,10 @@ var ChatInputArea = forwardRef(({
690
685
  )
691
686
  ] }),
692
687
  inputHint && /* @__PURE__ */ jsx5("div", { className: "text-sm text-red-500 bg-red-50 py-1 px-4 rounded-lg mt-1", children: inputHint }),
693
- hintText && inputMode === "text" && /* @__PURE__ */ jsx5("p", { className: "text-xs text-gray-500 ml-12 mb-2 mt-1", children: hintText }),
694
- inputMode === "voice" && !voiceTrigger && /* @__PURE__ */ jsx5(
695
- "p",
696
- {
697
- className: "text-[10px] text-gray-400 font-medium ml-12 text-center -mt-1 mb-1 cursor-pointer hover:text-gray-600 transition-colors",
698
- onClick: () => {
699
- var _a2;
700
- return (_a2 = voiceContainerRef.current) == null ? void 0 : _a2.focus();
701
- },
702
- children: isFocused ? "Click to talk or hold space to talk" : "Tap to talk or click here to focus and push space to talk"
703
- }
704
- )
688
+ /* @__PURE__ */ jsx5("div", { className: "ml-[46px] mb-2 mt-0.5 min-h-[0.75rem]", style: { marginLeft: "48px" }, children: /* @__PURE__ */ jsx5("p", { className: `text-[10px] leading-tight transition-all duration-200 ${voiceError ? "text-red-500" : voiceTrigger || isTranscribing ? "text-orange-600 font-medium" : "text-gray-400"}`, children: voiceError ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1 font-semibold italic", children: [
689
+ "Error: ",
690
+ voiceError
691
+ ] }) : isTranscribing ? "Transcribing, please wait..." : voiceTrigger ? "Listening... tap mic icon again to stop" : hintText || (voiceConfig ? "Type in text or tap mic icon to talk" : "Type your message...") }) })
705
692
  ] });
706
693
  });
707
694
  ChatInputArea.displayName = "ChatInputArea";
@@ -1224,7 +1211,7 @@ var ChatMessageList = ({
1224
1211
  "div",
1225
1212
  {
1226
1213
  ref: chatContainerRef,
1227
- className: "flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50",
1214
+ className: "flex-1 overflow-y-auto p-4 space-y-8 bg-gray-50",
1228
1215
  children: [
1229
1216
  chatHistory.length === 0 && !isProcessing && /* @__PURE__ */ jsxs8("div", { className: "text-center py-8", children: [
1230
1217
  /* @__PURE__ */ jsx11("h3", { className: "text-lg font-medium text-gray-700 mb-2", children: "How can I help you today?" }),
@@ -1280,7 +1267,7 @@ var ChatMessageList = ({
1280
1267
  "div",
1281
1268
  {
1282
1269
  ref: processingIndicatorRef,
1283
- className: "flex justify-start",
1270
+ className: "flex justify-start my-4",
1284
1271
  children: /* @__PURE__ */ jsx11("div", { className: "bg-white text-gray-800 border border-gray-200 rounded-lg px-4 py-2 max-w-[85%]", children: /* @__PURE__ */ jsxs8("div", { className: "flex items-center", children: [
1285
1272
  /* @__PURE__ */ jsx11("span", { className: "text-sm", children: processingHint }),
1286
1273
  /* @__PURE__ */ jsxs8("span", { className: "ml-2 flex space-x-1", children: [