Jaspers Hub

jaspers / agents

pluginOfficialReviewed★ 0↓ 1

Research Agents: Lets users define research agents that answer chat questions from SEC filings, uploaded documents, and the web, with every figure cited.

3.4 KB
1import { useRef, useState, type ReactElement } from 'react'
2import { DeleteButton } from './buttons'
3import { READ_TYPES, type Doc } from './docs'
4
5// The paperclip beside the message box. Hovered, it opens a menu: Attach file picks files to add, and
6// Added files opens the documents added so far, each deleted by two presses on its trash can. A press on the clip itself picks files too.
7
8const CLOSE_MS = 250
9
10export function Attach({
11 docs,
12 onFiles,
13 onDelete,
14}: {
15 docs: Doc[]
16 onFiles: (files: File[]) => void
17 onDelete: (name: string) => void
18}): ReactElement {
19 const [open, setOpen] = useState(false)
20 const [added, setAdded] = useState(false)
21 const picker = useRef<HTMLInputElement>(null)
22 const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
23
24 const hold = (): void => {
25 if (timer.current) clearTimeout(timer.current)
26 setOpen(true)
27 }
28 const release = (): void => {
29 if (timer.current) clearTimeout(timer.current)
30 timer.current = setTimeout(() => {
31 setOpen(false)
32 setAdded(false)
33 }, CLOSE_MS)
34 }
35 const pick = (): void => {
36 setOpen(false)
37 picker.current?.click()
38 }
39
40 return (
41 <div className="ag-attach" onMouseEnter={hold} onMouseLeave={release}>
42 <button type="button" className="ag-icon" aria-label="Attach" title="Attach" onClick={pick}>
43 <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
44 <path
45 d="M21 11.5 12.5 20a5 5 0 0 1-7.1-7.1l8.5-8.5a3.3 3.3 0 0 1 4.7 4.7l-8.5 8.5a1.7 1.7 0 0 1-2.4-2.4l7.8-7.8"
46 fill="none"
47 stroke="currentColor"
48 strokeWidth="1.8"
49 strokeLinecap="square"
50 />
51 </svg>
52 </button>
53 {open && (
54 <div className="ag-menu" role="menu">
55 <button type="button" role="menuitem" className="ag-menu-item" onClick={pick}>
56 Attach file
57 </button>
58 <div
59 role="menuitem"
60 aria-haspopup="menu"
61 className="ag-menu-item ag-has-sub"
62 onMouseEnter={() => setAdded(true)}
63 onMouseLeave={() => setAdded(false)}
64 >
65 <span>Added files</span>
66 <span className="ag-spacer" />
67 <span className="ag-muted">{docs.length} ▸</span>
68 {added && (
69 <div className="ag-menu ag-submenu" role="menu">
70 {docs.length === 0 && (
71 <div className="ag-menu-row ag-muted">None yet. Drop files on the view, or Attach file.</div>
72 )}
73 {docs.map((doc) => (
74 <div key={doc.id} className="ag-menu-row" title={doc.id}>
75 <span className="ag-line">{doc.name}</span>
76 <span className="ag-muted">{sizeOf(doc.size)}</span>
77 <DeleteButton label={`Delete ${doc.name}`} onDelete={() => onDelete(doc.id)} />
78 </div>
79 ))}
80 </div>
81 )}
82 </div>
83 </div>
84 )}
85 <input
86 ref={picker}
87 type="file"
88 multiple
89 hidden
90 accept={READ_TYPES.map((t) => `.${t}`).join(',')}
91 onChange={(e) => {
92 const files = [...(e.target.files ?? [])]
93 e.target.value = ''
94 onFiles(files)
95 }}
96 />
97 </div>
98 )
99}
100
101function sizeOf(bytes: number): string {
102 return bytes < 1024
103 ? `${bytes} B`
104 : bytes < 1024 * 1024
105 ? `${Math.round(bytes / 1024)} KB`
106 : `${(bytes / 1024 / 1024).toFixed(1)} MB`
107}