generated from arvinxx/npm-template
-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
绾歌
committed
Jun 26, 2024
1 parent
d8488ad
commit d2f5c07
Showing
8 changed files
with
1,040 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import BackBottom from '@/BackBottom'; | ||
import { createStyles } from 'antd-style'; | ||
import RcResizeObserver from 'rc-resize-observer'; | ||
import { CSSProperties, memo, useContext, useEffect, useRef, useState } from 'react'; | ||
import { Flexbox } from 'react-layout-kit'; | ||
|
||
import { ChatListItemProps } from '@/ChatList/ChatListItem'; | ||
import { ConfigProvider } from 'antd'; | ||
import ProInputArea, { ProInputAreaProps } from '../../ProInputArea'; | ||
import ChatList from '../components/ChatList'; | ||
import ChatScrollAnchor from '../components/ScrollAnchor'; | ||
import useProChatLocale from '../hooks/useProChatLocale'; | ||
import { useOverrideStyles } from './OverrideStyle'; | ||
import { ProChatChatReference } from './StoreUpdater'; | ||
import { ProChatProps } from './index'; | ||
|
||
const useStyles = createStyles( | ||
({ css, responsive, stylish }) => css` | ||
overflow: hidden scroll; | ||
height: 100%; | ||
${responsive.mobile} { | ||
${stylish.noScrollbar} | ||
width: 100%; | ||
} | ||
`, | ||
); | ||
|
||
/** | ||
* 对话组件的属性接口 | ||
*/ | ||
export interface ConversationProps extends ProChatProps<any> { | ||
/** | ||
* 是否显示标题 | ||
*/ | ||
showTitle?: boolean; | ||
/** | ||
* 样式对象 | ||
*/ | ||
style?: CSSProperties; | ||
/** | ||
* CSS类名 | ||
*/ | ||
className?: string; | ||
/** | ||
* 聊天引用 | ||
*/ | ||
chatRef?: ProChatChatReference; | ||
/** | ||
* 输入区域的渲染函数 | ||
* @param defaultDom 默认的 DOM 元素 | ||
* @param onMessageSend 发送消息的回调函数 | ||
* @param onClearAllHistory 清除所有历史记录的回调函数 | ||
* @returns 渲染的 React 元素 | ||
*/ | ||
inputAreaRender?: ProInputAreaProps['inputAreaRender']; | ||
/** | ||
* 输入框的渲染函数 | ||
* @param defaultDom 默认的 DOM 元素 | ||
* @param onMessageSend 发送消息的回调函数 | ||
* @param props 输入框的属性 | ||
*/ | ||
inputRender: ProInputAreaProps['inputRender']; | ||
|
||
/** | ||
* 聊天发送按钮的渲染配置 | ||
* @param defaultDom 默认的 DOM 元素 | ||
* @param defaultProps 默认的属性 | ||
*/ | ||
sendButtonRender?: ProInputAreaProps['sendButtonRender']; | ||
|
||
/** | ||
* 滚动时候的监听方法 | ||
*/ | ||
onScroll?: (e: Event) => void; | ||
|
||
renderErrorMessages?: ChatListItemProps['renderErrorMessages']; | ||
} | ||
|
||
const App = memo<ConversationProps>( | ||
({ | ||
renderInputArea, | ||
inputAreaRender, | ||
className, | ||
style, | ||
showTitle, | ||
chatRef, | ||
itemShouldUpdate, | ||
inputRender, | ||
chatItemRenderConfig, | ||
backToBottomConfig, | ||
renderErrorMessages, | ||
sendButtonRender, | ||
onScroll, | ||
markdownProps, | ||
}) => { | ||
const ref = useRef<HTMLDivElement>(null); | ||
const areaHtml = useRef<HTMLDivElement>(null); | ||
const { styles, cx } = useStyles(); | ||
const { styles: override } = useOverrideStyles(); | ||
const [isRender, setIsRender] = useState(false); | ||
const [height, setHeight] = useState('100%' as string | number); | ||
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); | ||
const { localeObject } = useProChatLocale(); | ||
|
||
useEffect(() => { | ||
// 保证 ref 永远存在 | ||
setIsRender(true); | ||
if (chatRef?.current) { | ||
chatRef.current.scrollToBottom = () => { | ||
(ref as any)?.current?.scrollTo({ | ||
behavior: 'smooth', | ||
left: 0, | ||
top: ref.current?.scrollHeight || 99999, | ||
}); | ||
}; | ||
} | ||
}, []); | ||
|
||
const prefixClass = getPrefixCls('pro-chat'); | ||
return ( | ||
<RcResizeObserver | ||
onResize={(e) => { | ||
if (e.height !== height) { | ||
setHeight(e.height); | ||
} | ||
}} | ||
> | ||
<Flexbox | ||
className={cx(override.container, className, `${prefixClass}-container`)} | ||
style={{ | ||
maxHeight: '100vh', | ||
height: '100%', | ||
...style, | ||
}} | ||
> | ||
<> | ||
<div | ||
ref={ref} | ||
className={cx(`${prefixClass}-chat-list-container`, styles)} | ||
style={{ | ||
height: (height as number) - (areaHtml.current?.clientHeight || 0) || '100%', | ||
}} | ||
> | ||
<ChatList | ||
showTitle={showTitle} | ||
itemShouldUpdate={itemShouldUpdate} | ||
chatItemRenderConfig={chatItemRenderConfig} | ||
markdownProps={markdownProps} | ||
renderErrorMessages={renderErrorMessages} | ||
/> | ||
{ref?.current && <ChatScrollAnchor target={ref} />} | ||
</div> | ||
{isRender && ref?.current ? ( | ||
<BackBottom | ||
style={{ | ||
bottom: 138, | ||
}} | ||
onScroll={onScroll} | ||
target={ref} | ||
text={localeObject.backToBottom} | ||
{...backToBottomConfig} | ||
/> | ||
) : null} | ||
</> | ||
{renderInputArea !== null && inputAreaRender !== null && ( | ||
<div ref={areaHtml}> | ||
{ | ||
<ProInputArea | ||
Check failure on line 168 in src/ProChat/container/AppNew.tsx GitHub Actions / test
Check failure on line 168 in src/ProChat/container/AppNew.tsx GitHub Actions / test
|
||
sendButtonRender={sendButtonRender} | ||
inputAreaRender={inputAreaRender || renderInputArea} | ||
inputRender={inputRender} | ||
sendShortcutKey="enter" | ||
extra={['image', 'audio']} | ||
/> | ||
} | ||
</div> | ||
)} | ||
</Flexbox> | ||
</RcResizeObserver> | ||
); | ||
}, | ||
); | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import ActionIcon from '@/ActionIcon'; | ||
import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; | ||
import { ConfigProvider, Popconfirm } from 'antd'; | ||
import { createStyles, cx } from 'antd-style'; | ||
import { Globe, RotateCw, Trash2 } from 'lucide-react'; | ||
import { Flexbox } from 'react-layout-kit'; | ||
// import { useStore } from '../ProChat/store'; | ||
|
||
const useStyles = createStyles(({ css, token }) => ({ | ||
extra: css` | ||
color: ${token.colorTextTertiary}; | ||
`, | ||
})); | ||
|
||
interface ControlPanelProps { | ||
className?: string; | ||
clearMessage?: () => void; | ||
actionsRender?: (defaultDoms: React.ReactNode[]) => React.ReactNode; | ||
flexConfig?: Record<string, any>; | ||
} | ||
|
||
export const ActionBar = ({ | ||
className, | ||
clearMessage, | ||
actionsRender, | ||
flexConfig, | ||
}: ControlPanelProps) => { | ||
const { localeObject } = useProChatLocale(); | ||
|
||
const { styles, theme } = useStyles(); | ||
const defaultDoms = [ | ||
<Popconfirm | ||
title={localeObject.clearModalTitle} | ||
okButtonProps={{ danger: true }} | ||
okText={localeObject.clearDialogue} | ||
cancelText={localeObject.cancel} | ||
key={'clear'} | ||
onConfirm={() => { | ||
clearMessage(); | ||
}} | ||
> | ||
<ActionIcon title={localeObject.clearCurrentDialogue} icon={Trash2} /> | ||
</Popconfirm>, | ||
<ActionIcon key={'regenerate'} title={localeObject.regenerate} icon={RotateCw} />, | ||
<ActionIcon key={'connectNetwork'} title={localeObject.connectNetwork} icon={Globe} />, | ||
]; | ||
|
||
return ( | ||
<ConfigProvider theme={{ token: { colorText: theme.colorTextSecondary } }}> | ||
<Flexbox | ||
align={'center'} | ||
direction={'horizontal-reverse'} | ||
paddingInline={12} | ||
className={cx(styles.extra, className)} | ||
gap={8} | ||
{...flexConfig} | ||
> | ||
{actionsRender?.(defaultDoms) ?? defaultDoms} | ||
</Flexbox> | ||
</ConfigProvider> | ||
); | ||
}; | ||
|
||
export default ActionBar; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import ActionIcon from '@/ActionIcon'; | ||
import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; | ||
import { ConfigProvider } from 'antd'; | ||
import { createStyles, cx } from 'antd-style'; | ||
import { isObject, isString } from 'lodash-es'; | ||
import { FileVideo, Image } from 'lucide-react'; | ||
import { Flexbox } from 'react-layout-kit'; | ||
import AudioIcon from '../icons/AudioLines'; | ||
|
||
const useStyles = createStyles(({ css, token }) => ({ | ||
extra: css` | ||
color: ${token.colorTextTertiary}; | ||
`, | ||
})); | ||
|
||
export type ExtraType = 'image' | 'audio' | 'video'; | ||
export interface ExtraItem { | ||
type: ExtraType; | ||
onChange?: () => void; | ||
onFinish?: () => void; | ||
render: () => JSX.Element; | ||
} | ||
export type ExtraModelProps = { | ||
className?: string; | ||
extra?: Array<ExtraItem | ExtraType>; | ||
}; | ||
|
||
export const ExtraModel = (props: ExtraModelProps) => { | ||
const { className, extra } = props; | ||
|
||
const { localeObject } = useProChatLocale(); | ||
|
||
const defaultDoms = [ | ||
{ | ||
type: 'video', | ||
render: <ActionIcon key={'video'} title={localeObject.video} icon={FileVideo} />, | ||
}, | ||
{ | ||
type: 'audio', | ||
render: <ActionIcon key={'audio'} title={localeObject.audio} icon={AudioIcon} />, | ||
}, | ||
{ | ||
type: 'image', | ||
render: <ActionIcon key={'image'} title={localeObject.image} icon={Image} />, | ||
}, | ||
]; | ||
|
||
const { styles, theme } = useStyles(); | ||
|
||
const renderContent = () => { | ||
if (!extra || extra.length === 0) { | ||
return null; | ||
} | ||
|
||
const getDefaultRender = (type: ExtraType) => { | ||
const defaultComponent = defaultDoms.find((dom) => dom.type === type); | ||
return defaultComponent ? defaultComponent.render : null; | ||
}; | ||
|
||
return extra.reverse().map((item) => { | ||
if (isString(item)) { | ||
return getDefaultRender(item as ExtraType); | ||
} else if (isObject(item) && 'type' in item && 'render' in item) { | ||
return item.render(); | ||
} else { | ||
return null; | ||
} | ||
}); | ||
}; | ||
renderContent(); | ||
return ( | ||
<ConfigProvider theme={{ token: { colorText: theme.colorTextSecondary } }}> | ||
<Flexbox | ||
align={'center'} | ||
direction={'horizontal-reverse'} | ||
paddingInline={12} | ||
className={cx(styles.extra, className)} | ||
gap={8} | ||
> | ||
{renderContent()} | ||
</Flexbox> | ||
</ConfigProvider> | ||
); | ||
}; | ||
|
||
export default ExtraModel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Input } from 'antd'; | ||
import { TextAreaProps } from 'antd/es/input'; | ||
import { TextAreaRef } from 'antd/es/input/TextArea'; | ||
import React from 'react'; | ||
|
||
export const ProTextArea: React.FC<TextAreaProps> = React.forwardRef<TextAreaRef, TextAreaProps>( | ||
(props, ref) => { | ||
const { disabled, ...rest } = props; | ||
|
||
return ( | ||
<Input.TextArea | ||
size="large" | ||
{...rest} | ||
ref={ref} | ||
disabled={disabled} | ||
className={`${props.className}-textarea`} | ||
onFocus={(e) => { | ||
props.onFocus?.(e); | ||
}} | ||
onPressEnter={(e) => { | ||
props.onPressEnter?.(e); | ||
}} | ||
/> | ||
); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { memo } from 'react'; | ||
|
||
const AudioIcon = memo(() => { | ||
return ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeWidth="2" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
className="lucide lucide-audio-lines" | ||
> | ||
<path d="M2 10v3" /> | ||
<path d="M6 6v11" /> | ||
<path d="M10 3v18" /> | ||
<path d="M14 8v7" /> | ||
<path d="M18 5v13" /> | ||
<path d="M22 10v3" /> | ||
</svg> | ||
); | ||
}); | ||
export default AudioIcon; |
Oops, something went wrong.