feat: Add collapsible groups for AI chat conversations
Implemented mobile-first collapsible conversation groups with full group management: Backend Changes: - Added PATCH /api/v1/ai/conversations/:id/group endpoint to move conversations - Added GET /api/v1/ai/groups endpoint to list user groups - Added updateConversationGroup() service method (ai.service.ts:687-710) - Added getConversationGroups() service method (ai.service.ts:712-730) - Uses existing metadata field in AIConversation entity (no migration needed) - Updated getUserConversations() to include metadata field Frontend Changes: - Implemented collapsible group headers with Folder/FolderOpen icons - Added organizeConversations() to group by metadata.groupName (lines 243-271) - Added toggleGroupCollapse() for expand/collapse functionality (lines 273-283) - Implemented context menu with "Move to Group" and "Delete" options (lines 309-320) - Created Move to Group dialog with existing groups list (lines 858-910) - Created Create New Group dialog with text input (lines 912-952) - Mobile-first design with touch-optimized targets and smooth animations - Right-click (desktop) or long-press (mobile) for context menu - Shows conversation count per group in header - Indented conversations (pl: 5) show visual hierarchy - Groups sorted alphabetically with "Ungrouped" always last Component Growth: - Backend: ai.controller.ts (+35 lines), ai.service.ts (+43 lines) - Frontend: AIChatInterface.tsx (663 → 955 lines, +292 lines) Mobile UX Enhancements: - MoreVert icon on mobile vs Delete icon on desktop - Touch-optimized group headers (larger padding) - Smooth Collapse animations (timeout: 'auto') - Context menu replaces inline actions on small screens 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Patch,
|
||||
Body,
|
||||
Param,
|
||||
Req,
|
||||
@@ -63,6 +64,36 @@ export class AIController {
|
||||
};
|
||||
}
|
||||
|
||||
@Public() // Public for testing
|
||||
@Patch('conversations/:id/group')
|
||||
async updateConversationGroup(
|
||||
@Req() req: any,
|
||||
@Param('id') conversationId: string,
|
||||
@Body() body: { groupName: string | null },
|
||||
) {
|
||||
const userId = req.user?.userId || 'test_user_123';
|
||||
const conversation = await this.aiService.updateConversationGroup(
|
||||
userId,
|
||||
conversationId,
|
||||
body.groupName,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: { conversation },
|
||||
};
|
||||
}
|
||||
|
||||
@Public() // Public for testing
|
||||
@Get('groups')
|
||||
async getConversationGroups(@Req() req: any) {
|
||||
const userId = req.user?.userId || 'test_user_123';
|
||||
const groups = await this.aiService.getConversationGroups(userId);
|
||||
return {
|
||||
success: true,
|
||||
data: { groups },
|
||||
};
|
||||
}
|
||||
|
||||
@Public() // Public for testing
|
||||
@Get('provider-status')
|
||||
async getProviderStatus() {
|
||||
|
||||
@@ -663,7 +663,7 @@ export class AIService {
|
||||
return this.conversationRepository.find({
|
||||
where: { userId },
|
||||
order: { updatedAt: 'DESC' },
|
||||
select: ['id', 'title', 'createdAt', 'updatedAt', 'totalTokens'],
|
||||
select: ['id', 'title', 'createdAt', 'updatedAt', 'totalTokens', 'metadata'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,6 +684,51 @@ export class AIService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update conversation group
|
||||
*/
|
||||
async updateConversationGroup(
|
||||
userId: string,
|
||||
conversationId: string,
|
||||
groupName: string | null,
|
||||
): Promise<AIConversation> {
|
||||
const conversation = await this.conversationRepository.findOne({
|
||||
where: { id: conversationId, userId },
|
||||
});
|
||||
|
||||
if (!conversation) {
|
||||
throw new BadRequestException('Conversation not found');
|
||||
}
|
||||
|
||||
// Update metadata with group information
|
||||
conversation.metadata = {
|
||||
...conversation.metadata,
|
||||
groupName: groupName,
|
||||
};
|
||||
|
||||
return this.conversationRepository.save(conversation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversation groups for a user
|
||||
*/
|
||||
async getConversationGroups(userId: string): Promise<string[]> {
|
||||
const conversations = await this.conversationRepository.find({
|
||||
where: { userId },
|
||||
select: ['metadata'],
|
||||
});
|
||||
|
||||
// Extract unique group names from metadata
|
||||
const groupNames = new Set<string>();
|
||||
conversations.forEach((conv) => {
|
||||
if (conv.metadata?.groupName && typeof conv.metadata.groupName === 'string') {
|
||||
groupNames.add(conv.metadata.groupName);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(groupNames).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a title for the conversation from the first message
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user