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:
@@ -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}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user