feat: create a chat assistant and extract SimilaritySlider (#67)

* feat: extract SimilaritySlider

* feat: create a chat assistant
This commit is contained in:
balibabu 2024-02-20 18:10:20 +08:00 committed by GitHub
parent a8294f2168
commit 8c4ec9955e
26 changed files with 716 additions and 196 deletions

View File

@ -0,0 +1,5 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.1875 12.1045L7.04673 12.5819C7.21217 12.6738 7.29489 12.7197 7.38249 12.7377C7.46002 12.7537 7.53998 12.7537 7.61751 12.7377C7.70511 12.7197 7.78783 12.6738 7.95327 12.5819L8.8125 12.1045M3.5625 10.6462L2.73007 10.1837C2.55535 10.0866 2.46798 10.0381 2.40437 9.96907C2.34809 9.908 2.3055 9.83562 2.27945 9.75677C2.25 9.66764 2.25 9.5677 2.25 9.36782V8.45867M2.25 5.542V4.63284C2.25 4.43297 2.25 4.33303 2.27945 4.2439C2.3055 4.16505 2.34809 4.09267 2.40437 4.0316C2.46798 3.96257 2.55535 3.91403 2.73007 3.81696L3.5625 3.3545M6.1875 1.89617L7.04673 1.41882C7.21217 1.32691 7.29489 1.28095 7.38249 1.26294C7.46002 1.24699 7.53998 1.24699 7.61751 1.26294C7.70511 1.28095 7.78783 1.32691 7.95327 1.41882L8.8125 1.89617M11.4375 3.3545L12.2699 3.81696C12.4447 3.91403 12.532 3.96257 12.5956 4.0316C12.6519 4.09266 12.6945 4.16505 12.7206 4.2439C12.75 4.33303 12.75 4.43297 12.75 4.63284V5.542M12.75 8.45867V9.36782C12.75 9.5677 12.75 9.66764 12.7206 9.75677C12.6945 9.83562 12.6519 9.908 12.5956 9.96907C12.532 10.0381 12.4447 10.0866 12.2699 10.1837L11.4375 10.6462M6.1875 6.27117L7.5 7.00033M7.5 7.00033L8.8125 6.27117M7.5 7.00033V8.45867M2.25 4.08367L3.5625 4.81283M11.4375 4.81283L12.75 4.08367M7.5 11.3753V12.8337"
stroke="#A5A3A9" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,29 @@
import { Form, Slider } from 'antd';
type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
};
const SimilaritySlider = () => {
return (
<>
<Form.Item<FieldType>
label="Similarity threshold"
name={'similarity_threshold'}
initialValue={0}
>
<Slider max={1} step={0.01} />
</Form.Item>
<Form.Item<FieldType>
label="Vector similarity weight"
name={'vector_similarity_weight'}
initialValue={0}
>
<Slider max={1} step={0.01} />
</Form.Item>
</>
);
};
export default SimilaritySlider;

View File

@ -11,3 +11,40 @@ export enum RunningStatus {
DONE = '3', // need to refresh
FAIL = '4', // need to refresh
}
export enum ModelVariableType {
Improvise = 'Improvise',
Precise = 'Precise',
Balance = 'Balance',
}
export const settledModelVariableMap = {
[ModelVariableType.Improvise]: {
temperature: 0.9,
top_p: 0.9,
frequency_penalty: 0.2,
presence_penalty: 0.4,
max_tokens: 512,
},
[ModelVariableType.Precise]: {
temperature: 0.1,
top_p: 0.3,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 215,
},
[ModelVariableType.Balance]: {
temperature: 0.5,
top_p: 0.5,
frequency_penalty: 0.7,
presence_penalty: 0.4,
max_tokens: 215,
},
};
export enum LlmModelType {
Embedding = 'embedding',
Chat = 'chat',
Image2text = 'image2text',
Speech2text = 'speech2text',
}

View File

