From ef9eee19d692375cbe041a3ce5a6fd195ee07274 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 8 Nov 2025 09:15:38 +0800 Subject: [PATCH] fix: implement tag suggestions functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend changes: - Fix ListAllUserStats to calculate and return tag statistics - Previously only returned name and timestamps, missing TagCount - Now properly aggregates tags, pinned memos, and memo type stats Frontend changes: - Initialize user stats on app startup to populate tag data - Show all tags when typing just '#' (fix empty Fuse.js search) - Auto-refresh stats after creating/updating/deleting memos - Fix Checkbox component ref warning with forwardRef 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/router/api/v1/user_service_stats.go | 51 +++++++++++++++++-- .../MemoEditor/Editor/TagSuggestions.tsx | 3 ++ web/src/components/ui/checkbox.tsx | 9 +++- web/src/store/memo.ts | 7 +++ web/src/store/user.ts | 6 +-- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/server/router/api/v1/user_service_stats.go b/server/router/api/v1/user_service_stats.go index 9b5ff26f8..ceb4be949 100644 --- a/server/router/api/v1/user_service_stats.go +++ b/server/router/api/v1/user_service_stats.go @@ -49,14 +49,59 @@ func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUser userMemoStatMap := make(map[int32]*v1pb.UserStats) for _, memo := range memos { + // Initialize user stats if not exists + if _, exists := userMemoStatMap[memo.CreatorID]; !exists { + userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{ + Name: fmt.Sprintf("users/%d/stats", memo.CreatorID), + TagCount: make(map[string]int32), + MemoDisplayTimestamps: []*timestamppb.Timestamp{}, + PinnedMemos: []string{}, + MemoTypeStats: &v1pb.UserStats_MemoTypeStats{ + LinkCount: 0, + CodeCount: 0, + TodoCount: 0, + UndoCount: 0, + }, + } + } + + stats := userMemoStatMap[memo.CreatorID] + + // Add display timestamp displayTs := memo.CreatedTs if instanceMemoRelatedSetting.DisplayWithUpdateTime { displayTs = memo.UpdatedTs } - userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{ - Name: fmt.Sprintf("users/%d/stats", memo.CreatorID), + stats.MemoDisplayTimestamps = append(stats.MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) + + // Count memo stats + stats.TotalMemoCount++ + + // Count tags and other properties + if memo.Payload != nil { + for _, tag := range memo.Payload.Tags { + stats.TagCount[tag]++ + } + if memo.Payload.Property != nil { + if memo.Payload.Property.HasLink { + stats.MemoTypeStats.LinkCount++ + } + if memo.Payload.Property.HasCode { + stats.MemoTypeStats.CodeCount++ + } + if memo.Payload.Property.HasTaskList { + stats.MemoTypeStats.TodoCount++ + } + if memo.Payload.Property.HasIncompleteTasks { + stats.MemoTypeStats.UndoCount++ + } + } + } + + // Track pinned memos + if memo.Pinned { + stats.PinnedMemos = append(stats.PinnedMemos, fmt.Sprintf("users/%d/memos/%d", memo.CreatorID, memo.ID)) } - userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps = append(userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) } userMemoStats := []*v1pb.UserStats{} diff --git a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx index fb6e7631f..c016a4164 100644 --- a/web/src/components/MemoEditor/Editor/TagSuggestions.tsx +++ b/web/src/components/MemoEditor/Editor/TagSuggestions.tsx @@ -38,6 +38,9 @@ const TagSuggestions = observer(({ editorRef, editorActions }: Props) => { const suggestionsRef = useRef([]); suggestionsRef.current = (() => { const search = getCurrentWord()[0].slice(1).toLowerCase(); + if (search === "") { + return tags; // Show all tags when no search term + } const fuse = new Fuse(tags); return fuse.search(search).map((result) => result.item); })(); diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx index 7af4abea4..3040a639b 100644 --- a/web/src/components/ui/checkbox.tsx +++ b/web/src/components/ui/checkbox.tsx @@ -3,9 +3,13 @@ import { CheckIcon } from "lucide-react"; import * as React from "react"; import { cn } from "@/lib/utils"; -function Checkbox({ className, ...props }: React.ComponentProps) { +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { return ( ); -} +}); +Checkbox.displayName = "Checkbox"; export { Checkbox }; diff --git a/web/src/store/memo.ts b/web/src/store/memo.ts index 5bf4e182b..8f704281e 100644 --- a/web/src/store/memo.ts +++ b/web/src/store/memo.ts @@ -3,6 +3,7 @@ import { makeAutoObservable } from "mobx"; import { memoServiceClient } from "@/grpcweb"; import { CreateMemoRequest, ListMemosRequest, Memo } from "@/types/proto/api/v1/memo_service"; import { RequestDeduplicator, createRequestKey, StoreError } from "./store-utils"; +import userStore from "./user"; class LocalState { stateId: string = uniqueId(); @@ -112,6 +113,8 @@ const memoStore = (() => { stateId: uniqueId(), memoMapByName: memoMap, }); + // Refresh user stats to update tag counts + userStore.fetchUserStats().catch(console.error); return memo; }; @@ -142,6 +145,8 @@ const memoStore = (() => { stateId: uniqueId(), memoMapByName: confirmedMemoMap, }); + // Refresh user stats to update tag counts + userStore.fetchUserStats().catch(console.error); return memo; } catch (error) { // Rollback on error @@ -166,6 +171,8 @@ const memoStore = (() => { stateId: uniqueId(), memoMapByName: memoMap, }); + // Refresh user stats to update tag counts + userStore.fetchUserStats().catch(console.error); }; return { diff --git a/web/src/store/user.ts b/web/src/store/user.ts index f7cf248fd..086434668 100644 --- a/web/src/store/user.ts +++ b/web/src/store/user.ts @@ -343,10 +343,10 @@ export const initialUserStore = async () => { }, }); - // Step 3: Fetch user settings + // Step 3: Fetch user settings and stats // CRITICAL: This must happen after currentUser is set in step 2 - // The fetchUserSettings() method checks state.currentUser internally - await userStore.fetchUserSettings(); + // The fetchUserSettings() and fetchUserStats() methods check state.currentUser internally + await Promise.all([userStore.fetchUserSettings(), userStore.fetchUserStats()]); // Step 4: Apply user preferences to instance // CRITICAL: This must happen after fetchUserSettings() completes