diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5dbcf9c..478ec4c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,9 @@ "version": "0.0.0", "dependencies": { "@base-ui/react": "^1.3.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/geist": "^5.2.8", "@tailwindcss/postcss": "^4.2.1", "@types/dagre": "^0.7.54", @@ -593,6 +596,59 @@ } } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.55.1", "resolved": "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-1.55.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index fecde6a..59f1cfd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,9 @@ }, "dependencies": { "@base-ui/react": "^1.3.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/geist": "^5.2.8", "@tailwindcss/postcss": "^4.2.1", "@types/dagre": "^0.7.54", diff --git a/frontend/src/pages/DataSources.tsx b/frontend/src/pages/DataSources.tsx index 66ef1bf..0859e22 100644 --- a/frontend/src/pages/DataSources.tsx +++ b/frontend/src/pages/DataSources.tsx @@ -3,10 +3,27 @@ import { useTranslation } from 'react-i18next'; import { api } from "@/lib/api"; import { DataSourceForm, type DataSourceConfig } from "@/components/DataSourceForm"; import { Button } from "@/components/ui/button"; -import { Plus, Database, Pencil, Trash2, Loader2, Info, ChevronLeft, FileText, Search, Network } from "lucide-react"; +import { Plus, Database, Pencil, Trash2, Loader2, Info, ChevronLeft, FileText, Search, Network, GripVertical } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useProjectStore } from "@/store/projectStore"; import { useNavigate } from "react-router-dom"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors +} from '@dnd-kit/core'; +import type { DragEndEvent } from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, + useSortable +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; const SOURCE_TYPES = [ { id: "csv", name: "CSV Upload", icon: }, @@ -51,6 +68,24 @@ export function DataSources() { setIsLoading(true); try { const data = await api.get(`/api/v1/datasources?project_id=${currentProject.id}`); + // 从 localStorage 中恢复顺序 + const savedOrderStr = localStorage.getItem(`datasources_order_${currentProject.id}`); + if (savedOrderStr) { + try { + const savedOrder = JSON.parse(savedOrderStr) as string[]; + // 按照保存的 ID 顺序重新排列,同时把新添加的数据源放在末尾 + data.sort((a, b) => { + const indexA = savedOrder.indexOf(a.id as unknown as string); + const indexB = savedOrder.indexOf(b.id as unknown as string); + if (indexA === -1 && indexB === -1) return 0; + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + } catch (e) { + console.error("Failed to parse saved datasource order", e); + } + } setDatasources(data); } catch (e) { console.error("Failed to fetch data sources", e); @@ -202,6 +237,113 @@ export function DataSources() { } }; + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, // Require 8px movement before drag starts to avoid accidental drags + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setDatasources((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + + const newItems = arrayMove(items, oldIndex, newIndex); + + // 保存新的顺序到 localStorage + if (currentProject) { + localStorage.setItem( + `datasources_order_${currentProject.id}`, + JSON.stringify(newItems.map(i => i.id)) + ); + } + + return newItems; + }); + } + }; + + // Sortable Item Component + const SortableDataSourceCard = ({ ds }: { ds: DataSourceConfig }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: ds.id! }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+
+
+ +
+
+ +
+
+

{ds.name}

+

{ds.type}

+
+
+
+ + + +
+
+ +
+
+ Host + + {ds.config.host || parseConnectionString(ds.config.connection_string, 'host') || "Local / File"} + +
+
+ Database + + {ds.config.database || parseConnectionString(ds.config.connection_string, 'database') || (ds.config.file_path ? ds.config.file_path.split('/').pop() : "-")} + +
+
+
+ ); + }; + return (
@@ -227,52 +369,22 @@ export function DataSources() {

{t('clickTopRightToAddFirstDataSource')}

) : ( -
- {datasources.map((ds) => ( -
-
-
-
- -
-
-

{ds.name}

-

{ds.type}

-
-
-
- - - -
-
- -
-
- Host - - {ds.config.host || parseConnectionString(ds.config.connection_string, 'host') || "Local / File"} - -
-
- Database - - {ds.config.database || parseConnectionString(ds.config.connection_string, 'database') || (ds.config.file_path ? ds.config.file_path.split('/').pop() : "-")} - -
-
+ + ds.id!)} + strategy={rectSortingStrategy} + > +
+ {datasources.map((ds) => ( + + ))}
- ))} -
+ + )}
diff --git a/frontend/src/pages/KnowledgeBases.tsx b/frontend/src/pages/KnowledgeBases.tsx index bdbfcaf..f98ed16 100644 --- a/frontend/src/pages/KnowledgeBases.tsx +++ b/frontend/src/pages/KnowledgeBases.tsx @@ -4,11 +4,28 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Save, Loader2, RefreshCw, Pencil, Trash2, FileText, Plus, BookOpen } from "lucide-react"; +import { Save, Loader2, RefreshCw, Pencil, Trash2, FileText, Plus, BookOpen, GripVertical } from "lucide-react"; import { api } from "@/lib/api"; import { useProjectStore } from "@/store/projectStore"; import { Textarea } from "@/components/ui/textarea"; import { KnowledgeBaseForm, type KnowledgeBaseFormValues } from "@/components/KnowledgeBaseForm"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors +} from '@dnd-kit/core'; +import type { DragEndEvent } from '@dnd-kit/core'; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy, + useSortable +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; interface KnowledgeBase { id: string; @@ -76,6 +93,25 @@ export function KnowledgeBases() { setIsLoading(true); try { const data = await api.get(`/api/v1/knowledge-bases?project_id=${currentProject.id}`); + + // 从 localStorage 中恢复顺序 + const savedOrderStr = localStorage.getItem(`knowledge_bases_order_${currentProject.id}`); + if (savedOrderStr) { + try { + const savedOrder = JSON.parse(savedOrderStr) as string[]; + data.sort((a, b) => { + const indexA = savedOrder.indexOf(a.id); + const indexB = savedOrder.indexOf(b.id); + if (indexA === -1 && indexB === -1) return 0; + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + } catch (e) { + console.error("Failed to parse saved kb order", e); + } + } + setKnowledgeBases(data); } catch (err: any) { console.error(err); @@ -247,6 +283,127 @@ export function KnowledgeBases() { } }; + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setKnowledgeBases((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + + const newItems = arrayMove(items, oldIndex, newIndex); + + // 保存新的顺序到 localStorage + if (currentProject) { + localStorage.setItem( + `knowledge_bases_order_${currentProject.id}`, + JSON.stringify(newItems.map(i => i.id)) + ); + } + + return newItems; + }); + } + }; + + const SortableKbCard = ({ kb }: { kb: KnowledgeBase }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: kb.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+
+
+
+ +
+
+ +
+
+

+ {kb.name} + {!kb.is_active && ( + Inactive + )} +

+

+ {kb.documents?.length || 0} Documents +

+
+
+
+ + + + +
+
+ +
+
+ Model + + {kb.embedding_model ? kb.embedding_model.substring(0, 15) + (kb.embedding_model.length > 15 ? '...' : '') : 'Default'} + +
+
+ Chunking + + {kb.chunk_size} / {kb.chunk_overlap} + +
+ {kb.description && ( +

+ {kb.description} +

+ )} +
+
+ ); + }; + return (
@@ -277,67 +434,22 @@ export function KnowledgeBases() {

{t('noKnowledgeBases')}

) : ( -
- {knowledgeBases.map((kb) => ( -
-
-
-
- -
-
-

- {kb.name} - {!kb.is_active && ( - Inactive - )} -

-

- {kb.documents?.length || 0} Documents -

-
-
-
- - - - -
-
- -
-
- Model - - {kb.embedding_model ? kb.embedding_model.substring(0, 15) + (kb.embedding_model.length > 15 ? '...' : '') : 'Default'} - -
-
- Chunking - - {kb.chunk_size} / {kb.chunk_overlap} - -
- {kb.description && ( -

- {kb.description} -

- )} -
+ + kb.id)} + strategy={rectSortingStrategy} + > +
+ {knowledgeBases.map((kb) => ( + + ))}
- ))} -
+ + )}