From 72fd8749d3cea129db0259d3136c1a8062e06c95 Mon Sep 17 00:00:00 2001 From: ved015 Date: Fri, 8 May 2026 21:35:30 +0530 Subject: [PATCH] add user personalization section --- apps/web/components/dashboard-view.tsx | 380 ++++++++++++++++++++++++- 1 file changed, 365 insertions(+), 15 deletions(-) diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index a29faf88..2622ceac 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -7,12 +7,14 @@ import { $fetch } from "@lib/api" import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import { useQuery } from "@tanstack/react-query" import { useRouter } from "next/navigation" +import Image from "next/image" import { ArrowRight, ExternalLink, FileText, Lightbulb, Link2, + Plug, RotateCcw, SearchIcon, Terminal, @@ -293,6 +295,262 @@ const PLUGIN_STATIC = [ }, ] as const +// Plugin catalog for tool usage display - maps plugin IDs to display names and icons +const PLUGIN_DISPLAY_CATALOG: Record< + string, + { name: string; icon: string; type: "Plugin" } +> = { + claude_code: { + name: "Claude Code", + icon: "/images/plugins/claude-code.svg", + type: "Plugin", + }, + opencode: { + name: "OpenCode", + icon: "/images/plugins/opencode.svg", + type: "Plugin", + }, + openclaw: { + name: "OpenClaw", + icon: "/images/plugins/openclaw.svg", + type: "Plugin", + }, + hermes: { + name: "Hermes", + icon: "/images/plugins/hermes.svg", + type: "Plugin", + }, +} + +// Types for tool usage +interface ToolUsageItem { + id: string + name: string + type: "Plugin" | "MCP" + icon: string | null + lastUsedAt: Date | null + hasBeenUsed: boolean +} + +// Parse API keys to extract tool usage data +function parseToolUsage( + apiKeys: Array<{ + id: string + name: string + createdAt: string + lastRequest: string | null + metadata: string + }>, +): ToolUsageItem[] { + const toolMap = new Map() + + for (const key of apiKeys) { + let meta: Record = {} + try { + meta = key.metadata ? JSON.parse(key.metadata) : {} + } catch { + continue + } + + const smType = meta.sm_type as string | undefined + const smClient = meta.sm_client as string | undefined + const smSource = meta.sm_source as string | undefined + const smKind = meta.sm_kind as string | undefined + + // Plugin keys + if (smType === "plugin_auth" && smClient) { + const catalog = PLUGIN_DISPLAY_CATALOG[smClient] + const existingItem = toolMap.get(`plugin_${smClient}`) + const lastUsed = key.lastRequest + ? new Date(key.lastRequest) + : key.createdAt + ? new Date(key.createdAt) + : null + const existingLastUsed = existingItem?.lastUsedAt + + // Keep the most recent usage + if ( + !existingItem || + (lastUsed && + (!existingLastUsed || + lastUsed.getTime() > existingLastUsed.getTime())) + ) { + toolMap.set(`plugin_${smClient}`, { + id: `plugin_${smClient}`, + name: catalog?.name ?? smClient, + type: "Plugin", + icon: catalog?.icon ?? null, + lastUsedAt: lastUsed, + hasBeenUsed: !!key.lastRequest, + }) + } + } + + // MCP keys + if (smSource === "mcp" || smKind === "mcp_oauth_exchange") { + const existingItem = toolMap.get("mcp") + const lastUsed = key.lastRequest + ? new Date(key.lastRequest) + : key.createdAt + ? new Date(key.createdAt) + : null + const existingLastUsed = existingItem?.lastUsedAt + + // Keep the most recent usage + if ( + !existingItem || + (lastUsed && + (!existingLastUsed || + lastUsed.getTime() > existingLastUsed.getTime())) + ) { + // Try to get MCP client name from metadata + const mcpClientName = meta.sm_internal_mcp_client_name as + | string + | undefined + toolMap.set("mcp", { + id: "mcp", + name: mcpClientName || "Supermemory MCP", + type: "MCP", + icon: null, + lastUsedAt: lastUsed, + hasBeenUsed: !!key.lastRequest, + }) + } + } + } + + // Sort by lastUsedAt (most recent first), then by hasBeenUsed + return Array.from(toolMap.values()).sort((a, b) => { + // Items that have been used come first + if (a.hasBeenUsed !== b.hasBeenUsed) { + return a.hasBeenUsed ? -1 : 1 + } + // Then sort by recency + if (!a.lastUsedAt && !b.lastUsedAt) return 0 + if (!a.lastUsedAt) return 1 + if (!b.lastUsedAt) return -1 + return b.lastUsedAt.getTime() - a.lastUsedAt.getTime() + }) +} + +// Format relative time for tool usage +function formatToolUsageTime(date: Date | null, hasBeenUsed: boolean): string { + if (!hasBeenUsed) return "Never used" + if (!date) return "Connected" + const diffMs = Date.now() - date.getTime() + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + const diffDays = Math.floor(diffHours / 24) + if (diffHours < 1) return "Just now" + if (diffHours < 24) return `${diffHours}h ago` + if (diffDays === 1) return "Yesterday" + if (diffDays < 7) return `${diffDays}d ago` + return date.toLocaleDateString() +} + +function RecentToolUsageCard({ + items, + onOpenPlugins, + onOpenIntegrations, +}: { + items: ToolUsageItem[] + onOpenPlugins: () => void + onOpenIntegrations: (integration?: IntegrationParamValue) => void +}) { + // Show at most 3 items + const displayItems = items.slice(0, 3) + + // Empty state - no tool activity + if (displayItems.length === 0) { + return ( +
+ +

