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

@@ -0,0 +1,179 @@
# 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 的檢查機制。

View File

@@ -0,0 +1,37 @@
# Proposal: Fix Chat UX Issues
## Status: DRAFT
## Why
使用者回報三個影響使用體驗的問題:
1. **聊天室無法辨識發文者**:訊息只顯示 email (sender_id),無法快速識別是誰發的,不像 LINE 等通訊軟體會顯示使用者名稱。
2. **時間顯示錯誤**:系統使用 UTC 時間,但使用者期望看到 GMT+8 (台灣時間)。目前前端依賴瀏覽器 locale可能導致不同使用者看到不同時區的時間。
3. **AI 報告生成卡住**:點擊生成報告後一直停在「準備中」狀態,沒有進一步的回應或錯誤訊息。
## What Changes
### 1. 發文者顯示名稱
- 後端 API 在回傳訊息時加入 `sender_display_name` 欄位
-`tr_users` 表格 JOIN 取得 display_name
- 前端顯示 display_name 而非 sender_id
### 2. 統一時區為 GMT+8
- 前端建立時間格式化工具函數,統一轉換為 GMT+8
- 所有時間顯示使用該工具函數
- 後端維持 UTC 儲存(國際標準做法)
### 3. AI 報告生成問題修復
- 新增 DIFY API 連線測試端點
- 啟動時檢查 DIFY_API_KEY 是否設定
- 改善錯誤訊息顯示
- 前端新增輪詢機制確保狀態更新
## Impact
- **低風險**:不影響現有資料結構,僅新增欄位和修改顯示邏輯
- **向後相容**sender_display_name 為選填欄位,舊訊息會 fallback 顯示 sender_id
- **效能影響極小**:只增加一個 LEFT JOIN 查詢

View File

@@ -0,0 +1,59 @@
# ai-report-generation Specification
## ADDED Requirements
### Requirement: DIFY Service Health Check
The system SHALL provide a health check mechanism to verify DIFY AI service connectivity and configuration.
#### Scenario: Check DIFY configuration on startup
- **WHEN** the application starts
- **AND** `DIFY_API_KEY` is not configured
- **THEN** the system SHALL log a warning message: "DIFY_API_KEY not configured - AI report generation will be unavailable"
#### Scenario: DIFY health check endpoint
- **WHEN** a user sends `GET /api/reports/health`
- **AND** `DIFY_API_KEY` is not configured
- **THEN** the system SHALL return:
```json
{
"status": "error",
"message": "DIFY_API_KEY 未設定,請聯繫系統管理員"
}
```
#### Scenario: DIFY service unreachable
- **WHEN** a user sends `GET /api/reports/health`
- **AND** `DIFY_API_KEY` is configured
- **BUT** the DIFY service cannot be reached
- **THEN** the system SHALL return:
```json
{
"status": "error",
"message": "無法連接 AI 服務,請稍後再試"
}
```
### Requirement: Report Generation Status Polling
The frontend SHALL implement polling mechanism to ensure report status updates are received even if WebSocket connection is unstable.
#### Scenario: Poll report status after generation trigger
- **WHEN** a user triggers report generation
- **AND** receives the initial `report_id`
- **THEN** the frontend SHALL poll `GET /api/rooms/{room_id}/reports/{report_id}` every 2 seconds
- **AND** continue polling until status is "completed" or "failed"
- **AND** timeout after 120 seconds with user-friendly error message
#### Scenario: Display generation progress
- **WHEN** polling returns status "collecting_data"
- **THEN** the UI SHALL display "正在收集聊天室資料..."
- **WHEN** polling returns status "generating_content"
- **THEN** the UI SHALL display "AI 正在分析並生成報告內容..."
- **WHEN** polling returns status "assembling_document"
- **THEN** the UI SHALL display "正在組裝報告文件..."
#### Scenario: Display generation error
- **WHEN** polling returns status "failed"
- **THEN** the UI SHALL display the `error_message` from the response
- **AND** provide option to retry generation

View File

@@ -0,0 +1,37 @@
# realtime-messaging Specification
## ADDED Requirements
### Requirement: Message Sender Display Name
The system SHALL include the sender's display name in message responses and broadcasts, enabling the UI to show user-friendly names instead of email addresses.
#### Scenario: Message response includes display name
- **WHEN** a message is retrieved via REST API or WebSocket
- **THEN** the response SHALL include `sender_display_name` field
- **AND** the display name SHALL be obtained by joining with the `tr_users` table
- **AND** if the sender does not exist in `tr_users`, the field SHALL fallback to `sender_id`
#### Scenario: WebSocket broadcast includes display name
- **WHEN** a new message is broadcast via WebSocket
- **THEN** the broadcast SHALL include `sender_display_name` field
- **AND** the value SHALL be the sender's display name from `tr_users` table
#### Scenario: Historical messages include display name
- **WHEN** a client requests message history via `GET /api/rooms/{room_id}/messages`
- **THEN** each message in the response SHALL include `sender_display_name`
- **AND** messages from unknown users SHALL show their `sender_id` as fallback
### Requirement: GMT+8 Timezone Display
The frontend SHALL display all timestamps in GMT+8 (Asia/Taipei) timezone for consistent user experience across all browsers.
#### Scenario: Message timestamp in GMT+8
- **WHEN** a message is displayed in the chat room
- **THEN** the timestamp SHALL be formatted in GMT+8 timezone
- **AND** use format "HH:mm" for today's messages
- **AND** use format "MM/DD HH:mm" for older messages
#### Scenario: Room list timestamps in GMT+8
- **WHEN** the room list is displayed
- **THEN** the "last updated" time SHALL be formatted in GMT+8 timezone