@ -124,3 +124,22 @@ export const useFetchKnowledgeBaseConfiguration = () => {
fetchKnowledgeBaseConfiguration();
}, [fetchKnowledgeBaseConfiguration]);
};
export const useFetchKnowledgeList = (): IKnowledge[] => {
const dispatch = useDispatch();
const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const { data = [] } = knowledgeModel;
const fetchList = useCallback(() => {
dispatch({
type: 'knowledgeModel/getList',
});
}, [dispatch]);
useEffect(() => {
fetchList();
}, [fetchList]);
return data;
};

39
web/src/hooks/llmHooks.ts Normal file
View File

@ -0,0 +1,39 @@
import { LlmModelType } from '@/constants/knowledge';
import { IThirdOAIModelCollection } from '@/interfaces/database/llm';
import { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'umi';
export const useFetchLlmList = (modelType: LlmModelType) => {
const dispatch = useDispatch();
const fetchLlmList = useCallback(() => {
dispatch({
type: 'settingModel/llm_list',
payload: { model_type: modelType },
});
}, [dispatch, modelType]);
useEffect(() => {
fetchLlmList();
}, [fetchLlmList]);
};
export const useSelectLlmOptions = () => {
const llmInfo: IThirdOAIModelCollection = useSelector(
(state: any) => state.settingModel.llmInfo,
);
const embeddingModelOptions = useMemo(() => {
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: x.llm_name,
value: x.llm_name,
})),
};
});
}, [llmInfo]);
return embeddingModelOptions;
};

View File

@ -0,0 +1,47 @@
export interface PromptConfig {
empty_response: string;
parameters: Parameter[];
prologue: string;
system: string;
}
export interface Parameter {
key: string;
optional: boolean;
}
export interface LlmSetting {
Creative: Variable;
Custom: Variable;
Evenly: Variable;
Precise: Variable;
}
export interface Variable {
frequency_penalty: number;
max_tokens: number;
presence_penalty: number;
temperature: number;
top_p: number;
}
export interface IDialog {
create_date: string;
create_time: number;
description: string;
icon: string;
id: string;
kb_ids: string[];
kb_names: string[];
language: string;
llm_id: string;
llm_setting: LlmSetting;
llm_setting_type: string;
name: string;
prompt_config: PromptConfig;
prompt_type: string;
status: string;
tenant_id: string;
update_date: string;
update_time: number;
}

View File

