Skip to content

Commit 404b273

Browse files
committed
feat: generate share images with detailed persona metrics
1 parent 02d170c commit 404b273

2 files changed

Lines changed: 232 additions & 69 deletions

File tree

apps/web/src/app/api/share/[format]/[userId]/route.tsx

Lines changed: 188 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function GET(
6262
// Fetch Profile
6363
const { data: profile, error } = await supabase
6464
.from("user_profiles")
65-
.select("*")
65+
.select("persona_id, persona_name, persona_tagline, persona_confidence, total_repos, total_commits, axes_json, narrative_json")
6666
.eq("user_id", userId)
6767
.single();
6868

@@ -85,11 +85,83 @@ export async function GET(
8585
const aura = getPersonaAura(profile.persona_id);
8686

8787
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:8108";
88+
const displayUrl = new URL(baseUrl).host;
89+
8890
const bgUrl = new URL(format === "story" ? aura.verticalBackground : aura.background, baseUrl).toString();
8991
const iconUrl = new URL(aura.icon, baseUrl).toString();
9092

91-
const width = format === "story" ? 1080 : format === "square" ? 1080 : 1200;
92-
const height = format === "story" ? 1920 : format === "square" ? 1080 : 630;
93+
// Dimensions (2x for Retina quality)
94+
const scale = 2;
95+
const width = (format === "story" ? 1080 : format === "square" ? 1080 : 1200) * scale;
96+
const height = (format === "story" ? 1920 : format === "square" ? 1080 : 630) * scale;
97+
98+
// -------------------------------------------------------------------------
99+
// Metric Computation
100+
// -------------------------------------------------------------------------
101+
const axes = profile.axes_json as any;
102+
const narrative = profile.narrative_json as any;
103+
104+
let metrics = {
105+
strongest: "N/A",
106+
style: "Balanced",
107+
rhythm: "Mixed",
108+
peak: "Varied"
109+
};
110+
111+
if (axes) {
112+
// 1. Strongest
113+
let maxScore = -1;
114+
let maxName = "";
115+
const AXIS_NAMES: Record<string, string> = {
116+
automation_heaviness: "Automation",
117+
guardrail_strength: "Guardrails",
118+
iteration_loop_intensity: "Loops",
119+
planning_signal: "Planning",
120+
surface_area_per_change: "Scope",
121+
shipping_rhythm: "Rhythm",
122+
};
123+
for (const [key, val] of Object.entries(axes)) {
124+
const axis = val as { score: number };
125+
if (axis.score > maxScore && AXIS_NAMES[key]) {
126+
maxScore = axis.score;
127+
maxName = AXIS_NAMES[key];
128+
}
129+
}
130+
const strongest = maxName ? `${maxName} ${maxScore}` : "N/A";
131+
132+
// 2. Style
133+
let style = "Mixed"; // Default
134+
const A = (axes.automation_heaviness?.score || 0);
135+
const B = (axes.guardrail_strength?.score || 0);
136+
const C = (axes.iteration_loop_intensity?.score || 0);
137+
const D = (axes.planning_signal?.score || 0);
138+
const E = (axes.surface_area_per_change?.score || 0);
139+
const F = (axes.shipping_rhythm?.score || 0);
140+
141+
if (A >= 70 && C >= 65) style = "Fast Builder";
142+
else if (B >= 70 && A >= 50) style = "Safe Shipper";
143+
else if (F >= 70 && C >= 60) style = "Rapid Cycler";
144+
else if (B >= 65 && C < 40) style = "Steady Hand";
145+
else if (A >= 60 && B < 40) style = "Bold Mover";
146+
else if (D >= 70) style = "Deep Planner";
147+
else if (E >= 70) style = "Wide Scoper";
148+
else style = "Balanced";
149+
150+
// 3. Rhythm
151+
let rhythm = "Mixed";
152+
const rScore = axes.shipping_rhythm?.score || 0;
153+
if (rScore >= 65) rhythm = "Bursty";
154+
else if (rScore < 35) rhythm = "Steady";
155+
156+
metrics = { ...metrics, strongest, style, rhythm };
157+
}
158+
159+
// Insight Text
160+
let insightText = "Your aggregated profile balances these styles across your repositories.";
161+
if (narrative) {
162+
if (narrative.insight) insightText = narrative.insight;
163+
else if (narrative.summary) insightText = narrative.summary;
164+
}
93165

94166
// Resolve fonts
95167
const [fontDataNormal, fontDataBold] = await Promise.all([fontNormalPromise, fontBoldPromise]);
@@ -98,6 +170,9 @@ export async function GET(
98170
if (fontDataNormal) fonts.push({ name: "Space Grotesk", data: fontDataNormal, style: "normal", weight: 400 });
99171
if (fontDataBold) fonts.push({ name: "Space Grotesk", data: fontDataBold, style: "normal", weight: 700 });
100172

173+
const paddingX = 60 * scale;
174+
const paddingY = 60 * scale;
175+
101176
return new ImageResponse(
102177
(
103178
<div
@@ -112,6 +187,7 @@ export async function GET(
112187
backgroundSize: "cover",
113188
backgroundPosition: "center",
114189
fontFamily: '"Space Grotesk", sans-serif',
190+
fontSize: 24 * scale,
115191
}}
116192
>
117193
{/* Overlay */}
@@ -132,48 +208,55 @@ export async function GET(
132208
justifyContent: "space-between",
133209
width: "100%",
134210
height: "100%",
135-
padding: format === "story" ? "120px 80px" : "80px",
211+
padding: `${paddingY}px ${paddingX}px`,
136212
}}
137213
>
138-
{/* Header */}
139-
<div style={{ display: "flex", flexDirection: "column" }}>
214+
{/* Header Area */}
215+
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
140216
<div
141217
style={{
142-
fontSize: format === "story" ? 32 : 24,
218+
display: "flex",
219+
fontSize: 18 * scale,
143220
fontWeight: 700,
144221
letterSpacing: "0.2em",
145222
textTransform: "uppercase",
146-
color: "rgba(255,255,255,0.6)",
147-
marginBottom: 20,
223+
color: "rgba(255,255,255,0.7)",
224+
marginBottom: 12 * scale,
148225
}}
149226
>
150227
My Unified VCP
151228
</div>
152229
<div
153230
style={{
154-
fontSize: format === "story" ? 80 : 64,
231+
display: "flex",
232+
fontSize: 64 * scale,
155233
fontWeight: 700,
156234
color: "white",
157-
lineHeight: 1.1,
158-
marginBottom: 20,
235+
marginBottom: 8 * scale,
236+
lineHeight: 1,
237+
textShadow: "0 2px 10px rgba(0,0,0,0.2)",
238+
maxWidth: "90%",
159239
}}
160240
>
161241
{personaName}
162242
</div>
163243
<div
164244
style={{
165-
fontSize: format === "story" ? 40 : 32,
245+
display: "flex",
246+
fontSize: 28 * scale,
166247
color: "rgba(255,255,255,0.9)",
167-
lineHeight: 1.4,
168-
marginBottom: 30,
248+
marginBottom: 12 * scale,
249+
fontWeight: 400,
250+
maxWidth: "90%",
169251
}}
170252
>
171253
{personaTagline}
172254
</div>
173255
{personaConfidence && (
174256
<div
175257
style={{
176-
fontSize: format === "story" ? 32 : 24,
258+
display: "flex",
259+
fontSize: 20 * scale,
177260
fontWeight: 500,
178261
color: "rgba(255,255,255,0.7)",
179262
}}
@@ -183,80 +266,124 @@ export async function GET(
183266
)}
184267
</div>
185268

186-
{/* Icon */}
269+
{/* Icon (Top Right) */}
187270
<img
188271
src={iconUrl}
189-
width={160}
190-
height={160}
272+
width={140 * scale}
273+
height={140 * scale}
191274
style={{
192275
position: "absolute",
193-
top: format === "story" ? 120 : 80,
194-
right: format === "story" ? 80 : 80,
195-
borderRadius: 80,
196-
border: "4px solid rgba(255,255,255,0.3)",
276+
top: paddingY,
277+
right: paddingX,
278+
borderRadius: 70 * scale,
279+
border: `${4 * scale}px solid rgba(255,255,255,0.2)`,
280+
boxShadow: `0 ${8 * scale}px ${30 * scale}px rgba(0,0,0,0.3)`,
197281
objectFit: "cover",
198282
}}
199283
/>
200284

201-
{/* Metrics */}
285+
{/* Insight Box */}
202286
<div
203287
style={{
204288
display: "flex",
205-
flexDirection: "row", // Explicit flex-direction
206-
gap: 40,
207-
marginTop: "auto",
289+
flexDirection: "row",
290+
width: "100%",
291+
marginTop: "auto", // Push to bottom for Story, or natural flow
292+
marginBottom: 20 * scale,
293+
padding: 24 * scale,
294+
backgroundColor: "rgba(255,255,255,0.1)",
295+
border: `${1 * scale}px solid rgba(255,255,255,0.2)`,
296+
borderRadius: 24 * scale,
297+
alignItems: "center",
208298
}}
209299
>
210-
<div style={{ display: "flex", flexDirection: "column" }}>
211-
<div
212-
style={{
213-
fontSize: 20,
214-
fontWeight: 600,
215-
letterSpacing: "0.1em",
216-
textTransform: "uppercase",
217-
color: "rgba(255,255,255,0.6)",
218-
}}
219-
>
220-
Repositories
221-
</div>
222-
<div style={{ fontSize: 56, fontWeight: 700, color: "white" }}>
223-
{totalRepos}
224-
</div>
300+
<div
301+
style={{
302+
display: "flex",
303+
fontSize: 22 * scale,
304+
color: "rgba(255,255,255,0.9)",
305+
lineHeight: 1.3,
306+
fontWeight: 400,
307+
maxHeight: 100 * scale,
308+
overflow: "hidden",
309+
textOverflow: "ellipsis",
310+
}}
311+
>
312+
{insightText}
225313
</div>
226-
<div style={{ display: "flex", flexDirection: "column" }}>
314+
</div>
315+
316+
{/* Grid Area */}
317+
<div
318+
style={{
319+
display: "flex",
320+
flexDirection: "row",
321+
flexWrap: "wrap",
322+
gap: 20 * scale,
323+
width: "100%",
324+
marginBottom: 20 * scale,
325+
}}
326+
>
327+
{[
328+
{ label: "Strongest", value: metrics.strongest },
329+
{ label: "Style", value: metrics.style },
330+
{ label: "Rhythm", value: metrics.rhythm },
331+
{ label: "Peak", value: metrics.peak },
332+
].map((item, i) => (
227333
<div
334+
key={i}
228335
style={{
229-
fontSize: 20,
230-
fontWeight: 600,
231-
letterSpacing: "0.1em",
232-
textTransform: "uppercase",
233-
color: "rgba(255,255,255,0.6)",
336+
display: "flex",
337+
flexDirection: "column",
338+
width: "48%", // Responsive 2-column grid
339+
flexGrow: 1,
340+
height: 120 * scale,
341+
padding: 20 * scale,
342+
backgroundColor: "rgba(255,255,255,0.1)",
343+
border: `${1 * scale}px solid rgba(255,255,255,0.2)`,
344+
borderRadius: 20 * scale,
345+
justifyContent: "center",
234346
}}
235347
>
236-
Commits
348+
<div
349+
style={{
350+
display: "flex",
351+
fontSize: 14 * scale,
352+
fontWeight: 700,
353+
letterSpacing: "0.15em",
354+
textTransform: "uppercase",
355+
color: "rgba(255,255,255,0.6)",
356+
marginBottom: 6 * scale,
357+
}}
358+
>
359+
{item.label}
360+
</div>
361+
<div style={{ display: "flex", fontSize: 32 * scale, fontWeight: 700, color: "white" }}>
362+
{item.value}
363+
</div>
237364
</div>
238-
<div style={{ fontSize: 56, fontWeight: 700, color: "white" }}>
239-
{totalCommits.toLocaleString()}
240-
</div>
241-
</div>
365+
))}
242366
</div>
243367

244368
{/* Footer */}
245369
<div
246370
style={{
247371
display: "flex",
248-
flexDirection: "row", // Explicit flex-direction
372+
flexDirection: "row",
249373
justifyContent: "space-between",
250-
borderTop: "2px solid rgba(255,255,255,0.2)",
251-
paddingTop: 40,
252-
marginTop: 60,
374+
width: "100%",
375+
maxWidth: "100%",
376+
borderTop: `${1 * scale}px solid rgba(255,255,255,0.15)`,
377+
paddingTop: 16 * scale,
378+
marginTop: 0,
379+
marginBottom: 0,
253380
}}
254381
>
255-
<div style={{ fontSize: 24, fontWeight: 500, color: "rgba(255,255,255,0.5)" }}>
256-
vibed.dev
382+
<div style={{ display: "flex", fontSize: 20 * scale, fontWeight: 500, color: "rgba(255,255,255,0.6)" }}>
383+
{displayUrl}
257384
</div>
258-
<div style={{ fontSize: 24, color: "rgba(255,255,255,0.5)" }}>
259-
#VCP
385+
<div style={{ display: "flex", fontSize: 20 * scale, color: "rgba(255,255,255,0.6)" }}>
386+
{totalRepos} repos • {totalCommits.toLocaleString()} commits
260387
</div>
261388
</div>
262389
</div>

0 commit comments

Comments
 (0)