Skip to content

Commit

Permalink
✨ feat: add ChatInputArea
Browse files Browse the repository at this point in the history
  • Loading branch information
canisminor1990 committed Jun 16, 2023
1 parent 31849d6 commit b1303f1
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 44 deletions.
36 changes: 36 additions & 0 deletions src/ChatInputArea/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ActionIcon, ChatInputArea, DraggablePanel, Icon, TokenTag } from '@lobehub/ui';
import { Button } from 'antd';
import { Archive, Eraser, Languages } from 'lucide-react';
import { useState } from 'react';
import styled from 'styled-components';

const View = styled.div`
position: relative;
display: flex;
flex-direction: column;
height: 400px;
`;

export default () => {
const [expand, setExpand] = useState<boolean>(false);
return (
<View>
<div style={{ flex: 1 }}></div>
<DraggablePanel expandable={false} fullscreen={expand} minHeight={200} placement="bottom">
<ChatInputArea
actions={
<>
<ActionIcon icon={Languages} />
<ActionIcon icon={Eraser} />
<TokenTag maxValue={5000} value={1000} />
</>
}
expand={expand}
footer={<Button icon={<Icon icon={Archive} />} />}
minHeight={200}
onExpandChange={setExpand}
/>
</DraggablePanel>
</View>
);
};
13 changes: 13 additions & 0 deletions src/ChatInputArea/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
nav: Components
group: Chat
title: ChatInputArea
---

## Default

<code src="./demos/index.tsx" nopadding></code>

## APIs

