3,200+ hardcoded color values replaced with CSS variable-backed Tailwind classes (bg-card, text-foreground, border-border, etc.). Enables light mode via CSS variable swap. Only syntax highlighting colors and intentional one-offs remain hardcoded (~15 values). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
262 lines
10 KiB
TypeScript
262 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Sparkles, FileText, Terminal, X, AlertTriangle } from 'lucide-react'
|
|
import { TicketPickerModal } from '@/components/session/TicketPickerModal'
|
|
import { RichTextInput } from '@/components/common/RichTextInput'
|
|
import { integrationsApi } from '@/api/integrations'
|
|
import type { AISessionCreateRequest } from '@/types/ai-session'
|
|
import type { PSATicketInfo, PsaConnectionResponse } from '@/types/integrations'
|
|
import type { FileUploadResponse } from '@/types/upload'
|
|
|
|
interface FlowPilotIntakeProps {
|
|
onSubmit: (request: AISessionCreateRequest) => void
|
|
isLoading: boolean
|
|
defaultProblem?: string
|
|
}
|
|
|
|
export function FlowPilotIntake({ onSubmit, isLoading, defaultProblem }: FlowPilotIntakeProps) {
|
|
const [text, setText] = useState(defaultProblem || '')
|
|
const [showLogs, setShowLogs] = useState(false)
|
|
const [logContent, setLogContent] = useState('')
|
|
const [showTicketPicker, setShowTicketPicker] = useState(false)
|
|
|
|
// PSA connection state
|
|
const [psaConnection, setPsaConnection] = useState<PsaConnectionResponse | null>(null)
|
|
const [psaChecked, setPsaChecked] = useState(false)
|
|
|
|
// Upload state (no session_id yet — uploads linked later)
|
|
const [_intakeUploads, setIntakeUploads] = useState<FileUploadResponse[]>([])
|
|
|
|
// Selected ticket state
|
|
const [selectedTicket, setSelectedTicket] = useState<PSATicketInfo | null>(null)
|
|
const [selectedTicketId, setSelectedTicketId] = useState<string | null>(null)
|
|
const [additionalContext, setAdditionalContext] = useState('')
|
|
|
|
// Check for PSA connection on mount
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
integrationsApi.getConnection()
|
|
.then((conn) => {
|
|
if (!cancelled) {
|
|
setPsaConnection(conn && conn.is_active ? conn : null)
|
|
setPsaChecked(true)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setPsaChecked(true)
|
|
})
|
|
return () => { cancelled = true }
|
|
}, [])
|
|
|
|
const handleTicketSelected = (ticketId: string, ticket: PSATicketInfo) => {
|
|
setSelectedTicketId(ticketId)
|
|
setSelectedTicket(ticket)
|
|
setShowTicketPicker(false)
|
|
}
|
|
|
|
const handleClearTicket = () => {
|
|
setSelectedTicketId(null)
|
|
setSelectedTicket(null)
|
|
setAdditionalContext('')
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
// Ticket-based submission
|
|
if (selectedTicket && selectedTicketId && psaConnection) {
|
|
const intake_content: Record<string, unknown> = {
|
|
text: additionalContext.trim() || undefined,
|
|
ticket_data: {
|
|
summary: selectedTicket.summary,
|
|
company: selectedTicket.company_name,
|
|
priority: selectedTicket.priority_name,
|
|
},
|
|
}
|
|
|
|
onSubmit({
|
|
intake_type: 'psa_ticket',
|
|
intake_content,
|
|
psa_ticket_id: selectedTicketId,
|
|
psa_connection_id: psaConnection.id,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Free-text / log submission
|
|
if (!text.trim() && !logContent.trim()) return
|
|
|
|
const intake_content: Record<string, unknown> = {}
|
|
if (text.trim()) intake_content.text = text.trim()
|
|
if (logContent.trim()) intake_content.log_content = logContent.trim()
|
|
|
|
const intake_type = logContent.trim()
|
|
? text.trim() ? 'combined' : 'log_paste'
|
|
: 'free_text'
|
|
|
|
onSubmit({ intake_type, intake_content })
|
|
}
|
|
|
|
const hasContent = text.trim() || logContent.trim() || selectedTicket
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-[50vh]">
|
|
<div className="text-center">
|
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-accent-dim">
|
|
<Sparkles size={24} className="text-primary animate-pulse" />
|
|
</div>
|
|
<p className="text-sm font-medium text-foreground">Analyzing your issue...</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">FlowPilot is classifying the problem and searching for relevant flows</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const submitLabel = selectedTicket && selectedTicketId
|
|
? `Start Session with Ticket #${selectedTicketId}`
|
|
: 'Start Session'
|
|
|
|
return (
|
|
<div className="flex items-start justify-center px-3 sm:px-4 pt-[6vh] sm:pt-[10vh]">
|
|
<div className="w-full max-w-2xl">
|
|
<div className="text-center mb-4 sm:mb-6">
|
|
<h1 className="font-heading text-xl sm:text-2xl font-bold tracking-tight text-foreground">
|
|
What are you troubleshooting?
|
|
</h1>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Describe the issue, paste an error message, or pull context from a ticket
|
|
</p>
|
|
</div>
|
|
|
|
<div className="card-flat p-3 sm:p-5 space-y-4">
|
|
{/* Selected ticket card */}
|
|
{selectedTicket && selectedTicketId && (
|
|
<div className="rounded-xl border border-primary/20 bg-primary/5 p-4">
|
|
<div className="flex items-start justify-between">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-semibold text-foreground">
|
|
<span className="text-primary">#{selectedTicketId}</span>
|
|
{' — '}
|
|
{selectedTicket.summary}
|
|
</p>
|
|
<div className="mt-1 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
|
{selectedTicket.company_name && <span>{selectedTicket.company_name}</span>}
|
|
{selectedTicket.priority_name && (
|
|
<>
|
|
<span className="text-text-muted">•</span>
|
|
<span>{selectedTicket.priority_name}</span>
|
|
</>
|
|
)}
|
|
{selectedTicket.status_name && (
|
|
<>
|
|
<span className="text-text-muted">•</span>
|
|
<span>{selectedTicket.status_name}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleClearTicket}
|
|
className="ml-2 rounded-md p-1 text-muted-foreground hover:bg-white/[0.06] hover:text-foreground transition-colors"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Additional context textarea */}
|
|
<textarea
|
|
value={additionalContext}
|
|
onChange={(e) => setAdditionalContext(e.target.value)}
|
|
placeholder="Add extra context (optional) — e.g. 'User called back and said it's also affecting their second monitor'"
|
|
className="mt-3 w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main text area (hidden when ticket is selected) */}
|
|
{!selectedTicket && (
|
|
<RichTextInput
|
|
value={text}
|
|
onChange={setText}
|
|
onFilesChange={setIntakeUploads}
|
|
placeholder="e.g. User can't access shared drive after password reset, getting 'Access Denied' in Event Viewer..."
|
|
rows={5}
|
|
/>
|
|
)}
|
|
|
|
{/* Input type toggles */}
|
|
{!selectedTicket && (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setShowLogs(!showLogs)}
|
|
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
showLogs
|
|
? 'bg-accent-dim text-primary border border-primary/20'
|
|
: 'bg-card/50 text-muted-foreground border border-border hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Terminal size={12} />
|
|
Paste Logs
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (psaConnection) {
|
|
setShowTicketPicker(true)
|
|
}
|
|
}}
|
|
disabled={!psaChecked || !psaConnection}
|
|
className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
psaConnection
|
|
? 'bg-card/50 text-muted-foreground border border-border hover:text-foreground hover:border-primary/20'
|
|
: 'bg-card/50 text-text-muted border border-border opacity-50 cursor-not-allowed'
|
|
}`}
|
|
title={!psaConnection ? 'Connect your PSA in Settings → Integrations' : 'Search for a ConnectWise ticket'}
|
|
>
|
|
<FileText size={12} />
|
|
Pull from Ticket
|
|
</button>
|
|
{psaChecked && !psaConnection && (
|
|
<span className="flex items-center gap-1 text-[0.625rem] text-amber-400/80">
|
|
<AlertTriangle size={10} />
|
|
No PSA connected
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Log paste area */}
|
|
{!selectedTicket && showLogs && (
|
|
<textarea
|
|
value={logContent}
|
|
onChange={(e) => setLogContent(e.target.value)}
|
|
placeholder="Paste log output, error messages, or Event Viewer entries here..."
|
|
className="w-full rounded-lg border border-border bg-card px-4 py-3 font-mono text-xs text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none"
|
|
rows={6}
|
|
/>
|
|
)}
|
|
|
|
{/* Submit */}
|
|
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-[0.6875rem] text-text-muted text-center sm:text-left">
|
|
FlowPilot will analyze your input and guide you through diagnosis
|
|
</p>
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={!hasContent}
|
|
className="w-full sm:w-auto min-h-[44px] rounded-lg bg-primary text-white px-5 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] disabled:opacity-40 transition-all whitespace-nowrap"
|
|
>
|
|
{submitLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Ticket picker modal */}
|
|
<TicketPickerModal
|
|
open={showTicketPicker}
|
|
onClose={() => setShowTicketPicker(false)}
|
|
onSelect={handleTicketSelected}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|