fixed textarea scrolling

This commit is contained in:
Lucas Oliveira 2025-10-02 17:33:35 -03:00
parent ae98857cf7
commit 80ac15edbb
3 changed files with 88 additions and 13 deletions

View file

@ -44,6 +44,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"rehype-mathjax": "^7.1.0", "rehype-mathjax": "^7.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
@ -8473,6 +8474,23 @@
"react": ">= 0.14.0" "react": ">= 0.14.0"
} }
}, },
"node_modules/react-textarea-autosize": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
"integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",
"use-latest": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -10126,6 +10144,51 @@
} }
} }
}, },
"node_modules/use-composed-ref": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
"integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-latest": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
"integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
"license": "MIT",
"dependencies": {
"use-isomorphic-layout-effect": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": { "node_modules/use-sidecar": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",

View file

@ -45,6 +45,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"rehype-mathjax": "^7.1.0", "rehype-mathjax": "^7.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",

View file

@ -17,6 +17,7 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { filterAccentClasses } from "@/components/knowledge-filter-panel"; import { filterAccentClasses } from "@/components/knowledge-filter-panel";
import { MarkdownRenderer } from "@/components/markdown-renderer"; import { MarkdownRenderer } from "@/components/markdown-renderer";
import { ProtectedRoute } from "@/components/protected-route"; import { ProtectedRoute } from "@/components/protected-route";
@ -132,6 +133,7 @@ function ChatPage() {
const [availableFilters, setAvailableFilters] = useState< const [availableFilters, setAvailableFilters] = useState<
KnowledgeFilterData[] KnowledgeFilterData[]
>([]); >([]);
const [textareaHeight, setTextareaHeight] = useState(40);
const [filterSearchTerm, setFilterSearchTerm] = useState(""); const [filterSearchTerm, setFilterSearchTerm] = useState("");
const [selectedFilterIndex, setSelectedFilterIndex] = useState(0); const [selectedFilterIndex, setSelectedFilterIndex] = useState(0);
const [isFilterHighlighted, setIsFilterHighlighted] = useState(false); const [isFilterHighlighted, setIsFilterHighlighted] = useState(false);
@ -2024,14 +2026,16 @@ function ChatPage() {
setIsFilterDropdownOpen(true); setIsFilterDropdownOpen(true);
setFilterSearchTerm(""); setFilterSearchTerm("");
setSelectedFilterIndex(0); setSelectedFilterIndex(0);
// Get button position for popover anchoring // Get button position for popover anchoring
const button = document.querySelector('[data-filter-button]') as HTMLElement; const button = document.querySelector(
"[data-filter-button]",
) as HTMLElement;
if (button) { if (button) {
const rect = button.getBoundingClientRect(); const rect = button.getBoundingClientRect();
setAnchorPosition({ setAnchorPosition({
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2 - 12 y: rect.top + rect.height / 2 - 12,
}); });
} }
} else { } else {
@ -2256,18 +2260,29 @@ function ChatPage() {
</span> </span>
</div> </div>
)} )}
<textarea <div className="relative" style={{height: `${textareaHeight + 60}px`}}>
<TextareaAutosize
ref={inputRef} ref={inputRef}
value={input} value={input}
onChange={onChange} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onHeightChange={(height) => setTextareaHeight(height)}
maxRows={7}
minRows={2}
placeholder="Type to ask a question..." placeholder="Type to ask a question..."
disabled={loading} disabled={loading}
className={`w-full bg-transparent px-4 ${ className={`w-full bg-transparent px-4 ${
selectedFilter ? "py-2 pb-4" : "py-4" selectedFilter ? "pt-2" : "pt-4"
} min-h-[100px] focus-visible:outline-none resize-none`} } focus-visible:outline-none resize-none`}
rows={1} rows={2}
/> />
{/* Safe area at bottom for buttons */}
<div
className="absolute bottom-0 left-0 right-0 bg-transparent pointer-events-none"
style={{ height: '60px' }}
/>
</div>
</div> </div>
<input <input
ref={fileInputRef} ref={fileInputRef}
@ -2344,9 +2359,7 @@ function ChatPage() {
> >
<span>No knowledge filter</span> <span>No knowledge filter</span>
{!selectedFilter && ( {!selectedFilter && (
<Check <Check className="h-4 w-4 shrink-0" />
className="h-4 w-4 shrink-0"
/>
)} )}
</button> </button>
)} )}
@ -2376,9 +2389,7 @@ function ChatPage() {
)} )}
</div> </div>
{selectedFilter?.id === filter.id && ( {selectedFilter?.id === filter.id && (
<Check <Check className="h-4 w-4 shrink-0" />
className="h-4 w-4 shrink-0"
/>
)} )}
</button> </button>
))} ))}