File size: 4,377 Bytes
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import * as Dialog from '@radix-ui/react-dialog'
import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '@/lib/http'
import { LoadingSkeleton } from '@/components/common/LoadingSkeleton'
import { NODE_COLOURS } from './KnowledgeGraph'
import type { GraphNode, GraphTraverseResponse } from '@/types/api'

interface Props {
  node:        GraphNode | null
  teamId:      string
  onClose:     () => void
  onAskAbout:  (query: string) => void
}

async function fetchTraverse(node: GraphNode, teamId: string): Promise<GraphTraverseResponse> {
  const params = new URLSearchParams({
    type:    node.label.toLowerCase(),
    name:    node.name,
    team_id: teamId,
  })
  const res = await apiFetch(`/graph/traverse?${params}`)
  return res.json()
}

export function GraphNodeDetailPanel({ node, teamId, onClose, onAskAbout }: Props) {
  const { data, isLoading, isError } = useQuery({
    queryKey:  ['graph-traverse', node?.label, node?.name, teamId],
    queryFn:   () => fetchTraverse(node!, teamId),
    enabled:   !!node,
    staleTime: 60_000,
    retry:     1,
  })

  const colour = node ? (NODE_COLOURS[node.label] ?? '#94a3b8') : '#94a3b8'

  return (
    <Dialog.Root open={!!node} onOpenChange={(open) => { if (!open) onClose() }}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm" />
        <Dialog.Content
          className="fixed right-0 top-0 z-50 flex h-full w-full max-w-sm flex-col border-l border-surface-subtle bg-white shadow-xl dark:bg-stone-900"
          aria-describedby="panel-desc"
        >
          {node && (
            <>
              {/* Header */}
              <div className="flex items-start justify-between border-b border-surface-subtle p-5">
                <div>
                  <Dialog.Title className="text-base font-semibold text-stone-900 dark:text-stone-100">
                    {node.name}
                  </Dialog.Title>
                  <span
                    className="mt-1 inline-block rounded px-2 py-0.5 text-xs font-medium text-white"
                    style={{ backgroundColor: colour }}
                  >
                    {node.label}
                  </span>
                </div>
                <Dialog.Close asChild>
                  <button
                    className="rounded p-1 text-stone-400 hover:text-stone-700 dark:hover:text-stone-200"
                    aria-label="Close panel"
                  >

                  </button>
                </Dialog.Close>
              </div>

              {/* Body */}
              <div id="panel-desc" className="flex-1 overflow-y-auto p-5">
                <p className="mb-3 text-xs font-semibold uppercase tracking-wide text-stone-400">
                  Related documents
                </p>

                {isLoading && <LoadingSkeleton rows={3} />}

                {isError && (
                  <p className="text-sm text-stone-500">Could not load related documents.</p>
                )}

                {data && data.chunks.length === 0 && (
                  <p className="text-sm text-stone-500">No related documents found.</p>
                )}

                {data && data.chunks.length > 0 && (
                  <div className="flex flex-col gap-3">
                    {data.chunks.slice(0, 6).map((chunk, i) => (
                      <div
                        key={i}
                        className="rounded-lg border border-surface-subtle p-3 text-sm text-stone-700 dark:text-stone-300"
                      >
                        {chunk.length > 200 ? chunk.slice(0, 200) + '…' : chunk}
                      </div>
                    ))}
                  </div>
                )}
              </div>

              {/* Footer */}
              <div className="border-t border-surface-subtle p-4">
                <button
                  onClick={() => {
                    onClose()
                    onAskAbout(`Tell me about ${node.name}`)
                  }}
                  className="w-full rounded-lg bg-brand py-2.5 text-sm font-medium text-white hover:bg-brand-dark"
                >
                  Ask about {node.name}
                </button>
              </div>
            </>
          )}
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}