feat: Improve file display, timezone handling, and LOT management

Changes:
- Fix datetime serialization with UTC 'Z' suffix for correct timezone display
- Add PDF upload support with extension fallback for MIME detection
- Fix LOT add/remove by creating new list for SQLAlchemy JSON change detection
- Add file message components (FileMessage, ImageLightbox, UploadPreview)
- Add multi-file upload support with progress tracking
- Link uploaded files to chat messages via message_id
- Include file attachments in AI report generation
- Update specs for file-storage, realtime-messaging, and ai-report-generation

🤖 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 12:39:15 +08:00
parent 599802b818
commit 44822a561a
36 changed files with 2252 additions and 156 deletions

View File

@@ -4,7 +4,18 @@ Contains the prompt construction logic for building the user query
sent to DIFY Chat API.
"""
from typing import List, Dict, Any
from datetime import datetime
from datetime import datetime, timezone, timedelta
# Taiwan timezone (GMT+8)
TZ_GMT8 = timezone(timedelta(hours=8))
def _to_gmt8(dt: datetime) -> datetime:
"""Convert datetime to GMT+8 timezone"""
if dt.tzinfo is None:
# Assume UTC if no timezone
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(TZ_GMT8)
INCIDENT_TYPE_MAP = {
@@ -81,11 +92,11 @@ def _format_room_info(room_data: Dict[str, Any]) -> str:
created_at = room_data.get("created_at")
if isinstance(created_at, datetime):
created_at = created_at.strftime("%Y-%m-%d %H:%M")
created_at = _to_gmt8(created_at).strftime("%Y-%m-%d %H:%M")
resolved_at = room_data.get("resolved_at")
if isinstance(resolved_at, datetime):
resolved_at = resolved_at.strftime("%Y-%m-%d %H:%M")
resolved_at = _to_gmt8(resolved_at).strftime("%Y-%m-%d %H:%M")
elif resolved_at is None:
resolved_at = "尚未解決"
@@ -145,7 +156,7 @@ def _format_messages(messages: List[Dict[str, Any]]) -> str:
created_at = msg.get("created_at")
if isinstance(created_at, datetime):
time_str = created_at.strftime("%Y-%m-%d %H:%M")
time_str = _to_gmt8(created_at).strftime("%Y-%m-%d %H:%M")
else:
time_str = str(created_at) if created_at else "未知時間"
@@ -164,26 +175,58 @@ def _format_messages(messages: List[Dict[str, Any]]) -> str:
def _format_files(files: List[Dict[str, Any]]) -> str:
"""Format file attachments section"""
"""Format file attachments section with context
Each file now includes:
- caption: User-provided description when uploading
- context_before: The message sent before this file
- context_after: The message sent after this file
This helps AI understand the context of each attachment.
"""
lines = ["## 附件清單"]
lines.append("每個附件包含上傳時的說明文字以及上下文訊息,幫助理解該附件的用途。")
lines.append("")
if not files:
lines.append("無附件")
return "\n".join(lines)
for f in files:
for i, f in enumerate(files, 1):
filename = f.get("filename", "未命名檔案")
file_type = f.get("file_type", "file")
uploader = f.get("uploader_name") or f.get("uploaded_by", "未知")
caption = f.get("caption") # User-provided description
context_before = f.get("context_before")
context_after = f.get("context_after")
uploaded_at = f.get("uploaded_at")
if isinstance(uploaded_at, datetime):
time_str = uploaded_at.strftime("%Y-%m-%d %H:%M")
time_str = _to_gmt8(uploaded_at).strftime("%Y-%m-%d %H:%M")
else:
time_str = str(uploaded_at) if uploaded_at else ""
type_label = "圖片" if file_type == "image" else "檔案"
lines.append(f"- [{type_label}] {filename} (由 {uploader}{time_str} 上傳)")
# Basic file info
lines.append(f"### 附件 {i}: {filename}")
lines.append(f"- 類型: {type_label}")
lines.append(f"- 上傳者: {uploader}")
lines.append(f"- 上傳時間: {time_str}")
# Caption/description if provided
if caption:
lines.append(f"- 說明: {caption}")
# Context messages to help AI understand when/why file was uploaded
if context_before or context_after:
lines.append("- 上下文:")
if context_before:
lines.append(f" - 前一則訊息: [{context_before['sender']}]: {context_before['content']}")
if context_after:
lines.append(f" - 後一則訊息: [{context_after['sender']}]: {context_after['content']}")
lines.append("") # Blank line between files
return "\n".join(lines)