File size: 9,612 Bytes
d5c104e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import React, { useState, useRef, useCallback } from 'react';
import { FaTimes, FaFileUpload, FaFileAlt } from 'react-icons/fa';
import Button from '@mui/material/Button';
import './AddFilesDialog.css';

const MAX_TOTAL_SIZE = 10 * 1024 * 1024; // 10 MB
const ALLOWED_EXTENSIONS = new Set([
  // Documents
  '.pdf', '.doc', '.docx', '.odt', '.txt', '.rtf', '.md',
  // Spreadsheets
  '.csv', '.xls', '.xlsx',
  // Presentations
  '.ppt', '.pptx',
  // Code files
  '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp', '.h',
  '.cs', '.html', '.css', '.scss', '.json', '.xml', '.sql', '.sh',
  '.rb', '.php', '.go'
]);

function AddFilesDialog({ isOpen, onClose, openSnackbar, setSessionContent }) {
  const [isUploading, setIsUploading] = useState(false);
  const [isDragging, setIsDragging] = useState(false);
  const [files, setFiles] = useState([]);
  const [urlInput, setUrlInput] = useState("");
  const fileInputRef = useRef(null);

  // Function to handle files dropped or selected
  const handleFiles = useCallback((incomingFiles) => {
    if (incomingFiles && incomingFiles.length > 0) {
      let currentTotalSize = files.reduce((acc, f) => acc + f.file.size, 0);
      const validFiles = [];

      for (const file of Array.from(incomingFiles)) {
        // 1. Check for duplicates
        if (files.some(existing => existing.file.name === file.name && existing.file.size === file.size)) {
          continue; // Skip duplicate file
        }

        // 2. Check file type
        const fileExtension = file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
        if (!ALLOWED_EXTENSIONS.has(fileExtension)) {
          openSnackbar(`File type not supported: ${file.name}`, 'error', 5000);
          continue; // Skip unsupported file type
        }

        // 3. Check total size limit
        if (currentTotalSize + file.size > MAX_TOTAL_SIZE) {
          openSnackbar('Total file size cannot exceed 10 MB', 'error', 5000);
          break; // Stop processing further files as limit is reached
        }

        currentTotalSize += file.size;
        validFiles.push({
          id: window.crypto.randomUUID(),
          file: file,
          progress: 0,
        });
      }

      if (validFiles.length > 0) {
        setFiles(prevFiles => [...prevFiles, ...validFiles]);
      }
    }
  }, [files, openSnackbar]);

  // Function to handle file removal
  const handleRemoveFile = useCallback((fileId) => {
    setFiles(prevFiles => prevFiles.filter(f => f.id !== fileId));
  }, []);

  // Ensure that the component does not render if isOpen is false
  if (!isOpen) {
    return null;
  }

  // Function to format file size in a human-readable format
  const formatFileSize = (bytes) => {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
  };

  // Handlers for drag and drop events
  const handleDragOver = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  };

  // Handler for when the drag leaves the drop zone
  const handleDragLeave = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  };

  // Handler for when files are dropped into the drop zone
  const handleDrop = (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
    handleFiles(e.dataTransfer.files);
  };

  // Handler for when files are selected via the file input
  const handleFileSelect = (e) => {
    handleFiles(e.target.files);
    // Reset input value to allow selecting the same file again
    e.target.value = null;
  };

  // Handler for clicking the drop zone to open the file dialog
  const handleBoxClick = () => {
    fileInputRef.current.click();
  };

  // Handler for resetting the file list
  const handleReset = () => {
    setFiles([]);
    setUrlInput("");
  };

  // Handler for adding files
  const handleAdd = () => {
    setIsUploading(true); // Start upload state, disable buttons
    
    // Regex to validate URL format
    const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/;
    const urls = urlInput.split('\n').map(url => url.trim()).filter(url => url);

    // 1. Validate URLs before proceeding
    if (files.length === 0 && urls.length === 0) {
      openSnackbar("Please add files or URLs before submitting.", "error", 5000);
      return;
    }

    for (const url of urls) {
      if (!urlRegex.test(url)) {
        openSnackbar(`Invalid URL format: ${url}`, 'error', 5000);
        setIsUploading(false); // Reset upload state on validation error
        return; // Stop the process if an invalid URL is found
      }
    }

    // 2. If all URLs are valid, proceed with logging/uploading
    const formData = new FormData();
    if (files.length > 0) {
      files.forEach(fileWrapper => {
        formData.append('files', fileWrapper.file, fileWrapper.file.name);
      });
    }
    formData.append('urls', JSON.stringify(urls));

    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/add-content', true);

    // Track upload progress
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percentage = Math.round((event.loaded / event.total) * 100);
        setFiles(prevFiles =>
          prevFiles.map(f => ({ ...f, progress: percentage }))
        );
      }
    };

    // Handle completion
    xhr.onload = () => {
      if (xhr.status === 200) {
        // --- ARTIFICIAL DELAY FOR LOCAL DEVELOPMENT ---
        // This timeout ensures the 100% progress bar is visible before the dialog closes.
        // This can be removed for production.
        setTimeout(() => {
          const result = JSON.parse(xhr.responseText);
          openSnackbar('Content added successfully!', 'success');
          setSessionContent(prev => ({
            files: [...prev.files, ...result.files_added],
            links: [...prev.links, ...result.links_added],
          }));
          handleReset();
          onClose();
        }, 500); // 0.5-second delay
      } else {
        const errorResult = JSON.parse(xhr.responseText);
        openSnackbar(errorResult.detail || 'Failed to add content.', 'error', 5000);
        setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error
        setIsUploading(false); // End upload state
      }
    };

    // Handle network errors
    xhr.onerror = () => {
      openSnackbar('An error occurred during the upload. Please check your network.', 'error', 5000);
      setFiles(prevFiles => prevFiles.map(f => ({ ...f, progress: 0 }))); // Reset progress on error
    };

    xhr.send(formData);
  };

  return (
    <div className="add-files-dialog" onClick={isUploading ? null : onClose}>
      <div className="add-files-dialog-inner" onClick={(e) => e.stopPropagation()}>
        <label className="dialog-title">Add Files and Links</label>
        <button className="close-btn" onClick={onClose} disabled={isUploading}>
          <FaTimes />
        </button>
        <div className="dialog-content-area">
          <div className="url-input-container">
            <textarea
              id="url-input"
              className="url-input-textarea"
              placeholder="Enter one URL per line"
              value={urlInput}
              onChange={(e) => setUrlInput(e.target.value)}
            />
          </div>
          <div
            className={`file-drop-zone ${isDragging ? 'dragging' : ''}`}
            onClick={handleBoxClick}
            onDragOver={handleDragOver}
            onDragLeave={handleDragLeave}
            onDrop={handleDrop}
          >
            <input
              type="file"
              ref={fileInputRef}
              onChange={handleFileSelect}
              style={{ display: 'none' }}
              multiple
            />
            <FaFileUpload className="upload-icon" />
            <p>Drag and drop files here, or click to select files</p>
          </div>

          {files.length > 0 && (
            <div className="file-list">
              {files.map(fileWrapper => (
                <div key={fileWrapper.id} className="file-item">
                  <FaFileAlt className="file-icon" />
                  <div className="file-info">
                    <span className="file-name">{fileWrapper.file.name}</span>
                    <span className="file-size">{formatFileSize(fileWrapper.file.size)}</span>
                  </div>
                  {isUploading && (
                    <div className="progress-bar-container">
                      <div className="progress-bar" style={{ width: `${fileWrapper.progress}%` }}></div>
                    </div>
                  )}
                  <button className="cancel-file-btn" onClick={() => handleRemoveFile(fileWrapper.id)} disabled={isUploading}>
                    <FaTimes />
                  </button>
                </div>
              ))}
            </div>
          )}

          <div className="dialog-actions">
            <Button
              disabled={isUploading}
              onClick={handleReset}
              sx={{ color: "#2196f3" }}
            >
              Reset
            </Button>
            <Button
              disabled={isUploading}
              onClick={handleAdd}
              variant="contained"
              color="success"
            >
              Add
            </Button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default AddFilesDialog;