View File

@@ -0,0 +1,78 @@
# Tasks: Fix Chat UX Issues
## Phase 1: 發文者顯示名稱
### T-1.1: 後端 Schema 新增 sender_display_name
- [x]`MessageResponse` schema 新增 `sender_display_name: Optional[str]` 欄位
- [x]`MessageBroadcast` schema 新增 `sender_display_name: Optional[str]` 欄位
### T-1.2: 後端 Service 查詢加入 User JOIN
- [x] 修改 `MessageService.get_messages()` 使用 LEFT JOIN 取得 display_name
- [x] 新增 `MessageService.get_display_name()` helper 方法
- [x] 修改 `MessageService.search_messages()` 使用 LEFT JOIN 取得 display_name
### T-1.3: WebSocket 廣播包含 display_name
- [x] 修改 WebSocket MESSAGE handler 查詢並包含 sender_display_name
- [x] 修改 WebSocket EDIT_MESSAGE handler 包含 sender_display_name
- [x] 修改 REST API create_message 端點包含 sender_display_name
### T-1.4: 前端顯示 display_name
- [x] 更新 `Message` 型別定義包含 `sender_display_name` 欄位
- [x] 更新 `MessageBroadcast` 型別定義包含 `sender_display_name` 欄位
- [x] 修改 `RoomDetail.tsx` 顯示 `sender_display_name || sender_id`
- [x] 修改 `useWebSocket.ts` 傳遞 `sender_display_name`
## Phase 2: 統一時區為 GMT+8
### T-2.1: 建立時間格式化工具
- [x] 建立 `frontend/src/utils/datetime.ts`
- [x] 實作 `formatDateTimeGMT8(date)` 函數
- [x] 實作 `formatTimeGMT8(date)` 函數
- [x] 實作 `formatMessageTime(date)` 函數 (智慧顯示)
- [x] 實作 `formatRelativeTimeGMT8(date)` 函數 (相對時間)
### T-2.2: 套用到所有時間顯示
- [x] `RoomDetail.tsx` - 訊息時間改用 `formatMessageTime`
- [x] `RoomList.tsx` - 最後活動時間改用 `formatRelativeTimeGMT8`
## Phase 3: AI 報告生成問題修復
### T-3.1: 後端 DIFY 健康檢查
- [x]`dify_client.py` 新增 `test_connection()` 方法
- [x] 新增 `HealthCheckResponse` schema
- [x] 新增 `GET /api/reports/health` 端點檢查 DIFY 狀態
- [x] 啟動時檢查 DIFY_API_KEY 並記錄警告
- [x] 在 main.py 註冊 health_router
### T-3.2: 改善錯誤處理與顯示
- [x] 確保 background task 錯誤正確寫入 report.error_message (已存在)
- [x] WebSocket 廣播失敗狀態時包含具體錯誤 (已存在)
### T-3.3: 前端輪詢機制
- [x] 修改 `useReports.ts` 新增 `useReportPolling()` hook
- [x] 新增 `useGenerateReportWithPolling()` hook
- [x] 修改 `RoomDetail.tsx` 實作輪詢邏輯
- [x] 顯示各階段進度訊息 (準備中 → 收集資料 → AI 生成 → 組裝文件)
- [x] 失敗時顯示錯誤訊息給使用者
### T-3.4: 報告下載中文檔名修復
- [x] 修改 `router.py` download_report 使用 RFC 5987 編碼處理中文檔名
- [x] 使用 `urllib.parse.quote()` 對檔名進行 URL 編碼
- [x] 提供 ASCII fallback 檔名相容舊版客戶端
## Phase 4: 測試與驗證
### T-4.1: 測試發文者顯示
- [ ] 測試新訊息 WebSocket 廣播包含 display_name
- [ ] 測試歷史訊息 API 回傳包含 display_name
- [ ] 測試未知使用者 (不在 users 表) fallback 顯示 sender_id
### T-4.2: 測試時區顯示
- [ ] 測試訊息時間顯示為台灣時間
- [ ] 測試跨日訊息時間正確
### T-4.3: 測試 AI 報告
- [ ] 測試 DIFY_API_KEY 未設定時的錯誤訊息
- [ ] 測試報告生成完整流程
- [ ] 測試生成失敗時的錯誤顯示
- [ ] 測試報告下載中文檔名正確顯示