<API></API>
104 changes: 104 additions & 0 deletions src/ChatInputArea/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Button } from 'antd';
import { Maximize2, Minimize2 } from 'lucide-react';
import { ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';

import { ActionIcon, TextArea } from '@/index';
import type { DivProps } from '@/types';

import { useStyles } from './style';

export interface ChatInputAreaProps extends DivProps {
actions?: ReactNode;
defaultValue?: string;
disabled?: boolean;
expand?: boolean;
footer?: ReactNode;
loading?: boolean;
minHeight?: number;
onExpandChange?: (expand: boolean) => void;
onInputChange?: (value: string) => void;
onSend?: (value: string) => void;
placeholder?: string;
}

const ChatInputArea = memo<ChatInputAreaProps>(
({
minHeight = 200,
className,
actions,
footer,
expand,
placeholder = 'Type something to chat...',
onExpandChange,
onSend,
defaultValue = '',
loading,
style,
disabled,
onInputChange,
...props
}) => {
const isChineseInput = useRef(false);
const [value, setValue] = useState<string>(defaultValue);
const { cx, styles } = useStyles();

const handleExpandClick = useCallback(() => {
if (onExpandChange) onExpandChange(!expand);
}, [expand]);

const handleSend = useCallback(() => {
if (disabled) return;
if (onSend) onSend(value);
setValue('');
}, [disabled, value]);

useEffect(() => {
if (onInputChange) onInputChange(value);
}, [value]);

return (
<section
className={cx(styles.container, className)}
style={{ minHeight, ...style }}
{...props}
>
<div className={styles.actionsBar}>
<div className={styles.actionLeft}>{actions}</div>
<div className={styles.actionsRight}>
<ActionIcon icon={expand ? Minimize2 : Maximize2} onClick={handleExpandClick} />
</div>
</div>
<TextArea
className={styles.textarea}
defaultValue={defaultValue}
onBlur={(e) => setValue(e.target.value)}
onChange={(e) => setValue(e.target.value)}
onCompositionEnd={() => {
isChineseInput.current = false;
}}
onCompositionStart={() => {
isChineseInput.current = true;
}}
onPressEnter={(e) => {
if (!loading && !e.shiftKey && !isChineseInput.current) {
e.preventDefault();
handleSend();
}
}}
placeholder={placeholder}
resize={false}
type="pure"
value={value}
/>
<div className={styles.footerBar}>
{footer}
<Button disabled={disabled} loading={loading} onClick={handleSend} type="primary">
Send
</Button>
</div>
</section>
);
},
);

export default ChatInputArea;
51 changes: 51 additions & 0 deletions src/ChatInputArea/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(({ css }) => {
return {
container: css`
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
padding: 12px 0 16px;
`,
actionsBar: css`
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
padding: 0 16px;
`,
actionLeft: css`
display: flex;
flex: 1;
gap: 4px;
align-items: center;
justify-content: flex-start;
`,
actionsRight: css`
display: flex;
flex: 0;
gap: 4px;
align-items: center;
justify-content: flex-end;
`,
textarea: css`
flex: 1;
padding: 0 24px;
`,
footerBar: css`
display: flex;
flex: none;
gap: 8px;
align-items: center;
justify-content: flex-end;
padding: 0 24px;
`,
};
});
15 changes: 11 additions & 4 deletions src/DraggablePanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ export interface DraggablePanelProps extends DivProps {
* @default true
*/
expandable?: boolean;
fullscreen?: boolean;
/**
* @description The style of the panel handler
* @type CSSProperties
*/
hanlderStyle?: React.CSSProperties;
maxHeight?: number;
maxWidth?: number;
/**
* @description The minimum height of the panel
Expand Down Expand Up @@ -107,6 +109,8 @@ export interface DraggablePanelProps extends DivProps {

const DraggablePanel = memo<DraggablePanelProps>(
({
fullscreen,
maxHeight,
pin = 'true',
mode = 'fixed',
children,
Expand Down Expand Up @@ -134,7 +138,7 @@ const DraggablePanel = memo<DraggablePanelProps>(
const isHovering = useHover(ref);
const isVertical = placement === 'top' || placement === 'bottom';

const { styles, cx } = useStyles('draggable-panel');
const { styles, cx } = useStyles();

const [isExpand, setIsExpand] = useControlledState(defaultExpand, {
value: expand,
Expand Down Expand Up @@ -193,9 +197,10 @@ const DraggablePanel = memo<DraggablePanelProps>(

const sizeProps = isExpand
? {
minWidth: typeof minWidth === 'number' ? Math.max(minWidth, 0) : 280,
minHeight: typeof minHeight === 'number' ? Math.max(minHeight, 0) : undefined,
maxWidth: typeof maxWidth === 'number' ? Math.max(maxWidth, 0) : undefined,
minWidth: typeof minWidth === 'number' ? Math.max(minWidth, 0) : minWidth,
minHeight: typeof minHeight === 'number' ? Math.max(minHeight, 0) : minHeight,
maxWidth: typeof maxWidth === 'number' ? Math.max(maxWidth, 0) : maxWidth,
maxHeight: typeof maxHeight === 'number' ? Math.max(maxHeight, 0) : maxHeight,
defaultSize,
size: size as Size,
}
Expand Down Expand Up @@ -273,6 +278,8 @@ const DraggablePanel = memo<DraggablePanelProps>(
</Resizable>
);

if (fullscreen) return <div className={cx(styles.fullscreen, className)}>{children}</div>;

return (
<aside
className={cx(
Expand Down
9 changes: 8 additions & 1 deletion src/DraggablePanel/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { createStyles, css, cx } from 'antd-style';
const offset = 16;
const toggleLength = 40;
const toggleShort = 16;
const prefix = 'draggable-panel';

export const useStyles = createStyles(({ token }, prefix: string) => {
export const useStyles = createStyles(({ token }) => {
const commonHandle = css`
position: relative;
Expand Down Expand Up @@ -262,5 +263,11 @@ export const useStyles = createStyles(({ token }, prefix: string) => {
}
`,
),
fullscreen: css`
position: absolute;
inset: 0;
width: 100%;
height: 100%;
`,
};
});
2 changes: 1 addition & 1 deletion src/Input/demos/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default () => {
placeholder: 'Type keywords...',
type: {
value: 'ghost',
options: ['ghost', 'block'],
options: ['ghost', 'block', 'pure'],
},
},
{ store },
Expand Down
2 changes: 1 addition & 1 deletion src/Input/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default () => {
placeholder: 'Type keywords...',
type: {
value: 'ghost',
options: ['ghost', 'block'],
options: ['ghost', 'block', 'pure'],
},
},
{ store },
Expand Down
26 changes: 21 additions & 5 deletions src/Input/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,42 @@ import { useStyles } from './style';

export interface InputProps extends AntdInputProps {
ref?: any;
type?: 'ghost' | 'block';
type?: 'ghost' | 'block' | 'pure';
}

export const Input = memo<InputProps>(
forwardRef(({ className, type = 'ghost', ...props }, ref) => {
const { styles, cx } = useStyles({ type });

return <AntInput className={cx(styles.input, className)} ref={ref} {...props} />;
return (
<AntInput
bordered={type !== 'pure'}
className={cx(styles.input, className)}
ref={ref}
{...props}
/>
);
}),
);

export interface TextAreaProps extends AntdTextAreaProps {
ref?: any;
type?: 'ghost' | 'block';
resize?: boolean;
type?: 'ghost' | 'block' | 'pure';
}

export const TextArea = memo<TextAreaProps>(
forwardRef(({ className, type = 'ghost', ...props }, ref) => {
forwardRef(({ className, type = 'ghost', resize = true, style, ...props }, ref) => {
const { styles, cx } = useStyles({ type });

return <AntInput.TextArea className={cx(styles.textarea, className)} ref={ref} {...props} />;
return (
<AntInput.TextArea
bordered={type !== 'pure'}
className={cx(styles.textarea, className)}
ref={ref}
style={resize ? style : { resize: 'none', ...style }}
{...props}
/>
);
}),
);
Loading

1 comment on commit b1303f1

@vercel
Copy link

@vercel vercel bot commented on b1303f1 Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

lobe-ui – ./

lobe-ui-lobehub.vercel.app
lobe-ui.vercel.app
ui.lobehub.com
lobe-ui-git-master-lobehub.vercel.app

Please sign in to comment.