Files
resolutionflow/frontend/src/components/session/AddSupportingDataModal.tsx
chihlasm 34b0f2ade9 fix: eliminate deprecated cyan, glass-border, and off-palette colors site-wide
- 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>
2026-03-27 05:42:08 +00:00

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