feat: chat交互
This commit is contained in:
parent
a2d98c4da8
commit
2661fcf0ab
|
|
@ -63,7 +63,7 @@ export function useChat({ api }: UseChatOptions) {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
idx += tokenLength;
|
idx += tokenLength;
|
||||||
await wait(500);
|
await wait(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages(prev =>
|
setMessages(prev =>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
.chatFoot {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
.chatList {
|
.chatList {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -25,7 +29,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatItem.left .bubble {
|
.chatItem.left .bubble {
|
||||||
background: #f5f5f5;
|
background: rgb(232, 232, 232);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatItem.right .bubble {
|
.chatItem.right .bubble {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { ChatMessage } from './types';
|
||||||
|
|
||||||
type ChatListProps = {
|
type ChatListProps = {
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
|
marginBottom?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatList({ messages }: ChatListProps) {
|
export function ChatList({ messages, marginBottom = 200}: ChatListProps) {
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
@ -23,9 +24,9 @@ export function ChatList({ messages }: ChatListProps) {
|
||||||
{messages.map(msg => (
|
{messages.map(msg => (
|
||||||
<div key={msg.id}>
|
<div key={msg.id}>
|
||||||
<ChatItem message={msg} />
|
<ChatItem message={msg} />
|
||||||
<div ref={msg.streaming ? bottomRef : null} />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div ref={bottomRef} className={styles.chatFoot} style={{ height: marginBottom }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.headerLabel {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Dropdown, Flex } from 'antd';
|
import { Dropdown, Flex } from 'antd';
|
||||||
import { RightOutlined } from '@ant-design/icons';
|
import { RightOutlined } from '@ant-design/icons';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
import './index.less';
|
import styles from './index.less';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export type ModelItem = { label: string, key: string }
|
export type ModelItem = { label: string, key: string }
|
||||||
|
|
@ -16,7 +16,7 @@ export type HeaderModelProps = {
|
||||||
export default (props: HeaderModelProps) => {
|
export default (props: HeaderModelProps) => {
|
||||||
const modelsItems: ModelItem[] | any[] = [{
|
const modelsItems: ModelItem[] | any[] = [{
|
||||||
key: 'chatgpt',
|
key: 'chatgpt',
|
||||||
label: 'ChatGtp'
|
label: 'ChatGPT'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
const onClick: MenuProps['onClick'] = e => {
|
const onClick: MenuProps['onClick'] = e => {
|
||||||
|
|
@ -27,10 +27,17 @@ export default (props: HeaderModelProps) => {
|
||||||
!props.value && props?.onChange && props.onChange(modelsItems[0]);
|
!props.value && props?.onChange && props.onChange(modelsItems[0]);
|
||||||
}, [props.value])
|
}, [props.value])
|
||||||
|
|
||||||
return <Dropdown menu={{ items: modelsItems, onClick }} arrow className='TitleLabel'>
|
return (
|
||||||
<Flex align='center'>
|
<div className={styles.headerLabel}>
|
||||||
<span >{props?.value?.label}</span>
|
{/* TODO: 样式不生效 */}
|
||||||
<RightOutlined style={{ fontSize: 16 }} />
|
<Dropdown menu={{ items: modelsItems, onClick }} arrow >
|
||||||
</Flex>
|
|
||||||
</Dropdown>
|
<Flex align='center'>
|
||||||
|
<span >{props?.value?.label}</span>
|
||||||
|
<RightOutlined style={{ fontSize: 16 }} />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
.InputBar {
|
.inputBar {
|
||||||
.CenterCard {
|
line-height: 0;
|
||||||
width: 70vw;
|
}
|
||||||
padding: 18px 18px 0 18px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding-bottom: 24px,
|
|
||||||
}
|
|
||||||
|
|
||||||
.Flex {
|
.centerCard {
|
||||||
flex: 1;
|
width: 70vw;
|
||||||
|
padding: 18px 18px 0 18px;
|
||||||
.ant-form-item {
|
background-color: white;
|
||||||
--ant-form-item-margin-bottom: 0;
|
border-radius: 8px;
|
||||||
}
|
padding-bottom: 24px,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { ProForm, ProFormTextArea } from '@ant-design/pro-components';
|
import { ProForm, ProFormTextArea } from '@ant-design/pro-components';
|
||||||
import { useModel, history } from '@umijs/max';
|
import { useModel, history } from '@umijs/max';
|
||||||
import { Button, Divider, Dropdown, Flex, message, Tag, theme } from 'antd';
|
import { Button, Divider, Dropdown, Flex, Input, message, Tag, theme } from 'antd';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import './index.less'
|
import styles from './index.less'
|
||||||
import { FileSearchOutlined, GlobalOutlined, PlusOutlined } from '@ant-design/icons';
|
import { FileSearchOutlined, GlobalOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { TweenOneGroup } from 'rc-tween-one';
|
import { TweenOneGroup } from 'rc-tween-one';
|
||||||
|
|
||||||
|
|
@ -17,12 +17,19 @@ export type InputBarEvent = {
|
||||||
defaultValues?: InputBarDefaultValue;
|
defaultValues?: InputBarDefaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InputBarToolEvent = {
|
||||||
|
tools: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export type InputBarSubmitHandler = (event: InputBarEvent) => void;
|
export type InputBarSubmitHandler = (event: InputBarEvent) => void;
|
||||||
|
|
||||||
export type InputBarProps = {
|
export type InputBarProps = {
|
||||||
onSubmit?: InputBarSubmitHandler;
|
onSubmit?: InputBarSubmitHandler;
|
||||||
|
onToolsChange?: InputBarToolsOnChangeHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InputBarToolsOnChangeHandler = (event: InputBarToolEvent) => void;
|
||||||
|
|
||||||
export const InputBarToolItems = [{
|
export const InputBarToolItems = [{
|
||||||
key: 'searchWeb',
|
key: 'searchWeb',
|
||||||
label: '搜索网站',
|
label: '搜索网站',
|
||||||
|
|
@ -50,30 +57,9 @@ const tagGroupStyle: React.CSSProperties = {
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
const InputBar = (props: InputBarProps) => {
|
const InputBar: React.FC<InputBarProps> = (props) => {
|
||||||
const textAreaRef = useRef<any>(null);
|
|
||||||
const [tools, setTools] = useState<any>([]);
|
const [tools, setTools] = useState<any>([]);
|
||||||
const textAreaFieldProps: any = {
|
const [value, setValue] = useState<string>('');
|
||||||
ref: textAreaRef,
|
|
||||||
autoSize: {
|
|
||||||
minRows: 1,
|
|
||||||
maxRows: 5,
|
|
||||||
},
|
|
||||||
variant: 'borderless',
|
|
||||||
onPressEnter: (e: any) => {
|
|
||||||
const { nativeEvent: { shiftKey, key } } = e;
|
|
||||||
if (shiftKey && key === "Enter") {
|
|
||||||
const { current: { resizableTextArea: { textArea } } } = textAreaRef;
|
|
||||||
e.preventDefault();
|
|
||||||
const text = textArea?.value + "";
|
|
||||||
textArea.value = "";
|
|
||||||
props?.onSubmit && props.onSubmit({
|
|
||||||
query: text,
|
|
||||||
tools: tools?.map((e:any) => e.key),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemOnClick = ({ key }: any) => {
|
const itemOnClick = ({ key }: any) => {
|
||||||
if (key === 'searchKB_add') {
|
if (key === 'searchKB_add') {
|
||||||
|
|
@ -120,19 +106,40 @@ const InputBar = (props: InputBarProps) => {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props?.onToolsChange && props?.onToolsChange({tools});
|
||||||
|
}, [tools])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='InputBar'>
|
<div className={styles.inputBar}>
|
||||||
<div className='CenterCard'>
|
<div className={styles.centerCard}>
|
||||||
<ProForm submitter={false}>
|
|
||||||
<Flex align='flex-end'>
|
<Flex align='flex-end'>
|
||||||
<Dropdown menu={{ items: InputBarToolItems, onClick: itemOnClick }} placement='top' arrow>
|
<Dropdown menu={{ items: InputBarToolItems, onClick: itemOnClick }} placement='top' arrow>
|
||||||
<Button icon={<PlusOutlined />} type='text'></Button>
|
<Button icon={<PlusOutlined />} type='text'></Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<div className='Flex'>
|
<Input.TextArea
|
||||||
<ProFormTextArea placeholder="按 Shift+Enter 进行提问" fieldProps={textAreaFieldProps} />
|
style={{ flex: 1}}
|
||||||
</div>
|
value={value}
|
||||||
</Flex>
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
</ProForm>
|
variant="borderless"
|
||||||
|
placeholder="按 Shift+Enter 进行提问"
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!value.trim()) return;
|
||||||
|
|
||||||
|
props?.onSubmit?.({
|
||||||
|
query: value.trim(),
|
||||||
|
tools: tools.map((e: any) => e.key),
|
||||||
|
});
|
||||||
|
|
||||||
|
setValue(''); // ✅ 绝对不会有循环引用
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<Tags />
|
<Tags />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const footerStyle: React.CSSProperties = {
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [model, setModel] = useState<ModelItem | undefined>(undefined);
|
const [model, setModel] = useState<ModelItem | undefined>(undefined);
|
||||||
|
const [marginBottom, setMarginBottom] = useState<number>(150);
|
||||||
const { messages, setMessages, loading, stop, sendMessage } = useChat({ api: '' });
|
const { messages, setMessages, loading, stop, sendMessage } = useChat({ api: '' });
|
||||||
const onSubmit: InputBarSubmitHandler = (e: InputBarEvent) => {
|
const onSubmit: InputBarSubmitHandler = (e: InputBarEvent) => {
|
||||||
sendMessage(e.query);
|
sendMessage(e.query);
|
||||||
|
|
@ -33,14 +34,14 @@ export default () => {
|
||||||
}}
|
}}
|
||||||
footer={[
|
footer={[
|
||||||
<div key='submit' className={styles.chatPageFooter}>
|
<div key='submit' className={styles.chatPageFooter}>
|
||||||
<InputBar onSubmit={onSubmit} />
|
<InputBar onSubmit={onSubmit} onToolsChange={e => setMarginBottom(e?.tools?.length ? 200 : 150)} />
|
||||||
</div>
|
</div>
|
||||||
]}
|
]}
|
||||||
title={<HeaderModelSelect value={model} onChange={val => setModel(val)} />}
|
title={<HeaderModelSelect value={model} onChange={val => setModel(val)} />}
|
||||||
>
|
>
|
||||||
<Flex vertical align='center'>
|
<Flex vertical align='center'>
|
||||||
<div className={styles.chatContent}>
|
<div className={styles.chatContent}>
|
||||||
<ChatList messages={messages} />
|
<ChatList messages={messages} marginBottom={marginBottom} />
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
<div className={styles.messageEnd} />
|
<div className={styles.messageEnd} />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue