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,10 +1,11 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Sparkles, Send, Loader2 } from 'lucide-react'
|
||||
import { Sparkles, Send, Loader2, Flag } from 'lucide-react'
|
||||
import { assistantChatApi } from '@/api/assistantChat'
|
||||
import { toast } from '@/lib/toast'
|
||||
import { ChatSidebar } from '@/components/assistant/ChatSidebar'
|
||||
import { ChatMessage } from '@/components/assistant/ChatMessage'
|
||||
import type { ChatListItem, AssistantChatMessage as ChatMessageType } from '@/types/assistant-chat'
|
||||
import { ConcludeSessionModal } from '@/components/assistant/ConcludeSessionModal'
|
||||
import type { ChatListItem, AssistantChatMessage as ChatMessageType, ConclusionOutcome } from '@/types/assistant-chat'
|
||||
import type { SuggestedFlow } from '@/types/copilot'
|
||||
|
||||
interface MessageWithMeta extends ChatMessageType {
|
||||
@@ -17,6 +18,7 @@ export default function AssistantChatPage() {
|
||||
const [messages, setMessages] = useState<MessageWithMeta[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConclude, setShowConclude] = useState(false)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
@@ -120,6 +122,55 @@ export default function AssistantChatPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleConclude = async (outcome: ConclusionOutcome, notes: string): Promise<string> => {
|
||||
if (!activeChatId) throw new Error('No active chat')
|
||||
const response = await assistantChatApi.concludeChat(activeChatId, { outcome, notes: notes || undefined })
|
||||
// Update chat in sidebar to show concluded status
|
||||
setChats(prev =>
|
||||
prev.map(c =>
|
||||
c.id === activeChatId
|
||||
? { ...c, concluded_at: response.concluded_at, conclusion_outcome: outcome }
|
||||
: c
|
||||
)
|
||||
)
|
||||
return response.summary
|
||||
}
|
||||
|
||||
const handleResumeNew = async (summary: string) => {
|
||||
try {
|
||||
const chat = await assistantChatApi.createChat()
|
||||
setChats(prev => [
|
||||
{ id: chat.id, title: chat.title, message_count: 0, pinned: false, created_at: chat.created_at, updated_at: chat.updated_at },
|
||||
...prev,
|
||||
])
|
||||
setActiveChatId(chat.id)
|
||||
setMessages([])
|
||||
|
||||
// Send the summary as the first message to prime the new chat
|
||||
const resumePrompt = `I'm continuing a previous troubleshooting session. Here's where we left off:\n\n${summary}\n\nPlease review this context and help me continue from where we stopped.`
|
||||
setInput('')
|
||||
setMessages([{ role: 'user', content: resumePrompt }])
|
||||
setLoading(true)
|
||||
|
||||
const response = await assistantChatApi.sendMessage(chat.id, resumePrompt)
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ role: 'assistant', content: response.content, suggestedFlows: response.suggested_flows },
|
||||
])
|
||||
setChats(prev =>
|
||||
prev.map(c =>
|
||||
c.id === chat.id
|
||||
? { ...c, message_count: 2, title: resumePrompt.slice(0, 100), updated_at: new Date().toISOString() }
|
||||
: c
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
toast.error('Failed to create resume chat')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
@@ -194,13 +245,27 @@ export default function AssistantChatPage() {
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading}
|
||||
className="bg-gradient-brand text-[#101114] p-3 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || loading}
|
||||
className="bg-gradient-brand text-[#101114] p-3 rounded-xl hover:opacity-90 active:scale-[0.97] transition-all disabled:opacity-40"
|
||||
title="Send message"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
{messages.length >= 2 && (
|
||||
<button
|
||||
onClick={() => setShowConclude(true)}
|
||||
disabled={loading}
|
||||
className="p-3 rounded-xl border text-muted-foreground hover:text-amber-400 hover:border-amber-400/30 hover:bg-amber-400/10 transition-all disabled:opacity-40"
|
||||
style={{ borderColor: 'var(--glass-border)' }}
|
||||
title="Conclude session"
|
||||
>
|
||||
<Flag size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -225,6 +290,15 @@ export default function AssistantChatPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Conclude Session Modal */}
|
||||
<ConcludeSessionModal
|
||||
isOpen={showConclude}
|
||||
onClose={() => setShowConclude(false)}
|
||||
onConclude={handleConclude}
|
||||
onResumeNew={handleResumeNew}
|
||||
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user