feat: chat交互

This commit is contained in:
李如威 2026-01-06 23:36:19 +08:00
parent a2d98c4da8
commit 2661fcf0ab
8 changed files with 84 additions and 67 deletions

View File

@ -63,7 +63,7 @@ export function useChat({ api }: UseChatOptions) {
)
)
idx += tokenLength;
await wait(500);
await wait(100);
}
setMessages(prev =>

View File

@ -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 {

View File

@ -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>
</>

View File

@ -0,0 +1,3 @@
.headerLabel {
cursor: pointer;
}

View File

@ -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>
)
}

View File

@ -1,17 +1,11 @@
.InputBar {
.CenterCard {
width: 70vw;
padding: 18px 18px 0 18px;
background-color: white;
border-radius: 8px;
padding-bottom: 24px,
}
.inputBar {
line-height: 0;
}
.Flex {
flex: 1;
.ant-form-item {
--ant-form-item-margin-bottom: 0;
}
}
.centerCard {
width: 70vw;
padding: 18px 18px 0 18px;
background-color: white;
border-radius: 8px;
padding-bottom: 24px,
}

View File

@ -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>

View File

@ -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} />