fix: keep outreach funnel response rollout-compatible
This commit is contained in:
1 parent
1515b3e291
commit
5c57ed337b
6 files changed
+12
-55
No files matched your search
@@ -38,29 +38,7 @@ function formatWaitTime(timestamp: number, now: number): string {
|
|||||||
return `in ${Math.ceil(remaining / 86_400_000)}d`;
|
return `in ${Math.ceil(remaining / 86_400_000)}d`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PhysiqueTargeting = { enabled: boolean; minScore: number; maxScore: number };
|
function PreparedCandidateDetails({ candidate }: { candidate: OutreachCandidate }) {
|
||||||
|
|
||||||
function targetingLabel(current: PhysiqueTargeting, prepared: PhysiqueTargeting | null): string {
|
|
||||||
if (current.enabled) {
|
|
||||||
const currentLabel = `Current target ${current.minScore}–${current.maxScore}`;
|
|
||||||
if (!prepared?.enabled) return `${currentLabel} · Prepared with filter off`;
|
|
||||||
if (prepared.minScore !== current.minScore || prepared.maxScore !== current.maxScore) {
|
|
||||||
return `${currentLabel} · Prepared for ${prepared.minScore}–${prepared.maxScore}`;
|
|
||||||
}
|
|
||||||
return currentLabel;
|
|
||||||
}
|
|
||||||
return prepared?.enabled
|
|
||||||
? `Current filter off · Prepared for ${prepared.minScore}–${prepared.maxScore}`
|
|
||||||
: 'Current filter off';
|
|
||||||
}
|
|
||||||
|
|
||||||
function PreparedCandidateDetails({
|
|
||||||
candidate,
|
|
||||||
currentTargeting,
|
|
||||||
}: {
|
|
||||||
candidate: OutreachCandidate;
|
|
||||||
currentTargeting: PhysiqueTargeting;
|
|
||||||
}) {
|
|
||||||
const preparation = candidate.preparation;
|
const preparation = candidate.preparation;
|
||||||
if (!preparation) return null;
|
if (!preparation) return null;
|
||||||
const targeting = preparation.physiqueTargeting;
|
const targeting = preparation.physiqueTargeting;
|
||||||
@@ -83,7 +61,7 @@ function PreparedCandidateDetails({
|
|||||||
<div className="candidate-preparation-copy">
|
<div className="candidate-preparation-copy">
|
||||||
<div className="candidate-score-grid" aria-label="Preparation scoring">
|
<div className="candidate-score-grid" aria-label="Preparation scoring">
|
||||||
<div><span>Attractiveness</span><strong>{preparation.attractivenessScore === null ? 'Not scored' : `${preparation.attractivenessScore}/100`}</strong><small>Used in the opener prompt</small></div>
|
<div><span>Attractiveness</span><strong>{preparation.attractivenessScore === null ? 'Not scored' : `${preparation.attractivenessScore}/100`}</strong><small>Used in the opener prompt</small></div>
|
||||||
<div><span>Physique</span><strong>{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}</strong><small>{targetingLabel(currentTargeting, targeting)}</small></div>
|
<div><span>Physique</span><strong>{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}</strong><small>{targeting?.enabled ? `Current target ${targeting.minScore}–${targeting.maxScore}` : 'Current filter off'}</small></div>
|
||||||
<div><span>Detected gender</span><strong>{preparation.detectedGender}</strong><small>Image analysis</small></div>
|
<div><span>Detected gender</span><strong>{preparation.detectedGender}</strong><small>Image analysis</small></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="candidate-preparation-metadata">
|
<div className="candidate-preparation-metadata">
|
||||||
@@ -106,15 +84,7 @@ function PreparedCandidateDetails({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CandidateCard({
|
function CandidateCard({ candidate, now }: { candidate: OutreachCandidate; now: number }) {
|
||||||
candidate,
|
|
||||||
currentTargeting,
|
|
||||||
now,
|
|
||||||
}: {
|
|
||||||
candidate: OutreachCandidate;
|
|
||||||
currentTargeting: PhysiqueTargeting;
|
|
||||||
now: number;
|
|
||||||
}) {
|
|
||||||
const waitingUntil = candidate.availableAt > now;
|
const waitingUntil = candidate.availableAt > now;
|
||||||
const isKnownStatus = KNOWN_STATUSES.has(candidate.status);
|
const isKnownStatus = KNOWN_STATUSES.has(candidate.status);
|
||||||
return (
|
return (
|
||||||
@@ -144,7 +114,7 @@ function CandidateCard({
|
|||||||
</div>
|
</div>
|
||||||
{candidate.note ? <p className="candidate-note"><strong>Latest note</strong>{candidate.note}</p> : null}
|
{candidate.note ? <p className="candidate-note"><strong>Latest note</strong>{candidate.note}</p> : null}
|
||||||
{candidate.owner ? <AccountOwnerBadge owner={candidate.owner} /> : null}
|
{candidate.owner ? <AccountOwnerBadge owner={candidate.owner} /> : null}
|
||||||
<PreparedCandidateDetails candidate={candidate} currentTargeting={currentTargeting} />
|
<PreparedCandidateDetails candidate={candidate} />
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -230,14 +200,7 @@ export function OutreachPage() {
|
|||||||
<div className="empty-state panel"><strong>The active queue is clear.</strong><span>New potential posts will appear here as discovery finds them.</span></div>
|
<div className="empty-state panel"><strong>The active queue is clear.</strong><span>New potential posts will appear here as discovery finds them.</span></div>
|
||||||
) : (
|
) : (
|
||||||
<ol aria-label="Active outreach candidates" className="outreach-candidates">
|
<ol aria-label="Active outreach candidates" className="outreach-candidates">
|
||||||
{data.candidates.map((candidate) => (
|
{data.candidates.map((candidate) => <CandidateCard candidate={candidate} key={candidate.postId} now={now} />)}
|
||||||
<CandidateCard
|
|
||||||
candidate={candidate}
|
|
||||||
currentTargeting={data.physiqueTargeting}
|
|
||||||
key={candidate.postId}
|
|
||||||
now={now}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ol>
|
</ol>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ Navigation is declared in `app/navigation.ts`; route elements are composed in `a
|
|||||||
|
|
||||||
The Monitoring settings section owns the optional physique-targeting toggle and inclusive 1–10 range. It explains that the signal is subjective, applies only to image posts, and screens out unscorable images while enabled.
|
The Monitoring settings section owns the optional physique-targeting toggle and inclusive 1–10 range. It explains that the signal is subjective, applies only to image posts, and screens out unscorable images while enabled.
|
||||||
|
|
||||||
Prepared cards in the authenticated outreach funnel expose the exact opener, grounded image analysis, attractiveness and physique scores, detected gender, strategy/tone/model metadata, and original source image. Physique scoring shows both the currently configured target and, when different, the range snapshot used during preparation; it does not label a historical snapshot as though it were the live setting. The source image uses the shared `OriginPostCard` privacy behavior: blurred initially, first click reveals it, and a later click opens the full-screen preview. Candidates from older persisted snapshots degrade cleanly when newer optional metadata is absent.
|
Prepared cards in the authenticated outreach funnel expose the exact opener, grounded image analysis, attractiveness and physique scores, current target range, detected gender, strategy/tone/model metadata, and original source image. The live range is returned through the existing preparation field so cached dashboard bundles remain compatible during a rolling deployment. The durable queue still owns the historical preparation snapshot used for reconciliation. The source image uses the shared `OriginPostCard` privacy behavior: blurred initially, first click reveals it, and a later click opens the full-screen preview. Candidates from older persisted snapshots degrade cleanly when newer optional metadata is absent.
|
||||||
|
|
||||||
## State model
|
## State model
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,6 @@ export const OutreachCandidateSchema = z.strictObject({
|
|||||||
|
|
||||||
export const OutreachFunnelResponseSchema = z.strictObject({
|
export const OutreachFunnelResponseSchema = z.strictObject({
|
||||||
generatedAt: z.number(),
|
generatedAt: z.number(),
|
||||||
physiqueTargeting: z.strictObject({
|
|
||||||
enabled: z.boolean(),
|
|
||||||
minScore: z.number().int().min(1).max(10),
|
|
||||||
maxScore: z.number().int().min(1).max(10),
|
|
||||||
}),
|
|
||||||
activeCount: z.number().int().nonnegative(),
|
activeCount: z.number().int().nonnegative(),
|
||||||
stages: OutreachStageCountsSchema,
|
stages: OutreachStageCountsSchema,
|
||||||
outcomes: z.strictObject({
|
outcomes: z.strictObject({
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ export function createOutreachRouter(ctx: WebContext): Router {
|
|||||||
detectedGender: candidate.prepared.detectedGender,
|
detectedGender: candidate.prepared.detectedGender,
|
||||||
attractivenessScore: candidate.prepared.attractivenessScore ?? null,
|
attractivenessScore: candidate.prepared.attractivenessScore ?? null,
|
||||||
physiqueScore: candidate.prepared.physiqueScore ?? null,
|
physiqueScore: candidate.prepared.physiqueScore ?? null,
|
||||||
physiqueTargeting: candidate.prepared.physiqueTargeting ?? null,
|
physiqueTargeting: candidate.sourceType === 'image'
|
||||||
|
? { ...ctx.runtimeConfig.reddit.physiqueTargeting }
|
||||||
|
: null,
|
||||||
sourceImageUrl: candidate.prepared.sourceImageUrl ?? null,
|
sourceImageUrl: candidate.prepared.sourceImageUrl ?? null,
|
||||||
strategyId: candidate.prepared.strategyId,
|
strategyId: candidate.prepared.strategyId,
|
||||||
experimentId: candidate.prepared.experimentId,
|
experimentId: candidate.prepared.experimentId,
|
||||||
@@ -112,7 +114,6 @@ export function createOutreachRouter(ctx: WebContext): Router {
|
|||||||
};
|
};
|
||||||
const response = {
|
const response = {
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
physiqueTargeting: { ...ctx.runtimeConfig.reddit.physiqueTargeting },
|
|
||||||
activeCount: ACTIVE_STATUSES.reduce((total, status) => total + stats.byStatus[status], 0),
|
activeCount: ACTIVE_STATUSES.reduce((total, status) => total + stats.byStatus[status], 0),
|
||||||
stages,
|
stages,
|
||||||
outcomes: { sent: stats.byStatus.sent, rejected: stats.byStatus.rejected },
|
outcomes: { sent: stats.byStatus.sent, rejected: stats.byStatus.rejected },
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ describe('dashboard foundation', () => {
|
|||||||
})),
|
})),
|
||||||
http.get('/api/outreach', () => HttpResponse.json({
|
http.get('/api/outreach', () => HttpResponse.json({
|
||||||
generatedAt: Date.parse('2026-08-09T08:00:00Z'),
|
generatedAt: Date.parse('2026-08-09T08:00:00Z'),
|
||||||
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
|
|
||||||
activeCount: 3,
|
activeCount: 3,
|
||||||
stages: { discovered: 1, screening: 0, ready: 1, sending: 0, retryable: 1 },
|
stages: { discovered: 1, screening: 0, ready: 1, sending: 0, retryable: 1 },
|
||||||
outcomes: { sent: 12, rejected: 7 },
|
outcomes: { sent: 12, rejected: 7 },
|
||||||
@@ -138,7 +137,7 @@ describe('dashboard foundation', () => {
|
|||||||
message: 'You look incredible in that pose — what inspired this post?',
|
message: 'You look incredible in that pose — what inspired this post?',
|
||||||
imageAnalysis: 'VISIBLE_FACTS:\nBODY: VISIBLE - full-body pose',
|
imageAnalysis: 'VISIBLE_FACTS:\nBODY: VISIBLE - full-body pose',
|
||||||
detectedGender: 'female', attractivenessScore: 68, physiqueScore: 6,
|
detectedGender: 'female', attractivenessScore: 68, physiqueScore: 6,
|
||||||
physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 },
|
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
|
||||||
sourceImageUrl: 'https://i.redd.it/candidate-1.jpg',
|
sourceImageUrl: 'https://i.redd.it/candidate-1.jpg',
|
||||||
strategyId: 'direct-observation', experimentId: 'strategies:direct-observation:abc123',
|
strategyId: 'direct-observation', experimentId: 'strategies:direct-observation:abc123',
|
||||||
promptVersion: 'abc123', selectionReason: 'exploit', modelId: 'example/image-model',
|
promptVersion: 'abc123', selectionReason: 'exploit', modelId: 'example/image-model',
|
||||||
@@ -163,7 +162,7 @@ describe('dashboard foundation', () => {
|
|||||||
expect(preparation).toHaveTextContent('You look incredible in that pose');
|
expect(preparation).toHaveTextContent('You look incredible in that pose');
|
||||||
expect(preparation).toHaveTextContent('68/100');
|
expect(preparation).toHaveTextContent('68/100');
|
||||||
expect(preparation).toHaveTextContent('6/10');
|
expect(preparation).toHaveTextContent('6/10');
|
||||||
expect(preparation).toHaveTextContent('Current target 1–6 · Prepared with filter off');
|
expect(preparation).toHaveTextContent('Current target 1–6');
|
||||||
expect(preparation).toHaveTextContent('example/image-model');
|
expect(preparation).toHaveTextContent('example/image-model');
|
||||||
const sourceImage = within(preparation).getByRole('img', { name: 'Source post: [25F] A potential post' });
|
const sourceImage = within(preparation).getByRole('img', { name: 'Source post: [25F] A potential post' });
|
||||||
expect(sourceImage).toHaveClass('is-blurred');
|
expect(sourceImage).toHaveClass('is-blurred');
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ describe('outreach route', () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(parsed).toMatchObject({
|
expect(parsed).toMatchObject({
|
||||||
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
|
|
||||||
activeCount: 6,
|
activeCount: 6,
|
||||||
stages: { discovered: 2, screening: 1, ready: 1, sending: 1, retryable: 1 },
|
stages: { discovered: 2, screening: 1, ready: 1, sending: 1, retryable: 1 },
|
||||||
outcomes: { sent: 2, rejected: 1 },
|
outcomes: { sent: 2, rejected: 1 },
|
||||||
@@ -85,7 +84,7 @@ describe('outreach route', () => {
|
|||||||
message: 'Prepared opener for Alice',
|
message: 'Prepared opener for Alice',
|
||||||
attractivenessScore: 68,
|
attractivenessScore: 68,
|
||||||
physiqueScore: 6,
|
physiqueScore: 6,
|
||||||
physiqueTargeting: { enabled: true, minScore: 4, maxScore: 7 },
|
physiqueTargeting: { enabled: true, minScore: 1, maxScore: 6 },
|
||||||
sourceImageUrl: 'https://i.redd.it/post-1.jpg',
|
sourceImageUrl: 'https://i.redd.it/post-1.jpg',
|
||||||
strategyId: 'direct-observation',
|
strategyId: 'direct-observation',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user