@ -53,7 +53,7 @@ const RenameModal = () => {
useEffect(() => {
form.setFieldValue('name', initialName);
}, [initialName, documentId]);
}, [initialName, documentId, form]);
return (
<Modal

View File

@ -17,13 +17,14 @@ import {
UploadFile,
} from 'antd';
import pick from 'lodash/pick';
import { useCallback, useEffect, useMemo } from 'react';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
import { IKnowledge } from '@/interfaces/database/knowledge';
import { IThirdOAIModelCollection } from '@/interfaces/database/llm';
import { PlusOutlined } from '@ant-design/icons';
import { LlmModelType } from '../../constant';
import styles from './index.less';
const { Title } = Typography;
@ -35,9 +36,6 @@ const Configuration = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const loading = useOneNamespaceEffectsLoading('kSModel', ['updateKb']);
const llmInfo: IThirdOAIModelCollection = useSelector(
(state: any) => state.settingModel.llmInfo,
);
const knowledgeDetails: IKnowledge = useSelector(
(state: any) => state.kSModel.knowledgeDetails,
);
@ -51,17 +49,7 @@ const Configuration = () => {
const parserList = useSelectParserList();
const embeddingModelOptions = useMemo(() => {
return Object.entries(llmInfo).map(([key, value]) => {
return {
label: key,
options: value.map((x) => ({
label: x.llm_name,
value: x.llm_name,
})),
};
});
}, [llmInfo]);
const embeddingModelOptions = useSelectLlmOptions();
const onFinish = async (values: any) => {
console.info(values);
@ -86,13 +74,6 @@ const Configuration = () => {
console.log('Failed:', errorInfo);
};
const fetchLlmList = useCallback(() => {
dispatch({
type: 'settingModel/llm_list',
payload: { model_type: 'embedding' },
});
}, [dispatch]);
useEffect(() => {
const avatar = knowledgeDetails.avatar;
let fileList: UploadFile[] = [];
@ -115,9 +96,7 @@ const Configuration = () => {
useFetchParserList();
useFetchKnowledgeBaseConfiguration();
useEffect(() => {
fetchLlmList();
}, [fetchLlmList]);
useFetchLlmList(LlmModelType.Embedding);
return (
<div className={styles.configurationWrapper}>

View File

@ -16,14 +16,10 @@ const KnowledgeTesting = () => {
const handleTesting = async () => {
const values = await form.validateFields();
console.info(values);
const similarity_threshold = values.similarity_threshold / 100;
const vector_similarity_weight = values.vector_similarity_weight / 100;
dispatch({
type: 'testingModel/testDocumentChunk',
payload: {
...values,
similarity_threshold,
vector_similarity_weight,
kb_id: knowledgeBaseId,
},
});

View File

@ -1,3 +1,5 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined, HistoryOutlined } from '@ant-design/icons';
import {
Button,
Card,
@ -6,22 +8,15 @@ import {
Form,
Input,
Slider,
SliderSingleProps,
Space,
Tag,
} from 'antd';
import { DeleteOutlined, HistoryOutlined } from '@ant-design/icons';
import { FormInstance } from 'antd/lib';
import styles from './index.less';
const list = [1, 2, 3];
const marks: SliderSingleProps['marks'] = {
0: '0',
100: '1',
};
type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
@ -29,12 +24,6 @@ type FieldType = {
question: string;
};
const formatter = (value: number | undefined) => {
return typeof value === 'number' ? value / 100 : 0;
};
const tooltip = { formatter };
interface IProps {
form: FormInstance;
handleTesting: () => Promise<any>;
@ -59,23 +48,12 @@ const TestingControl = ({ form, handleTesting }: IProps) => {
layout="vertical"
form={form}
initialValues={{
similarity_threshold: 20,
vector_similarity_weight: 30,
similarity_threshold: 0.2,
vector_similarity_weight: 0.3,
top_k: 1024,
}}
>
<Form.Item<FieldType>
label="Similarity threshold"
name={'similarity_threshold'}
>
<Slider marks={marks} defaultValue={0} tooltip={tooltip} />
</Form.Item>
<Form.Item<FieldType>
label="Vector similarity weight"
name={'vector_similarity_weight'}
>
<Slider marks={marks} defaultValue={0} tooltip={tooltip} />
</Form.Item>
<SimilaritySlider></SimilaritySlider>
<Form.Item<FieldType> label="Top k" name={'top_k'}>
<Slider marks={{ 0: 0, 2048: 2048 }} defaultValue={0} max={2048} />
</Form.Item>

View File

@ -1,11 +1,20 @@
import { Form, Input } from 'antd';
import { Form, Input, Select } from 'antd';
import classNames from 'classnames';
import { ISegmentedContentProps } from './interface';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import styles from './index.less';
const { Option } = Select;
const AssistantSetting = ({ show }: ISegmentedContentProps) => {
const knowledgeList = useFetchKnowledgeList();
const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name,
value: x.id,
}));
return (
<section
className={classNames({
@ -19,15 +28,43 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
>
<Input placeholder="e.g. Resume Jarvis" />
</Form.Item>
<Form.Item name={'avatar'} label="Assistant avatar">
<Form.Item name={'icon'} label="Assistant avatar">
<Input />
</Form.Item>
<Form.Item name={'keywords'} label="Keywords">
<Input.TextArea autoSize={{ minRows: 3 }} />
<Form.Item name={'language'} label="Language" initialValue={'Chinese'}>
<Select
options={[
{ value: 'Chinese', label: 'Chinese' },
{ value: 'English', label: 'English' },
]}
/>
</Form.Item>
<Form.Item name={'opener'} label="Set an opener">
<Form.Item
name={['prompt_config', 'empty_response']}
label="Empty response"
>
<Input placeholder="" />
</Form.Item>
<Form.Item name={['prompt_config', 'prologue']} label="Set an opener">
<Input.TextArea autoSize={{ minRows: 5 }} />
</Form.Item>
<Form.Item
label="Select one context"
name="kb_ids"
rules={[
{
required: true,
message: 'Please select!',
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder="Please select"
></Select>
</Form.Item>
</section>
);
};

View File

@ -0,0 +1,7 @@
export const variableEnabledFieldMap = {
temperatureEnabled: 'temperature',
topPEnabled: 'top_p',
presencePenaltyEnabled: 'presence_penalty',
frequencyPenaltyEnabled: 'frequency_penalty',
maxTokensEnabled: 'max_tokens',
};

View File

@ -41,3 +41,10 @@
width: 0;
margin: 0;
}
.sliderInputNumber {
width: 80px;
}
.variableSlider {
width: 100%;
}

View File

@ -2,17 +2,20 @@ import { ReactComponent as ChatConfigurationAtom } from '@/assets/svg/chat-confi
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import { Divider, Flex, Form, Modal, Segmented } from 'antd';
import { SegmentedValue } from 'antd/es/segmented';
import { useState } from 'react';
import omit from 'lodash/omit';
import { useRef, useState } from 'react';
import AssistantSetting from './assistant-setting';
import ModelSetting from './model-setting';
import PromptEngine from './prompt-engine';
import { useSetDialog } from '../hooks';
import { variableEnabledFieldMap } from './constants';
import styles from './index.less';
enum ConfigurationSegmented {
AssistantSetting = 'Assistant Setting',
ModelSetting = 'Model Setting',
PromptEngine = 'Prompt Engine',
ModelSetting = 'Model Setting',
}
const segmentedMap = {
@ -45,10 +48,24 @@ const ChatConfigurationModal = ({
const [value, setValue] = useState<ConfigurationSegmented>(
ConfigurationSegmented.AssistantSetting,
);
const promptEngineRef = useRef(null);
const setDialog = useSetDialog();
const handleOk = async () => {
const x = await form.validateFields();
console.info(x);
const values = await form.validateFields();
const nextValues: any = omit(values, Object.keys(variableEnabledFieldMap));
const finalValues = {
...nextValues,
prompt_config: {
...nextValues.prompt_config,
parameters: promptEngineRef.current,
},
};
console.info(promptEngineRef.current);
console.info(nextValues);
console.info(finalValues);
setDialog(finalValues);
};
const handleCancel = () => {
@ -97,7 +114,14 @@ const ChatConfigurationModal = ({
colon={false}
>
{Object.entries(segmentedMap).map(([key, Element]) => (
<Element key={key} show={key === value}></Element>
<Element
key={key}
show={key === value}
form={form}
{...(key === ConfigurationSegmented.PromptEngine
? { ref: promptEngineRef }
: {})}
></Element>
))}
</Form>
</Modal>

View File

@ -1,3 +1,14 @@
import { FormInstance } from 'antd';
export interface ISegmentedContentProps {
show: boolean;
form: FormInstance;
}
export interface IVariable {
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
max_tokens: number;
}

View File

@ -1,12 +1,48 @@
import { Divider, Flex, Form, InputNumber, Select, Slider } from 'antd';
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd';
import classNames from 'classnames';
import { useEffect } from 'react';
import { ISegmentedContentProps } from './interface';
import { useFetchLlmList, useSelectLlmOptions } from '@/hooks/llmHooks';
import { variableEnabledFieldMap } from './constants';
import styles from './index.less';
const { Option } = Select;
const ModelSetting = ({ show, form }: ISegmentedContentProps) => {
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: x,
value: x,
}));
const parameters: ModelVariableType = Form.useWatch('parameters', form);
const modelOptions = useSelectLlmOptions();
const handleParametersChange = (value: ModelVariableType) => {
console.info(value);
};
useEffect(() => {
const variable = settledModelVariableMap[parameters];
form.setFieldsValue({ llm_setting: variable });
}, [parameters, form]);
useEffect(() => {
const values = Object.keys(variableEnabledFieldMap).reduce<
Record<string, boolean>
>((pre, field) => {
pre[field] = true;
return pre;
}, {});
form.setFieldsValue(values);
}, [form]);
useFetchLlmList(LlmModelType.Chat);
const ModelSetting = ({ show }: ISegmentedContentProps) => {
return (
<section
className={classNames({
@ -15,135 +51,170 @@ const ModelSetting = ({ show }: ISegmentedContentProps) => {
>
<Form.Item
label="Model"
name="model"
// rules={[{ required: true, message: 'Please input!' }]}
name="llm_id"
rules={[{ required: true, message: 'Please select!' }]}
>
<Select />
<Select options={modelOptions} />
</Form.Item>
<Divider></Divider>
<Form.Item
label="Parameters"
name="parameters"
initialValue={ModelVariableType.Precise}
// rules={[{ required: true, message: 'Please input!' }]}
>
<Select />
<Select<ModelVariableType>
options={parameterOptions}
onChange={handleParametersChange}
/>
</Form.Item>
<Form.Item label="Temperature">
<Flex gap={20}>
<Form.Item label="Temperature" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'temperatureEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'temperature']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'temperature']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Top P">
<Flex gap={20}>
<Form.Item label="Top P" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item name={'topPEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'top_p']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'top_p']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Presence Penalty">
<Flex gap={20}>
<Form.Item label="Presence Penalty" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'presencePenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'presence_penalty']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'presence_penalty']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Frequency Penalty">
<Flex gap={20}>
<Form.Item label="Frequency Penalty" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item
name={'frequencyPenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'frequency_penalty']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={1} step={0.01} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'frequency_penalty']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
/>
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label="Max Tokens">
<Flex gap={20}>
<Form.Item label="Max Tokens" tooltip={'xx'}>
<Flex gap={20} align="center">
<Form.Item name={'maxTokensEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Flex flex={1}>
<Form.Item
name={['address', 'province']}
name={['llm_setting', 'max_tokens']}
noStyle
rules={[{ required: true, message: 'Province is required' }]}
>
<Slider style={{ display: 'inline-block', width: '100%' }} />
<Slider className={styles.variableSlider} max={2048} />
</Form.Item>
</Flex>
<Form.Item
name={['address', 'street']}
name={['llm_setting', 'max_tokens']}
noStyle
rules={[{ required: true, message: 'Street is required' }]}
>
<InputNumber
style={{
width: 50,
}}
className={styles.sliderInputNumber}
max={2048}
min={0}
/>
</Form.Item>
</Flex>

View File

@ -1,3 +1,4 @@
import SimilaritySlider from '@/components/similarity-slider';
import { DeleteOutlined } from '@ant-design/icons';
import {
Button,
@ -6,13 +7,19 @@ import {
Form,
Input,
Row,
Select,
Slider,
Switch,
Table,
TableProps,
} from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { v4 as uuid } from 'uuid';
import { EditableCell, EditableRow } from './editable-cell';
import { ISegmentedContentProps } from './interface';
@ -21,12 +28,20 @@ import styles from './index.less';
interface DataType {
key: string;
variable: string;
optional: boolean;
}
const { Option } = Select;
type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
top_n?: number;
};
const PromptEngine = ({ show }: ISegmentedContentProps) => {
const PromptEngine = (
{ show, form }: ISegmentedContentProps,
ref: ForwardedRef<Array<Omit<DataType, 'variable'>>>,
) => {
const [dataSource, setDataSource] = useState<DataType[]>([]);
const components = {
@ -52,6 +67,44 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
setDataSource(newData);
};
const handleAdd = () => {
setDataSource((state) => [
...state,
{
key: uuid(),
variable: '',
optional: true,
},
]);
};
const handleOptionalChange = (row: DataType) => (checked: boolean) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.key === item.key);
const item = newData[index];
newData.splice(index, 1, {
...item,
optional: checked,
});
setDataSource(newData);
};
useImperativeHandle(
ref,
() => {
return dataSource
.filter((x) => x.variable.trim() !== '')
.map((x) => ({ key: x.variable, optional: x.optional }));
},
[dataSource],
);
useEffect(() => {
form.setFieldValue(['prompt_config', 'parameters'], dataSource);
const x = form.getFieldValue(['prompt_config', 'parameters']);
console.info(x);
}, [dataSource, form]);
const columns: TableProps<DataType>['columns'] = [
{
title: 'key',
@ -71,8 +124,14 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
key: 'optional',
width: 40,
align: 'center',
render() {
return <Switch size="small" />;
render(text, record) {
return (
<Switch
size="small"
checked={text}
onChange={handleOptionalChange(record)}
/>
);
},
},
{
@ -87,17 +146,6 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
},
];
const handleAdd = () => {
setDataSource((state) => [
...state,
{
key: uuid(),
variable: '',
optional: true,
},
]);
};
return (
<section
className={classNames({
@ -106,12 +154,20 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
>
<Form.Item
label="Orchestrate"
name="orchestrate"
rules={[{ required: true, message: 'Please input!' }]}
name={['prompt_config', 'system']}
initialValue={`你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。回答需要考虑聊天历史。
{knowledge}
`}
>
<Input.TextArea autoSize={{ maxRows: 5, minRows: 5 }} />
</Form.Item>
<Divider></Divider>
<SimilaritySlider></SimilaritySlider>
<Form.Item<FieldType> label="Top n" name={'top_n'} initialValue={0}>
<Slider max={30} />
</Form.Item>
<section className={classNames(styles.variableContainer)}>
<Row align={'middle'} justify="end">
<Col span={6} className={styles.variableAlign}>
@ -139,25 +195,8 @@ const PromptEngine = ({ show }: ISegmentedContentProps) => {
</Row>
)}
</section>
<Form.Item
label="Select one context"
name="context"
rules={[
{
required: true,
message: 'Please select your favourite colors!',
type: 'array',
},
]}
>
<Select mode="multiple" placeholder="Please select favourite colors">
<Option value="red">Red</Option>
<Option value="green">Green</Option>
<Option value="blue">Blue</Option>
</Select>
</Form.Item>
</section>
);
};
export default PromptEngine;
export default forwardRef(PromptEngine);

View File

@ -0,0 +1,29 @@
import { IDialog } from '@/interfaces/database/chat';
import { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'umi';
export const useFetchDialogList = () => {
const dispatch = useDispatch();
const dialogList: IDialog[] = useSelector(
(state: any) => state.chatModel.dialogList,
);
useEffect(() => {
dispatch({ type: 'chatModel/listDialog' });
}, [dispatch]);
return dialogList;
};
export const useSetDialog = () => {
const dispatch = useDispatch();
const setDialog = useCallback(
(payload: IDialog) => {
dispatch({ type: 'chatModel/setDialog', payload });
},
[dispatch],
);
return setDialog;
};

View File

@ -4,6 +4,17 @@
.chatAppWrapper {
width: 288px;
padding: 26px;
.chatAppCard {
:global(.ant-card-body) {
padding: 10px;
}
.cubeIcon {
&:hover {
cursor: pointer;
}
}
}
}
.chatTitleWrapper {
width: 220px;

View File

@ -1,14 +1,73 @@
import { FormOutlined } from '@ant-design/icons';
import { Button, Card, Divider, Flex, Space, Tag } from 'antd';
import { useSelector } from 'umi';
import { DeleteOutlined, EditOutlined, FormOutlined } from '@ant-design/icons';
import {
Button,
Card,
Divider,
Dropdown,
Flex,
MenuProps,
Space,
Tag,
} from 'antd';
import ChatContainer from './chat-container';
import { ReactComponent as ChatAppCube } from '@/assets/svg/chat-app-cube.svg';
import ModalManager from '@/components/modal-manager';
import classNames from 'classnames';
import ChatConfigurationModal from './chat-configuration-modal';
import { useFetchDialogList } from './hooks';
import { useState } from 'react';
import styles from './index.less';
const Chat = () => {
const { name } = useSelector((state: any) => state.chatModel);
const dialogList = useFetchDialogList();
const [activated, setActivated] = useState<string>('');
const handleAppCardEnter = (id: string) => () => {
setActivated(id);
};
const handleAppCardLeave = () => {
setActivated('');
};
const items: MenuProps['items'] = [
{
key: '1',
label: (
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.antgroup.com"
>
1st menu item
</a>
),
},
];
const appItems: MenuProps['items'] = [
{
key: '1',
label: (
<Space>
<EditOutlined />
Edit
</Space>
),
},
{ type: 'divider' },
{
key: '2',
label: (
<Space>
<DeleteOutlined />
Delete chat
</Space>
),
},
];
return (
<Flex className={styles.chatWrapper}>
@ -32,9 +91,33 @@ const Chat = () => {
</ModalManager>
<Divider></Divider>
<Card>
<p>Card content</p>
</Card>
<Space direction={'vertical'} size={'middle'}>
{dialogList.map((x) => (
<Card
key={x.id}
className={classNames(styles.chatAppCard)}
onMouseEnter={handleAppCardEnter(x.id)}
onMouseLeave={handleAppCardLeave}
>
<Flex justify="space-between" align="center">
<Space>
{x.icon}
<section>
<b>{x.name}</b>
<div>{x.description}</div>
</section>
</Space>
{activated === x.id && (
<section>
<Dropdown menu={{ items: appItems }}>
<ChatAppCube className={styles.cubeIcon}></ChatAppCube>
</Dropdown>
</section>
)}
</Flex>
</Card>
))}
</Space>
</Flex>
</Flex>
<Divider type={'vertical'} className={styles.divider}></Divider>
@ -49,7 +132,9 @@ const Chat = () => {
<b>Chat</b>
<Tag>25</Tag>
</Space>
<FormOutlined />
<Dropdown menu={{ items }}>
<FormOutlined />
</Dropdown>
</Flex>
<Divider></Divider>
<section className={styles.chatTitleContent}>today</section>

View File

@ -1,13 +1,18 @@
import { IDialog } from '@/interfaces/database/chat';
import chatService from '@/services/chatService';
import { message } from 'antd';
import { DvaModel } from 'umi';
export interface ChatModelState {
name: string;
dialogList: IDialog[];
}
const model: DvaModel<ChatModelState> = {
namespace: 'chatModel',
state: {
name: 'kate',
dialogList: [],
},
reducers: {
save(state, action) {
@ -16,16 +21,41 @@ const model: DvaModel<ChatModelState> = {
...action.payload,
};
},
},
subscriptions: {
setup({ dispatch, history }) {
return history.listen((query) => {
console.log(query);
});
setDialogList(state, { payload }) {
return {
...state,
dialogList: payload,
};
},
},
effects: {
*query({ payload }, { call, put }) {},
*getDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.getDialog, payload);
},
*setDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.setDialog, payload);
if (data.retcode === 0) {
yield put({ type: 'listDialog' });
message.success('Created successfully !');
}
},
*listDialog({ payload }, { call, put }) {
const { data } = yield call(chatService.listDialog, payload);
yield put({ type: 'setDialogList', payload: data.data });
},
*listConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.listConversation, payload);
},
*getConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.getConversation, payload);
},
*setConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.setConversation, payload);
},
*completeConversation({ payload }, { call, put }) {
const { data } = yield call(chatService.completeConversation, payload);
},
},
};

View File

@ -2,33 +2,14 @@ import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
import ModalManager from '@/components/modal-manager';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Flex, Space } from 'antd';
import { useCallback, useEffect } from 'react';
import { useDispatch, useNavigate, useSelector } from 'umi';
import KnowledgeCard from './knowledge-card';
import KnowledgeCreatingModal from './knowledge-creating-modal';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import styles from './index.less';
const Knowledge = () => {
const dispatch = useDispatch();
const knowledgeModel = useSelector((state: any) => state.knowledgeModel);
const navigate = useNavigate();
const { data = [] } = knowledgeModel;
const fetchList = useCallback(() => {
dispatch({
type: 'knowledgeModel/getList',
payload: {},
});
}, []);
// const handleAddKnowledge = () => {
// navigate(`/knowledge/${KnowledgeRouteKey.Configuration}`);
// };
useEffect(() => {
fetchList();
}, [fetchList]);
const data = useFetchKnowledgeList();
return (
<div className={styles.knowledge}>

View File

@ -37,7 +37,7 @@ const SettingList = () => {
type: 'settingModel/my_llm',
payload: {},
});
}, []);
}, [dispatch]);
return (
<div

View File

@ -0,0 +1,48 @@
import api from '@/utils/api';
import registerServer from '@/utils/registerServer';
import request from '@/utils/request';
const {
getDialog,
setDialog,
listDialog,
getConversation,
setConversation,
completeConversation,
listConversation,
} = api;
const methods = {
getDialog: {
url: getDialog,
method: 'get',
},
setDialog: {
url: setDialog,
method: 'post',
},
listDialog: {
url: listDialog,
method: 'get',
},
listConversation: {
url: listConversation,
method: 'get',
},
getConversation: {
url: getConversation,
method: 'get',
},
setConversation: {
url: setConversation,
method: 'post',
},
completeConversation: {
url: completeConversation,
method: 'post',
},
} as const;
const chatService = registerServer<keyof typeof methods>(methods, request);
export default chatService;

View File

@ -42,4 +42,14 @@ export default {
document_create: `${api_host}/document/create`,
document_run: `${api_host}/document/run`,
document_change_parser: `${api_host}/document/change_parser`,
setDialog: `${api_host}/dialog/set`,
getDialog: `${api_host}/dialog/get`,
listDialog: `${api_host}/dialog/list`,
setConversation: `${api_host}/conversation/set`,
getConversation: `${api_host}/conversation/get`,
listConversation: `${api_host}/conversation/list`,
removeConversation: `${api_host}/conversation/rm`,
completeConversation: `${api_host}/conversation/completion`,
};

View File

@ -5,22 +5,23 @@
*/
// import numeral from 'numeral';
import JSEncrypt from 'jsencrypt';
import { Base64 } from 'js-base64';
import JSEncrypt from 'jsencrypt';
export const getWidth = () => {
return { width: window.innerWidth };
};
export const rsaPsw = (password: string) => {
const pub = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB-----END PUBLIC KEY-----"
const encryptor = new JSEncrypt()
const pub =
'-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB-----END PUBLIC KEY-----';
const encryptor = new JSEncrypt();
encryptor.setPublicKey(pub)
encryptor.setPublicKey(pub);
return encryptor.encrypt(Base64.encode(password))
}
return encryptor.encrypt(Base64.encode(password));
};
export default {
getWidth,
rsaPsw
rsaPsw,
};