diff --git a/surfsense_web/app/preview/chat-comments/page.tsx b/surfsense_web/app/preview/chat-comments/page.tsx index 9272f5f87..5e3466468 100644 --- a/surfsense_web/app/preview/chat-comments/page.tsx +++ b/surfsense_web/app/preview/chat-comments/page.tsx @@ -4,6 +4,8 @@ import { useState } from "react"; import { CommentComposer } from "@/components/chat-comments/comment-composer/comment-composer"; import { CommentItem } from "@/components/chat-comments/comment-item/comment-item"; import type { CommentData } from "@/components/chat-comments/comment-item/types"; +import { CommentThread } from "@/components/chat-comments/comment-thread/comment-thread"; +import type { CommentThreadData } from "@/components/chat-comments/comment-thread/types"; import { MemberMentionPicker } from "@/components/chat-comments/member-mention-picker/member-mention-picker"; import type { MemberOption } from "@/components/chat-comments/member-mention-picker/types"; @@ -91,6 +93,97 @@ const fakeCommentsData: CommentData[] = [ }, ]; +const fakeThreadsData: CommentThreadData[] = [ + { + id: 1, + messageId: 101, + content: "This is a great response! @Alice Smith can you review?", + contentRendered: "This is a great response! @Alice Smith can you review?", + author: { + id: "550e8400-e29b-41d4-a716-446655440002", + displayName: "Bob Johnson", + email: "bob.johnson@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 3600000).toISOString(), + updatedAt: new Date(Date.now() - 3600000).toISOString(), + isEdited: false, + canEdit: true, + canDelete: true, + replyCount: 2, + replies: [ + { + id: 2, + content: "I checked this yesterday and it looks good.", + contentRendered: "I checked this yesterday and it looks good.", + author: { + id: "550e8400-e29b-41d4-a716-446655440001", + displayName: "Alice Smith", + email: "alice@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 1800000).toISOString(), + updatedAt: new Date(Date.now() - 1800000).toISOString(), + isEdited: false, + canEdit: false, + canDelete: true, + }, + { + id: 3, + content: "Thanks @Alice Smith!", + contentRendered: "Thanks @Alice Smith!", + author: { + id: "550e8400-e29b-41d4-a716-446655440002", + displayName: "Bob Johnson", + email: "bob.johnson@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 900000).toISOString(), + updatedAt: new Date(Date.now() - 900000).toISOString(), + isEdited: false, + canEdit: true, + canDelete: true, + }, + ], + }, + { + id: 4, + messageId: 101, + content: "Can we also add some documentation for this feature?", + contentRendered: "Can we also add some documentation for this feature?", + author: { + id: "550e8400-e29b-41d4-a716-446655440003", + displayName: "Charlie Brown", + email: "charlie@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 7200000).toISOString(), + updatedAt: new Date(Date.now() - 7200000).toISOString(), + isEdited: false, + canEdit: false, + canDelete: true, + replyCount: 1, + replies: [ + { + id: 5, + content: "Good idea @Charlie Brown, I'll create a ticket for that.", + contentRendered: "Good idea @Charlie Brown, I'll create a ticket for that.", + author: { + id: "550e8400-e29b-41d4-a716-446655440002", + displayName: "Bob Johnson", + email: "bob.johnson@example.com", + avatarUrl: null, + }, + createdAt: new Date(Date.now() - 6000000).toISOString(), + updatedAt: new Date(Date.now() - 6000000).toISOString(), + isEdited: false, + canEdit: true, + canDelete: true, + }, + ], + }, +]; + export default function ChatCommentsPreviewPage() { const [highlightedIndex, setHighlightedIndex] = useState(0); const [selectedMember, setSelectedMember] = useState(null); @@ -133,31 +226,35 @@ export default function ChatCommentsPreviewPage() { )} + {/* Comment Thread Section */} +
+

Comment Thread

+

+ Two top-level comments with replies. Click Reply to open composer. Click the replies count to collapse/expand. +

