- 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>
180 lines
4.7 KiB
Markdown
180 lines
4.7 KiB
Markdown
# Design: Fix Chat UX Issues
|
|
|
|
## Technical Design
|
|
|
|
### 1. 發文者顯示名稱 (Sender Display Name)
|
|
|
|
#### 後端改動
|
|
|
|
**Schema 變更 (`app/modules/realtime/schemas.py`):**
|
|
```python
|
|
class MessageResponse(BaseModel):
|
|
message_id: str
|
|
room_id: str
|
|
sender_id: str
|
|
sender_display_name: Optional[str] = None # 新增欄位
|
|
content: str
|
|
# ... 其他欄位
|
|
```
|
|
|
|
**訊息查詢修改 (`app/modules/realtime/services/message_service.py`):**
|
|
```python
|
|
from app.modules.auth.models import User
|
|
|
|
# 在 get_messages() 中 JOIN users 表
|
|
messages = (
|
|
db.query(Message, User.display_name)
|
|
.outerjoin(User, Message.sender_id == User.user_id)
|
|
.filter(Message.room_id == room_id)
|
|
.order_by(desc(Message.created_at))
|
|
.limit(limit)
|
|
.all()
|
|
)
|
|
|
|
# 轉換為 response
|
|
for msg, display_name in messages:
|
|
msg_response = MessageResponse.from_orm(msg)
|
|
msg_response.sender_display_name = display_name or msg.sender_id
|
|
```
|
|
|
|
**WebSocket 廣播修改 (`app/modules/realtime/schemas.py`):**
|
|
```python
|
|
class MessageBroadcast(BaseModel):
|
|
type: str = "message"
|
|
message_id: str
|
|
sender_id: str
|
|
sender_display_name: Optional[str] = None # 新增
|
|
# ... 其他欄位
|
|
```
|
|
|
|
#### 前端改動
|
|
|
|
**RoomDetail.tsx:**
|
|
```tsx
|
|
// 將 message.sender_id 改為
|
|
{message.sender_display_name || message.sender_id}
|
|
```
|
|
|
|
### 2. 統一時區為 GMT+8
|
|
|
|
#### 前端工具函數 (`frontend/src/utils/datetime.ts`)
|
|
|
|
```typescript
|
|
/**
|
|
* 格式化日期時間為 GMT+8 (台灣時間)
|
|
*/
|
|
export function formatDateTimeGMT8(date: Date | string): string {
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
return d.toLocaleString('zh-TW', {
|
|
timeZone: 'Asia/Taipei',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 格式化時間為 GMT+8 (僅時:分)
|
|
*/
|
|
export function formatTimeGMT8(date: Date | string): string {
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
|
return d.toLocaleString('zh-TW', {
|
|
timeZone: 'Asia/Taipei',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
```
|
|
|
|
#### 使用位置
|
|
- `RoomDetail.tsx` - 訊息時間
|
|
- `RoomList.tsx` - 房間最後更新時間
|
|
- `Reports.tsx` - 報告生成時間
|
|
|
|
### 3. AI 報告生成問題修復
|
|
|
|
#### 新增健康檢查端點 (`app/modules/report_generation/router.py`)
|
|
|
|
```python
|
|
@router.get("/health", response_model=schemas.HealthCheckResponse)
|
|
async def check_dify_health():
|
|
"""檢查 DIFY AI 服務連線狀態"""
|
|
if not settings.DIFY_API_KEY:
|
|
return {"status": "error", "message": "DIFY_API_KEY 未設定"}
|
|
|
|
try:
|
|
# 測試 API 連線
|
|
result = await dify_service.test_connection()
|
|
return {"status": "ok", "message": "AI 服務正常"}
|
|
except Exception as e:
|
|
return {"status": "error", "message": str(e)}
|
|
```
|
|
|
|
#### 啟動時檢查 (`app/main.py`)
|
|
|
|
```python
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
# 檢查 DIFY API Key
|
|
if not settings.DIFY_API_KEY:
|
|
logger.warning("DIFY_API_KEY not configured - AI report generation will be unavailable")
|
|
```
|
|
|
|
#### 前端改善 (`frontend/src/hooks/useReports.ts`)
|
|
|
|
```typescript
|
|
// 新增輪詢直到報告完成或失敗
|
|
const pollReportStatus = async (reportId: string) => {
|
|
const maxAttempts = 60 // 最多輪詢 2 分鐘
|
|
let attempts = 0
|
|
|
|
while (attempts < maxAttempts) {
|
|
const status = await api.get(`/rooms/${roomId}/reports/${reportId}`)
|
|
if (status.data.status === 'completed' || status.data.status === 'failed') {
|
|
return status.data
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
attempts++
|
|
}
|
|
|
|
throw new Error('報告生成超時')
|
|
}
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
### Message Flow with Display Name
|
|
|
|
```
|
|
1. User sends message via WebSocket
|
|
2. Backend creates message in DB
|
|
3. Backend queries User table for sender's display_name
|
|
4. Backend broadcasts MessageBroadcast with sender_display_name
|
|
5. Frontend displays sender_display_name in chat bubble
|
|
```
|
|
|
|
### Report Generation Flow (Fixed)
|
|
|
|
```
|
|
1. User clicks "Generate Report"
|
|
2. Frontend: POST /api/rooms/{id}/reports/generate
|
|
3. Backend: Creates report record (status=pending)
|
|
4. Backend: Returns report_id immediately
|
|
5. Frontend: Starts polling GET /api/rooms/{id}/reports/{report_id}
|
|
6. Backend: Background task updates status (collecting_data → generating_content → assembling_document → completed)
|
|
7. Backend: Broadcasts WebSocket updates for each status change
|
|
8. Frontend: Updates UI based on poll response OR WebSocket message
|
|
9. If completed: Enable download button
|
|
10. If failed: Show error message
|
|
```
|
|
|
|
## Database Changes
|
|
|
|
無資料庫結構變更。僅新增 JOIN 查詢。
|
|
|
|
## Configuration Changes
|
|
|
|
無新增設定項目。僅改善現有 DIFY_API_KEY 的檢查機制。
|