371 lines
9.5 KiB
Markdown
371 lines
9.5 KiB
Markdown
# ✅ Collector Handoff Persistence - Complete
|
|
|
|
## Overview
|
|
|
|
The Collector AI now **persists its checklist state** to Firestore on every chat turn, ensuring the checklist survives across sessions and page refreshes.
|
|
|
|
---
|
|
|
|
## What Was Added
|
|
|
|
### 1. **Structured Output from AI**
|
|
|
|
The Collector AI now returns both:
|
|
- **Conversational reply** (user-facing message)
|
|
- **Collector handoff data** (structured checklist state)
|
|
|
|
```typescript
|
|
{
|
|
"reply": "✅ I see you've uploaded 2 documents. Anything else?",
|
|
"collectorHandoff": {
|
|
"hasDocuments": true,
|
|
"documentCount": 2,
|
|
"githubConnected": false,
|
|
"extensionLinked": false,
|
|
"readyForExtraction": false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. **Persistence to Firestore**
|
|
|
|
**Location:** `projects/{projectId}/phaseData.phaseHandoffs.collector`
|
|
|
|
**Structure:**
|
|
```typescript
|
|
interface CollectorPhaseHandoff {
|
|
phase: 'collector';
|
|
readyForNextPhase: boolean; // Ready for extraction?
|
|
confidence: number; // 0.5 or 0.9
|
|
confirmed: {
|
|
hasDocuments?: boolean; // Docs uploaded?
|
|
documentCount?: number; // How many?
|
|
githubConnected?: boolean; // GitHub connected?
|
|
githubRepo?: string; // Repo name
|
|
extensionLinked?: boolean; // Extension connected?
|
|
};
|
|
uncertain: {
|
|
extensionDeclined?: boolean; // User said no to extension?
|
|
noGithubYet?: boolean; // User doesn't have GitHub?
|
|
};
|
|
missing: string[]; // What's still needed
|
|
questionsForUser: string[]; // Follow-up questions
|
|
sourceEvidence: string[]; // Source references
|
|
version: string; // "1.0"
|
|
timestamp: string; // ISO timestamp
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. **Code Changes**
|
|
|
|
#### **`app/api/ai/chat/route.ts`**
|
|
|
|
Added structured output schema:
|
|
```typescript
|
|
const ChatReplySchema = z.object({
|
|
reply: z.string(),
|
|
collectorHandoff: z.object({
|
|
hasDocuments: z.boolean().optional(),
|
|
documentCount: z.number().optional(),
|
|
githubConnected: z.boolean().optional(),
|
|
githubRepo: z.string().optional(),
|
|
extensionLinked: z.boolean().optional(),
|
|
extensionDeclined: z.boolean().optional(),
|
|
noGithubYet: z.boolean().optional(),
|
|
readyForExtraction: z.boolean().optional(),
|
|
}).optional(),
|
|
});
|
|
```
|
|
|
|
Added persistence logic:
|
|
```typescript
|
|
// If in collector mode and AI provided handoff data, persist it
|
|
if (resolvedMode === 'collector_mode' && reply.collectorHandoff) {
|
|
const handoff: CollectorPhaseHandoff = {
|
|
phase: 'collector',
|
|
readyForNextPhase: reply.collectorHandoff.readyForExtraction ?? false,
|
|
confidence: reply.collectorHandoff.readyForExtraction ? 0.9 : 0.5,
|
|
confirmed: {
|
|
hasDocuments: reply.collectorHandoff.hasDocuments,
|
|
documentCount: reply.collectorHandoff.documentCount,
|
|
githubConnected: reply.collectorHandoff.githubConnected,
|
|
githubRepo: reply.collectorHandoff.githubRepo,
|
|
extensionLinked: reply.collectorHandoff.extensionLinked,
|
|
},
|
|
uncertain: {
|
|
extensionDeclined: reply.collectorHandoff.extensionDeclined,
|
|
noGithubYet: reply.collectorHandoff.noGithubYet,
|
|
},
|
|
missing: [],
|
|
questionsForUser: [],
|
|
sourceEvidence: [],
|
|
version: '1.0',
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
// Persist to Firestore
|
|
await adminDb.collection('projects').doc(projectId).set(
|
|
{
|
|
'phaseData.phaseHandoffs.collector': handoff,
|
|
},
|
|
{ merge: true }
|
|
);
|
|
}
|
|
```
|
|
|
|
Added console logging:
|
|
```typescript
|
|
console.log(`[AI Chat] Collector handoff persisted:`, {
|
|
hasDocuments: handoff.confirmed.hasDocuments,
|
|
githubConnected: handoff.confirmed.githubConnected,
|
|
extensionLinked: handoff.confirmed.extensionLinked,
|
|
readyForExtraction: handoff.readyForNextPhase,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
#### **`lib/ai/prompts/collector.ts`**
|
|
|
|
Added structured output instructions:
|
|
|
|
```markdown
|
|
**STRUCTURED OUTPUT:**
|
|
In addition to your conversational reply, you MUST also return a collectorHandoff object tracking the checklist state:
|
|
|
|
```json
|
|
{
|
|
"reply": "Your conversational response here",
|
|
"collectorHandoff": {
|
|
"hasDocuments": true, // Are documents uploaded?
|
|
"documentCount": 5, // How many?
|
|
"githubConnected": true, // Is GitHub connected?
|
|
"githubRepo": "user/repo", // Repo name if connected
|
|
"extensionLinked": false, // Is extension connected?
|
|
"extensionDeclined": false, // Did user say no to extension?
|
|
"noGithubYet": false, // Did user say they don't have GitHub yet?
|
|
"readyForExtraction": false // Is user ready to move to extraction? (true when they say "yes" to "Is that everything?")
|
|
}
|
|
}
|
|
```
|
|
|
|
Update this object on EVERY response based on the current state of:
|
|
- What you see in projectContext (documents, GitHub, extension)
|
|
- What the user explicitly confirms or declines
|
|
|
|
This data will be persisted to Firestore so the checklist state survives across sessions.
|
|
```
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
### **Flow:**
|
|
|
|
1. **User sends message** → "I uploaded some docs"
|
|
|
|
2. **Collector AI analyzes** `projectContext`:
|
|
- Sees `knowledgeSummary.bySourceType.imported_document = 3`
|
|
- Sees `project.githubRepo = null`
|
|
- Sees no extension data
|
|
|
|
3. **AI responds with structured output**:
|
|
```json
|
|
{
|
|
"reply": "✅ I see you've uploaded 3 documents. Do you have a GitHub repo?",
|
|
"collectorHandoff": {
|
|
"hasDocuments": true,
|
|
"documentCount": 3,
|
|
"githubConnected": false,
|
|
"extensionLinked": false,
|
|
"readyForExtraction": false
|
|
}
|
|
}
|
|
```
|
|
|
|
4. **Backend persists handoff to Firestore**:
|
|
- Writes to `projects/{projectId}/phaseData.phaseHandoffs.collector`
|
|
- Logs checklist state to console
|
|
|
|
5. **On next page load/refresh**:
|
|
- Checklist state is still there
|
|
- AI can see previous state and continue from where it left off
|
|
|
|
---
|
|
|
|
## Benefits
|
|
|
|
### ✅ **Checklist Survives Sessions**
|
|
- User can close browser and come back
|
|
- Progress is never lost
|
|
|
|
### ✅ **Debugging & Analytics**
|
|
- Can see exact checklist state at any point
|
|
- Helps debug "why did AI ask that?" questions
|
|
|
|
### ✅ **Smart Handoff Protocol**
|
|
- When `readyForExtraction = true`, system knows to transition
|
|
- Can build automatic phase transitions later
|
|
|
|
### ✅ **Historical Tracking**
|
|
- Timestamp on every update
|
|
- Can see how checklist evolved over time
|
|
|
|
---
|
|
|
|
## Example Firestore Document
|
|
|
|
```json
|
|
{
|
|
"projects": {
|
|
"abc123": {
|
|
"name": "My SaaS",
|
|
"currentPhase": "collector",
|
|
"phaseData": {
|
|
"phaseHandoffs": {
|
|
"collector": {
|
|
"phase": "collector",
|
|
"readyForNextPhase": false,
|
|
"confidence": 0.5,
|
|
"confirmed": {
|
|
"hasDocuments": true,
|
|
"documentCount": 3,
|
|
"githubConnected": true,
|
|
"githubRepo": "user/my-saas",
|
|
"extensionLinked": false
|
|
},
|
|
"uncertain": {
|
|
"extensionDeclined": false,
|
|
"noGithubYet": false
|
|
},
|
|
"missing": [],
|
|
"questionsForUser": [],
|
|
"sourceEvidence": [],
|
|
"version": "1.0",
|
|
"timestamp": "2025-11-17T22:30:00.000Z"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### **Manual Test:**
|
|
|
|
1. Start a new project chat
|
|
2. Say: "I uploaded some documents"
|
|
3. Check Firestore:
|
|
```bash
|
|
# In Firebase Console → Firestore
|
|
projects/{projectId}/phaseData.phaseHandoffs.collector
|
|
```
|
|
4. Verify `confirmed.hasDocuments = true`
|
|
5. Refresh page
|
|
6. Send another message
|
|
7. Verify handoff updates with latest state
|
|
|
|
### **Console Output:**
|
|
|
|
Watch for this log on each collector message:
|
|
```
|
|
[AI Chat] Collector handoff persisted: {
|
|
hasDocuments: true,
|
|
githubConnected: false,
|
|
extensionLinked: false,
|
|
readyForExtraction: false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
### **1. Auto-Transition to Extraction**
|
|
|
|
When `readyForExtraction = true`, automatically switch mode:
|
|
|
|
```typescript
|
|
if (handoff.readyForNextPhase) {
|
|
await adminDb.collection('projects').doc(projectId).update({
|
|
currentPhase: 'analyzed',
|
|
});
|
|
|
|
// Next message will be in extraction_review_mode
|
|
}
|
|
```
|
|
|
|
### **2. Visual Checklist UI**
|
|
|
|
Display the checklist state in the UI:
|
|
|
|
```tsx
|
|
<ChecklistCard>
|
|
<ChecklistItem
|
|
checked={handoff.confirmed.hasDocuments}
|
|
label="Documents uploaded"
|
|
/>
|
|
<ChecklistItem
|
|
checked={handoff.confirmed.githubConnected}
|
|
label="GitHub connected"
|
|
/>
|
|
<ChecklistItem
|
|
checked={handoff.confirmed.extensionLinked}
|
|
label="Extension linked"
|
|
/>
|
|
</ChecklistCard>
|
|
```
|
|
|
|
### **3. Analytics Dashboard**
|
|
|
|
Track average time to complete collector phase:
|
|
- Time from first message to `readyForExtraction = true`
|
|
- Most common blockers (missing docs? no GitHub?)
|
|
- Drop-off points
|
|
|
|
### **4. Smart Reminders**
|
|
|
|
If user hasn't interacted in 24 hours and checklist incomplete:
|
|
- Send email: "Hey! You're 2/3 done setting up your project..."
|
|
- Show prompt on next login
|
|
|
|
---
|
|
|
|
## Files Changed
|
|
|
|
1. ✅ `app/api/ai/chat/route.ts` - Added handoff persistence
|
|
2. ✅ `lib/ai/prompts/collector.ts` - Added structured output instructions
|
|
3. ✅ `lib/types/phase-handoff.ts` - Type already existed (no changes needed)
|
|
|
|
---
|
|
|
|
## Status
|
|
|
|
✅ **Complete and deployed**
|
|
|
|
- Collector AI returns structured handoff data
|
|
- Handoff data persists to Firestore on every message
|
|
- Console logging for debugging
|
|
- No linting errors
|
|
- Dev server restarted with changes
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The Collector AI now **maintains persistent checklist state** in Firestore, ensuring users never lose progress and enabling future features like:
|
|
- Auto-transitions between phases
|
|
- Visual checklist UI
|
|
- Analytics and reminders
|
|
|
|
**Status:** 🚀 **Ready for testing!**
|
|
|