feat: AI chat session conclusion + survey completion & management

AI Assistant - Conclude Session:
- 3-step modal: select outcome (resolved/escalated/paused), add notes, AI-generated summary
- AI generates structured ticket notes from conversation transcript (PSA-ready format)
- Copy to clipboard for pasting into ticketing systems
- "Resume in New Chat" for paused sessions (pre-loads context into new chat)
- Backend: POST /chats/{id}/conclude endpoint, conclusion_summary/outcome/concluded_at fields
- Migration 048: add conclusion fields to assistant_chats

Survey Completion Flow:
- Email-to-self option after submission (branded HTML email with formatted responses)
- Finish button navigates to /survey/thank-you page
- Thank you page with close-window message and feedback email callout
- Already-submitted state updated with same messaging
- Backend: POST /survey/email-copy public endpoint

Survey Admin Management:
- Read/unread indicators (cyan dot, bold name, auto-mark on expand)
- Unread count stat card
- Per-row context menu: mark read/unread, archive/unarchive, delete
- Bulk actions bar: select all, mark read/unread, archive, delete
- Show Archived toggle to filter archived responses
- Backend: 7 new admin endpoints (read, unread, archive, unarchive, delete, bulk)
- Migration 049: add is_read, archived_at to survey_responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-05 20:00:28 -05:00
parent e4c5948fbd
commit 882f67f42e
20 changed files with 1627 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
"""Pydantic schemas for standalone AI assistant chat."""
from typing import Optional, Any
from typing import Optional, Any, Literal
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
@@ -57,3 +57,14 @@ class RetentionSettingsResponse(BaseModel):
class RetentionSettingsUpdate(BaseModel):
chat_retention_days: Optional[int] = Field(None, ge=1, le=365)
chat_retention_max_count: Optional[int] = Field(None, ge=10, le=10000)
class ConcludeChatRequest(BaseModel):
outcome: Literal["resolved", "escalated", "paused"]
notes: Optional[str] = Field(None, max_length=2000)
class ConcludeChatResponse(BaseModel):
summary: str
outcome: str
concluded_at: datetime

View File

@@ -46,6 +46,12 @@ class SurveyInviteStatus(BaseModel):
status: str
class SurveyEmailCopyRequest(BaseModel):
"""Request to email a copy of responses to the respondent."""
email: str = Field(..., max_length=255)
response_id: str
class SurveyResponseDetail(BaseModel):
"""Full survey response returned to admin."""
id: str
@@ -53,6 +59,8 @@ class SurveyResponseDetail(BaseModel):
responses: dict[str, Any]
source: str
invite_name: Optional[str]
is_read: bool = False
archived_at: Optional[datetime] = None
created_at: datetime
@@ -61,3 +69,4 @@ class SurveyResponseListResponse(BaseModel):
responses: list[SurveyResponseDetail]
total: int
this_week: int
unread: int