Research Agents: Lets you define research agents that chat and answer with citations from SEC filings, your documents, and the web, via Jaspers' research-store service.
| 1 | import { useEffect, useState, type ReactElement, type ReactNode } from 'react' |
| 2 | |
| 3 | // The view's small buttons, icons named for screen readers and on hover. A delete takes a second |
| 4 | // press, since a view's frame shows no dialog to confirm with. |
| 5 | |
| 6 | const CONFIRM_MS = 3000 |
| 7 | |
| 8 | export function IconButton({ |
| 9 | label, |
| 10 | onClick, |
| 11 | className, |
| 12 | children, |
| 13 | }: { |
| 14 | label: string |
| 15 | onClick: () => void |
| 16 | className?: string |
| 17 | children: ReactNode |
| 18 | }): ReactElement { |
| 19 | return ( |
| 20 | <button |
| 21 | type="button" |
| 22 | className={className ? `ag-icon ${className}` : 'ag-icon'} |
| 23 | aria-label={label} |
| 24 | title={label} |
| 25 | onClick={onClick} |
| 26 | > |
| 27 | <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"> |
| 28 | {children} |
| 29 | </svg> |
| 30 | </button> |
| 31 | ) |
| 32 | } |
| 33 | |
| 34 | /** The + that starts a new one of what its list holds, at the end of its bar. */ |
| 35 | export function AddButton({ label, onClick }: { label: string; onClick: () => void }): ReactElement { |
| 36 | return ( |
| 37 | <IconButton label={label} onClick={onClick} className="ag-add"> |
| 38 | <path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" strokeWidth="1.8" /> |
| 39 | </IconButton> |
| 40 | ) |
| 41 | } |
| 42 | |
| 43 | /** A trash can that turns red on a press: a second press within a few seconds deletes. */ |
| 44 | export function DeleteButton({ label, onDelete }: { label: string; onDelete: () => void }): ReactElement { |
| 45 | const [armed, setArmed] = useState(false) |
| 46 | useEffect(() => { |
| 47 | if (!armed) return |
| 48 | const timer = setTimeout(() => setArmed(false), CONFIRM_MS) |
| 49 | return () => clearTimeout(timer) |
| 50 | }, [armed]) |
| 51 | return ( |
| 52 | <IconButton |
| 53 | label={armed ? `${label}: press again` : label} |
| 54 | className={armed ? 'ag-danger' : undefined} |
| 55 | onClick={() => { |
| 56 | if (armed) { |
| 57 | setArmed(false) |
| 58 | onDelete() |
| 59 | } else { |
| 60 | setArmed(true) |
| 61 | } |
| 62 | }} |
| 63 | > |
| 64 | <path |
| 65 | d="M4.5 6.5h15M9.5 6.5V4.5h5v2M6.5 6.5l1 13h9l1-13M10.5 10v6M13.5 10v6" |
| 66 | fill="none" |
| 67 | stroke="currentColor" |
| 68 | strokeWidth="1.6" |
| 69 | /> |
| 70 | </IconButton> |
| 71 | ) |
| 72 | } |
| 73 | |
| 74 | export function EditButton({ label, onClick }: { label: string; onClick: () => void }): ReactElement { |
| 75 | return ( |
| 76 | <IconButton label={label} onClick={onClick}> |
| 77 | <path d="M15 5.5 18.5 9 9 18.5H5.5V15ZM12.5 8l3.5 3.5" fill="none" stroke="currentColor" strokeWidth="1.6" /> |
| 78 | </IconButton> |
| 79 | ) |
| 80 | } |
| 81 | |
| 82 | export function RefreshButton({ label, onClick }: { label: string; onClick: () => void }): ReactElement { |
| 83 | return ( |
| 84 | <IconButton label={label} onClick={onClick}> |
| 85 | <path d="M18.5 12a6.5 6.5 0 1 1-2-4.7M18.5 4.5v4h-4" fill="none" stroke="currentColor" strokeWidth="1.6" /> |
| 86 | </IconButton> |
| 87 | ) |
| 88 | } |
| 89 | |
| 90 | export function StopButton({ label, onClick }: { label: string; onClick: () => void }): ReactElement { |
| 91 | return ( |
| 92 | <IconButton label={label} onClick={onClick}> |
| 93 | <rect x="7" y="7" width="10" height="10" fill="currentColor" /> |
| 94 | </IconButton> |
| 95 | ) |
| 96 | } |
| 97 | |
| 98 | /** Copies on a press, and shows a check for a moment once it has. */ |
| 99 | export function CopyButton({ label, onCopy }: { label: string; onCopy: () => Promise<void> }): ReactElement { |
| 100 | const [copied, setCopied] = useState(false) |
| 101 | useEffect(() => { |
| 102 | if (!copied) return |
| 103 | const timer = setTimeout(() => setCopied(false), CONFIRM_MS / 2) |
| 104 | return () => clearTimeout(timer) |
| 105 | }, [copied]) |
| 106 | return ( |
| 107 | <IconButton |
| 108 | label={copied ? 'Copied' : label} |
| 109 | onClick={() => |
| 110 | void onCopy().then( |
| 111 | () => setCopied(true), |
| 112 | () => {}, |
| 113 | ) |
| 114 | } |
| 115 | > |
| 116 | {copied ? ( |
| 117 | <path d="M5 12.5 10 17.5 19 7" fill="none" stroke="currentColor" strokeWidth="1.8" /> |
| 118 | ) : ( |
| 119 | <path d="M8.5 8.5h11v11h-11zM15.5 8.5v-4h-11v11h4" fill="none" stroke="currentColor" strokeWidth="1.6" /> |
| 120 | )} |
| 121 | </IconButton> |
| 122 | ) |
| 123 | } |