Skip to content

feat: transform contact chat into structured interview experience#24

Merged
rupurt merged 26 commits intomainfrom
interview-contact
Dec 25, 2025
Merged

feat: transform contact chat into structured interview experience#24
rupurt merged 26 commits intomainfrom
interview-contact

Conversation

@rupurt
Copy link
Copy Markdown
Contributor

@rupurt rupurt commented Dec 24, 2025

Summary

  • Add 8-question interview flow with visual card selection (intent, role, AI maturity, working style, timeline, company size, industry, budget)
  • Implement lead scoring algorithm with hot/warm/cool/cold tiers for prioritization
  • Create InterviewContainer orchestrating structured → chat → post-contact phases
  • Update Claude system prompt with context-aware interview answers
  • Add email notification with interview profile and lead score badges

Architecture

  • Frontend: State machine in useInterview hook manages phases/questions/answers
  • Components: AnswerCard, InterviewQuestion, ProgressIndicator, ResponseStarter
  • Backend: Phase-based routing (structured, chat, post_contact) with in-memory session storage
  • Database: New migration (0002) adds 11 columns for interview data

Test Plan

  • All 122 unit tests passing (pnpm test)
  • TypeScript type checking passes (pnpm typecheck)
  • Biome linting passes (pnpm lint)
  • Build succeeds (pnpm build)
  • Manual test: Interview flow completes all questions
  • Manual test: Lead score calculates correctly
  • Manual test: Email contains interview data
  • Deploy and run D1 migration

Copilot AI review requested due to automatic review settings December 24, 2025 23:32
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Dec 24, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
vibes-website 2328dd3 Commit Preview URL

Branch Preview URL
Dec 25 2025, 12:20 AM

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR transforms the contact chat from a freeform conversation into a structured interview experience with 8 visual card-based questions that lead into an AI-powered chat, followed by lead scoring and qualification. The implementation adds a multi-phase flow (structured → chat → post-contact → complete) managed by a state machine, integrates interview context into Claude's system prompt, and calculates lead scores (hot/warm/cool/cold) based on structured answers.

Key Changes:

  • Adds 8-question structured interview with visual card selection for intent, role, AI maturity, working style, timeline, company size, industry, and budget
  • Implements lead scoring algorithm (0-13 points) with automatic tier classification
  • Creates InterviewContainer component orchestrating all phases with progress indicators and response starters
  • Updates Claude system prompt to be context-aware using interview answers
  • Enhances email notifications with lead score badges and interview profile tables

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
workers/chat-api/src/types.ts Adds interview answer types, phase enums, and updates ChatRequest/ChatResponse interfaces
workers/chat-api/src/scoring.ts Implements lead scoring algorithm with weighted criteria (timeline, budget, intent, AI maturity, company size)
workers/chat-api/src/scoring.test.ts Comprehensive test coverage for scoring logic including edge cases
workers/chat-api/src/leads.ts Updates saveLead to accept and store interview answers, return score/tier
workers/chat-api/src/index.ts Adds phase-based routing, in-memory session storage for interview answers, and structured answer handling
workers/chat-api/src/email.ts Adds interview profile section and lead score badges to notification emails
workers/chat-api/src/claude.ts Creates context-aware system prompt builder using interview answers
workers/chat-api/schema.sql Adds 11 new columns for interview data and lead scoring
workers/chat-api/migrations/0002_interview_fields.sql Migration to add interview fields to existing leads table
vitest.config.ts Extends test coverage to include workers directory
src/features/chat/hooks/useInterview.ts State machine hook managing interview phases, question progression, and answer collection
src/features/chat/hooks/useInterview.test.ts Tests for interview state transitions through all phases
src/features/chat/hooks/useChat.ts Updates to pass interview answers in API calls
src/features/chat/config/questions.ts Defines 8 interview questions with options, icons, and response starters
src/features/chat/config/questions.test.ts Validates question structure and helper functions
src/features/chat/components/AnswerCard.tsx Visual card component for interview option selection
src/features/chat/components/InterviewQuestion.tsx Displays question with grid of AnswerCard options
src/features/chat/components/ProgressIndicator.tsx Shows interview progress with dots and optional label
src/features/chat/components/ResponseStarter.tsx Clickable suggestion pills to reduce chat friction
src/features/chat/components/InterviewContainer.tsx Orchestrates entire interview flow with phase-based rendering
src/features/chat/components/ChatInput.tsx Updates to support controlled value from parent
docs/plans/08-interview-contact-chat/implementation.md Detailed 20-task implementation plan
docs/plans/08-interview-contact-chat/design.md Design specification with scoring criteria and UI patterns
biome.json Adds .wrangler to ignored paths
Comments suppressed due to low confidence (2)

