|
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
|
|
growQscoreProjectionState,
|
|
|
|
|
missionArtifacts,
|
|
|
|
|
missionServiceSessions,
|
|
|
|
|
missionSuggestions,
|
|
|
|
|
qscoreSnapshots,
|
|
|
|
|
users,
|
|
|
|
|
type GrowHomeNotificationRow,
|
|
|
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
|
|
import { interviewService, resumeService, roleplayService } from "../services/product-service-clients.js";
|
|
|
|
|
import { refineHomeNotificationsWithAgent } from "./home-feed-agent.js";
|
|
|
|
|
import {
|
|
|
|
|
ALLOWED_NOTIFICATION_HREFS,
|
|
|
|
|
isAllowedNotificationHref,
|
|
|
|
|
MODULE_IDS,
|
|
|
|
|
MODULE_META,
|
|
|
|
|
type HomeFeedResponse,
|
|
|
|
|
@@ -35,7 +36,7 @@ const SERVICE_HREFS = {
|
|
|
|
|
interview: "/agents/interview",
|
|
|
|
|
roleplay: "/agents/roleplay",
|
|
|
|
|
qscore: "/agents/qscore",
|
|
|
|
|
mission: "/missions",
|
|
|
|
|
mission: "/missions/active",
|
|
|
|
|
social: "/social",
|
|
|
|
|
pathways: "/pathways",
|
|
|
|
|
rewards: "/rewards",
|
|
|
|
|
@@ -50,10 +51,13 @@ type HomeContext = {
|
|
|
|
|
qscore: { score: number; signalCount: number; summary: string | null; dimensions: Record<string, unknown> | null } | undefined;
|
|
|
|
|
qscoreSignals: Array<{ signalId: string; score: number; source: string | null; updatedAt: Date }>;
|
|
|
|
|
activeMissions: Array<{ instanceId: string; missionId: string; title: string; status: string; progressPercent: number; currentStageId: string | null; updatedAt: Date }>;
|
|
|
|
|
missionSuggestions: Array<{ id: string; missionInstanceId: string; missionId: string; stageId: string | null; role: string; type: string; title: string; body: string; reason: string | null; priority: number; urgency: string; ctaLabel: string; ctaHref: string; updatedAt: Date }>;
|
|
|
|
|
sessions: Array<{ serviceId: string; externalId: string; status: string; updatedAt: Date; metadata: Record<string, unknown> | null }>;
|
|
|
|
|
artifacts: Array<{ serviceId: string | null; type: string; title: string; status: string; summary: string | null; createdAt: Date }>;
|
|
|
|
|
events: Array<{ source: string; type: string; occurredAt: Date; payload: Record<string, unknown> }>;
|
|
|
|
|
serviceStates: Record<string, unknown>;
|
|
|
|
|
userProfile?: Record<string, unknown>;
|
|
|
|
|
preferences: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
|
|
@@ -64,13 +68,68 @@ function numberFrom(value: unknown): number | undefined {
|
|
|
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function arrayOfStrings(value: unknown): string[] {
|
|
|
|
|
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recordOf(value: unknown): Record<string, unknown> {
|
|
|
|
|
return isRecord(value) ? value : {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function profileFromPreferences(preferences: Record<string, unknown>) {
|
|
|
|
|
const onboarding = recordOf(preferences.onboarding);
|
|
|
|
|
const interview = recordOf(preferences.interview_preferences);
|
|
|
|
|
const resume = recordOf(preferences.resume_preferences);
|
|
|
|
|
const mission = recordOf(preferences.mission_preferences);
|
|
|
|
|
const targetRoles = arrayOfStrings(preferences.target_roles);
|
|
|
|
|
const targetCompanies = arrayOfStrings(preferences.target_companies);
|
|
|
|
|
const focusAreas = arrayOfStrings(interview.focus_areas);
|
|
|
|
|
return {
|
|
|
|
|
targetRole: targetRoles[0] ?? (typeof resume.target_title === "string" ? resume.target_title : "Senior Data Scientist"),
|
|
|
|
|
targetCompany: targetCompanies[0] ?? "target company",
|
|
|
|
|
industry: typeof preferences.industry === "string" ? preferences.industry : "AI / SaaS",
|
|
|
|
|
focusAreas,
|
|
|
|
|
weakSpots: arrayOfStrings(interview.weak_spots),
|
|
|
|
|
jobDescription: typeof interview.job_description === "string" ? interview.job_description : undefined,
|
|
|
|
|
activeGoal: typeof mission.active_goal === "string" ? mission.active_goal : typeof onboarding.goal === "string" ? onboarding.goal : undefined,
|
|
|
|
|
onboardingComplete: Boolean(onboarding.completed_at),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serviceHref(service: "resume" | "interview" | "roleplay" | "qscore", ctx: HomeContext, mission?: { instanceId?: string; missionId?: string; stageId?: string | null }) {
|
|
|
|
|
const profile = profileFromPreferences(ctx.preferences);
|
|
|
|
|
const params = new URLSearchParams({ source: "home" });
|
|
|
|
|
if (mission?.instanceId) params.set("missionInstanceId", mission.instanceId);
|
|
|
|
|
if (mission?.missionId) params.set("missionId", mission.missionId);
|
|
|
|
|
if (mission?.stageId) params.set("stageId", mission.stageId);
|
|
|
|
|
params.set("targetRole", profile.targetRole);
|
|
|
|
|
if (profile.targetCompany !== "target company") params.set("targetCompany", profile.targetCompany);
|
|
|
|
|
if (profile.industry) params.set("industry", profile.industry);
|
|
|
|
|
if (profile.focusAreas.length) params.set("focusAreas", profile.focusAreas.slice(0, 4).join(","));
|
|
|
|
|
if (profile.weakSpots.length) params.set("weakSpots", profile.weakSpots.slice(0, 3).join(","));
|
|
|
|
|
if (profile.jobDescription) params.set("jobDescription", profile.jobDescription.slice(0, 900));
|
|
|
|
|
if (service === "interview") return `/agents/interview/setup?${params.toString()}`;
|
|
|
|
|
if (service === "roleplay") return `/agents/roleplay/setup?${params.toString()}`;
|
|
|
|
|
if (service === "resume") return `/agents/resume?${params.toString()}`;
|
|
|
|
|
return `/agents/qscore?${params.toString()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sourceFromSuggestionRole(role: string): HomeSource {
|
|
|
|
|
const value = role.toLowerCase();
|
|
|
|
|
if (value.includes("resume")) return "resume";
|
|
|
|
|
if (value.includes("roleplay")) return "roleplay";
|
|
|
|
|
if (value.includes("interview")) return "interview";
|
|
|
|
|
if (value.includes("q")) return "qscore";
|
|
|
|
|
return "mission";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sanitizeUrgency(value: string): HomeUrgency {
|
|
|
|
|
if (value === "now" || value === "today" || value === "soon" || value === "calm") return value;
|
|
|
|
|
return "calm";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sanitizeHref(href: string | undefined, fallback: string) {
|
|
|
|
|
if (href && ALLOWED_NOTIFICATION_HREFS.has(href)) return href;
|
|
|
|
|
if (href && isAllowedNotificationHref(href)) return href;
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -97,7 +156,9 @@ function hasAnyRealActivity(ctx: HomeContext) {
|
|
|
|
|
ctx.activeMissions.length ||
|
|
|
|
|
ctx.sessions.length ||
|
|
|
|
|
ctx.artifacts.length ||
|
|
|
|
|
ctx.events.length,
|
|
|
|
|
ctx.events.length ||
|
|
|
|
|
ctx.missionSuggestions.length ||
|
|
|
|
|
profileFromPreferences(ctx.preferences).onboardingComplete,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -119,6 +180,7 @@ function buildDayOneSeeds(): SeedNotification[] {
|
|
|
|
|
|
|
|
|
|
function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
const seeds = buildDayOneSeeds().filter((seed) => seed.moduleId === "pathways" || seed.moduleId === "rewards");
|
|
|
|
|
const profile = profileFromPreferences(ctx.preferences);
|
|
|
|
|
const qscore = ctx.qscore?.score ?? Math.round(ctx.qscoreSignals.reduce((sum, s) => sum + s.score, 0) / Math.max(ctx.qscoreSignals.length, 1));
|
|
|
|
|
const ats = latestScore(ctx.qscoreSignals, "resume.ats_compatibility");
|
|
|
|
|
const interviewOverall = latestScore(ctx.qscoreSignals, "interview.overall_score");
|
|
|
|
|
@@ -130,14 +192,55 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
const roleplayReview = serviceEvent(ctx, "roleplay.", "review");
|
|
|
|
|
const resumeAnalysis = serviceEvent(ctx, "resume.", "analysis");
|
|
|
|
|
|
|
|
|
|
for (const suggestion of ctx.missionSuggestions.slice(0, 5)) {
|
|
|
|
|
const mission = ctx.activeMissions.find((item) => item.instanceId === suggestion.missionInstanceId);
|
|
|
|
|
const source = sourceFromSuggestionRole(suggestion.role);
|
|
|
|
|
const href = sanitizeHref(suggestion.ctaHref, mission ? `/missions/active?missionInstanceId=${encodeURIComponent(mission.instanceId)}` : SERVICE_HREFS.mission);
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "suggestions",
|
|
|
|
|
title: suggestion.title,
|
|
|
|
|
subtitle: suggestion.body,
|
|
|
|
|
tag: suggestion.ctaLabel.replace(/\s+/g, " ").slice(0, 14),
|
|
|
|
|
urgency: sanitizeUrgency(suggestion.urgency),
|
|
|
|
|
href,
|
|
|
|
|
source,
|
|
|
|
|
reason: suggestion.reason ?? undefined,
|
|
|
|
|
priority: Math.max(100, suggestion.priority + 10),
|
|
|
|
|
});
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: suggestion.role.toLowerCase().includes("resume") || suggestion.role.toLowerCase().includes("interview") || suggestion.role.toLowerCase().includes("roleplay") ? "productivity" : "missions",
|
|
|
|
|
title: `${suggestion.role}: ${suggestion.title}`,
|
|
|
|
|
subtitle: mission ? `${mission.title} · ${suggestion.body}` : suggestion.body,
|
|
|
|
|
tag: suggestion.urgency === "now" ? "Now" : suggestion.urgency === "today" ? "Today" : "Next",
|
|
|
|
|
urgency: sanitizeUrgency(suggestion.urgency),
|
|
|
|
|
href,
|
|
|
|
|
source,
|
|
|
|
|
reason: suggestion.reason ?? undefined,
|
|
|
|
|
priority: suggestion.priority,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (profile.onboardingComplete) {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "suggestions",
|
|
|
|
|
title: `${profile.targetRole} plan is calibrated`,
|
|
|
|
|
subtitle: profile.activeGoal ?? `Today's recommendations are tuned for ${profile.targetRole}${profile.targetCompany !== "target company" ? ` at ${profile.targetCompany}` : ""}.`,
|
|
|
|
|
tag: "Profile",
|
|
|
|
|
urgency: "today",
|
|
|
|
|
href: "/suggestions",
|
|
|
|
|
source: "system",
|
|
|
|
|
priority: 91,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctx.qscore || ctx.qscoreSignals.length) {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "suggestions",
|
|
|
|
|
title: qscore >= 80 ? "Protect your Q Score momentum" : "Raise your Q Score next",
|
|
|
|
|
subtitle: qscore >= 80 ? `Readiness is trending at ${qscore}. Keep one proof action moving.` : `Current estimate is ${qscore || 64}. Resume + mock practice are the fastest signals.`,
|
|
|
|
|
subtitle: qscore >= 80 ? `Readiness is trending at ${qscore}. Keep one proof action moving for ${profile.targetRole}.` : `Current estimate is ${qscore || 64}. Resume + mock practice are fastest for ${profile.targetRole}.`,
|
|
|
|
|
tag: "Q Score",
|
|
|
|
|
urgency: qscore >= 80 ? "today" : "now",
|
|
|
|
|
href: SERVICE_HREFS.qscore,
|
|
|
|
|
href: serviceHref("qscore", ctx),
|
|
|
|
|
source: "qscore",
|
|
|
|
|
priority: 95,
|
|
|
|
|
});
|
|
|
|
|
@@ -147,10 +250,10 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "suggestions",
|
|
|
|
|
title: ats >= 80 ? "ATS is demo-ready" : "Resume ATS needs one pass",
|
|
|
|
|
subtitle: ats >= 80 ? `ATS ${Math.round(ats)} — review role-specific keywords before applying.` : `ATS ${Math.round(ats)} — add JD keywords and measurable bullets.`,
|
|
|
|
|
subtitle: ats >= 80 ? `ATS ${Math.round(ats)} — review ${profile.targetRole} keywords before applying.` : `ATS ${Math.round(ats)} — add JD keywords and measurable data-science bullets.`,
|
|
|
|
|
tag: ats >= 80 ? "Ready" : "Fix",
|
|
|
|
|
urgency: ats >= 80 ? "today" : "now",
|
|
|
|
|
href: SERVICE_HREFS.resume,
|
|
|
|
|
href: serviceHref("resume", ctx),
|
|
|
|
|
source: "resume",
|
|
|
|
|
priority: 92,
|
|
|
|
|
});
|
|
|
|
|
@@ -163,7 +266,7 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
subtitle: mission.currentStageId ? `Current stage: ${mission.currentStageId.replaceAll("-", " ")}` : "Next action is ready on the mission dashboard.",
|
|
|
|
|
tag: mission.status === "paused" ? "Paused" : "Active",
|
|
|
|
|
urgency: mission.status === "paused" ? "soon" : "today",
|
|
|
|
|
href: SERVICE_HREFS.mission,
|
|
|
|
|
href: `/missions/active?missionInstanceId=${encodeURIComponent(mission.instanceId)}`,
|
|
|
|
|
source: "mission",
|
|
|
|
|
priority: 90 - mission.progressPercent,
|
|
|
|
|
});
|
|
|
|
|
@@ -186,7 +289,7 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "social",
|
|
|
|
|
title: "Turn proof into LinkedIn updates",
|
|
|
|
|
subtitle: ctx.artifacts.length ? `${ctx.artifacts.length} artifact${ctx.artifacts.length === 1 ? "" : "s"} can feed headline, featured, or post ideas.` : "Connect LinkedIn and use mission proof to improve your profile.",
|
|
|
|
|
subtitle: ctx.artifacts.length ? `${ctx.artifacts.length} artifact${ctx.artifacts.length === 1 ? "" : "s"} can feed headline, featured, or post ideas.` : `Connect LinkedIn and use ${profile.targetRole} proof to improve your profile.`,
|
|
|
|
|
tag: ctx.artifacts.length ? "Proof" : "Setup",
|
|
|
|
|
urgency: ctx.artifacts.length ? "today" : "soon",
|
|
|
|
|
href: SERVICE_HREFS.social,
|
|
|
|
|
@@ -198,51 +301,52 @@ function buildDynamicSeeds(ctx: HomeContext): SeedNotification[] {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "productivity",
|
|
|
|
|
title: ats !== undefined ? `Resume ATS ${Math.round(ats)}` : "Resume analysis is ready",
|
|
|
|
|
subtitle: ats !== undefined && ats >= 80 ? "Use this version for role-fit scan or final polish." : "Open Resume Builder for recommendations and bullet fixes.",
|
|
|
|
|
subtitle: ats !== undefined && ats >= 80 ? `Use this version for ${profile.targetRole} role-fit scan or final polish.` : "Open Resume Builder for recommendations and bullet fixes.",
|
|
|
|
|
tag: "Resume",
|
|
|
|
|
urgency: ats !== undefined && ats < 75 ? "now" : "today",
|
|
|
|
|
href: SERVICE_HREFS.resume,
|
|
|
|
|
href: serviceHref("resume", ctx),
|
|
|
|
|
source: "resume",
|
|
|
|
|
priority: 90,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firstMission = ctx.activeMissions[0];
|
|
|
|
|
if (interviewReview || interviewOverall !== undefined || interviewSession) {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "productivity",
|
|
|
|
|
title: interviewOverall !== undefined ? `Mock interview score ${Math.round(interviewOverall)}` : "Mock interview review is tracking",
|
|
|
|
|
subtitle: interviewReview?.type.includes("processing") ? "Review is still processing; check back from the interview page." : "Open interview practice for review, next drill, or a new session.",
|
|
|
|
|
subtitle: interviewReview?.type.includes("processing") ? "Review is still processing; check back from the interview page." : `Open ${profile.targetRole} interview practice for review, next drill, or a new session.`,
|
|
|
|
|
tag: interviewReview?.type.includes("processing") ? "Wait" : "Mock",
|
|
|
|
|
urgency: interviewReview?.type.includes("processing") ? "soon" : "today",
|
|
|
|
|
href: SERVICE_HREFS.interview,
|
|
|
|
|
href: serviceHref("interview", ctx, { instanceId: firstMission?.instanceId, missionId: firstMission?.missionId, stageId: firstMission?.currentStageId }),
|
|
|
|
|
source: "interview",
|
|
|
|
|
priority: 86,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
pushSeed(seeds, { moduleId: "productivity", title: "Schedule a mock interview", subtitle: "Generate a behavioral or role-related session from your target role.", tag: "Mock", urgency: "soon", href: SERVICE_HREFS.interview, source: "interview", priority: 72 });
|
|
|
|
|
pushSeed(seeds, { moduleId: "productivity", title: `Schedule a ${profile.targetRole} mock`, subtitle: "Generate a behavioral or role-related session from your target role.", tag: "Mock", urgency: "soon", href: serviceHref("interview", ctx, { instanceId: firstMission?.instanceId, missionId: firstMission?.missionId, stageId: firstMission?.currentStageId }), source: "interview", priority: 72 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (roleplayReview || roleplayComms !== undefined || roleplaySession) {
|
|
|
|
|
pushSeed(seeds, {
|
|
|
|
|
moduleId: "productivity",
|
|
|
|
|
title: roleplayComms !== undefined ? `Roleplay communication ${Math.round(roleplayComms)}` : "Roleplay scenario is ready",
|
|
|
|
|
subtitle: "Practice recruiter, manager, salary, or stakeholder conversations.",
|
|
|
|
|
subtitle: `Practice recruiter, manager, salary, or stakeholder conversations for ${profile.targetRole}.`,
|
|
|
|
|
tag: "Roleplay",
|
|
|
|
|
urgency: "soon",
|
|
|
|
|
href: SERVICE_HREFS.roleplay,
|
|
|
|
|
href: serviceHref("roleplay", ctx, { instanceId: firstMission?.instanceId, missionId: firstMission?.missionId, stageId: firstMission?.currentStageId }),
|
|
|
|
|
source: "roleplay",
|
|
|
|
|
priority: 78,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ctx.activeMissions.length) {
|
|
|
|
|
pushSeed(seeds, { moduleId: "missions", title: "Start Interview-to-Offer", subtitle: "Bundle resume fit, mock practice, and Q Score deltas into one journey.", tag: "Begin", urgency: "today", href: SERVICE_HREFS.mission, source: "mission", priority: 80 });
|
|
|
|
|
pushSeed(seeds, { moduleId: "missions", title: "Start Interview-to-Offer", subtitle: `Bundle resume fit, mock practice, and Q Score deltas for ${profile.targetRole}.`, tag: "Begin", urgency: "today", href: "/missions/available", source: "mission", priority: 80 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return seeds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function collectContext(userId: string): Promise<HomeContext> {
|
|
|
|
|
async function collectContext(userId: string, input: { userProfile?: Record<string, unknown>; preferences?: Record<string, unknown> } = {}): Promise<HomeContext> {
|
|
|
|
|
const [user] = await db.select({ id: users.id, email: users.email, displayName: users.displayName }).from(users).where(eq(users.id, userId)).limit(1);
|
|
|
|
|
const [qscore] = await db.select().from(growQscoreProjectionState).where(eq(growQscoreProjectionState.userId, userId)).limit(1);
|
|
|
|
|
const qscoreSignals = await db
|
|
|
|
|
@@ -257,6 +361,27 @@ async function collectContext(userId: string): Promise<HomeContext> {
|
|
|
|
|
.where(eq(growActiveMissions.userId, userId))
|
|
|
|
|
.orderBy(desc(growActiveMissions.updatedAt))
|
|
|
|
|
.limit(6);
|
|
|
|
|
const suggestions = await db
|
|
|
|
|
.select({
|
|
|
|
|
id: missionSuggestions.id,
|
|
|
|
|
missionInstanceId: missionSuggestions.missionInstanceId,
|
|
|
|
|
missionId: missionSuggestions.missionId,
|
|
|
|
|
stageId: missionSuggestions.stageId,
|
|
|
|
|
role: missionSuggestions.role,
|
|
|
|
|
type: missionSuggestions.type,
|
|
|
|
|
title: missionSuggestions.title,
|
|
|
|
|
body: missionSuggestions.body,
|
|
|
|
|
reason: missionSuggestions.reason,
|
|
|
|
|
priority: missionSuggestions.priority,
|
|
|
|
|
urgency: missionSuggestions.urgency,
|
|
|
|
|
ctaLabel: missionSuggestions.ctaLabel,
|
|
|
|
|
ctaHref: missionSuggestions.ctaHref,
|
|
|
|
|
updatedAt: missionSuggestions.updatedAt,
|
|
|
|
|
})
|
|
|
|
|
.from(missionSuggestions)
|
|
|
|
|
.where(and(eq(missionSuggestions.userId, userId), eq(missionSuggestions.status, "active")))
|
|
|
|
|
.orderBy(desc(missionSuggestions.priority), desc(missionSuggestions.updatedAt))
|
|
|
|
|
.limit(12);
|
|
|
|
|
const sessions = await db
|
|
|
|
|
.select({ serviceId: missionServiceSessions.serviceId, externalId: missionServiceSessions.externalId, status: missionServiceSessions.status, updatedAt: missionServiceSessions.updatedAt, metadata: missionServiceSessions.metadata })
|
|
|
|
|
.from(missionServiceSessions)
|
|
|
|
|
@@ -296,10 +421,13 @@ async function collectContext(userId: string): Promise<HomeContext> {
|
|
|
|
|
: undefined,
|
|
|
|
|
qscoreSignals,
|
|
|
|
|
activeMissions,
|
|
|
|
|
missionSuggestions: suggestions,
|
|
|
|
|
sessions: sessions.map((s) => ({ ...s, metadata: isRecord(s.metadata) ? s.metadata : null })),
|
|
|
|
|
artifacts,
|
|
|
|
|
events: events.map((e) => ({ ...e, payload: isRecord(e.payload) ? e.payload : {} })),
|
|
|
|
|
serviceStates: { resume: resumeState, interview: interviewState, roleplay: roleplayState },
|
|
|
|
|
userProfile: input.userProfile,
|
|
|
|
|
preferences: input.preferences ?? {},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -430,8 +558,8 @@ async function buildIdentity(ctx: HomeContext) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getHomeFeed(userId: string, opts: { refresh?: boolean } = {}): Promise<HomeFeedResponse> {
|
|
|
|
|
const ctx = await collectContext(userId);
|
|
|
|
|
export async function getHomeFeed(userId: string, opts: { refresh?: boolean; userProfile?: Record<string, unknown>; preferences?: Record<string, unknown> } = {}): Promise<HomeFeedResponse> {
|
|
|
|
|
const ctx = await collectContext(userId, { userProfile: opts.userProfile, preferences: opts.preferences });
|
|
|
|
|
const persisted = await readPersistedNotifications(userId);
|
|
|
|
|
const newest = persisted[0]?.createdAt?.getTime() ?? 0;
|
|
|
|
|
const hasDemo = persisted.some((row) => row.generatedBy === "demo");
|
|
|
|
|
@@ -459,6 +587,9 @@ export async function getHomeFeed(userId: string, opts: { refresh?: boolean } =
|
|
|
|
|
artifacts: ctx.artifacts,
|
|
|
|
|
recentEvents: ctx.events,
|
|
|
|
|
serviceStates: ctx.serviceStates,
|
|
|
|
|
missionSuggestions: ctx.missionSuggestions,
|
|
|
|
|
userProfile: ctx.userProfile,
|
|
|
|
|
preferences: ctx.preferences,
|
|
|
|
|
routeRules: SERVICE_HREFS,
|
|
|
|
|
},
|
|
|
|
|
seeds: deterministic,
|
|
|
|
|
|