File size: 6,501 Bytes
34ca534
 
f5628ad
e3e8fd0
f5628ad
34ca534
f5628ad
 
ee2b23c
 
f5628ad
34ca534
 
 
 
 
 
 
 
 
53ddb38
34ca534
 
 
 
dcac13e
34ca534
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
 
 
 
 
34ca534
f5628ad
e3e8fd0
34ca534
 
f5628ad
 
 
34ca534
f5628ad
 
 
ee2b23c
 
 
 
 
 
34ca534
ee2b23c
e3e8fd0
ee2b23c
 
 
 
 
 
 
34ca534
ee2b23c
 
 
f5628ad
 
 
 
 
 
 
 
ee2b23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f5628ad
ee2b23c
 
 
 
34ca534
ee2b23c
 
 
34ca534
 
 
 
 
 
ee2b23c
 
 
 
 
 
 
 
34ca534
ee2b23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34ca534
ee2b23c
 
 
 
f5628ad
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { useState, useRef, useEffect } from "react";
import { uploadFiles, uploadUrl, getUploadStatus, getDocuments } from "../api";

export default function FileUpload({ onUploadComplete, onBriefing, currentWorkspace = "default" }) {
  const [uploading, setUploading] = useState(false);
  const [stageMsg, setStageMsg] = useState("");
  const [dragOver, setDragOver] = useState(false);
  const [error, setError] = useState(null);
  const [tab, setTab] = useState("file"); // "file" | "url"
  const [urlInput, setUrlInput] = useState("");
  const fileInputRef = useRef(null);
  const pollRef = useRef(null);

  // Clean up polling on unmount
  useEffect(() => () => clearInterval(pollRef.current), []);

  function startPolling(jobId) {
    pollRef.current = setInterval(async () => {
      try {
        const status = await getUploadStatus(jobId);
        if (status.status !== "contextualizing") setStageMsg(status.message);

        if (status.status === "ready") {
          clearInterval(pollRef.current);
          const docs = await getDocuments(currentWorkspace);
          onUploadComplete(docs?.documents || []);
          if (status.briefing && onBriefing) onBriefing(status.briefing);
          setUploading(false);
          setStageMsg("");
        } else if (status.status === "failed") {
          clearInterval(pollRef.current);
          setError(status.error || "Processing failed");
          setUploading(false);
          setStageMsg("");
        }
      } catch {
        // transient poll error — keep trying
      }
    }, 2000);
  }

  async function handleFiles(files) {
    if (!files.length) return;
    setUploading(true);
    setError(null);
    setStageMsg("Uploading...");
    try {
      const data = await uploadFiles(files, currentWorkspace);
      setStageMsg(data.message || "Embedding...");
      startPolling(data.job_id);
    } catch (err) {
      setError(err.response?.data?.detail || "Upload failed");
      setUploading(false);
      setStageMsg("");
    }
  }

  async function handleUrlSubmit(e) {
    e.preventDefault();
    const url = urlInput.trim();
    if (!url) return;
    setUploading(true);
    setError(null);
    setStageMsg("Ingesting URL...");
    try {
      const data = await uploadUrl(url, currentWorkspace);
      onUploadComplete(data.documents);
      if (data.briefing && onBriefing) onBriefing(data.briefing);
      setUrlInput("");
    } catch (err) {
      setError(err.response?.data?.detail || "URL ingestion failed");
    } finally {
      setUploading(false);
      setStageMsg("");
    }
  }

  function handleDrop(e) {
    e.preventDefault();
    setDragOver(false);
    handleFiles(Array.from(e.dataTransfer.files));
  }

  return (
    <div>
      {/* Tab switcher */}
      <div className="flex gap-1 mb-3">
        <button
          onClick={() => setTab("file")}
          className={`flex-1 text-xs py-1.5 rounded-lg font-medium transition-colors ${
            tab === "file"
              ? "bg-indigo-600 text-white"
              : "bg-gray-100 text-gray-500 hover:bg-gray-200"
          }`}
        >
          File
        </button>
        <button
          onClick={() => setTab("url")}
          className={`flex-1 text-xs py-1.5 rounded-lg font-medium transition-colors ${
            tab === "url"
              ? "bg-indigo-600 text-white"
              : "bg-gray-100 text-gray-500 hover:bg-gray-200"
          }`}
        >
          URL
        </button>
      </div>

      {tab === "file" ? (
        <>
          <div
            onClick={() => !uploading && fileInputRef.current?.click()}
            onDrop={handleDrop}
            onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
            onDragLeave={() => setDragOver(false)}
            className={`border-2 border-dashed rounded-xl p-5 text-center transition-all ${
              uploading
                ? "border-indigo-300 bg-indigo-50/50 cursor-default"
                : dragOver
                ? "border-indigo-500 bg-indigo-50 cursor-pointer"
                : "border-gray-200 hover:border-indigo-300 hover:bg-indigo-50/50 cursor-pointer"
            }`}
          >
            {uploading ? (
              <div className="flex flex-col items-center gap-2">
                <svg className="w-6 h-6 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
                </svg>
                <p className="text-sm text-indigo-600 font-medium">{stageMsg || "Processing..."}</p>
              </div>
            ) : (
              <>
                <svg className="w-8 h-8 text-indigo-400 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5"
                    d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
                </svg>
                <p className="text-sm text-gray-600">Drop files here</p>
                <p className="text-xs text-gray-400 mt-1">PDF, TXT, or CSV</p>
              </>
            )}
          </div>
          <input
            ref={fileInputRef}
            type="file"
            multiple
            accept=".pdf,.txt,.csv"
            className="hidden"
            onChange={(e) => handleFiles(Array.from(e.target.files))}
          />
        </>
      ) : (
        <form onSubmit={handleUrlSubmit} className="space-y-2">
          <input
            type="url"
            value={urlInput}
            onChange={(e) => setUrlInput(e.target.value)}
            placeholder="https://example.com/article"
            className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
            disabled={uploading}
          />
          <button
            type="submit"
            disabled={uploading || !urlInput.trim()}
            className="w-full py-2 bg-indigo-600 text-white rounded-xl text-sm font-medium hover:bg-indigo-700 disabled:bg-indigo-200 disabled:cursor-not-allowed transition-colors"
          >
            {uploading ? stageMsg || "Ingesting..." : "Ingest URL"}
          </button>
        </form>
      )}

      {error && <p className="text-xs text-red-500 mt-2">{error}</p>}
    </div>
  );
}