11import React , { useEffect , useState } from 'react' ;
22import Plotly from 'plotly.js' ;
3+ const PCA = require ( 'ml-pca' ) ;
4+ import { Binary } from 'mongodb' ;
35
4- type HoverInfo = {
5- x : number ;
6- y : number ;
7- text : string ;
8- } | null ;
6+ type HoverInfo = { x : number ; y : number ; text : string } | null ;
97
10- export const VectorVisualizer : React . FC = ( ) => {
8+ export interface VectorVisualizerProps {
9+ dataService : {
10+ find : (
11+ ns : string ,
12+ filter : Record < string , unknown > ,
13+ options ?: { limit ?: number }
14+ ) => Promise < any [ ] > ;
15+ } ;
16+ collection : { namespace : string } ;
17+ }
18+
19+ function normalizeTo2D ( vectors : Binary [ ] ) : { x : number ; y : number } [ ] {
20+ const raw = vectors . map ( ( v ) => Array . from ( v . toFloat32Array ( ) ) ) ;
21+ const pca = new PCA ( raw ) ;
22+ const reduced = pca . predict ( raw , { nComponents : 2 } ) . to2DArray ( ) ;
23+ return reduced . map ( ( [ x , y ] : [ number , number ] ) => ( { x, y } ) ) ;
24+ }
25+
26+ export const VectorVisualizer : React . FC < VectorVisualizerProps > = ( {
27+ dataService,
28+ collection,
29+ } ) => {
1130 const [ hoverInfo , setHoverInfo ] = useState < HoverInfo > ( null ) ;
1231
1332 useEffect ( ( ) => {
@@ -17,77 +36,93 @@ export const VectorVisualizer: React.FC = () => {
1736 let isMounted = true ;
1837
1938 const plot = async ( ) => {
20- await Plotly . newPlot (
21- container ,
22- [
23- {
24- x : [ 1 , 2 , 3 , 4 , 5 ] ,
25- y : [ 10 , 15 , 13 , 17 , 12 ] ,
26- mode : 'markers' ,
27- type : 'scatter' ,
28- name : 'baskd' ,
29- text : [ 'doc1' , 'doc2' , 'doc3' , 'doc4' , 'doc5' ] ,
30- hoverinfo : 'none' ,
31- marker : {
32- size : 15 ,
33- color : 'teal' ,
34- line : { width : 1 , color : '#fff' } ,
39+ try {
40+ const ns = collection ?. namespace ;
41+ if ( ! ns || ! dataService ) return ;
42+
43+ const docs = await dataService . find ( ns , { } , { limit : 1000 } ) ;
44+ const vectors = docs . map ( ( doc ) => doc . review_vec ) . filter ( Boolean ) ;
45+
46+ if ( ! vectors . length ) return ;
47+
48+ const points = normalizeTo2D ( vectors ) ;
49+
50+ await Plotly . newPlot (
51+ container ,
52+ [
53+ {
54+ x : points . map ( ( p ) => p . x ) ,
55+ y : points . map ( ( p ) => p . y ) ,
56+ mode : 'markers' ,
57+ type : 'scatter' ,
58+ text : docs . map ( ( doc ) => doc . review || '[no text]' ) ,
59+ hoverinfo : 'none' ,
60+ marker : {
61+ size : 12 ,
62+ color : 'teal' ,
63+ line : { width : 1 , color : '#fff' } ,
64+ } ,
3565 } ,
66+ ] ,
67+ {
68+ hovermode : 'closest' ,
69+ margin : { l : 40 , r : 10 , t : 30 , b : 30 } ,
70+ plot_bgcolor : '#f9f9f9' ,
71+ paper_bgcolor : '#f9f9f9' ,
3672 } ,
37- ] ,
38- {
39- margin : { l : 40 , r : 10 , t : 40 , b : 40 } ,
40- hovermode : 'closest' ,
41- hoverdistance : 30 ,
42- dragmode : 'zoom' ,
43- plot_bgcolor : '#f7f7f7' ,
44- paper_bgcolor : '#f7f7f7' ,
45- xaxis : { gridcolor : '#e0e0e0' } ,
46- yaxis : { gridcolor : '#e0e0e0' } ,
47- } ,
48- { responsive : true }
49- ) ;
50-
51- const handleHover = ( data : any ) => {
52- const point = data . points ?. [ 0 ] ;
53- if ( ! point ) return ;
54-
55- const containerRect = container . getBoundingClientRect ( ) ;
56- const relX = data . event . clientX - containerRect . left ;
57- const relY = data . event . clientY - containerRect . top ;
58-
59- if ( isMounted ) {
60- setHoverInfo ( { x : relX , y : relY , text : point . text } ) ;
61- }
62- } ;
63-
64- const handleUnhover = ( ) => {
65- if ( isMounted ) {
66- setHoverInfo ( null ) ;
67- }
68- } ;
69-
70- container . addEventListener ( 'plotly_hover' , handleHover ) ;
71- container . addEventListener ( 'plotly_unhover' , handleUnhover ) ;
72-
73- // Cleanup
74- return ( ) => {
75- isMounted = false ;
76- container . removeEventListener ( 'plotly_hover' , handleHover ) ;
77- container . removeEventListener ( 'plotly_unhover' , handleUnhover ) ;
78- } ;
73+ { responsive : true }
74+ ) ;
75+
76+ const handleHover = ( event : Event ) => {
77+ const e = event as CustomEvent < {
78+ points : { text : string } [ ] ;
79+ event : MouseEvent ;
80+ } > ;
81+
82+ const point = e . detail ?. points ?. [ 0 ] ;
83+ const mouse = e . detail ?. event ;
84+ if ( ! point || ! mouse ) return ;
85+
86+ const rect = container . getBoundingClientRect ( ) ;
87+ setHoverInfo ( {
88+ x : mouse . clientX - rect . left ,
89+ y : mouse . clientY - rect . top ,
90+ text : point . text ,
91+ } ) ;
92+ } ;
93+
94+ const handleUnhover = ( ) => setHoverInfo ( null ) ;
95+
96+ container . addEventListener (
97+ 'plotly_hover' ,
98+ handleHover as EventListener
99+ ) ;
100+ container . addEventListener (
101+ 'plotly_unhover' ,
102+ handleUnhover as EventListener
103+ ) ;
104+
105+ return ( ) => {
106+ container . removeEventListener (
107+ 'plotly_hover' ,
108+ handleHover as EventListener
109+ ) ;
110+ container . removeEventListener (
111+ 'plotly_unhover' ,
112+ handleUnhover as EventListener
113+ ) ;
114+ } ;
115+ } catch ( err ) {
116+ console . error ( 'VectorVisualizer error:' , err ) ;
117+ }
79118 } ;
80119
81- let cleanup : ( ( ) => void ) | undefined ;
82- void plot ( ) . then ( ( c ) => {
83- if ( typeof c === 'function' ) cleanup = c ;
84- } ) ;
120+ void plot ( ) ;
85121
86122 return ( ) => {
87123 isMounted = false ;
88- if ( cleanup ) cleanup ( ) ;
89124 } ;
90- } , [ ] ) ;
125+ } , [ collection ?. namespace , dataService ] ) ;
91126
92127 return (
93128 < div style = { { position : 'relative' , width : '100%' , height : '100%' } } >
@@ -103,8 +138,8 @@ export const VectorVisualizer: React.FC = () => {
103138 padding : '4px 8px' ,
104139 borderRadius : 4 ,
105140 pointerEvents : 'none' ,
106- whiteSpace : 'nowrap' ,
107141 zIndex : 1000 ,
142+ whiteSpace : 'nowrap' ,
108143 } }
109144 >
110145 { hoverInfo . text }
0 commit comments