-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
import ChatMessage from './ChatMessage';
import { CanvasType, Message, PendingMessage } from '../utils/types';
import CanvasPyInterpreter from './CanvasPyInterpreter';
import StorageUtils from '../utils/storage';
import { useVSCodeContext } from '../utils/llama-vscode';
+import { useChatTextarea, ChatTextareaApi } from './useChatTextarea.ts';
/**
* A message display is a message node with additional information for rendering.
canvasData,
replaceMessageAndGenerate,
} = useAppContext();
- const textarea = useOptimizedTextarea(prefilledMsg.content());
+
+ const textarea: ChatTextareaApi = useChatTextarea(prefilledMsg.content());
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
// TODO: improve this when we have "upload file" feature
</div>
{/* chat input */}
- <div className="flex flex-row items-center pt-8 pb-6 sticky bottom-0 bg-base-100">
+ <div className="flex flex-row items-end pt-8 pb-6 sticky bottom-0 bg-base-100">
<textarea
- className="textarea textarea-bordered w-full"
+ // Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
+ // Large screens (lg:): Disable manual resize, apply max-height for autosize limit
+ className="textarea textarea-bordered w-full resize-vertical lg:resize-none lg:max-h-48 lg:overflow-y-auto" // Adjust lg:max-h-48 as needed (e.g., lg:max-h-60)
placeholder="Type a message (Shift+Enter to add a new line)"
ref={textarea.ref}
+ onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens)
onKeyDown={(e) => {
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
- if (e.key === 'Enter' && e.shiftKey) return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendNewMessage();
}}
id="msg-input"
dir="auto"
+ // Set a base height of 2 rows for mobile views
+ // On lg+ screens, the hook will calculate and set the initial height anyway
+ rows={2}
></textarea>
+
{isGenerating(currConvId ?? '') ? (
<button
className="btn btn-neutral ml-2"
</div>
);
}
-
-export interface OptimizedTextareaValue {
- value: () => string;
- setValue: (value: string) => void;
- focus: () => void;
- ref: React.RefObject<HTMLTextAreaElement>;
-}
-
-// This is a workaround to prevent the textarea from re-rendering when the inner content changes
-// See https://github.com/ggml-org/llama.cpp/pull/12299
-function useOptimizedTextarea(initValue: string): OptimizedTextareaValue {
- const [savedInitValue, setSavedInitValue] = useState<string>(initValue);
- const textareaRef = useRef<HTMLTextAreaElement>(null);
-
- useEffect(() => {
- if (textareaRef.current && savedInitValue) {
- textareaRef.current.value = savedInitValue;
- setSavedInitValue('');
- }
- }, [textareaRef, savedInitValue, setSavedInitValue]);
-
- return {
- value: () => {
- return textareaRef.current?.value ?? savedInitValue;
- },
- setValue: (value: string) => {
- if (textareaRef.current) {
- textareaRef.current.value = value;
- }
- },
- focus: () => {
- if (textareaRef.current) {
- // focus and move the cursor to the end
- textareaRef.current.focus();
- textareaRef.current.selectionStart = textareaRef.current.value.length;
- }
- },
- ref: textareaRef,
- };
-}
--- /dev/null
+import { useEffect, useRef, useState, useCallback } from 'react';
+
+// Media Query for detecting "large" screens (matching Tailwind's lg: breakpoint)
+const LARGE_SCREEN_MQ = '(min-width: 1024px)';
+
+// Calculates and sets the textarea height based on its scrollHeight
+const adjustTextareaHeight = (textarea: HTMLTextAreaElement | null) => {
+ if (!textarea) return;
+
+ // Only perform auto-sizing on large screens
+ if (!window.matchMedia(LARGE_SCREEN_MQ).matches) {
+ // On small screens, reset inline height and max-height styles.
+ // This allows CSS (e.g., `rows` attribute or classes) to control the height,
+ // and enables manual resizing if `resize-vertical` is set.
+ textarea.style.height = ''; // Use 'auto' or '' to reset
+ textarea.style.maxHeight = '';
+ return; // Do not adjust height programmatically on small screens
+ }
+
+ const computedStyle = window.getComputedStyle(textarea);
+ // Get the max-height specified by CSS (e.g., from `lg:max-h-48`)
+ const currentMaxHeight = computedStyle.maxHeight;
+
+ // Temporarily remove max-height to allow scrollHeight to be calculated correctly
+ textarea.style.maxHeight = 'none';
+ // Reset height to 'auto' to measure the actual scrollHeight needed
+ textarea.style.height = 'auto';
+ // Set the height to the calculated scrollHeight
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ // Re-apply the original max-height from CSS to enforce the limit
+ textarea.style.maxHeight = currentMaxHeight;
+};
+
+// Interface describing the API returned by the hook
+export interface ChatTextareaApi {
+ value: () => string;
+ setValue: (value: string) => void;
+ focus: () => void;
+ ref: React.RefObject<HTMLTextAreaElement>;
+ onInput: (event: React.FormEvent<HTMLTextAreaElement>) => void; // Input handler
+}
+
+// This is a workaround to prevent the textarea from re-rendering when the inner content changes
+// See https://github.com/ggml-org/llama.cpp/pull/12299
+// combined now with auto-sizing logic.
+export function useChatTextarea(initValue: string): ChatTextareaApi {
+ const [savedInitValue, setSavedInitValue] = useState<string>(initValue);
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
+
+ // Effect to set initial value and height on mount or when initValue changes
+ useEffect(() => {
+ const textarea = textareaRef.current;
+ if (textarea) {
+ if (typeof savedInitValue === 'string' && savedInitValue.length > 0) {
+ textarea.value = savedInitValue;
+ // Call adjustTextareaHeight - it will check screen size internally
+ setTimeout(() => adjustTextareaHeight(textarea), 0);
+ setSavedInitValue(''); // Reset after applying
+ } else {
+ // Adjust height even if there's no initial value (for initial render)
+ setTimeout(() => adjustTextareaHeight(textarea), 0);
+ }
+ }
+ }, [textareaRef, savedInitValue]); // Depend on ref and savedInitValue
+
+ const handleInput = useCallback(
+ (event: React.FormEvent<HTMLTextAreaElement>) => {
+ // Call adjustTextareaHeight on every input - it will decide whether to act
+ adjustTextareaHeight(event.currentTarget);
+ },
+ []
+ );
+
+ return {
+ // Method to get the current value directly from the textarea
+ value: () => {
+ return textareaRef.current?.value ?? '';
+ },
+ // Method to programmatically set the value and trigger height adjustment
+ setValue: (value: string) => {
+ const textarea = textareaRef.current;
+ if (textarea) {
+ textarea.value = value;
+ // Call adjustTextareaHeight - it will check screen size internally
+ setTimeout(() => adjustTextareaHeight(textarea), 0);
+ }
+ },
+ focus: () => {
+ if (textareaRef.current) {
+ textareaRef.current.focus();
+ }
+ },
+ ref: textareaRef,
+ onInput: handleInput,
+ };
+}
import { useEffect, useState } from 'react';
import { MessageExtraContext } from './types';
-import { OptimizedTextareaValue } from '../components/ChatScreen';
+import { ChatTextareaApi } from '../components/useChatTextarea.ts';
// Extra context when using llama.cpp WebUI from llama-vscode, inside an iframe
// Ref: https://github.com/ggml-org/llama.cpp/pull/11940
* window.postMessage({ command: 'setText', text: 'Spot the syntax error', context: 'def test()\n return 123' }, '*');
*/
-export const useVSCodeContext = (textarea: OptimizedTextareaValue) => {
+export const useVSCodeContext = (textarea: ChatTextareaApi) => {
const [extraContext, setExtraContext] = useState<MessageExtraContext | null>(
null
);