made filter popover appear at @ place
This commit is contained in:
parent
3fae4c0fa3
commit
9fcad31aa9
2 changed files with 133 additions and 41 deletions
|
|
@ -1,31 +1,33 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Popover = PopoverPrimitive.Root
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||||
|
|
||||||
const PopoverContent = React.forwardRef<
|
const PopoverContent = React.forwardRef<
|
||||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
))
|
));
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent }
|
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,7 @@ 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 {
|
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover";
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} 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";
|
||||||
|
|
@ -136,6 +132,7 @@ 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 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);
|
||||||
|
|
@ -150,6 +147,59 @@ function ChatPage() {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCursorPosition = (textarea: HTMLTextAreaElement) => {
|
||||||
|
// Create a hidden div with the same styles as the textarea
|
||||||
|
const div = document.createElement('div');
|
||||||
|
const computedStyle = getComputedStyle(textarea);
|
||||||
|
|
||||||
|
// Copy all computed styles to the hidden div
|
||||||
|
for (const style of computedStyle) {
|
||||||
|
(div.style as any)[style] = computedStyle.getPropertyValue(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the div to be hidden but not un-rendered
|
||||||
|
div.style.position = 'absolute';
|
||||||
|
div.style.visibility = 'hidden';
|
||||||
|
div.style.whiteSpace = 'pre-wrap';
|
||||||
|
div.style.wordWrap = 'break-word';
|
||||||
|
div.style.overflow = 'hidden';
|
||||||
|
div.style.height = 'auto';
|
||||||
|
div.style.width = `${textarea.getBoundingClientRect().width}px`;
|
||||||
|
|
||||||
|
// Get the text up to the cursor position
|
||||||
|
const cursorPos = textarea.selectionStart || 0;
|
||||||
|
const textBeforeCursor = textarea.value.substring(0, cursorPos);
|
||||||
|
|
||||||
|
// Add the text before cursor
|
||||||
|
div.textContent = textBeforeCursor;
|
||||||
|
|
||||||
|
// Create a span to mark the end position
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = '|'; // Cursor marker
|
||||||
|
div.appendChild(span);
|
||||||
|
|
||||||
|
// Add the text after cursor to handle word wrapping
|
||||||
|
const textAfterCursor = textarea.value.substring(cursorPos);
|
||||||
|
div.appendChild(document.createTextNode(textAfterCursor));
|
||||||
|
|
||||||
|
// Add the div to the document temporarily
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Get positions
|
||||||
|
const inputRect = textarea.getBoundingClientRect();
|
||||||
|
const divRect = div.getBoundingClientRect();
|
||||||
|
const spanRect = span.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the cursor position relative to the input
|
||||||
|
const x = inputRect.left + (spanRect.left - divRect.left);
|
||||||
|
const y = inputRect.top + (spanRect.top - divRect.top);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(div);
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
const handleEndpointChange = (newEndpoint: EndpointType) => {
|
const handleEndpointChange = (newEndpoint: EndpointType) => {
|
||||||
setEndpoint(newEndpoint);
|
setEndpoint(newEndpoint);
|
||||||
// Clear the conversation when switching endpoints to avoid response ID conflicts
|
// Clear the conversation when switching endpoints to avoid response ID conflicts
|
||||||
|
|
@ -692,7 +742,6 @@ function ChatPage() {
|
||||||
};
|
};
|
||||||
}, [endpoint, setPreviousResponseIds]);
|
}, [endpoint, setPreviousResponseIds]);
|
||||||
|
|
||||||
// Handle click outside to close dropdown
|
|
||||||
|
|
||||||
const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery(
|
const { data: nudges = [], cancel: cancelNudges } = useGetNudgesQuery(
|
||||||
previousResponseIds[endpoint],
|
previousResponseIds[endpoint],
|
||||||
|
|
@ -2065,6 +2114,12 @@ function ChatPage() {
|
||||||
setFilterSearchTerm(searchTerm);
|
setFilterSearchTerm(searchTerm);
|
||||||
setSelectedFilterIndex(0);
|
setSelectedFilterIndex(0);
|
||||||
|
|
||||||
|
// Only set anchor position when @ is first detected (search term is empty)
|
||||||
|
if (searchTerm === "" && !anchorPosition) {
|
||||||
|
const cursorPos = getCursorPosition(e.target);
|
||||||
|
setAnchorPosition(cursorPos);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFilterDropdownOpen) {
|
if (!isFilterDropdownOpen) {
|
||||||
loadAvailableFilters();
|
loadAvailableFilters();
|
||||||
setIsFilterDropdownOpen(true);
|
setIsFilterDropdownOpen(true);
|
||||||
|
|
@ -2074,6 +2129,7 @@ function ChatPage() {
|
||||||
console.log("Closing dropdown - no @ found");
|
console.log("Closing dropdown - no @ found");
|
||||||
setIsFilterDropdownOpen(false);
|
setIsFilterDropdownOpen(false);
|
||||||
setFilterSearchTerm("");
|
setFilterSearchTerm("");
|
||||||
|
setAnchorPosition(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset dismissed flag when user moves to a different word
|
// Reset dismissed flag when user moves to a different word
|
||||||
|
|
@ -2205,30 +2261,64 @@ function ChatPage() {
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute bottom-3 left-3 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isFilterDropdownOpen) {
|
||||||
|
loadAvailableFilters();
|
||||||
|
setIsFilterDropdownOpen(true);
|
||||||
|
// Get cursor position when manually opening
|
||||||
|
if (inputRef.current) {
|
||||||
|
const cursorPos = getCursorPosition(inputRef.current);
|
||||||
|
setAnchorPosition(cursorPos);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsFilterDropdownOpen(false);
|
||||||
|
setAnchorPosition(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AtSign className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Popover
|
<Popover
|
||||||
open={isFilterDropdownOpen}
|
open={isFilterDropdownOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
setIsFilterDropdownOpen(open);
|
setIsFilterDropdownOpen(open);
|
||||||
if (open) {
|
if (!open) {
|
||||||
loadAvailableFilters();
|
setAnchorPosition(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
{anchorPosition && (
|
||||||
<Button
|
<PopoverAnchor
|
||||||
type="button"
|
asChild
|
||||||
variant="ghost"
|
style={{
|
||||||
size="sm"
|
position: 'fixed',
|
||||||
className="absolute bottom-3 left-3 h-8 w-8 p-0 rounded-full hover:bg-muted/50"
|
left: anchorPosition.x,
|
||||||
|
top: anchorPosition.y,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AtSign className="h-4 w-4" />
|
<div />
|
||||||
</Button>
|
</PopoverAnchor>
|
||||||
</PopoverTrigger>
|
)}
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-64 p-2"
|
className="w-64 p-2"
|
||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={6}
|
||||||
|
alignOffset={-18}
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
// Prevent auto focus on the popover content
|
||||||
|
e.preventDefault();
|
||||||
|
// Keep focus on the input
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{filterSearchTerm && (
|
{filterSearchTerm && (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue