Commit 86e301ce by Archer Committed by GitHub

fix: input guide (#7033)

parent 3869ea07
......@@ -13,7 +13,7 @@ description: FastGPT 对话问题引导
## 自定义词库接口
需要保证这个接口可以被用户浏览器访问。
需要保证这个接口可以被用户浏览器访问,需要注意允许跨域。
**请求:**
......@@ -21,7 +21,7 @@ description: FastGPT 对话问题引导
curl --location --request GET 'http://localhost:3000/api/core/chat/inputGuide/query?appId=663c75302caf8315b1c00194&searchKey=你'
```
其中 `appId` 为应用ID,`searchKey` 为搜索关键字,最多是50个字符。
其中 `appId` 为应用 ID,`searchKey` 为搜索关键字,最多是 50 个字符。
**响应**
......@@ -30,21 +30,13 @@ curl --location --request GET 'http://localhost:3000/api/core/chat/inputGuide/qu
"code": 200,
"statusText": "",
"message": "",
"data": [
"是你",
"你是谁呀",
"你好好呀",
"你好呀",
"你是谁!",
"你好"
]
"data": ["是你", "你是谁呀", "你好好呀", "你好呀", "你是谁!", "你好"]
}
```
data是一个数组,包含了搜索到的问题,最多只需要返回5个问题。
data 是一个数组,包含了搜索到的问题,最多只需要返回 5 个问题。
**参数说明:**
- appId - 应用ID
- appId - 应用 ID
- searchKey - 搜索关键字
---
title: 'V4.15.0-beta4 (in progress)'
description: 'FastGPT V4.15.0-beta4 Release Notes'
---
## 🚀 New Features
1. Added virtual list rendering for apps and Knowledge Bases.
2. Added separate OpenAPI documentation to distinguish it from the dev API documentation.
## ⚙️ Improvements
1. Added validation to input guide configuration to avoid invalid custom lexicon URLs.
## 🐛 Bug Fixes
## 🛠️ Code Improvements
---
title: 'V4.15.0-beta4(进行中)'
description: 'FastGPT V4.15.0-beta4 更新说明'
---
## 🚀 新增内容
1. 应用/知识库增加虚拟列表渲染。
2. 增加单独的 openapi 文档,区分 devapi 文档。
## ⚙️ 优化
1. 输入引导配置增加校验,避免错误配置了自定义词库地址。
## 🐛 修复
## 🛠️ 代码优化
{
"title": "4.15.x",
"description": "",
"pages": ["41503", "41502", "4150"]
"pages": ["41504", "41503", "41502", "4150"]
}
{
"title": "4.15.x",
"description": "",
"pages": ["41503", "41502", "4150"]
"pages": ["41504", "41503", "41502", "4150"]
}
......@@ -142,6 +142,7 @@ description: FastGPT Toc
- [/en/self-host/upgrading/4-14/4149](/en/self-host/upgrading/4-14/4149)
- [/en/self-host/upgrading/4-15/41502](/en/self-host/upgrading/4-15/41502)
- [/en/self-host/upgrading/4-15/41503](/en/self-host/upgrading/4-15/41503)
- [/en/self-host/upgrading/4-15/41504](/en/self-host/upgrading/4-15/41504)
- [/en/self-host/upgrading/outdated/40](/en/self-host/upgrading/outdated/40)
- [/en/self-host/upgrading/outdated/41](/en/self-host/upgrading/outdated/41)
- [/en/self-host/upgrading/outdated/4100](/en/self-host/upgrading/outdated/4100)
......
......@@ -145,6 +145,7 @@ description: FastGPT 文档目录
- [/self-host/upgrading/4-15/4150](/self-host/upgrading/4-15/4150)
- [/self-host/upgrading/4-15/41502](/self-host/upgrading/4-15/41502)
- [/self-host/upgrading/4-15/41503](/self-host/upgrading/4-15/41503)
- [/self-host/upgrading/4-15/41504](/self-host/upgrading/4-15/41504)
- [/self-host/upgrading/outdated/40](/self-host/upgrading/outdated/40)
- [/self-host/upgrading/outdated/41](/self-host/upgrading/outdated/41)
- [/self-host/upgrading/outdated/4100](/self-host/upgrading/outdated/4100)
......
......@@ -145,16 +145,16 @@
"content/guide/workspace/team/invitation_link.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/workspace/team/team_roles_permissions.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/workspace/team/team_roles_permissions.mdx": "2026-05-07T15:06:40+08:00",
"content/openapi/app.en.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/app.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/chat.en.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/chat.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/dataset.en.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/dataset.mdx": "2026-05-29T19:08:31+08:00",
"content/openapi/app.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/app.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/dataset.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/dataset.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/index.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/index.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/intro.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/intro.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/intro.en.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/intro.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/config/env.en.mdx": "2026-05-27T12:17:46+08:00",
......@@ -424,4 +424,4 @@
"content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00",
"content/toc.en.mdx": "2026-05-25T11:21:30+08:00",
"content/toc.mdx": "2026-05-25T11:21:30+08:00"
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@
"Click_to_delete_this_field": "Click to delete this field",
"Create_dataset": "Create a knowledge base",
"Custom_params": "input parameters",
"Delete_all": "Clear All Lexicon",
"Edit_tool": "Edit tool",
"Filed_is_deprecated": "This field is deprecated",
"HTTPTools_Create_Type": "Create Type",
......@@ -62,11 +63,15 @@
"chat_agent_intro": "Let AI call tools autonomously",
"chat_debug": "Chat Preview",
"chat_logs": "Logs",
"chat_input_guide_lexicon_is_empty": "Lexicon not configured yet",
"click_to_config": "Go to configuration",
"code_applied_successfully": "Code applied successfully",
"code_function_describe": "Describe your code function, enter \"/\" to select the variable",
"config_ai_model_params": "Click to configure AI model related properties",
"config_file_upload": "Click to Configure File Upload Rules",
"config_input_guide": "Set Up Input Guide",
"config_input_guide_lexicon": "Set Up Lexicon",
"config_input_guide_lexicon_title": "Set Up Lexicon",
"config_question_guide": "Configuration guess you want to ask",
"confirm_copy_app_tip": "The system will create an app with the same configuration for you, but permissions will not be copied. Please confirm!",
"confirm_del_app_tip": "Are you sure you want to delete this app?\nAll associated chat records will also be deleted.",
......@@ -91,7 +96,9 @@
"cron.every_month": "Run Monthly",
"cron.every_week": "Run Weekly",
"cron.interval": "Run at Intervals",
"csv_input_lexicon_tip": "Only CSV batch import is supported, click to download the template",
"current_package": "Current package: ",
"custom_input_guide_url": "Custom Lexicon URL",
"custom_plugin": "Custom Plugin",
"custom_plugin_associated_plugin_label": "Associated Plugin",
"custom_plugin_associated_plugin_placeholder": "Enter plugin name or appId to search",
......@@ -128,6 +135,7 @@
"dataset.create_dataset_tips": "For more advanced operations, please go to",
"dataset_create_failed": "Knowledge base creation failed",
"dataset_create_success": "The knowledge base was created successfully and files are being indexed in the background.",
"delete_all_input_guide_confirm": "Are you sure you want to clear the input guide lexicon?",
"dataset_empty_tips": "You don’t have a knowledge base yet, create one first.",
"dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.",
"dataset_select": "Optional knowledge base",
......@@ -160,7 +168,13 @@
"import_configs_failed": "Import configuration failed, please ensure the configuration is correct!",
"import_configs_success": "Import Successful",
"initial_form": "initial state",
"input_guide": "Input Guide",
"input_guide_config_required": "Configure a lexicon first, or enter a valid custom lexicon URL",
"input_guide_custom_url_invalid": "Enter a valid custom lexicon URL",
"input_guide_lexicon": "Lexicon",
"input_guide_tip": "You can set up some preset questions. When the user inputs a question, related questions from these presets will be suggested.",
"input_params_tips": "Tool input parameters will not be passed directly to the request URL; they can be referenced on the right side using \"/\".",
"insert_input_guide,_some_data_already_exists": "Duplicate data detected, automatically filtered, {{len}} items inserted",
"interval.12_hours": "Every 12 Hours",
"interval.2_hours": "Every 2 Hours",
"interval.3_hours": "Every 3 Hours",
......@@ -227,6 +241,7 @@
"logs_keys_user": "User",
"logs_keys_versionName": "Version Name",
"logs_message_total": "Total Messages",
"new_input_guide_lexicon": "New Lexicon",
"logs_new_user_count": "New users",
"logs_points": "Points Consumed",
"logs_points_description": "Points consumed by this application",
......
......@@ -4,6 +4,7 @@
"Click_to_delete_this_field": "点击删除该字段",
"Create_dataset": "创建知识库",
"Custom_params": "输入参数",
"Delete_all": "清空词库",
"Edit_tool": "编辑工具",
"Filed_is_deprecated": "该字段已弃用",
"HTTPTools_Create_Type": "创建方式",
......@@ -62,11 +63,15 @@
"chat_agent_intro": "让AI助手自主调用工具",
"chat_debug": "调试预览",
"chat_logs": "对话日志",
"chat_input_guide_lexicon_is_empty": "还没有配置词库",
"click_to_config": "去配置",
"code_applied_successfully": "代码应用成功",
"code_function_describe": "描述你的代码功能,输入“/”可选择变量",
"config_ai_model_params": "点击配置 AI 模型相关属性",
"config_file_upload": "点击配置文件上传规则",
"config_input_guide": "配置输入引导",
"config_input_guide_lexicon": "配置词库",
"config_input_guide_lexicon_title": "配置词库",
"config_question_guide": "配置猜你想问",
"confirm_copy_app_tip": "系统将为您创建一个相同配置应用,但权限不会进行复制,请确认!",
"confirm_del_app_tip": "确认删除该 Agent?删除 Agent 会将其关联的对话记录一并删除。",
......@@ -91,7 +96,9 @@
"cron.every_month": "每月执行",
"cron.every_week": "每周执行",
"cron.interval": "间隔执行",
"csv_input_lexicon_tip": "仅支持 CSV 批量导入,点击下载模板",
"current_package": "当前套餐:",
"custom_input_guide_url": "自定义词库地址",
"custom_plugin": "自定义插件",
"custom_plugin_associated_plugin_label": "关联插件",
"custom_plugin_associated_plugin_placeholder": "输入插件名或 appId 查找插件",
......@@ -128,6 +135,7 @@
"dataset.create_dataset_tips": "更多高级操作请前往",
"dataset_create_failed": "知识库创建失败",
"dataset_create_success": "知识库创建成功,正在后台索引文件",
"delete_all_input_guide_confirm": "确定要清空输入引导词库吗?",
"dataset_empty_tips": "你还没有知识库,先创建一个吧",
"dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。",
"dataset_select": "可选知识库",
......@@ -160,7 +168,13 @@
"import_configs_failed": "导入配置失败,请确保配置正常!",
"import_configs_success": "导入成功",
"initial_form": "初始状态",
"input_guide": "输入引导",
"input_guide_config_required": "请先配置词库,或填写有效的自定义词库地址",
"input_guide_custom_url_invalid": "请输入有效的自定义词库地址",
"input_guide_lexicon": "词库",
"input_guide_tip": "可以配置一些预设的问题。在用户输入问题时,会从这些预设问题中获取相关问题进行提示。",
"input_params_tips": "工具的输入参数,不会直接传递到请求地址,可在右侧通过 \"/\" 来引用变量",
"insert_input_guide,_some_data_already_exists": "有重复数据,已自动过滤,共插入 {{len}} 条数据",
"interval.12_hours": "每12小时",
"interval.2_hours": "每2小时",
"interval.3_hours": "每3小时",
......@@ -227,6 +241,7 @@
"logs_keys_user": "使用者",
"logs_keys_versionName": "版本名",
"logs_message_total": "消息总数",
"new_input_guide_lexicon": "新词库",
"logs_new_user_count": "新增用户",
"logs_points": "积分消耗",
"logs_points_description": "该应用消耗的积分",
......
......@@ -4,6 +4,7 @@
"Click_to_delete_this_field": "點擊刪除該字段",
"Create_dataset": "創建知識庫",
"Custom_params": "輸入參數",
"Delete_all": "清除所有詞彙",
"Filed_is_deprecated": "該字段已棄用",
"HTTPTools_Create_Type": "創建方式",
"HTTPTools_Create_Type_Tip": "選擇後不支持修改",
......@@ -60,11 +61,15 @@
"chat_agent_intro": "讓AI助手自主調用工具",
"chat_debug": "聊天預覽",
"chat_logs": "對話紀錄",
"chat_input_guide_lexicon_is_empty": "尚未設定詞彙庫",
"click_to_config": "去配置",
"code_applied_successfully": "代碼應用成功",
"code_function_describe": "描述你的代碼功能,輸入“/”可選擇變量",
"config_ai_model_params": "點選設定 AI 模型相關屬性",
"config_file_upload": "點選設定檔案上傳規則",
"config_input_guide": "設定輸入導引",
"config_input_guide_lexicon": "設定詞彙庫",
"config_input_guide_lexicon_title": "設定詞彙庫",
"config_question_guide": "設定猜你想問",
"confirm_copy_app_tip": "系統將為您建立一個相同設定的應用程式,但權限不會複製,請確認!",
"confirm_del_app_tip": "確認刪除該 Agent?\n刪除 Agent 會將其關聯的對話記錄一併刪除。",
......@@ -88,7 +93,9 @@
"cron.every_month": "每月執行",
"cron.every_week": "每週執行",
"cron.interval": "間隔執行",
"csv_input_lexicon_tip": "僅支援 CSV 批次匯入,點選下載範本",
"current_package": "當前套餐:",
"custom_input_guide_url": "自訂詞彙庫網址",
"custom_plugin": "自訂外掛",
"custom_plugin_associated_plugin_label": "關聯外掛",
"custom_plugin_associated_plugin_placeholder": "輸入外掛名稱或 appId 尋找外掛",
......@@ -124,6 +131,7 @@
"dataset.create_dataset_tips": "更多高級操作請前往",
"dataset_create_failed": "知識庫創建失敗",
"dataset_create_success": "知識庫創建成功,正在後台索引文件",
"delete_all_input_guide_confirm": "確定要清除輸入導引詞彙庫嗎?",
"dataset_empty_tips": "你還沒有知識庫,先創建一個吧",
"dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。",
"dataset_select": "可選知識庫",
......@@ -155,7 +163,13 @@
"import_configs_failed": "匯入設定失敗,請確認設定是否正常!",
"import_configs_success": "匯入成功",
"initial_form": "初始狀態",
"input_guide": "輸入導引",
"input_guide_config_required": "請先設定詞彙庫,或填寫有效的自訂詞彙庫網址",
"input_guide_custom_url_invalid": "請輸入有效的自訂詞彙庫網址",
"input_guide_lexicon": "詞彙庫",
"input_guide_tip": "您可以設定一些預設問題。當使用者輸入問題時,系統會從這些預設問題中提供相關問題作為提示。",
"input_params_tips": "工具的輸入參數,不會直接傳遞到請求地址,可在右側通過 \"/\" 來引用變數",
"insert_input_guide,_some_data_already_exists": "偵測到重複資料,已自動過濾,共插入 {{len}} 筆資料",
"interval.12_hours": "每 12 小時",
"interval.2_hours": "每 2 小時",
"interval.3_hours": "每 3 小時",
......@@ -222,6 +236,7 @@
"logs_keys_user": "使用者",
"logs_keys_versionName": "版本名稱",
"logs_message_total": "訊息總數",
"new_input_guide_lexicon": "新增詞彙庫",
"logs_new_user_count": "新增用戶",
"logs_points": "積分消耗",
"logs_points_description": "該應用消耗的積分",
......
{
"name": "@fastgpt/app",
"version": "4.15.0-2",
"version": "4.15.0-4",
"private": false,
"browserslist": [
"Chrome >= 80",
......
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Button, Flex, Switch } from '@chakra-ui/react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import type { ChatInputGuideConfigType } from '@fastgpt/global/core/app/type';
import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import MyInput from '@/components/MyInput';
import { getDocPath } from '@/web/common/system/doc';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useInputGuideConfigForm } from './useInputGuideConfigForm';
type InputGuideConfigModalProps = {
isOpen: boolean;
value: ChatInputGuideConfigType;
total: number;
onClose: () => void;
onChange: (e: ChatInputGuideConfigType) => void;
onOpenLexiconConfig: () => void;
};
const InputGuideConfigModal = ({
isOpen,
value,
total,
onClose,
onChange,
onOpenLexiconConfig
}: InputGuideConfigModalProps) => {
const { t } = useTranslation();
const { customUrlRegister, draftOpen, handleConfirmSubmit, setDraftOpen } =
useInputGuideConfigForm({
isOpen,
value,
total,
onClose,
onChange
});
return (
<MyModal
title={t('app:input_guide')}
isOpen={isOpen}
onClose={onClose}
size={'sm'}
isCentered
closeOnOverlayClick={false}
showCloseButton={false}
footer={
<>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:Cancel')}
</Button>
<Button onClick={handleConfirmSubmit}>{t('common:Confirm')}</Button>
</>
}
>
<Flex flexDirection={'column'} gap={8}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<FormLabel>{t('common:is_open')}</FormLabel>
<Switch
isChecked={draftOpen}
onChange={(e) => {
setDraftOpen(e.target.checked);
}}
/>
</Flex>
{draftOpen && (
<>
<Flex alignItems={'center'}>
<FormLabel>{t('app:input_guide_lexicon')}</FormLabel>
<Box fontSize={'xs'} px={2} bg={'myGray.100'} ml={1} rounded={'full'}>
{total}
</Box>
<Box flex={'1 0 0'} />
<Button
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon boxSize={'4'} name={'common/settingLight'} />}
onClick={onOpenLexiconConfig}
>
{t('app:config_input_guide_lexicon')}
</Button>
</Flex>
<Box>
<Flex alignItems={'center'}>
<FormLabel>{t('app:custom_input_guide_url')}</FormLabel>
<Flex
onClick={() => window.open(getDocPath('/guide/build/general/chat_input_guide'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}
>
<MyIcon name={'book'} w={'17px'} ml={4} mr={1} color={'myGray.600'} />
{t('common:Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>
<MyInput mt={2} bg={'myGray.50'} {...customUrlRegister} />
</Box>
</>
)}
</Flex>
</MyModal>
);
};
export default React.memo(InputGuideConfigModal);
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Button, Checkbox, Flex } from '@chakra-ui/react';
import React, { useCallback, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import MyModal from '@fastgpt/web/components/v2/common/MyModal';
import MyInput from '@/components/MyInput';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { fileDownload } from '@/web/common/file/utils';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import HighlightText from '@fastgpt/web/components/common/String/HighlightText';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useInputGuideLexicon } from './useInputGuideLexicon';
import type { ChatInputGuideListResponseType } from '@fastgpt/global/openapi/core/chat/inputGuide/api';
const csvTemplate = `"第一列内容"
"只会将第一列内容导入,其余列会被忽略"
"AIGC发展分为几个阶段?"`;
type LexiconItem = ChatInputGuideListResponseType['list'][number];
const downloadCsvTemplate = () => {
fileDownload({
text: csvTemplate,
type: 'text/csv;charset=utf-8',
filename: 'questionGuide_template.csv'
});
};
const CommitInput = ({
defaultValue = '',
placeholder,
onCommit
}: {
defaultValue?: string;
placeholder?: string;
onCommit: (value: string) => void;
}) => {
const committedRef = useRef(false);
const commit = useCallback(
(value: string) => {
if (committedRef.current) return;
committedRef.current = true;
onCommit(value.trim());
},
[onCommit]
);
return (
<MyInput
autoFocus
defaultValue={defaultValue}
placeholder={placeholder}
rightIcon={<MyIcon name={'save'} boxSize={4} cursor={'pointer'} />}
onBlur={(e) => commit(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
commit(e.currentTarget.value);
e.currentTarget.blur();
}
}}
/>
);
};
const LexiconToolbar = ({
onImport,
onSearch
}: {
onImport: () => void;
onSearch: (searchKey: string) => void;
}) => {
const { t } = useTranslation();
return (
<Flex gap={4} px={8} py={4} mb={4} alignItems={'center'} borderBottom={'base'}>
<Box flex={1}>
<MyInput
leftIcon={<MyIcon name={'common/searchLight'} boxSize={4} color={'myGray.500'} />}
bg={'myGray.50'}
w={'full'}
h={9}
placeholder={t('common:Search')}
onChange={(e) => onSearch(e.target.value)}
/>
</Box>
<Button
onClick={onImport}
variant={'whiteBase'}
size={'sm'}
leftIcon={<MyIcon name={'common/importLight'} boxSize={4} />}
>
{t('common:Import')}
</Button>
<Box cursor={'pointer'} onClick={downloadCsvTemplate}>
<QuestionTip ml={-2} label={t('app:csv_input_lexicon_tip')} />
</Box>
</Flex>
);
};
const LexiconActions = ({
hasSelectedRows,
onCreate,
onDeleteAll,
onDeleteSelected
}: {
hasSelectedRows: boolean;
onCreate: () => void;
onDeleteAll: () => void;
onDeleteSelected: () => void;
}) => {
const { t } = useTranslation();
return (
<Flex mb={1} justifyContent={'space-between'}>
<Box flex={1} />
<Flex gap={4}>
<Button
variant={'whiteBase'}
display={hasSelectedRows ? 'flex' : 'none'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
onClick={onDeleteSelected}
>
{t('common:Delete')}
</Button>
<PopoverConfirm
Trigger={
<Button
variant={'whiteBase'}
display={hasSelectedRows ? 'none' : 'flex'}
size={'sm'}
leftIcon={<MyIcon name={'delete'} boxSize={4} />}
>
{t('app:Delete_all')}
</Button>
}
type="delete"
content={t('app:delete_all_input_guide_confirm')}
onConfirm={onDeleteAll}
/>
<Button
display={hasSelectedRows ? 'none' : 'flex'}
onClick={onCreate}
size={'sm'}
leftIcon={<MyIcon name={'common/addLight'} boxSize={4} />}
>
{t('common:Add')}
</Button>
</Flex>
</Flex>
);
};
const LexiconRow = ({
hasSelectedRows,
isEditing,
item,
searchKey,
selected,
onDelete,
onEdit,
onSelect,
onUpdate
}: {
hasSelectedRows: boolean;
isEditing: boolean;
item: LexiconItem;
searchKey: string;
selected: boolean;
onDelete: (dataId: string) => void;
onEdit: (dataId: string) => void;
onSelect: (dataId: string, checked: boolean) => void;
onUpdate: (data: { text: string; dataId: string }) => void;
}) => {
return (
<Flex
alignItems={'center'}
h={10}
mt={2}
_hover={{
'& .icon-list': {
display: 'flex'
}
}}
>
<Checkbox
mr={2}
isChecked={selected}
icon={<MyIcon name={'common/check'} w={'12px'} />}
onChange={(e) => onSelect(item._id, e.target.checked)}
/>
{isEditing ? (
<Box h={'full'} flex={'1 0 0'}>
<CommitInput
defaultValue={item.text}
onCommit={(text) => {
onUpdate({
text,
dataId: item._id
});
}}
/>
</Box>
) : (
<Flex
h={'40px'}
w={0}
flex={'1 0 0'}
rounded={'md'}
px={4}
bg={'myGray.50'}
alignItems={'center'}
border={'base'}
_hover={{ borderColor: 'primary.300' }}
>
<Box className="textEllipsis" w={0} flex={'1 0 0'}>
<HighlightText rawText={item.text} matchText={searchKey} />
</Box>
{!hasSelectedRows && (
<Box className="icon-list" display={'none'}>
<MyIcon
name={'edit'}
boxSize={4}
mr={2}
color={'myGray.600'}
cursor={'pointer'}
onClick={() => onEdit(item._id)}
/>
<MyIcon
name={'delete'}
boxSize={4}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
onClick={() => onDelete(item._id)}
/>
</Box>
)}
</Flex>
)}
</Flex>
);
};
const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => void }) => {
const { t } = useTranslation();
const {
SelectFile,
ScrollList,
clearSelectedRows,
createNewData,
editDataId,
isCreatingNewData,
isLoading,
onDeleteAllData,
onDeleteData,
onOpenSelectFile,
onSelectFile,
onSelectRow,
onStartCreateData,
onStartEditData,
onUpdateData,
scrollDataList,
searchKey,
selectedRows,
setSearchKey
} = useInputGuideLexicon({ appId });
const hasSelectedRows = selectedRows.length > 0;
const handleDeleteSelected = useCallback(() => {
onDeleteData(selectedRows);
clearSelectedRows();
}, [clearSelectedRows, onDeleteData, selectedRows]);
const handleDeleteAll = useCallback(() => {
onDeleteAllData();
clearSelectedRows();
}, [clearSelectedRows, onDeleteAllData]);
return (
<MyModal
title={t('app:config_input_guide_lexicon_title')}
isOpen={true}
onClose={onClose}
isLoading={isLoading}
h={'600px'}
size={'md'}
bodyStyles={{
px: 0,
pt: 4,
pb: 0
}}
>
<LexiconToolbar onImport={onOpenSelectFile} onSearch={setSearchKey} />
<Box px={8}>
<LexiconActions
hasSelectedRows={hasSelectedRows}
onCreate={onStartCreateData}
onDeleteAll={handleDeleteAll}
onDeleteSelected={handleDeleteSelected}
/>
{isCreatingNewData && (
<Box mt={5} ml={scrollDataList.length > 0 ? 7 : 0}>
<CommitInput
placeholder={t('app:new_input_guide_lexicon')}
onCommit={(text) => createNewData([text])}
/>
</Box>
)}
</Box>
<ScrollList
px={8}
flex={'1 0 0'}
fontSize={'sm'}
EmptyChildren={<EmptyTip text={t('app:chat_input_guide_lexicon_is_empty')} />}
>
{scrollDataList.map(({ data: item }) => (
<LexiconRow
key={item._id}
hasSelectedRows={hasSelectedRows}
isEditing={editDataId === item._id}
item={item}
searchKey={searchKey}
selected={selectedRows.includes(item._id)}
onDelete={(dataId) => onDeleteData([dataId])}
onEdit={onStartEditData}
onSelect={onSelectRow}
onUpdate={onUpdateData}
/>
))}
</ScrollList>
<SelectFile onSelect={onSelectFile} />
</MyModal>
);
};
export default React.memo(LexiconConfigModal);
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import type { ChatInputGuideConfigType } from '@fastgpt/global/core/app/type';
import { getCountChatInputGuideTotal } from '@/web/core/chat/inputGuide/api';
import { useQuery } from '@tanstack/react-query';
import { defaultChatInputGuideConfig } from '@fastgpt/global/core/app/constants';
import ChatFunctionTip from '../Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import InputGuideConfigModal from './InputGuideConfigModal';
import dynamic from 'next/dynamic';
const LexiconConfigModal = dynamic(() => import('./LexiconConfigModal'), {
ssr: false
});
type InputGuideConfigProps = {
appId: string;
value?: ChatInputGuideConfigType;
onChange: (e: ChatInputGuideConfigType) => void;
};
const InputGuideConfig = ({
appId,
value = defaultChatInputGuideConfig,
onChange
}: InputGuideConfigProps) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const {
isOpen: isOpenLexiconConfig,
onOpen: onOpenLexiconConfig,
onClose: onCloseLexiconConfig
} = useDisclosure();
const { data } = useQuery(
['chatInputGuideTotal', appId, isOpenLexiconConfig],
() => getCountChatInputGuideTotal({ appId }),
{
enabled: !!appId
}
);
const total = data?.total ?? 0;
const statusText = value.open
? t('common:core.app.whisper.Open')
: t('common:core.app.whisper.Close');
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Flex alignItems={'center'}>
<FormLabel>{t('app:input_guide')}</FormLabel>
<ChatFunctionTip type={'inputGuide'} />
</Flex>
<Box flex={1} />
<MyTooltip label={t('app:config_input_guide')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{statusText}
</Button>
</MyTooltip>
<InputGuideConfigModal
isOpen={isOpen}
value={value}
total={total}
onClose={onClose}
onChange={onChange}
onOpenLexiconConfig={onOpenLexiconConfig}
/>
{isOpenLexiconConfig && <LexiconConfigModal appId={appId} onClose={onCloseLexiconConfig} />}
</Flex>
);
};
export default React.memo(InputGuideConfig);
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { useForm, useWatch } from 'react-hook-form';
import type { ChatInputGuideConfigType } from '@fastgpt/global/core/app/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
type UseInputGuideConfigFormProps = {
isOpen: boolean;
value: ChatInputGuideConfigType;
total: number;
onClose: () => void;
onChange: (e: ChatInputGuideConfigType) => void;
};
const isHttpUrl = (value: string) => {
try {
const url = new URL(value);
return ['http:', 'https:'].includes(url.protocol);
} catch {
return false;
}
};
/**
* 管理输入引导主配置弹窗的草稿态。
* 弹窗内修改只写入 react-hook-form,确认时才校验并提交,避免开关或输入框变更立即影响应用配置。
*/
export const useInputGuideConfigForm = ({
isOpen,
value,
total,
onClose,
onChange
}: UseInputGuideConfigFormProps) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, reset, control, setValue, handleSubmit } = useForm<ChatInputGuideConfigType>({
defaultValues: value
});
const draftOpen = useWatch({ control, name: 'open' });
const draftCustomUrl = useWatch({ control, name: 'customUrl' }) || '';
const trimmedCustomUrl = draftCustomUrl.trim();
// 每次重新打开弹窗时从外部配置恢复草稿,关闭后丢弃未确认的修改。
useEffect(() => {
if (!isOpen) return;
reset(value);
}, [isOpen, reset, value]);
const handleConfirm = useCallback(
(data: ChatInputGuideConfigType) => {
if (data.open) {
if (trimmedCustomUrl && !isHttpUrl(trimmedCustomUrl)) {
toast({
status: 'warning',
title: t('app:input_guide_custom_url_invalid')
});
return;
}
// 这里不是自定义地址字段自身错误,用户也可以通过配置词库满足条件,因此只 toast 提醒。
if (!trimmedCustomUrl && total <= 0) {
toast({
status: 'warning',
title: t('app:input_guide_config_required')
});
return;
}
}
onChange({
...data,
customUrl: trimmedCustomUrl
});
onClose();
},
[onChange, onClose, t, toast, total, trimmedCustomUrl]
);
const handleConfirmSubmit = useMemo(
() => handleSubmit(handleConfirm),
[handleConfirm, handleSubmit]
);
const setDraftOpen = useCallback(
(open: boolean) => {
setValue('open', open);
},
[setValue]
);
const customUrlRegister = register('customUrl');
return {
customUrlRegister,
draftOpen: !!draftOpen,
handleConfirmSubmit,
setDraftOpen
};
};
import { useCallback, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { readCsvRawText } from '@fastgpt/web/common/file/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import {
delAllChatInputGuide,
delChatInputGuide,
getChatInputGuideList,
postChatInputGuides,
putChatInputGuide
} from '@/web/core/chat/inputGuide/api';
const normalizeGuideTextList = (textList: string[]) =>
textList.map((text) => text.trim()).filter(Boolean);
/**
* 封装输入引导词库弹窗的数据加载和写操作。
* 列表增删改采用本地乐观更新,批量导入统一在这里去空白,避免弹窗组件关心 CSV 或 API 细节。
*/
export const useInputGuideLexicon = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { File: SelectFile, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.csv'
});
const [newData, setNewData] = useState<string>();
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [editDataId, setEditDataId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
const {
scrollDataList,
setData,
ScrollList,
isLoading: isRequesting,
fetchData,
scroll2Top
} = useVirtualScrollPagination(getChatInputGuideList, {
refreshDeps: [searchKey],
itemHeight: 48,
overscan: 20,
pageSize: 20,
defaultParams: {
appId,
searchKey
}
});
const { run: createNewData, loading: isCreating } = useRequest(
async (textList: string[]) => {
const normalizedTextList = normalizeGuideTextList(textList);
if (normalizedTextList.length === 0) {
return Promise.resolve();
}
scroll2Top();
return postChatInputGuides({
appId,
textList: normalizedTextList
}).then((res) => {
if (res.insertLength < normalizedTextList.length) {
toast({
status: 'warning',
title: t('app:insert_input_guide,_some_data_already_exists', { len: res.insertLength })
});
} else {
toast({
status: 'success',
title: t('common:add_success')
});
}
fetchData({ init: true });
});
},
{
onSuccess() {
setNewData(undefined);
},
errorToast: t('common:create_failed')
}
);
const onUpdateData = useCallback(
({ text, dataId }: { text: string; dataId: string }) => {
const trimmedText = text.trim();
// 空文本不落库,也不把本地列表乐观更新成空值。
if (!trimmedText) {
setEditDataId(undefined);
return;
}
setData((state) =>
state.map((item) => {
if (item._id === dataId) {
return {
...item,
text: trimmedText
};
}
return item;
})
);
void putChatInputGuide({
appId,
text: trimmedText,
dataId
});
setEditDataId(undefined);
},
[appId, setData]
);
const onDeleteData = useCallback(
(dataIdList: string[]) => {
if (dataIdList.length === 0) return;
setData((state) => state.filter((item) => !dataIdList.includes(item._id)));
void delChatInputGuide({
appId,
dataIdList
});
},
[appId, setData]
);
const onDeleteAllData = useCallback(() => {
setData([]);
void delAllChatInputGuide({
appId
});
}, [appId, setData]);
const onStartCreateData = useCallback(() => {
setNewData('');
}, []);
const onStartEditData = useCallback((dataId: string) => {
setEditDataId(dataId);
}, []);
const clearSelectedRows = useCallback(() => {
setSelectedRows([]);
}, []);
const onSelectRow = useCallback((dataId: string, checked: boolean) => {
setSelectedRows((state) => {
if (checked) {
return state.includes(dataId) ? state : [...state, dataId];
}
return state.filter((id) => id !== dataId);
});
}, []);
const onSelectFile = useCallback(
async (files: File[]) => {
const file = files?.[0];
if (file) {
const list = await readCsvRawText({ file });
// CSV 只取第一列作为引导词,后续列按模板说明忽略。
const textList = list.map((item) => item[0] || '');
createNewData(textList);
}
},
[createNewData]
);
return {
SelectFile,
ScrollList,
clearSelectedRows,
createNewData,
editDataId,
isCreatingNewData: newData !== undefined,
isLoading: isRequesting || isCreating,
onDeleteAllData,
onDeleteData,
onOpenSelectFile,
onSelectRow,
onSelectFile,
onStartCreateData,
onStartEditData,
onUpdateData,
scrollDataList,
searchKey,
selectedRows,
setSearchKey
};
};
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Box, Button, Flex, ModalBody, useDisclosure, Image, HStack } from '@chakra-ui/react';
import { Box, Button, Flex, ModalBody, useDisclosure, HStack } from '@chakra-ui/react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'next-i18next';
import { TTSTypeEnum } from '@/web/core/app/constants';
......@@ -18,6 +18,17 @@ import { AppContext } from '@/pageComponents/app/detail/context';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MultipleRowSelect from '@fastgpt/web/components/common/MySelect/MultipleRowSelect';
type TTSSelectorItemType = {
alias: string;
avatar?: string;
label: string | React.ReactNode;
value: string;
children: {
label: string;
value: string;
}[];
};
const TTSSelect = ({
value = defaultTTSConfig,
onChange
......@@ -32,16 +43,29 @@ const TTSSelect = ({
const appId = useContextSelector(AppContext, (v) => v.appId);
const selectorList = useMemo(
() => [
{ label: t('app:tts_close'), value: TTSTypeEnum.none, children: [] },
{ label: t('app:tts_browser'), value: TTSTypeEnum.web, children: [] },
(): TTSSelectorItemType[] => [
{
alias: t('app:tts_close'),
label: t('app:tts_close'),
value: TTSTypeEnum.none,
children: []
},
{
alias: t('app:tts_browser'),
label: t('app:tts_browser'),
value: TTSTypeEnum.web,
children: []
},
...ttsModelList.map((model) => {
const providerData = getModelProvider(model.provider, i18n.language);
const modelName = t(model.name as any);
return {
alias: modelName,
avatar: providerData.avatar,
label: (
<HStack>
<HStack minW={0} maxW={'100%'} spacing={2}>
<Avatar borderRadius={'0'} w={'1.25rem'} src={providerData.avatar} />
<Box>{t(model.name as any)}</Box>
<Box noOfLines={1}>{modelName}</Box>
</HStack>
),
value: model.model,
......@@ -53,7 +77,7 @@ const TTSSelect = ({
};
})
],
[ttsModelList, t]
[getModelProvider, i18n.language, t, ttsModelList]
);
const formatValue = useMemo(() => {
......@@ -69,17 +93,21 @@ const TTSSelect = ({
const formLabel = useMemo(() => {
const provider = selectorList.find((item) => item.value === formatValue[0]) || selectorList[0];
const voice = provider.children.find((item) => item.value === formatValue[1]);
const providerText =
provider.alias ||
(typeof provider.label === 'string' ? provider.label : String(provider.value));
const selectedText = voice ? `${providerText} / ${voice.label}` : providerText;
return (
<Box>
{voice ? (
<Flex maxW={['200px', '250px']} overflow={'hidden'} alignItems={'center'}>
<Box>{provider.label}</Box>
<Box>/</Box>
<Box>{voice.label}</Box>
</Flex>
) : (
provider.label
)}
<Box minW={0} maxW={'100%'}>
<Flex maxW={['200px', '250px']} minW={0} alignItems={'center'}>
{provider.avatar && (
<Avatar borderRadius={'0'} w={'1.25rem'} flexShrink={0} mr={2} src={provider.avatar} />
)}
<Box minW={0} noOfLines={1}>
{selectedText}
</Box>
</Flex>
</Box>
);
}, [formatValue, selectorList]);
......@@ -103,7 +131,7 @@ const TTSSelect = ({
});
}
},
[ttsModelList, onChange, value]
[onChange, value]
);
const onCloseTTSModal = useCallback(() => {
......
......@@ -2,7 +2,7 @@ import { Box, Flex } from '@chakra-ui/react';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import React, { useRef } from 'react';
import React, { useMemo } from 'react';
enum FnTypeEnum {
inputGuide = 'inputGuide',
......@@ -19,63 +19,66 @@ enum FnTypeEnum {
const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
const { t } = useTranslation();
const map = useRef({
[FnTypeEnum.inputGuide]: {
icon: '/imgs/app/inputGuide-icon.svg',
title: t('chat:input_guide'),
desc: t('chat:input_guide_tip'),
imgUrl: '/imgs/app/inputGuide.svg'
},
[FnTypeEnum.nextQuestion]: {
icon: '/imgs/app/nextQuestion-icon.svg',
title: t('common:core.app.Question Guide'),
desc: t('app:question_guide_tip'),
imgUrl: '/imgs/app/nextQuestion.svg'
},
[FnTypeEnum.tts]: {
icon: '/imgs/app/tts-icon.svg',
title: t('common:core.app.TTS'),
desc: t('common:core.app.TTS Tip'),
imgUrl: '/imgs/app/tts.svg'
},
[FnTypeEnum.variable]: {
icon: '/imgs/app/variable-icon.svg',
title: t('common:core.module.Variable'),
desc: t('common:core.app.tip.variableTip'),
imgUrl: '/imgs/app/variable.svg'
},
[FnTypeEnum.welcome]: {
icon: '/imgs/app/welcome-icon.svg',
title: t('common:core.app.Welcome Text'),
desc: t('common:core.app.tip.welcomeTextTip'),
imgUrl: '/imgs/app/welcome.svg'
},
[FnTypeEnum.file]: {
icon: '/imgs/app/fileinput.svg',
title: t('app:file_upload'),
desc: t('app:file_upload_tip'),
imgUrl: '/imgs/app/fileUploadPlaceholder.png'
},
[FnTypeEnum.visionModel]: {
icon: '/imgs/app/question.svg',
title: t('app:vision_model_title'),
desc: t('app:open_vision_function_tip'),
imgUrl: '/imgs/app/visionModel.svg'
},
[FnTypeEnum.instruction]: {
icon: '/imgs/app/help.svg',
title: t('workflow:plugin.Instructions'),
desc: t('workflow:plugin.Instruction_Tip'),
imgUrl: '/imgs/app/instruction.svg'
},
[FnTypeEnum.autoExec]: {
icon: '/imgs/app/autoExec-icon.svg',
title: t('common:core.app.Auto execute'),
desc: t('app:auto_execute_tip'),
imgUrl: '/imgs/app/autoExec.svg'
}
});
const data = map.current[type];
const map = useMemo(
() => ({
[FnTypeEnum.inputGuide]: {
icon: '/imgs/app/inputGuide-icon.svg',
title: t('app:input_guide'),
desc: t('app:input_guide_tip'),
imgUrl: '/imgs/app/inputGuide.svg'
},
[FnTypeEnum.nextQuestion]: {
icon: '/imgs/app/nextQuestion-icon.svg',
title: t('common:core.app.Question Guide'),
desc: t('app:question_guide_tip'),
imgUrl: '/imgs/app/nextQuestion.svg'
},
[FnTypeEnum.tts]: {
icon: '/imgs/app/tts-icon.svg',
title: t('common:core.app.TTS'),
desc: t('common:core.app.TTS Tip'),
imgUrl: '/imgs/app/tts.svg'
},
[FnTypeEnum.variable]: {
icon: '/imgs/app/variable-icon.svg',
title: t('common:core.module.Variable'),
desc: t('common:core.app.tip.variableTip'),
imgUrl: '/imgs/app/variable.svg'
},
[FnTypeEnum.welcome]: {
icon: '/imgs/app/welcome-icon.svg',
title: t('common:core.app.Welcome Text'),
desc: t('common:core.app.tip.welcomeTextTip'),
imgUrl: '/imgs/app/welcome.svg'
},
[FnTypeEnum.file]: {
icon: '/imgs/app/fileinput.svg',
title: t('app:file_upload'),
desc: t('app:file_upload_tip'),
imgUrl: '/imgs/app/fileUploadPlaceholder.png'
},
[FnTypeEnum.visionModel]: {
icon: '/imgs/app/question.svg',
title: t('app:vision_model_title'),
desc: t('app:open_vision_function_tip'),
imgUrl: '/imgs/app/visionModel.svg'
},
[FnTypeEnum.instruction]: {
icon: '/imgs/app/help.svg',
title: t('workflow:plugin.Instructions'),
desc: t('workflow:plugin.Instruction_Tip'),
imgUrl: '/imgs/app/instruction.svg'
},
[FnTypeEnum.autoExec]: {
icon: '/imgs/app/autoExec-icon.svg',
title: t('common:core.app.Auto execute'),
desc: t('app:auto_execute_tip'),
imgUrl: '/imgs/app/autoExec.svg'
}
}),
[t]
);
const data = map[type];
return (
<QuestionTip
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment