useChatTextarea.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { useEffect, useRef, useState, useCallback } from 'react';
  2. import { throttle } from '../utils/misc';
  3. // Media Query for detecting "large" screens (matching Tailwind's lg: breakpoint)
  4. const LARGE_SCREEN_MQ = '(min-width: 1024px)';
  5. // Calculates and sets the textarea height based on its scrollHeight
  6. const adjustTextareaHeight = throttle(
  7. (textarea: HTMLTextAreaElement | null) => {
  8. if (!textarea) return;
  9. // Only perform auto-sizing on large screens
  10. if (!window.matchMedia(LARGE_SCREEN_MQ).matches) {
  11. // On small screens, reset inline height and max-height styles.
  12. // This allows CSS (e.g., `rows` attribute or classes) to control the height,
  13. // and enables manual resizing if `resize-vertical` is set.
  14. textarea.style.height = ''; // Use 'auto' or '' to reset
  15. textarea.style.maxHeight = '';
  16. return; // Do not adjust height programmatically on small screens
  17. }
  18. const computedStyle = window.getComputedStyle(textarea);
  19. // Get the max-height specified by CSS (e.g., from `lg:max-h-48`)
  20. const currentMaxHeight = computedStyle.maxHeight;
  21. // Temporarily remove max-height to allow scrollHeight to be calculated correctly
  22. textarea.style.maxHeight = 'none';
  23. // Reset height to 'auto' to measure the actual scrollHeight needed
  24. textarea.style.height = 'auto';
  25. // Set the height to the calculated scrollHeight
  26. textarea.style.height = `${textarea.scrollHeight}px`;
  27. // Re-apply the original max-height from CSS to enforce the limit
  28. textarea.style.maxHeight = currentMaxHeight;
  29. },
  30. 100
  31. ); // Throttle to prevent excessive calls
  32. // Interface describing the API returned by the hook
  33. export interface ChatTextareaApi {
  34. value: () => string;
  35. setValue: (value: string) => void;
  36. focus: () => void;
  37. ref: React.RefObject<HTMLTextAreaElement>;
  38. refOnSubmit: React.MutableRefObject<(() => void) | null>; // Submit handler
  39. onInput: (event: React.FormEvent<HTMLTextAreaElement>) => void; // Input handler
  40. }
  41. // This is a workaround to prevent the textarea from re-rendering when the inner content changes
  42. // See https://github.com/ggml-org/llama.cpp/pull/12299
  43. // combined now with auto-sizing logic.
  44. export function useChatTextarea(initValue: string): ChatTextareaApi {
  45. const [savedInitValue, setSavedInitValue] = useState<string>(initValue);
  46. const textareaRef = useRef<HTMLTextAreaElement>(null);
  47. const onSubmitRef = useRef<(() => void) | null>(null);
  48. // Effect to set initial value and height on mount or when initValue changes
  49. useEffect(() => {
  50. const textarea = textareaRef.current;
  51. if (textarea) {
  52. if (typeof savedInitValue === 'string' && savedInitValue.length > 0) {
  53. textarea.value = savedInitValue;
  54. // Call adjustTextareaHeight - it will check screen size internally
  55. setTimeout(() => adjustTextareaHeight(textarea), 0);
  56. setSavedInitValue(''); // Reset after applying
  57. } else {
  58. // Adjust height even if there's no initial value (for initial render)
  59. setTimeout(() => adjustTextareaHeight(textarea), 0);
  60. }
  61. }
  62. }, [textareaRef, savedInitValue]); // Depend on ref and savedInitValue
  63. // On input change, we adjust the height of the textarea
  64. const handleInput = useCallback(
  65. (event: React.FormEvent<HTMLTextAreaElement>) => {
  66. // Call adjustTextareaHeight on every input - it will decide whether to act
  67. adjustTextareaHeight(event.currentTarget);
  68. },
  69. []
  70. );
  71. return {
  72. // Method to get the current value directly from the textarea
  73. value: () => {
  74. return textareaRef.current?.value ?? '';
  75. },
  76. // Method to programmatically set the value and trigger height adjustment
  77. setValue: (value: string) => {
  78. const textarea = textareaRef.current;
  79. if (textarea) {
  80. textarea.value = value;
  81. // Call adjustTextareaHeight - it will check screen size internally
  82. setTimeout(() => adjustTextareaHeight(textarea), 0);
  83. }
  84. },
  85. focus: () => {
  86. if (textareaRef.current) {
  87. textareaRef.current.focus();
  88. }
  89. },
  90. ref: textareaRef,
  91. refOnSubmit: onSubmitRef,
  92. onInput: handleInput, // for adjusting height on input
  93. };
  94. }