ChatInput.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. /**
  2. * 聊天输入组件 - 移动端优化
  3. */
  4. 'use client';
  5. import { useState, FormEvent, KeyboardEvent, useRef, useEffect } from 'react';
  6. interface ChatInputProps {
  7. onSend: (message: string) => void;
  8. isLoading: boolean;
  9. onAbort: () => void;
  10. }
  11. export default function ChatInput({ onSend, isLoading, onAbort }: ChatInputProps) {
  12. const [input, setInput] = useState('');
  13. const textareaRef = useRef<HTMLTextAreaElement>(null);
  14. // 自动调整 textarea 高度
  15. useEffect(() => {
  16. if (textareaRef.current) {
  17. textareaRef.current.style.height = 'auto';
  18. textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 150)}px`;
  19. }
  20. }, [input]);
  21. const handleSubmit = (e: FormEvent) => {
  22. e.preventDefault();
  23. if (input.trim() && !isLoading) {
  24. onSend(input.trim());
  25. setInput('');
  26. // 重置 textarea 高度
  27. if (textareaRef.current) {
  28. textareaRef.current.style.height = 'auto';
  29. }
  30. }
  31. };
  32. const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
  33. if (e.key === 'Enter' && !e.shiftKey) {
  34. e.preventDefault();
  35. handleSubmit(e);
  36. }
  37. };
  38. return (
  39. <form onSubmit={handleSubmit} className="flex items-end gap-2">
  40. <div className="flex-1 relative">
  41. <textarea
  42. ref={textareaRef}
  43. value={input}
  44. onChange={(e) => setInput(e.target.value)}
  45. onKeyDown={handleKeyDown}
  46. placeholder="输入消息... (Shift+Enter 换行)"
  47. disabled={isLoading}
  48. rows={1}
  49. className="w-full resize-none border border-gray-300 dark:border-gray-600 rounded-xl px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white text-[16px] leading-relaxed transition-all"
  50. style={{ minHeight: '48px', maxHeight: '150px' }}
  51. />
  52. {/* 字符计数 */}
  53. {input.length > 0 && (
  54. <span className="absolute bottom-2 right-3 text-xs text-gray-400">
  55. {input.length}
  56. </span>
  57. )}
  58. </div>
  59. {/* 移动端:图标按钮 */}
  60. {isLoading ? (
  61. <button
  62. type="button"
  63. onClick={onAbort}
  64. className="flex items-center justify-center w-12 h-12 min-w-[48px] bg-red-500 hover:bg-red-600 text-white rounded-xl transition-all active:scale-95 shadow-sm"
  65. aria-label="停止生成"
  66. >
  67. <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
  68. <rect x="6" y="6" width="12" height="12" rx="2" />
  69. </svg>
  70. </button>
  71. ) : (
  72. <button
  73. type="submit"
  74. disabled={!input.trim()}
  75. className="flex items-center justify-center w-12 h-12 min-w-[48px] bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-xl transition-all active:scale-95 shadow-sm disabled:active:scale-100"
  76. aria-label="发送消息"
  77. >
  78. <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  79. <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
  80. </svg>
  81. </button>
  82. )}
  83. </form>
  84. );
  85. }