feat: expand outreach targeting and messaging
This commit is contained in:
1 parent
7defff422d
commit
d0a8bd0e2f
40 files changed
+890
-82
No files matched your search
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { OutreachCandidate, OutreachCandidateStatus } from '../../../../../packages/api-contracts/src/index.js';
|
||||
import { AccountOwnerBadge } from '../../components/account-owner-badge.js';
|
||||
import { OriginPostCard } from '../../components/origin-post.js';
|
||||
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
|
||||
import { formatExactTime, formatShortRelativeTime } from '../conversations/relative-time.js';
|
||||
import { outreachApi } from './outreach-api.js';
|
||||
@@ -37,6 +38,52 @@ function formatWaitTime(timestamp: number, now: number): string {
|
||||
return `in ${Math.ceil(remaining / 86_400_000)}d`;
|
||||
}
|
||||
|
||||
function PreparedCandidateDetails({ candidate }: { candidate: OutreachCandidate }) {
|
||||
const preparation = candidate.preparation;
|
||||
if (!preparation) return null;
|
||||
const targeting = preparation.physiqueTargeting;
|
||||
const strategy = preparation.scripted
|
||||
? 'Scripted opener'
|
||||
: preparation.strategyId ?? 'Default strategy';
|
||||
|
||||
return (
|
||||
<section aria-label={`Prepared outreach for u/${candidate.author}`} className="candidate-preparation">
|
||||
<div className="candidate-preparation-grid">
|
||||
{preparation.sourceImageUrl ? (
|
||||
<OriginPostCard post={{
|
||||
title: candidate.title,
|
||||
url: candidate.postUrl,
|
||||
imageUrl: preparation.sourceImageUrl,
|
||||
subreddit: candidate.subreddit,
|
||||
postedAt: candidate.postCreatedAt,
|
||||
}} />
|
||||
) : null}
|
||||
<div className="candidate-preparation-copy">
|
||||
<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>Physique</span><strong>{preparation.physiqueScore === null ? 'Not scorable' : `${preparation.physiqueScore}/10`}</strong><small>{targeting?.enabled ? `Target ${targeting.minScore}–${targeting.maxScore}` : 'Range filter off'}</small></div>
|
||||
<div><span>Detected gender</span><strong>{preparation.detectedGender}</strong><small>Image analysis</small></div>
|
||||
</div>
|
||||
<div className="candidate-preparation-metadata">
|
||||
<span><strong>Strategy</strong> {strategy}</span>
|
||||
{preparation.selectionReason ? <span><strong>Selection</strong> {preparation.selectionReason}</span> : null}
|
||||
<span><strong>Tone</strong> {preparation.toneId}</span>
|
||||
<span><strong>Model</strong> {preparation.modelId}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="candidate-prepared-message">
|
||||
<span>Prepared message</span>
|
||||
<blockquote>{preparation.message}</blockquote>
|
||||
</div>
|
||||
<details className="candidate-analysis">
|
||||
<summary>Show image analysis</summary>
|
||||
<pre>{preparation.imageAnalysis}</pre>
|
||||
</details>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CandidateCard({ candidate, now }: { candidate: OutreachCandidate; now: number }) {
|
||||
const waitingUntil = candidate.availableAt > now;
|
||||
const isKnownStatus = KNOWN_STATUSES.has(candidate.status);
|
||||
@@ -61,11 +108,13 @@ function CandidateCard({ candidate, now }: { candidate: OutreachCandidate; now:
|
||||
<div className="candidate-meta">
|
||||
<RedditUsernameLink username={candidate.author} />
|
||||
<span>{candidate.hasPreparedMessage ? 'Message prepared' : 'Not prepared yet'}</span>
|
||||
{candidate.physiqueScore !== null ? <span>Physique {candidate.physiqueScore}/10</span> : null}
|
||||
<span>{candidate.attemptCount} {candidate.attemptCount === 1 ? 'attempt' : 'attempts'}</span>
|
||||
{waitingUntil ? <span title={formatExactTime(candidate.availableAt)}>Retry {formatWaitTime(candidate.availableAt, now)}</span> : null}
|
||||
</div>
|
||||
{candidate.note ? <p className="candidate-note"><strong>Latest note</strong>{candidate.note}</p> : null}
|
||||
{candidate.owner ? <AccountOwnerBadge owner={candidate.owner} /> : null}
|
||||
<PreparedCandidateDetails candidate={candidate} />
|
||||
</article>
|
||||
</li>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user