feat: Add Chat UX improvements with notifications and @mention support

- Add ActionBar component with expandable toolbar for mobile
- Add @mention functionality with autocomplete dropdown
- Add browser notification system (push, sound, vibration)
- Add NotificationSettings modal for user preferences
- Add mention badges on room list cards
- Add ReportPreview with Markdown rendering and copy/download
- Add message copy functionality with hover actions
- Add backend mentions field to messages with Alembic migration
- Add lots field to rooms, remove templates
- Optimize WebSocket database session handling
- Various UX polish (animations, accessibility)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-08 08:20:37 +08:00
parent 92834dbe0e
commit 599802b818
72 changed files with 6810 additions and 702 deletions

View File

@@ -3,17 +3,14 @@
Provides functionality for creating and managing incident response chat rooms
"""
from app.modules.chat_room.router import router
from app.modules.chat_room.models import IncidentRoom, RoomMember, RoomTemplate
from app.modules.chat_room.models import IncidentRoom, RoomMember
from app.modules.chat_room.services.room_service import room_service
from app.modules.chat_room.services.membership_service import membership_service
from app.modules.chat_room.services.template_service import template_service
__all__ = [
"router",
"IncidentRoom",
"RoomMember",
"RoomTemplate",
"room_service",
"membership_service",
"template_service"
]

View File

@@ -3,11 +3,10 @@
Tables:
- tr_incident_rooms: Stores room metadata and configuration
- tr_room_members: User-room associations with roles
- tr_room_templates: Predefined templates for common incident types
Note: All tables use 'tr_' prefix to avoid conflicts in shared database.
"""
from sqlalchemy import Column, Integer, String, Text, DateTime, Enum, ForeignKey, UniqueConstraint, Index
from sqlalchemy import Column, Integer, String, Text, DateTime, Enum, ForeignKey, UniqueConstraint, Index, JSON
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
@@ -58,6 +57,7 @@ class IncidentRoom(Base):
location = Column(String(255))
description = Column(Text)
resolution_notes = Column(Text)
lots = Column(JSON, default=list, nullable=False, comment="LOT batch numbers (JSON array)")
# User tracking
created_by = Column(String(255), nullable=False) # User email/ID who created the room
@@ -111,18 +111,4 @@ class RoomMember(Base):
UniqueConstraint("room_id", "user_id", "removed_at", name="uq_tr_room_member_active"),
Index("ix_tr_room_members_room_user", "room_id", "user_id"),
Index("ix_tr_room_members_user", "user_id"),
)
class RoomTemplate(Base):
"""Predefined templates for common incident types"""
__tablename__ = "tr_room_templates"
template_id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(100), unique=True, nullable=False)
description = Column(Text)
incident_type = Column(Enum(IncidentType), nullable=False)
default_severity = Column(Enum(SeverityLevel), nullable=False)
default_members = Column(Text) # JSON array of user roles
metadata_fields = Column(Text) # JSON schema for additional fields
)

View File

