Merge branch 'main' into limit-vdb-metadata-size

This commit is contained in:
yangdx 2025-10-17 21:36:06 +08:00
commit 03333d63f3
11 changed files with 102 additions and 59 deletions

5
.gitignore vendored
View file

@ -9,9 +9,10 @@ __pycache__/
# Virtual Environment # Virtual Environment
.venv/ .venv/
env/
venv/ venv/
*.env*
# Enviroment Variable Files
.env
# Build / Distribution # Build / Distribution
dist/ dist/

View file

@ -1 +1 @@
__api_version__ = "0240" __api_version__ = "0241"

View file

@ -345,14 +345,17 @@ def create_app(args):
finalize_share_data() finalize_share_data()
# Initialize FastAPI # Initialize FastAPI
base_description = (
"Providing API for LightRAG core, Web UI and Ollama Model Emulation"
)
swagger_description = (
base_description
+ (" (With authentication)" if api_key else "")
+ "\n\n[View ReDoc documentation](/redoc)"
)
app_kwargs = { app_kwargs = {
"title": "LightRAG Server API", "title": "LightRAG Server API",
"description": ( "description": swagger_description,
"Providing API for LightRAG core, Web UI and Ollama Model Emulation"
+ "(With authentication)"
if api_key
else ""
),
"version": __api_version__, "version": __api_version__,
"openapi_url": "/openapi.json", # Explicitly set OpenAPI schema URL "openapi_url": "/openapi.json", # Explicitly set OpenAPI schema URL
"docs_url": "/docs", # Explicitly set docs URL "docs_url": "/docs", # Explicitly set docs URL

View file

@ -2629,9 +2629,9 @@ def fix_tuple_delimiter_corruption(
record, record,
) )
# Fix: <X|#|> -> <|#|>, <|#|Y> -> <|#|>, <X|#|Y> -> <|#|>, <||#||> -> <|#|>, <||#> -> <|#|> (one extra characters outside pipes) # Fix: <X|#|> -> <|#|>, <|#|Y> -> <|#|>, <X|#|Y> -> <|#|>, <||#||> -> <|#|> (one extra characters outside pipes)
record = re.sub( record = re.sub(
rf"<.?\|{escaped_delimiter_core}\|*?>", rf"<.?\|{escaped_delimiter_core}\|.?>",
tuple_delimiter, tuple_delimiter,
record, record,
) )
@ -2651,7 +2651,6 @@ def fix_tuple_delimiter_corruption(
) )
# Fix: <|#| -> <|#|>, <|#|| -> <|#|> (missing closing >) # Fix: <|#| -> <|#|>, <|#|| -> <|#|> (missing closing >)
record = re.sub( record = re.sub(
rf"<\|{escaped_delimiter_core}\|+(?!>)", rf"<\|{escaped_delimiter_core}\|+(?!>)",
tuple_delimiter, tuple_delimiter,
@ -2665,6 +2664,13 @@ def fix_tuple_delimiter_corruption(
record, record,
) )
# Fix: <||#> -> <|#|> (double pipe at start, missing pipe at end)
record = re.sub(
rf"<\|+{escaped_delimiter_core}>",
tuple_delimiter,
record,
)
# Fix: <|| -> <|#|> # Fix: <|| -> <|#|>
record = re.sub( record = re.sub(
r"<\|\|(?!>)", r"<\|\|(?!>)",

View file

@ -0,0 +1,4 @@
# Development environment configuration
VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -1,2 +1,4 @@
# Development environment configuration # Development environment configuration
VITE_BACKEND_URL=/api VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true
VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -1,3 +1,3 @@
VITE_BACKEND_URL=http://localhost:9621 VITE_BACKEND_URL=http://localhost:9621
VITE_API_PROXY=true VITE_API_PROXY=true
VITE_API_ENDPOINTS=/,/api,/documents,/graphs,/graph,/health,/query,/docs,/openapi.json,/login,/auth-status VITE_API_ENDPOINTS=/api,/documents,/graphs,/graph,/health,/query,/docs,/redoc,/openapi.json,/login,/auth-status

View file

@ -175,49 +175,59 @@ const GraphLabels = () => {
> >
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} /> <RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
</Button> </Button>
<AsyncSelect<string> <div className="w-full min-w-[280px] max-w-[500px]">
key={selectKey} // Force re-render when data changes <AsyncSelect<string>
className="min-w-[300px]" key={selectKey} // Force re-render when data changes
triggerClassName="max-h-8" className="min-w-[300px]"
searchInputClassName="max-h-8" triggerClassName="max-h-8 w-full overflow-hidden"
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')} searchInputClassName="max-h-8"
fetcher={fetchData} triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
renderOption={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>} fetcher={fetchData}
getOptionValue={(item) => item} renderOption={(item) => (
getDisplayValue={(item) => <div style={{ whiteSpace: 'pre' }}>{item}</div>} <div className="truncate" title={item}>
notFound={<div className="py-6 text-center text-sm">{t('graphPanel.graphLabels.noLabels')}</div>} {item}
ariaLabel={t('graphPanel.graphLabels.label')} </div>
placeholder={t('graphPanel.graphLabels.placeholder')} )}
searchPlaceholder={t('graphPanel.graphLabels.placeholder')} getOptionValue={(item) => item}
noResultsMessage={t('graphPanel.graphLabels.noLabels')} getDisplayValue={(item) => (
value={label !== null ? label : '*'} <div className="min-w-0 flex-1 truncate text-left" title={item}>
onChange={(newLabel) => { {item}
const currentLabel = useSettingsStore.getState().queryLabel; </div>
)}
notFound={<div className="py-6 text-center text-sm">{t('graphPanel.graphLabels.noLabels')}</div>}
ariaLabel={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')}
searchPlaceholder={t('graphPanel.graphLabels.placeholder')}
noResultsMessage={t('graphPanel.graphLabels.noLabels')}
value={label !== null ? label : '*'}
onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel;
// select the last item means query all // select the last item means query all
if (newLabel === '...') { if (newLabel === '...') {
newLabel = '*'; newLabel = '*';
} }
// Handle reselecting the same label // Handle reselecting the same label
if (newLabel === currentLabel && newLabel !== '*') { if (newLabel === currentLabel && newLabel !== '*') {
newLabel = '*'; newLabel = '*';
} }
// Add selected label to search history (except for special cases) // Add selected label to search history (except for special cases)
if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') { if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') {
SearchHistoryManager.addToHistory(newLabel); SearchHistoryManager.addToHistory(newLabel);
} }
// Reset graphDataFetchAttempted flag to ensure data fetch is triggered // Reset graphDataFetchAttempted flag to ensure data fetch is triggered
useGraphStore.getState().setGraphDataFetchAttempted(false); useGraphStore.getState().setGraphDataFetchAttempted(false);
// Update the label to trigger data loading // Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel); useSettingsStore.getState().setQueryLabel(newLabel);
}} }}
clearable={false} // Prevent clearing value on reselect clearable={false} // Prevent clearing value on reselect
debounceTime={500} debounceTime={500}
/> />
</div>
</div> </div>
) )
} }

