File size: 5,603 Bytes
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { useRef, useState } from 'react'
import { useUIStore } from '@/stores/uiStore'
import { ApiError } from '@/lib/http'
import { env } from '@/config/env'
import { cn } from '@/lib/utils'

const ACCEPTED = '.pdf,.docx,.doc,.txt,.md,.csv,.xlsx,.xls'
const ACCEPTED_SET = new Set(ACCEPTED.split(','))
const MAX_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB

interface UploadResult {
  filename: string
  task_id:  string
}

interface Props {
  teamId?: string
  onQueued?: (result: UploadResult) => void
}

export function FileUploadWidget({ teamId = 'default', onQueued }: Props) {
  const inputRef              = useRef<HTMLInputElement>(null)
  const [dragging, setDragging] = useState(false)
  const [pending, setPending]   = useState<File | null>(null)
  const [loading, setLoading]   = useState(false)
  const [result, setResult]     = useState<UploadResult | null>(null)
  const [fileError, setFileError] = useState<string | null>(null)
  const addToast              = useUIStore((s) => s.addToast)

  const validate = (file: File): string | null => {
    const ext = '.' + file.name.split('.').pop()?.toLowerCase()
    if (!ACCEPTED_SET.has(ext)) return `Unsupported type (${ext}). Accepted: PDF, DOCX, TXT, MD, CSV, XLSX`
    if (file.size > MAX_SIZE_BYTES) return `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max 50 MB`
    return null
  }

  const pick = (file: File) => {
    setResult(null)
    const err = validate(file)
    setFileError(err)
    setPending(err ? null : file)
  }

  const upload = async () => {
    if (!pending) return
    setLoading(true)
    try {
      const form = new FormData()
      form.append('file', pending)
      form.append('team_id', teamId)

      const res = await fetch(`${env.apiBaseUrl}/api/ingest/file`, {
        method:      'POST',
        credentials: 'include',
        body:        form,
        // No Content-Type — browser sets multipart boundary automatically
      })

      if (!res.ok) {
        const requestId = res.headers.get('X-Request-ID') ?? undefined
        const text = await res.text().catch(() => res.statusText)
        if (res.status >= 500) {
          addToast({
            type:    'error',
            message: requestId ? `Upload error [${requestId}]` : 'Upload failed — server error',
          })
        } else {
          addToast({ type: 'error', message: text || 'Upload failed' })
        }
        throw new ApiError(res.status, text, requestId)
      }

      const data = await res.json() as UploadResult
      setResult(data)
      setPending(null)
      onQueued?.(data)
      addToast({ type: 'success', message: `${data.filename} queued for processing` })
    } catch (err) {
      if (!(err instanceof ApiError)) {
        addToast({ type: 'error', message: 'Upload failed — no connection' })
      }
    } finally {
      setLoading(false)
    }
  }

  const onDrop = (e: React.DragEvent) => {
    e.preventDefault()
    setDragging(false)
    const file = e.dataTransfer.files[0]
    if (file) pick(file)
  }

  return (
    <div className="flex flex-col gap-3">
      {/* Drop zone */}
      <div
        role="button"
        tabIndex={0}
        onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
        onDragLeave={() => setDragging(false)}
        onDrop={onDrop}
        onClick={() => inputRef.current?.click()}
        onKeyDown={(e) => e.key === 'Enter' && inputRef.current?.click()}
        className={cn(
          'flex cursor-pointer flex-col items-center gap-2 rounded-xl border-2 border-dashed p-8 text-center transition-colors',
          dragging
            ? 'border-brand bg-brand/5'
            : 'border-stone-200 hover:border-brand/50 dark:border-stone-700',
        )}
        aria-label="Upload file — drag and drop or click to browse"
      >
        <span className="text-2xl" aria-hidden>📄</span>
        <p className="text-sm font-medium text-stone-700 dark:text-stone-300">
          Drag a file here or <span className="text-brand underline">browse</span>
        </p>
        <p className="text-xs text-stone-400">PDF · DOCX · TXT · MD · CSV · XLSX — max 50 MB</p>
      </div>

      <input
        ref={inputRef}
        type="file"
        accept={ACCEPTED}
        className="hidden"
        onChange={(e) => { const f = e.target.files?.[0]; if (f) pick(f) }}
      />

      {/* Validation error */}
      {fileError && (
        <p className="text-xs text-red-600 dark:text-red-400">{fileError}</p>
      )}

      {/* Pending file preview */}
      {pending && (
        <div className="flex items-center justify-between rounded-lg border border-surface-subtle px-4 py-3">
          <div>
            <p className="text-sm font-medium">{pending.name}</p>
            <p className="text-xs text-stone-500">{(pending.size / 1024).toFixed(0)} KB</p>
          </div>
          <button
            onClick={upload}
            disabled={loading}
            className="rounded-lg bg-brand px-4 py-1.5 text-xs font-medium text-white hover:bg-brand-dark disabled:opacity-60"
          >
            {loading ? 'Uploading…' : 'Upload'}
          </button>
        </div>
      )}

      {/* Success result */}
      {result && (
        <div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm dark:border-green-800 dark:bg-green-950/30">
          <p className="font-medium text-green-700 dark:text-green-400">Queued — processing in background</p>
          <p className="mt-0.5 font-mono text-xs text-stone-500">Task {result.task_id.slice(0, 12)}…</p>
        </div>
      )}
    </div>
  )
}