ChatInput.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
  2. interface ChatInputProps {
  3. onSendMessage: (text: string) => void;
  4. onStopTask: () => void;
  5. disabled: boolean;
  6. showStopButton: boolean;
  7. setContent?: (setter: (text: string) => void) => void;
  8. isDarkMode?: boolean;
  9. }
  10. export default function ChatInput({
  11. onSendMessage,
  12. onStopTask,
  13. disabled,
  14. showStopButton,
  15. setContent,
  16. isDarkMode = false,
  17. }: ChatInputProps) {
  18. const [text, setText] = useState('');
  19. const isSendButtonDisabled = useMemo(() => disabled || text.trim() === '', [disabled, text]);
  20. const textareaRef = useRef<HTMLTextAreaElement>(null);
  21. // Handle text changes and resize textarea
  22. const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  23. const newText = e.target.value;
  24. setText(newText);
  25. // Resize textarea
  26. const textarea = textareaRef.current;
  27. if (textarea) {
  28. textarea.style.height = 'auto';
  29. textarea.style.height = `${Math.min(textarea.scrollHeight, 100)}px`;
  30. }
  31. };
  32. // Expose a method to set content from outside
  33. useEffect(() => {
  34. if (setContent) {
  35. setContent(setText);
  36. }
  37. }, [setContent]);
  38. // Initial resize when component mounts
  39. useEffect(() => {
  40. const textarea = textareaRef.current;
  41. if (textarea) {
  42. textarea.style.height = 'auto';
  43. textarea.style.height = `${Math.min(textarea.scrollHeight, 100)}px`;
  44. }
  45. }, []);
  46. const handleSubmit = useCallback(
  47. (e: React.FormEvent) => {
  48. e.preventDefault();
  49. if (text.trim()) {
  50. onSendMessage(text);
  51. setText('');
  52. }
  53. },
  54. [text, onSendMessage],
  55. );
  56. const handleKeyDown = useCallback(
  57. (e: React.KeyboardEvent) => {
  58. if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
  59. e.preventDefault();
  60. handleSubmit(e);
  61. }
  62. },
  63. [handleSubmit],
  64. );
  65. return (
  66. <form
  67. onSubmit={handleSubmit}
  68. className={`overflow-hidden rounded-lg border transition-colors ${disabled ? 'cursor-not-allowed' : 'focus-within:border-sky-400 hover:border-sky-400'} ${isDarkMode ? 'border-slate-700' : ''}`}
  69. aria-label="Chat input form">
  70. <div className="flex flex-col">
  71. <textarea
  72. ref={textareaRef}
  73. value={text}
  74. onChange={handleTextChange}
  75. onKeyDown={handleKeyDown}
  76. disabled={disabled}
  77. aria-disabled={disabled}
  78. rows={5}
  79. className={`w-full resize-none border-none p-2 focus:outline-none ${
  80. disabled
  81. ? isDarkMode
  82. ? 'cursor-not-allowed bg-slate-800 text-gray-400'
  83. : 'cursor-not-allowed bg-gray-100 text-gray-500'
  84. : isDarkMode
  85. ? 'bg-slate-800 text-gray-200'
  86. : 'bg-white'
  87. }`}
  88. placeholder="What can I help with?"
  89. aria-label="Message input"
  90. />
  91. <div
  92. className={`flex items-center justify-between px-2 py-1.5 ${
  93. disabled ? (isDarkMode ? 'bg-slate-800' : 'bg-gray-100') : isDarkMode ? 'bg-slate-800' : 'bg-white'
  94. }`}>
  95. <div className="flex gap-2 text-gray-500">{/* Icons can go here */}</div>
  96. {showStopButton ? (
  97. <button
  98. type="button"
  99. onClick={onStopTask}
  100. className="rounded-md bg-red-500 px-3 py-1 text-white transition-colors hover:bg-red-600">
  101. Stop
  102. </button>
  103. ) : (
  104. <button
  105. type="submit"
  106. disabled={isSendButtonDisabled}
  107. aria-disabled={isSendButtonDisabled}
  108. className={`rounded-md bg-[#19C2FF] px-3 py-1 text-white transition-colors hover:enabled:bg-[#0073DC] ${isSendButtonDisabled ? 'cursor-not-allowed opacity-50' : ''}`}>
  109. Send
  110. </button>
  111. )}
  112. </div>
  113. </div>
  114. </form>
  115. );
  116. }