+ +
+ {fakeThreadsData.map((thread) => ( + alert(`Reply to ${commentId}: ${content}`)} + onEditComment={(commentId) => alert(`Edit comment ${commentId}`)} + onDeleteComment={(commentId) => alert(`Delete comment ${commentId}`)} + /> + ))} +
+
+ {/* Comment Item Section */}
-

Comment Item

+

Comment Item (Standalone)

- Hover over comments to see the action menu. Mentions are highlighted. + Individual comment components. Hover to see action menu.

- {/* Comment with replies */} -
- alert(`Edit comment ${id}`)} - onDelete={(id) => alert(`Delete comment ${id}`)} - onReply={(id) => alert(`Reply to comment ${id}`)} - /> - alert(`Edit reply ${id}`)} - onDelete={(id) => alert(`Delete reply ${id}`)} - /> -
- - {/* Standalone comment */} alert(`Edit comment ${id}`)} diff --git a/surfsense_web/components/chat-comments/comment-thread/comment-thread.tsx b/surfsense_web/components/chat-comments/comment-thread/comment-thread.tsx new file mode 100644 index 000000000..89bd7bef1 --- /dev/null +++ b/surfsense_web/components/chat-comments/comment-thread/comment-thread.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { ChevronDown, ChevronRight, MessageSquare } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { CommentComposer } from "../comment-composer/comment-composer"; +import { CommentItem } from "../comment-item/comment-item"; +import type { CommentThreadProps } from "./types"; + +export function CommentThread({ + thread, + members, + membersLoading = false, + onCreateReply, + onEditComment, + onDeleteComment, + isSubmitting = false, +}: CommentThreadProps) { + const [isRepliesExpanded, setIsRepliesExpanded] = useState(true); + const [isReplyComposerOpen, setIsReplyComposerOpen] = useState(false); + + const parentComment = { + id: thread.id, + content: thread.content, + contentRendered: thread.contentRendered, + author: thread.author, + createdAt: thread.createdAt, + updatedAt: thread.updatedAt, + isEdited: thread.isEdited, + canEdit: thread.canEdit, + canDelete: thread.canDelete, + }; + + const handleReply = () => { + setIsReplyComposerOpen(true); + setIsRepliesExpanded(true); + }; + + const handleReplySubmit = (content: string) => { + onCreateReply(thread.id, content); + setIsReplyComposerOpen(false); + }; + + const handleReplyCancel = () => { + setIsReplyComposerOpen(false); + }; + + return ( +
+ onEditComment(id, "")} + onDelete={onDeleteComment} + /> + +
+ + {thread.replies.length > 1 && ( + + )} +
+ + {thread.replies.length > 0 && (thread.replies.length === 1 || isRepliesExpanded) && ( +
+ {thread.replies.map((reply) => ( + onEditComment(id, "")} + onDelete={onDeleteComment} + /> + ))} +
+ )} + + {isReplyComposerOpen && ( +
+ +
+ )} +
+ ); +} + diff --git a/surfsense_web/components/chat-comments/comment-thread/types.ts b/surfsense_web/components/chat-comments/comment-thread/types.ts new file mode 100644 index 000000000..b9bfc1599 --- /dev/null +++ b/surfsense_web/components/chat-comments/comment-thread/types.ts @@ -0,0 +1,28 @@ +import type { MemberOption } from "../member-mention-picker/types"; +import type { CommentData } from "../comment-item/types"; + +export interface CommentThreadData { + id: number; + messageId: number; + content: string; + contentRendered: string; + author: CommentData["author"]; + createdAt: string; + updatedAt: string; + isEdited: boolean; + canEdit: boolean; + canDelete: boolean; + replyCount: number; + replies: CommentData[]; +} + +export interface CommentThreadProps { + thread: CommentThreadData; + members: MemberOption[]; + membersLoading?: boolean; + onCreateReply: (commentId: number, content: string) => void; + onEditComment: (commentId: number, content: string) => void; + onDeleteComment: (commentId: number) => void; + isSubmitting?: boolean; +} +