Merge branch 'main' into improve-property-tooltip
This commit is contained in:
@@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
|
||||
import { MoonIcon, SunIcon } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { controlButtonVariant } from '@/lib/constants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Component that toggles the theme between light and dark.
|
||||
@@ -11,13 +12,14 @@ export default function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
const setLight = useCallback(() => setTheme('light'), [setTheme])
|
||||
const setDark = useCallback(() => setTheme('dark'), [setTheme])
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (theme === 'dark') {
|
||||
return (
|
||||
<Button
|
||||
onClick={setLight}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Switch to light theme"
|
||||
tooltip={t('header.themeToggle.switchToLight')}
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
@@ -29,7 +31,7 @@ export default function ThemeToggle() {
|
||||
<Button
|
||||
onClick={setDark}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Switch to dark theme"
|
||||
tooltip={t('header.themeToggle.switchToDark')}
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
|
@@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
|
||||
import { clearDocuments } from '@/api/lightrag'
|
||||
|
||||
import { EraserIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ClearDocumentsDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleClear = useCallback(async () => {
|
||||
try {
|
||||
const result = await clearDocuments()
|
||||
if (result.status === 'success') {
|
||||
toast.success('Documents cleared successfully')
|
||||
toast.success(t('documentPanel.clearDocuments.success'))
|
||||
setOpen(false)
|
||||
} else {
|
||||
toast.error(`Clear Documents Failed:\n${result.message}`)
|
||||
toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Clear Documents Failed:\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
|
||||
}
|
||||
}, [setOpen])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
|
||||
<EraserIcon/> Clear
|
||||
<Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
|
||||
<EraserIcon/> {t('documentPanel.clearDocuments.button')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Clear documents</DialogTitle>
|
||||
<DialogDescription>Do you really want to clear all documents?</DialogDescription>
|
||||
<DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
|
||||
<DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Button variant="destructive" onClick={handleClear}>
|
||||
YES
|
||||
{t('documentPanel.clearDocuments.confirmButton')}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
@@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
|
||||
import { uploadDocument } from '@/api/lightrag'
|
||||
|
||||
import { UploadIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function UploadDocumentsDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
||||
@@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
|
||||
filesToUpload.map(async (file) => {
|
||||
try {
|
||||
const result = await uploadDocument(file, (percentCompleted: number) => {
|
||||
console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
|
||||
console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
|
||||
setProgresses((pre) => ({
|
||||
...pre,
|
||||
[file.name]: percentCompleted
|
||||
}))
|
||||
})
|
||||
if (result.status === 'success') {
|
||||
toast.success(`Upload Success:\n${file.name} uploaded successfully`)
|
||||
toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
|
||||
} else {
|
||||
toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
|
||||
toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
|
||||
toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
toast.error('Upload Failed\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
// setOpen(false)
|
||||
@@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
|
||||
<UploadIcon /> Upload
|
||||
<Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
|
||||
<UploadIcon /> {t('documentPanel.uploadDocuments.button')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload documents</DialogTitle>
|
||||
<DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Drag and drop your documents here or click to browse.
|
||||
{t('documentPanel.uploadDocuments.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FileUploader
|
||||
maxFileCount={Infinity}
|
||||
maxSize={200 * 1024 * 1024}
|
||||
description="supported types: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||
description={t('documentPanel.uploadDocuments.fileTypes')}
|
||||
onUpload={handleDocumentsUpload}
|
||||
progresses={progresses}
|
||||
disabled={isUploading}
|
||||
|
@@ -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>
|
||||
)}
|
||||
|
@@ -4,8 +4,10 @@ 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 GraphLabels = () => {
|
||||
const { t } = useTranslation()
|
||||
const label = useSettingsStore.use.queryLabel()
|
||||
const graphLabels = useGraphStore.use.graphLabels()
|
||||
|
||||
@@ -45,7 +47,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]
|
||||
)
|
||||
@@ -68,14 +70,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}
|
||||
clearable={false} // Prevent clearing value on reselect
|
||||
|
@@ -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')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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()
|
||||
|
@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Component that displays a checkbox with a label.
|
||||
@@ -205,11 +206,13 @@ export default function Settings() {
|
||||
[setTempApiKey]
|
||||
)
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Refresh Layout"
|
||||
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
|
||||
size="icon"
|
||||
onClick={refreshLayout}
|
||||
>
|
||||
@@ -217,7 +220,7 @@ export default function Settings() {
|
||||
</Button>
|
||||
<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>
|
||||
@@ -231,7 +234,7 @@ export default function Settings() {
|
||||
<LabeledCheckBox
|
||||
checked={enableHealthCheck}
|
||||
onCheckedChange={setEnableHealthCheck}
|
||||
label="Health Check"
|
||||
label={t('graphPanel.sideBar.settings.healthCheck')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
@@ -239,12 +242,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 />
|
||||
@@ -252,12 +255,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 />
|
||||
@@ -265,51 +268,50 @@ 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="Minimum Degree"
|
||||
label={t('graphPanel.sideBar.settings.minDegree')}
|
||||
min={0}
|
||||
value={graphMinDegree}
|
||||
onEditFinished={setGraphMinDegree}
|
||||
/>
|
||||
<LabeledNumberInput
|
||||
label="Max Layout Iterations"
|
||||
label={t('graphPanel.sideBar.settings.maxLayoutIterations')}
|
||||
min={1}
|
||||
max={30}
|
||||
value={graphLayoutMaxIterations}
|
||||
onEditFinished={setGraphLayoutMaxIterations}
|
||||
/>
|
||||
|
||||
<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"
|
||||
/>
|
||||
@@ -320,7 +322,7 @@ export default function Settings() {
|
||||
size="sm"
|
||||
className="max-h-full shrink-0"
|
||||
>
|
||||
Save
|
||||
{t('graphPanel.sideBar.settings.save')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 />
|
||||
|
@@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||
|
||||
import { LoaderIcon, CopyIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type MessageWithError = Message & {
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleCopyMarkdown = useCallback(async () => {
|
||||
if (message.content) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(message.content)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
console.error(t('chat.copyError'), err)
|
||||
}
|
||||
}
|
||||
}, [message])
|
||||
@@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
||||
<Button
|
||||
onClick={handleCopyMarkdown}
|
||||
className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
|
||||
tooltip="Copy to clipboard"
|
||||
tooltip={t('retrievePanel.chatMessage.copyTooltip')}
|
||||
variant="default"
|
||||
size="icon"
|
||||
>
|
||||
|
@@ -14,8 +14,10 @@ import {
|
||||
SelectValue
|
||||
} from '@/components/ui/Select'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function QuerySettings() {
|
||||
const { t } = useTranslation()
|
||||
const querySettings = useSettingsStore((state) => state.querySettings)
|
||||
|
||||
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
||||
@@ -25,8 +27,8 @@ export default function QuerySettings() {
|
||||
return (
|
||||
<Card className="flex shrink-0 flex-col">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>Configure your query parameters</CardDescription>
|
||||
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
||||
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||
<div className="relative size-full">
|
||||
@@ -35,8 +37,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Query Mode"
|
||||
tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
|
||||
text={t('retrievePanel.querySettings.queryMode')}
|
||||
tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Select
|
||||
@@ -48,11 +50,11 @@ export default function QuerySettings() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="naive">Naive</SelectItem>
|
||||
<SelectItem value="local">Local</SelectItem>
|
||||
<SelectItem value="global">Global</SelectItem>
|
||||
<SelectItem value="hybrid">Hybrid</SelectItem>
|
||||
<SelectItem value="mix">Mix</SelectItem>
|
||||
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
|
||||
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
|
||||
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
|
||||
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
|
||||
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -62,8 +64,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Response Format"
|
||||
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
|
||||
text={t('retrievePanel.querySettings.responseFormat')}
|
||||
tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Select
|
||||
@@ -75,9 +77,9 @@ export default function QuerySettings() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
|
||||
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
|
||||
<SelectItem value="Bullet Points">Bullet Points</SelectItem>
|
||||
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
|
||||
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
|
||||
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -87,8 +89,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Top K Results"
|
||||
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
|
||||
text={t('retrievePanel.querySettings.topK')}
|
||||
tooltip={t('retrievePanel.querySettings.topKTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -97,7 +99,7 @@ export default function QuerySettings() {
|
||||
value={querySettings.top_k}
|
||||
onValueChange={(v) => handleChange('top_k', v)}
|
||||
min={1}
|
||||
placeholder="Number of results"
|
||||
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -106,8 +108,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Max Tokens for Text Unit"
|
||||
tooltip="Maximum number of tokens allowed for each retrieved text chunk"
|
||||
text={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -116,14 +118,14 @@ export default function QuerySettings() {
|
||||
value={querySettings.max_token_for_text_unit}
|
||||
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for text unit"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
text="Max Tokens for Global Context"
|
||||
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
|
||||
text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -132,15 +134,15 @@ export default function QuerySettings() {
|
||||
value={querySettings.max_token_for_global_context}
|
||||
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for global context"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Max Tokens for Local Context"
|
||||
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
|
||||
text={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -149,7 +151,7 @@ export default function QuerySettings() {
|
||||
value={querySettings.max_token_for_local_context}
|
||||
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for local context"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
@@ -158,8 +160,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="History Turns"
|
||||
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
|
||||
text={t('retrievePanel.querySettings.historyTurns')}
|
||||
tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
@@ -170,7 +172,7 @@ export default function QuerySettings() {
|
||||
value={querySettings.history_turns}
|
||||
onValueChange={(v) => handleChange('history_turns', v)}
|
||||
min={0}
|
||||
placeholder="Number of history turns"
|
||||
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -179,8 +181,8 @@ export default function QuerySettings() {
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="High-Level Keywords"
|
||||
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
|
||||
text={t('retrievePanel.querySettings.hlKeywords')}
|
||||
tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Input
|
||||
@@ -194,15 +196,15 @@ export default function QuerySettings() {
|
||||
.filter((k) => k !== '')
|
||||
handleChange('hl_keywords', keywords)
|
||||
}}
|
||||
placeholder="Enter keywords"
|
||||
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Low-Level Keywords"
|
||||
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
|
||||
text={t('retrievePanel.querySettings.llKeywords')}
|
||||
tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Input
|
||||
@@ -216,7 +218,7 @@ export default function QuerySettings() {
|
||||
.filter((k) => k !== '')
|
||||
handleChange('ll_keywords', keywords)
|
||||
}}
|
||||
placeholder="Enter keywords"
|
||||
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
@@ -226,8 +228,8 @@ export default function QuerySettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Only Need Context"
|
||||
tooltip="If True, only returns the retrieved context without generating a response"
|
||||
text={t('retrievePanel.querySettings.onlyNeedContext')}
|
||||
tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
@@ -242,8 +244,8 @@ export default function QuerySettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Only Need Prompt"
|
||||
tooltip="If True, only returns the generated prompt without producing a response"
|
||||
text={t('retrievePanel.querySettings.onlyNeedPrompt')}
|
||||
tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
@@ -258,8 +260,8 @@ export default function QuerySettings() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Stream Response"
|
||||
tooltip="If True, enables streaming output for real-time responses"
|
||||
text={t('retrievePanel.querySettings.streamResponse')}
|
||||
tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
Reference in New Issue
Block a user