@@ -13,7 +13,6 @@ from app.modules.chat_room import schemas
from app.modules.chat_room.models import MemberRole, RoomStatus
from app.modules.chat_room.services.room_service import room_service
from app.modules.chat_room.services.membership_service import membership_service
from app.modules.chat_room.services.template_service import template_service
from app.modules.chat_room.dependencies import (
get_current_room,
require_room_permission,
@@ -36,25 +35,7 @@ async def create_room(
"""Create a new incident room"""
user_email = current_user["username"]
# Check if using template
if room_data.template:
template = template_service.get_template_by_name(db, room_data.template)
if template:
room = template_service.create_room_from_template(
db,
template.template_id,
user_email,
room_data.title,
room_data.location,
room_data.description
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Template '{room_data.template}' not found"
)
else:
room = room_service.create_room(db, user_email, room_data)
room = room_service.create_room(db, user_email, room_data)
# Get user role for response
role = membership_service.get_user_role_in_room(db, room.room_id, user_email)
@@ -508,12 +489,56 @@ async def get_user_permissions(
return permissions[role]
# Template Endpoints
@router.get("/templates", response_model=List[schemas.TemplateResponse])
async def list_templates(
db: Session = Depends(get_db),
_: dict = Depends(get_current_user)
# LOT Endpoints
@router.post("/{room_id}/lots", response_model=List[str])
async def add_lot(
room_id: str,
request: schemas.AddLotRequest,
_: None = Depends(require_room_permission("update_metadata")),
db: Session = Depends(get_db)
):
"""List available room templates"""
templates = template_service.get_templates(db)
return [schemas.TemplateResponse.from_orm(t) for t in templates]
"""Add a LOT batch number to the room"""
from app.modules.chat_room.models import IncidentRoom
room = db.query(IncidentRoom).filter(IncidentRoom.room_id == room_id).first()
if not room:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found")
# Get current lots or initialize empty list
current_lots = room.lots or []
# Prevent duplicates
if request.lot not in current_lots:
current_lots.append(request.lot)
room.lots = current_lots
room.last_updated_at = datetime.utcnow()
db.commit()
db.refresh(room)
return room.lots
@router.delete("/{room_id}/lots/{lot}", response_model=List[str])
async def remove_lot(
room_id: str,
lot: str,
_: None = Depends(require_room_permission("update_metadata")),
db: Session = Depends(get_db)
):
"""Remove a LOT batch number from the room"""
from app.modules.chat_room.models import IncidentRoom
room = db.query(IncidentRoom).filter(IncidentRoom.room_id == room_id).first()
if not room:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Room not found")
current_lots = room.lots or []
if lot in current_lots:
current_lots.remove(lot)
room.lots = current_lots
room.last_updated_at = datetime.utcnow()
db.commit()
db.refresh(room)
return room.lots

View File

@@ -46,7 +46,7 @@ class CreateRoomRequest(BaseModel):
severity: SeverityLevel = Field(..., description="Severity level")
location: Optional[str] = Field(None, max_length=255, description="Incident location")
description: Optional[str] = Field(None, description="Detailed description")
template: Optional[str] = Field(None, description="Template name to use")
lots: Optional[List[str]] = Field(None, description="LOT batch numbers")
class UpdateRoomRequest(BaseModel):
@@ -57,6 +57,7 @@ class UpdateRoomRequest(BaseModel):
location: Optional[str] = Field(None, max_length=255)
description: Optional[str] = None
resolution_notes: Optional[str] = None
lots: Optional[List[str]] = Field(None, description="LOT batch numbers")
class AddMemberRequest(BaseModel):
@@ -111,6 +112,7 @@ class RoomResponse(BaseModel):
location: Optional[str] = None
description: Optional[str] = None
resolution_notes: Optional[str] = None
lots: List[str] = []
created_by: str
created_at: datetime
resolved_at: Optional[datetime] = None
@@ -137,18 +139,9 @@ class RoomListResponse(BaseModel):
offset: int
class TemplateResponse(BaseModel):
"""Room template information"""
template_id: int
name: str
description: Optional[str] = None
incident_type: IncidentType
default_severity: SeverityLevel
default_members: Optional[List[dict]] = None
metadata_fields: Optional[dict] = None
class Config:
from_attributes = True
class AddLotRequest(BaseModel):
"""Request to add a LOT to a room"""
lot: str = Field(..., min_length=1, description="LOT batch number to add")
class PermissionResponse(BaseModel):

View File

@@ -4,10 +4,8 @@ Business logic for room management operations
"""
from app.modules.chat_room.services.room_service import room_service
from app.modules.chat_room.services.membership_service import membership_service
from app.modules.chat_room.services.template_service import template_service
__all__ = [
"room_service",
"membership_service",
"template_service"
]

View File

@@ -42,6 +42,7 @@ class RoomService:
severity=room_data.severity,
location=room_data.location,
description=room_data.description,
lots=room_data.lots or [],
status=RoomStatus.ACTIVE,
created_by=user_id,
created_at=datetime.utcnow(),
@@ -219,6 +220,9 @@ class RoomService:
if updates.resolution_notes is not None:
room.resolution_notes = updates.resolution_notes
if updates.lots is not None:
room.lots = updates.lots
# Handle status transitions
if updates.status is not None:
if not self._validate_status_transition(room.status, updates.status):

View File

@@ -1,179 +0,0 @@
"""Template service for room templates
Handles business logic for room template operations
"""
from sqlalchemy.orm import Session
from typing import List, Optional
import json
from datetime import datetime
from app.modules.chat_room.models import RoomTemplate, IncidentRoom, RoomMember, IncidentType, SeverityLevel, MemberRole
from app.modules.chat_room.services.room_service import room_service
from app.modules.chat_room.services.membership_service import membership_service
class TemplateService:
"""Service for room template operations"""
def get_templates(self, db: Session) -> List[RoomTemplate]:
"""Get all available templates
Args:
db: Database session
Returns:
List of templates
"""
return db.query(RoomTemplate).all()
def get_template_by_name(
self,
db: Session,
template_name: str
) -> Optional[RoomTemplate]:
"""Get template by name
Args:
db: Database session
template_name: Template name
Returns:
Template or None if not found
"""
return db.query(RoomTemplate).filter(
RoomTemplate.name == template_name
).first()
def create_room_from_template(
self,
db: Session,
template_id: int,
user_id: str,
title: str,
location: Optional[str] = None,
description: Optional[str] = None
) -> Optional[IncidentRoom]:
"""Create a room from a template
Args:
db: Database session
template_id: Template ID
user_id: User creating the room
title: Room title
location: Optional location override
description: Optional description override
Returns:
Created room or None if template not found
"""
# Get template
template = db.query(RoomTemplate).filter(
RoomTemplate.template_id == template_id
).first()
if not template:
return None
# Create room with template defaults
room = IncidentRoom(
title=title,
incident_type=template.incident_type,
severity=template.default_severity,
location=location,
description=description or template.description,
created_by=user_id,
status="active",
created_at=datetime.utcnow(),
last_activity_at=datetime.utcnow(),
last_updated_at=datetime.utcnow(),
member_count=1
)
db.add(room)
db.flush() # Get room_id
# Add creator as owner
owner = RoomMember(
room_id=room.room_id,
user_id=user_id,
role=MemberRole.OWNER,
added_by=user_id,
added_at=datetime.utcnow()
)
db.add(owner)
# Add default members from template
if template.default_members:
try:
default_members = json.loads(template.default_members)
for member_config in default_members:
if member_config.get("user_id") != user_id: # Don't duplicate owner
member = RoomMember(
room_id=room.room_id,
user_id=member_config["user_id"],
role=member_config.get("role", MemberRole.VIEWER),
added_by=user_id,
added_at=datetime.utcnow()
)
db.add(member)
room.member_count += 1
except (json.JSONDecodeError, KeyError):
# Invalid template configuration, skip default members
pass
db.commit()
db.refresh(room)
return room
def initialize_default_templates(self, db: Session) -> None:
"""Initialize default templates if none exist
Args:
db: Database session
"""
# Check if templates already exist
existing = db.query(RoomTemplate).count()
if existing > 0:
return
# Create default templates
templates = [
RoomTemplate(
name="equipment_failure",
description="Equipment failure incident requiring immediate attention",
incident_type=IncidentType.EQUIPMENT_FAILURE,
default_severity=SeverityLevel.HIGH,
default_members=json.dumps([
{"user_id": "maintenance_team@panjit.com.tw", "role": "editor"},
{"user_id": "engineering@panjit.com.tw", "role": "viewer"}
])
),
RoomTemplate(
name="material_shortage",
description="Material shortage affecting production",
incident_type=IncidentType.MATERIAL_SHORTAGE,
default_severity=SeverityLevel.MEDIUM,
default_members=json.dumps([
{"user_id": "procurement@panjit.com.tw", "role": "editor"},
{"user_id": "logistics@panjit.com.tw", "role": "editor"}
])
),
RoomTemplate(
name="quality_issue",
description="Quality control issue requiring investigation",
incident_type=IncidentType.QUALITY_ISSUE,
default_severity=SeverityLevel.HIGH,
default_members=json.dumps([
{"user_id": "quality_team@panjit.com.tw", "role": "editor"},
{"user_id": "production_manager@panjit.com.tw", "role": "viewer"}
])
)
]
for template in templates:
db.add(template)
db.commit()
# Create singleton instance
template_service = TemplateService()