|
@@ -1,4 +1,4 @@
|
|
|
-import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
|
|
+import { useEffect, useMemo, useState } from 'react';
|
|
|
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
|
|
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
|
|
|
import ChatMessage from './ChatMessage';
|
|
import ChatMessage from './ChatMessage';
|
|
|
import { CanvasType, Message, PendingMessage } from '../utils/types';
|
|
import { CanvasType, Message, PendingMessage } from '../utils/types';
|
|
@@ -6,6 +6,7 @@ import { classNames, cleanCurrentUrl, throttle } from '../utils/misc';
|
|
|
import CanvasPyInterpreter from './CanvasPyInterpreter';
|
|
import CanvasPyInterpreter from './CanvasPyInterpreter';
|
|
|
import StorageUtils from '../utils/storage';
|
|
import StorageUtils from '../utils/storage';
|
|
|
import { useVSCodeContext } from '../utils/llama-vscode';
|
|
import { useVSCodeContext } from '../utils/llama-vscode';
|
|
|
|
|
+import { useChatTextarea, ChatTextareaApi } from './useChatTextarea.ts';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* A message display is a message node with additional information for rendering.
|
|
* A message display is a message node with additional information for rendering.
|
|
@@ -99,7 +100,8 @@ export default function ChatScreen() {
|
|
|
canvasData,
|
|
canvasData,
|
|
|
replaceMessageAndGenerate,
|
|
replaceMessageAndGenerate,
|
|
|
} = useAppContext();
|
|
} = useAppContext();
|
|
|
- const textarea = useOptimizedTextarea(prefilledMsg.content());
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const textarea: ChatTextareaApi = useChatTextarea(prefilledMsg.content());
|
|
|
|
|
|
|
|
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
|
|
const { extraContext, clearExtraContext } = useVSCodeContext(textarea);
|
|
|
// TODO: improve this when we have "upload file" feature
|
|
// TODO: improve this when we have "upload file" feature
|
|
@@ -248,14 +250,16 @@ export default function ChatScreen() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* chat input */}
|
|
{/* 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
|
|
<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)"
|
|
placeholder="Type a message (Shift+Enter to add a new line)"
|
|
|
ref={textarea.ref}
|
|
ref={textarea.ref}
|
|
|
|
|
+ onInput={textarea.onInput} // Hook's input handler (will only resize height on lg+ screens)
|
|
|
onKeyDown={(e) => {
|
|
onKeyDown={(e) => {
|
|
|
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
if (e.nativeEvent.isComposing || e.keyCode === 229) return;
|
|
|
- if (e.key === 'Enter' && e.shiftKey) return;
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
e.preventDefault();
|
|
e.preventDefault();
|
|
|
sendNewMessage();
|
|
sendNewMessage();
|
|
@@ -263,7 +267,11 @@ export default function ChatScreen() {
|
|
|
}}
|
|
}}
|
|
|
id="msg-input"
|
|
id="msg-input"
|
|
|
dir="auto"
|
|
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>
|
|
></textarea>
|
|
|
|
|
+
|
|
|
{isGenerating(currConvId ?? '') ? (
|
|
{isGenerating(currConvId ?? '') ? (
|
|
|
<button
|
|
<button
|
|
|
className="btn btn-neutral ml-2"
|
|
className="btn btn-neutral ml-2"
|
|
@@ -286,43 +294,3 @@ export default function ChatScreen() {
|
|
|
</div>
|
|
</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,
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|