src/features/chat/components/InterviewContainer.tsx:1

  • The contact detection logic is fragile and relies on simple string matching. The email detection (includes('@') && includes('.')) will produce false positives (e.g., "john@company.co.uk" is valid but "visit example.com @ 3pm" would also match). Consider using a more robust email regex pattern or relying on the backend's leadExtracted flag instead of client-side heuristics.
import { cn } from '@/lib/cn'

workers/chat-api/src/claude.ts:1

  • The commented-out "Constraints" line appears to be a leftover from the original prompt. Since timeline and budget are now captured in the structured interview, this line should either be removed or updated to clarify that only technical requirements and integrations need to be gathered during the chat phase.
import type { InterviewAnswers, Message } from './types'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings December 24, 2025 23:47
@rupurt rupurt enabled auto-merge December 24, 2025 23:48
- Replace ChatContainer with InterviewContainer on contact page
- Update E2E tests to check for interview interface instead of chat
- Skip interview navigation test due to hydration timing (same as form toggle)
- Extract setStructuredAnswer helper with validation for type-safe answer storage
- Add useMemo to ProgressIndicator for dot config memoization
- Replace fragile client-side email detection with backend leadExtracted callback
- Add onLeadExtracted callback to useChat hook
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (2)

workers/chat-api/src/claude.ts:1

  • The commented-out Constraints line in the system prompt appears to be an oversight. This field is still being extracted and used in the PRD generation, so either the line should be uncommented or the extraction logic should be updated to reflect that constraints are no longer gathered during chat.
import type { InterviewAnswers, Message } from './types'

src/features/chat/components/AnswerCard.tsx:1

  • The role='button' attribute is redundant since the element is already a button element. This adds unnecessary markup and could potentially confuse assistive technologies. Remove the role attribute.
