feat: AI chat conclusion + survey completion & management (#95)
* fix: increase assistant chat input height from 1 to 3 rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Anthropic prompt caching to assistant chat Cache the static system prompt and conversation history prefix across turns, reducing input token costs by ~80% on multi-turn conversations. RAG context is intentionally uncached since it changes per query. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Microsoft Learn MCP integration + refine assistant system prompt - Integrate Microsoft Learn MCP server via Anthropic's MCP connector for real-time documentation lookups (docs search, fetch, code samples) - Refine system prompt: clear persona, structured answer guidelines, when to use RAG flows vs Microsoft Learn, guardrails against fabrication - Add ENABLE_MCP_MICROSOFT_LEARN config toggle (default: True) - Fix bugs from prior edit: wrong MCP URL, broken indentation, undefined usage/token variables, NOT_GIVEN for disabled MCP params - Log MCP tool usage and cache performance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 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> * fix: initialize VerifyEmailPage state from token to avoid setState in effect Moves the no-token error case from useEffect into initial state to satisfy the react-hooks/set-state-in-effect ESLint rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #95.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||
|
||||
// ── Survey Data Types ──
|
||||
@@ -147,6 +147,12 @@ export default function SurveyPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isComplete, setIsComplete] = useState(false)
|
||||
const [submitError, setSubmitError] = useState('')
|
||||
const [emailInput, setEmailInput] = useState('')
|
||||
const [emailSending, setEmailSending] = useState(false)
|
||||
const [emailSent, setEmailSent] = useState(false)
|
||||
const [emailError, setEmailError] = useState('')
|
||||
const [responseId, setResponseId] = useState<string | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
const token = searchParams.get('t')
|
||||
@@ -203,6 +209,8 @@ export default function SurveyPage() {
|
||||
const errData = await res.json().catch(() => null)
|
||||
throw new Error(errData?.detail || `Submission failed (${res.status})`)
|
||||
}
|
||||
const data = await res.json()
|
||||
setResponseId(data.id)
|
||||
setIsComplete(true)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
} catch (err) {
|
||||
@@ -257,9 +265,20 @@ export default function SurveyPage() {
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<h2 className="font-heading text-2xl font-bold mb-2.5">Already Submitted</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto leading-relaxed">
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto leading-relaxed mb-3">
|
||||
{inviteName ? `Thanks ${inviteName} — y` : 'Y'}our response has already been recorded. We appreciate your time!
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70 max-w-[400px] mx-auto leading-relaxed mb-8">
|
||||
You can safely close this browser window now.
|
||||
</p>
|
||||
<div className="glass-card-static p-5 max-w-[400px] mx-auto text-center">
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Have feedback unrelated to the survey?{' '}
|
||||
<a href="mailto:feedback@resolutionflow.com" className="text-primary hover:underline font-medium">
|
||||
feedback@resolutionflow.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -385,13 +404,86 @@ export default function SurveyPage() {
|
||||
<div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)' }}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<h2 className="font-heading text-2xl font-bold mb-2.5">Done — Thank You!</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto mb-7 leading-relaxed">
|
||||
Your answers will directly shape how FlowPilot troubleshoots. We truly appreciate your time and expertise.
|
||||
<h2 className="font-heading text-2xl font-bold mb-2.5">Response Submitted!</h2>
|
||||
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto mb-8 leading-relaxed">
|
||||
Your answers will directly shape how FlowPilot troubleshoots. Would you like a copy of your responses?
|
||||
</p>
|
||||
|
||||
{/* Email a copy */}
|
||||
<div className="glass-card-static p-6 max-w-[420px] mx-auto mb-5">
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground mb-3">
|
||||
Email a copy to yourself
|
||||
</p>
|
||||
{!emailSent ? (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={e => setEmailInput(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-[#5a6170] focus:outline-none"
|
||||
style={{ background: 'rgba(16, 17, 20, 0.6)', border: '1px solid var(--glass-border)' }}
|
||||
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)' }}
|
||||
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)' }}
|
||||
disabled={emailSending}
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!emailInput.trim() || !responseId) return
|
||||
setEmailSending(true)
|
||||
setEmailError('')
|
||||
try {
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||
const res = await fetch(`${apiUrl}/api/v1/survey/email-copy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailInput.trim(), response_id: responseId }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => null)
|
||||
throw new Error(err?.detail || 'Failed to send')
|
||||
}
|
||||
setEmailSent(true)
|
||||
} catch (err) {
|
||||
setEmailError(err instanceof Error ? err.message : 'Failed to send email')
|
||||
} finally {
|
||||
setEmailSending(false)
|
||||
}
|
||||
}}
|
||||
disabled={!emailInput.trim() || emailSending}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-[9px] text-sm font-semibold bg-gradient-brand text-[#101114] transition-all duration-150 hover:opacity-90 active:scale-[0.97] disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
|
||||
>
|
||||
{emailSending ? (
|
||||
<>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
Sending...
|
||||
</>
|
||||
) : 'Send'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 py-2 text-sm text-emerald-400">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
Email sent! Check your inbox.
|
||||
</div>
|
||||
)}
|
||||
{emailError && (
|
||||
<p className="text-xs text-rose-400 mt-2">{emailError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Copy + Finish buttons */}
|
||||
<div className="flex gap-2.5 justify-center flex-wrap">
|
||||
<button onClick={copyAll} className="inline-flex items-center gap-2 px-6 py-3 rounded-[10px] text-sm font-semibold bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]">
|
||||
Copy Responses to Clipboard
|
||||
<button onClick={copyAll} className="inline-flex items-center gap-2 px-5 py-2.5 rounded-[10px] text-sm font-semibold transition-all duration-150 text-muted-foreground hover:text-foreground" style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.06)' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/survey/thank-you')}
|
||||
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-[10px] text-sm font-semibold bg-gradient-brand text-[#101114] shadow-lg shadow-primary/20 transition-all duration-150 hover:opacity-90 active:scale-[0.97]"
|
||||
>
|
||||
Finish
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user