From 90ff97e1898e1149a30dc143e8346caf26568cdd Mon Sep 17 00:00:00 2001 From: Skyler Date: Mon, 27 Apr 2026 13:11:44 -0700 Subject: [PATCH 1/4] Polish sidebar list, credit usage, and theme UI - Refine chat history rows, project layout, and ellipsis/pin alignment - Simplify CreditUsage to rich card; hover for token details - Sidebar nav spacing; theme, logos, and wordmark tweaks Made-with: Cursor Polish sidebar history scroll, fades, and credit usage card Made-with: Cursor --- frontend/public/maple-logo-dark.svg | 2 + frontend/public/maple-logo.svg | 2 + frontend/src/chat.css | 30 +- frontend/src/components/AccountMenu.tsx | 49 +- frontend/src/components/ChatHistoryList.tsx | 452 ++++++++++-------- .../components/ConversationProjectDialog.tsx | 6 +- .../components/ConversationProjectPicker.tsx | 2 +- frontend/src/components/CreditUsage.tsx | 250 ++++------ frontend/src/components/MapleWordmark.tsx | 12 +- frontend/src/components/ProjectDetailView.tsx | 32 +- frontend/src/components/Sidebar.tsx | 62 ++- frontend/src/components/TopNav.tsx | 14 +- frontend/src/components/UnifiedChat.tsx | 8 +- frontend/src/components/markdown.tsx | 6 +- frontend/src/contexts/ThemeContext.tsx | 34 +- frontend/src/index.css | 51 +- frontend/src/routes/_auth.chat.$chatId.tsx | 6 +- frontend/src/utils/mockSidebarChats.ts | 66 +++ frontend/src/utils/paginatedLists.ts | 18 +- frontend/src/vite-env.d.ts | 2 + frontend/tailwind.config.js | 1 + 21 files changed, 616 insertions(+), 489 deletions(-) create mode 100644 frontend/src/utils/mockSidebarChats.ts diff --git a/frontend/public/maple-logo-dark.svg b/frontend/public/maple-logo-dark.svg index 7fe0aba8..bb539706 100644 --- a/frontend/public/maple-logo-dark.svg +++ b/frontend/public/maple-logo-dark.svg @@ -1,9 +1,11 @@ + + diff --git a/frontend/public/maple-logo.svg b/frontend/public/maple-logo.svg index cb7c342c..03a690dd 100644 --- a/frontend/public/maple-logo.svg +++ b/frontend/public/maple-logo.svg @@ -1,7 +1,9 @@ + + diff --git a/frontend/src/chat.css b/frontend/src/chat.css index 551d1304..7c99fbbd 100644 --- a/frontend/src/chat.css +++ b/frontend/src/chat.css @@ -4,8 +4,9 @@ margin: 0; color: var(--color-fg-default); background-color: var(--color-canvas-default); - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; word-wrap: break-word; } @@ -341,8 +342,9 @@ .markdown-body p { margin-top: 0; margin-bottom: 10px; - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; } .markdown-body blockquote { @@ -357,8 +359,9 @@ margin-top: 0; margin-bottom: 0; padding-left: 2em; - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; } .markdown-body ul { @@ -608,13 +611,15 @@ .markdown-body li > p { margin-top: 16px; - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; } .markdown-body li { - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; } .markdown-body li + li { @@ -651,8 +656,9 @@ word-wrap: break-word; overflow-wrap: break-word; white-space: normal; - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.65; + letter-spacing: 0.1px; } .markdown-body table tr { diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index cb801acd..1368c09e 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -29,7 +29,7 @@ import { useNavigate, useRouter } from "@tanstack/react-router"; import { Dialog, DialogTrigger } from "./ui/dialog"; import { AccountDialog } from "./AccountDialog"; import { CreditUsage } from "./CreditUsage"; -import { Badge } from "./ui/badge"; +import { Badge } from "@/components/ui/badge"; import { AlertDialog, @@ -112,6 +112,7 @@ export function AccountMenu() { const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false); const [showAboutMenu, setShowAboutMenu] = useState(false); const [portalError, setPortalError] = useState(null); + const useMockCreditPreview = import.meta.env.DEV && !billingStatus; const hasStripeAccount = billingStatus?.stripe_customer_id !== null; const productName = billingStatus?.product_name || ""; @@ -291,23 +292,8 @@ export function AccountMenu() { !open && setShowAboutMenu(false)}> -
-
-
- - - {billingStatus ? `${billingStatus.product_name} Plan` : "Loading..."} - - -
-
- -
-
-
+ {useMockCreditPreview ? ( +
-
+ ) : ( +
+
+ + + +
+
+ +
+
+ )} {/* align=start: panel aligns to sidebar content edge; center was relative to the small icon */} listAllConversationProjects(opensecret), + queryFn: () => listAllConversationProjects(), enabled: !!userId }); @@ -581,6 +595,19 @@ export function ChatHistoryList({ const normalizedQuery = searchQuery.trim().toLowerCase(); + const mockSidebarChatCount = getMockSidebarChatCount(); + const mockSidebarConversations = useMemo( + () => buildMockSidebarConversations(mockSidebarChatCount), + [mockSidebarChatCount] + ); + + const displayConversations = useMemo(() => { + if (mockSidebarChatCount <= 0) return conversations; + const realIds = new Set(conversations.map((c) => c.id)); + const mocks = mockSidebarConversations.filter((m) => !realIds.has(m.id)); + return [...mocks, ...conversations]; + }, [conversations, mockSidebarChatCount, mockSidebarConversations]); + const filteredProjects = useMemo(() => { if (!normalizedQuery) return conversationProjects; @@ -622,7 +649,7 @@ export function ChatHistoryList({ }, [getConversationTitle, normalizedQuery, pinnedConversations]); const filteredRecentConversations = useMemo(() => { - const filtered = conversations.filter( + const filtered = displayConversations.filter( (conversation) => !conversation.pinned && !conversation.project_id ); @@ -631,7 +658,7 @@ export function ChatHistoryList({ return filtered.filter((conversation) => getConversationTitle(conversation).toLowerCase().includes(normalizedQuery) ); - }, [conversations, getConversationTitle, normalizedQuery]); + }, [displayConversations, getConversationTitle, normalizedQuery]); // Filter archived chats based on search query const filteredArchivedChats = useMemo(() => { @@ -711,6 +738,7 @@ export function ChatHistoryList({ // Toggle selection of a single chat const toggleSelection = useCallback( (chatId: string) => { + if (isMockSidebarChatId(chatId)) return; const newSelection = new Set(selectedIds); if (newSelection.has(chatId)) { newSelection.delete(chatId); @@ -795,6 +823,7 @@ export function ChatHistoryList({ // Long press handlers for mobile selection mode activation const handleLongPressStart = useCallback( (chatId: string) => { + if (isMockSidebarChatId(chatId)) return; if (isSelectionMode) return; // Already in selection mode longPressTimerRef.current = setTimeout(() => { @@ -954,27 +983,27 @@ export function ChatHistoryList({ const handleCreateProject = useCallback( async (name: string) => { - const project = await opensecret.createConversationProject({ name }); + const project = await createConversationProject({ name }); await invalidateConversationData(); await handleViewProject(project.id); }, - [handleViewProject, invalidateConversationData, opensecret] + [handleViewProject, invalidateConversationData] ); const handleRenameProject = useCallback( async (name: string) => { if (!selectedProject) return; - await opensecret.updateConversationProject(selectedProject.id, { name }); + await updateConversationProject(selectedProject.id, { name }); await invalidateConversationData(); }, - [invalidateConversationData, opensecret, selectedProject] + [invalidateConversationData, selectedProject] ); const handleDeleteProject = useCallback(async () => { if (!selectedProject) return; try { - await opensecret.deleteConversationProject(selectedProject.id); + await deleteConversationProject(selectedProject.id); await invalidateConversationData(); if (expandedProjectId === selectedProject.id) { @@ -997,7 +1026,6 @@ export function ChatHistoryList({ }, [ expandedProjectId, invalidateConversationData, - opensecret, selectedProject, selectedProjectId, setSelectedProjectId @@ -1093,6 +1121,8 @@ export function ChatHistoryList({ // Handle conversation selection const handleSelectConversation = useCallback( async (conversation: Conversation) => { + if (isMockSidebarChatId(conversation.id)) return; + setSelectedProjectId(conversation.project_id ?? null); if (window.location.pathname !== "/") { @@ -1150,25 +1180,22 @@ export function ChatHistoryList({ ); } - const renderConversationRow = (conversation: Conversation, options?: { compact?: boolean }) => { + const renderConversationRow = (conversation: Conversation) => { const title = getConversationTitle(conversation); + const isMockRow = isMockSidebarChatId(conversation.id); const isActive = conversation.id === currentChatId; const isSelected = selectedIds.has(conversation.id); - const fadeClass = isMobile - ? SIDEBAR_TITLE_FADE_MOBILE - : isActive - ? SIDEBAR_TITLE_FADE_DESKTOP_ACTIVE - : SIDEBAR_TITLE_FADE_DESKTOP; - const titlePaddingClass = isMobile - ? "pr-8" - : "pr-2 transition-[padding] duration-150 ease-out group-hover:pr-8"; + const titlePaddingClass = isMockRow ? "pr-2" : "pr-8"; + + const isBoldState = (isActive && !isSelectionMode) || (isSelectionMode && isSelected); + const rowTextClass = isBoldState + ? "font-bold text-foreground" + : "text-foreground/95 group-hover:text-foreground"; return (
event.preventDefault()} >
- {isSelectionMode ? ( -
- toggleSelection(conversation.id)} - onClick={(event) => event.stopPropagation()} - className="data-[state=checked]:bg-primary" - /> -
- ) : null}
-
+
{conversation.pinned ? ( - + ) : null} -
- {title} -
-
-
-
-
+
{title}
{new Date(conversation.last_activity_at * 1000).toLocaleDateString()}
- {!isSelectionMode ? ( + {isSelectionMode ? ( +
e.stopPropagation()} + > + toggleSelection(conversation.id)} + onClick={(event) => event.stopPropagation()} + className="data-[state=checked]:bg-primary" + /> +
+ ) : null} + {!isSelectionMode && !isMockRow ? ( <> - - - - - - onSelectionChange(new Set([conversation.id]))}> - - Select - - handleToggleConversationPin(conversation)}> - {conversation.pinned ? ( - - ) : ( - - )} - {conversation.pinned ? "Unpin Chat" : "Pin Chat"} - - - - - Move to Project - - - handleMoveConversationToProject(conversation, null)} +
+ ) : null}
@@ -1303,10 +1335,15 @@ export function ChatHistoryList({
-
+
Projects @@ -1319,7 +1356,7 @@ export function ChatHistoryList({ disabled className="flex w-full items-center gap-2 rounded-2xl py-1.5 pl-0 pr-1 text-left text-muted-foreground/50 cursor-not-allowed" > - + New project @@ -1331,9 +1368,9 @@ export function ChatHistoryList({ )} @@ -1343,62 +1380,64 @@ export function ChatHistoryList({ const isProjectSelected = selectedProjectId === project.id; return (
-
+
- - - - - - void handleViewProject(project.id)}> - - View Project - - - handleOpenRenameProjectDialog(project)}> - - Rename Project - - handleOpenDeleteProjectDialog(project)}> - - Delete Project - - - +
+
{isProjectExpanded && filteredExpandedProjectConversations.length > 0 ? ( -
+
{filteredExpandedProjectConversations.map((conversation) => - renderConversationRow(conversation, { compact: true }) + renderConversationRow(conversation) )}
) : null} @@ -1409,9 +1448,8 @@ export function ChatHistoryList({ {filteredPinnedConversations.length > 0 ? (
-
- - Pinned +
+ Pinned
{filteredPinnedConversations.map((conversation) => renderConversationRow(conversation))}
@@ -1445,9 +1483,9 @@ export function ChatHistoryList({ className="mb-2 flex w-full items-center gap-2 text-xs font-medium uppercase tracking-wider text-muted-foreground transition-colors hover:text-foreground" > {isArchivedExpanded ? ( - + ) : ( - + )} Archived ({filteredArchivedChats.length}) @@ -1456,68 +1494,66 @@ export function ChatHistoryList({
{filteredArchivedChats.map((chat) => { const isActive = chat.id === currentChatId; - const archivedFadeClass = isMobile - ? SIDEBAR_TITLE_FADE_MOBILE - : isActive - ? SIDEBAR_TITLE_FADE_DESKTOP_ACTIVE - : SIDEBAR_TITLE_FADE_DESKTOP; - const archivedTitlePaddingClass = isMobile - ? "pr-8" - : "pr-2 transition-[padding] duration-150 ease-out group-hover:pr-8"; + const archivedTitlePaddingClass = "pr-8"; return (
{ setSelectedProjectId(null); router.navigate({ to: "/chat/$chatId", params: { chatId: chat.id } }); }} - className={`relative min-w-0 flex-1 cursor-pointer rounded-2xl py-1 transition-all hover:text-primary ${ - isActive ? "text-primary" : "text-muted-foreground" + className={`relative ${ROW_CONTENT_Z} min-w-0 flex-1 cursor-pointer py-1 pl-0 pr-2 ${ + isActive + ? "font-bold text-foreground" + : "text-foreground/95 group-hover:text-foreground" }`} > -
-
+
+
{chat.title}
-
-
-
-
{new Date(chat.updated_at || chat.created_at).toLocaleDateString()}
- - - - - - handleOpenRenameDialogArchived(chat)}> - - Rename Chat - - handleOpenDeleteDialogArchived(chat)}> - - Delete Chat - - - +
+
); })} diff --git a/frontend/src/components/ConversationProjectDialog.tsx b/frontend/src/components/ConversationProjectDialog.tsx index 02ed6fd4..47ac74dd 100644 --- a/frontend/src/components/ConversationProjectDialog.tsx +++ b/frontend/src/components/ConversationProjectDialog.tsx @@ -69,7 +69,11 @@ export function ConversationProjectDialog({ onOpenChange(false); } catch (submitError) { console.error("Failed to save conversation project:", submitError); - setError("Failed to save project. Please try again."); + const errorMessage = + submitError instanceof Error && submitError.message + ? `Failed to save project: ${submitError.message}` + : "Failed to save project. Please try again."; + setError(errorMessage); } finally { setIsLoading(false); } diff --git a/frontend/src/components/ConversationProjectPicker.tsx b/frontend/src/components/ConversationProjectPicker.tsx index 325ad236..3bb22af1 100644 --- a/frontend/src/components/ConversationProjectPicker.tsx +++ b/frontend/src/components/ConversationProjectPicker.tsx @@ -27,7 +27,7 @@ export function ConversationProjectPicker({ const userId = os.auth.user?.user.id; const { data: projects = [] } = useQuery({ queryKey: ["conversationProjects", userId], - queryFn: () => listAllConversationProjects(os), + queryFn: () => listAllConversationProjects(), enabled: !!userId }); diff --git a/frontend/src/components/CreditUsage.tsx b/frontend/src/components/CreditUsage.tsx index b32ee2aa..579b362b 100644 --- a/frontend/src/components/CreditUsage.tsx +++ b/frontend/src/components/CreditUsage.tsx @@ -1,10 +1,5 @@ -import { useId } from "react"; - import { useLocalState } from "@/state/useLocalState"; import { formatResetDate } from "@/utils/dateFormat"; -import { cn } from "@/utils/utils"; - -type CreditUsageLayout = "bar" | "ring"; /** Dev-only opt-in: localStorage `maple_mock_credit_scenario` = demo | full | high | warn | ok | off */ type MockScenario = "demo" | "full" | "high" | "warn" | "ok"; @@ -52,115 +47,84 @@ function mockUsageResetIso(): string { return d.toISOString(); } -function usageTone(percentUsed: number): "danger" | "warn" | "ok" { - if (percentUsed >= 90) return "danger"; - if (percentUsed >= 75) return "warn"; - return "ok"; -} - -function toneColor(tone: ReturnType): string { - switch (tone) { - case "danger": - return "hsl(var(--maple-error))"; - case "warn": - return "hsl(var(--maple-warning))"; - default: - return "hsl(var(--maple-success))"; - } +function toPlanNameLabel(rawPlanName: string | undefined): string { + const cleaned = (rawPlanName ?? "Pro").trim(); + const hasPlanSuffix = /\bplan\b/i.test(cleaned); + return hasPlanSuffix ? cleaned : `${cleaned} Plan`; } -function toneTextClass(tone: ReturnType): string { - switch (tone) { - case "danger": - return "text-maple-error"; - case "warn": - return "text-maple-warning"; - default: - return "text-maple-success"; - } -} - -type RingMeterProps = { - percent: number; - size?: number; - stroke?: number; +type CreditUsageViewProps = { + planLabel: string; + percentRemaining: number; + roundedRemaining: number; + total: number; + tokensRemaining: number; + apiBalance?: number; + hasApiCredits: boolean; + resetFullLabel: string; + formatCredits: (n: number) => string; }; -function RingMeter({ percent, size = 32, stroke = 3.5 }: RingMeterProps) { - const gradId = `credit-ring-grad-${useId().replace(/:/g, "")}`; - const r = (size - stroke) / 2; - const c = 2 * Math.PI * r; - const clamped = Math.min(100, Math.max(0, percent)); - const offset = c - (clamped / 100) * c; - - return ( - - - - - - - - - - - ); -} - -function UsageRing({ - percentUsed, - roundedPercent, - size = 32, - stroke = 3.5 -}: { - percentUsed: number; - roundedPercent: number; - size?: number; - stroke?: number; -}) { +function CreditUsageView(p: CreditUsageViewProps) { return (
- +
+ + {p.planLabel} + + · + + {p.roundedRemaining}% left + + {p.resetFullLabel && ( + <> + · + + {p.resetFullLabel} + + + )} +
+ {/* Bar + token row: hover or focus the bar area to reveal exact token amounts */} +
+
+
+
+
+
+
+ + {p.formatCredits(p.tokensRemaining)} / {p.formatCredits(p.total)} tokens + {p.hasApiCredits && ( + + +{p.formatCredits(p.apiBalance ?? 0)} + + )} + +
+
+
+
); } -export function CreditUsage({ layout = "bar" }: { layout?: CreditUsageLayout }) { +export function CreditUsage({ mockScenario }: { mockScenario?: MockScenario }) { const { billingStatus } = useLocalState(); const totalLive = billingStatus?.total_tokens; @@ -168,13 +132,15 @@ export function CreditUsage({ layout = "bar" }: { layout?: CreditUsageLayout }) const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null && usedLive > 0; const mockFlag = readMockScenario(); - const useMock = !hasRealUsage && import.meta.env.DEV && mockFlag !== null && mockFlag !== "off"; + const forcedMock = import.meta.env.DEV ? mockScenario : undefined; + const scenario = forcedMock ?? (mockFlag !== null && mockFlag !== "off" ? mockFlag : null); + const useMock = !hasRealUsage && import.meta.env.DEV && scenario !== null; if (!hasRealUsage && !useMock) { return null; } - const mock = useMock ? mockPreset(mockFlag as MockScenario) : null; + const mock = useMock ? mockPreset(scenario as MockScenario) : null; const total = hasRealUsage ? totalLive! : mock!.total_tokens; const used = hasRealUsage ? usedLive! : mock!.used_tokens; const productName = hasRealUsage ? billingStatus?.product_name : "Pro"; @@ -182,9 +148,9 @@ export function CreditUsage({ layout = "bar" }: { layout?: CreditUsageLayout }) const apiBalance = hasRealUsage ? billingStatus!.api_credit_balance : mock?.api_credit_balance; const percentUsed = Math.min(100, Math.max(0, (used / total) * 100)); - const roundedPercent = Math.round(percentUsed); - const tone = usageTone(percentUsed); - const barColor = toneColor(tone); + const percentRemaining = Math.max(0, 100 - percentUsed); + const roundedRemaining = Math.round(percentRemaining); + const tokensRemaining = Math.max(0, total - used); const isMaxPlan = productName?.toLowerCase().includes("max") ?? false; if (isMaxPlan && percentUsed < 90) { @@ -193,58 +159,22 @@ export function CreditUsage({ layout = "bar" }: { layout?: CreditUsageLayout }) const hasApiCredits = apiBalance !== undefined && apiBalance > 0; - const formatCredits = (credits: number) => { - return new Intl.NumberFormat("en-US").format(credits); + const formatCredits = (credits: number) => new Intl.NumberFormat("en-US").format(credits); + + const planLabel = toPlanNameLabel(productName); + const resetFullLabel = formatResetDate(usageResetDate); + + const props: CreditUsageViewProps = { + planLabel, + percentRemaining, + roundedRemaining, + total, + tokensRemaining, + apiBalance, + hasApiCredits, + resetFullLabel, + formatCredits }; - const statusLabel = - percentUsed >= 100 ? "Limit reached" : percentUsed >= 90 ? "Almost full" : "Plan credits"; - - if (layout === "ring") { - return ( -
-
-
-
- - {statusLabel} - -
-

- {hasApiCredits && ( - <> - +{formatCredits(apiBalance ?? 0)} extra - · - - )} - {formatResetDate(usageResetDate)} -

-
- -
-
- ); - } - - return ( -
-
- {percentUsed >= 100 ? "Plan credits (full)" : "Plan Credits"} - {roundedPercent}% -
-
-
-
-
- {hasApiCredits && + {formatCredits(apiBalance ?? 0)} extra credits} - {formatResetDate(usageResetDate)} -
-
- ); + return ; } diff --git a/frontend/src/components/MapleWordmark.tsx b/frontend/src/components/MapleWordmark.tsx index 4aa716a1..0d4b1fd7 100644 --- a/frontend/src/components/MapleWordmark.tsx +++ b/frontend/src/components/MapleWordmark.tsx @@ -19,17 +19,19 @@ export function MapleWordmark({ }) { return ( - {WORD_PATHS.map((d, i) => ( - - ))} + + {WORD_PATHS.map((d, i) => ( + + ))} + ); } diff --git a/frontend/src/components/ProjectDetailView.tsx b/frontend/src/components/ProjectDetailView.tsx index db808eaa..1416b862 100644 --- a/frontend/src/components/ProjectDetailView.tsx +++ b/frontend/src/components/ProjectDetailView.tsx @@ -12,7 +12,13 @@ import { SquarePen, Trash2 } from "lucide-react"; -import { useOpenSecret, type Conversation } from "@opensecret/react"; +import { + useOpenSecret, + getConversationProject, + updateConversationProject, + deleteConversationProject, + type Conversation +} from "@opensecret/react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Sidebar, SidebarToggle } from "@/components/Sidebar"; import { useIsMobile } from "@/utils/utils"; @@ -177,13 +183,13 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { const { data: project, isPending: isProjectPending } = useQuery({ queryKey: ["conversationProject", projectId], - queryFn: () => os.getConversationProject(projectId), + queryFn: () => getConversationProject(projectId), enabled: !!projectId && hasAuthUser }); const { data: conversationProjects = [] } = useQuery({ queryKey: ["conversationProjects", userId], - queryFn: () => listAllConversationProjects(os), + queryFn: () => listAllConversationProjects(), enabled: !!userId }); @@ -318,28 +324,28 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { const handleSaveProjectInstructions = useCallback( async (instructions: string | null) => { - await os.updateConversationProject(projectId, { instructions }); + await updateConversationProject(projectId, { instructions }); await invalidateConversationData(); }, - [invalidateConversationData, os, projectId] + [invalidateConversationData, projectId] ); const handleRenameProject = useCallback( async (name: string) => { - await os.updateConversationProject(projectId, { name }); + await updateConversationProject(projectId, { name }); await invalidateConversationData(); }, - [invalidateConversationData, os, projectId] + [invalidateConversationData, projectId] ); const handleDeleteProject = useCallback(async () => { - await os.deleteConversationProject(projectId); + await deleteConversationProject(projectId); await invalidateConversationData(); setSelectedProjectId(null); window.history.replaceState({}, "", "/"); window.dispatchEvent(new CustomEvent("newchat", { detail: { projectId: null } })); window.dispatchEvent(new Event("projectselected")); - }, [invalidateConversationData, os, projectId, setSelectedProjectId]); + }, [invalidateConversationData, projectId, setSelectedProjectId]); const handleRenameConversation = useCallback( async (conversationId: string, newTitle: string) => { @@ -449,7 +455,7 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { return (
@@ -463,7 +469,7 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { return (
@@ -585,7 +591,7 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { return (
@@ -640,7 +646,7 @@ export function ProjectDetailView({ projectId }: ProjectDetailViewProps) { type="button" variant="ghost" size="icon" - className="absolute right-2 top-2 h-8 w-8" + className="absolute right-2 top-2 h-8 w-8 text-foreground/40 transition-colors hover:text-foreground group-hover:text-foreground focus-visible:text-foreground" onClick={(event) => { event.preventDefault(); event.stopPropagation(); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 004d6c7b..39e715b2 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -192,11 +192,11 @@ export function Sidebar({
-
+
{/* Header section */}
@@ -214,10 +214,11 @@ export function Sidebar({
diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index de8ed404..45db5f7c 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -41,9 +41,17 @@ export function TopNav() {
- - - Maple + + + Maple
diff --git a/frontend/src/components/UnifiedChat.tsx b/frontend/src/components/UnifiedChat.tsx index aa3e5e00..428bb825 100644 --- a/frontend/src/components/UnifiedChat.tsx +++ b/frontend/src/components/UnifiedChat.tsx @@ -917,7 +917,7 @@ function ToolCallRenderer({
- + {hasMore && (
- + {hasMore && (
- ) : ( -
-
- - - -
-
- -
- )} + + + +
{/* align=start: panel aligns to sidebar content edge; center was relative to the small icon */} listAllConversationProjects(), + queryFn: () => listAllConversationProjects(opensecret), enabled: !!userId }); @@ -595,19 +587,6 @@ export function ChatHistoryList({ const normalizedQuery = searchQuery.trim().toLowerCase(); - const mockSidebarChatCount = getMockSidebarChatCount(); - const mockSidebarConversations = useMemo( - () => buildMockSidebarConversations(mockSidebarChatCount), - [mockSidebarChatCount] - ); - - const displayConversations = useMemo(() => { - if (mockSidebarChatCount <= 0) return conversations; - const realIds = new Set(conversations.map((c) => c.id)); - const mocks = mockSidebarConversations.filter((m) => !realIds.has(m.id)); - return [...mocks, ...conversations]; - }, [conversations, mockSidebarChatCount, mockSidebarConversations]); - const filteredProjects = useMemo(() => { if (!normalizedQuery) return conversationProjects; @@ -649,7 +628,7 @@ export function ChatHistoryList({ }, [getConversationTitle, normalizedQuery, pinnedConversations]); const filteredRecentConversations = useMemo(() => { - const filtered = displayConversations.filter( + const filtered = conversations.filter( (conversation) => !conversation.pinned && !conversation.project_id ); @@ -658,7 +637,7 @@ export function ChatHistoryList({ return filtered.filter((conversation) => getConversationTitle(conversation).toLowerCase().includes(normalizedQuery) ); - }, [displayConversations, getConversationTitle, normalizedQuery]); + }, [conversations, getConversationTitle, normalizedQuery]); // Filter archived chats based on search query const filteredArchivedChats = useMemo(() => { @@ -738,7 +717,6 @@ export function ChatHistoryList({ // Toggle selection of a single chat const toggleSelection = useCallback( (chatId: string) => { - if (isMockSidebarChatId(chatId)) return; const newSelection = new Set(selectedIds); if (newSelection.has(chatId)) { newSelection.delete(chatId); @@ -823,7 +801,6 @@ export function ChatHistoryList({ // Long press handlers for mobile selection mode activation const handleLongPressStart = useCallback( (chatId: string) => { - if (isMockSidebarChatId(chatId)) return; if (isSelectionMode) return; // Already in selection mode longPressTimerRef.current = setTimeout(() => { @@ -983,27 +960,27 @@ export function ChatHistoryList({ const handleCreateProject = useCallback( async (name: string) => { - const project = await createConversationProject({ name }); + const project = await opensecret.createConversationProject({ name }); await invalidateConversationData(); await handleViewProject(project.id); }, - [handleViewProject, invalidateConversationData] + [handleViewProject, invalidateConversationData, opensecret] ); const handleRenameProject = useCallback( async (name: string) => { if (!selectedProject) return; - await updateConversationProject(selectedProject.id, { name }); + await opensecret.updateConversationProject(selectedProject.id, { name }); await invalidateConversationData(); }, - [invalidateConversationData, selectedProject] + [invalidateConversationData, opensecret, selectedProject] ); const handleDeleteProject = useCallback(async () => { if (!selectedProject) return; try { - await deleteConversationProject(selectedProject.id); + await opensecret.deleteConversationProject(selectedProject.id); await invalidateConversationData(); if (expandedProjectId === selectedProject.id) { @@ -1026,6 +1003,7 @@ export function ChatHistoryList({ }, [ expandedProjectId, invalidateConversationData, + opensecret, selectedProject, selectedProjectId, setSelectedProjectId @@ -1121,8 +1099,6 @@ export function ChatHistoryList({ // Handle conversation selection const handleSelectConversation = useCallback( async (conversation: Conversation) => { - if (isMockSidebarChatId(conversation.id)) return; - setSelectedProjectId(conversation.project_id ?? null); if (window.location.pathname !== "/") { @@ -1182,10 +1158,9 @@ export function ChatHistoryList({ const renderConversationRow = (conversation: Conversation) => { const title = getConversationTitle(conversation); - const isMockRow = isMockSidebarChatId(conversation.id); const isActive = conversation.id === currentChatId; const isSelected = selectedIds.has(conversation.id); - const titlePaddingClass = isMockRow ? "pr-2" : "pr-8"; + const titlePaddingClass = "pr-8"; const isBoldState = (isActive && !isSelectionMode) || (isSelectionMode && isSelected); const rowTextClass = isBoldState @@ -1213,10 +1188,9 @@ export function ChatHistoryList({ onTouchMove={handleLongPressMove} onTouchEnd={handleLongPressEnd} onTouchCancel={handleLongPressEnd} - title={isMockRow ? "Dev preview row (not a real chat)" : undefined} className={`relative ${ROW_CONTENT_Z} min-w-0 flex-1 py-1 pr-2 ${rowTextClass} ${ - isMockRow ? "cursor-default" : "cursor-pointer" - } ${isSelectionMode ? "pl-8" : "pl-0"}`} + isSelectionMode ? "pl-8" : "pl-0" + } cursor-pointer`} >
@@ -1247,7 +1221,7 @@ export function ChatHistoryList({ />
) : null} - {!isSelectionMode && !isMockRow ? ( + {!isSelectionMode ? ( <>