Skip to content

Commit 156fc13

Browse files
devakoneclaude
andcommitted
fix: resolve lint error in FirstTimePublicProfileBanner with useSyncExternalStore
- Refactor FirstTimePublicProfileBanner to use useSyncExternalStore instead of useState/useEffect pattern that triggered cascading render lint error - Add llmKeySource display to UnifiedInsightSection showing narrative source (User key, Platform key, Sponsor key, or Deterministic fallback) - Pass llmKeySource prop through page.tsx to UnifiedInsightSection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 17cf930 commit 156fc13

3 files changed

Lines changed: 57 additions & 13 deletions

File tree

apps/web/src/app/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,7 @@ function AuthenticatedDashboard({
12601260
highlights={narrativeHighlights}
12611261
isLLMGenerated={hasLLMNarrative}
12621262
llmModel={stats.userProfile.llmModel}
1263+
llmKeySource={stats.userProfile.llmKeySource}
12631264
/>
12641265
) : null}
12651266

apps/web/src/components/public-profile/FirstTimePublicProfileBanner.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
3+
import { useState, useSyncExternalStore, useCallback } from "react";
44
import Link from "next/link";
55
import { X, Sparkles } from "lucide-react";
66

77
const STORAGE_KEY = "vcp_public_profile_intro_seen";
88

9+
// Subscribe to localStorage changes (for cross-tab sync if needed)
10+
function subscribe(callback: () => void) {
11+
window.addEventListener("storage", callback);
12+
return () => window.removeEventListener("storage", callback);
13+
}
14+
15+
function getSnapshot(): boolean {
16+
return localStorage.getItem(STORAGE_KEY) === "1";
17+
}
18+
19+
function getServerSnapshot(): boolean {
20+
return true; // On server, assume dismissed to avoid hydration mismatch
21+
}
22+
923
interface FirstTimePublicProfileBannerProps {
1024
/** Whether user's public profile is already enabled */
1125
profileEnabled: boolean;
@@ -22,22 +36,23 @@ export function FirstTimePublicProfileBanner({
2236
profileEnabled,
2337
hasUsername,
2438
}: FirstTimePublicProfileBannerProps) {
25-
const [dismissed, setDismissed] = useState(true); // Start true to avoid flash
26-
const [mounted, setMounted] = useState(false);
39+
// Use useSyncExternalStore to safely read from localStorage
40+
const dismissedFromStorage = useSyncExternalStore(
41+
subscribe,
42+
getSnapshot,
43+
getServerSnapshot
44+
);
2745

28-
useEffect(() => {
29-
setMounted(true);
30-
const seen = localStorage.getItem(STORAGE_KEY) === "1";
31-
setDismissed(seen);
32-
}, []);
46+
const [localDismissed, setLocalDismissed] = useState(false);
47+
const dismissed = dismissedFromStorage || localDismissed;
3348

34-
const dismiss = () => {
49+
const dismiss = useCallback(() => {
3550
localStorage.setItem(STORAGE_KEY, "1");
36-
setDismissed(true);
37-
};
51+
setLocalDismissed(true);
52+
}, []);
3853

39-
// Don't render during SSR or if dismissed or if profile already enabled
40-
if (!mounted || dismissed || profileEnabled) {
54+
// Don't render if dismissed or if profile already enabled
55+
if (dismissed || profileEnabled) {
4156
return null;
4257
}
4358

apps/web/src/components/vcp/unified/UnifiedInsightSection.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface UnifiedInsightSectionProps {
1818
isLLMGenerated?: boolean;
1919
/** LLM model used */
2020
llmModel?: string | null;
21+
/** Source of LLM key used for this narrative */
22+
llmKeySource?: string | null;
2123
/** Additional class names */
2224
className?: string;
2325
}
@@ -48,8 +50,30 @@ export function UnifiedInsightSection({
4850
highlights = [],
4951
isLLMGenerated = false,
5052
llmModel,
53+
llmKeySource,
5154
className,
5255
}: UnifiedInsightSectionProps) {
56+
const modelLabel = llmModel?.trim() || null;
57+
const providerLabel = modelLabel?.startsWith("gpt-")
58+
? "OpenAI"
59+
: modelLabel?.startsWith("claude-")
60+
? "Anthropic"
61+
: modelLabel?.startsWith("gemini-")
62+
? "Google"
63+
: null;
64+
const sourceLabel =
65+
llmKeySource === "user"
66+
? "User key"
67+
: llmKeySource === "platform"
68+
? "Platform key"
69+
: llmKeySource === "sponsor"
70+
? "Sponsor key"
71+
: "Deterministic fallback";
72+
const generatedWithLabel =
73+
isLLMGenerated && modelLabel
74+
? `${providerLabel ? `${providerLabel} ` : ""}${modelLabel}`
75+
: null;
76+
5377
return (
5478
<div className={cn("border-t border-black/5 px-8 py-6 sm:px-10", className)}>
5579
<div className="rounded-xl border-l-4 border-l-violet-500 bg-violet-50 px-5 py-4">
@@ -85,6 +109,10 @@ export function UnifiedInsightSection({
85109
))}
86110
</ul>
87111
) : null}
112+
<p className="mt-4 text-xs text-slate-500">
113+
Narrative source: {sourceLabel}
114+
{generatedWithLabel ? ` (${generatedWithLabel})` : ""}
115+
</p>
88116
</div>
89117
</div>
90118
);

0 commit comments

Comments
 (0)