- Replace all rgba(6,182,212,...) cyan focus borders and accents with rgba(249,115,22,...) ember orange across 21+ component files - Remove all var(--glass-border) references (undefined variable) with var(--color-border-default) across 24 files - Remove deprecated blur orbs and glass-morphism effects from SurveyPage, SurveyThankYouPage, and LoginPage - Migrate landing.css from hardcoded hex to CSS custom properties (~97 replacements for single-source theming) - Fix off-palette grays in FlowPilotAnalyticsPage chart styling (#8891a0 → #848b9b, #18191f → var(--color-bg-card)) - Update stale comments: "cyan brand" → "accent brand" in GlowEdge, "gradient cyan square" → "gradient orange square" in BrandLogo - Rename glow-cyan SVG filter ID to glow-accent - Fix category color comment: "cyan" → "deep orange" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
264 lines
8.8 KiB
TypeScript
264 lines
8.8 KiB
TypeScript
import { useState, useRef, useCallback } from 'react'
|
|
import { Code2, ImageIcon, Upload } from 'lucide-react'
|
|
import { Modal } from '@/components/common/Modal'
|
|
import { Button } from '@/components/ui/Button'
|
|
import { cn } from '@/lib/utils'
|
|
import { createSupportingData } from '@/api/supportingData'
|
|
import { toast } from '@/lib/toast'
|
|
|
|
interface AddSupportingDataModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
sessionId: string
|
|
onAdded: () => void
|
|
}
|
|
|
|
type TabType = 'text_snippet' | 'screenshot'
|
|
|
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
|
|
export function AddSupportingDataModal({ isOpen, onClose, sessionId, onAdded }: AddSupportingDataModalProps) {
|
|
const [activeTab, setActiveTab] = useState<TabType>('text_snippet')
|
|
const [label, setLabel] = useState('')
|
|
const [textContent, setTextContent] = useState('')
|
|
const [imageBase64, setImageBase64] = useState<string | null>(null)
|
|
const [imageContentType, setImageContentType] = useState<string | null>(null)
|
|
const [imageFileName, setImageFileName] = useState<string | null>(null)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
const resetForm = () => {
|
|
setLabel('')
|
|
setTextContent('')
|
|
setImageBase64(null)
|
|
setImageContentType(null)
|
|
setImageFileName(null)
|
|
setError(null)
|
|
setActiveTab('text_snippet')
|
|
}
|
|
|
|
const handleClose = () => {
|
|
resetForm()
|
|
onClose()
|
|
}
|
|
|
|
const processFile = useCallback((file: File) => {
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
setError('File must be under 2MB')
|
|
return
|
|
}
|
|
if (!['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) {
|
|
setError('Only PNG, JPEG, and SVG files are supported')
|
|
return
|
|
}
|
|
setError(null)
|
|
setImageFileName(file.name)
|
|
setImageContentType(file.type)
|
|
|
|
const reader = new FileReader()
|
|
reader.onload = () => {
|
|
const result = reader.result as string
|
|
// Strip the data:... prefix to get raw base64
|
|
const base64 = result.includes(',') ? result.split(',')[1] : result
|
|
setImageBase64(base64)
|
|
}
|
|
reader.readAsDataURL(file)
|
|
}, [])
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) processFile(file)
|
|
}
|
|
|
|
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
|
const items = e.clipboardData?.items
|
|
if (!items) return
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
e.preventDefault()
|
|
const file = item.getAsFile()
|
|
if (file) processFile(file)
|
|
return
|
|
}
|
|
}
|
|
}, [processFile])
|
|
|
|
const handleSubmit = async () => {
|
|
if (!label.trim()) {
|
|
setError('Label is required')
|
|
return
|
|
}
|
|
|
|
if (activeTab === 'text_snippet' && !textContent.trim()) {
|
|
setError('Content is required')
|
|
return
|
|
}
|
|
|
|
if (activeTab === 'screenshot' && !imageBase64) {
|
|
setError('Please select or paste an image')
|
|
return
|
|
}
|
|
|
|
setIsSubmitting(true)
|
|
setError(null)
|
|
try {
|
|
await createSupportingData(sessionId, {
|
|
label: label.trim(),
|
|
data_type: activeTab,
|
|
content: activeTab === 'text_snippet' ? textContent : imageBase64!,
|
|
content_type: activeTab === 'screenshot' ? (imageContentType ?? undefined) : undefined,
|
|
})
|
|
toast.success('Supporting data added')
|
|
onAdded()
|
|
handleClose()
|
|
} catch (err) {
|
|
console.error('Failed to add supporting data:', err)
|
|
setError('Failed to save. Please try again.')
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} onClose={handleClose} title="Add Supporting Data">
|
|
{/* Tabs */}
|
|
<div className="mb-4 flex gap-1 rounded-lg bg-accent p-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('text_snippet')}
|
|
className={cn(
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
|
activeTab === 'text_snippet'
|
|
? 'bg-card text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
)}
|
|
>
|
|
<Code2 className="h-4 w-4" />
|
|
Text Snippet
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('screenshot')}
|
|
className={cn(
|
|
'flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
|
activeTab === 'screenshot'
|
|
? 'bg-card text-foreground shadow-sm'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
)}
|
|
>
|
|
<ImageIcon className="h-4 w-4" />
|
|
Screenshot
|
|
</button>
|
|
</div>
|
|
|
|
{/* Label */}
|
|
<div className="mb-4">
|
|
<label htmlFor="sd-label" className="mb-1 block text-sm font-medium text-foreground">
|
|
Label
|
|
</label>
|
|
<input
|
|
id="sd-label"
|
|
type="text"
|
|
value={label}
|
|
onChange={(e) => setLabel(e.target.value)}
|
|
placeholder={activeTab === 'text_snippet' ? 'e.g. Error log output' : 'e.g. Blue screen photo'}
|
|
className={cn(
|
|
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm',
|
|
'text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Text Snippet Tab Content */}
|
|
{activeTab === 'text_snippet' && (
|
|
<div className="mb-4">
|
|
<label htmlFor="sd-content" className="mb-1 block text-sm font-medium text-foreground">
|
|
Content
|
|
</label>
|
|
<textarea
|
|
id="sd-content"
|
|
value={textContent}
|
|
onChange={(e) => setTextContent(e.target.value)}
|
|
placeholder="Paste log output, error messages, config snippets..."
|
|
rows={8}
|
|
className={cn(
|
|
'w-full resize-y rounded-md border border-border bg-card px-3 py-2 text-sm',
|
|
'font-mono text-foreground placeholder:text-muted-foreground',
|
|
'focus:border-[rgba(249,115,22,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20'
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Screenshot Tab Content */}
|
|
{activeTab === 'screenshot' && (
|
|
<div className="mb-4" onPaste={handlePaste}>
|
|
<label className="mb-1 block text-sm font-medium text-foreground">
|
|
Image
|
|
</label>
|
|
{imageBase64 ? (
|
|
<div className="relative rounded-md border border-border bg-card p-2">
|
|
<img
|
|
src={`data:${imageContentType};base64,${imageBase64}`}
|
|
alt="Preview"
|
|
className="mx-auto max-h-48 rounded object-contain"
|
|
/>
|
|
<p className="mt-2 text-center text-xs text-muted-foreground">{imageFileName}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setImageBase64(null)
|
|
setImageContentType(null)
|
|
setImageFileName(null)
|
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
}}
|
|
className="mt-2 w-full text-center text-xs text-muted-foreground hover:text-foreground"
|
|
>
|
|
Remove and choose another
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className={cn(
|
|
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed border-border',
|
|
'bg-card/50 py-10 transition-colors hover:border-muted-foreground'
|
|
)}
|
|
>
|
|
<Upload className="h-8 w-8 text-muted-foreground" />
|
|
<p className="text-sm text-muted-foreground">Click to upload or paste from clipboard</p>
|
|
<p className="text-xs text-muted-foreground">PNG, JPEG, or SVG - max 2MB</p>
|
|
</div>
|
|
)}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/png,image/jpeg,image/svg+xml"
|
|
onChange={handleFileChange}
|
|
className="hidden"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<p className="mb-4 text-sm text-rose-500">{error}</p>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button variant="secondary" onClick={handleClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSubmit} loading={isSubmitting}>
|
|
{isSubmitting ? 'Saving...' : 'Add'}
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|
|
|
|
export default AddSupportingDataModal
|