mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
refactor: remove unused comment-related state and components; streamline comment panel styling for improved responsiveness
This commit is contained in:
parent
831ea5d34c
commit
5ecf4e3e9d
11 changed files with 78 additions and 212 deletions
|
|
@ -405,7 +405,6 @@ export default function NewChatPage() {
|
||||||
id: currentThread?.id ?? null,
|
id: currentThread?.id ?? null,
|
||||||
visibility: currentThread?.visibility ?? null,
|
visibility: currentThread?.visibility ?? null,
|
||||||
hasComments: currentThread?.has_comments ?? false,
|
hasComments: currentThread?.has_comments ?? false,
|
||||||
addingCommentToMessageId: null,
|
|
||||||
}));
|
}));
|
||||||
}, [currentThread, setCurrentThreadState]);
|
}, [currentThread, setCurrentThreadState]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,17 @@
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import type { ChatVisibility } from "@/lib/chat/thread-persistence";
|
import type { ChatVisibility } from "@/lib/chat/thread-persistence";
|
||||||
import { reportPanelAtom, reportPanelOpenAtom } from "./report-panel.atom";
|
import { reportPanelAtom } from "./report-panel.atom";
|
||||||
|
|
||||||
// TODO: Update `hasComments` to true when the first comment is created on a thread.
|
|
||||||
// Currently it only updates on thread load. The gutter still works because
|
|
||||||
// `addingCommentToMessageId` keeps it open, but the state is technically stale.
|
|
||||||
|
|
||||||
// TODO: Reset `addingCommentToMessageId` to null after a comment is successfully created.
|
|
||||||
// Currently it stays set until navigation or clicking another message's bubble.
|
|
||||||
// Not causing issues since panel visibility is driven by per-message comment count.
|
|
||||||
|
|
||||||
// TODO: Consider calling `resetCurrentThreadAtom` when unmounting the chat page
|
|
||||||
// for explicit cleanup, though React navigation handles this implicitly.
|
|
||||||
|
|
||||||
interface CurrentThreadState {
|
interface CurrentThreadState {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
visibility: ChatVisibility | null;
|
visibility: ChatVisibility | null;
|
||||||
hasComments: boolean;
|
hasComments: boolean;
|
||||||
addingCommentToMessageId: number | null;
|
|
||||||
/** Whether the right-side comments panel is collapsed (desktop only) */
|
|
||||||
commentsCollapsed: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: CurrentThreadState = {
|
const initialState: CurrentThreadState = {
|
||||||
id: null,
|
id: null,
|
||||||
visibility: null,
|
visibility: null,
|
||||||
hasComments: false,
|
hasComments: false,
|
||||||
addingCommentToMessageId: null,
|
|
||||||
commentsCollapsed: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const currentThreadAtom = atom<CurrentThreadState>(initialState);
|
export const currentThreadAtom = atom<CurrentThreadState>(initialState);
|
||||||
|
|
@ -36,63 +20,22 @@ export const commentsEnabledAtom = atom(
|
||||||
(get) => get(currentThreadAtom).visibility === "SEARCH_SPACE"
|
(get) => get(currentThreadAtom).visibility === "SEARCH_SPACE"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const showCommentsGutterAtom = atom((get) => {
|
|
||||||
const thread = get(currentThreadAtom);
|
|
||||||
// Hide gutter if comments are collapsed
|
|
||||||
if (thread.commentsCollapsed) return false;
|
|
||||||
// Hide gutter if report panel is open (report panel takes the right side)
|
|
||||||
if (get(reportPanelOpenAtom)) return false;
|
|
||||||
return (
|
|
||||||
thread.visibility === "SEARCH_SPACE" &&
|
|
||||||
(thread.hasComments || thread.addingCommentToMessageId !== null)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addingCommentToMessageIdAtom = atom(
|
|
||||||
(get) => get(currentThreadAtom).addingCommentToMessageId,
|
|
||||||
(get, set, messageId: number | null) => {
|
|
||||||
set(currentThreadAtom, { ...get(currentThreadAtom), addingCommentToMessageId: messageId });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setter atom for updating thread visibility
|
|
||||||
export const setThreadVisibilityAtom = atom(null, (get, set, newVisibility: ChatVisibility) => {
|
export const setThreadVisibilityAtom = atom(null, (get, set, newVisibility: ChatVisibility) => {
|
||||||
set(currentThreadAtom, { ...get(currentThreadAtom), visibility: newVisibility });
|
set(currentThreadAtom, { ...get(currentThreadAtom), visibility: newVisibility });
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resetCurrentThreadAtom = atom(null, (_, set) => {
|
export const resetCurrentThreadAtom = atom(null, (_, set) => {
|
||||||
set(currentThreadAtom, initialState);
|
set(currentThreadAtom, initialState);
|
||||||
// Also close the report panel when resetting the thread
|
|
||||||
set(reportPanelAtom, { isOpen: false, reportId: null, title: null, wordCount: null });
|
set(reportPanelAtom, { isOpen: false, reportId: null, title: null, wordCount: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Atom to read whether comments panel is collapsed */
|
|
||||||
export const commentsCollapsedAtom = atom((get) => get(currentThreadAtom).commentsCollapsed);
|
|
||||||
|
|
||||||
/** Atom to toggle the comments collapsed state */
|
|
||||||
export const toggleCommentsCollapsedAtom = atom(null, (get, set) => {
|
|
||||||
const current = get(currentThreadAtom);
|
|
||||||
set(currentThreadAtom, { ...current, commentsCollapsed: !current.commentsCollapsed });
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Atom to explicitly set the comments collapsed state */
|
|
||||||
export const setCommentsCollapsedAtom = atom(null, (get, set, collapsed: boolean) => {
|
|
||||||
set(currentThreadAtom, { ...get(currentThreadAtom), commentsCollapsed: collapsed });
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Target comment ID to scroll to (from URL navigation or inbox click) */
|
/** Target comment ID to scroll to (from URL navigation or inbox click) */
|
||||||
export const targetCommentIdAtom = atom<number | null>(null);
|
export const targetCommentIdAtom = atom<number | null>(null);
|
||||||
|
|
||||||
/** Setter for target comment ID - also ensures comments are not collapsed */
|
export const setTargetCommentIdAtom = atom(null, (_, set, commentId: number | null) => {
|
||||||
export const setTargetCommentIdAtom = atom(null, (get, set, commentId: number | null) => {
|
|
||||||
// Ensure comments are not collapsed when navigating to a comment
|
|
||||||
if (commentId !== null) {
|
|
||||||
set(currentThreadAtom, { ...get(currentThreadAtom), commentsCollapsed: false });
|
|
||||||
}
|
|
||||||
set(targetCommentIdAtom, commentId);
|
set(targetCommentIdAtom, commentId);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Clear target after navigation completes */
|
|
||||||
export const clearTargetCommentIdAtom = atom(null, (_, set) => {
|
export const clearTargetCommentIdAtom = atom(null, (_, set) => {
|
||||||
set(targetCommentIdAtom, null);
|
set(targetCommentIdAtom, null);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ import {
|
||||||
useAssistantState,
|
useAssistantState,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
|
import { CheckIcon, CopyIcon, DownloadIcon, MessageSquare, RefreshCwIcon } from "lucide-react";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
addingCommentToMessageIdAtom,
|
|
||||||
commentsCollapsedAtom,
|
|
||||||
commentsEnabledAtom,
|
commentsEnabledAtom,
|
||||||
targetCommentIdAtom,
|
targetCommentIdAtom,
|
||||||
} from "@/atoms/chat/current-thread.atom";
|
} from "@/atoms/chat/current-thread.atom";
|
||||||
|
|
@ -26,7 +24,6 @@ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
import { CommentPanelContainer } from "@/components/chat-comments/comment-panel-container/comment-panel-container";
|
||||||
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
import { CommentSheet } from "@/components/chat-comments/comment-sheet/comment-sheet";
|
||||||
import { CommentTrigger } from "@/components/chat-comments/comment-trigger/comment-trigger";
|
|
||||||
import { useComments } from "@/hooks/use-comments";
|
import { useComments } from "@/hooks/use-comments";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -96,20 +93,17 @@ function parseMessageId(assistantUiMessageId: string | undefined): number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AssistantMessage: FC = () => {
|
export const AssistantMessage: FC = () => {
|
||||||
const [messageHeight, setMessageHeight] = useState<number | undefined>(undefined);
|
|
||||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
|
const [isInlineOpen, setIsInlineOpen] = useState(false);
|
||||||
const messageRef = useRef<HTMLDivElement>(null);
|
const messageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const commentPanelRef = useRef<HTMLDivElement>(null);
|
||||||
|
const commentTriggerRef = useRef<HTMLButtonElement>(null);
|
||||||
const messageId = useAssistantState(({ message }) => message?.id);
|
const messageId = useAssistantState(({ message }) => message?.id);
|
||||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||||
const dbMessageId = parseMessageId(messageId);
|
const dbMessageId = parseMessageId(messageId);
|
||||||
const commentsEnabled = useAtomValue(commentsEnabledAtom);
|
const commentsEnabled = useAtomValue(commentsEnabledAtom);
|
||||||
const commentsCollapsed = useAtomValue(commentsCollapsedAtom);
|
|
||||||
const [addingCommentToMessageId, setAddingCommentToMessageId] = useAtom(
|
|
||||||
addingCommentToMessageIdAtom
|
|
||||||
);
|
|
||||||
|
|
||||||
// Screen size detection for responsive comment UI
|
// Desktop: >= 1024px (inline expandable), Medium: 768px-1023px (right sheet), Mobile: <768px (bottom sheet)
|
||||||
// Mobile: < 768px (bottom sheet), Medium: 768px - 1024px (right sheet), Desktop: >= 1024px (inline panel)
|
|
||||||
const isMediumScreen = useMediaQuery("(min-width: 768px) and (max-width: 1023px)");
|
const isMediumScreen = useMediaQuery("(min-width: 768px) and (max-width: 1023px)");
|
||||||
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
|
|
||||||
|
|
@ -122,10 +116,8 @@ export const AssistantMessage: FC = () => {
|
||||||
enabled: !!dbMessageId,
|
enabled: !!dbMessageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Target comment navigation - read target from global atom
|
|
||||||
const targetCommentId = useAtomValue(targetCommentIdAtom);
|
const targetCommentId = useAtomValue(targetCommentIdAtom);
|
||||||
|
|
||||||
// Check if target comment belongs to this message (including replies)
|
|
||||||
const hasTargetComment = useMemo(() => {
|
const hasTargetComment = useMemo(() => {
|
||||||
if (!targetCommentId || !commentsData?.comments) return false;
|
if (!targetCommentId || !commentsData?.comments) return false;
|
||||||
return commentsData.comments.some(
|
return commentsData.comments.some(
|
||||||
|
|
@ -135,27 +127,35 @@ export const AssistantMessage: FC = () => {
|
||||||
|
|
||||||
const commentCount = commentsData?.total_count ?? 0;
|
const commentCount = commentsData?.total_count ?? 0;
|
||||||
const hasComments = commentCount > 0;
|
const hasComments = commentCount > 0;
|
||||||
const isAddingComment = dbMessageId !== null && addingCommentToMessageId === dbMessageId;
|
|
||||||
const showCommentPanel = hasComments || isAddingComment;
|
|
||||||
|
|
||||||
const handleToggleAddComment = () => {
|
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
|
||||||
if (!dbMessageId) return;
|
|
||||||
setAddingCommentToMessageId(isAddingComment ? null : dbMessageId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommentTriggerClick = () => {
|
|
||||||
setIsSheetOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Close floating panel when clicking outside (but not on portaled popover/dropdown content)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!messageRef.current) return;
|
if (!isInlineOpen) return;
|
||||||
const el = messageRef.current;
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
const update = () => setMessageHeight(el.offsetHeight);
|
const target = e.target as Element;
|
||||||
update();
|
if (
|
||||||
const observer = new ResizeObserver(update);
|
commentPanelRef.current?.contains(target) ||
|
||||||
observer.observe(el);
|
commentTriggerRef.current?.contains(target) ||
|
||||||
return () => observer.disconnect();
|
target.closest?.("[data-radix-popper-content-wrapper]")
|
||||||
}, []);
|
) return;
|
||||||
|
setIsInlineOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [isInlineOpen]);
|
||||||
|
|
||||||
|
// Auto-open floating panel on desktop when this message has the target comment
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasTargetComment && isDesktop && commentsLoaded) {
|
||||||
|
setIsInlineOpen(true);
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
messageRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [hasTargetComment, isDesktop, commentsLoaded]);
|
||||||
|
|
||||||
// Auto-open sheet on mobile/tablet when this message has the target comment
|
// Auto-open sheet on mobile/tablet when this message has the target comment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -164,20 +164,6 @@ export const AssistantMessage: FC = () => {
|
||||||
}
|
}
|
||||||
}, [hasTargetComment, isDesktop, commentsLoaded]);
|
}, [hasTargetComment, isDesktop, commentsLoaded]);
|
||||||
|
|
||||||
// Scroll message into view when it contains target comment (desktop)
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasTargetComment && isDesktop && commentsLoaded && messageRef.current) {
|
|
||||||
// Small delay to ensure DOM is ready after comments render
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
messageRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}, [hasTargetComment, isDesktop, commentsLoaded]);
|
|
||||||
|
|
||||||
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
|
|
||||||
|
|
||||||
// Determine sheet side based on screen size
|
|
||||||
const sheetSide = isMediumScreen ? "right" : "bottom";
|
const sheetSide = isMediumScreen ? "right" : "bottom";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -186,54 +172,23 @@ export const AssistantMessage: FC = () => {
|
||||||
className="aui-assistant-message-root group fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
|
className="aui-assistant-message-root group fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
|
||||||
data-role="assistant"
|
data-role="assistant"
|
||||||
>
|
>
|
||||||
<AssistantMessageInner />
|
{/* Comment trigger — right-aligned, just below user query on all screen sizes */}
|
||||||
|
{showCommentTrigger && (
|
||||||
{/* Desktop comment panel - only on lg screens and above, hidden when collapsed */}
|
<div className="mr-2 mb-1 flex justify-end">
|
||||||
{searchSpaceId && commentsEnabled && !isMessageStreaming && !commentsCollapsed && (
|
|
||||||
<div className="absolute left-full top-0 ml-4 hidden lg:block w-72">
|
|
||||||
<div
|
|
||||||
className={`sticky top-3 ${showCommentPanel ? "opacity-100" : "opacity-0 group-hover:opacity-100"} transition-opacity`}
|
|
||||||
>
|
|
||||||
{!hasComments && (
|
|
||||||
<CommentTrigger
|
|
||||||
commentCount={0}
|
|
||||||
isOpen={isAddingComment}
|
|
||||||
onClick={handleToggleAddComment}
|
|
||||||
disabled={!dbMessageId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showCommentPanel && dbMessageId && (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
hasComments ? "" : "mt-2 animate-in fade-in slide-in-from-top-2 duration-200"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CommentPanelContainer
|
|
||||||
messageId={dbMessageId}
|
|
||||||
isOpen={true}
|
|
||||||
maxHeight={messageHeight}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile & Medium screen comment trigger - shown below lg breakpoint */}
|
|
||||||
{showCommentTrigger && !isDesktop && (
|
|
||||||
<div className="ml-2 mt-1 flex justify-start">
|
|
||||||
<button
|
<button
|
||||||
|
ref={isDesktop ? commentTriggerRef : undefined}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCommentTriggerClick}
|
onClick={isDesktop ? () => setIsInlineOpen((prev) => !prev) : () => setIsSheetOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 rounded-full px-3 py-1.5 text-sm transition-colors",
|
"flex items-center gap-1.5 rounded-full px-3 py-1 text-sm transition-colors",
|
||||||
hasComments
|
isDesktop && isInlineOpen
|
||||||
? "border border-primary/50 bg-primary/5 text-primary hover:bg-primary/10"
|
? "bg-primary/10 text-primary"
|
||||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
: hasComments
|
||||||
|
? "text-primary hover:bg-primary/10"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageSquare className={cn("size-4", hasComments && "fill-current")} />
|
<MessageSquare className={cn("size-3.5", hasComments && "fill-current")} />
|
||||||
{hasComments ? (
|
{hasComments ? (
|
||||||
<span>
|
<span>
|
||||||
{commentCount} {commentCount === 1 ? "comment" : "comments"}
|
{commentCount} {commentCount === 1 ? "comment" : "comments"}
|
||||||
|
|
@ -245,7 +200,23 @@ export const AssistantMessage: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comment sheet - bottom for mobile, right for medium screens */}
|
{/* Desktop floating comment panel — overlays on top of chat content */}
|
||||||
|
{showCommentTrigger && isDesktop && isInlineOpen && dbMessageId && (
|
||||||
|
<div
|
||||||
|
ref={commentPanelRef}
|
||||||
|
className="absolute right-0 top-10 z-30 w-full max-w-md animate-in fade-in slide-in-from-top-2 duration-200"
|
||||||
|
>
|
||||||
|
<CommentPanelContainer
|
||||||
|
messageId={dbMessageId}
|
||||||
|
isOpen={true}
|
||||||
|
variant="inline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AssistantMessageInner />
|
||||||
|
|
||||||
|
{/* Comment sheet — bottom for mobile, right for medium screens */}
|
||||||
{showCommentTrigger && !isDesktop && (
|
{showCommentTrigger && !isDesktop && (
|
||||||
<CommentSheet
|
<CommentSheet
|
||||||
messageId={dbMessageId}
|
messageId={dbMessageId}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import { useParams } from "next/navigation";
|
||||||
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
|
||||||
import { showCommentsGutterAtom } from "@/atoms/chat/current-thread.atom";
|
|
||||||
import {
|
import {
|
||||||
mentionedDocumentsAtom,
|
mentionedDocumentsAtom,
|
||||||
sidebarSelectedDocumentsAtom,
|
sidebarSelectedDocumentsAtom,
|
||||||
|
|
@ -95,8 +94,6 @@ export const Thread: FC<ThreadProps> = ({ messageThinkingSteps = new Map() }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadContent: FC = () => {
|
const ThreadContent: FC = () => {
|
||||||
const showGutter = useAtomValue(showCommentsGutterAtom);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadPrimitive.Root
|
<ThreadPrimitive.Root
|
||||||
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background"
|
className="aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-background"
|
||||||
|
|
@ -106,10 +103,7 @@ const ThreadContent: FC = () => {
|
||||||
>
|
>
|
||||||
<ThreadPrimitive.Viewport
|
<ThreadPrimitive.Viewport
|
||||||
turnAnchor="top"
|
turnAnchor="top"
|
||||||
className={cn(
|
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
|
||||||
"aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4 transition-[padding] duration-300 ease-out",
|
|
||||||
showGutter && "lg:pr-30"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<AssistantIf condition={({ thread }) => thread.isEmpty}>
|
<AssistantIf condition={({ thread }) => thread.isEmpty}>
|
||||||
<ThreadWelcome />
|
<ThreadWelcome />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@ export interface CommentPanelContainerProps {
|
||||||
messageId: number;
|
messageId: number;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Variant for responsive styling - desktop shows border/bg, mobile is plain */
|
/** Variant for responsive styling - desktop shows border/bg, mobile is plain, inline fits within message width */
|
||||||
variant?: "desktop" | "mobile";
|
variant?: "desktop" | "mobile" | "inline";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,15 @@ export function CommentPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMobile = variant === "mobile";
|
const isMobile = variant === "mobile";
|
||||||
|
const isInline = variant === "inline";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[120px] items-center justify-center p-4",
|
"flex min-h-[120px] items-center justify-center p-4",
|
||||||
!isMobile && "w-96 rounded-lg border bg-card"
|
isInline && "w-full rounded-xl border bg-card shadow-lg",
|
||||||
|
!isMobile && !isInline && "w-96 rounded-lg border bg-card"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
|
@ -65,8 +67,13 @@ export function CommentPanel({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex flex-col", isMobile ? "w-full" : "w-85 rounded-lg border bg-card")}
|
className={cn(
|
||||||
style={!isMobile && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
"flex flex-col",
|
||||||
|
isMobile && "w-full",
|
||||||
|
isInline && "w-full rounded-xl border bg-card shadow-lg max-h-80",
|
||||||
|
!isMobile && !isInline && "w-85 rounded-lg border bg-card"
|
||||||
|
)}
|
||||||
|
style={!isMobile && !isInline && effectiveMaxHeight ? { maxHeight: effectiveMaxHeight } : undefined}
|
||||||
>
|
>
|
||||||
{hasThreads && (
|
{hasThreads && (
|
||||||
<div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}>
|
<div className={cn("min-h-0 flex-1 overflow-y-auto scrollbar-thin", isMobile && "pb-24")}>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,6 @@ export interface CommentPanelProps {
|
||||||
onDeleteComment: (commentId: number) => void;
|
onDeleteComment: (commentId: number) => void;
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
/** Variant for responsive styling - desktop shows border/bg, mobile is plain */
|
/** Variant for responsive styling - desktop shows border/bg, mobile is plain, inline fits within message width */
|
||||||
variant?: "desktop" | "mobile";
|
variant?: "desktop" | "mobile" | "inline";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { MessageSquarePlus } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import type { CommentTriggerProps } from "./types";
|
|
||||||
|
|
||||||
export function CommentTrigger({ commentCount, isOpen, onClick, disabled }: CommentTriggerProps) {
|
|
||||||
const hasComments = commentCount > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant={hasComments ? "outline" : isOpen ? "secondary" : "ghost"}
|
|
||||||
size="icon"
|
|
||||||
disabled={disabled}
|
|
||||||
className={cn(
|
|
||||||
"relative size-10 rounded-full transition-all duration-200",
|
|
||||||
hasComments
|
|
||||||
? "border-primary/50 bg-primary/5 text-primary hover:bg-primary/10 hover:border-primary"
|
|
||||||
: isOpen
|
|
||||||
? "text-foreground"
|
|
||||||
: "text-muted-foreground hover:text-foreground",
|
|
||||||
!hasComments && !isOpen && "opacity-0 group-hover:opacity-100",
|
|
||||||
disabled && "cursor-not-allowed opacity-50"
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<MessageSquarePlus className={cn("size-5", (hasComments || isOpen) && "fill-current")} />
|
|
||||||
{hasComments && (
|
|
||||||
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground">
|
|
||||||
{commentCount > 9 ? "9+" : commentCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export interface CommentTriggerProps {
|
|
||||||
commentCount: number;
|
|
||||||
isOpen: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
|
||||||
import { setCommentsCollapsedAtom, setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom";
|
||||||
import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item";
|
import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item";
|
||||||
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";
|
||||||
|
|
@ -168,7 +168,6 @@ export function InboxSidebar({
|
||||||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
const isMobile = !useMediaQuery("(min-width: 640px)");
|
||||||
const searchSpaceId = params?.search_space_id ? Number(params.search_space_id) : null;
|
const searchSpaceId = params?.search_space_id ? Number(params.search_space_id) : null;
|
||||||
|
|
||||||
const [, setCommentsCollapsed] = useAtom(setCommentsCollapsedAtom);
|
|
||||||
const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom);
|
const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom);
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
@ -831,11 +830,9 @@ export function InboxSidebar({
|
||||||
className="h-8 w-8 rounded-full"
|
className="h-8 w-8 rounded-full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isDocked) {
|
if (isDocked) {
|
||||||
setCommentsCollapsed(false);
|
|
||||||
onDockedChange(false);
|
onDockedChange(false);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} else {
|
} else {
|
||||||
setCommentsCollapsed(true);
|
|
||||||
onDockedChange(true);
|
onDockedChange(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -538,9 +538,6 @@ function MobileReportDrawer() {
|
||||||
*
|
*
|
||||||
* On desktop (lg+): Renders as a right-side split panel (flex sibling to the chat thread)
|
* On desktop (lg+): Renders as a right-side split panel (flex sibling to the chat thread)
|
||||||
* On mobile/tablet: Renders as a Vaul bottom drawer
|
* On mobile/tablet: Renders as a Vaul bottom drawer
|
||||||
*
|
|
||||||
* When open on desktop, the comments gutter is automatically suppressed
|
|
||||||
* (handled via showCommentsGutterAtom in current-thread.atom.ts)
|
|
||||||
*/
|
*/
|
||||||
export function ReportPanel() {
|
export function ReportPanel() {
|
||||||
const panelState = useAtomValue(reportPanelAtom);
|
const panelState = useAtomValue(reportPanelAtom);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue