From a9cf4730f89ff93fa7dcc2b5db5aeac143a9785f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=BE=E6=AD=8C?= Date: Mon, 17 Jun 2024 13:56:23 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20language=20supp?= =?UTF-8?q?ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locale/en-US.ts | 2 ++ src/locale/zh-CN.ts | 3 ++- src/locale/zh-HK.ts | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 960d9d6..5d794d1 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -5,6 +5,7 @@ export default { clearDialogue: 'Clear dialogue', clearModalTitle: 'You are about to clear the session, and you will not be able to retrieve it after clearing. Do you want to clear the current session?', + connectNetwork: 'Connect to network', defaultHelloMessage: 'Let us start chatting', cancel: 'Cancel', confirm: 'Confirm', @@ -14,4 +15,5 @@ export default { edit: 'Edit', history: 'History', regenerate: 'Regenerate', + refresh: 'Refresh', }; diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 10580e9..08d3821 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -2,7 +2,7 @@ export default { placeholder: '请输入消息...', backToBottom: '返回底部', clearCurrentDialogue: '清空当前对话', - clearDialogue: '清空对话', + connectNetwork: '是否连接网络', clearModalTitle: '你即将要清空会话,清空后将无法找回。是否清空当前会话?', defaultHelloMessage: '让我们开始对话吧', cancel: '取消', @@ -13,4 +13,5 @@ export default { edit: '编辑', history: '历史范围', regenerate: '重新生成', + refresh: '刷新', }; diff --git a/src/locale/zh-HK.ts b/src/locale/zh-HK.ts index e3ba0d1..517a6fe 100644 --- a/src/locale/zh-HK.ts +++ b/src/locale/zh-HK.ts @@ -4,6 +4,7 @@ export default { clearCurrentDialogue: '清除當前對話', clearDialogue: '清除對話', clearModalTitle: '您即將清除會話,清除後將無法恢復。您確定要清除當前會話嗎?', + connectNetwork: '是否連接網絡', defaultHelloMessage: '讓我們開始聊天吧', cancel: '取消', confirm: '確認', @@ -13,4 +14,5 @@ export default { edit: '編輯', history: '歷史', regenerate: '重新生成', + refresh: '刷新', }; From 4c97c443acb61502677b9499bbebc28e6ea0c52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=BE=E6=AD=8C?= Date: Mon, 17 Jun 2024 17:16:23 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E2=9C=A8=20feat:=20=20develop=20proInput?= =?UTF-8?q?Area?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ProInputArea/ControlPanel.tsx | 60 ++++ .../components/ProInputArea/ExtraModel.tsx | 60 ++++ .../components/ProInputArea/ProTextArea.tsx | 26 ++ .../components/ProInputArea/StopLoading.tsx | 38 +++ src/ProChat/components/ProInputArea/index.tsx | 274 ++++++++++++++++++ src/ProChat/container/App.tsx | 11 +- 6 files changed, 464 insertions(+), 5 deletions(-) create mode 100644 src/ProChat/components/ProInputArea/ControlPanel.tsx create mode 100644 src/ProChat/components/ProInputArea/ExtraModel.tsx create mode 100644 src/ProChat/components/ProInputArea/ProTextArea.tsx create mode 100644 src/ProChat/components/ProInputArea/StopLoading.tsx create mode 100644 src/ProChat/components/ProInputArea/index.tsx diff --git a/src/ProChat/components/ProInputArea/ControlPanel.tsx b/src/ProChat/components/ProInputArea/ControlPanel.tsx new file mode 100644 index 0000000..cf5f896 --- /dev/null +++ b/src/ProChat/components/ProInputArea/ControlPanel.tsx @@ -0,0 +1,60 @@ +import { createStyles, cx } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; + +import ActionIcon from '@/ActionIcon'; +import { ConfigProvider, Popconfirm } from 'antd'; +import { Globe, RotateCw, Trash2 } from 'lucide-react'; + +import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; +import { useStore } from '../../store'; + +const useStyles = createStyles(({ css, token }) => ({ + extra: css` + color: ${token.colorTextTertiary}; + `, +})); + +export const ActionBar = ({ className }: { className?: string }) => { + const [clearMessage, actionsRender, flexConfig] = useStore((s) => [ + s.clearMessage, + s.actions?.render, + s.actions?.flexConfig, + ]); + + const { localeObject } = useProChatLocale(); + + const { styles, theme } = useStyles(); + const defaultDoms = [ + { + clearMessage(); + }} + > + + , + , + , + ]; + + return ( + + + {actionsRender?.(defaultDoms) ?? defaultDoms} + + + ); +}; + +export default ActionBar; diff --git a/src/ProChat/components/ProInputArea/ExtraModel.tsx b/src/ProChat/components/ProInputArea/ExtraModel.tsx new file mode 100644 index 0000000..cf5f896 --- /dev/null +++ b/src/ProChat/components/ProInputArea/ExtraModel.tsx @@ -0,0 +1,60 @@ +import { createStyles, cx } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; + +import ActionIcon from '@/ActionIcon'; +import { ConfigProvider, Popconfirm } from 'antd'; +import { Globe, RotateCw, Trash2 } from 'lucide-react'; + +import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; +import { useStore } from '../../store'; + +const useStyles = createStyles(({ css, token }) => ({ + extra: css` + color: ${token.colorTextTertiary}; + `, +})); + +export const ActionBar = ({ className }: { className?: string }) => { + const [clearMessage, actionsRender, flexConfig] = useStore((s) => [ + s.clearMessage, + s.actions?.render, + s.actions?.flexConfig, + ]); + + const { localeObject } = useProChatLocale(); + + const { styles, theme } = useStyles(); + const defaultDoms = [ + { + clearMessage(); + }} + > + + , + , + , + ]; + + return ( + + + {actionsRender?.(defaultDoms) ?? defaultDoms} + + + ); +}; + +export default ActionBar; diff --git a/src/ProChat/components/ProInputArea/ProTextArea.tsx b/src/ProChat/components/ProInputArea/ProTextArea.tsx new file mode 100644 index 0000000..5d816a1 --- /dev/null +++ b/src/ProChat/components/ProInputArea/ProTextArea.tsx @@ -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 = React.forwardRef( + (props, ref) => { + const { disabled, ...rest } = props; + + return ( + { + props.onFocus?.(e); + }} + onPressEnter={(e) => { + props.onPressEnter?.(e); + }} + /> + ); + }, +); diff --git a/src/ProChat/components/ProInputArea/StopLoading.tsx b/src/ProChat/components/ProInputArea/StopLoading.tsx new file mode 100644 index 0000000..450b982 --- /dev/null +++ b/src/ProChat/components/ProInputArea/StopLoading.tsx @@ -0,0 +1,38 @@ +import { useTheme } from 'antd-style'; +import { memo } from 'react'; + +const StopLoadingIcon = memo(() => { + const theme = useTheme(); + return ( + + + + + + + + + + ); +}); +export default StopLoadingIcon; diff --git a/src/ProChat/components/ProInputArea/index.tsx b/src/ProChat/components/ProInputArea/index.tsx new file mode 100644 index 0000000..e749cf2 --- /dev/null +++ b/src/ProChat/components/ProInputArea/index.tsx @@ -0,0 +1,274 @@ +import useProChatLocale from '@/ProChat/hooks/useProChatLocale'; +import { SendOutlined } from '@ant-design/icons'; +import { Button, ButtonProps, ConfigProvider } from 'antd'; +import { createStyles, cx } from 'antd-style'; +import { TextAreaProps } from 'antd/es/input'; +import { ReactNode, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { Flexbox } from 'react-layout-kit'; +import { useStore } from '../../store'; +import ControlPanel from './ControlPanel'; +import { ProTextArea } from './ProTextArea'; +import StopLoadingIcon from './StopLoading'; +const ENTER = 'enter'; +const SHIFT_ENTER = 'shiftEnter'; +const useStyles = createStyles(({ css, responsive, token }) => ({ + container: css` + position: sticky; + z-index: ${token.zIndexPopupBase}; + bottom: 0; + + padding-top: 12px; + padding-bottom: 24px; + + background-image: linear-gradient(to top, ${token.colorBgLayout} 88%, transparent 100%); + + ${responsive.mobile} { + width: 100%; + } + `, + boxShadow: css` + position: relative; + border-radius: 8px; + box-shadow: ${token.boxShadowSecondary}; + `, + input: css` + width: 100%; + border: none; + outline: none; + border-radius: 8px; + `, + btn: css` + position: absolute; + z-index: 10; + right: 8px; + bottom: 6px; + + color: ${token.colorTextTertiary}; + &:hover { + color: ${token.colorTextSecondary}; + } + `, + extra: css` + color: ${token.colorTextTertiary}; + `, +})); +export enum ExtraType { + Image = 'image', + Voice = 'voice', + Video = 'video', +} +export interface ExtraItem { + type: ExtraType; + onChange?: () => void; + onFinish?: () => void; + render: () => JSX.Element; +} +export type ProInputAreaProps = { + className?: string; + extra?: Array; + sendShortcutKey?: 'enter' | 'shiftEnter'; + onSend?: (message: string) => boolean | Promise; + sendButtonRender?: (defaultDom: ReactNode, defaultProps: ButtonProps) => ReactNode; + inputRender?: ( + defaultDom: ReactNode, + onMessageSend: (message: string) => void | Promise, + defaultProps: TextAreaProps, + ) => ReactNode; + inputAreaRender?: ( + defaultDom: ReactNode, + onMessageSend: (message: string) => void | Promise, + onClearAllHistory: () => void, + ) => ReactNode; +}; + +export const ProInputArea = (props: ProInputAreaProps) => { + // 拿到 Props 中的 需求 + const { className, onSend, inputAreaRender, inputRender, sendButtonRender, sendShortcutKey } = + props || {}; + // 拿到 本地仓库 透出的 方法 + const [sendMessage, isLoading, placeholder, inputAreaProps, clearMessage, stopGenerateMessage] = + useStore((s) => [ + s.sendMessage, + !!s.chatLoadingId, + s.placeholder, + s.inputAreaProps, + s.clearMessage, + s.stopGenerateMessage, + ]); + // 可以用 getPrefixCls 设置全局的样式前缀 + const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); + const isChineseInput = useRef(false); + // 配置全局主题 + const { styles, theme } = useStyles(); + // 配置全局的国际化 + const { localeObject } = useProChatLocale(); + + const { value, onChange } = inputAreaProps || {}; + const [message, setMessage] = useState(''); + const [currentShortcutKey] = useState(sendShortcutKey || 'enter'); + // 兼容中文的受控输入逻辑 + useEffect(() => { + if (!isChineseInput.current && onChange) { + onChange(message); + } + }, [message]); + + useEffect(() => { + if (value) { + setMessage(value); + } + }, [value]); + + const send = async () => { + if (onSend) { + const success = await onSend(message); + if (success) { + sendMessage(message); + setMessage(''); + } + } else { + sendMessage(message); + setMessage(''); + } + }; + + const prefixClass = getPrefixCls('pro-chat-input-area'); + + /** + * 默认的自动完成文本区域属性 + * + * @property {string} placeholder - 输入框的占位符 + * @property {Object} inputAreaProps - 输入框的其他属性 + * @property {string} inputAreaProps.className - 输入框的类名 + * @property {string} prefixClass - 输入框的前缀类名 + * @property {string} value - 输入框的值 + * @property {function} onChange - 输入框值改变时的回调函数 + * @property {Object} autoSize - 输入框的自动调整大小配置 + * @property {number} autoSize.maxRows - 输入框的最大行数 + * @property {function} onCompositionStart - 输入法开始输入时的回调函数 + * @property {function} onCompositionEnd - 输入法结束输入时的回调函数 + * @property {function} onPressEnter - 按下回车键时的回调函数 + */ + + const defaultProTextAreaProps = { + placeholder: placeholder || localeObject.placeholder, + ...inputAreaProps, + className: cx(styles.input, inputAreaProps?.className, `${prefixClass}-component`), + value: message, + onChange: (e) => { + setMessage(e.target.value); + }, + autoSize: { maxRows: 8 }, + onCompositionStart: () => { + isChineseInput.current = true; + }, + onCompositionEnd: (e) => { + isChineseInput.current = false; + setMessage(e.target.value); + }, + onPressEnter: (e) => { + if (currentShortcutKey === ENTER) { + if (!isLoading && !e.shiftKey && !isChineseInput.current) { + e.preventDefault(); + send(); + } + } else if (currentShortcutKey === SHIFT_ENTER) { + if (!isLoading && e.shiftKey && !isChineseInput.current) { + e.preventDefault(); + send(); + } + } + }, + }; + + // 默认输入框 + const defaultInput = ; + + /** + * 支持下自定义输入框 + */ + const inputDom = inputRender + ? inputRender?.( + defaultInput, + (message) => { + sendMessage(message); + }, + defaultProTextAreaProps, + ) + : defaultInput; + + /** + * 根据 isLoading 状态返回默认的按钮道具。 + * 如果 isLoading 为 true,则按钮将具有文本类型,即 stopGenerateMessage 点击处理程序, + * 和 StopLoadingIcon 作为图标。 + * 如果 isLoading 为 false,则按钮将具有文本类型、发送点击处理程序、 + * 和 SendOutlined 图标作为图标。 + * @returns The default button props. + */ + const defaultButtonProps = useMemo(() => { + return isLoading + ? ({ + type: 'text', + className: styles.btn, + onClick: () => stopGenerateMessage(), + icon: , + } as const) + : ({ + type: 'text', + className: styles.btn, + onClick: () => send(), + icon: , + } as const); + }, [isLoading, message]); + + // 默认按钮 + const defaultButtonDom = +
+
+
+
+ + +
+
+ + + +
+
+
+