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:
@@ -14,7 +14,7 @@ from app.core.email import EmailService
|
||||
from app.core.rate_limit import limiter
|
||||
from app.models.survey_invite import SurveyInvite
|
||||
from app.models.survey_response import SurveyResponse
|
||||
from app.schemas.survey import SurveyInviteStatus, SurveySubmission, SurveySubmissionResponse
|
||||
from app.schemas.survey import SurveyEmailCopyRequest, SurveyInviteStatus, SurveySubmission, SurveySubmissionResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -88,3 +88,79 @@ async def submit_survey(
|
||||
await db.commit()
|
||||
|
||||
return SurveySubmissionResponse(id=str(response.id))
|
||||
|
||||
|
||||
# Question metadata for formatting email copy
|
||||
_QUESTION_LABELS = {
|
||||
"prereqs": "Q1. Before you start troubleshooting, what info do you need?",
|
||||
"verify_fix": "Q2. After you apply a fix, how do you verify it actually worked?",
|
||||
"steps_at_a_time": "Q3. How many steps do you prefer to see at once?",
|
||||
"first_step": "Q4. \"Internet is down.\" What's your FIRST move?",
|
||||
"junior_mistake": "Q5. Most common mistake junior engineers make?",
|
||||
"pivot": "Q6. When do you stop pursuing one theory and pivot?",
|
||||
"scenario_approach": "Q7. First 3 diagnostic steps for this ticket.",
|
||||
"scenario_deeper": "Q8. Server pings fine, you can RDP in. What next?",
|
||||
"doc_pct": "Q9. Percentage of steps you actually document?",
|
||||
"go_to_commands": "Q10. Top 3 go-to PowerShell commands?",
|
||||
"secret_weapon": "Q11. Secret weapon command/tool/technique?",
|
||||
"gotcha": "Q12. Issue where the obvious diagnosis was WRONG?",
|
||||
"hard_rules": "Q13. Which rules do you follow?",
|
||||
"prioritization": "Q14. Rank factors by diagnostic priority.",
|
||||
"detail_level": "Q15. How specific should AI suggestions be?",
|
||||
"ai_personality": "Q16. What makes an AI feel like a useful colleague?",
|
||||
}
|
||||
|
||||
_QUESTION_ORDER = [
|
||||
"prereqs", "verify_fix", "steps_at_a_time", "first_step", "junior_mistake",
|
||||
"pivot", "scenario_approach", "scenario_deeper", "doc_pct", "go_to_commands",
|
||||
"secret_weapon", "gotcha", "hard_rules", "prioritization", "detail_level", "ai_personality",
|
||||
]
|
||||
|
||||
|
||||
@router.post("/survey/email-copy")
|
||||
@limiter.limit("5/hour")
|
||||
async def email_survey_copy(
|
||||
request: Request,
|
||||
data: SurveyEmailCopyRequest,
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
"""Email a copy of survey responses to the respondent."""
|
||||
from uuid import UUID as _UUID
|
||||
|
||||
try:
|
||||
resp_id = _UUID(data.response_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid response ID")
|
||||
|
||||
result = await db.execute(
|
||||
select(SurveyResponse).where(SurveyResponse.id == resp_id)
|
||||
)
|
||||
response = result.scalar_one_or_none()
|
||||
if not response:
|
||||
raise HTTPException(status_code=404, detail="Response not found")
|
||||
|
||||
# Build formatted responses for the email
|
||||
answers = response.responses or {}
|
||||
formatted_lines = []
|
||||
for qid in _QUESTION_ORDER:
|
||||
label = _QUESTION_LABELS.get(qid, qid)
|
||||
val = answers.get(qid)
|
||||
if isinstance(val, list):
|
||||
answer_str = ", ".join(str(v) for v in val)
|
||||
elif val is not None:
|
||||
answer_str = str(val)
|
||||
else:
|
||||
answer_str = "(no answer)"
|
||||
formatted_lines.append(f"{label}\n{answer_str}")
|
||||
|
||||
try:
|
||||
await EmailService.send_survey_copy_email(
|
||||
to_email=data.email,
|
||||
respondent_name=response.respondent_name,
|
||||
formatted_responses="\n\n".join(formatted_lines),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to send survey copy email to %s", data.email)
|
||||
raise HTTPException(status_code=502, detail="Failed to send email. Please try again.")
|
||||
|
||||
return {"message": "Email sent"}
|
||||
|
||||
Reference in New Issue
Block a user