Files
resolutionflow/frontend/src/components/flowpilot/FlowPilotIntake.tsx
Michael Chihlas 303a558432 refactor: replace hardcoded hex values with Tailwind semantic tokens
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>
2026-03-22 04:34:35 -04:00

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">&bull;</span>
<span>{selectedTicket.priority_name}</span>
</>
)}
{selectedTicket.status_name && (
<>
<span className="text-text-muted">&bull;</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>
)
}