various bug fixes and UI tweaks
This commit is contained in:
parent
8dcbe7efee
commit
3c9518e3ec
19 changed files with 347 additions and 189 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -15,7 +15,8 @@
|
|||
"Bash(npm run build:*)",
|
||||
"Bash(source:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
],
|
||||
"deny": []
|
||||
},
|
||||
|
|
|
|||
58
CLAUDE.md
58
CLAUDE.md
|
|
@ -3,14 +3,28 @@
|
|||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
- Build: `npm run build` (use this for all testing and verification)
|
||||
- Lint code: `npm run lint`
|
||||
- Preview production build: `npm run preview`
|
||||
- Python testing: When modifying Python files, activate the virtual environment with `source backend/venv/bin/activate` and test syntax by importing the module with `python -c "import app.services.module_name"`
|
||||
- **Build**: `npm run build` (use this for all testing and verification)
|
||||
- **Dev Build**: `npm run build:dev` (development mode build)
|
||||
- **Lint**: `npm run lint`
|
||||
- **Preview**: `npm run preview`
|
||||
- **Backend**: `cd backend && python run.py`
|
||||
|
||||
**Note**: This project is hosted on a server. Always use `npm run build` instead of `npm run dev` for testing changes.
|
||||
|
||||
**Python Testing**: After modifying any Python files in the backend, ALWAYS activate the virtual environment and test for syntax errors by attempting to import the modified module. This catches syntax errors before deployment.
|
||||
## Backend Commands
|
||||
- **Start Backend**: `python run.py` (from backend/ directory)
|
||||
- **Backend Server**: Runs on port 5137 with Hypercorn ASGI server
|
||||
- **Database**: MongoDB with PyMongo
|
||||
- **Authentication**: JWT tokens via Flask-JWT-Extended
|
||||
|
||||
## Testing
|
||||
**Python Backend**: After modifying any Python files:
|
||||
```bash
|
||||
source backend/venv/bin/activate
|
||||
python -c "import app.services.module_name" # Test specific module
|
||||
python -c "from app import create_app; app = create_app()" # Test app creation
|
||||
```
|
||||
**Frontend**: Run `npm run build` to verify TypeScript compilation
|
||||
|
||||
## Code Style Guidelines
|
||||
- **Imports**: Group imports by source (React, third-party, local)
|
||||
|
|
@ -26,4 +40,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
- **URL Construction**: ALWAYS use `import.meta.env.BASE_URL` when constructing URLs for static assets, images, or links. This project uses base path `/semblance/` in production. Example: `${import.meta.env.BASE_URL}image.png` instead of `/image.png`
|
||||
|
||||
## Project Stack
|
||||
Vite, React, TypeScript, Tailwind CSS, shadcn-ui
|
||||
**Frontend**: Vite, React 18, TypeScript, Tailwind CSS, shadcn-ui
|
||||
**Backend**: Flask 2.2.3, Hypercorn, PyMongo, JWT Extended
|
||||
**Key Libraries**:
|
||||
- UI: Radix UI components, Lucide React icons
|
||||
- State: TanStack Query, React Hook Form with Zod validation
|
||||
- Routing: React Router DOM
|
||||
- AI/LLM: OpenAI, Google Generative AI
|
||||
- Charts: Recharts
|
||||
- Drag & Drop: DND Kit
|
||||
|
||||
## API Configuration
|
||||
- **Frontend API Base**: `/semblance_back/api` (configurable via VITE_API_BASE_URL)
|
||||
- **Backend Proxy**: Vite dev server proxies `/api` to `localhost:5137`
|
||||
- **Production Base Path**: `/semblance/` (configured in vite.config.ts)
|
||||
- **Authentication**: JWT tokens stored in localStorage
|
||||
|
||||
## File Organization
|
||||
- **Backend Services**: `/backend/app/services/` - Business logic and AI integrations
|
||||
- **Backend Models**: `/backend/app/models/` - Data models (User, FocusGroup, Persona)
|
||||
- **Backend Routes**: `/backend/app/routes/` - API endpoints
|
||||
- **AI Prompts**: `/backend/prompts/` - LLM prompt templates
|
||||
- **Frontend Components**:
|
||||
- `/src/components/ui/` - Reusable shadcn-ui components
|
||||
- `/src/components/focus-group-session/` - Focus group specific components
|
||||
- `/src/components/persona/` - Persona management components
|
||||
- **Types**: `/src/types/` - TypeScript type definitions
|
||||
- **Contexts**: `/src/contexts/` - React context providers
|
||||
|
||||
## Environment
|
||||
- **Base Path**: Uses `/semblance/` in production (configured in vite.config.ts)
|
||||
- **Backend Port**: 5137 (Hypercorn ASGI server)
|
||||
- **Frontend Dev Port**: 5173
|
||||
- **Temp Directories**: Backend creates `/backend/temp/` for file handling
|
||||
BIN
backend/.DS_Store
vendored
BIN
backend/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -700,17 +700,23 @@ class AIModeratorService:
|
|||
'status': 'completed'
|
||||
})
|
||||
|
||||
# Add mode event for AI auto-completion (only for auto_complete reason)
|
||||
if reason == 'auto_complete':
|
||||
# Add mode event for AI-driven session conclusions
|
||||
# This includes auto_complete, natural_completion, discussion_guide_completed, etc.
|
||||
ai_driven_reasons = [
|
||||
'auto_complete', 'natural_completion', 'discussion_guide_completed',
|
||||
'action_limit_reached', 'time_limit'
|
||||
]
|
||||
|
||||
if reason in ai_driven_reasons:
|
||||
mode_event_id = FocusGroup.add_mode_event(
|
||||
focus_group_id=focus_group_id,
|
||||
event_type='ai_session_concluded'
|
||||
)
|
||||
|
||||
if mode_event_id:
|
||||
print(f"🎯 Added AI session concluded mode event for focus group {focus_group_id}")
|
||||
print(f"🎯 Added AI session concluded mode event for focus group {focus_group_id} (reason: {reason})")
|
||||
else:
|
||||
print(f"Warning: Failed to add AI session concluded mode event for focus group {focus_group_id}")
|
||||
print(f"Warning: Failed to add AI session concluded mode event for focus group {focus_group_id} (reason: {reason})")
|
||||
|
||||
print(f"🎬 Session ended for focus group {focus_group_id} with reason: {reason}")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class AutonomousConversationController:
|
|||
self.logger = logger or logging.getLogger(__name__)
|
||||
self.is_running = False
|
||||
self.conversation_state = "idle" # idle, running, paused, completed, error
|
||||
self.is_generating = False # Track when actively generating responses
|
||||
self.last_action_time = None
|
||||
self.action_count = 0
|
||||
self.max_actions_per_session = 500 # Safety limit
|
||||
|
|
@ -164,8 +165,20 @@ class AutonomousConversationController:
|
|||
|
||||
# Only add completion message for specific reasons (skip manual_stop)
|
||||
if reason in completion_messages:
|
||||
completion_message = completion_messages[reason]
|
||||
await self._add_moderator_message(completion_message, "system")
|
||||
# Use the AI moderator service to properly end the session with mode events
|
||||
from app.services.ai_moderator_service import AIModeratorService
|
||||
|
||||
ending_result = AIModeratorService.end_session_with_concluding_statement(
|
||||
self.focus_group_id, reason
|
||||
)
|
||||
|
||||
if "error" in ending_result:
|
||||
self.logger.error(f"Error ending session with concluding statement: {ending_result['error']}")
|
||||
# Fallback to simple message
|
||||
completion_message = completion_messages[reason]
|
||||
await self._add_moderator_message(completion_message, "system")
|
||||
else:
|
||||
self.logger.info(f"Successfully ended session with concluding statement: {ending_result.get('concluding_statement', '')[:100]}...")
|
||||
|
||||
# For discussion guide completion, ensure all items are marked as completed (100% progress)
|
||||
if reason in ["discussion_guide_completed", "natural_completion"]:
|
||||
|
|
@ -525,6 +538,7 @@ class AutonomousConversationController:
|
|||
async def _execute_participant_respond(self, details: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute participant respond action."""
|
||||
try:
|
||||
self.is_generating = True # Mark as generating
|
||||
|
||||
participant_id = details["participant_id"]
|
||||
topic_context = details["topic_context"]
|
||||
|
|
@ -542,10 +556,14 @@ class AutonomousConversationController:
|
|||
import traceback
|
||||
self.logger.error(f"❌ Full traceback: {traceback.format_exc()}")
|
||||
return {"error": error_msg}
|
||||
finally:
|
||||
self.is_generating = False # Always clear generating state
|
||||
|
||||
async def _execute_participant_interaction(self, details: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute participant interaction action."""
|
||||
try:
|
||||
self.is_generating = True # Mark as generating
|
||||
|
||||
participant_ids = details["participant_ids"]
|
||||
moderator_prompt = details["moderator_prompt"]
|
||||
|
||||
|
|
@ -565,6 +583,8 @@ class AutonomousConversationController:
|
|||
|
||||
except Exception as e:
|
||||
return {"error": f"Error in participant interaction: {str(e)}"}
|
||||
finally:
|
||||
self.is_generating = False # Always clear generating state
|
||||
|
||||
async def _execute_probe_trigger(self, details: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Execute probe trigger action."""
|
||||
|
|
@ -1035,6 +1055,7 @@ class AutonomousConversationController:
|
|||
"focus_group_id": self.focus_group_id,
|
||||
"is_running": self.is_running,
|
||||
"conversation_state": self.conversation_state,
|
||||
"is_generating": self.is_generating,
|
||||
"action_count": self.action_count,
|
||||
"last_action_time": self.last_action_time.isoformat() if self.last_action_time else None,
|
||||
"consecutive_silence_count": self.consecutive_silence_count,
|
||||
|
|
|
|||
1
dist/assets/index-ByQ3S_f0.css
vendored
1
dist/assets/index-ByQ3S_f0.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/index-DXcM4-s2.css
vendored
Normal file
1
dist/assets/index-DXcM4-s2.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -7,8 +7,8 @@
|
|||
<meta name="description" content="Lovable Generated Project" />
|
||||
<meta name="author" content="Lovable" />
|
||||
<meta property="og:image" content="/og-image.png" />
|
||||
<script type="module" crossorigin src="/semblance/assets/index-ImyDGn9B.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-ByQ3S_f0.css">
|
||||
<script type="module" crossorigin src="/semblance/assets/index-DCgbxyKr.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/semblance/assets/index-DXcM4-s2.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ interface DiscussionPanelProps {
|
|||
personas: Persona[];
|
||||
isSpeaking: boolean;
|
||||
focusGroupId: string;
|
||||
isAiModeActive?: boolean; // Whether the focus group is in AI mode
|
||||
isAiModeActive?: boolean; // Whether the focus group is in AI mode (shows continuous loading when true)
|
||||
selectedParticipantIds: string[];
|
||||
onToggleHighlight: (messageId: string) => void;
|
||||
onAdvanceDiscussion: () => void;
|
||||
|
|
@ -1103,12 +1103,18 @@ const DiscussionPanel = ({
|
|||
/>
|
||||
)
|
||||
))}
|
||||
{isTyping && (
|
||||
{(isTyping || isAiModeActive) && (
|
||||
<div className="flex items-center space-x-2 text-sm text-slate-500 animate-pulse">
|
||||
<div className="bg-primary/10 p-2 rounded-full">
|
||||
<MessageCircle className="h-4 w-4 text-primary" />
|
||||
{isAiModeActive ? (
|
||||
<Bot className="h-4 w-4 text-primary animate-spin" />
|
||||
) : (
|
||||
<MessageCircle className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<span>Generating AI response...</span>
|
||||
<span>
|
||||
{isAiModeActive ? 'AI is generating next response...' : 'Generating AI response...'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* This empty div helps with proper scroll positioning */}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
import { UserCircle, Bot, Users, Check } from 'lucide-react';
|
||||
import { Persona } from '@/types/persona';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { getPersonaAvatarSrc } from '@/utils/avatarUtils';
|
||||
import { useNavigation } from '@/contexts/NavigationContext';
|
||||
|
||||
interface ParticipantPanelProps {
|
||||
participants: Persona[];
|
||||
|
|
@ -12,10 +13,16 @@ interface ParticipantPanelProps {
|
|||
|
||||
const ParticipantPanel = ({ participants, selectedParticipantIds, onToggleParticipantFilter }: ParticipantPanelProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { id: focusGroupId } = useParams<{ id: string }>();
|
||||
const { setPreviousRoute } = useNavigation();
|
||||
|
||||
const handleAvatarClick = (participant: Persona) => {
|
||||
const personaId = participant.id || participant._id;
|
||||
if (personaId) {
|
||||
if (personaId && focusGroupId) {
|
||||
// Set navigation context so back button returns to focus group session
|
||||
setPreviousRoute(`/focus-groups/${focusGroupId}`, {
|
||||
focusGroupId: focusGroupId
|
||||
});
|
||||
navigate(`/personas/${personaId}`);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Navigation from '@/components/Navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ArrowLeft, Edit } from 'lucide-react';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@/components/ui/breadcrumb';
|
||||
import { ArrowLeft, Edit, Home, Users, User } from 'lucide-react';
|
||||
import { useNavigation } from '@/contexts/NavigationContext';
|
||||
import { focusGroupsApi } from '@/lib/api';
|
||||
|
||||
import { PersonaSidebar } from './PersonaSidebar';
|
||||
import { PersonaCooperProfile } from './PersonaCooperProfile';
|
||||
|
|
@ -25,6 +35,29 @@ export default function PersonaProfile() {
|
|||
handleSaveEdit
|
||||
} = usePersonaDetails();
|
||||
|
||||
const { navigationState } = useNavigation();
|
||||
const [focusGroupName, setFocusGroupName] = useState<string>('');
|
||||
|
||||
// Fetch focus group name if coming from focus group session
|
||||
useEffect(() => {
|
||||
if (navigationState.focusGroupId && navigationState.previousRoute?.startsWith('/focus-groups/')) {
|
||||
const fetchFocusGroupName = async () => {
|
||||
try {
|
||||
const response = await focusGroupsApi.getById(navigationState.focusGroupId);
|
||||
if (response?.data?.name) {
|
||||
setFocusGroupName(response.data.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching focus group name:', error);
|
||||
}
|
||||
};
|
||||
fetchFocusGroupName();
|
||||
}
|
||||
}, [navigationState.focusGroupId, navigationState.previousRoute]);
|
||||
|
||||
// Determine if we should show breadcrumbs
|
||||
const showBreadcrumbs = navigationState.previousRoute?.startsWith('/focus-groups/') && navigationState.focusGroupId;
|
||||
|
||||
if (isLoading) {
|
||||
return <PersonaProfileSkeleton />;
|
||||
}
|
||||
|
|
@ -46,6 +79,39 @@ export default function PersonaProfile() {
|
|||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Breadcrumbs */}
|
||||
{showBreadcrumbs && (
|
||||
<div className="mb-4">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/focus-groups" className="flex items-center">
|
||||
<Home className="h-4 w-4 mr-1" />
|
||||
Focus Groups
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
href={`/focus-groups/${navigationState.focusGroupId}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
<Users className="h-4 w-4 mr-1" />
|
||||
{focusGroupName || 'Focus Group Session'}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage className="flex items-center">
|
||||
<User className="h-4 w-4 mr-1" />
|
||||
{currentPersona?.name || 'Participant'}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mb-6 relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -178,9 +178,9 @@ export function PersonaSidebar({ persona }: PersonaSidebarProps) {
|
|||
{persona.additionalInformation.split('\n').map((line, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
{line.trim().startsWith('•') || line.trim().startsWith('-') ? (
|
||||
line.trim()
|
||||
line.trim().substring(1).trim()
|
||||
) : (
|
||||
`• ${line.trim()}`
|
||||
line.trim()
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -623,8 +623,13 @@ export function usePersonaDetails() {
|
|||
}, [id, location.search]);
|
||||
|
||||
const handleGoBack = () => {
|
||||
// Check if we came from a focus group session (participant avatar click)
|
||||
if (navigationState.previousRoute && navigationState.previousRoute.startsWith('/focus-groups/') && navigationState.focusGroupId) {
|
||||
// Navigate back to the focus group session
|
||||
navigate(`/focus-groups/${navigationState.focusGroupId}`);
|
||||
}
|
||||
// Check if we came from focus group editing
|
||||
if (navigationState.previousRoute === '/focus-groups' && navigationState.focusGroupTab) {
|
||||
else if (navigationState.previousRoute === '/focus-groups' && navigationState.focusGroupTab) {
|
||||
// Navigate back to focus group editing with the specified tab
|
||||
if (navigationState.isNewFocusGroup) {
|
||||
// For new focus groups, go to focus-groups page in create mode with the specified tab
|
||||
|
|
|
|||
|
|
@ -179,13 +179,9 @@
|
|||
}
|
||||
|
||||
.sidebar-sub-item {
|
||||
@apply ml-7 text-sm text-muted-foreground;
|
||||
@apply text-sm text-muted-foreground;
|
||||
}
|
||||
|
||||
.sidebar-sub-item::before {
|
||||
content: "•";
|
||||
@apply text-slate-400 mr-2 -ml-3;
|
||||
}
|
||||
|
||||
/* Persona card overlay styles */
|
||||
.persona-card {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ const FocusGroupSession = () => {
|
|||
const [themeGenerationComplete, setThemeGenerationComplete] = useState(false);
|
||||
const [themeGenerationError, setThemeGenerationError] = useState(false);
|
||||
|
||||
// AI mode generation state - removed since we show loading continuously during AI mode
|
||||
|
||||
// Set position dialog state
|
||||
const [setPositionDialog, setSetPositionDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
|
|
@ -166,6 +168,8 @@ const FocusGroupSession = () => {
|
|||
// Check for the expected ai_mode status
|
||||
const isActive = status === 'ai_mode';
|
||||
|
||||
// Removed individual generation checking since we show loading continuously during AI mode
|
||||
|
||||
// DEBUG: Log the status check details
|
||||
// AI Mode Status Check
|
||||
|
||||
|
|
@ -214,7 +218,7 @@ const FocusGroupSession = () => {
|
|||
console.debug('Status check error details:', errorDetails);
|
||||
|
||||
// Don't change the current state on error - return current state
|
||||
return { aiActive: isAiModeActive, sessionStatus: sessionStatus };
|
||||
return { aiActive: isAiModeActive, sessionStatus: sessionStatus, isGenerating: false };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue