various bug fixes and UI tweaks

This commit is contained in:
michael 2025-08-07 16:34:37 -05:00
parent 8dcbe7efee
commit 3c9518e3ec
19 changed files with 347 additions and 189 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -15,7 +15,8 @@
"Bash(npm run build:*)",
"Bash(source:*)",
"Bash(python:*)",
"Bash(find:*)"
"Bash(find:*)",
"Bash(npx tsc:*)"
],
"deny": []
},

View file

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

Binary file not shown.

View file

@ -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}")

View file

@ -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,

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

File diff suppressed because one or more lines are too long

4
dist/index.html vendored
View file

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

View file

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

View file

@ -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}`);
}
};

View file

@ -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"

View file

@ -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>
))}

View file

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

View file

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

View file

@ -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 };
}
};