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
+
+
+ Set up integrations →
+
+
+ )
+ }
+
+ return (
+
+
+ {displayItems.map((item) => (
+
+ {
+ if (item.type === "Plugin") {
+ onOpenPlugins()
+ } else {
+ // Open MCP settings
+ onOpenIntegrations("mcp" as IntegrationParamValue)
+ }
+ }}
+ className="group w-full flex items-center gap-2.5 rounded-lg px-2 py-2 hover:bg-surface-hover transition-colors cursor-pointer"
+ >
+ {/* Icon */}
+
+ {item.icon ? (
+
+ ) : item.type === "MCP" ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Name and type */}
+
+
+ {item.name}
+
+
+ {item.type}
+
+
+
+ {/* Last used */}
+
+ {formatToolUsageTime(item.lastUsedAt, item.hasBeenUsed)}
+
+
+
+ ))}
+
+
+ )
+}
+
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
+
+
+
+ )}
>
)}