main
reng 1 month ago
parent 6298cc25ca
commit 506a98cb7d
  1. 7
      v2/app/package-lock.json
  2. 1
      v2/app/package.json
  3. 57
      v2/app/src/components/graph.jsx
  4. 99
      v2/app/src/components/point.jsx
  5. 25
      v2/app/src/utils/draw.js
  6. 2
      v2/app/src/utils/parsing.js

@ -15,6 +15,7 @@
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-opener": "^2",
"gsap": "^3.14.2",
"html-to-text": "^9.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@ -2302,6 +2303,12 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/gsap": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
"integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/hls.js": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",

@ -17,6 +17,7 @@
"@tauri-apps/plugin-fs": "^2.4.4",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-opener": "^2",
"gsap": "^3.14.2",
"html-to-text": "^9.0.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",

@ -1,15 +1,18 @@
import { Canvas } from '@react-three/fiber'
import * as THREE from 'three';
import { useEffect, useState } from 'react'
import { get3dCoordinates } from '../utils/draw';
import { Box, Html, OrbitControls, PerspectiveCamera, Bounds, useBounds } from '@react-three/drei'
import { generateLinesFromPoints, get3dCoordinates } from '../utils/draw';
import { OrbitControls, PerspectiveCamera, Bounds, useBounds, Line } from '@react-three/drei'
import Point from './point.jsx';
import * as THREE from 'three';
const PointScale=200;
const PointSize=6;
export default function Graph({results}){
const [points, setPoints]=useState();
const [lines, setLines]=useState();
const [showContent, setShowContent]=useState(false);
const bounds = useBounds();
function zoomToFit(){
@ -38,6 +41,11 @@ export default function Graph({results}){
get3dCoordinates(results.map(item=>item.vector)).then(newPoints=>{
console.log("3D Points:", newPoints);
setPoints(newPoints);
generateLinesFromPoints(newPoints).then(newLines=>{
console.log("Lines:", newLines);
setLines(newLines);
});
});
@ -45,7 +53,11 @@ export default function Graph({results}){
return (<>
<button className='self-end text-xs' onClick={zoomToFit}>Zoom to Fit</button>
<div className='flex flex-row items-center'>
<input type="checkbox" id="toggle-content" className='self-end mr-2' defaultChecked={showContent}
onChange={(e)=>setShowContent(e.target.checked)} />
<label htmlFor="toggle-content" className='self-end text-xs mr-4'>Show Content</label>
</div>
<Canvas className='w-full aspect-[16/9] border border-gray-300'>
<color attach="background" args={['#000000']} />
{/* <PerspectiveCamera makeDefault position={[20, 20, 20]} /> */}
@ -56,29 +68,22 @@ export default function Graph({results}){
<Bounds fit clip observe margin={1.2}>
<group>
{Array.isArray(points) && points?.map((point, index)=>(
<mesh key={index} position={[point[0]*PointScale, point[1]*PointScale, point[2]*PointScale]}>
<sphereGeometry args={[PointSize, 16, 16]} />
<meshStandardMaterial
color={new THREE.Color().setHSL(index / points.length, 0.7, 0.5)}
emissive={new THREE.Color().setHSL(index / points.length, 0.7, 0.2)}
emissiveIntensity={0.5}
/>
<Html>
<div className='bg-white p-2 border border-gray-300 rounded shadow-lg w-[20vw] select-none'>
<pre className='text-xs whitespace-pre-wrap'>{(()=>{
let p=JSON.parse(results[index].payload.text);
return `${p.content}\nKeywords: ${p.keywords.join(", ")}`;
})()}</pre>
</div>
</Html>
</mesh>
<Point key={index} point={point} index={index} totalPoints={points.length} result={results[index]}
showContent={showContent}/>
))}
</group>
<group>
{Array.isArray(lines) && lines?.map((line, index)=>(
<Line
key={index}
points={line} // Array of [x, y, z] or Three.Vector3
color="white"
lineWidth={0.5}
/>
))}
</group>
</Bounds>
{/* <Box position={[0,0,0]} args={[1,1,1]}>
<meshStandardMaterial color={'blue'} />
</Box> */}
{/* <gridHelper args={[50, 50, 0xAAAAAA, 0xCCCCCC]} position={[0, -5, 0]} /> */}
</Canvas>
</>
)

@ -0,0 +1,99 @@
import { Html } from '@react-three/drei'
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import gsap from 'gsap';
export const PointScale=200;
const PointSize=6;
const PointDuration=5;
const KeywordOffset=0;
export default function Point({point, index, totalPoints, result, showContent}) {
const [payload, setPayload]=useState();
const [showPayload, setShowPayload]=useState(false);
const meshRef=useRef();
const refText=useRef();
useEffect(()=>{
if(!payload) return;
const lifeTime=payload.number/payload.total;
console.log("Point lifeTime:", lifeTime);
// animate point size based on lifeTime
const targetSize=PointSize *0.2;
const initialSize=0;
const timeline=gsap.timeline({ repeat:-1});
timeline.set(meshRef.current.scale, {
x: initialSize, y: initialSize, z: initialSize,
});
timeline.set(refText.current, {
scale: initialSize,
opacity: 0,
});
timeline.fromTo(meshRef.current.scale, {
x: initialSize, y: initialSize, z: initialSize,
}, {
x: PointSize, y: PointSize, z: PointSize,
duration:PointDuration,
delay: lifeTime *5,
ease: "power3.in",
onUpdate:()=>{
if(refText.current){
refText.current.style.transform=`scale(${meshRef.current.scale.x / PointSize})`;
refText.current.style.opacity=`${meshRef.current.scale.x / PointSize}`;
}
}
});
timeline.to(meshRef.current.scale, {
x: targetSize, y: targetSize, z: targetSize,
duration:PointDuration,
ease: "power3.out",
onUpdate:()=>{
if(refText.current){
refText.current.style.transform=`scale(${meshRef.current.scale.x / PointSize})`;
refText.current.style.opacity=`${meshRef.current.scale.x / PointSize}`;
}
}
});
timeline.play();
return ()=>{
timeline.kill();
};
},[payload]);
useEffect(()=>{
setPayload(JSON.parse(result.payload.text));
},[point]);
return (
<mesh ref={meshRef} position={[point[0]*PointScale, point[1]*PointScale, point[2]*PointScale]}>
<sphereGeometry args={[PointSize, 32, 32]} />
<meshStandardMaterial
color={new THREE.Color().setHSL(1, 0, 0.2)}
emissive={new THREE.Color().setHSL(1, 0, 0.2)}
emissiveIntensity={0.5}
/>
<Html>
<div ref={refText} className='text-white p-2 w-[20vw] select-none translate-x-[-50%] translate-y-[-50%] text-center opacity-0'>
<div className='flex flex-row justify-center flex-wrap'>
{payload?.keywords.map((el, index)=><span className='px-2' style={{transform: `translate(${Math.random() * KeywordOffset}px,${Math.random() * KeywordOffset}px)`}}>{el}</span>)}
</div>
{showContent && <pre className='text-xs w-[25vw] whitespace-pre-wrap bg-gray-300 text-black rounded p-2'>{(()=>{
return `${payload?.content}`;
})()}</pre>}
</div>
</Html>
</mesh>
)
}

@ -1,4 +1,5 @@
import { UMAP } from 'umap-js';
import { PointScale } from '../components/point';
export async function get3dCoordinates(rawPoints) {
@ -15,4 +16,28 @@ export async function get3dCoordinates(rawPoints) {
return [];
}
}
export async function generateLinesFromPoints(points){
if(!Array.isArray(points) || points.length<2){
return [];
}
const lines=[];
for(let i=0; i<points.length -1; i++){
const start=points[i];
const end=points[i+1];
const distance=Math.sqrt(
Math.pow(end[0]-start[0], 2) +
Math.pow(end[1]-start[1], 2) +
Math.pow(end[2]-start[2], 2)
);
if(distance > 20) continue; // skip if too far apart
lines.push([start.map(coord => coord * PointScale), end.map(coord => coord * PointScale)]);
}
return lines;
}

@ -172,7 +172,7 @@ async function searchQdrant(query_embeddings){
},
body: JSON.stringify({
vector: query_embeddings,
limit: 10,
limit: 50,
with_payload: true,
with_vector: true
})

Loading…
Cancel
Save