ocr improved

This commit is contained in:
2026-01-13 18:25:49 +08:00
parent 9745ca2476
commit a5eb381384
104 changed files with 818 additions and 229 deletions

View File

@@ -53,7 +53,7 @@ const AppContent = () => {
if (!initializing && !isAuthenticated) {
const currentPath = window.location.hash.slice(1);
if (currentPath !== '/login') {
console.log('Not authenticated, redirecting to login');
console.log('Not authenticated, redirecting to login', { initializing, isAuthenticated, currentPath });
navigate('/login');
}
}

View File

@@ -361,7 +361,8 @@ export const queryText = async (request: QueryRequest): Promise<QueryResponse> =
export const queryTextStream = async (
request: QueryRequest,
onChunk: (chunk: string) => void,
onError?: (error: string) => void
onError?: (error: string) => void,
signal?: AbortSignal
) => {
const apiKey = useSettingsStore.getState().apiKey;
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
@@ -385,6 +386,7 @@ export const queryTextStream = async (
method: 'POST',
headers: headers,
body: JSON.stringify(request),
signal,
});
if (!response.ok) {
@@ -467,6 +469,15 @@ export const queryTextStream = async (
}
} catch (error) {
// Check if this is an abort error
if (error instanceof DOMException && error.name === 'AbortError') {
console.log('Stream request aborted');
if (onError) {
onError('Request cancelled');
}
return;
}
const message = errorMessage(error);
// Check if this is an authentication error
@@ -789,6 +800,11 @@ export const createWorkspace = async (name: string): Promise<WorkspaceResponse>
return response.data
}
export const renameWorkspace = async (oldName: string, newName: string): Promise<WorkspaceResponse> => {
const response = await axiosInstance.patch(`/workspaces/${encodeURIComponent(oldName)}`, { new_name: newName })
return response.data
}
export const deleteWorkspace = async (name: string): Promise<{ message: string }> => {
const response = await axiosInstance.delete(`/workspaces/${encodeURIComponent(name)}`)
return response.data

View File

@@ -1,11 +1,11 @@
import { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react'
import { useSettingsStore } from '@/stores/settings'
import { listWorkspaces, createWorkspace, deleteWorkspace } from '@/api/lightrag'
import { listWorkspaces, createWorkspace, deleteWorkspace, renameWorkspace } from '@/api/lightrag'
import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose } from '@/components/ui/Dialog'
import { PlusIcon, SearchIcon, TrashIcon } from 'lucide-react'
import { PlusIcon, SearchIcon, TrashIcon, PencilIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export function WorkspaceSelector() {
@@ -20,6 +20,9 @@ export function WorkspaceSelector() {
const [creating, setCreating] = useState(false)
const [deleting, setDeleting] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [renaming, setRenaming] = useState(false)
const [showRenameDialog, setShowRenameDialog] = useState(false)
const [renameNewName, setRenameNewName] = useState('')
// Fetch workspaces on mount
useEffect(() => {
@@ -58,6 +61,24 @@ export function WorkspaceSelector() {
}
}
const handleRenameWorkspace = async () => {
if (!workspace || !renameNewName.trim()) return
setRenaming(true)
try {
await renameWorkspace(workspace, renameNewName.trim())
await fetchWorkspaces()
// Update selected workspace to new name
setWorkspace(renameNewName.trim())
setRenameNewName('')
setShowRenameDialog(false)
} catch (error) {
console.error('Failed to rename workspace:', error)
alert(`Failed to rename workspace: ${error}`)
} finally {
setRenaming(false)
}
}
const handleDeleteWorkspace = async () => {
if (!workspace) return
setDeleting(true)
@@ -116,38 +137,77 @@ export function WorkspaceSelector() {
<div className="flex items-center gap-2">
{workspace && (
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogTrigger asChild>
<Button size="icon" variant="outline" className="text-destructive">
<TrashIcon className="size-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('workspace.deleteTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm">
{t('workspace.deleteConfirm', { workspace })}
</p>
<p className="text-sm text-muted-foreground">
{t('workspace.deleteWarning')}
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<Button
onClick={handleDeleteWorkspace}
disabled={deleting}
variant="destructive"
>
{deleting ? t('common.deleting') : t('common.delete')}
<>
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
<DialogTrigger asChild>
<Button size="icon" variant="outline">
<PencilIcon className="size-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('workspace.renameTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder={t('workspace.newNamePlaceholder')}
value={renameNewName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setRenameNewName(e.target.value)}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') handleRenameWorkspace()
}}
/>
<p className="text-sm text-muted-foreground">
{t('workspace.renameDescription', { workspace })}
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<Button
onClick={handleRenameWorkspace}
disabled={!renameNewName.trim() || renaming}
>
{renaming ? t('common.renaming') : t('common.rename')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogTrigger asChild>
<Button size="icon" variant="outline" className="text-destructive">
<TrashIcon className="size-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('workspace.deleteTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm">
{t('workspace.deleteConfirm', { workspace })}
</p>
<p className="text-sm text-muted-foreground">
{t('workspace.deleteWarning')}
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<Button
onClick={handleDeleteWorkspace}
disabled={deleting}
variant="destructive"
>
{deleting ? t('common.deleting') : t('common.delete')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)}
<Dialog>

View File

@@ -18,6 +18,16 @@ import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/pris
import { LoaderIcon, CopyIcon, ChevronDownIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
// Helper function to transform citation patterns into hyperlinks
const transformCitations = (text: string): string => {
// Replace [DC] document_name with [DC] [document_name](/documents/download/encoded)
// Pattern: [DC] followed by whitespace then capture until whitespace or punctuation (excluding brackets)
return text.replace(/\[DC\]\s+([^\s\]]+)/g, (match, docName) => {
const encoded = encodeURIComponent(docName);
return `[DC] [${docName}](/documents/download/${encoded})`;
});
};
export type MessageWithError = Message & {
id: string // Unique identifier for stable React keys
isError?: boolean
@@ -55,6 +65,10 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
? message.content
: (displayContent !== undefined ? displayContent : (message.content || ''))
// Apply citation transformation for hyperlinks
const transformedThinkingContent = finalThinkingContent ? transformCitations(finalThinkingContent) : ''
const transformedDisplayContent = finalDisplayContent ? transformCitations(finalDisplayContent) : ''
// Load KaTeX dynamically
useEffect(() => {
const loadKaTeX = async () => {
@@ -151,7 +165,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
skipHtml={false}
components={thinkingMarkdownComponents}
>
{finalThinkingContent}
{transformedThinkingContent}
</ReactMarkdown>
</div>
)}
@@ -177,7 +191,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => { //
skipHtml={false}
components={mainMarkdownComponents}
>
{finalDisplayContent}
{transformedDisplayContent}
</ReactMarkdown>
{message.role === 'assistant' && finalDisplayContent && finalDisplayContent.length > 0 && (
<Button

View File

@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button'
import { ZapIcon } from 'lucide-react'
import { Train } from 'lucide-react'
import AppSettings from '@/components/AppSettings'
const LoginPage = () => {
@@ -154,7 +154,7 @@ const LoginPage = () => {
<div className="flex flex-col items-center space-y-4">
<div className="flex items-center gap-3">
<img src="logo.svg" alt="RailSeek Logo" className="h-12 w-12" />
<ZapIcon className="size-10 text-emerald-400" aria-hidden="true" />
<Train className="size-10 text-emerald-400" aria-hidden="true" />
</div>
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold tracking-tight">RailSeek</h1>

View File

@@ -8,7 +8,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useDebounce } from '@/hooks/useDebounce'
import QuerySettings from '@/components/retrieval/QuerySettings'
import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
import { EraserIcon, SendIcon } from 'lucide-react'
import { EraserIcon, SendIcon, Square } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { QueryMode } from '@/api/lightrag'
@@ -68,17 +68,30 @@ export default function RetrievalTesting() {
const isReceivingResponseRef = useRef(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
// Add cleanup effect for memory leak prevention
useEffect(() => {
// Component cleanup - reset timer state to prevent memory leaks
// Component cleanup - reset timer state and abort any ongoing request
return () => {
if (thinkingStartTime.current) {
thinkingStartTime.current = null;
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
};
}, []);
// Stop retrieval function
const stopRetrieval = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
}, []);
// Scroll to bottom function - restored smooth scrolling with better handling
const scrollToBottom = useCallback(() => {
// Set flag to indicate this is a programmatic scroll
@@ -268,13 +281,23 @@ export default function RetrievalTesting() {
...(modeOverride ? { mode: modeOverride } : {})
}
// Create abort controller for streaming cancellation
if (state.querySettings.stream) {
abortControllerRef.current = new AbortController();
}
try {
// Run query
if (state.querySettings.stream) {
let errorMessage = ''
await queryTextStream(queryParams, updateAssistantMessage, (error) => {
errorMessage += error
})
await queryTextStream(
queryParams,
updateAssistantMessage,
(error) => {
errorMessage += error
},
abortControllerRef.current?.signal
)
if (errorMessage) {
if (assistantMessage.content) {
errorMessage = assistantMessage.content + '\n' + errorMessage
@@ -292,6 +315,8 @@ export default function RetrievalTesting() {
// Clear loading and add messages to state
setIsLoading(false)
isReceivingResponseRef.current = false
// Clean up abort controller
abortControllerRef.current = null
// Enhanced cleanup with error handling to prevent memory leaks
try {
@@ -472,10 +497,22 @@ export default function RetrievalTesting() {
<div className="absolute left-0 top-full mt-1 text-xs text-red-500">{inputError}</div>
)}
</div>
<Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon />
{t('retrievePanel.retrieval.send')}
</Button>
{isLoading ? (
<Button
type="button"
variant="destructive"
onClick={stopRetrieval}
size="sm"
>
<Square />
{t('retrievePanel.retrieval.stop')}
</Button>
) : (
<Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon />
{t('retrievePanel.retrieval.send')}
</Button>
)}
</form>
</div>
<QuerySettings />

View File

@@ -99,11 +99,6 @@ export default function SiteHeader() {
<div className="flex h-10 flex-1 items-center justify-center">
<TabsNavigation />
{isGuestMode && (
<div className="ml-2 self-center px-2 py-1 text-xs bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 rounded-md">
{t('login.guestMode', 'Guest Mode')}
</div>
)}
</div>
<nav className="w-[200px] flex items-center justify-end">
@@ -113,23 +108,16 @@ export default function SiteHeader() {
v{versionDisplay}
</span>
)}
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
<GithubIcon className="size-4" aria-hidden="true" />
</a>
</Button>
<AppSettings />
{!isGuestMode && (
<Button
variant="ghost"
size="icon"
side="bottom"
tooltip={`${t('header.logout')} (${username})`}
onClick={handleLogout}
>
<LogOutIcon className="size-4" aria-hidden="true" />
</Button>
)}
<Button
variant="ghost"
size="icon"
side="bottom"
tooltip={isGuestMode ? t('header.login') : `${t('header.logout')} (${username})`}
onClick={handleLogout}
>
<LogOutIcon className="size-4" aria-hidden="true" />
</Button>
</div>
</nav>
</div>

View File

@@ -28,6 +28,9 @@
"createDescription": "Create a new isolated workspace for your documents and indexes.",
"create": "Create",
"creating": "Creating...",
"renameTitle": "Rename Workspace",
"newNamePlaceholder": "New workspace name",
"renameDescription": "Rename workspace '{{workspace}}' to a new name. This will update the workspace directory and all references.",
"deleteTitle": "Delete Workspace",
"deleteConfirm": "Are you sure you want to delete workspace '{{workspace}}'?",
"deleteWarning": "This will permanently delete all data in this workspace and cannot be undone."
@@ -52,7 +55,9 @@
"saving": "Saving...",
"saveFailed": "Save failed",
"delete": "Delete",
"deleting": "Deleting..."
"deleting": "Deleting...",
"rename": "Rename",
"renaming": "Renaming..."
},
"documentPanel": {
"clearDocuments": {

View File

@@ -53,6 +53,7 @@ class NavigationService {
* Navigate to login page and reset application state
*/
navigateToLogin() {
console.log('navigateToLogin called, navigate is set?', !!this.navigate);
if (!this.navigate) {
console.error('Navigation function not set');
return;
@@ -69,6 +70,7 @@ class NavigationService {
this.resetAllApplicationState(true);
useAuthStore.getState().logout();
console.log('Navigating to /login');
this.navigate('/login');
}