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