diff --git a/docs/ahooks-usage-guide.md b/docs/ahooks-usage-guide.md new file mode 100644 index 0000000..ea4101e --- /dev/null +++ b/docs/ahooks-usage-guide.md @@ -0,0 +1,287 @@ +# Ahooks 使用指南(基础、进阶与常见用法) + +Ahooks 是一套高质量、可复用的 React Hooks 集合,适合在业务工程中快速构建稳定的交互与数据逻辑。本文档覆盖快速上手、基础用法、`useRequest` 核心与进阶、常见场景模板、最佳实践与坑位排查,结合本项目技术栈(React 18 + Vite + MUI)。 + +目录 +- 为什么选 Ahooks +- 安装与类型支持 +- 快速上手:页面标题(`useTitle`) +- 基础能力:状态/事件/定时/存储/性能 +- useRequest 基础与进阶(自动/手动、轮询、缓存、SWR、重试等) +- 常见场景模板(搜索防抖、任务轮询、分页、依赖请求、自动保存) +- 最佳实践与常见坑位 +- 速查表与参考资料 + +## 为什么选用 Ahooks + +- 大量生产验证的 Hooks,覆盖状态管理、请求、DOM、浏览器、性能优化等场景。 +- API 统一、可组合,适合封装成业务级自定义 Hook。 +- 降低维护成本:内置防抖/节流、轮询、缓存、并发控制、卸载安全、SWR 等复杂能力。 + +## 安装与类型支持 + +```bash +pnpm add ahooks +``` + +TypeScript 用户无需额外配置,Ahooks 提供完整的类型提示。 + +## 快速上手:更新页面标题(useTitle) + +最直接的需求就是“更新页面标题”。`useTitle` 在组件挂载时设置 `document.title`,卸载时可恢复。 + +```tsx +import { useTitle } from 'ahooks'; + +export default function KnowledgeListPage() { + useTitle('TERES · 知识库列表', { restoreOnUnmount: true }); + return (
...
); +} +``` + +如需统一加前后缀,可封装: + +```tsx +import { useTitle } from 'ahooks'; + +export function usePageTitle(title?: string, opts?: { prefix?: string; suffix?: string }) { + const final = [opts?.prefix, title, opts?.suffix].filter(Boolean).join(' · '); + useTitle(final || 'TERES', { restoreOnUnmount: true }); +} +``` + +## 基础能力(常用 Hooks) + +- useBoolean / useToggle:布尔与枚举状态切换 + ```tsx + const [visible, { setTrue, setFalse, toggle }] = useBoolean(false); + ``` + +- useDebounceFn / useThrottleFn:防抖与节流(输入、滚动、窗口变化) + ```tsx + const { run: onSearchChange, cancel } = useDebounceFn((kw: string) => setKeywords(kw), { wait: 300 }); + ``` + +- useEventListener:声明式事件监听(自动清理) + ```tsx + useEventListener('resize', () => console.log(window.innerWidth), { target: window }); + ``` + +- useLocalStorageState / useSessionStorageState:状态持久化 + ```tsx + const [lang, setLang] = useLocalStorageState('lang', { defaultValue: 'zh' }); + ``` + +- useInterval / useTimeout:定时任务(自动清理) + ```tsx + useInterval(() => refresh(), 30000); + ``` + +- useMemoizedFn:稳定函数引用,避免子组件无谓重渲染/解绑 + ```tsx + const onNodeClick = useMemoizedFn((id: string) => openDetail(id)); + ``` + +- useLatest:防陈旧闭包,异步/周期函数读取最新数据 + ```tsx + const latestState = useLatest(state); + useInterval(() => { doSomething(latestState.current); }, 1000); + ``` + +- useSafeState / useUnmount:卸载安全,避免内存泄漏或报错 + ```tsx + const [data, setData] = useSafeState(null); + useUnmount(() => cancelRequest()); + ``` + +## useRequest(核心与进阶) + +`useRequest` 是 Ahooks 的异步数据管理核心,内置自动/手动触发、轮询、防抖/节流、屏幕聚焦重新请求、错误重试、loading 延迟、SWR(stale-while-revalidate)缓存等能力。[0][1] + +### 基础用法(自动请求) + +```tsx +import { useRequest } from 'ahooks'; +import agentService from '@/services/agent_service'; + +export default function AgentList() { + const { data, loading, error, refresh } = useRequest(() => agentService.fetchAgentList()); + if (loading) return
Loading...
; + if (error) return
Error: {String(error)}
; + return
{JSON.stringify(data)}
; +} +``` + +### 手动触发与参数管理 + +```tsx +const { run, runAsync, loading, params } = useRequest((kw: string) => agentService.searchAgents({ keywords: kw }), { + manual: true, + onSuccess: (data, [kw]) => console.log('searched', kw, data), +}); + +// 触发 +run('rag'); +// 或 await runAsync('rag').catch(console.error); +``` + +### 生命周期回调 + +```tsx +useRequest(api, { + onBefore: (p) => console.log('before', p), + onSuccess: (d) => console.log('success', d), + onError: (e) => console.error('error', e), + onFinally: (p, d, e) => console.log('finally'), +}); +``` + +### 刷新上一次请求(refresh)与数据变更(mutate) + +```tsx +const { run, refresh, mutate, data } = useRequest(() => api.getUser(1), { manual: true }); + +// 修改页面数据,不等待接口返回 +mutate((old) => ({ ...old, name: 'New Name' })); +// 使用上一次参数重新发起请求 +refresh(); +``` + +### 取消响应(cancel)与竞态取消 + +```tsx +const { run, cancel } = useRequest(api.longTask, { manual: true }); +run(); +// 某些场景需要忽略本次 promise 的响应 +cancel(); +``` + +### 轮询与停止条件 + +```tsx +const { data, cancel } = useRequest(() => agentService.getJobStatus(), { + pollingInterval: 5000, + pollingWhenHidden: false, + onSuccess: (res) => { if (res.done) cancel(); }, +}); +``` + +### 防抖 / 节流 / 重试 / 延迟 / 焦点刷新 + +```tsx +useRequest(searchService.query, { + manual: true, + debounceWait: 300, + throttleWait: 1000, + retryCount: 2, + loadingDelay: 200, + refreshOnWindowFocus: true, +}); +``` + +### 缓存(cacheKey)与过期时间(staleTime) + +```tsx +const { data } = useRequest(() => agentService.fetchAgentList(), { + cacheKey: 'agent:list', + staleTime: 60_000, // 1分钟内复用缓存(SWR 策略) +}); +``` + +### 条件就绪(ready)与依赖刷新(refreshDeps) + +```tsx +useRequest(() => api.getById(id), { + ready: !!id, + refreshDeps: [id], +}); +``` + +### 默认参数(defaultParams) + +```tsx +const { data } = useRequest((id: number) => api.getById(id), { + defaultParams: [1], +}); +``` + +## 常见场景模板 + +### 搜索输入防抖 + +```tsx +const { run: search } = useRequest((kw: string) => api.search({ keywords: kw }), { manual: true }); +const { run: onChange, cancel } = useDebounceFn((kw: string) => search(kw), { wait: 300 }); +``` + +### 任务状态轮询,完成后停止 + +```tsx +const { data, cancel } = useRequest(() => api.getJobStatus(jobId), { + pollingInterval: 5000, + pollingWhenHidden: false, + onSuccess: (res) => { if (res.done) cancel(); }, +}); +``` + +### 分页列表(前端分页) + +```tsx +const { data, loading } = useRequest(() => api.list({ page, pageSize, keywords }), { + refreshDeps: [page, pageSize, keywords], +}); +``` + +### 依赖请求(根据上游结果触发下游) + +```tsx +const userReq = useRequest(() => api.getUser(userId), { ready: !!userId }); +const postsReq = useRequest(() => api.getPosts(userReq.data?.id), { + ready: !!userReq.data?.id, +}); +``` + +### 自动保存(组件生命周期安全) + +```tsx +useInterval(async () => { + if (!graphValid) return; + await api.setAgentDSL(payload); +}, 30_000); +``` + +## 最佳实践与常见坑位 + +- 顶层调用 Hooks,避免条件语句中调用,保持顺序一致。 +- 依赖项变动可能导致频繁触发;`useMemoizedFn` 可保持函数引用稳定。 +- 轮询需显式停止;结合 `cancel()` 与 `pollingWhenHidden=false`。 +- 缓存需设置合理 `staleTime`,避免过时数据;SWR 适合只读列表数据。 +- 组件卸载时自动忽略响应,避免卸载后 setState。 +- SSR 环境下注意 `window/document` 可用性,必要时使用 `useIsomorphicLayoutEffect`。 + +## 与本项目的结合建议 + +- 知识库列表搜索:将 `setTimeout` 防抖替换为 `useDebounceFn`,代码更简洁且可取消。 +- 任务状态轮询:用 `useRequest` 的 `pollingInterval + cancel()` 替代手写 `setInterval`。 +- 自动保存:使用 `useInterval`,确保随组件生命周期清理(我们已修复泄漏问题)。 +- 事件绑定:用 `useEventListener` 管理 `window`/`document` 事件,自动清理。 +- 页面标题:用 `useTitle` 或统一的 `usePageTitle` 封装。 + +## 速查表(精选) + +- 状态:`useBoolean`、`useToggle`、`useSetState`、`useControllableValue` +- 请求:`useRequest` +- DOM/事件:`useEventListener`、`useInViewport`、`useClickAway` +- 定时:`useInterval`、`useTimeout` +- 存储:`useLocalStorageState`、`useSessionStorageState` +- 性能/安全:`useDebounceFn`、`useThrottleFn`、`useMemoizedFn`、`useLatest`、`useSafeState` + +## 参考资料 + +- 官方文档(useRequest 快速上手)[0] https://ahooks.js.org/zh-CN/hooks/use-request/index +- 官方文档(useRequest 基础用法)[1] https://ahooks.js.org/zh-CN/hooks/use-request/basic +- 项目仓库:https://github.com/alibaba/hooks + +--- + +实战建议:如果只需要“更新 web 标题”,直接在页面组件里使用 `useTitle`;若需统一标题规范或根据路由动态拼接,封装一个 `usePageTitle` 更可维护。`useRequest` 作为核心请求管理,优先在网络交互中使用它的自动/手动、轮询、缓存与重试能力,替代手写的定时器与状态机。 \ No newline at end of file diff --git a/index.html b/index.html index e7f4571..199ae81 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - teres_web_frontend + RAG Empowerment System
diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..ae94b95 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 52850a1..3d88310 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import AppRoutes from './routes'; import SnackbarProvider from './components/Provider/SnackbarProvider'; import DialogProvider from './components/Provider/DialogProvider'; import AuthGuard from './components/AuthGuard'; +import { useTitle } from 'ahooks'; import './locales'; import './utils/request' @@ -40,6 +41,8 @@ function MaterialUIApp() { } function App() { + useTitle('RAG Empowerment System'); + return ( ); diff --git a/src/components/FormField/RadioFormField.tsx b/src/components/FormField/RadioFormField.tsx new file mode 100644 index 0000000..0bd8a78 --- /dev/null +++ b/src/components/FormField/RadioFormField.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { + Box, + Typography, + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, + FormHelperText, +} from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +export interface RadioOption { + value: string | number; + label: string; + disabled?: boolean; +} + +export interface RadioFormFieldProps { + name: string; + label?: string; + options: RadioOption[]; + helperText?: string; + defaultValue?: string | number; + disabled?: boolean; + required?: boolean; + row?: boolean; + size?: 'small' | 'medium'; + onChangeValue?: (value: string | number) => void; +} + +export const RadioFormField: React.FC = ({ + name, + label, + options, + helperText, + defaultValue = '', + disabled = false, + required = false, + row = true, + size = 'medium', + onChangeValue, +}) => { + const { control } = useFormContext(); + const { t } = useTranslation(); + + return ( + + + {required && *} + {label} + + ( + + {label && + {label} + } + { + field.onChange(value); + onChangeValue?.(value); + }} + > + {options.map((item) => ( + } + label={item.label} + /> + ))} + + {(error || helperText) && ( + + {error?.message || helperText} + + )} + + )} + /> + + ); +}; + +export default RadioFormField; \ No newline at end of file diff --git a/src/components/FormField/index.ts b/src/components/FormField/index.ts index 873c6b4..2b35cb6 100644 --- a/src/components/FormField/index.ts +++ b/src/components/FormField/index.ts @@ -9,6 +9,7 @@ export { SliderFormField } from './SliderFormField'; export { TextFormField } from './TextFormField'; export { DatePickerFormField } from './DatePickerFormField'; export { CheckboxFormField } from './CheckboxFormField'; +export { RadioFormField } from './RadioFormField'; // 类型导出 export type { SliderInputFormFieldProps } from './SliderInputFormField'; diff --git a/src/hooks/agent-hooks.ts b/src/hooks/agent-hooks.ts index c3ce55d..da3cdf1 100644 --- a/src/hooks/agent-hooks.ts +++ b/src/hooks/agent-hooks.ts @@ -44,7 +44,13 @@ export const useAgentList = (initialParams?: IAgentPaginationParams) => { setLoading(true); setError(null); try { - const response = await agentService.listCanvas(params); + const envMode = import.meta.env.MODE; + let response: any = null; + if (envMode === 'flask') { + response = await agentService.teamlistCanvas(params); + } else { + response = await agentService.listCanvas(params); + } const res = response.data || {}; logger.info('useAgentList fetchAgentList', res); const data = res.data diff --git a/src/interfaces/database/agent.ts b/src/interfaces/database/agent.ts index a96c317..e55a743 100644 --- a/src/interfaces/database/agent.ts +++ b/src/interfaces/database/agent.ts @@ -268,25 +268,6 @@ export interface IAgentLogMessage { id: string; } -export interface IPipeLineListRequest { - page?: number; - page_size?: number; - keywords?: string; - orderby?: string; - desc?: boolean; - // canvas_category?: AgentCategory; -} - -// { -// "create_date": "Thu, 06 Nov 2025 15:52:42 GMT", -// "create_time": 1762415562692, -// "id": "931d1cfabae511f097ca36b0b158b556", -// "title": "blank_2025_11_06_15_52_42", -// "update_date": "Thu, 06 Nov 2025 15:52:42 GMT", -// "update_time": 1762415562692, -// "user_canvas_id": "1edc3ddabadb11f08d0636b0b158b556" -// } - export interface IAgentVersionItem { id: string; title: string; diff --git a/src/interfaces/database/knowledge.ts b/src/interfaces/database/knowledge.ts index a9f3043..1e3e98d 100644 --- a/src/interfaces/database/knowledge.ts +++ b/src/interfaces/database/knowledge.ts @@ -102,11 +102,11 @@ export interface IKnowledge { /** 解析器ID */ parser_id: string; /** 管道ID */ - pipeline_id: string; + pipeline_id?: string; /** 管道名称 */ - pipeline_name: string; + pipeline_name?: string; /** 管道头像 */ - pipeline_avatar: string; + pipeline_avatar?: string; /** 权限设置 */ permission: string; /** 相似度阈值 */ diff --git a/src/interfaces/request/agent.ts b/src/interfaces/request/agent.ts index c08b392..5924607 100644 --- a/src/interfaces/request/agent.ts +++ b/src/interfaces/request/agent.ts @@ -1,3 +1,4 @@ +import { AgentCategory } from "../../constants/agent"; import { DSL } from "../database/agent"; /** @@ -7,6 +8,9 @@ export interface IAgentPaginationParams { keywords?: string; page?: number; page_size?: number; + orderby?: string; + desc?: boolean; + canvas_category?: AgentCategory; } diff --git a/src/locales/en.ts b/src/locales/en.ts index 9ec42e6..3b1cb62 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -644,6 +644,7 @@ export default { eidtLinkDataPipeline: 'Edit Data Pipeline', linkPipelineSetTip: 'Manage data pipeline linkage with this dataset', default: 'Default', + buildMode: 'Build Mode', dataPipeline: 'Data Pipeline', linkDataPipeline: 'Link Data Pipeline', enableAutoGenerate: 'Enable Auto Generate', diff --git a/src/pages/agent/components/AgentTopbar.tsx b/src/pages/agent/components/AgentTopbar.tsx index febb913..1c5210f 100644 --- a/src/pages/agent/components/AgentTopbar.tsx +++ b/src/pages/agent/components/AgentTopbar.tsx @@ -13,7 +13,7 @@ export interface AgentTopbarProps { onOpenSettings?: () => void; } -export default function AgentTopbar({ title, id, onRefresh, subtitle, actionsRight, onOpenVersions, onOpenSettings }: AgentTopbarProps) { +export default function AgentTopbar({ title, onRefresh, subtitle, actionsRight, onOpenVersions, onOpenSettings }: AgentTopbarProps) { const { t } = useTranslation(); return ( ); } @@ -341,6 +341,42 @@ export function TOCEnhanceItem() { ); } +// Pipeline 选择器 +export function PipelineSelectorItem() { + const { t } = useTranslation(); + const [options, setOptions] = useState([]); + + useEffect(() => { + const fetchPipelines = async () => { + try { + const envMode = import.meta.env.MODE; + const service = envMode === 'flask' ? agentService.teamlistCanvas : agentService.listCanvas; + const res = await service({ canvas_category: AgentCategory.DataflowCanvas, page_size: 100 }); + const data = res?.data?.data || {}; + const list = data.canvas || []; + const mapped: SelectOption[] = list.map((item: any) => ({ + value: item.id, + label: item.title || item.name || item.id, + disabled: false, + })); + setOptions(mapped); + } catch (err) { + console.error('Failed to fetch pipelines:', err); + } + }; + fetchPipelines(); + }, []); + + return ( + + ); +} + /* ============================================================================ * 第二部分:RAPTOR策略 (RAPTOR Strategy) * ============================================================================ */ @@ -510,7 +546,33 @@ export function CommunityReportItem() { * 组合配置组件 (Composite Configuration Components) * ============================================================================ */ -// RAPTOR策略配置项组合 +/* + * 基础配置项 (Basic Configuration Items) + */ +export function BasicConfigItems() { + return ( + + {/* PDF解析器 */} + + {/* 建议文本块大小 */} + + {/* 文本分段标识符 */} + + {/* 目录增强 */} + + {/* 自动关键词提取 */} + + {/* 自动问题提取 */} + + {/* 表格转HTML */} + + + ); +} + +/** + * RAPTOR策略配置项组合 + */ export function RaptorConfigItems() { return ( @@ -530,7 +592,9 @@ export function RaptorConfigItems() { ); } -// 知识图谱配置项组合 +/** + * 知识图谱配置项组合 + */ export function KnowledgeGraphConfigItems() { return ( diff --git a/src/pages/knowledge/configuration/naive.tsx b/src/pages/knowledge/configuration/naive.tsx index 64eca7f..acf33b6 100644 --- a/src/pages/knowledge/configuration/naive.tsx +++ b/src/pages/knowledge/configuration/naive.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Box, Typography, @@ -13,20 +13,23 @@ import { useTranslation } from 'react-i18next'; import { ConfigurationFormContainer, MainContainer } from './configuration-form-container'; import { ChunkMethodItem, - ChunkTokenNumberItem, - AutoKeywordsItem, - AutoQuestionsItem, - HtmlForExcelItem, - LayoutRecognizeItem, - DelimiterItem, - TOCEnhanceItem, RaptorConfigItems, - KnowledgeGraphConfigItems + KnowledgeGraphConfigItems, + PipelineSelectorItem, + BasicConfigItems, } from './common-items'; +import { RadioFormField } from '@/components/FormField'; export function NaiveConfiguration() { - const { formState: { errors } } = useFormContext(); + const { formState: { errors }, watch } = useFormContext(); const { t } = useTranslation(); + const [buildMode, setBuildMode] = useState<'buildIn' | 'pipeline'>('buildIn'); + + // 根据表单的 pipeline_id 自动切换 buildMode + const pipelineId = watch('pipeline_id'); + useEffect(() => { + setBuildMode(pipelineId ? 'pipeline' : 'buildIn'); + }, [pipelineId]); return ( @@ -38,26 +41,27 @@ export function NaiveConfiguration() { {/* 切片方法 */} - - + + setBuildMode(String(v) as 'buildIn' | 'pipeline')} + /> + {buildMode === 'buildIn' ? ( + + ) : ( + + )} - - - {/* PDF解析器 */} - - {/* 建议文本块大小 */} - - {/* 文本分段标识符 */} - - {/* 目录增强 */} - - {/* 自动关键词提取 */} - - {/* 自动问题提取 */} - - {/* 表格转HTML */} - + + {buildMode === 'buildIn' && ( + + )} diff --git a/src/pages/knowledge/setting.tsx b/src/pages/knowledge/setting.tsx index ba71e59..71050e3 100644 --- a/src/pages/knowledge/setting.tsx +++ b/src/pages/knowledge/setting.tsx @@ -136,6 +136,7 @@ function KnowledgeBaseSetting() { parser_id: data.parser_id, embd_id: data.embd_id, parser_config: data.parser_config, + pipeline_id: data.pipeline_id, }; await updateKnowledgeModelConfig(configData); diff --git a/src/services/agent_service.ts b/src/services/agent_service.ts index e07abf5..f63d9ec 100644 --- a/src/services/agent_service.ts +++ b/src/services/agent_service.ts @@ -13,6 +13,12 @@ const agentService = { * 获取团队下的Canvas列表 */ listCanvas: (params?: IAgentPaginationParams) => { + return request.get(api.listCanvas, { params }); + }, + /** + * 获取团队下的Canvas列表 + */ + teamlistCanvas: (params?: IAgentPaginationParams) => { return request.get(api.listTeamCanvas, { params }); }, /**