File size: 4,209 Bytes
b2c1c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useRef, useState } from "react";
import { FileText, Trash2, UploadCloud, X } from "lucide-react";
import { api } from "../lib/api.js";

function formatSize(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

export default function DocumentsPanel({ onClose }) {
  const [documents, setDocuments] = useState([]);
  const [uploading, setUploading] = useState(false);
  const [dragging, setDragging] = useState(false);
  const [error, setError] = useState("");
  const inputRef = useRef(null);

  async function refresh() {
    try {
      setDocuments(await api("/api/documents"));
    } catch (err) {
      setError(err.message);
    }
  }

  useEffect(() => {
    refresh();
  }, []);

  async function upload(files) {
    setError("");
    for (const file of files) {
      setUploading(true);
      try {
        const form = new FormData();
        form.append("file", file);
        await api("/api/documents", { method: "POST", body: form });
        await refresh();
      } catch (err) {
        setError(err.message);
      } finally {
        setUploading(false);
      }
    }
  }

  async function remove(id) {
    try {
      await api(`/api/documents/${id}`, { method: "DELETE" });
      setDocuments((prev) => prev.filter((d) => d.id !== id));
    } catch (err) {
      setError(err.message);
    }
  }

  return (
    <>
      <div className="drawer-backdrop" onClick={onClose} />
      <div className="drawer">
        <div className="drawer-header">
          Knowledge base
          <button className="icon-btn" onClick={onClose}>
            <X size={17} />
          </button>
        </div>
        <div className="drawer-body">
          <div
            className={"dropzone" + (dragging ? " dragging" : "")}
            onClick={() => inputRef.current?.click()}
            onDragOver={(e) => {
              e.preventDefault();
              setDragging(true);
            }}
            onDragLeave={() => setDragging(false)}
            onDrop={(e) => {
              e.preventDefault();
              setDragging(false);
              upload([...e.dataTransfer.files]);
            }}
          >
            <UploadCloud size={22} style={{ marginBottom: 6 }} />
            <div>
              {uploading
                ? "Uploading and indexing..."
                : "Drop files here or click to upload"}
            </div>
            <div style={{ fontSize: 12, marginTop: 4 }}>
              PDF, DOCX, TXT or MD, up to 10 MB. Files are chunked, embedded
              and made searchable to the agent.
            </div>
            <input
              ref={inputRef}
              type="file"
              accept=".pdf,.docx,.txt,.md"
              multiple
              hidden
              onChange={(e) => {
                upload([...e.target.files]);
                e.target.value = "";
              }}
            />
          </div>

          {error && <div className="auth-error">{error}</div>}

          {documents.length === 0 && !uploading && (
            <div className="centered-note">
              No documents yet. Upload one and ask Synapse about it.
            </div>
          )}

          {documents.map((doc) => (
            <div className="doc-item" key={doc.id}>
              <FileText size={17} style={{ color: "var(--accent-strong)", flexShrink: 0 }} />
              <div className="doc-info">
                <div className="doc-name" title={doc.filename}>{doc.filename}</div>
                <div className="doc-sub">
                  {formatSize(doc.size_bytes)}
                  {doc.status === "ready" && ` · ${doc.chunk_count} chunks`}
                  {doc.status === "failed" && doc.error ? ` · ${doc.error.slice(0, 80)}` : ""}
                </div>
              </div>
              <span className={"badge " + doc.status}>{doc.status}</span>
              <button className="icon-btn danger" title="Delete" onClick={() => remove(doc.id)}>
                <Trash2 size={15} />
              </button>
            </div>
          ))}
        </div>
      </div>
    </>
  );
}