diff --git a/src/hooks/setting-hooks.ts b/src/hooks/setting-hooks.ts index 1cd13d6..f95b425 100644 --- a/src/hooks/setting-hooks.ts +++ b/src/hooks/setting-hooks.ts @@ -69,7 +69,7 @@ export function useLlmModelSetting() { const fetchMyLlm = async () => { try { - const res = await userService.my_llm(); + const res = await userService.my_llm({include_details: true}); const llm_dic = res.data.data || {}; setMyLlm(llm_dic); } catch (error) { diff --git a/src/interfaces/database/knowledge.ts b/src/interfaces/database/knowledge.ts index eef789d..f354dec 100644 --- a/src/interfaces/database/knowledge.ts +++ b/src/interfaces/database/knowledge.ts @@ -343,6 +343,12 @@ export interface IChunk { tag_feas?: Record; } +export interface IChunkDetail extends IChunk { + /** 文档块ID */ + id: string; +} + + /** * 测试文档块接口 * 用于知识库测试和检索的文档块信息 diff --git a/src/locales/en.ts b/src/locales/en.ts index 2327afd..3904b20 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1553,6 +1553,9 @@ Important structured information may include: names, dates, locations, events, k fileTypeNotSupportedPreview: 'File type not supported for preview', filePreview: 'File Preview', loadingFile: 'Loading file...', + editChunk: 'Edit Chunk', + content: 'Content', + saving: 'Saving...', }, fileUpload: { uploadFiles: 'Upload Files', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 6891a6f..8a91be0 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1517,6 +1517,9 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 loadingPreview: '正在加载预览...', downloadFile: '下载文件', openInNewTab: '在新标签页中打开', + editChunk: '编辑Chunk', + content: '内容', + saving: '保存中...', }, fileUpload: { uploadFiles: '上传文件', diff --git a/src/pages/chunk/components/ChunkListResult.tsx b/src/pages/chunk/components/ChunkListResult.tsx index 1109737..ec4511b 100644 --- a/src/pages/chunk/components/ChunkListResult.tsx +++ b/src/pages/chunk/components/ChunkListResult.tsx @@ -24,6 +24,10 @@ import { DialogContentText, Toolbar, FormControlLabel, + TextField, + Switch, + Backdrop, + Popover, } from '@mui/material'; import { Image as ImageIcon, @@ -35,9 +39,13 @@ import { ToggleOff as DisableIcon, SelectAll as SelectAllIcon, Clear as ClearIcon, + Edit as EditIcon, + Close as CloseIcon, + Save as SaveIcon, + ZoomIn as ZoomInIcon, } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; -import type { IChunk } from '@/interfaces/database/knowledge'; +import type { IChunk, IChunkDetail } from '@/interfaces/database/knowledge'; import knowledgeService from '@/services/knowledge_service'; interface ChunkListResultProps { @@ -54,7 +62,7 @@ interface ChunkListResultProps { } function ChunkListResult(props: ChunkListResultProps) { - const { doc_id, chunks, total, loading, error, page, pageSize, onPageChange, onRefresh, docName } = props; + const { doc_id, chunks, total, loading, page, pageSize, onPageChange, onRefresh } = props; const { t } = useTranslation(); // 选择状态 @@ -65,6 +73,19 @@ function ChunkListResult(props: ChunkListResultProps) { const [operationLoading, setOperationLoading] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + // 图片预览状态 + const [imagePreviewOpen, setImagePreviewOpen] = useState(false); + const [previewImageUrl, setPreviewImageUrl] = useState(''); + const [imageHoverAnchor, setImageHoverAnchor] = useState(null); + const [hoveredImageUrl, setHoveredImageUrl] = useState(''); + + // 编辑状态 + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingChunk, setEditingChunk] = useState(null); + const [editContent, setEditContent] = useState(''); + const [editAvailable, setEditAvailable] = useState(true); + const [editLoading, setEditLoading] = useState(false); + // 处理单个选择 const handleSelectChunk = useCallback((chunkId: string, checked: boolean) => { setSelectedChunks(prev => { @@ -115,6 +136,84 @@ function ChunkListResult(props: ChunkListResultProps) { setOperationLoading(false); } }, [selectedChunks, handleClearSelection, onRefresh]); + + // 图片hover处理 + const handleImageHover = useCallback((event: React.MouseEvent, imageUrl: string) => { + setImageHoverAnchor(event.currentTarget); + setHoveredImageUrl(imageUrl); + }, []); + + const handleImageHoverClose = useCallback(() => { + setImageHoverAnchor(null); + setHoveredImageUrl(''); + }, []); + + // 图片点击放大 + const handleImageClick = useCallback((imageUrl: string) => { + setPreviewImageUrl(imageUrl); + setImagePreviewOpen(true); + }, []); + + // 编辑chunk + const handleEditChunk = useCallback(async (chunk: IChunk) => { + try { + setEditLoading(true); + // 获取chunk详情 + const chunk_id = chunk.chunk_id || ''; + const response = await knowledgeService.getChunk({ chunk_id }); + const chunkDetail: IChunkDetail = response.data.data; + + setEditingChunk(chunkDetail); + setEditContent(chunkDetail.content_with_weight || ''); + + const available_int = chunk.available_int + const available_int_D = chunkDetail.available_int; + if (available_int_D === undefined) { + setEditAvailable(available_int === 1); + } else { + setEditAvailable(available_int_D === 1); + } + + setEditDialogOpen(true); + } catch (err) { + console.error('Failed to get chunk detail:', err); + } finally { + setEditLoading(false); + } + }, []); + + // 保存编辑 + const handleSaveEdit = useCallback(async () => { + if (!editingChunk) return; + + try { + setEditLoading(true); + await knowledgeService.updateChunk({ + ...editingChunk, + chunk_id: editingChunk.id || '', + content_with_weight: editContent, + available_int: editAvailable ? 1 : 0, + }); + + setEditDialogOpen(false); + setEditingChunk(null); + // delay 800 ms + await new Promise(resolve => setTimeout(resolve, 800)); + onRefresh?.(); + } catch (err) { + console.error('Failed to update chunk:', err); + } finally { + setEditLoading(false); + } + }, [editingChunk, editContent, editAvailable, onRefresh]); + + // 取消编辑 + const handleCancelEdit = useCallback(() => { + setEditDialogOpen(false); + setEditingChunk(null); + setEditContent(''); + setEditAvailable(true); + }, []); // 删除chunks const handleDeleteChunks = useCallback(async () => { @@ -148,16 +247,6 @@ function ChunkListResult(props: ChunkListResultProps) { ); } - if (error) { - return ( - - - {error} - - - ); - } - if (!chunks || chunks.length === 0) { return ( @@ -176,6 +265,7 @@ function ChunkListResult(props: ChunkListResultProps) { const selectedEnabledCount = selectedChunks.filter(id => chunks.find(chunk => chunk.chunk_id === id)?.available_int === 1 ).length; + const selectedDisabledCount = selectedChunks.length - selectedEnabledCount; return ( @@ -261,7 +351,7 @@ function ChunkListResult(props: ChunkListResultProps) { {chunks.map((chunk, index) => ( - + - {/* 头部信息 */} - + {/* 头部操作区域 */} + handleSelectChunk(chunk.chunk_id, e.target.checked)} - sx={{ mr: 1 }} /> - - - - - - - Chunk #{((page - 1) * pageSize) + index + 1} - - - ID: {chunk.chunk_id} - - - - + : } + label={chunk.available_int === 1 ? t('chunkPage.enabled') : t('chunkPage.disabled')} + size="small" + color={chunk.available_int === 1 ? 'success' : 'default'} + variant={chunk.available_int === 1 ? 'filled' : 'outlined'} + /> + + + {/* 主要内容区域 - 左右布局 */} + + {/* 左侧图片区域 */} + {chunk.image_id && ( - - - - - - )} - : } - label={chunk.available_int === 1 ? t('chunkPage.enabled') : t('chunkPage.disabled')} - size="small" - color={chunk.available_int === 1 ? 'success' : 'default'} - variant={chunk.available_int === 1 ? 'filled' : 'outlined'} - /> - - - - {/* 内容区域 */} - - - {t('chunkPage.contentPreview')} - - - - {chunk.content_with_weight || t('chunkPage.noContent')} - - - - - {/* 图片显示区域 */} - {chunk.image_id && ( - - - {t('chunkPage.relatedImage')} - - { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - }} - /> - + onClick={() => handleImageClick(`${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)} + onMouseEnter={(e) => handleImageHover(e, `${import.meta.env.VITE_API_BASE_URL}/v1/document/image/${chunk.image_id}`)} + onMouseLeave={handleImageHoverClose} + > + { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + }} + /> + {/* 悬浮覆盖层 */} + + + + + )} - )} + + {/* 右侧内容区域 */} + + handleEditChunk(chunk)} + > + + {chunk.content_with_weight || t('chunkPage.noContent')} + + + {/* 编辑按钮 */} + { + e.stopPropagation(); + handleEditChunk(chunk); + }} + > + + + + + {/* 关键词区域 */} {((chunk.important_kwd ?? []).length > 0 || (chunk.question_kwd ?? []).length > 0 || (chunk.tag_kwd ?? []).length > 0) && ( @@ -473,6 +600,120 @@ function ChunkListResult(props: ChunkListResultProps) { )} + {/* 图片hover预览 */} + + + + + + + {/* 图片放大预览 */} + setImagePreviewOpen(false)} + maxWidth='md' + fullWidth + sx={{ + '& .MuiDialog-paper': { + backgroundColor: 'rgba(0, 0, 0, 0.2)', + } + }} + > + + + + + setImagePreviewOpen(false)} + sx={{ color: 'white' }} + > + + + + + + {/* 编辑chunk对话框 */} + + + {t('chunkPage.editChunk')} + + + + setEditContent(e.target.value)} + variant="outlined" + sx={{ mb: 2 }} + /> + setEditAvailable(e.target.checked)} + /> + } + label={t('chunkPage.enabled')} + /> + + + + + + + + {/* 删除确认对话框 */} {group.label}, ...group.options.map((option) => ( - {option.label} + + + {option.label} + )) ])} diff --git a/src/pages/knowledge/testing.tsx b/src/pages/knowledge/testing.tsx index 64efacc..fe60a22 100644 --- a/src/pages/knowledge/testing.tsx +++ b/src/pages/knowledge/testing.tsx @@ -32,8 +32,10 @@ import type { INextTestingResult } from '@/interfaces/database/knowledge'; import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs'; import TestChunkResult from './components/TestChunkResult'; import { useSnackbar } from '@/components/Provider/SnackbarProvider'; -import { toLower } from 'lodash'; +import { type LLMFactory } from '@/constants/llm'; import { t } from 'i18next'; +import { LlmSvgIcon } from '@/components/AppSvgIcon'; +import { getFactoryIconName } from '@/utils/common'; // 语言选项常量 const options = [ @@ -231,7 +233,7 @@ function KnowledgeBaseTesting() { {/* 面包屑导航 */} - {group.label}, ...group.options.map((option) => ( - {option.label} + + + {option.label} + )) ])} diff --git a/src/pages/setting/components/Dialog/OllamaDialog.tsx b/src/pages/setting/components/Dialog/OllamaDialog.tsx index 91f8cc3..7b41a9b 100644 --- a/src/pages/setting/components/Dialog/OllamaDialog.tsx +++ b/src/pages/setting/components/Dialog/OllamaDialog.tsx @@ -102,8 +102,8 @@ function OllamaDialog({ reset, } = useForm({ defaultValues: { - model_type: 'chat', - llm_name: '', + model_type: initialData?.model_type || 'chat', + llm_name: initialData?.llm_name || '', api_base: initialData?.api_base, api_key: initialData?.api_key, max_tokens: initialData?.max_tokens, @@ -146,8 +146,8 @@ function OllamaDialog({ useEffect(() => { if (open) { reset({ - model_type: 'chat', - llm_name: '', + model_type: initialData?.model_type || 'chat', + llm_name: initialData?.llm_name || '', api_base: initialData?.api_base, api_key: initialData?.api_key, max_tokens: initialData?.max_tokens, diff --git a/src/pages/setting/components/Dialog/SystemModelDialog.tsx b/src/pages/setting/components/Dialog/SystemModelDialog.tsx index 25be35d..e5e0ffe 100644 --- a/src/pages/setting/components/Dialog/SystemModelDialog.tsx +++ b/src/pages/setting/components/Dialog/SystemModelDialog.tsx @@ -17,10 +17,10 @@ import { import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { LlmSvgIcon } from '@/components/AppSvgIcon'; -import { IconMap, type LLMFactory } from '@/constants/llm'; +import { type LLMFactory } from '@/constants/llm'; import type { ITenantInfo } from '@/interfaces/database/knowledge'; -import type { LlmModelType } from '@/constants/knowledge'; -import type { IMyLlmModel, IThirdOAIModel } from '@/interfaces/database/llm'; +import type { IThirdOAIModel } from '@/interfaces/database/llm'; +import { getFactoryIconName } from '@/utils/common'; interface AllModelOptionItem { label: string; @@ -75,11 +75,6 @@ function SystemModelDialog({ defaultValues: {} }); - // 获取工厂图标名称 - const getFactoryIconName = (factoryName: LLMFactory) => { - return IconMap[factoryName] || 'default'; - }; - // all model options 包含了全部的 options const llmOptions = useMemo(() => allModelOptions?.llmOptions || [], [allModelOptions]); const embdOptions = useMemo(() => allModelOptions?.embeddingOptions || [], [allModelOptions]); diff --git a/src/pages/setting/components/LLMFactoryCard.tsx b/src/pages/setting/components/LLMFactoryCard.tsx index 0ef0988..6016715 100644 --- a/src/pages/setting/components/LLMFactoryCard.tsx +++ b/src/pages/setting/components/LLMFactoryCard.tsx @@ -6,6 +6,7 @@ import { IconMap, type LLMFactory } from "@/constants/llm"; import type { IFactory } from "@/interfaces/database/llm"; import { Box, Button, Card, CardContent, Chip, Typography } from "@mui/material"; import { useTranslation } from 'react-i18next'; +import { getFactoryIconName } from '@/utils/common'; // 模型类型标签颜色映射 export const MODEL_TYPE_COLORS: Record = { @@ -30,11 +31,6 @@ const LLMFactoryCard: React.FC = ({ }) => { const { t } = useTranslation(); - // 获取工厂图标名称 - const getFactoryIconName = (factoryName: LLMFactory) => { - return IconMap[factoryName] || 'default'; - }; - return ( void) => { // 根据工厂类型打开对应的对话框 const openFactoryDialog = useCallback((factoryName: string, data?: any, isEdit = false) => { - // 使用通用的 ConfigurationDialog 替代特定的 Dialog configurationDialog.openConfigurationDialog(factoryName, data, isEdit); }, [configurationDialog]); diff --git a/src/pages/setting/models.tsx b/src/pages/setting/models.tsx index 93e3cec..e5d19b8 100644 --- a/src/pages/setting/models.tsx +++ b/src/pages/setting/models.tsx @@ -32,8 +32,16 @@ import { ModelDialogs } from './components/ModelDialogs'; import { useDialog } from '@/hooks/useDialog'; import logger from '@/utils/logger'; import { LLM_FACTORY_LIST, LocalLlmFactories, type LLMFactory } from '@/constants/llm'; +import { LlmSvgIcon } from '@/components/AppSvgIcon'; +import { getFactoryIconName } from '@/utils/common'; -function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model: ILlmItem) => void }) { +interface MyLlmGridItemProps { + model: ILlmItem, + onDelete: (model: ILlmItem) => void, + onEditLlm?: (model: ILlmItem) => void, +} + +function MyLlmGridItem({ model, onDelete, onEditLlm }: MyLlmGridItemProps) { return ( @@ -41,7 +49,21 @@ function MyLlmGridItem({ model, onDelete }: { model: ILlmItem, onDelete: (model: {model.name} - + + { + onEditLlm && ( + onEditLlm(model)} + > + + + ) + } { + const handleEditLlmFactory = useCallback((factoryName: string, llmmodel?: ILlmItem) => { if (factoryName == null) { return; } @@ -164,10 +186,17 @@ function ModelsPage() { // local llm modelDialogs.ollamaDialog.openDialog({ llm_factory: factoryN, + ...llmmodel, + llm_name: llmmodel?.name || '', + model_type: llmmodel?.type || 'chat', }, true); } else if (configurationFactories.includes(factoryN)) { // custom configuration llm - modelDialogs.configurationDialog.openConfigurationDialog(factoryN); + modelDialogs.configurationDialog.openConfigurationDialog(factoryN, { + ...llmmodel, + llm_name: llmmodel?.name || '', + model_type: llmmodel?.type || 'chat', + }); } else { // llm set api modelDialogs.apiKeyDialog.openApiKeyDialog(factoryN, {}, true); @@ -246,9 +275,9 @@ function ModelsPage() { {Object.entries(myLlm).map(([factoryName, group]) => ( - + - + : } - {/* 模型工厂名称 */} - - {factoryName} - + + {/* svg icon */} + + {/* 模型工厂名称 */} + + {factoryName} + {/* 模型标签 */} - + {group.tags.split(',').map((tag) => ( } onClick={() => handleEditLlmFactory(factoryName)} > - { showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')} + {showAddModel(factoryName) ? t('setting.addModel') : t('setting.edit')}