View file

@ -45,7 +45,13 @@ export type MessageWithError = Message & {
} }
// Restore original component definition and export // Restore original component definition and export
export const ChatMessage = ({ message }: { message: MessageWithError }) => { // Remove isComplete prop export const ChatMessage = ({
message,
isTabActive = true
}: {
message: MessageWithError
isTabActive?: boolean
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const [katexPlugin, setKatexPlugin] = useState<((options?: KaTeXOptions) => any) | null>(null) const [katexPlugin, setKatexPlugin] = useState<((options?: KaTeXOptions) => any) | null>(null)
@ -148,8 +154,13 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
} rounded-lg px-4 py-2`} } rounded-lg px-4 py-2`}
> >
{/* Thinking process display - only for assistant messages */} {/* Thinking process display - only for assistant messages */}
{/* Always render to prevent layout shift when switching tabs */}
{message.role === 'assistant' && (isThinking || thinkingTime !== null) && ( {message.role === 'assistant' && (isThinking || thinkingTime !== null) && (
<div className="mb-2"> <div className={cn(
'mb-2',
// Reduce visual priority in inactive tabs while maintaining layout
!isTabActive && 'opacity-50'
)}>
<div <div
className="flex items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200 text-sm cursor-pointer select-none" className="flex items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors duration-200 text-sm cursor-pointer select-none"
onClick={() => { onClick={() => {
@ -161,7 +172,8 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
> >
{isThinking ? ( {isThinking ? (
<> <>
<LoaderIcon className="mr-2 size-4 animate-spin" /> {/* Only show spinner animation in active tab to save resources */}
{isTabActive && <LoaderIcon className="mr-2 size-4 animate-spin" />}
<span>{t('retrievePanel.chatMessage.thinking')}</span> <span>{t('retrievePanel.chatMessage.thinking')}</span>
</> </>
) : ( ) : (
@ -247,11 +259,12 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
</ReactMarkdown> </ReactMarkdown>
</div> </div>
)} )}
{(() => { {/* Loading indicator - only show in active tab */}
{isTabActive && (() => {
// More comprehensive loading state check // More comprehensive loading state check
const hasVisibleContent = finalDisplayContent && finalDisplayContent.trim() !== ''; const hasVisibleContent = finalDisplayContent && finalDisplayContent.trim() !== '';
const isLoadingState = !hasVisibleContent && !isThinking && !thinkingTime; const isLoadingState = !hasVisibleContent && !isThinking && !thinkingTime;
return isLoadingState && <LoaderIcon className="animate-spin duration-2000" />; return isLoadingState && <LoaderIcon className="animate-spin duration-2000" />
})()} })()}
</div> </div>
) )

View file

@ -103,6 +103,10 @@ const parseCOTContent = (content: string) => {
export default function RetrievalTesting() { export default function RetrievalTesting() {
const { t } = useTranslation() const { t } = useTranslation()
// Get current tab to determine if this tab is active (for performance optimization)
const currentTab = useSettingsStore.use.currentTab()
const isRetrievalTabActive = currentTab === 'retrieval'
const [messages, setMessages] = useState<MessageWithError[]>(() => { const [messages, setMessages] = useState<MessageWithError[]>(() => {
try { try {
const history = useSettingsStore.getState().retrievalHistory || [] const history = useSettingsStore.getState().retrievalHistory || []
@ -716,7 +720,7 @@ export default function RetrievalTesting() {
<CopyIcon className="size-4" /> <CopyIcon className="size-4" />
</Button> </Button>
)} )}
<ChatMessage message={message} /> <ChatMessage message={message} isTabActive={isRetrievalTabActive} />
{message.role === 'assistant' && ( {message.role === 'assistant' && (
<Button <Button
onClick={() => handleCopyMessage(message)} onClick={() => handleCopyMessage(message)}

View file

@ -40,7 +40,7 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: endpoint === '/api' ? rewrite: endpoint === '/api' ?
(path) => path.replace(/^\/api/, '') : (path) => path.replace(/^\/api/, '') :
endpoint === '/docs' || endpoint === '/openapi.json' ? endpoint === '/docs' || endpoint === '/redoc' || endpoint === '/openapi.json' ?
(path) => path : undefined (path) => path : undefined
} }
]) ])