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