mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
- Database schema with ChatConversation + ChatMessage models - Transition contract for backward-compatible chat API - Redis lifecycle mapping and rehydration strategy - Security test matrix and performance SLOs - Rate limiting and failure mode specifications - 3-phase implementation (backend → service → UI) - Estimated timeline: 5 weekspull/6395/head
1 changed files with 601 additions and 0 deletions
@ -0,0 +1,601 @@ |
|||||
|
# Persistent Chat Widget with History Sidebar - Implementation Plan |
||||
|
|
||||
|
## Problem Statement |
||||
|
|
||||
|
Current AI chat implementation loses conversation history on page refresh and is only accessible from the portfolio/analysis page. Users need: |
||||
|
1. Persistent conversation history across sessions |
||||
|
2. Ability to switch between past conversations |
||||
|
3. Global access from any page (floating widget) |
||||
|
4. Similar UX to OpenAI's ChatGPT interface |
||||
|
|
||||
|
## Current Architecture |
||||
|
|
||||
|
**Frontend:** `apps/client/src/app/pages/portfolio/analysis/ai-chat-panel/` |
||||
|
- Embedded in portfolio analysis page only |
||||
|
- Component state for messages (lost on refresh) |
||||
|
- localStorage: `gf_ai_chat_messages`, `gf_ai_chat_session_id` |
||||
|
- Max 200 messages in localStorage (recently added) |
||||
|
|
||||
|
**Backend:** `apps/api/src/app/endpoints/ai/ai.controller.ts` |
||||
|
- `POST /ai/chat` - single conversation endpoint |
||||
|
- `POST /ai/chat/feedback` - feedback submission |
||||
|
- Redis memory: 10 turns, 24h TTL per session |
||||
|
|
||||
|
**Limitations:** |
||||
|
- No cross-session persistence |
||||
|
- No database storage |
||||
|
- Single conversation only |
||||
|
- Page-scoped (not global) |
||||
|
|
||||
|
## Solution Architecture |
||||
|
|
||||
|
### Ticket-Ready Deliverables (Must Close Before Build) |
||||
|
|
||||
|
- [ ] Define `POST /ai/chat` transition contract: request/response schema, compatibility window, and deprecation path. |
||||
|
- [ ] Harden Prisma design: role enum, composite conversation list index, and conversation-to-memory session mapping field. |
||||
|
- [ ] Define Redis lifecycle policy: keying, TTL behavior, memory rehydration behavior, and delete semantics. |
||||
|
- [ ] Add security test matrix: cross-user conversation access, ownership checks, and expected status codes (`403`/`404`). |
||||
|
- [ ] Define measurable performance SLOs and regression gates (p95 latency + pagination/query budgets). |
||||
|
- [ ] Define failure-mode behavior for DB/Redis outages, retry policy, and UI degraded-state signaling. |
||||
|
- [ ] Define API rate limits and `429` response contract for chat and conversation endpoints. |
||||
|
- [ ] Define search implementation details (fields, indexing strategy, query behavior). |
||||
|
- [ ] Define responsive UX specifications (desktop/tablet/mobile layout and collision behavior). |
||||
|
|
||||
|
### Phase 1: Database Schema & Backend |
||||
|
|
||||
|
#### 1.1 Prisma Schema Extensions |
||||
|
|
||||
|
```prisma |
||||
|
// prisma/schema.prisma |
||||
|
|
||||
|
model ChatConversation { |
||||
|
id String @id @default(cuid()) |
||||
|
userId String |
||||
|
memorySessionId String @unique |
||||
|
title String? |
||||
|
createdAt DateTime @default(now()) |
||||
|
updatedAt DateTime @updatedAt |
||||
|
messages ChatMessage[] |
||||
|
user User @relation(fields: [userId], references: [id]) |
||||
|
|
||||
|
@@index([userId, updatedAt]) |
||||
|
@@index([userId, createdAt]) |
||||
|
} |
||||
|
|
||||
|
model ChatMessage { |
||||
|
id String @id @default(cuid()) |
||||
|
conversationId String |
||||
|
role ChatMessageRole |
||||
|
content String @db.Text |
||||
|
metadata Json? // Store response data (toolCalls, verification, etc.) |
||||
|
createdAt DateTime @default(now()) |
||||
|
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) |
||||
|
|
||||
|
@@index([conversationId, createdAt]) |
||||
|
} |
||||
|
|
||||
|
enum ChatMessageRole { |
||||
|
USER |
||||
|
ASSISTANT |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 1.2 Backend Changes |
||||
|
|
||||
|
**New DTOs:** `apps/api/src/app/endpoints/ai/ai-chat-conversation.dto.ts` |
||||
|
```typescript |
||||
|
export class CreateConversationDto { |
||||
|
title?: string; |
||||
|
} |
||||
|
|
||||
|
export class UpdateConversationDto { |
||||
|
title?: string; |
||||
|
} |
||||
|
|
||||
|
export class ListConversationsQueryDto { |
||||
|
limit?: number = 20; |
||||
|
offset?: number = 0; |
||||
|
search?: string; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**New Service:** `apps/api/src/app/endpoints/ai/ai-chat-conversation.service.ts` |
||||
|
- `createConversation(userId, title?)` |
||||
|
- `listConversations(userId, pagination)` |
||||
|
- `getConversation(id, userId)` |
||||
|
- `listMessages(conversationId, userId, pagination)` |
||||
|
- `deleteConversation(id, userId)` |
||||
|
- `updateConversation(id, data, userId)` |
||||
|
|
||||
|
**Updated Controller:** `apps/api/src/app/endpoints/ai/ai.controller.ts` |
||||
|
```typescript |
||||
|
@Post('conversations') |
||||
|
createConversation(@Body() dto: CreateConversationDto) |
||||
|
|
||||
|
@Get('conversations') |
||||
|
listConversations(@Query() query: ListConversationsQueryDto) |
||||
|
|
||||
|
@Get('conversations/:id') |
||||
|
getConversation(@Param('id') id: string) |
||||
|
|
||||
|
@Get('conversations/:id/messages') |
||||
|
listMessages(@Param('id') id: string, @Query() query: ListMessagesQueryDto) |
||||
|
|
||||
|
@Delete('conversations/:id') |
||||
|
deleteConversation(@Param('id') id: string) |
||||
|
|
||||
|
@Patch('conversations/:id') |
||||
|
updateConversation(@Param('id') id: string, @Body() dto: UpdateConversationDto) |
||||
|
``` |
||||
|
|
||||
|
**Transition Contract (Backward-Compatible):** |
||||
|
```typescript |
||||
|
// Existing endpoint stays active during migration window |
||||
|
@Post('chat') |
||||
|
chat(@Body() dto: AiChatDto & { conversationId?: string }) |
||||
|
``` |
||||
|
|
||||
|
- Request accepts both `conversationId` and legacy `sessionId`. |
||||
|
- Resolution order: `conversationId` first, `sessionId` fallback, then create new conversation. |
||||
|
- Response includes `conversationId` and keeps `memory.sessionId` for compatibility with existing clients. |
||||
|
- Deprecation: `sessionId` request usage marked deprecated after widget rollout + one release cycle. |
||||
|
|
||||
|
**Updated AI Service:** `apps/api/src/app/endpoints/ai/ai.service.ts` |
||||
|
- Resolve active conversation using transition contract (`conversationId` / `sessionId`) |
||||
|
- Auto-create conversation on first message if no conversation mapping exists |
||||
|
- Save all user/assistant messages to database after response completes |
||||
|
- Load recent conversation messages when resuming and rehydrate memory when Redis session expires |
||||
|
- Keep Redis memory isolated by user + memorySessionId to support safe conversation switching |
||||
|
|
||||
|
#### 1.3 Redis Lifecycle Mapping |
||||
|
|
||||
|
- Memory key format remains: `ai-agent-memory-${userId}-${memorySessionId}` |
||||
|
- `ChatConversation.memorySessionId` is the canonical mapping from conversation to Redis memory. |
||||
|
- Conversation delete flow removes: |
||||
|
- `ChatConversation` row (cascade `ChatMessage`) |
||||
|
- Redis key for `memorySessionId` |
||||
|
- Expired Redis memory triggers rehydration from last `N` persisted messages for that conversation. |
||||
|
- Redis TTL remains 24h for memory state; persisted DB history remains source of truth. |
||||
|
|
||||
|
#### 1.4 Error Handling and Degraded Modes |
||||
|
|
||||
|
- **DB unavailable on `POST /ai/chat`:** |
||||
|
- Continue assistant response using Redis memory when available. |
||||
|
- Return `historyPersistence: 'degraded'` in response metadata. |
||||
|
- Queue unsaved message pair for retry with exponential backoff (`1s, 2s, 4s, 8s, 16s`, max 5 attempts). |
||||
|
- Client shows non-blocking banner: `History sync delayed. Retrying.` |
||||
|
- **Redis unavailable on `POST /ai/chat`:** |
||||
|
- Rebuild prompt context from last 10 persisted conversation messages in DB. |
||||
|
- Return `memorySource: 'database_fallback'` in response metadata. |
||||
|
- **DB and Redis unavailable:** |
||||
|
- Return `503 Service Unavailable` with `Retry-After`. |
||||
|
- Keep current input in client draft box for user retry. |
||||
|
- **Network timeout from client to API:** |
||||
|
- Show pending state (`Sending...`) for request timeout window. |
||||
|
- Retry once automatically in background, then surface actionable error. |
||||
|
|
||||
|
#### 1.5 Rate Limiting |
||||
|
|
||||
|
- `POST /ai/conversations`: max **50 conversations per user/day**. |
||||
|
- `POST /ai/chat`: max **100 messages per conversation/hour** and **300 messages per user/hour**. |
||||
|
- `POST /ai/chat/feedback`: max **30 feedback events per user/hour**. |
||||
|
- Enforce at API layer and return `429` with payload: |
||||
|
- `code: 'RATE_LIMIT_EXCEEDED'` |
||||
|
- `scope: 'conversation' | 'user' | 'feedback'` |
||||
|
- `retryAfterSeconds: number` |
||||
|
|
||||
|
### Phase 2: Frontend Service |
||||
|
|
||||
|
#### 2.1 New AI Chat Service |
||||
|
|
||||
|
**File:** `apps/client/src/app/services/ai-chat.service.ts` |
||||
|
|
||||
|
```typescript |
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class AiChatService { |
||||
|
private currentConversation$ = new BehaviorSubject<ChatConversation | null>(null); |
||||
|
private conversations$ = new BehaviorSubject<ChatConversation[]>([]); |
||||
|
|
||||
|
// Conversation CRUD |
||||
|
createConversation(title?: string): Observable<ChatConversation> |
||||
|
loadConversations(pagination?: ListConversationsQuery): Observable<ChatConversation[]> |
||||
|
loadConversation(id: string): Observable<ChatConversation> |
||||
|
deleteConversation(id: string): Observable<void> |
||||
|
updateConversation(id: string, data: UpdateConversationDto): Observable<ChatConversation> |
||||
|
|
||||
|
// Messages |
||||
|
sendMessage(conversationId: string, query: string): Observable<AiAgentChatResponse> |
||||
|
loadMessages(conversationId: string): Observable<AiChatMessage[]> |
||||
|
|
||||
|
// State |
||||
|
getCurrentConversation(): Observable<ChatConversation | null> |
||||
|
getConversations(): Observable<ChatConversation[]> |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 2.2 Enhanced localStorage Structure |
||||
|
|
||||
|
```typescript |
||||
|
interface ChatStorageState { |
||||
|
currentConversationId: string; |
||||
|
conversations: Array<{ |
||||
|
id: string; |
||||
|
title?: string; |
||||
|
lastUpdated: Date; |
||||
|
messageCount: number; |
||||
|
}>; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 2.3 Search Specification |
||||
|
|
||||
|
- Search scope: conversation `title` + rolling preview text built from the first 3 user messages. |
||||
|
- Data model support: |
||||
|
- Add `searchText` field on `ChatConversation`. |
||||
|
- Update `searchText` when title changes or one of first 3 user messages changes. |
||||
|
- Query strategy: |
||||
|
- PostgreSQL full-text search: `to_tsvector('english', searchText)` + `plainto_tsquery`. |
||||
|
- Add `GIN` index via SQL migration for `searchText` vector expression. |
||||
|
- API behavior: |
||||
|
- Debounce 300ms on client before query. |
||||
|
- Return top 20 matches ordered by rank, then by `updatedAt DESC`. |
||||
|
|
||||
|
### Phase 3: UI Components |
||||
|
|
||||
|
#### 3.1 Global Chat Widget |
||||
|
|
||||
|
**Location:** `apps/client/src/app/components/global/ai-chat-widget/` |
||||
|
|
||||
|
**Files:** |
||||
|
- `ai-chat-widget.component.ts` - Main widget controller |
||||
|
- `ai-chat-widget.component.scss` - Fixed positioning styles |
||||
|
- `ai-chat-widget.component.html` - Template |
||||
|
|
||||
|
**Key Features:** |
||||
|
```typescript |
||||
|
@Component({ |
||||
|
selector: 'gf-ai-chat-widget', |
||||
|
template: ` |
||||
|
<button class="chat-toggle" (click)="toggleChat()"> |
||||
|
<mat-icon>chat</mat-icon> |
||||
|
</button> |
||||
|
|
||||
|
<div class="chat-container" *ngIf="isOpen"> |
||||
|
<gf-ai-chat-sidebar |
||||
|
[conversations]="conversations" |
||||
|
(conversationSelected)="loadConversation($event)" |
||||
|
(newChat)="startNewChat()" |
||||
|
></gf-ai-chat-sidebar> |
||||
|
|
||||
|
<gf-ai-chat-panel |
||||
|
[conversationId]="currentConversationId" |
||||
|
[messages]="messages" |
||||
|
(messageSent)="handleMessage($event)" |
||||
|
></gf-ai-chat-panel> |
||||
|
</div> |
||||
|
`, |
||||
|
styles: [` |
||||
|
.chat-toggle { |
||||
|
position: fixed; |
||||
|
bottom: 24px; |
||||
|
right: 24px; |
||||
|
z-index: 1000; |
||||
|
} |
||||
|
.chat-container { |
||||
|
position: fixed; |
||||
|
bottom: 80px; |
||||
|
right: 24px; |
||||
|
width: 800px; |
||||
|
height: 600px; |
||||
|
z-index: 1000; |
||||
|
} |
||||
|
`] |
||||
|
}) |
||||
|
export class GfAiChatWidgetComponent { |
||||
|
isOpen = false; |
||||
|
conversations$ = this.aiChatService.getConversations(); |
||||
|
// ... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 3.2 Chat History Sidebar |
||||
|
|
||||
|
**Location:** `apps/client/src/app/components/global/ai-chat-widget/ai-chat-sidebar/` |
||||
|
|
||||
|
**Files:** |
||||
|
- `ai-chat-sidebar.component.ts` |
||||
|
- `ai-chat-sidebar.component.scss` |
||||
|
- `ai-chat-sidebar.component.html` |
||||
|
|
||||
|
**Features:** |
||||
|
- List of conversations with titles |
||||
|
- Search/filter |
||||
|
- New chat button |
||||
|
- Delete conversation option |
||||
|
- Auto-generate titles from first message |
||||
|
|
||||
|
#### 3.3 Responsive UX Specification |
||||
|
|
||||
|
- **Desktop (`>= 1200px`)**: |
||||
|
- Floating widget anchored bottom-right. |
||||
|
- Container target width `800px`, height `600px`. |
||||
|
- Sidebar default open. |
||||
|
- **Tablet (`768px - 1199px`)**: |
||||
|
- Floating widget anchored bottom-right. |
||||
|
- Container target width `600px`, height `70vh`. |
||||
|
- Sidebar collapsible and closed by default. |
||||
|
- **Mobile (`< 768px`)**: |
||||
|
- Full-screen modal overlay. |
||||
|
- Slide-out sidebar with explicit back button. |
||||
|
- Input composer fixed to bottom safe area. |
||||
|
- **Collision behavior**: |
||||
|
- Widget offsets from existing floating action buttons. |
||||
|
- Highest overlay z-index in app shell layer. |
||||
|
- Escape key and backdrop click close behavior defined consistently. |
||||
|
|
||||
|
### Phase 4: Migration & Integration |
||||
|
|
||||
|
#### 4.1 Refactor Existing Chat Panel |
||||
|
|
||||
|
**Current:** `apps/client/src/app/pages/portfolio/analysis/ai-chat-panel/ai-chat-panel.component.ts` |
||||
|
|
||||
|
**Changes:** |
||||
|
- Remove localStorage logic (move to service) |
||||
|
- Use `AiChatService` for all operations |
||||
|
- Keep as embedded option in portfolio page |
||||
|
- Share service with global widget |
||||
|
|
||||
|
#### 4.2 Global Integration |
||||
|
|
||||
|
**File:** `apps/client/src/app/app.component.ts` |
||||
|
|
||||
|
```typescript |
||||
|
@Component({ |
||||
|
selector: 'gf-root', |
||||
|
template: ` |
||||
|
<router-outlet></router-outlet> |
||||
|
<gf-ai-chat-widget></gf-ai-chat-widget> |
||||
|
` |
||||
|
}) |
||||
|
export class AppComponent {} |
||||
|
``` |
||||
|
|
||||
|
#### 4.3 Permission & Feature Flag |
||||
|
|
||||
|
**Service:** Check user permissions before showing widget |
||||
|
```typescript |
||||
|
if (this.user.hasPermissionToReadAiPrompt) { |
||||
|
showChatWidget = true; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Implementation Order |
||||
|
|
||||
|
### Week 0: Contract + Safety Baseline |
||||
|
1. Finalize `POST /ai/chat` compatibility contract (`conversationId`, legacy `sessionId`, response fields). |
||||
|
2. Finalize Redis lifecycle mapping and delete semantics. |
||||
|
3. Finalize Prisma enums/indexes and migration plan. |
||||
|
4. Finalize degraded-mode behavior for DB/Redis/network failure paths. |
||||
|
5. Finalize API rate-limit policy and `429` response payload contract. |
||||
|
6. Finalize search implementation spec and indexing plan. |
||||
|
7. Finalize responsive UX specs for desktop/tablet/mobile. |
||||
|
8. Define measurable SLOs and add regression gate placeholders. |
||||
|
9. Define security test matrix for ownership and cross-user access. |
||||
|
|
||||
|
### Week 1: Backend Foundation |
||||
|
1. Add Prisma models + migration |
||||
|
2. Create conversation service |
||||
|
3. Add controller endpoints |
||||
|
4. Update AI service to persist messages |
||||
|
5. Write tests for conversation CRUD |
||||
|
|
||||
|
### Week 2: Frontend Service |
||||
|
1. Create `AiChatService` with API integration |
||||
|
2. Migrate existing chat panel to use service |
||||
|
3. Add conversation loading/saving logic |
||||
|
4. Update localStorage structure |
||||
|
5. Test service in isolation |
||||
|
|
||||
|
### Week 3: UI Components |
||||
|
1. Build chat sidebar component |
||||
|
2. Extract chat panel logic to shared component |
||||
|
3. Create global chat widget wrapper |
||||
|
4. Add to app component |
||||
|
5. Implement conversation switching |
||||
|
6. Polish UI (animations, responsive design) |
||||
|
|
||||
|
## Acceptance Criteria |
||||
|
|
||||
|
- [ ] Widget toggle button visible on all pages (with permission) |
||||
|
- [ ] Clicking toggle opens/closes floating chat window |
||||
|
- [ ] Sidebar shows list of past conversations (title + timestamp) |
||||
|
- [ ] Clicking conversation loads it from database |
||||
|
- [ ] "New chat" button clears and starts fresh conversation |
||||
|
- [ ] All conversations persist across page refreshes |
||||
|
- [ ] All conversations persist across browser sessions |
||||
|
- [ ] Conversations sync across devices (database-backed) |
||||
|
- [ ] Search/filter conversations by title |
||||
|
- [ ] Delete individual conversations |
||||
|
- [ ] Rename conversations |
||||
|
- [ ] Existing portfolio chat still works embedded |
||||
|
- [ ] `POST /ai/chat` remains backward-compatible for existing `sessionId` clients during transition |
||||
|
- [ ] Conversation endpoints enforce ownership checks (cross-user IDs blocked with `403`/`404`) |
||||
|
- [ ] Conversation list query uses indexed path and meets p95 target |
||||
|
- [ ] Conversation message load is paginated and meets p95 target |
||||
|
- [ ] Redis delete + rehydration behavior is deterministic and covered by tests |
||||
|
- [ ] DB and Redis failure modes follow documented degraded behavior and user-facing status messaging |
||||
|
- [ ] API rate limits enforced with deterministic `429` contract for each scoped limit |
||||
|
- [ ] Search returns ranked matches over title + first-message preview text |
||||
|
- [ ] Widget behavior matches desktop/tablet/mobile UX specification |
||||
|
|
||||
|
## Testing Strategy |
||||
|
|
||||
|
### Backend Tests |
||||
|
```typescript |
||||
|
// ai-chat-conversation.service.spec.ts |
||||
|
describe('AiChatConversationService', () => { |
||||
|
it('should create conversation with auto-generated title'); |
||||
|
it('should list conversations for user'); |
||||
|
it('should paginate results'); |
||||
|
it('should delete conversation and cascade messages'); |
||||
|
it('should reject cross-user conversation access'); |
||||
|
it('should use memorySessionId mapping for Redis lifecycle'); |
||||
|
}); |
||||
|
|
||||
|
// ai.controller.spec.ts |
||||
|
describe('AiController chat transition contract', () => { |
||||
|
it('should accept conversationId on POST /ai/chat'); |
||||
|
it('should accept legacy sessionId on POST /ai/chat'); |
||||
|
it('should return conversationId and memory.sessionId in response'); |
||||
|
it('should return deterministic 429 payload when rate limits are exceeded'); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### Frontend Tests |
||||
|
```typescript |
||||
|
// ai-chat.service.spec.ts |
||||
|
describe('AiChatService', () => { |
||||
|
it('should sync with localStorage'); |
||||
|
it('should handle network errors gracefully'); |
||||
|
it('should cache conversations locally'); |
||||
|
}); |
||||
|
|
||||
|
// ai-chat-widget.component.spec.ts |
||||
|
describe('GfAiChatWidgetComponent', () => { |
||||
|
it('should toggle open/closed'); |
||||
|
it('should load conversation from service'); |
||||
|
it('should create new conversation'); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### E2E Tests |
||||
|
- Create conversation → refresh → verify persists |
||||
|
- Switch between conversations → verify correct context |
||||
|
- Delete conversation → verify removed from list |
||||
|
- Search conversations → verify filtering |
||||
|
- Attempt to load another user's conversation ID → verify `403`/`404` |
||||
|
- Continue old `sessionId` chat flow after deploy → verify compatibility |
||||
|
- Expire Redis key for active conversation → verify memory rehydration from persisted messages |
||||
|
- Simulate DB write outage on send → verify degraded status + retry behavior |
||||
|
- Simulate Redis outage on send → verify DB-context fallback path |
||||
|
- Exceed per-conversation and per-user limits → verify `429` payload and retry timing |
||||
|
- Validate search ranking over title + first-message preview text |
||||
|
- Validate desktop/tablet/mobile widget layouts and sidebar behavior |
||||
|
|
||||
|
### Manual Verification |
||||
|
- Send message in widget and embedded panel → latest message renders first (reverse chronological UI behavior). |
||||
|
- Rate assistant response after list reordering → feedback applies to the correct assistant message. |
||||
|
- Delete conversation and refresh page → conversation remains deleted and does not reappear. |
||||
|
- Force API timeout from browser devtools and verify pending + retry + actionable error messaging. |
||||
|
|
||||
|
## Performance Targets (SLOs) |
||||
|
|
||||
|
- Conversation list (`GET /ai/conversations`, limit 50): p95 API latency < 250ms (excluding network RTT). |
||||
|
- Message page load (`GET /ai/conversations/:id/messages`, limit 100): p95 API latency < 350ms. |
||||
|
- Chat send overhead from persistence layer (`POST /ai/chat`): additional p95 < 120ms over current baseline. |
||||
|
- Widget open to first message paint (cached list path): p95 < 400ms on desktop local benchmark. |
||||
|
- DB query budget: |
||||
|
- List conversations: max 2 queries/request |
||||
|
- Load messages page: max 2 queries/request |
||||
|
- Send message persist path: max 4 queries/request |
||||
|
|
||||
|
## Performance Strategies |
||||
|
|
||||
|
- **localStorage caching**: Keep lightweight conversation summary cache locally. |
||||
|
- **Lazy loading**: Load messages on demand by conversation. |
||||
|
- **Pagination defaults**: 50 conversations, 100 messages per page. |
||||
|
- **Debounced search**: 300ms debounce before API search. |
||||
|
- **Optimistic UI**: Update local list immediately, reconcile with server response. |
||||
|
- **Backpressure**: Rate-limit enforcement protects API and LLM spend under burst traffic. |
||||
|
- **Degraded mode retries**: Background sync retries prevent data loss on transient failures. |
||||
|
|
||||
|
## Migration Strategy |
||||
|
|
||||
|
### Database Migration |
||||
|
```bash |
||||
|
# Create new tables |
||||
|
pnpm nx run api:prisma:migrate |
||||
|
|
||||
|
# Backfill existing Redis sessions (optional) |
||||
|
# Script to load active Redis sessions into database |
||||
|
``` |
||||
|
|
||||
|
### Zero-Downtime Deployment |
||||
|
1. Deploy backend contract changes first (`conversationId` + legacy `sessionId` support). |
||||
|
2. Keep existing frontend on legacy `sessionId` flow. |
||||
|
3. Deploy frontend widget/service using conversation APIs. |
||||
|
4. Enable feature flag for a subset of users and monitor latency/error/security metrics. |
||||
|
5. Remove legacy `sessionId` client usage after one stable release cycle. |
||||
|
|
||||
|
## Rollback Plan |
||||
|
|
||||
|
If critical issues arise: |
||||
|
1. Frontend: Fall back to localStorage-only mode |
||||
|
2. Backend: Keep old endpoints functional |
||||
|
3. Feature flag: Disable global widget |
||||
|
4. Database: Keep existing data, revert schema changes |
||||
|
|
||||
|
## Open Questions |
||||
|
|
||||
|
1. Should we store message metadata (toolCalls, verification) in JSON or separate tables? |
||||
|
- **Decision**: JSON for simplicity (RDBMS not optimal for sparse data) |
||||
|
|
||||
|
2. Should we auto-generate conversation titles? |
||||
|
- **Decision**: Yes, use first 50 chars of first user message |
||||
|
|
||||
|
3. Should conversations be shareable (like ChatGPT)? |
||||
|
- **Decision**: No, private-only for MVP |
||||
|
|
||||
|
4. Max messages per conversation? |
||||
|
- **Decision**: No limit (database), but lazy load last 100 |
||||
|
|
||||
|
5. Should we export conversations? |
||||
|
- **Decision**: Future enhancement, not MVP |
||||
|
|
||||
|
## Files to Create |
||||
|
|
||||
|
1. `apps/api/src/app/endpoints/ai/ai-chat-conversation.dto.ts` |
||||
|
2. `apps/api/src/app/endpoints/ai/ai-chat-conversation.service.ts` |
||||
|
3. `apps/api/src/app/endpoints/ai/ai-chat-conversation.controller.ts` |
||||
|
4. `apps/client/src/app/services/ai-chat.service.ts` |
||||
|
5. `apps/client/src/app/components/global/ai-chat-widget/` |
||||
|
6. `apps/client/src/app/components/global/ai-chat-widget/ai-chat-widget.component.ts` |
||||
|
7. `apps/client/src/app/components/global/ai-chat-widget/ai-chat-sidebar/` |
||||
|
|
||||
|
## Files to Modify |
||||
|
|
||||
|
1. `prisma/schema.prisma` |
||||
|
2. `apps/api/src/app/endpoints/ai/ai.service.ts` |
||||
|
3. `apps/api/src/app/endpoints/ai/ai.controller.ts` |
||||
|
4. `apps/client/src/app/pages/portfolio/analysis/ai-chat-panel/ai-chat-panel.component.ts` |
||||
|
5. `apps/client/src/app/app.component.ts` |
||||
|
|
||||
|
## Estimated Effort |
||||
|
|
||||
|
- **Week 0 hardening**: 3-4 days |
||||
|
- **Backend implementation**: 4-6 days |
||||
|
- **Frontend service + search**: 4-5 days |
||||
|
- **UI components + responsive UX**: 4-6 days |
||||
|
- **Testing + stabilization**: 4-5 days |
||||
|
|
||||
|
**Total**: ~19-26 days (4-5 weeks) |
||||
|
|
||||
|
### Scope Control Option (If 3 Weeks Is Required) |
||||
|
|
||||
|
- Keep transition contract, Redis mapping, security tests, and core SLOs. |
||||
|
- Defer full-text search to title-only search for MVP. |
||||
|
- Defer tablet-specific layout polish to post-MVP. |
||||
|
|
||||
|
## Plan Confidence |
||||
|
|
||||
|
- **Architecture confidence**: Medium-high (strong phase structure, explicit rollout, measurable SLOs). |
||||
|
- **Primary risks**: Transition contract drift and Redis lifecycle edge cases. |
||||
|
- **Risk control basis**: Ownership tests, compatibility tests, and perf/security gates in CI. |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. Convert the five ticket-ready deliverables into Linear sub-tasks with owners. |
||||
|
2. Approve transition contract and Redis lifecycle spec before schema migration. |
||||
|
3. Implement Week 0 hardening items and merge behind feature flag. |
||||
|
4. Execute Week 1-3 phases with SLO/security gates active in CI. |
||||
|
5. Run rollout checklist and remove legacy `sessionId` path after stability window. |
||||
Loading…
Reference in new issue