Merge branch 'PingChuan' into shiy-login

This commit is contained in:
2025-12-09 21:05:01 +08:00
8 changed files with 778 additions and 802 deletions
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -407,7 +407,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
confirmLoading={renameLoading} confirmLoading={renameLoading}
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
destroyOnClose destroyOnHidden
> >
<div className="py-4"> <div className="py-4">
<Input <Input
@@ -437,7 +437,7 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
okText="删除" okText="删除"
cancelText="取消" cancelText="取消"
okType="danger" okType="danger"
destroyOnClose destroyOnHidden
> >
<div className="py-4"> <div className="py-4">
<p> <strong>"{deletingConversation?.name}"</strong> </p> <p> <strong>"{deletingConversation?.name}"</strong> </p>
@@ -577,7 +577,7 @@ export default function AreaDatasetConfig() {
), ),
value: ds.id, value: ds.id,
}))} }))}
dropdownStyle={{ maxHeight: '300px' }} styles={{ popup: { root: { maxHeight: '300px' } } }}
/> />
</Form.Item> </Form.Item>
@@ -594,7 +594,7 @@ export default function AreaDatasetConfig() {
</Form.Item> </Form.Item>
{/* 知识库描述 */} {/* 知识库描述 */}
<Form.Item {/* <Form.Item
name="dataset_description" name="dataset_description"
label="知识库描述" label="知识库描述"
> >
@@ -603,7 +603,7 @@ export default function AreaDatasetConfig() {
rows={3} rows={3}
maxLength={500} maxLength={500}
/> />
</Form.Item> </Form.Item> */}
{/* 高级设置折叠面板 */} {/* 高级设置折叠面板 */}
<div style={{ marginTop: '24px' }}> <div style={{ marginTop: '24px' }}>
@@ -1,10 +1,8 @@
import { Form, Input, Button, Card, Spin, Divider, Select, Slider, InputNumber, Tooltip, Checkbox } from 'antd'; import { CheckCircleFilled, QuestionCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { SaveOutlined, QuestionCircleOutlined, CheckCircleFilled } from '@ant-design/icons'; import { Button, Card, Checkbox, Descriptions, Divider, InputNumber, Select, Slider, Spin, Tag, Tooltip } from 'antd';
import { useDatasetSettings, type SearchMethod } from '~/hooks/dify-dataset-manager/dataset-settings'; import { useDatasetSettings, type SearchMethod } from '~/hooks/dify-dataset-manager/dataset-settings';
import type { DatasetSettingsProps } from '~/types/dify-dataset-manager/dataset-settings'; import type { DatasetSettingsProps } from '~/types/dify-dataset-manager/dataset-settings';
const { TextArea } = Input;
// 检索方式选项 // 检索方式选项
const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description: string }[] = [ const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description: string }[] = [
{ label: '向量检索', value: 'semantic_search', description: '基于语义理解的智能检索,适合需要理解上下文的场景' }, { label: '向量检索', value: 'semantic_search', description: '基于语义理解的智能检索,适合需要理解上下文的场景' },
@@ -15,23 +13,21 @@ const SEARCH_METHOD_OPTIONS: { label: string; value: SearchMethod; description:
/** /**
* 知识库设置组件 * 知识库设置组件
* 用于修改知识库名称和描述 * 使用 Descriptions 展示只读的知识库基本信息,提供可编辑的检索设置
* 注:Dify API 不支持修改知识库名称和描述,故这些字段仅作只读展示
*/ */
export default function DatasetSettings({ export default function DatasetSettings({
dataset, dataset,
onDatasetUpdated, onDatasetUpdated,
}: DatasetSettingsProps) { }: DatasetSettingsProps) {
const [form] = Form.useForm();
const { const {
saving, saving,
hasChanges, hasChanges,
retrievalSettings, retrievalSettings,
handleValuesChange,
handleSave, handleSave,
handleReset, handleReset,
updateRetrievalSettings, updateRetrievalSettings,
} = useDatasetSettings(dataset, form, onDatasetUpdated); } = useDatasetSettings(dataset, onDatasetUpdated);
// 是否需要显示 Reranking 提示(语义检索和混合检索需要,且强制开启) // 是否需要显示 Reranking 提示(语义检索和混合检索需要,且强制开启)
const showRerankingInfo = retrievalSettings.searchMethod === 'semantic_search' || retrievalSettings.searchMethod === 'hybrid_search'; const showRerankingInfo = retrievalSettings.searchMethod === 'semantic_search' || retrievalSettings.searchMethod === 'hybrid_search';
@@ -53,66 +49,56 @@ export default function DatasetSettings({
<div className="page-header"> <div className="page-header">
<h1></h1> <h1></h1>
<p className="page-description"> <p className="page-description">
</p> </p>
</div> </div>
{/* 设置表单 */} {/* 知识库基本信息 */}
<Card className="settings-card"> <Card
<Form className="settings-card"
form={form} title={
layout="vertical" <span style={{ fontSize: 16, fontWeight: 500 }}>
onValuesChange={handleValuesChange}
</span>
}
>
<Descriptions
column={{ xs: 1, sm: 2, md: 2, lg: 3 }}
labelStyle={{
color: '#8c8c8c',
fontWeight: 400,
}}
contentStyle={{
color: '#262626',
fontWeight: 500,
}}
> >
<Form.Item <Descriptions.Item label="知识库名称" span={3}>
name="name" <span style={{ fontSize: 15 }}>{dataset.name}</span>
label="知识库名称" </Descriptions.Item>
rules={[ {dataset.description && (
{ required: true, message: '请输入知识库名称' }, <Descriptions.Item label="描述" span={3}>
{ max: 100, message: '名称不能超过100个字符' }, <span style={{ color: '#595959' }}>{dataset.description}</span>
]} </Descriptions.Item>
> )}
<Input placeholder="请输入知识库名称" maxLength={100} /> <Descriptions.Item label="索引方式">
</Form.Item> <Tag color={dataset.indexing_technique === 'high_quality' ? 'green' : 'blue'}>
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
<Form.Item </Tag>
name="description" </Descriptions.Item>
label="知识库描述" <Descriptions.Item label="文档数量">
extra="描述知识库的用途和内容(仅展示,暂不支持修改)" <span style={{ fontFamily: 'monospace' }}>{dataset.document_count}</span>
> </Descriptions.Item>
<TextArea <Descriptions.Item label="总字符数">
placeholder="请输入知识库描述" <span style={{ fontFamily: 'monospace' }}>{dataset.word_count?.toLocaleString() || 0}</span>
rows={4} </Descriptions.Item>
maxLength={500} <Descriptions.Item label="创建时间" span={2}>
showCount {new Date(dataset.created_at * 1000).toLocaleString('zh-CN')}
disabled </Descriptions.Item>
/> <Descriptions.Item label="最后更新">
</Form.Item> {new Date(dataset.updated_at * 1000).toLocaleString('zh-CN')}
</Descriptions.Item>
{/* 只读信息 */} </Descriptions>
<div className="readonly-info">
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.document_count}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.word_count?.toLocaleString()}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{new Date(dataset.created_at * 1000).toLocaleString('zh-CN')}
</span>
</div>
</div>
</Form>
</Card> </Card>
{/* 检索设置卡片 */} {/* 检索设置卡片 */}
@@ -55,7 +55,7 @@ export default function DatasetLayout({
placeholder="选择知识库" placeholder="选择知识库"
suffixIcon={<SwapOutlined />} suffixIcon={<SwapOutlined />}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
dropdownStyle={{ minWidth: 200 }} styles={{ popup: { root: { minWidth: 200 } } }}
> >
{availableDatasets.map(ds => ( {availableDatasets.map(ds => (
<Select.Option key={ds.dataset_id} value={ds.dataset_id}> <Select.Option key={ds.dataset_id} value={ds.dataset_id}>
@@ -1,5 +1,5 @@
import { SearchOutlined, FileSearchOutlined } from '@ant-design/icons'; import { FileSearchOutlined, SearchOutlined } from '@ant-design/icons';
import { Button, Tag, Input, Slider, Spin, Select, Flex, Switch, InputNumber, Tooltip } from 'antd'; import { Button, Card, Flex, Input, InputNumber, Select, Slider, Spin, Switch, Tag, Tooltip } from 'antd';
import type { RetrieveRecord } from '~/api/dify-dataset/type'; import type { RetrieveRecord } from '~/api/dify-dataset/type';
import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test'; import { useRetrieveTest } from '~/hooks/dify-dataset-manager/retrieve-test';
import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test'; import type { RetrieveTestProps } from '~/types/dify-dataset-manager/retrieve-test';
@@ -25,62 +25,46 @@ function ResultItem({ record, index }: { record: RetrieveRecord; index: number }
const scoreColor = record.score > 0.8 ? '#52c41a' : record.score > 0.5 ? '#faad14' : '#666'; const scoreColor = record.score > 0.8 ? '#52c41a' : record.score > 0.5 ? '#faad14' : '#666';
return ( return (
<Flex <div className="segment-item">
vertical <div className="segment-header">
gap={12} <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
style={{ <Tag style={{ background: scoreColor, color: '#fff', border: 'none', margin: 0 }}>
padding: 16,
background: colors.bgContainer,
borderRadius: 8,
border: `1px solid ${colors.border}`,
}}
>
<Flex justify="space-between" align="center">
<Flex gap={8} align="center">
<Tag style={{ background: scoreColor, color: '#fff', border: 'none' }}>
{scorePercent}% {scorePercent}%
</Tag> </Tag>
<span style={{ color: colors.textSecondary, fontSize: 12 }}> <span className="segment-index">#{index + 1}</span>
#{index + 1} · {record.segment.word_count} · {record.segment.hit_count} <span className="segment-chars">
{record.segment.word_count} · {record.segment.hit_count}
</span> </span>
</Flex> </div>
{record.segment.document && ( {record.segment.document && (
<span style={{ color: colors.textTertiary, fontSize: 12 }}> <span className="segment-chars">
: {record.segment.document.name} : {record.segment.document.name}
</span> </span>
)} )}
</Flex> </div>
<div style={{ <div className="segment-content">
color: colors.text,
fontSize: 14,
lineHeight: 1.6,
whiteSpace: 'pre-wrap',
}}>
{record.segment.content.length > 500 {record.segment.content.length > 500
? record.segment.content.substring(0, 500) + '...' ? record.segment.content.substring(0, 500) + '...'
: record.segment.content} : record.segment.content}
</div> </div>
{record.segment.answer && ( {record.segment.answer && (
<Flex <div style={{
vertical padding: 12,
gap={4} background: '#f0f0f0',
style={{ borderRadius: 6,
padding: 12, marginTop: 8,
background: colors.fillTertiary, }}>
borderRadius: 6, <span style={{ color: '#8c8c8c', fontSize: 12, display: 'block', marginBottom: 4 }}>
}}
>
<span style={{ color: colors.textSecondary, fontSize: 12 }}>
: :
</span> </span>
<span style={{ color: colors.text, fontSize: 14 }}> <span style={{ color: '#262626', fontSize: 14 }}>
{record.segment.answer.length > 200 {record.segment.answer.length > 200
? record.segment.answer.substring(0, 200) + '...' ? record.segment.answer.substring(0, 200) + '...'
: record.segment.answer} : record.segment.answer}
</span> </span>
</Flex> </div>
)} )}
</Flex> </div>
); );
} }
@@ -277,63 +261,32 @@ export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
</Flex> </Flex>
{/* 右侧面板 - 结果展示 */} {/* 右侧面板 - 结果展示 */}
<Flex <div className="preview-panel">
vertical <Card
flex={1} title={
gap={16} <div className="preview-header">
style={{ <span></span>
padding: 20, <span className="segment-count">
background: colors.bgElevated, {retrieveResults.length > 0 ? `${retrieveResults.length} 条结果` : '0 条结果'}
overflow: 'auto',
}}
>
{retrieving ? (
<Flex
flex={1}
align="center"
justify="center"
vertical
gap={12}
>
<Spin size="large" />
<span style={{ color: colors.textSecondary }}>
...
</span>
</Flex>
) : retrieveResults.length === 0 ? (
<Flex
flex={1}
align="center"
justify="center"
vertical
gap={12}
>
<FileSearchOutlined style={{
fontSize: 48,
color: colors.textQuaternary,
}} />
<span style={{ color: colors.textTertiary }}>
</span>
</Flex>
) : (
<>
<Flex justify="space-between" align="center">
<span style={{
fontSize: 14,
color: colors.text,
fontWeight: 500,
}}>
</span> </span>
<span style={{ </div>
fontSize: 13, }
color: colors.textSecondary, className="preview-card"
}}> >
{retrieveResults.length} {retrieving ? (
</span> <div className="preview-loading">
</Flex> <Spin size="large" />
<Flex vertical gap={12}> <div className="loading-text">...</div>
</div>
) : retrieveResults.length === 0 ? (
<div className="preview-empty">
<div className="empty-icon">
<FileSearchOutlined />
</div>
<p></p>
</div>
) : (
<div className="preview-segments">
{retrieveResults.map((record, index) => ( {retrieveResults.map((record, index) => (
<ResultItem <ResultItem
key={record.segment.id} key={record.segment.id}
@@ -341,10 +294,10 @@ export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
index={index} index={index}
/> />
))} ))}
</Flex> </div>
</> )}
)} </Card>
</Flex> </div>
</Flex> </Flex>
); );
} }
@@ -1,8 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd'; import { message } from 'antd';
import type { FormInstance } from 'antd'; import { useCallback, useEffect, useState } from 'react';
import { fetchDataset, updateDatasetSettings } from '~/api/dify-dataset/api/datasetApi';
import type { Dataset, RetrievalModel } from '~/api/dify-dataset/type/datasetTypes'; import type { Dataset, RetrievalModel } from '~/api/dify-dataset/type/datasetTypes';
import { updateDatasetSettings, fetchDataset } from '~/api/dify-dataset/api/datasetApi';
import { DIFY_CONFIG } from '~/config/api-config'; import { DIFY_CONFIG } from '~/config/api-config';
/** /**
@@ -75,10 +74,10 @@ function formValuesToRetrievalModel(values: RetrievalSettingsFormValues): Retrie
/** /**
* 知识库设置状态管理 Hook * 知识库设置状态管理 Hook
* 注:Dify API 不支持修改知识库名称和描述,只支持修改检索设置
*/ */
export function useDatasetSettings( export function useDatasetSettings(
dataset: Dataset | null, dataset: Dataset | null,
form: FormInstance,
onDatasetUpdated: (dataset: Dataset) => void onDatasetUpdated: (dataset: Dataset) => void
) { ) {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -89,18 +88,36 @@ export function useDatasetSettings(
() => retrievalModelToFormValues(dataset?.retrieval_model_dict) () => retrievalModelToFormValues(dataset?.retrieval_model_dict)
); );
// 初始化表单数据 // 原始检索设置,用于对比变化
const [originalSettings, setOriginalSettings] = useState<RetrievalSettingsFormValues>(
() => retrievalModelToFormValues(dataset?.retrieval_model_dict)
);
// 初始化检索设置
useEffect(() => { useEffect(() => {
if (dataset) { if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
console.log('[DatasetSettings] 初始化检索设置, retrieval_model_dict:', dataset.retrieval_model_dict); console.log('[DatasetSettings] 初始化检索设置, retrieval_model_dict:', dataset.retrieval_model_dict);
setRetrievalSettings(retrievalModelToFormValues(dataset.retrieval_model_dict)); const settings = retrievalModelToFormValues(dataset.retrieval_model_dict);
setRetrievalSettings(settings);
setOriginalSettings(settings);
setHasChanges(false); setHasChanges(false);
} }
}, [dataset, form]); }, [dataset]);
/**
* 检查检索设置是否有变化
*/
const checkRetrievalChanges = useCallback((newSettings: RetrievalSettingsFormValues) => {
const hasChanged =
newSettings.searchMethod !== originalSettings.searchMethod ||
newSettings.topK !== originalSettings.topK ||
newSettings.scoreThresholdEnabled !== originalSettings.scoreThresholdEnabled ||
newSettings.scoreThreshold !== originalSettings.scoreThreshold ||
newSettings.rerankingEnable !== originalSettings.rerankingEnable ||
newSettings.weights !== originalSettings.weights;
setHasChanges(hasChanged);
}, [originalSettings]);
/** /**
* 更新检索设置 * 更新检索设置
@@ -112,40 +129,14 @@ export function useDatasetSettings(
setRetrievalSettings(prev => { setRetrievalSettings(prev => {
const newSettings = { ...prev, [key]: value }; const newSettings = { ...prev, [key]: value };
// 检查是否有变化 // 检查是否有变化
checkForChanges(newSettings); checkRetrievalChanges(newSettings);
return newSettings; return newSettings;
}); });
}, [dataset]); }, [checkRetrievalChanges]);
/**
* 检查是否有变化
*/
const checkForChanges = useCallback((newRetrievalSettings?: RetrievalSettingsFormValues) => {
const values = form.getFieldsValue();
const currentRetrieval = newRetrievalSettings || retrievalSettings;
const originalRetrieval = retrievalModelToFormValues(dataset?.retrieval_model_dict);
const nameChanged = values.name !== dataset?.name;
const retrievalChanged =
currentRetrieval.searchMethod !== originalRetrieval.searchMethod ||
currentRetrieval.topK !== originalRetrieval.topK ||
currentRetrieval.scoreThresholdEnabled !== originalRetrieval.scoreThresholdEnabled ||
currentRetrieval.scoreThreshold !== originalRetrieval.scoreThreshold ||
currentRetrieval.rerankingEnable !== originalRetrieval.rerankingEnable ||
currentRetrieval.weights !== originalRetrieval.weights;
setHasChanges(nameChanged || retrievalChanged);
}, [form, dataset, retrievalSettings]);
/**
* 处理表单值变化
*/
const handleValuesChange = useCallback(() => {
checkForChanges();
}, [checkForChanges]);
/** /**
* 保存设置 * 保存设置
* 注:仅保存检索设置,Dify API 不支持修改名称和描述
*/ */
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
if (!dataset) { if (!dataset) {
@@ -154,12 +145,10 @@ export function useDatasetSettings(
} }
try { try {
const values = await form.validateFields();
setSaving(true); setSaving(true);
// 构建完整的更新请求 // 仅更新检索设置
await updateDatasetSettings(dataset.id, { await updateDatasetSettings(dataset.id, {
name: values.name,
retrieval_model: formValuesToRetrievalModel(retrievalSettings), retrieval_model: formValuesToRetrievalModel(retrievalSettings),
}); });
@@ -167,8 +156,9 @@ export function useDatasetSettings(
const fullDataset = await fetchDataset(dataset.id); const fullDataset = await fetchDataset(dataset.id);
console.log('[DatasetSettings] 保存后获取完整数据:', fullDataset); console.log('[DatasetSettings] 保存后获取完整数据:', fullDataset);
message.success('保存成功'); message.success('检索设置保存成功');
onDatasetUpdated(fullDataset); onDatasetUpdated(fullDataset);
setOriginalSettings(retrievalSettings);
setHasChanges(false); setHasChanges(false);
} catch (err: any) { } catch (err: any) {
console.error('保存设置失败:', err); console.error('保存设置失败:', err);
@@ -176,21 +166,17 @@ export function useDatasetSettings(
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, [dataset, form, retrievalSettings, onDatasetUpdated]); }, [dataset, retrievalSettings, onDatasetUpdated]);
/** /**
* 重置表单 * 重置检索设置
*/ */
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
if (dataset) { if (dataset) {
form.setFieldsValue({ setRetrievalSettings(originalSettings);
name: dataset.name,
description: dataset.description || '',
});
setRetrievalSettings(retrievalModelToFormValues(dataset.retrieval_model_dict));
setHasChanges(false); setHasChanges(false);
} }
}, [dataset, form]); }, [dataset, originalSettings]);
return { return {
// 状态 // 状态
@@ -199,7 +185,6 @@ export function useDatasetSettings(
retrievalSettings, retrievalSettings,
// 方法 // 方法
handleValuesChange,
handleSave, handleSave,
handleReset, handleReset,
updateRetrievalSettings, updateRetrievalSettings,
@@ -6,7 +6,7 @@
.dataset-manager-wrapper { .dataset-manager-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height:90%;
max-height: 100%; max-height: 100%;
background: #fff; background: #fff;
overflow: hidden; overflow: hidden;
@@ -26,13 +26,14 @@
/* 左侧侧边栏 */ /* 左侧侧边栏 */
.dataset-sidebar { .dataset-sidebar {
width: 200px; width: 26vh;
min-width: 200px; min-width: 11vh;
background: #fafafa; background: #fafafa;
border-right: 1px solid #f0f0f0; border-right: 1px solid #f0f0f0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
max-height: 90vh;
} }
/* 返回按钮 */ /* 返回按钮 */
@@ -229,6 +230,7 @@
/* 右侧主内容区 */ /* 右侧主内容区 */
.dataset-main { .dataset-main {
flex: 1; flex: 1;
height: 85vh;
min-width: 0; min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -735,6 +737,7 @@
/* 右侧预览面板 */ /* 右侧预览面板 */
.preview-panel { .preview-panel {
height: 85vh;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
background: #f5f5f5; background: #f5f5f5;
@@ -852,6 +855,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
/* 关键修复:添加高度约束和内部滚动 */
height: 100%;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
} }
.segment-item { .segment-item {