refactor handlers

This commit is contained in:
Lucas Oliveira 2025-10-02 15:14:35 -03:00
parent 740836a593
commit a6c5ba0d44

View file

@ -20,7 +20,11 @@ import { MarkdownRenderer } from "@/components/markdown-renderer";
import { ProtectedRoute } from "@/components/protected-route"; import { ProtectedRoute } from "@/components/protected-route";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover"; import {
Popover,
PopoverAnchor,
PopoverContent,
} from "@/components/ui/popover";
import { useAuth } from "@/contexts/auth-context"; import { useAuth } from "@/contexts/auth-context";
import { type EndpointType, useChat } from "@/contexts/chat-context"; import { type EndpointType, useChat } from "@/contexts/chat-context";
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
@ -132,7 +136,10 @@ function ChatPage() {
const [dropdownDismissed, setDropdownDismissed] = useState(false); const [dropdownDismissed, setDropdownDismissed] = useState(false);
const [isUserInteracting, setIsUserInteracting] = useState(false); const [isUserInteracting, setIsUserInteracting] = useState(false);
const [isForkingInProgress, setIsForkingInProgress] = useState(false); const [isForkingInProgress, setIsForkingInProgress] = useState(false);
const [anchorPosition, setAnchorPosition] = useState<{ x: number; y: number } | null>(null); const [anchorPosition, setAnchorPosition] = useState<{
x: number;
y: number;
} | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -149,7 +156,7 @@ function ChatPage() {
const getCursorPosition = (textarea: HTMLTextAreaElement) => { const getCursorPosition = (textarea: HTMLTextAreaElement) => {
// Create a hidden div with the same styles as the textarea // Create a hidden div with the same styles as the textarea
const div = document.createElement('div'); const div = document.createElement("div");
const computedStyle = getComputedStyle(textarea); const computedStyle = getComputedStyle(textarea);
// Copy all computed styles to the hidden div // Copy all computed styles to the hidden div
@ -158,12 +165,12 @@ function ChatPage() {
} }
// Set the div to be hidden but not un-rendered // Set the div to be hidden but not un-rendered
div.style.position = 'absolute'; div.style.position = "absolute";
div.style.visibility = 'hidden'; div.style.visibility = "hidden";
div.style.whiteSpace = 'pre-wrap'; div.style.whiteSpace = "pre-wrap";
div.style.wordWrap = 'break-word'; div.style.wordWrap = "break-word";
div.style.overflow = 'hidden'; div.style.overflow = "hidden";
div.style.height = 'auto'; div.style.height = "auto";
div.style.width = `${textarea.getBoundingClientRect().width}px`; div.style.width = `${textarea.getBoundingClientRect().width}px`;
// Get the text up to the cursor position // Get the text up to the cursor position
@ -174,8 +181,8 @@ function ChatPage() {
div.textContent = textBeforeCursor; div.textContent = textBeforeCursor;
// Create a span to mark the end position // Create a span to mark the end position
const span = document.createElement('span'); const span = document.createElement("span");
span.textContent = '|'; // Cursor marker span.textContent = "|"; // Cursor marker
div.appendChild(span); div.appendChild(span);
// Add the text after cursor to handle word wrapping // Add the text after cursor to handle word wrapping
@ -742,7 +749,6 @@ function ChatPage() {
}; };
}, [endpoint, setPreviousResponseIds]); }, [endpoint, setPreviousResponseIds]);
const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery( const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery(
previousResponseIds[endpoint], previousResponseIds[endpoint],
); );
@ -1830,7 +1836,8 @@ function ChatPage() {
)); ));
})()} })()}
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Found {(() => { Found{" "}
{(() => {
let resultsToCount = fc.result; let resultsToCount = fc.result;
if ( if (
fc.result.length > 0 && fc.result.length > 0 &&
@ -1841,7 +1848,8 @@ function ChatPage() {
resultsToCount = fc.result[0].results; resultsToCount = fc.result[0].results;
} }
return resultsToCount.length; return resultsToCount.length;
})()} result })()}{" "}
result
{(() => { {(() => {
let resultsToCount = fc.result; let resultsToCount = fc.result;
if ( if (
@ -1876,6 +1884,172 @@ function ChatPage() {
handleSendMessage(suggestion); handleSendMessage(suggestion);
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Handle backspace for filter clearing
if (e.key === "Backspace" && selectedFilter && input.trim() === "") {
e.preventDefault();
if (isFilterHighlighted) {
// Second backspace - remove the filter
setSelectedFilter(null);
setIsFilterHighlighted(false);
} else {
// First backspace - highlight the filter
setIsFilterHighlighted(true);
}
return;
}
if (isFilterDropdownOpen) {
const filteredFilters = availableFilters.filter((filter) =>
filter.name.toLowerCase().includes(filterSearchTerm.toLowerCase()),
);
if (e.key === "Escape") {
e.preventDefault();
setIsFilterDropdownOpen(false);
setFilterSearchTerm("");
setSelectedFilterIndex(0);
setDropdownDismissed(true);
// Keep focus on the textarea so user can continue typing normally
inputRef.current?.focus();
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedFilterIndex((prev) =>
prev < filteredFilters.length - 1 ? prev + 1 : 0,
);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedFilterIndex((prev) =>
prev > 0 ? prev - 1 : filteredFilters.length - 1,
);
return;
}
if (e.key === "Enter") {
// Check if we're at the end of an @ mention (space before cursor or end of input)
const cursorPos = e.currentTarget.selectionStart || 0;
const textBeforeCursor = input.slice(0, cursorPos);
const words = textBeforeCursor.split(" ");
const lastWord = words[words.length - 1];
if (lastWord.startsWith("@") && filteredFilters[selectedFilterIndex]) {
e.preventDefault();
handleFilterSelect(filteredFilters[selectedFilterIndex]);
return;
}
}
if (e.key === " ") {
// Select filter on space if we're typing an @ mention
const cursorPos = e.currentTarget.selectionStart || 0;
const textBeforeCursor = input.slice(0, cursorPos);
const words = textBeforeCursor.split(" ");
const lastWord = words[words.length - 1];
if (lastWord.startsWith("@") && filteredFilters[selectedFilterIndex]) {
e.preventDefault();
handleFilterSelect(filteredFilters[selectedFilterIndex]);
return;
}
}
}
if (e.key === "Enter" && !e.shiftKey && !isFilterDropdownOpen) {
e.preventDefault();
if (input.trim() && !loading) {
// Trigger form submission by finding the form and calling submit
const form = e.currentTarget.closest("form");
if (form) {
form.requestSubmit();
}
}
}
};
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInput(newValue);
// Clear filter highlight when user starts typing
if (isFilterHighlighted) {
setIsFilterHighlighted(false);
}
// Find if there's an @ at the start of the last word
const words = newValue.split(" ");
const lastWord = words[words.length - 1];
if (lastWord.startsWith("@") && !dropdownDismissed) {
const searchTerm = lastWord.slice(1); // Remove the @
console.log("Setting search term:", searchTerm);
setFilterSearchTerm(searchTerm);
setSelectedFilterIndex(0);
// Only set anchor position when @ is first detected (search term is empty)
if (searchTerm === "") {
const pos = getCursorPosition(e.target);
setAnchorPosition(pos);
}
if (!isFilterDropdownOpen) {
loadAvailableFilters();
setIsFilterDropdownOpen(true);
}
} else if (isFilterDropdownOpen) {
// Close dropdown if @ is no longer present
console.log("Closing dropdown - no @ found");
setIsFilterDropdownOpen(false);
setFilterSearchTerm("");
}
// Reset dismissed flag when user moves to a different word
if (dropdownDismissed && !lastWord.startsWith("@")) {
setDropdownDismissed(false);
}
};
const onAtClick = () => {
if (inputRef.current) {
// Insert @ at current cursor position
const textarea = inputRef.current;
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const currentValue = textarea.value;
// Insert @ at cursor position
const newValue =
currentValue.substring(0, start) + "@" + currentValue.substring(end);
setInput(newValue);
// Set cursor position after the @
const newCursorPos = start + 1;
setTimeout(() => {
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
}, 0);
// Open popover and set anchor position
loadAvailableFilters();
setIsFilterDropdownOpen(true);
setFilterSearchTerm("");
setSelectedFilterIndex(0);
// Get cursor position for popover anchoring
setTimeout(() => {
const cursorPos = getCursorPosition(textarea);
setAnchorPosition(cursorPos);
}, 0);
}
};
return ( return (
<div <div
className={`fixed inset-0 md:left-72 top-[53px] flex flex-col transition-all duration-300 ${ className={`fixed inset-0 md:left-72 top-[53px] flex flex-col transition-all duration-300 ${
@ -2095,156 +2269,8 @@ function ChatPage() {
<textarea <textarea
ref={inputRef} ref={inputRef}
value={input} value={input}
onChange={(e) => { onChange={onChange}
const newValue = e.target.value; onKeyDown={handleKeyDown}
setInput(newValue);
// Clear filter highlight when user starts typing
if (isFilterHighlighted) {
setIsFilterHighlighted(false);
}
// Find if there's an @ at the start of the last word
const words = newValue.split(" ");
const lastWord = words[words.length - 1];
if (lastWord.startsWith("@") && !dropdownDismissed) {
const searchTerm = lastWord.slice(1); // Remove the @
console.log("Setting search term:", searchTerm);
setFilterSearchTerm(searchTerm);
setSelectedFilterIndex(0);
// Only set anchor position when @ is first detected (search term is empty)
if (searchTerm === "") {
const pos = getCursorPosition(e.target);
setAnchorPosition(pos);
}
if (!isFilterDropdownOpen) {
loadAvailableFilters();
setIsFilterDropdownOpen(true);
}
} else if (isFilterDropdownOpen) {
// Close dropdown if @ is no longer present
console.log("Closing dropdown - no @ found");
setIsFilterDropdownOpen(false);
setFilterSearchTerm("");
}
// Reset dismissed flag when user moves to a different word
if (dropdownDismissed && !lastWord.startsWith("@")) {
setDropdownDismissed(false);
}
}}
onKeyDown={(e) => {
// Handle backspace for filter clearing
if (
e.key === "Backspace" &&
selectedFilter &&
input.trim() === ""
) {
e.preventDefault();
if (isFilterHighlighted) {
// Second backspace - remove the filter
setSelectedFilter(null);
setIsFilterHighlighted(false);
} else {
// First backspace - highlight the filter
setIsFilterHighlighted(true);
}
return;
}
if (isFilterDropdownOpen) {
const filteredFilters = availableFilters.filter((filter) =>
filter.name
.toLowerCase()
.includes(filterSearchTerm.toLowerCase()),
);
if (e.key === "Escape") {
e.preventDefault();
setIsFilterDropdownOpen(false);
setFilterSearchTerm("");
setSelectedFilterIndex(0);
setDropdownDismissed(true);
// Keep focus on the textarea so user can continue typing normally
inputRef.current?.focus();
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedFilterIndex((prev) =>
prev < filteredFilters.length - 1 ? prev + 1 : 0,
);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedFilterIndex((prev) =>
prev > 0 ? prev - 1 : filteredFilters.length - 1,
);
return;
}
if (e.key === "Enter") {
// Check if we're at the end of an @ mention (space before cursor or end of input)
const cursorPos = e.currentTarget.selectionStart || 0;
const textBeforeCursor = input.slice(0, cursorPos);
const words = textBeforeCursor.split(" ");
const lastWord = words[words.length - 1];
if (
lastWord.startsWith("@") &&
filteredFilters[selectedFilterIndex]
) {
e.preventDefault();
handleFilterSelect(
filteredFilters[selectedFilterIndex],
);
return;
}
}
if (e.key === " ") {
// Select filter on space if we're typing an @ mention
const cursorPos = e.currentTarget.selectionStart || 0;
const textBeforeCursor = input.slice(0, cursorPos);
const words = textBeforeCursor.split(" ");
const lastWord = words[words.length - 1];
if (
lastWord.startsWith("@") &&
filteredFilters[selectedFilterIndex]
) {
e.preventDefault();
handleFilterSelect(
filteredFilters[selectedFilterIndex],
);
return;
}
}
}
if (
e.key === "Enter" &&
!e.shiftKey &&
!isFilterDropdownOpen
) {
e.preventDefault();
if (input.trim() && !loading) {
// Trigger form submission by finding the form and calling submit
const form = e.currentTarget.closest("form");
if (form) {
form.requestSubmit();
}
}
}
}}
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 ${
@ -2268,38 +2294,7 @@ function ChatPage() {
onMouseDown={(e) => { onMouseDown={(e) => {
e.preventDefault(); e.preventDefault();
}} }}
onClick={(e) => { onClick={onAtClick}
if (inputRef.current) {
// Insert @ at current cursor position
const textarea = inputRef.current;
const start = textarea.selectionStart || 0;
const end = textarea.selectionEnd || 0;
const currentValue = textarea.value;
// Insert @ at cursor position
const newValue = currentValue.substring(0, start) + '@' + currentValue.substring(end);
setInput(newValue);
// Set cursor position after the @
const newCursorPos = start + 1;
setTimeout(() => {
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
}, 0);
// Open popover and set anchor position
loadAvailableFilters();
setIsFilterDropdownOpen(true);
setFilterSearchTerm("");
setSelectedFilterIndex(0);
// Get cursor position for popover anchoring
setTimeout(() => {
const cursorPos = getCursorPosition(textarea);
setAnchorPosition(cursorPos);
}, 0);
}
}}
> >
<AtSign className="h-4 w-4" /> <AtSign className="h-4 w-4" />
</Button> </Button>
@ -2313,12 +2308,12 @@ function ChatPage() {
<PopoverAnchor <PopoverAnchor
asChild asChild
style={{ style={{
position: 'fixed', position: "fixed",
left: anchorPosition.x, left: anchorPosition.x,
top: anchorPosition.y, top: anchorPosition.y,
width: 1, width: 1,
height: 1, height: 1,
pointerEvents: 'none', pointerEvents: "none",
}} }}
> >
<div /> <div />