mattrguerra/cortexmainPrivate on GitHub, published here in full

SearchPalette.tsx

Language TypeScript2.7 KB82 lines

src/components/SearchPalette.tsx

  1. import { useEffect, useRef, useState } from 'react'
  2. import type { SearchHit } from '../ipc/gen/SearchHit'
  3. import { relativeDay, snippetParts } from '../lib/format'
  4. import { useStore } from '../state/store'
  5. export function SearchPalette() {
  6. const [q, setQ] = useState('')
  7. const [hits, setHits] = useState<SearchHit[]>([])
  8. const [cursor, setCursor] = useState(0)
  9. const client = useStore((s) => s.client)
  10. const open = useStore((s) => s.open)
  11. const setPanel = useStore((s) => s.setPanel)
  12. const input = useRef<HTMLInputElement>(null)
  13. useEffect(() => input.current?.focus(), [])
  14. useEffect(() => {
  15. if (!client) return
  16. const t = setTimeout(
  17. () =>
  18. void client.search(q).then((h) => {
  19. setHits(h)
  20. setCursor(0)
  21. }),
  22. 120,
  23. )
  24. return () => clearTimeout(t)
  25. }, [q, client])
  26. return (
  27. <div className="overlay" onMouseDown={(e) => e.target === e.currentTarget && setPanel(null)}>
  28. <div
  29. className="palette glass-strong"
  30. role="dialog"
  31. aria-label="Search chats"
  32. onKeyDown={(e) => {
  33. if (e.key === 'ArrowDown') {
  34. e.preventDefault()
  35. setCursor((c) => Math.min(c + 1, hits.length - 1))
  36. }
  37. if (e.key === 'ArrowUp') {
  38. e.preventDefault()
  39. setCursor((c) => Math.max(c - 1, 0))
  40. }
  41. if (e.key === 'Enter' && hits[cursor]) void open(hits[cursor].conversation_id)
  42. }}
  43. >
  44. <input
  45. ref={input}
  46. className="search-input big"
  47. placeholder="Search every chat"
  48. value={q}
  49. onChange={(e) => setQ(e.target.value)}
  50. aria-label="Search every chat"
  51. data-testid="search-input"
  52. />
  53. <ul className="hits" role="listbox">
  54. {hits.map((h, i) => (
  55. <li
  56. key={`${h.conversation_id}-${h.message_id ?? 't'}`}
  57. role="option"
  58. aria-selected={i === cursor}
  59. className={i === cursor ? 'cursor' : ''}
  60. onMouseEnter={() => setCursor(i)}
  61. onClick={() => void open(h.conversation_id)}
  62. >
  63. <div className="hit-head">
  64. <strong>{h.title}</strong>
  65. <span className="muted small">{relativeDay(h.created_at)}</span>
  66. </div>
  67. <div className="hit-snippet">
  68. {snippetParts(h.snippet).map((p, j) =>
  69. p.hit ? <mark key={j}>{p.text}</mark> : <span key={j}>{p.text}</span>,
  70. )}
  71. </div>
  72. </li>
  73. ))}
  74. {q.trim() && hits.length === 0 && <li className="empty-row">Nothing found.</li>}
  75. </ul>
  76. </div>
  77. </div>
  78. )
  79. }