import { cn } from '@/lib/cn'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +26
const dots = useMemo(
() =>
Array.from({ length: total }, (_, i) => ({
id: `progress-dot-${i}`,
index: i,
state: i < current ? 'completed' : i === current ? 'current' : 'upcoming',
})),
[total, current],
)
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array creation in useMemo is unnecessarily complex. Since the dots are simple derived state based on current and total, the memoization adds overhead without clear benefit. The component will re-render when these props change anyway, making the memoization redundant. Consider simplifying by directly mapping in the JSX or creating a simple derived array outside useMemo.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +48
function setStructuredAnswer(
sessionId: string,
questionId: string,
answer: string,
): InterviewAnswers {
const answers = sessionInterviewAnswers.get(sessionId) ?? {}

if (VALID_QUESTION_IDS.has(questionId)) {
// Type assertion is safe here because we've validated questionId
;(answers as Record<string, string>)[questionId] = answer
}

sessionInterviewAnswers.set(sessionId, answers)
return answers
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setStructuredAnswer function silently ignores invalid question IDs without logging or error handling. When an invalid questionId is provided, the answer is lost without notification, making debugging difficult. Consider logging a warning or throwing an error when an invalid questionId is encountered to aid in debugging and prevent silent data loss.

Copilot uses AI. Check for mistakes.
import type { ChatRequest, ChatResponse, Env, InterviewAnswers, LeadTierValue } from './types'

// In-memory store for interview answers per session
const sessionInterviewAnswers = new Map<string, InterviewAnswers>()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using in-memory storage for interview answers creates a memory leak in a serverless worker environment. The Map will grow unbounded as sessions are created but never cleaned up. Consider either implementing a TTL-based cleanup mechanism, moving this storage to D1, or using Durable Objects if session state persistence is required.

Copilot uses AI. Check for mistakes.
@rupurt rupurt disabled auto-merge December 24, 2025 23:49
- Simplify ProgressIndicator by removing useMemo overhead (reviewer feedback)
- Add warning log when invalid questionId is provided to setStructuredAnswer
- Add MAX_CACHED_SESSIONS limit (1000) with LRU eviction to prevent memory leak
  in long-running isolates
- Add lucide-react library for consistent outline icon styling
- Update AnswerCard to render LucideIcon components instead of emoji strings
- Replace all emoji icons in interview questions with semantic Lucide icons
- Icons now follow theme colors (text-accent when selected, text-muted-foreground default)
- Use stroke-[1.5] for clean outline appearance
Copilot AI review requested due to automatic review settings December 24, 2025 23:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 35 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to +17
// Limited to prevent unbounded growth in long-running isolates.
// When limit is exceeded, oldest sessions are evicted (Map maintains insertion order).
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-memory Map for session storage lacks documentation about eviction behavior and potential data loss on Worker restart. Add a comment explaining that this cache is ephemeral and answers are lost on Worker restarts/migrations, which is acceptable since they're also stored in the database via saveLead.

Suggested change
// Limited to prevent unbounded growth in long-running isolates.
// When limit is exceeded, oldest sessions are evicted (Map maintains insertion order).
// This is an ephemeral, per-isolate cache only: all entries are lost on Worker
// restart or migration, and there is no cross-request or cross-region durability.
// Limited to prevent unbounded growth in long-running isolates. When the limit
// is exceeded, the oldest sessions are evicted (Map maintains insertion order).
// This potential data loss is acceptable because finalized answers are also
// persisted to the database via `saveLead`, which is the source of truth.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +106
// Store/update interview answers for this session
if (body.interviewAnswers) {
// Evict oldest sessions if limit exceeded
while (sessionInterviewAnswers.size >= MAX_CACHED_SESSIONS) {
const oldestKey = sessionInterviewAnswers.keys().next().value
if (oldestKey) sessionInterviewAnswers.delete(oldestKey)
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate eviction logic appears in two places (lines 52-55 and 102-106). Extract this into a helper function to avoid repetition and ensure consistent eviction behavior across both code paths.

Suggested change
// Store/update interview answers for this session
if (body.interviewAnswers) {
// Evict oldest sessions if limit exceeded
while (sessionInterviewAnswers.size >= MAX_CACHED_SESSIONS) {
const oldestKey = sessionInterviewAnswers.keys().next().value
if (oldestKey) sessionInterviewAnswers.delete(oldestKey)
}
const evictOldInterviewAnswerSessions = () => {
// Evict oldest sessions if limit exceeded
while (sessionInterviewAnswers.size >= MAX_CACHED_SESSIONS) {
const oldestKey = sessionInterviewAnswers.keys().next().value
if (oldestKey) {
sessionInterviewAnswers.delete(oldestKey)
} else {
break
}
}
}
// Store/update interview answers for this session
if (body.interviewAnswers) {
evictOldInterviewAnswerSessions()

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,174 @@
import type { LucideIcon } from 'lucide-react'
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The questions.ts file imports LucideIcon type but the actual icon components are imported in the same file. Consider separating the type definitions from the icon imports for better code organization and to avoid potential circular dependency issues if this config is used in other type-only contexts.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +22
function buildDots(total: number, current: number): DotConfig[] {
return Array.from({ length: total }, (_, i) => ({
key: `progress-dot-${i}`,
index: i,
state: i < current ? 'completed' : i === current ? 'current' : 'upcoming',
}))
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildDots function is extracted but only used once. While this improves readability, consider whether the added indirection is necessary for a single-use function. If kept, consider making it a const arrow function for consistency with React conventions.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +49
function setStructuredAnswer(
sessionId: string,
questionId: string,
answer: string,
): InterviewAnswers {
const answers = sessionInterviewAnswers.get(sessionId) ?? {}

if (VALID_QUESTION_IDS.has(questionId)) {
// Type assertion is safe here because we've validated questionId
;(answers as Record<string, string>)[questionId] = answer
} else {
console.warn(`Invalid questionId "${questionId}" for session ${sessionId} - answer discarded`)
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion on line 46 bypasses TypeScript's type safety. Consider using a more type-safe approach such as defining a union type of valid question IDs or using a type guard function to narrow the type instead of using a runtime Set check with a type assertion.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +78
const getActiveStarters = (): string[] => {
if (phase !== 'chat' || isLoading) return []

// Determine which starters to show based on message count
const userMessages = messages.filter((m) => m.role === 'user').length
if (userMessages === 0) return getResponseStarters('problem')
if (userMessages === 1) return getResponseStarters('vision')
if (userMessages === 2) return getResponseStarters('users')
return []
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response starter logic is hardcoded with magic numbers (0, 1, 2) and assumes a specific conversation flow. Consider extracting this into a configuration object or enum that maps conversation stages to starter types for better maintainability and clarity.

Copilot uses AI. Check for mistakes.
Comment on lines +99 to +131
if (answers.intent)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Intent</td><td style="padding:4px 8px;">${INTENT_LABELS[answers.intent] ?? answers.intent}</td></tr>`,
)
if (answers.role)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Role</td><td style="padding:4px 8px;">${ROLE_LABELS[answers.role] ?? answers.role}</td></tr>`,
)
if (answers.ai_maturity)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">AI Maturity</td><td style="padding:4px 8px;">${AI_MATURITY_LABELS[answers.ai_maturity] ?? answers.ai_maturity}</td></tr>`,
)
if (answers.working_style)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Working Style</td><td style="padding:4px 8px;">${WORKING_STYLE_LABELS[answers.working_style] ?? answers.working_style}</td></tr>`,
)
if (answers.timeline)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Timeline</td><td style="padding:4px 8px;">${TIMELINE_LABELS[answers.timeline] ?? answers.timeline}</td></tr>`,
)
if (answers.company_size)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Company Size</td><td style="padding:4px 8px;">${COMPANY_SIZE_LABELS[answers.company_size] ?? answers.company_size}</td></tr>`,
)
if (answers.industry)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Industry</td><td style="padding:4px 8px;">${INDUSTRY_LABELS[answers.industry] ?? answers.industry}</td></tr>`,
)
if (answers.budget_range)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Budget</td><td style="padding:4px 8px;">${BUDGET_LABELS[answers.budget_range] ?? answers.budget_range}</td></tr>`,
)

Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The email template uses inline HTML generation with repetitive style attributes. Consider extracting the table row generation into a helper function that takes a label and value to reduce duplication across all 8 interview fields.

Suggested change
if (answers.intent)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Intent</td><td style="padding:4px 8px;">${INTENT_LABELS[answers.intent] ?? answers.intent}</td></tr>`,
)
if (answers.role)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Role</td><td style="padding:4px 8px;">${ROLE_LABELS[answers.role] ?? answers.role}</td></tr>`,
)
if (answers.ai_maturity)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">AI Maturity</td><td style="padding:4px 8px;">${AI_MATURITY_LABELS[answers.ai_maturity] ?? answers.ai_maturity}</td></tr>`,
)
if (answers.working_style)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Working Style</td><td style="padding:4px 8px;">${WORKING_STYLE_LABELS[answers.working_style] ?? answers.working_style}</td></tr>`,
)
if (answers.timeline)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Timeline</td><td style="padding:4px 8px;">${TIMELINE_LABELS[answers.timeline] ?? answers.timeline}</td></tr>`,
)
if (answers.company_size)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Company Size</td><td style="padding:4px 8px;">${COMPANY_SIZE_LABELS[answers.company_size] ?? answers.company_size}</td></tr>`,
)
if (answers.industry)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Industry</td><td style="padding:4px 8px;">${INDUSTRY_LABELS[answers.industry] ?? answers.industry}</td></tr>`,
)
if (answers.budget_range)
rows.push(
`<tr><td style="color:#64748b;padding:4px 8px;">Budget</td><td style="padding:4px 8px;">${BUDGET_LABELS[answers.budget_range] ?? answers.budget_range}</td></tr>`,
)
const createRow = (label: string, value: string): string =>
`<tr><td style="color:#64748b;padding:4px 8px;">${label}</td><td style="padding:4px 8px;">${value}</td></tr>`
if (answers.intent) {
const value = INTENT_LABELS[answers.intent] ?? answers.intent
rows.push(createRow('Intent', value))
}
if (answers.role) {
const value = ROLE_LABELS[answers.role] ?? answers.role
rows.push(createRow('Role', value))
}
if (answers.ai_maturity) {
const value = AI_MATURITY_LABELS[answers.ai_maturity] ?? answers.ai_maturity
rows.push(createRow('AI Maturity', value))
}
if (answers.working_style) {
const value = WORKING_STYLE_LABELS[answers.working_style] ?? answers.working_style
rows.push(createRow('Working Style', value))
}
if (answers.timeline) {
const value = TIMELINE_LABELS[answers.timeline] ?? answers.timeline
rows.push(createRow('Timeline', value))
}
if (answers.company_size) {
const value = COMPANY_SIZE_LABELS[answers.company_size] ?? answers.company_size
rows.push(createRow('Company Size', value))
}
if (answers.industry) {
const value = INDUSTRY_LABELS[answers.industry] ?? answers.industry
rows.push(createRow('Industry', value))
}
if (answers.budget_range) {
const value = BUDGET_LABELS[answers.budget_range] ?? answers.budget_range
rows.push(createRow('Budget', value))
}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
import { cn } from '@/lib/cn'
import type { LucideIcon } from 'lucide-react'
import type { ComponentProps } from 'react'

interface AnswerCardProps extends Omit<ComponentProps<'button'>, 'onSelect'> {
icon: LucideIcon
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AnswerCard component imports LucideIcon type but receives icon components directly. The icon prop type should be LucideIcon (the component type) not string, which matches the actual usage where Icon components are passed and rendered on line 41.

Copilot uses AI. Check for mistakes.
Switch interview UI from visual icon cards to a conversational text-based
format with letter prefixes (A, B, C, etc.):

- Remove lucide-react dependency
- Simplify AnswerCard to horizontal text layout with letter badge
- Update InterviewQuestion to vertical list layout
- Remove icon field from question options
- Add message tracking to useInterview hook (questions as assistant
  messages, answers as user messages)
- Create SuggestionChips component for quick-tap answer options
- Redesign InterviewContainer as unified chat view
- Add placeholder prop to ChatInput for phase-specific prompts
- Combine interview and AI chat messages in single thread
- Allow typed custom answers in addition to suggestion chips
Copilot AI review requested due to automatic review settings December 25, 2025 00:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +12
export interface InterviewQuestion {
id: string
question: string
subtitle?: string
options: QuestionOption[]
phase: 'opener' | 'personality' | 'qualification' | 'post_contact'
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The QuestionOption interface includes an 'icon' field (line 3) but AnswerCard.tsx uses a 'prefix' prop with letter values (lines 5-6). The questions.ts file defines icon as string (e.g., '🎯'), while AnswerCard expects a letter prefix like 'A', 'B'. This inconsistency suggests either the icon field is unused or there's a mismatch in the component contract.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
function getLetterPrefix(index: number): string {
return String.fromCharCode(65 + index)
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function converts index to letter (0 → 'A', 1 → 'B') but questions.ts defines options with icon emojis. The component generates letter prefixes instead of using the icon field from the question options, creating an inconsistency between the data structure and its usage.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +19
const MAX_CACHED_SESSIONS = 1000
const sessionInterviewAnswers = new Map<string, InterviewAnswers>()
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-memory Map will lose all interview answers when the worker restarts or is redeployed. Since structured answers are critical for lead scoring, consider persisting them to D1 database or using Durable Objects for stateful storage across worker restarts.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +55
while (sessionInterviewAnswers.size >= MAX_CACHED_SESSIONS) {
const oldestKey = sessionInterviewAnswers.keys().next().value
if (oldestKey) sessionInterviewAnswers.delete(oldestKey)
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This eviction logic is duplicated in lines 103-106. Extract this into a helper function like evictOldestSession() to reduce duplication and ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +11
interface AnswerCardProps extends Omit<ComponentProps<'button'>, 'onSelect'> {
/** Letter prefix like "A", "B", "C" */
prefix: string
label: string
value: string
selected?: boolean
onSelect: (value: string) => void
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component expects a 'prefix' prop (letter like 'A', 'B') but questions.ts defines options with an 'icon' field (emojis). This creates confusion about whether the component should display letters or icons. Consider renaming to 'icon' and updating the component to match the data structure, or update questions.ts to remove the icon field if it's unused.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +50
function createInitialMessages(firstQuestion: InterviewQuestion): InterviewMessage[] {
return [
{
id: 'welcome',
role: 'assistant',
content:
"Hey! I'm here to learn about what you're building. Let me ask a few quick questions to understand how we can help.",
timestamp: new Date(),
},
{
id: `q-${firstQuestion.id}`,
role: 'assistant',
content: firstQuestion.question,
timestamp: new Date(),
},
]
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The welcome message hardcodes 'a few quick questions' but the actual count is 7. Consider making this message template-driven or referencing the actual count from structuredQuestions.length for maintainability if question count changes.

Copilot uses AI. Check for mistakes.
- Remove progress indicator check (no longer shown in chat UI)
- Update to check for welcome message and suggestion chips
- Update skipped navigation test to match new flow
@rupurt rupurt enabled auto-merge December 25, 2025 00:20
@rupurt rupurt merged commit 1947ce6 into main Dec 25, 2025
3 checks passed
@rupurt rupurt deleted the interview-contact branch December 25, 2025 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants