feat: Add collapsible groups for AI chat conversations
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

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:
2025-10-02 22:29:14 +00:00
parent 9fab99da1d
commit e860b3848e
4 changed files with 448 additions and 34 deletions

View File

@@ -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() {

View File

@@ -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
*/