File size: 4,304 Bytes
7c5ed63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { useImportZip } from '../../hooks/useSession';

/** True for the file-like drag payloads we care about; filters out text /
 *  link drags so the overlay doesn't hijack ordinary in-page drags. */
function isFileDrag(dt: DataTransfer | null): boolean {
  return dt !== null && Array.from(dt.types).includes('Files');
}

/** Cheap client-side zip check for immediate feedback. The server still
 *  validates the bytes and bundle shape; this only gates the drop. */
export function isZipFile(file: { name: string; type: string }): boolean {
  const name = file.name.toLowerCase();
  return (
    name.endsWith('.zip') ||
    file.type === 'application/zip' ||
    file.type === 'application/x-zip-compressed'
  );
}

/**
 * Window-level drop target for importing a `/export-debug-zip` bundle.
 *
 * Renders nothing until a file is dragged over the window, then shows a
 * full-screen overlay. Dropping a `.zip` posts it to the import endpoint and
 * navigates to the imported session; non-zip files get an alert and are
 * ignored. The pointer-events are disabled so the overlay itself never swallows
 * the drop — the window listener owns the interaction.
 */
export function ZipDropOverlay() {
  const navigate = useNavigate();
  const { mutateAsync: importZip, isPending: importing } = useImportZip();
  const [dragging, setDragging] = useState(false);
  // Count nested enter/leave pairs so dragging over a child element doesn't
  // briefly drop the counter to zero and flicker the overlay.
  const depth = useRef(0);

  useEffect(() => {
    async function importFile(file: File) {
      try {
        const result = await importZip(file);
        void navigate(`/sessions/${result.sessionId}`);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        window.alert(`Import failed: ${message}`);
      }
    }

    function onDragEnter(e: DragEvent) {
      if (!isFileDrag(e.dataTransfer)) return;
      e.preventDefault();
      depth.current += 1;
      setDragging(true);
    }
    function onDragOver(e: DragEvent) {
      if (!isFileDrag(e.dataTransfer)) return;
      // Required — without preventDefault the browser cancels the drag and
      // never fires `drop`.
      e.preventDefault();
      if (e.dataTransfer !== null) e.dataTransfer.dropEffect = 'copy';
    }
    function onDragLeave(e: DragEvent) {
      if (!isFileDrag(e.dataTransfer)) return;
      e.preventDefault();
      depth.current = Math.max(0, depth.current - 1);
      if (depth.current === 0) setDragging(false);
    }
    function onDrop(e: DragEvent) {
      // Gate on file drags first so non-file drops (e.g. selected text or a
      // URL into the search input) keep their native behavior.
      if (!isFileDrag(e.dataTransfer)) return;
      e.preventDefault();
      depth.current = 0;
      setDragging(false);
      const file = e.dataTransfer?.files[0];
      if (file === undefined) return;
      if (!isZipFile(file)) {
        window.alert('Please drop a .zip bundle exported from kimi-code (/export-debug-zip).');
        return;
      }
      void importFile(file);
    }

    window.addEventListener('dragenter', onDragEnter);
    window.addEventListener('dragover', onDragOver);
    window.addEventListener('dragleave', onDragLeave);
    window.addEventListener('drop', onDrop);
    return () => {
      window.removeEventListener('dragenter', onDragEnter);
      window.removeEventListener('dragover', onDragOver);
      window.removeEventListener('dragleave', onDragLeave);
      window.removeEventListener('drop', onDrop);
    };
  }, [importZip, navigate]);

  if (!dragging && !importing) return null;

  return (
    <div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/50">
      <div className="border-2 border-dashed border-border-strong bg-surface-1 px-10 py-8 text-center">
        <div className="font-mono text-[13px] text-fg-0">
          {importing ? 'importing debug zip…' : 'drop debug zip to import'}
        </div>
        <div className="mt-2 font-mono text-[11px] text-fg-3">
          from kimi-code /export-debug-zip
        </div>
      </div>
    </div>
  );
}