index.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. import * as React from 'react';
  2. import { useLocation } from 'react-router-dom';
  3. import { observer } from 'mobx-react';
  4. import './style.less';
  5. import {
  6. Button, Input, Form, Divider, Splitter, Select, InputNumber, InputNumberProps,
  7. Radio, Switch, Row, Col, Slider, Space, RadioChangeEvent,
  8. Spin, message
  9. } from 'antd';
  10. import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
  11. import { apis } from '@/apis';
  12. import router from '@/router';
  13. const { TextArea } = Input;
  14. const FormItem = Form.Item;
  15. const { Option } = Select;
  16. const MAX_COUNT = 10;
  17. const Index = 1;
  18. const QuestionAnswerInfo: React.FC = () => {
  19. const [form] = Form.useForm();
  20. // top_p
  21. const [topPValue, setTopPValue] = React.useState(0.1);
  22. const TopPDecimalStep: React.FC = () => {
  23. const onChange: InputNumberProps['onChange'] = (value) => {
  24. if (Number.isNaN(value)) {
  25. return;
  26. }
  27. setTopPValue(value as number);
  28. };
  29. return (
  30. <Row>
  31. <Col span={12}>
  32. <Slider
  33. min={0}
  34. max={1}
  35. onChange={onChange}
  36. value={typeof topPValue === 'number' ? topPValue : 0}
  37. step={0.1}
  38. />
  39. </Col>
  40. <Col span={4}>
  41. <InputNumber
  42. min={0}
  43. max={1}
  44. style={{ margin: '0 16px', width: '100px' }}
  45. step={0.01}
  46. value={topPValue}
  47. onChange={onChange}
  48. />
  49. </Col>
  50. </Row>
  51. );
  52. };
  53. const [tempValue, setTempValue] = React.useState(0.01);
  54. // temperature
  55. const TempDecimalStep: React.FC = () => {
  56. const onChange: InputNumberProps['onChange'] = (value) => {
  57. if (Number.isNaN(value)) {
  58. return;
  59. }
  60. setTempValue(value as number);
  61. };
  62. return (
  63. <Row>
  64. <Col span={12}>
  65. <Slider
  66. min={0}
  67. max={1}
  68. onChange={onChange}
  69. value={typeof tempValue === 'number' ? tempValue : 0}
  70. step={0.01}
  71. />
  72. </Col>
  73. <Col span={4}>
  74. <InputNumber
  75. min={0}
  76. max={1}
  77. style={{ margin: '0 16px', width: '100px' }}
  78. step={0.01}
  79. value={tempValue}
  80. onChange={onChange}
  81. />
  82. </Col>
  83. </Row>
  84. );
  85. };
  86. type ModelList = {
  87. label: string,
  88. value: string,
  89. }[];
  90. type KnowledgeList = {
  91. label: string,
  92. value: string,
  93. }[];
  94. const [step, setStep] = React.useState(1);
  95. const [pageLoading, setPageLoading] = React.useState(false);
  96. const [modelList, setModelList] = React.useState<ModelList>([]);
  97. const [knowledgeList, setKnowledgeList] = React.useState<KnowledgeList>([]);
  98. const [isVisible, setIsVisible] = React.useState(false);
  99. const [isVisibleCus, setIsVisibleCus] = React.useState(false);
  100. const [isVisibleSlice, setIsVisibleSlice] = React.useState(false);
  101. const [isVisibleRerank, setIsVisibleRerank] = React.useState(false);
  102. const [name, setName] = React.useState('');
  103. const style: React.CSSProperties = {
  104. display: 'flex',
  105. flexDirection: 'column',
  106. gap: 8,
  107. width: 300,
  108. };
  109. const location = useLocation();
  110. const init = async (id: string) => {
  111. await Promise.all([
  112. api.fetchKnowlegde(),
  113. api.fetchModelList(),
  114. ])
  115. if (id) {
  116. await api.fetchDetail(id);
  117. }
  118. }
  119. React.useEffect(() => {
  120. const id = location?.state?.id;
  121. init(id)
  122. }, []);
  123. // 定义一个状态来存储输入框数组
  124. const [inputs, setInputs] = React.useState([{ id: 1, value: '' }]);
  125. // 添加新输入框的函数
  126. const addInput = () => {
  127. const newId = inputs.length + 1; // 生成新的唯一ID
  128. setInputs([...inputs, { id: newId, value: '' }]);
  129. };
  130. const delInput = () => {
  131. const newId = inputs.length - 1; // 生成新的唯一ID
  132. setInputs(inputs.slice(0, inputs.length - 1));
  133. };
  134. // 处理输入变更的函数
  135. const handleChange = (id: number, value: string) => {
  136. setInputs(inputs.map(input => (input.id === id ? { ...input, value } : input)));
  137. };
  138. // const onChange: InputNumberProps['onChange'] = (value) => {
  139. // console.log('changed', value);
  140. // };
  141. // const onChangeShow = (checked: boolean) => {
  142. // console.log(`switch to ${checked}`);
  143. // };
  144. // const onChangeModel = (checked: boolean) => {
  145. // setIsVisibleRerank(!isVisibleRerank);
  146. // };
  147. // const onChangeCount = (value: string) => {
  148. // if (value === 'fixed') {
  149. // setIsVisibleSlice(!isVisibleSlice);
  150. // } else {
  151. // setIsVisibleSlice(false);
  152. // }
  153. // };
  154. // 召回方式
  155. const onChangeRecallMethod = (e: RadioChangeEvent) => {
  156. };
  157. // 获取应用详情
  158. const api = {
  159. fetchDetail: async (app_id: string) => {
  160. setPageLoading(true);
  161. try {
  162. const res = await apis.fetchApplicationDetail(app_id)
  163. const sd = res.data.questionlist.map((item: any, index: number) => {
  164. return {
  165. "id": index + 1,
  166. "value": item.question,
  167. }
  168. });
  169. const info = res.data.detail;
  170. if (info.paramDesc === 'custom') {
  171. setIsVisibleCus(!isVisibleCus); //自定义回答风格
  172. }
  173. // if (jsonObj.rerank_status === 1) {
  174. // setIsVisibleRerank(!isVisibleRerank) //模型
  175. // }
  176. // //召回切片数量
  177. // if (jsonObj.slice_config_type === 'fixed') {
  178. // setIsVisibleSlice(!isVisibleSlice);
  179. // }else {
  180. // setIsVisibleSlice(false);
  181. // }
  182. setTopPValue(info.topP as number);
  183. setTempValue(info.temperature as number);
  184. setName(info.name);
  185. form.setFieldsValue({
  186. id: info.id,
  187. name: info.name, //应用名称
  188. desc: info.desc, //应用描述
  189. prompt: info.prompt, //应用提示语
  190. top_p: info.top_p, //topP
  191. temperature: info.temperature, //温度
  192. knowledge_ids: info.knowledge_ids,
  193. slice_count: info.slice_count,
  194. model: info.model,
  195. icon_color: info.icon_color,
  196. icon_type: info.icon_type,
  197. questionList: sd, //问题列表
  198. // max_token: info.max_token, //应用最大token
  199. // updateDate: info.updateDate, // 更新时间
  200. // param_desc: info.paramDesc, //回答风格
  201. // questionList: sd, //问题列表
  202. // knowledge_ids: jsonObj.knowledge_ids, //知识库id
  203. // model: jsonObj.model, //模型名称
  204. // slice_config_type: jsonObj.slice_config_type, //切片类型
  205. // recall_method: jsonObj.recall_method, //召回方式
  206. // slice_count: jsonObj.slice_count, //切片数量
  207. // rerank_model_name: jsonObj.rerank_model_name, //模型名称
  208. // recall_slice_splicing_method: jsonObj.recall_slice_splicing_method,
  209. // rerank_status: jsonObj.rerank_status, //开启rerank
  210. // show_recall_result: jsonObj.show_recall_result, //是否展示召回结果
  211. // recall_index_type_list: jsonObj.recall_index_type_list, //知识库id
  212. // rerank_index_type_list: jsonObj.rerank_index_type_list, //知识库id
  213. })
  214. console.log(sd, 'sd');
  215. setInputs(sd);
  216. } catch (error) {
  217. console.error(error);
  218. } finally {
  219. setPageLoading(false);
  220. }
  221. },
  222. //获取知识库列表
  223. fetchKnowlegde: async () => {
  224. try {
  225. const res = await apis.fetchKnowledgeList();
  226. const list = res.data.map((item: any) => {
  227. return {
  228. label: item.name,
  229. value: item.knowledgeId,
  230. }
  231. });
  232. setKnowledgeList(list);
  233. } catch (error: any) {
  234. console.error(error);
  235. }
  236. },
  237. // 获取模型列表
  238. fetchModelList: async () => {
  239. try {
  240. const res = await apis.fetchModelList();
  241. const list = res.data.data.map((item: any) => {
  242. return {
  243. label: item.modelName,
  244. value: item.modelCode,
  245. }
  246. });
  247. setModelList(list);
  248. } catch (error: any) {
  249. console.error(error);
  250. }
  251. },
  252. }
  253. const handleRedioClick = (value: string) => {
  254. setIsVisibleCus(false);
  255. }
  256. return (
  257. <div className='questionAnswerInfo'>
  258. <Spin spinning={pageLoading}>
  259. <Form
  260. style={{ paddingTop: '20px' }}
  261. form={form}
  262. layout='vertical'
  263. initialValues={{
  264. max_token: 1024
  265. }}
  266. >
  267. <div style={{ display: step === 1 ? 'block' : 'none' }} className='questionAnswerInfo-content'>
  268. <FormItem
  269. label='问答应用名称'
  270. name='name'
  271. rules={[{ required: true, message: '问答应用名称不能为空' }]}
  272. >
  273. <Input placeholder="应用名称" style={{ width: 646, padding: 8 }} />
  274. </FormItem>
  275. <FormItem
  276. label='问答应用描述'
  277. name='desc'
  278. rules={[{ required: true, message: '问答应用描述不能为空' }]}
  279. >
  280. <TextArea
  281. showCount
  282. maxLength={500}
  283. placeholder="请输入描述"
  284. style={{ height: 120, resize: 'none', width: 646 }}
  285. />
  286. </FormItem>
  287. <div>
  288. <h4>添加预设问题</h4>
  289. <div>
  290. {
  291. inputs.map(input => (
  292. <div key={input.id} style={{ paddingTop: '10px' }}>
  293. <label>问题 {input.id}</label>
  294. <Input
  295. style={{ width: 300, paddingTop: 8, marginLeft: 20 }}
  296. type="text"
  297. value={input.value}
  298. onChange={e => handleChange(input.id, e.target.value)}
  299. />
  300. <PlusCircleOutlined style={{ marginLeft: 20 }} onClick={addInput} />
  301. <MinusCircleOutlined style={{ marginLeft: 20 }} onClick={delInput} />
  302. </div>
  303. ))}
  304. </div>
  305. </div>
  306. <br />
  307. <Button type='primary' onClick={() => {
  308. form.validateFields(['name', 'desc']).then(async (values) => {
  309. setStep(2);
  310. setInputs(inputs);
  311. console.log(inputs, 'inputs');
  312. }).catch((error) => {
  313. console.error(error);
  314. });
  315. }} >
  316. 下一步
  317. </Button>
  318. </div>
  319. <div style={{ display: step === 2 ? 'block' : 'none' }} className='questionAnswerInfo-content'>
  320. <div style={{ paddingTop: '20px', display: 'flex', justifyContent: 'flex-end' }}>
  321. <div>
  322. <Button
  323. style={{ background: '#f5f5f5' }}
  324. onClick={() => {
  325. setStep(1);
  326. }}
  327. >
  328. 上一步
  329. </Button>
  330. <Button
  331. type='primary'
  332. onClick={() => {
  333. form.validateFields().then(async (values) => {
  334. const data = values;
  335. const question: string[] = [];
  336. if (inputs) {
  337. inputs.map((item, index) => {
  338. question.push(item.value);
  339. });
  340. }
  341. console.log(question, 'question');
  342. const info = {
  343. id: values.id,
  344. name: values.name, //应用名称
  345. desc: values.desc, //应用描述
  346. prompt: values.prompt, //应用提示语
  347. top_p: values.top_p, //topP
  348. temperature: values.temperature, //温度
  349. knowledge_ids: values.knowledge_ids,
  350. slice_count: values.slice_count,
  351. model: values.model,
  352. icon_color: values.icon_color,
  353. icon_type: values.icon_type,
  354. questionList: question,
  355. // knowledge_info: {
  356. // model: values.model, // 默认模型名称
  357. // knowledge_ids: values.knowledge_ids, // 知识库id列表
  358. // slice_config_type: values.slice_config_type, // 切片类型,默认为 fixed
  359. // recall_method: values.recall_method, // 召回方式,默认为 embedding
  360. // recall_index_type_list: values.recall_index_type_list, // 索引配置类型列表,默认为空数组
  361. // slice_count: values.slice_count, // 切片数量,默认为空字符串
  362. // rerank_status: values.rerank_status ? 1 : 0, // 是否开启rerank,默认关闭
  363. // rerank_model_name: values.rerank_model_name, // 模型名称,默认为空字符串
  364. // show_recall_result: values.show_recall_result, // 是否展示召回结果,默认为空字符串
  365. // recall_slice_splicing_method: values.recall_slice_splicing_method // 召回切片拼接方式,默认为空字符串
  366. // }
  367. };
  368. const id = location?.state?.id;
  369. let res = null;
  370. if (id) {
  371. // 编辑应用
  372. res = await apis.modifyApplicationApi(id, info);
  373. } else {
  374. // 创建应用
  375. res = await await apis.createApplicationApi(info);
  376. }
  377. if (res.data.code !== 200) {
  378. message.error(res.data.message);
  379. } else {
  380. router.navigate({ pathname: '/questionAnswer' });
  381. }
  382. }).catch((error) => {
  383. console.error(error);
  384. });
  385. }}
  386. >发布应用</Button>
  387. </div>
  388. </div>
  389. <Splitter style={{ height: '100%', boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)' }}>
  390. <Splitter.Panel defaultSize="40%">
  391. <div style={{ width: '100%', height: '100%' }}>
  392. <h2>Prompt编写</h2>
  393. <div style={{ paddingTop: '20px' }}>
  394. <TextArea
  395. autoSize
  396. readOnly
  397. placeholder="编写Prompt过程中可以引入2项变量:{{知识}} 代表知识库中检索到的知识内容, {{用户}}代表用户输入的内容。您可以在编写Prompt过程中将变量拼接在合适的位置。插入:{{知识}} 插入:{{用户}}"
  398. style={{ width: '100%', height: '300px' }}
  399. />
  400. </div>
  401. <Divider plain></Divider>
  402. <div >
  403. <FormItem name='prompt' >
  404. <TextArea
  405. placeholder="提示词"
  406. rows={20}
  407. />
  408. </FormItem>
  409. </div>
  410. </div>
  411. </Splitter.Panel>
  412. <Splitter.Panel defaultSize="60%">
  413. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', background: '#f5f5f5', width: '100%', height: '100%' }}>
  414. <div style={{ width: '50%', height: '100%' }}>
  415. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', paddingTop: '50px', fontSize: '16px' }}>
  416. 欢迎使用 {name}
  417. </div>
  418. <div style={{
  419. display: 'flex', justifyContent: 'center', alignItems: 'center',
  420. paddingTop: '20px'
  421. }}>
  422. <FormItem
  423. label='引用知识库'
  424. name='knowledge_ids'
  425. rules={[{ required: true, message: '知识库不能为空' }]}>
  426. <Select
  427. mode='multiple'
  428. maxCount={MAX_COUNT}
  429. // suffixIcon={suffix}
  430. style={{ width: '300px', height: '48px' }}
  431. placeholder='请选择知识库'
  432. >
  433. {
  434. knowledgeList.map((item, index) => {
  435. return <Option value={item.value} key={index}>
  436. {item.label}
  437. </Option>
  438. })
  439. }
  440. </Select>
  441. </FormItem>
  442. </div>
  443. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  444. <FormItem
  445. label='调用模型'
  446. name="model"
  447. rules={[{ required: true, message: '模型不能为空' }]}>
  448. <Select
  449. placeholder='请选择模型'
  450. allowClear={true}
  451. style={{ width: '300px', height: '48px' }}
  452. >
  453. {
  454. modelList.map((item, index) => {
  455. return <Option value={item.value} key={index}>
  456. {item.label}
  457. </Option>
  458. })
  459. }
  460. </Select>
  461. </FormItem>
  462. </div>
  463. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  464. <FormItem
  465. label='max token'
  466. name='max_token'
  467. rules={[{ required: true, message: 'max token不能为空' }]}>
  468. <InputNumber
  469. className='questionAnswerInfo-content-title'
  470. />
  471. </FormItem>
  472. </div>
  473. {/* {
  474. !isVisible &&
  475. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  476. <a onClick={() => {
  477. setIsVisible(!isVisible);
  478. }} className='questionAnswerInfo-content-title'>
  479. 更多设置
  480. </a>
  481. </div>
  482. } */}
  483. {/* {isVisible &&
  484. <div>
  485. {isVisibleCus &&
  486. <Space style={{ width: '100%' }} direction="vertical">
  487. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  488. <FormItem
  489. label='Top-p'
  490. name='topP'
  491. style={{ width: '300px' }}
  492. >
  493. <TopPDecimalStep />
  494. </FormItem>
  495. </div>
  496. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  497. <FormItem
  498. label='Temperature'
  499. name='temperature'
  500. style={{ width: '300px' }}
  501. >
  502. <TempDecimalStep />
  503. </FormItem>
  504. </div>
  505. </Space >
  506. }
  507. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  508. <FormItem
  509. label='回答风格'
  510. name='param_desc'
  511. rules={[{ required: true, message: '回答风格不能为空' }]}>
  512. <Radio.Group buttonStyle="solid"
  513. className='questionAnswerInfo-content-title'>
  514. <Radio.Button onClick={() => {
  515. handleRedioClick('')
  516. }} value='strict'>严谨</Radio.Button>
  517. <Radio.Button onClick={() => {
  518. handleRedioClick('')
  519. }} value='moderate'>适中</Radio.Button>
  520. <Radio.Button onClick={() => {
  521. handleRedioClick('')
  522. }} value='flexib'>发散</Radio.Button>
  523. <Radio.Button value='custom'
  524. onClick={() => {
  525. setIsVisibleCus(!isVisibleCus);
  526. }}
  527. >自定义
  528. </Radio.Button>
  529. </Radio.Group>
  530. </FormItem>
  531. </div>
  532. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  533. <FormItem
  534. label='展示引用知识'
  535. name='show_recall_result'
  536. className='questionAnswerInfo-content-title'>
  537. <Switch onChange={onChangeShow} />
  538. </FormItem>
  539. </div>
  540. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  541. <FormItem
  542. label='召回方式'
  543. name='recall_method'
  544. rules={[{ required: true, message: '召回方式不能为空' }]}>
  545. <Radio.Group
  546. style={style}
  547. onChange={onChangeRecallMethod}
  548. options={[
  549. { value: 'embedding', label: '向量化检索' },
  550. { value: 'keyword', label: '关键词检索' },
  551. { value: 'mixed', label: '混合检索' },
  552. ]}
  553. />
  554. </FormItem>
  555. </div>
  556. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  557. <p className='questionAnswerInfo-content-title'>重排方式</p>
  558. </div>
  559. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  560. <FormItem
  561. label='Rerank模型'
  562. name='rerank_status'
  563. valuePropName='checked'
  564. className='questionAnswerInfo-content-title'
  565. >
  566. <Switch onChange={onChangeModel} />
  567. </FormItem>
  568. </div>
  569. {isVisibleRerank &&
  570. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  571. <FormItem
  572. label='模型选择'
  573. name='rerank_model_name'
  574. >
  575. <Select
  576. style={{ width: '300px', height: '48px' }}
  577. placeholder='请选择模型'
  578. defaultValue={'默认rerank模型'}
  579. >
  580. <Option value='rerank'>默认rerank模型</Option>
  581. </Select>
  582. </FormItem>
  583. </div>
  584. }
  585. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  586. <FormItem
  587. label='召回切片数量'
  588. name='slice_config_type'
  589. rules={[{ required: true, message: '召回方式不能为空' }]}>
  590. <Select
  591. style={{ width: '300px', height: '48px' }}
  592. placeholder='请选择'
  593. onChange={onChangeCount}>
  594. <Option value="fixed">手动设置</Option>
  595. <Option value="customized">自动设置</Option>
  596. </Select>
  597. </FormItem>
  598. </div>
  599. {isVisibleSlice &&
  600. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  601. <FormItem
  602. label='召回切片数'
  603. name='slice_count'
  604. rules={[{ required: true, message: '切片数不能为空' }]}>
  605. <InputNumber max={1024} changeOnWheel className='questionAnswerInfo-content-title' />
  606. </FormItem>
  607. </div>
  608. }
  609. <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  610. <FormItem
  611. label='召回切片拼接方式'
  612. name='recall_slice_splicing_method'
  613. >
  614. <TextArea
  615. rows={4}
  616. className='questionAnswerInfo-content-title'
  617. placeholder="请输入内容"
  618. />
  619. </FormItem>
  620. </div>
  621. </div>
  622. } */}
  623. </div>
  624. </div>
  625. </Splitter.Panel>
  626. </Splitter>
  627. </div>
  628. </Form>
  629. </Spin>
  630. </div>
  631. );
  632. };
  633. export default observer(QuestionAnswerInfo);