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

@@ -7,6 +7,7 @@ FastAPI router with all report-related endpoints:
- GET /api/rooms/{room_id}/reports/{report_id}/download - Download report .docx
"""
import logging
from urllib.parse import quote
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
@@ -64,6 +65,33 @@ async def _broadcast_report_progress(
await ws_manager.broadcast_to_room(room_id, payload)
router = APIRouter(prefix="/api/rooms/{room_id}/reports", tags=["Report Generation"])
health_router = APIRouter(prefix="/api/reports", tags=["Report Generation"])
@health_router.get("/health", response_model=schemas.HealthCheckResponse)
async def check_dify_health():
"""Check DIFY AI service connection status
Returns:
Health check result with status and message
"""
if not settings.DIFY_API_KEY:
return schemas.HealthCheckResponse(
status="error",
message="DIFY_API_KEY 未設定,請聯繫系統管理員"
)
try:
result = await dify_service.test_connection()
return schemas.HealthCheckResponse(
status=result["status"],
message=result["message"]
)
except DifyAPIError as e:
return schemas.HealthCheckResponse(
status="error",
message=str(e)
)
@router.post("/generate", response_model=schemas.ReportGenerateResponse, status_code=status.HTTP_202_ACCEPTED)
@@ -202,6 +230,75 @@ async def get_report_status(
)
@router.get("/{report_id}/markdown", response_model=schemas.ReportMarkdownResponse)
async def get_report_markdown(
room_id: str,
report_id: str,
db: Session = Depends(get_db),
room_context: dict = Depends(require_room_member),
):
"""Get report content as Markdown for in-page preview
Args:
room_id: Room ID
report_id: Report ID to get markdown for
Returns:
Markdown formatted report content
"""
report = (
db.query(GeneratedReport)
.filter(
GeneratedReport.report_id == report_id,
GeneratedReport.room_id == room_id,
)
.first()
)
if not report:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Report not found",
)
if report.status != ReportStatus.COMPLETED.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Report is not ready. Status: {report.status}",
)
if not report.report_json:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Report content not found",
)
# Collect room data for markdown generation
data_service = ReportDataService(db)
room_data = data_service.collect_room_data(room_id)
if not room_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Room not found",
)
prompt_data = data_service.to_prompt_dict(room_data)
# Generate markdown
markdown_content = docx_service.to_markdown(
room_data=prompt_data["room_data"],
ai_content=report.report_json,
files=prompt_data["files"],
)
return schemas.ReportMarkdownResponse(
report_id=report.report_id,
report_title=report.report_title,
markdown=markdown_content,
)
@router.get("/{report_id}/download")
async def download_report(
room_id: str,
@@ -262,11 +359,16 @@ async def download_report(
filename = report.report_title or f"report_{report.report_id[:8]}"
filename = f"{filename}.docx"
# Use RFC 5987 format for non-ASCII filenames
# Provide ASCII fallback for older clients + UTF-8 encoded version
ascii_filename = f"report_{report.report_id[:8]}.docx"
encoded_filename = quote(filename)
return StreamingResponse(
io.BytesIO(content),
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Disposition": f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}",
},
)