Added Minimum Degree

This commit is contained in:
baoheping
2025-03-08 09:26:21 +00:00
parent 4e59a293fe
commit 59a2202e7c
23 changed files with 705 additions and 155 deletions

View File

@@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
import { controlButtonVariant } from '@/lib/constants'
import Button from '@/components/ui/Button'
import { useTranslation } from 'react-i18next'
/**
* Component that toggles full screen mode.
*/
const FullScreenControl = () => {
const { isFullScreen, toggle } = useFullScreen()
const { t } = useTranslation()
return (
<>
{isFullScreen ? (
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon">
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
<MinimizeIcon />
</Button>
) : (
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Full Screen" size="icon">
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.fullScreen')} size="icon">
<MaximizeIcon />
</Button>
)}

View File

@@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph'
import { labelListLimit } from '@/lib/constants'
import MiniSearch from 'minisearch'
import { useTranslation } from 'react-i18next'
const lastGraph: any = {
graph: null,
@@ -13,6 +14,7 @@ const lastGraph: any = {
}
const GraphLabels = () => {
const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel()
const graph = useGraphStore.use.sigmaGraph()
@@ -69,7 +71,7 @@ const GraphLabels = () => {
return result.length <= labelListLimit
? result
: [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`]
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
},
[getSearchEngine]
)
@@ -84,14 +86,14 @@ const GraphLabels = () => {
className="ml-2"
triggerClassName="max-h-8"
searchInputClassName="max-h-8"
triggerTooltip="Select query label"
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
fetcher={fetchData}
renderOption={(item) => <div>{item}</div>}
getOptionValue={(item) => item}
getDisplayValue={(item) => <div>{item}</div>}
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
label="Label"
placeholder="Search labels..."
label={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')}
value={label !== null ? label : ''}
onChange={setQueryLabel}
/>

View File

@@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
import { searchResultLimit } from '@/lib/constants'
import { useGraphStore } from '@/stores/graph'
import MiniSearch from 'minisearch'
import { useTranslation } from 'react-i18next'
interface OptionItem {
id: string
@@ -44,6 +45,7 @@ export const GraphSearchInput = ({
onFocus?: GraphSearchInputProps['onFocus']
value?: GraphSearchInputProps['value']
}) => {
const { t } = useTranslation()
const graph = useGraphStore.use.sigmaGraph()
const searchEngine = useMemo(() => {
@@ -97,7 +99,7 @@ export const GraphSearchInput = ({
{
type: 'message',
id: messageId,
message: `And ${result.length - searchResultLimit} others`
message: t('graphPanel.search.message', { count: result.length - searchResultLimit })
}
]
},
@@ -118,7 +120,7 @@ export const GraphSearchInput = ({
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
}}
label={'item'}
placeholder="Search nodes..."
placeholder={t('graphPanel.search.placeholder')}
/>
)
}

View File

@@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
import { useSettingsStore } from '@/stores/settings'
import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
type LayoutName =
| 'Circular'
@@ -28,6 +29,7 @@ type LayoutName =
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
const sigma = useSigma()
const { stop, start, isRunning } = layout
const { t } = useTranslation()
/**
* Init component when Sigma or component settings change.
@@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
<Button
size="icon"
onClick={() => (isRunning ? stop() : start())}
tooltip={isRunning ? 'Stop the layout animation' : 'Start the layout animation'}
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
variant={controlButtonVariant}
>
{isRunning ? <PauseIcon /> : <PlayIcon />}
@@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
*/
const LayoutsControl = () => {
const sigma = useSigma()
const { t } = useTranslation()
const [layout, setLayout] = useState<LayoutName>('Circular')
const [opened, setOpened] = useState<boolean>(false)
@@ -149,7 +152,7 @@ const LayoutsControl = () => {
size="icon"
variant={controlButtonVariant}
onClick={() => setOpened((e: boolean) => !e)}
tooltip="Layout Graph"
tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
>
<GripIcon />
</Button>
@@ -166,7 +169,7 @@ const LayoutsControl = () => {
key={name}
className="cursor-pointer text-xs"
>
{name}
{t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
</CommandItem>
))}
</CommandGroup>

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
import Text from '@/components/ui/Text'
import useLightragGraph from '@/hooks/useLightragGraph'
import { useTranslation } from 'react-i18next'
/**
* Component that view properties of elements in graph.
@@ -147,21 +148,22 @@ const PropertyRow = ({
}
const NodePropertiesView = ({ node }: { node: NodeType }) => {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">{t('graphPanel.propertiesView.node.title')}</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={node.id} />
<PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
<PropertyRow
name={'Labels'}
name={t('graphPanel.propertiesView.node.labels')}
value={node.labels.join(', ')}
onClick={() => {
useGraphStore.getState().setSelectedNode(node.id, true)
}}
/>
<PropertyRow name={'Degree'} value={node.degree} />
<PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
</div>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.node.properties')}</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(node.properties)
.sort()
@@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
{node.relationships.length > 0 && (
<>
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
Relationships
{t('graphPanel.propertiesView.node.relationships')}
</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{node.relationships.map(({ type, id, label }) => {
@@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
}
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
const { t } = useTranslation()
return (
<div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">{t('graphPanel.propertiesView.edge.title')}</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={edge.id} />
{edge.type && <PropertyRow name={'Type'} value={edge.type} />}
<PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
{edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
<PropertyRow
name={'Source'}
name={t('graphPanel.propertiesView.edge.source')}
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
onClick={() => {
useGraphStore.getState().setSelectedNode(edge.source, true)
}}
/>
<PropertyRow
name={'Target'}
name={t('graphPanel.propertiesView.edge.target')}
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
onClick={() => {
useGraphStore.getState().setSelectedNode(edge.target, true)
}}
/>
</div>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.edge.properties')}</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(edge.properties)
.sort()

View File

@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useBackendState } from '@/stores/state'
import { SettingsIcon } from 'lucide-react'
import { useTranslation } from "react-i18next";
/**
* Component that displays a checkbox with a label.
@@ -195,10 +196,12 @@ export default function Settings() {
[setTempApiKey]
)
const { t } = useTranslation();
return (
<Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild>
<Button variant={controlButtonVariant} tooltip="Settings" size="icon">
<Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
<SettingsIcon />
</Button>
</PopoverTrigger>
@@ -212,7 +215,7 @@ export default function Settings() {
<LabeledCheckBox
checked={enableHealthCheck}
onCheckedChange={setEnableHealthCheck}
label="Health Check"
label={t("graphPanel.sideBar.settings.healthCheck")}
/>
<Separator />
@@ -220,12 +223,12 @@ export default function Settings() {
<LabeledCheckBox
checked={showPropertyPanel}
onCheckedChange={setShowPropertyPanel}
label="Show Property Panel"
label={t("graphPanel.sideBar.settings.showPropertyPanel")}
/>
<LabeledCheckBox
checked={showNodeSearchBar}
onCheckedChange={setShowNodeSearchBar}
label="Show Search Bar"
label={t("graphPanel.sideBar.settings.showSearchBar")}
/>
<Separator />
@@ -233,12 +236,12 @@ export default function Settings() {
<LabeledCheckBox
checked={showNodeLabel}
onCheckedChange={setShowNodeLabel}
label="Show Node Label"
label={t("graphPanel.sideBar.settings.showNodeLabel")}
/>
<LabeledCheckBox
checked={enableNodeDrag}
onCheckedChange={setEnableNodeDrag}
label="Node Draggable"
label={t("graphPanel.sideBar.settings.nodeDraggable")}
/>
<Separator />
@@ -246,28 +249,34 @@ export default function Settings() {
<LabeledCheckBox
checked={showEdgeLabel}
onCheckedChange={setShowEdgeLabel}
label="Show Edge Label"
label={t("graphPanel.sideBar.settings.showEdgeLabel")}
/>
<LabeledCheckBox
checked={enableHideUnselectedEdges}
onCheckedChange={setEnableHideUnselectedEdges}
label="Hide Unselected Edges"
label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
/>
<LabeledCheckBox
checked={enableEdgeEvents}
onCheckedChange={setEnableEdgeEvents}
label="Edge Events"
label={t("graphPanel.sideBar.settings.edgeEvents")}
/>
<Separator />
<LabeledNumberInput
label="Max Query Depth"
label={t("graphPanel.sideBar.settings.maxQueryDepth")}
min={1}
value={graphQueryMaxDepth}
onEditFinished={setGraphQueryMaxDepth}
/>
<LabeledNumberInput
label="Max Layout Iterations"
label={t("graphPanel.sideBar.settings.minDegree")}
min={0}
value={graphMinDegree}
onEditFinished={setGraphMinDegree}
/>
<LabeledNumberInput
label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
min={1}
max={20}
value={graphLayoutMaxIterations}
@@ -277,14 +286,14 @@ export default function Settings() {
<Separator />
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">API Key</label>
<label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
<div className="w-0 flex-1">
<Input
type="password"
value={tempApiKey}
onChange={handleTempApiKeyChange}
placeholder="Enter your API key"
placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
className="max-h-full w-full min-w-0"
autoComplete="off"
/>
@@ -295,7 +304,7 @@ export default function Settings() {
size="sm"
className="max-h-full shrink-0"
>
Save
{t("graphPanel.sideBar.settings.save")}
</Button>
</form>
</div>

View File

@@ -1,58 +1,60 @@
import { LightragStatus } from '@/api/lightrag'
import { useTranslation } from 'react-i18next'
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
const { t } = useTranslation()
if (!status) {
return <div className="text-muted-foreground text-sm">Status information unavailable</div>
return <div className="text-muted-foreground text-sm">{t('graphPanel.statusCard.unavailable')}</div>
}
return (
<div className="min-w-[300px] space-y-3 text-sm">
<div className="space-y-1">
<h4 className="font-medium">Storage Info</h4>
<h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>Working Directory:</span>
<span>{t('graphPanel.statusCard.workingDirectory')}:</span>
<span className="truncate">{status.working_directory}</span>
<span>Input Directory:</span>
<span>{t('graphPanel.statusCard.inputDirectory')}:</span>
<span className="truncate">{status.input_directory}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">LLM Configuration</h4>
<h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>LLM Binding:</span>
<span>{t('graphPanel.statusCard.llmBinding')}:</span>
<span>{status.configuration.llm_binding}</span>
<span>LLM Binding Host:</span>
<span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
<span>{status.configuration.llm_binding_host}</span>
<span>LLM Model:</span>
<span>{t('graphPanel.statusCard.llmModel')}:</span>
<span>{status.configuration.llm_model}</span>
<span>Max Tokens:</span>
<span>{t('graphPanel.statusCard.maxTokens')}:</span>
<span>{status.configuration.max_tokens}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">Embedding Configuration</h4>
<h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>Embedding Binding:</span>
<span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
<span>{status.configuration.embedding_binding}</span>
<span>Embedding Binding Host:</span>
<span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
<span>{status.configuration.embedding_binding_host}</span>
<span>Embedding Model:</span>
<span>{t('graphPanel.statusCard.embeddingModel')}:</span>
<span>{status.configuration.embedding_model}</span>
</div>
</div>
<div className="space-y-1">
<h4 className="font-medium">Storage Configuration</h4>
<h4 className="font-medium">{t('graphPanel.statusCard.storageConfig')}</h4>
<div className="text-muted-foreground grid grid-cols-2 gap-1">
<span>KV Storage:</span>
<span>{t('graphPanel.statusCard.kvStorage')}:</span>
<span>{status.configuration.kv_storage}</span>
<span>Doc Status Storage:</span>
<span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
<span>{status.configuration.doc_status_storage}</span>
<span>Graph Storage:</span>
<span>{t('graphPanel.statusCard.graphStorage')}:</span>
<span>{status.configuration.graph_storage}</span>
<span>Vector Storage:</span>
<span>{t('graphPanel.statusCard.vectorStorage')}:</span>
<span>{status.configuration.vector_storage}</span>
</div>
</div>

View File

@@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
import { useEffect, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import StatusCard from '@/components/graph/StatusCard'
import { useTranslation } from 'react-i18next'
const StatusIndicator = () => {
const { t } = useTranslation()
const health = useBackendState.use.health()
const lastCheckTime = useBackendState.use.lastCheckTime()
const status = useBackendState.use.status()
@@ -33,7 +35,7 @@ const StatusIndicator = () => {
)}
/>
<span className="text-muted-foreground text-xs">
{health ? 'Connected' : 'Disconnected'}
{health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
</span>
</div>
</PopoverTrigger>

View File

@@ -3,12 +3,14 @@ import { useCallback } from 'react'
import Button from '@/components/ui/Button'
import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
import { controlButtonVariant } from '@/lib/constants'
import { useTranslation } from "react-i18next";
/**
* Component that provides zoom controls for the graph viewer.
*/
const ZoomControl = () => {
const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
const { t } = useTranslation();
const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
@@ -16,16 +18,16 @@ const ZoomControl = () => {
return (
<>
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip="Zoom In" size="icon">
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip={t("graphPanel.sideBar.zoomControl.zoomIn")} size="icon">
<ZoomInIcon />
</Button>
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip="Zoom Out" size="icon">
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip={t("graphPanel.sideBar.zoomControl.zoomOut")} size="icon">
<ZoomOutIcon />
</Button>
<Button
variant={controlButtonVariant}
onClick={handleResetZoom}
tooltip="Reset Zoom"
tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
size="icon"
>
<FullscreenIcon />