Skip to content

Commit eac7a1b

Browse files
dongzhang84claude
andcommitted
feat: generate and display suggested replies during Scan Now
- refresh-opportunities.ts: generate replies in parallel for high/medium intent posts at scan time (low intent skipped to limit gpt-4o calls and stay within timeout) - OpportunityCard.tsx: show first suggested reply inline on card with copy button and variation count hint Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 0b3c9cd commit eac7a1b

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

components/OpportunityCard.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export default function OpportunityCard({ opportunity, externalReplied, onViewRe
4545
if (externalReplied) setReplied(true)
4646
}, [externalReplied])
4747
const [replying, setReplying] = useState(false)
48+
const [copied, setCopied] = useState(false)
49+
50+
function handleCopy(text: string) {
51+
navigator.clipboard.writeText(text).then(() => {
52+
setCopied(true)
53+
setTimeout(() => setCopied(false), 2000)
54+
})
55+
}
4856
const [fading, setFading] = useState(false)
4957
const [hidden, setHidden] = useState(false)
5058

@@ -127,6 +135,38 @@ export default function OpportunityCard({ opportunity, externalReplied, onViewRe
127135
{opportunity.reasoning}
128136
</p>
129137

138+
{/* Suggested reply */}
139+
{opportunity.suggestedReplies?.length > 0 && (
140+
<div className="space-y-2 pt-1">
141+
<div className="flex items-center gap-2">
142+
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
143+
Suggested Reply
144+
</span>
145+
{opportunity.suggestedReplies[0].label && (
146+
<span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full">
147+
{opportunity.suggestedReplies[0].label}
148+
</span>
149+
)}
150+
</div>
151+
<p className="text-sm text-slate-700 leading-relaxed bg-slate-50 border-l-2 border-blue-200 px-3 py-2 rounded-r-md whitespace-pre-wrap line-clamp-4">
152+
{opportunity.suggestedReplies[0].text}
153+
</p>
154+
<div className="flex items-center gap-3">
155+
<button
156+
onClick={() => handleCopy(opportunity.suggestedReplies[0].text)}
157+
className="text-xs text-muted-foreground hover:text-foreground border border-border rounded px-2 py-1 transition-colors"
158+
>
159+
{copied ? 'Copied ✓' : 'Copy'}
160+
</button>
161+
{opportunity.suggestedReplies.length > 1 && (
162+
<span className="text-xs text-muted-foreground">
163+
+{opportunity.suggestedReplies.length - 1} more variation{opportunity.suggestedReplies.length > 2 ? 's' : ''} in modal
164+
</span>
165+
)}
166+
</div>
167+
</div>
168+
)}
169+
130170
{/* Actions */}
131171
<div className="flex gap-2 pt-1">
132172
{replied ? (

lib/refresh-opportunities.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { prisma } from '@/lib/db/client'
22
import { fetchSubredditPosts, type RedditPost } from '@/lib/reddit'
33
import { fetchHNStories, type HNStory } from '@/lib/hn'
44
import { scorePosts, type PostToScore } from '@/lib/scorer'
5+
import { generateReplies } from '@/lib/reply-generator'
56

67
interface NormalizedPost {
78
externalId: string
@@ -109,6 +110,25 @@ export async function refreshOpportunitiesForUser(userId: string): Promise<numbe
109110

110111
const metaMap = new Map(capped.map((p) => [p.externalId, p]))
111112

113+
// Generate replies in parallel for high/medium intent posts only
114+
const repliesMap = new Map<string, object[]>()
115+
await Promise.all(
116+
scored
117+
.filter((p) => p.intentLevel === 'high' || p.intentLevel === 'medium')
118+
.map(async (p) => {
119+
const meta = metaMap.get(p.externalId)!
120+
try {
121+
const replies = await generateReplies(
122+
{ title: p.title, body: p.body ?? '', subreddit: meta.subreddit },
123+
{ productDescription: profile.productDescription ?? '', targetCustomer: profile.targetCustomer ?? '' }
124+
)
125+
repliesMap.set(p.externalId, replies as object[])
126+
} catch {
127+
console.error(`[refresh] Failed to generate replies for ${p.externalId}`)
128+
}
129+
})
130+
)
131+
112132
const result = await prisma.opportunity.createMany({
113133
data: scored.map((p) => {
114134
const meta = metaMap.get(p.externalId)!
@@ -127,7 +147,7 @@ export async function refreshOpportunitiesForUser(userId: string): Promise<numbe
127147
relevanceScore: p.relevanceScore,
128148
intentLevel: p.intentLevel,
129149
reasoning: p.reasoning,
130-
suggestedReplies: [],
150+
suggestedReplies: repliesMap.get(p.externalId) ?? [],
131151
}
132152
}),
133153
skipDuplicates: true,

0 commit comments

Comments
 (0)