ocr improved
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user