diff --git a/frontend/package.json b/frontend/package.json index 66154ac..70fe166 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,9 @@ "description": "AI Chat Memory - Capture and review your conversations", "author": "Vesti", "scripts": { + "predev": "pnpm -C ../packages/vesti-content-package build", "dev": "plasmo dev", + "dev:verbose": "pnpm exec plasmo dev --verbose", "prebuild": "pnpm -C ../packages/vesti-content-package build && pnpm -C ../packages/vesti-ui build && pnpm -C .. install --frozen-lockfile", "build": "plasmo build", "package": "node -e \"console.error('Local packaging is disabled. Use CI workflow extension-package or run pnpm -C frontend package:safe.'); process.exit(1)\"", diff --git a/packages/vesti-ui/src/tabs/library-tab.tsx b/packages/vesti-ui/src/tabs/library-tab.tsx index 11b2049..d9b0075 100644 --- a/packages/vesti-ui/src/tabs/library-tab.tsx +++ b/packages/vesti-ui/src/tabs/library-tab.tsx @@ -7,6 +7,7 @@ import { useRef, useMemo, useContext, + type ReactNode, type FocusEvent, type KeyboardEvent, } from "react"; @@ -96,12 +97,16 @@ type LibrarySplitContextValue = { exitSplit: () => void | Promise; }; -const LibrarySplitContext = createContext(null); +const LibrarySplitContext = createContext( + null, +); function useLibrarySplitContext(): LibrarySplitContextValue { const context = useContext(LibrarySplitContext); if (!context) { - throw new Error("LibrarySplitContext is only available inside LibraryTab split workspace."); + throw new Error( + "LibrarySplitContext is only available inside LibraryTab split workspace.", + ); } return context; } @@ -124,6 +129,38 @@ function SplitNavigationToggle() { ); } +function DetailSectionEyebrow({ children }: { children: string }) { + return ( +
+ {children} +
+ ); +} + +function DetailSectionCard({ + children, + className = "", +}: { + children: ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +function MetaChip({ children }: { children: string }) { + return ( + + {children} + + ); +} + type SplitNoteEditorPanelProps = { selectedConversation: Conversation; selectedNote: Note | null; @@ -153,12 +190,8 @@ function SplitNoteEditorPanel({ onOpenConversation, formatTimeAgo, }: SplitNoteEditorPanelProps) { - const { - pendingExcerpts, - consumePendingExcerpt, - noteSaveStatus, - exitSplit, - } = useLibrarySplitContext(); + const { pendingExcerpts, consumePendingExcerpt, noteSaveStatus, exitSplit } = + useLibrarySplitContext(); const splitTextareaRef = useRef(null); useEffect(() => { @@ -242,7 +275,7 @@ function SplitNoteEditorPanel({ placeholder="Extracted excerpts and your notes will appear here..." className="min-h-[520px] w-full resize-none overflow-hidden border-0 bg-transparent px-0 pb-14 text-[13px] leading-[1.75] text-text-primary outline-none placeholder:text-text-tertiary" style={{ - fontFamily: "\"JetBrains Mono\", \"SF Mono\", Menlo, monospace", + fontFamily: '"JetBrains Mono", "SF Mono", Menlo, monospace', }} /> - - - - -
-
- - Folders - - -
- {folderItems.length > 0 && ( -
- {folderItems.map((folder) => { - const isSelected = selectedTag === folder.name; - return ( -
{ - void flushPendingNoteSave(); - setViewMode("conversations"); - setListFilter("all"); - setSelectedTag(folder.name); - setSelectedConversationId(null); - setIsSplitNavigationOpen(false); - }} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - void flushPendingNoteSave(); - setViewMode("conversations"); - setListFilter("all"); - setSelectedTag(folder.name); - setSelectedConversationId(null); - setIsSplitNavigationOpen(false); - } - }} - className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-colors duration-200 my-1 rounded-lg group cursor-pointer relative ${ - isSelected && viewMode === "conversations" - ? "bg-bg-surface-card-active" - : "hover:bg-bg-surface-card" + {/* Left Column - Sidebar (200px) */} +
- )} -
- -
- -
- - - {/* Middle Column - Conversation/Note List (320px) */} -
- {viewMode === "conversations" ? ( - <> -
-
-
-

- {selectedTag - ? selectedTag - : listFilter === "starred" - ? "Starred" - : listFilter === "recent" - ? "Recent" - : "All Conversations"} -

+ All Conversations + - · {filteredConversations.length} conversations + {conversations.length} -
+ + +
-
-
- {filteredConversations.map((conv) => { - const isSelected = conv.id === selectedConversationId; - return ( -
- void selectConversation(conv.id, { - closeNavigation: isSplitActive, - }) - } - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - void selectConversation(conv.id, { - closeNavigation: isSplitActive, - }); - } - }} - className={`w-full text-left p-3 rounded-lg transition-all duration-200 relative group cursor-pointer ${ - isSelected - ? "bg-bg-surface-card-active shadow-[0_1px_3px_rgba(0,0,0,0.04)]" - : "bg-bg-surface-card hover:bg-bg-surface-card-hover hover:shadow-[0_1px_3px_rgba(0,0,0,0.04)]" - }`} +
+
+ + Folders + + - {openConversationMenuId === conv.id && ( -
event.stopPropagation()} - > - - -
+ {folderItems.length > 0 && ( +
+ {folderItems.map((folder) => { + const isSelected = selectedTag === folder.name; + return ( +
{ - void handleConversationChangeFolder(conv); - setOpenConversationMenuId(null); + void flushPendingNoteSave(); + setViewMode("conversations"); + setListFilter("all"); + setSelectedTag(folder.name); + setSelectedConversationId(null); + setIsSplitNavigationOpen(false); }} - className="w-full flex items-center gap-2 px-3 py-2 text-[13px] font-sans text-text-primary hover:bg-bg-surface-card transition-colors" - > - - Change folder - - -
- -
- )} -

- {conv.title} -

-
-
-

- {conv.snippet} -

-
- - {getPlatformLabel(conv.platform)} - - {normalizeTags(conv.tags).slice(0, 2).map((tag) => ( - - {tag} - - ))} - - {formatTimeAgo(conv.updated_at)} + + {folder.name} - {conv.has_note && ( - + +
+ {openFolderMenuName === folder.name && ( +
event.stopPropagation()} + > + +
+ +
)}
-
-
+ ); + })}
- ); - })} -
- - ) : ( - <> -
-
-
-

My Notes

- · {notes.length} notes -
+ )} +
+ +
-
+ + + {/* Middle Column - Conversation/Note List (320px) */} +
+ {viewMode === "conversations" ? ( + <> +
+
+
+

+ {selectedTag + ? selectedTag + : listFilter === "starred" + ? "Starred" + : listFilter === "recent" + ? "Recent" + : "All Conversations"} +

+ + · {filteredConversations.length} conversations + +
+
+
-
- {notesLoading && notes.length === 0 ? ( -
- Loading notes... -
- ) : ( - notes.map((note) => { - const isSelected = note.id === selectedNoteId; - const preview = note.content.replace(/[#*\[\]]/g, "").slice(0, 100); - return ( -
- - {formatTimeAgo(note.updated_at)} - - + {openConversationMenuId === conv.id && ( +
event.stopPropagation()} + > + + + + +
+ +
+ )} +

+ {conv.title} +

+
+
+

+ {conv.snippet} +

+
+ + {getPlatformLabel(conv.platform)} + + {normalizeTags(conv.tags) + .slice(0, 2) + .map((tag) => ( + + {tag} + + ))} + + {formatTimeAgo(conv.updated_at)} + + {conv.has_note && ( + + )} +
- -
- - + ); + })} +
+ + ) : ( + <> +
+
+
+

+ My Notes +

+ + · {notes.length} notes +
+
- ); - }) +
+ +
+ {notesLoading && notes.length === 0 ? ( +
+ Loading notes... +
+ ) : ( + notes.map((note) => { + const isSelected = note.id === selectedNoteId; + const preview = note.content + .replace(/[#*\[\]]/g, "") + .slice(0, 100); + return ( +
+ + {formatTimeAgo(note.updated_at)} + + +
+ + +
+
+ ); + }) + )} +
+ )}
- - )} -
-
+
-
- {/* Right Column - Reader/Editor (flex-1) */} - {viewMode === "conversations" && selectedConversation && ( -
-
- {/* Block A - Header */} -
-
-
-

- {selectedConversation.title} -

-
- - {getPlatformLabel(selectedConversation.platform)} - - · - {formatDate(messageDate)} - · - {messageCount} messages - {selectedConversation.url && ( - <> - · +
+ {/* Right Column - Reader/Editor (flex-1) */} + {viewMode === "conversations" && selectedConversation && ( +
+
+ {/* Block A - Header */} +
+
+
+

+ {selectedConversation.title} +

+
+ + {getPlatformLabel(selectedConversation.platform)} + + · + {formatDate(messageDate)} + · + {messageCount} messages + {selectedConversation.url && ( + <> + · + + + )} +
+
+ {isDesktopSplitAvailable && !isSplitActive ? ( - - )} + ) : null} +
+
+ {activeTags.map((tag) => ( + {tag} + ))} +
-
- {isDesktopSplitAvailable && !isSplitActive ? ( - - ) : null} -
-
- {activeTags.map((tag) => ( - - {tag} - - ))} -
-
- {/* Block B - Gardener Summary Card */} -
-
-
-
- {hasAnalysis ? ( - <> - - Analyzed - {activeTopicName && ( - <> - · - {activeTopicName} - - )} - {activeTags.length > 0 && ( + + {overviewSectionLabel} + + +
+
+ {hasAnalysis ? ( <> - · - - {activeTags.join(", ")} - + + Analyzed + {activeTopicName && ( + <> + · + + {activeTopicName} + + + )} + {activeTags.length > 0 && ( + <> + · + + {activeTags.join(", ")} + + + )} + ) : ( + + Not analyzed yet + )} - - ) : ( - Not analyzed yet - )} -
-
+
+
-
+
- {/* Summary 区域 */} -
- {summaryLoading ? ( -

- Loading summary... -

- ) : summaryData ? ( -
- -
-
- + > + + {summaryData.meta?.title || "Summary"} + + + +
+
+ +
+
-
-
- ) : ( -
-

- No summary yet. Generate one to see structured insights. -

- {storage.generateSummary && ( - <> - {summaryGenerating && pipelineStages.length > 0 && ( - - )} - - + > + Generate Summary + + + )} +
)}
- )} -
- {/* 操作栏 */} -
- {storage.generateSummary && summaryData && ( - - )} - - -
-
-
- - {/* Block C - Conversation Preview */} -
- {/* 默认预览条 - 折叠时显示 */} - {!isReaderConversationExpanded && ( -
-
-

- {messagesLoading - ? "Loading..." - : messages.length === 0 - ? "No messages captured yet." - : buildMessagePreviewText(messages[0], { maxChars: 120 })} -

- {messageCount > 1 && ( + > + + Regenerate + + )} - )} -
- {isConversationContentSettled && timestampFooter && ( - - )} -
- )} - - {/* 展开后的完整消息流 */} -
-
- {!isSplitActive && messageCount > 1 && ( -
- )} -
-
- {messagesLoading && ( -
- Loading messages... -
- )} - {!messagesLoading && messagesError && ( -
- Unable to load messages. + + + + Original Conversation + + + {/* 默认预览条 - 折叠时显示 */} + {!isReaderConversationExpanded && ( +
+
+
+ Preview +
+

+ {messagesLoading + ? "Loading original conversation..." + : originalConversationPreview} +

+ {canToggleConversationExpanded && ( +
+ +
+ )}
- )} - {messages.map((message, messageIndex) => { - const isUser = message.role === "user"; - const messageAnnotations = getAnnotationsForMessage(message.id); - const latestAnnotation = getLatestAnnotationForMessage(message.id); - const annotationCount = messageAnnotations.length; - const isAnnotationActive = activeAnnotationMessageId === message.id; + {isConversationContentSettled && timestampFooter && ( + + )} +
+ )} - return ( -
-
+
+ {canToggleConversationExpanded && ( +
+ +
+ + +
+ setMessageContentRef(message.id, node) + } + onMouseUp={() => { + const element = + messageContentRefs.current.get( + message.id, + ) ?? null; + window.setTimeout(() => { + updateReaderSelectionAction( + message, + messageIndex + 1, + element, + ); + }, 0); + }} + > + +
+
+
+
+ ); + })} +
+ {isAnnotationPanelOpen && + !isAnnotationDrawerOverlay && + activeAnnotationMessage && + annotationPopoverStyle && (
-
setMessageContentRef(message.id, node)} - onMouseUp={() => { - const element = - messageContentRefs.current.get(message.id) ?? null; - window.setTimeout(() => { - updateReaderSelectionAction( - message, - messageIndex + 1, - element - ); - }, 0); - }} - > - -
+ {renderAnnotationPanelContent()}
-
-
- ); - })} + )} + {readerSelectionAction ? ( + + ) : null} +
+
+ + {isConversationContentSettled && timestampFooter && ( + + )}
+ - {isAnnotationPanelOpen && - !isAnnotationDrawerOverlay && - activeAnnotationMessage && - annotationPopoverStyle && ( -
+ +
+
+ {renderAnnotationPanelContent()} +
+ + + )} + + {/* Related Notes */} + {selectedConversation && + relatedNotesForConversation.length > 0 && ( + <> + + Related Notes + + +
+ {relatedNotesForConversation.map((note) => ( + + ))} +
+
+ + )} + + + Related Conversations + + +
+ {relatedLoading && ( +
+ Finding related conversations...
)} - {readerSelectionAction ? ( - + ))} +
+
+
+
+ )} + + {isSplitActive && selectedConversation ? ( +
+ + conversations.find( + (conversation) => conversation.id === convId, + ), + ) + .filter( + (conversation): conversation is Conversation => + Boolean(conversation), + ) + : [] + } + onTitleChange={setNoteTitle} + onContentChange={setNoteContent} + onAppendExcerpt={appendExcerptToDraft} + onCreateConversationNote={handleCreateConversationNote} + onDeleteCurrentNote={handleDeleteCurrentSplitNote} + onOpenConversation={switchToConversation} + formatTimeAgo={formatTimeAgo} + /> +
+ ) : null} + + {!isSplitActive && viewMode === "notes" && selectedNote && ( +
+
+
+ {editingTitle ? ( + setNoteTitle(e.target.value)} + onBlur={() => setEditingTitle(false)} + onKeyDown={(e) => { + if (e.key === "Enter") setEditingTitle(false); + if (e.key === "Escape") { + setNoteTitle(selectedNote.title); + setEditingTitle(false); + } }} + className="w-full text-2xl font-serif font-normal text-text-primary bg-transparent border-b border-accent-primary outline-none" + /> + ) : ( +

setEditingTitle(true)} + className="text-2xl font-serif font-normal text-text-primary cursor-text hover:opacity-70 transition-opacity" > - - Extract - - ) : null} + {noteTitle || selectedNote.title} +

+ )}
-
- {isConversationContentSettled && timestampFooter && ( - - )} -
-
+
+ + {noteSaveStatus === "saving" + ? "Saving..." + : noteSaveStatus === "unsaved" + ? "Unsaved changes" + : `Updated ${formatTimeAgo(selectedNote.updated_at)}`} + +
- {isAnnotationPanelOpen && isAnnotationDrawerOverlay && activeAnnotationMessage && ( - <> -