import { useState, useEffect, useRef } from 'react' import { Box, TextField, List, ListItem, Divider, IconButton, Tooltip } from '@mui/material' import { Search, Delete, Add, Refresh, CheckBox, CheckBoxOutlineBlank, Info, Person, DataArray } from '@mui/icons-material' import { useI18nStore } from '../../../stores/i18n.store' import { useRedisStateStore } from '../../../stores/redis-state.store' import { parseRedisVersion } from '../../../../core/redis-version' import { useCommonStore } from '../../../stores/common.store' import { request } from '../../../stores/socket.service' import { KeyTypeProps, createPaging, rePaging, Paging } from './key-type-base' import KeyPagerInline from './KeyPagerInline' import P3xrAccordion from '../../../components/P3xrAccordion' import P3xrButton from '../../../components/P3xrButton' export default function KeyVectorset({ response, value, keyName, onRefresh }: KeyTypeProps) { const strings = useI18nStore(s => s.strings) const connection = useRedisStateStore(s => s.connection) const { toast, confirm } = useCommonStore() const readonly = connection?.readonly === true const [elements, setElements] = useState([]) const [paging, setPaging] = useState(() => createPaging(0)) const [simResults, setSimResults] = useState>([]) const [autoRefresh, setAutoRefresh] = useState(false) const [elementInput, setElementInput] = useState('') const [vectorInput, setVectorInput] = useState('') const [simCountInput, setSimCountInput] = useState(10) const [simFilterInput, setSimFilterInput] = useState('') const [simSearchInput, setSimSearchInput] = useState('') const [showAddForm, setShowAddForm] = useState(false) const autoRefreshRef = useRef(null) // Parse info directly from value — no effect needed, computed on render const infoItems = (() => { try { let info: any if (typeof value === 'object' && value !== null && !ArrayBuffer.isView(value)) { info = value } else if (typeof value === 'string') { info = JSON.parse(value) } else if (ArrayBuffer.isView(value)) { info = JSON.parse(new TextDecoder().decode(value)) } if (info && typeof info === 'object') { return Object.entries(info).map(([key, value]) => ({ key, value })) } } catch {} return [] })() useEffect(() => { if (autoRefresh) { autoRefreshRef.current = setInterval(() => { onRefresh(); loadElements() }, 10000) } else { clearInterval(autoRefreshRef.current) } return () => clearInterval(autoRefreshRef.current) }, [autoRefresh]) useEffect(() => { loadElements() }, [keyName]) async function loadElements() { try { const resp: any = await request({ action: 'vectorset/elements', payload: { key: keyName }, }) const elems = resp.elements || [] setElements(elems) const p = rePaging(paging, elems.length) setPaging(p) } catch { setElements([]) } } async function searchByElement() { if (!simSearchInput.trim()) return try { const resp: any = await request({ action: 'vectorset/sim', payload: { key: keyName, mode: 'element', element: simSearchInput.trim(), count: simCountInput, filter: simFilterInput.trim() || undefined }, }) setSimResults(resp.results || []) toast(strings?.page?.key?.vectorset?.searchComplete || 'Search complete') } catch (e: any) { toast(e.message || 'Error') } } async function searchByVector() { if (!simSearchInput.trim()) return try { const values = simSearchInput.split(',').map(Number) const resp: any = await request({ action: 'vectorset/sim', payload: { key: keyName, mode: 'vector', values, count: simCountInput, filter: simFilterInput.trim() || undefined }, }) setSimResults(resp.results || []) toast(strings?.page?.key?.vectorset?.searchComplete || 'Search complete') } catch (e: any) { toast(e.message || 'Error') } } async function getAttributes(element: string) { try { const resp: any = await request({ action: 'vectorset/getattr', payload: { key: keyName, element }, }) const attrs = resp.attributes if (attrs && Object.keys(attrs).length > 0) { toast(`${element}: ${JSON.stringify(attrs)}`) } else { toast(`${element}: ${strings?.page?.key?.vectorset?.noAttributes || 'No attributes'}`) } } catch (e: any) { if (e?.message) toast(e.message) } } async function addElement() { if (!elementInput.trim() || !vectorInput.trim()) return try { await request({ action: 'vectorset/add', payload: { key: keyName, element: elementInput.trim(), values: vectorInput.split(',').map(Number) }, }) toast(strings?.page?.key?.vectorset?.addedSuccessfully || 'Element added successfully') setElementInput('') setVectorInput('') onRefresh() loadElements() } catch (e: any) { toast(e.message || 'Error') } } async function removeElement(element: string) { try { await confirm({ message: strings?.confirm?.delete || 'Delete?' }) await request({ action: 'vectorset/remove', payload: { key: keyName, element }, }) toast(strings?.page?.key?.vectorset?.deletedSuccessfully || 'Element deleted successfully') onRefresh() loadElements() } catch (e: any) { if (e?.message) toast(e.message) } } async function searchByElementDirect(element: string) { try { const resp: any = await request({ action: 'vectorset/sim', payload: { key: keyName, mode: 'element', element, count: simCountInput }, }) setSimResults(resp.results || []) toast(strings?.page?.key?.vectorset?.searchComplete || 'Search complete') } catch (e: any) { toast(e.message || 'Error') } } return ( {/* INFO */}
: } label={strings?.label?.autoRefresh || 'Auto'} breakpoint={1280} onClick={(e) => { e.stopPropagation(); setAutoRefresh(v => !v) }} /> {!autoRefresh && ( } label={strings?.intention?.refresh || 'Refresh'} breakpoint={1280} onClick={(e) => { e.stopPropagation(); onRefresh(); loadElements() }} /> )} } > {infoItems.map((item, i) => ( {item.key} {String(item.value)} {i < infoItems.length - 1 && } ))} {/* ELEMENTS */}
setPaging(p)} /> {strings?.page?.key?.vectorset?.element || 'Element'} {strings?.page?.key?.vectorset?.score || 'Score'} {!readonly && ( setShowAddForm(v => !v)} /> )} {elements.slice(paging.startIndex, paging.startIndex + paging.pageCount).map((elem: any, i: number) => ( {elem.element} {elem.score?.toFixed(4)} { setSimSearchInput(elem.element) searchByElementDirect(elem.element) }} /> getAttributes(elem.element)} /> {!readonly && ( removeElement(elem.element)} /> )} ))} {!readonly && showAddForm && ( setElementInput(e.target.value)} onKeyUp={e => e.key === 'Enter' && addElement()} /> setVectorInput(e.target.value)} onKeyUp={e => e.key === 'Enter' && addElement()} /> } label={strings?.intention?.add || 'Add'} raised color="primary" onClick={addElement} /> )} {/* SIMILARITY SEARCH */}
setSimSearchInput(e.target.value)} onKeyUp={e => e.key === 'Enter' && searchByElement()} /> setSimCountInput(parseInt(e.target.value) || 10)} /> {parseRedisVersion(useRedisStateStore.getState().info?.server?.redis_version).isAtLeast(8, 2) && ( setSimFilterInput(e.target.value)} onKeyUp={e => e.key === 'Enter' && searchByElement()} /> )} } label={strings?.page?.key?.vectorset?.byElement || 'By Element'} raised color="primary" onClick={searchByElement} /> } label={strings?.page?.key?.vectorset?.byVector || 'By Vector'} raised color="primary" onClick={searchByVector} /> {simResults.length > 0 && ( <> {simResults.map((entry, i) => ( {entry.element} {entry.score} {i < simResults.length - 1 && } ))} )}
) }