+ No recent tool activity +

+ +
+ ) + } + + return ( +
+
    + {displayItems.map((item) => ( +
  • + +
  • + ))} +
+
+ ) +} + function RecommendedPluginsCard({ profession, setProfession, @@ -678,6 +936,35 @@ export function DashboardView({ enabled: !!user, }) + // Fetch API keys for tool usage tracking + const { data: apiKeysData } = useQuery({ + queryKey: ["api-keys-tool-usage"], + queryFn: async () => { + const API_URL = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + const res = await fetch(`${API_URL}/v3/auth/keys`, { + credentials: "include", + }) + if (!res.ok) return { keys: [] } + return (await res.json()) as { + keys: Array<{ + id: string + name: string + createdAt: string + lastRequest: string | null + metadata: string + }> + } + }, + staleTime: 5 * 60 * 1000, + enabled: !!user, + }) + + const toolUsageItems = useMemo( + () => parseToolUsage(apiKeysData?.keys ?? []), + [apiKeysData], + ) + const { copy: personalizedCopy, profession, @@ -839,7 +1126,7 @@ export function DashboardView({

- {/* Recently saved + Suggested for you */} + {/* Recently saved + Suggested for you + Pick up where you left off */} {recents.length > 0 ? ( <> - {/* Shared header row — both labels aligned */} + {/* Shared header row — all labels aligned */}

@@ -859,6 +1146,11 @@ export function DashboardView({ Suggested for you

+
+

+ Pick up where you left off +

+
{/* Content row */} @@ -900,24 +1192,82 @@ export function DashboardView({ onOpenIntegrations={onOpenIntegrations} /> + + {/* Pick up where you left off - desktop only */} +
+ +
+ + {/* Mobile/tablet: Show pick up where you left off below */} + {toolUsageItems.length > 0 && ( +
+

+ Pick up where you left off +

+ +
+ )} ) : ( - /* No recents yet — show suggestions full-width */ + /* No recents yet — show suggestions and tool usage */ <> -

- Suggested for you -

-
- +
+
+

+ Suggested for you +

+
+ {toolUsageItems.length > 0 && ( +
+

+ Pick up where you left off +

+
+ )}
+
+
+ +
+ {toolUsageItems.length > 0 && ( +
+ +
+ )} +
+ {/* Mobile: Show tool usage below */} + {toolUsageItems.length > 0 && ( +
+

+ Pick up where you left off +

+ +
+ )} )}