workspace working

This commit is contained in:
2026-01-12 22:31:11 +08:00
parent 2738a822d1
commit 370fe6368a
149 changed files with 4648 additions and 660 deletions

View File

@@ -267,6 +267,7 @@ const axiosInstance = axios.create({
axiosInstance.interceptors.request.use((config) => {
const apiKey = useSettingsStore.getState().apiKey
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
const workspace = useSettingsStore.getState().workspace
// Always include token if it exists, regardless of path
if (token) {
@@ -275,6 +276,10 @@ axiosInstance.interceptors.request.use((config) => {
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
// Add workspace header if workspace is selected
if (workspace) {
config.headers['X-Workspace'] = workspace
}
return config
})
@@ -360,6 +365,7 @@ export const queryTextStream = async (
) => {
const apiKey = useSettingsStore.getState().apiKey;
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
const workspace = useSettingsStore.getState().workspace;
const headers: HeadersInit = {
'Content-Type': 'application/json',
'Accept': 'application/x-ndjson',
@@ -370,6 +376,9 @@ export const queryTextStream = async (
if (apiKey) {
headers['X-API-Key'] = apiKey;
}
if (workspace) {
headers['X-Workspace'] = workspace;
}
try {
const response = await fetch(`${backendBaseUrl}/query/stream`, {
@@ -779,3 +788,8 @@ export const createWorkspace = async (name: string): Promise<WorkspaceResponse>
const response = await axiosInstance.post('/workspaces/', { name })
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 } from '@/api/lightrag'
import { listWorkspaces, createWorkspace, deleteWorkspace } 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 } from 'lucide-react'
import { PlusIcon, SearchIcon, TrashIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export function WorkspaceSelector() {
@@ -18,6 +18,8 @@ export function WorkspaceSelector() {
const [search, setSearch] = useState('')
const [newWorkspaceName, setNewWorkspaceName] = useState('')
const [creating, setCreating] = useState(false)
const [deleting, setDeleting] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
// Fetch workspaces on mount
useEffect(() => {
@@ -56,12 +58,29 @@ export function WorkspaceSelector() {
}
}
const handleDeleteWorkspace = async () => {
if (!workspace) return
setDeleting(true)
try {
await deleteWorkspace(workspace)
await fetchWorkspaces()
// Clear selected workspace if it was deleted
setWorkspace('')
setShowDeleteDialog(false)
} catch (error) {
console.error('Failed to delete workspace:', error)
alert(`Failed to delete workspace: ${error}`)
} finally {
setDeleting(false)
}
}
const filteredWorkspaces = workspaceList.filter(name =>
name.toLowerCase().includes(search.toLowerCase())
)
return (
<div className="flex items-center gap-2">
<div className="flex flex-row items-center gap-2">
<Select
value={workspace || ''}
onValueChange={(value) => setWorkspace(value)}
@@ -95,42 +114,79 @@ export function WorkspaceSelector() {
</SelectContent>
</Select>
<Dialog>
<DialogTrigger asChild>
<Button size="icon" variant="outline">
<PlusIcon className="size-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('workspace.createTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder={t('workspace.namePlaceholder')}
value={newWorkspaceName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewWorkspaceName(e.target.value)}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') handleCreateWorkspace()
}}
/>
<p className="text-sm text-muted-foreground">
{t('workspace.createDescription')}
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<Button
onClick={handleCreateWorkspace}
disabled={!newWorkspaceName.trim() || creating}
>
{creating ? t('common.creating') : t('common.create')}
<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')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<Dialog>
<DialogTrigger asChild>
<Button size="icon" variant="outline">
<PlusIcon className="size-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('workspace.createTitle')}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder={t('workspace.namePlaceholder')}
value={newWorkspaceName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewWorkspaceName(e.target.value)}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') handleCreateWorkspace()
}}
/>
<p className="text-sm text-muted-foreground">
{t('workspace.createDescription')}
</p>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<Button
onClick={handleCreateWorkspace}
disabled={!newWorkspaceName.trim() || creating}
>
{creating ? t('common.creating') : t('common.create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
)
}

View File

@@ -202,6 +202,7 @@ export default function DocumentManager() {
const setShowFileName = useSettingsStore.use.setShowFileName()
const documentsPageSize = useSettingsStore.use.documentsPageSize()
const setDocumentsPageSize = useSettingsStore.use.setDocumentsPageSize()
const workspace = useSettingsStore.use.workspace()
// New pagination state
const [currentPageDocs, setCurrentPageDocs] = useState<DocStatusResponse[]>([])
@@ -901,6 +902,7 @@ export default function DocumentManager() {
statusFilter,
sortField,
sortDirection,
workspace,
fetchPaginatedDocuments
]);

View File

@@ -68,70 +68,76 @@ export default function SiteHeader() {
}
return (
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
<div className="min-w-[200px] w-auto flex items-center">
<a href={webuiPrefix} className="flex items-center gap-2">
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
</a>
{webuiTitle && (
<div className="flex items-center">
<span className="mx-1 text-xs text-gray-500 dark:text-gray-400">|</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="font-medium text-sm cursor-default">
{webuiTitle}
</span>
</TooltipTrigger>
{webuiDescription && (
<TooltipContent side="bottom">
{webuiDescription}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
)}
<span className="mx-2 text-xs text-gray-300 dark:text-gray-600">|</span>
<WorkspaceSelector />
</div>
<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">
<div className="flex items-center gap-2">
{versionDisplay && (
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">
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>
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex flex-col w-full border-b px-4 backdrop-blur">
{/* First row: Title and other header items */}
<div className="flex h-10 items-center justify-between">
<div className="min-w-[200px] w-auto flex items-center">
<a href={webuiPrefix} className="flex items-center gap-2">
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
</a>
{webuiTitle && (
<div className="flex items-center">
<span className="mx-1 text-xs text-gray-500 dark:text-gray-400">|</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="font-medium text-sm cursor-default">
{webuiTitle}
</span>
</TooltipTrigger>
{webuiDescription && (
<TooltipContent side="bottom">
{webuiDescription}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
</nav>
<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">
<div className="flex items-center gap-2">
{versionDisplay && (
<span className="text-xs text-gray-500 dark:text-gray-400 mr-1">
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>
)}
</div>
</nav>
</div>
{/* Second row: Workspace selector */}
<div className="flex items-center gap-2 py-1 border-t border-border/40">
<WorkspaceSelector />
</div>
</header>
)
}

View File

@@ -27,7 +27,10 @@
"namePlaceholder": "Workspace name",
"createDescription": "Create a new isolated workspace for your documents and indexes.",
"create": "Create",
"creating": "Creating..."
"creating": "Creating...",
"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."
},
"login": {
"description": "Please enter your account and password to log in to the system",
@@ -47,7 +50,9 @@
"cancel": "Cancel",
"save": "Save",
"saving": "Saving...",
"saveFailed": "Save failed"
"saveFailed": "Save failed",
"delete": "Delete",
"deleting": "Deleting..."
},
"documentPanel": {
"clearDocuments": {