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; idx += tokenLength;
await wait(500); await wait(100);
} }
setMessages(prev => setMessages(prev =>

View File

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

View File

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

View File

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

View File

@ -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 (
<div className={styles.headerLabel}>
{/* TODO: 样式不生效 */}
<Dropdown menu={{ items: modelsItems, onClick }} arrow >
<Flex align='center'> <Flex align='center'>
<span >{props?.value?.label}</span> <span >{props?.value?.label}</span>
<RightOutlined style={{ fontSize: 16 }} /> <RightOutlined style={{ fontSize: 16 }} />
</Flex> </Flex>
</Dropdown> </Dropdown>
</div>
)
} }

View File

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

View File

@ -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}
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> </Flex>
</ProForm>
<Tags /> <Tags />
</div> </div>
</div> </div>

View File

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