Skip to content

Commit

Permalink
✨ feat: add ProChat
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Oct 20, 2023
1 parent 78e6bd7 commit ec20b42
Show file tree
Hide file tree
Showing 79 changed files with 4,445 additions and 19 deletions.
31 changes: 31 additions & 0 deletions src/ProChat/components/ChatList/Actions/Assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ActionIconGroup from '@/ActionIconGroup';
import { RenderAction } from '@/ChatList';
import { useChatListActionsBar } from '@/hooks/useChatListActionsBar';
import { memo } from 'react';

import { ErrorActionsBar } from './Error';

export const AssistantActionsBar: RenderAction = memo(({ text, id, onActionClick, error }) => {
const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);

if (id === 'default') return;

if (error) return <ErrorActionsBar onActionClick={onActionClick} text={text} />;

return (
<ActionIconGroup
dropdownMenu={[
edit,
copy,
regenerate,
divider,
// TODO: need a translate
divider,
del,
]}
items={[regenerate, copy]}
onActionClick={onActionClick}
type="ghost"
/>
);
});
10 changes: 10 additions & 0 deletions src/ProChat/components/ChatList/Actions/Error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ActionIconGroup from '@/ActionIconGroup';
import { ActionsBarProps } from '@/ChatList/ActionsBar';
import { useChatListActionsBar } from '@/hooks/useChatListActionsBar';
import { memo } from 'react';

export const ErrorActionsBar = memo<ActionsBarProps>(({ text, onActionClick }) => {
const { regenerate, del } = useChatListActionsBar(text);

return <ActionIconGroup items={[regenerate, del]} onActionClick={onActionClick} type="ghost" />;
});
10 changes: 10 additions & 0 deletions src/ProChat/components/ChatList/Actions/Fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useChatListActionsBar } from '@/hooks/useChatListActionsBar';
import { ActionIconGroup, RenderAction } from '@lobehub/ui';
import { memo } from 'react';

export const DefaultActionsBar: RenderAction = memo(({ text, onActionClick }) => {
const { del } = useChatListActionsBar(text);
return (
<ActionIconGroup dropdownMenu={[del]} items={[]} onActionClick={onActionClick} type="ghost" />
);
});
15 changes: 15 additions & 0 deletions src/ProChat/components/ChatList/Actions/Function.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useChatListActionsBar } from '@/hooks/useChatListActionsBar';
import { ActionIconGroup, RenderAction } from '@lobehub/ui';
import { memo } from 'react';

export const FunctionActionsBar: RenderAction = memo(({ text, onActionClick }) => {
const { regenerate, divider, del } = useChatListActionsBar(text);
return (
<ActionIconGroup
dropdownMenu={[regenerate, divider, del]}
items={[regenerate]}
onActionClick={onActionClick}
type="ghost"
/>
);
});
24 changes: 24 additions & 0 deletions src/ProChat/components/ChatList/Actions/User.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useChatListActionsBar } from '@/hooks/useChatListActionsBar';
import { ActionIconGroup, RenderAction } from '@lobehub/ui';
import { memo } from 'react';

export const UserActionsBar: RenderAction = memo(({ text, onActionClick }) => {
const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);

return (
<ActionIconGroup
dropdownMenu={[
edit,
copy,
regenerate,
divider,
// TODO: need a translate
divider,
del,
]}
items={[regenerate, edit]}
onActionClick={onActionClick}
type="ghost"
/>
);
});
13 changes: 13 additions & 0 deletions src/ProChat/components/ChatList/Actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ChatListProps } from '@lobehub/ui';

import { AssistantActionsBar } from './Assistant';
import { DefaultActionsBar } from './Fallback';
import { FunctionActionsBar } from './Function';
import { UserActionsBar } from './User';

export const renderActions: ChatListProps['renderActions'] = {
assistant: AssistantActionsBar,
function: FunctionActionsBar,
system: DefaultActionsBar,
user: UserActionsBar,
};
30 changes: 30 additions & 0 deletions src/ProChat/components/ChatList/Extras/Assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Tag from '@/components/Tag';
import { RenderMessageExtra } from '@/index';

import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useChatStore } from '@/ProChat/store';
import { agentSelectors } from '@/ProChat/store/selectors';

export const AssistantMessageExtra: RenderMessageExtra = memo(({ extra }) => {
const model = useChatStore(agentSelectors.currentAgentModel);

const showModelTag = extra?.fromModel && model !== extra?.fromModel;
const hasTranslate = !!extra?.translate;

const showExtra = showModelTag || hasTranslate;

if (!showExtra) return;

return (
<Flexbox gap={8} style={{ marginTop: 8 }}>
{showModelTag && (
<div>
{/*TODO: need a model icons */}
<Tag>{extra?.fromModel as string}</Tag>
</div>
)}
</Flexbox>
);
});
18 changes: 18 additions & 0 deletions src/ProChat/components/ChatList/Extras/User.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RenderMessageExtra } from '@/index';
import { Divider } from 'antd';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

export const UserMessageExtra: RenderMessageExtra = memo(({ extra }) => {
const hasTranslate = !!extra?.translate;

return (
<Flexbox gap={8} style={{ marginTop: hasTranslate ? 8 : 0 }}>
{extra?.translate && (
<div>
<Divider style={{ margin: '12px 0' }} />
</div>
)}
</Flexbox>
);
});
9 changes: 9 additions & 0 deletions src/ProChat/components/ChatList/Extras/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChatListProps } from '@lobehub/ui';

import { AssistantMessageExtra } from './Assistant';
import { UserMessageExtra } from './User';

export const renderMessagesExtra: ChatListProps['renderMessagesExtra'] = {
assistant: AssistantMessageExtra,
user: UserMessageExtra,
};
53 changes: 53 additions & 0 deletions src/ProChat/components/ChatList/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useTheme } from 'antd-style';

const Svg = () => (
<svg viewBox="0 0 32 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="0" cy="12" r="0" transform="translate(8 0)">
<animate
attributeName="r"
begin="0"
calcMode="spline"
dur="1.2s"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.6 0.4 0.8"
keyTimes="0;0.2;0.7;1"
repeatCount="indefinite"
values="0; 4; 0; 0"
/>
</circle>
<circle cx="0" cy="12" r="0" transform="translate(16 0)">
<animate
attributeName="r"
begin="0.3"
calcMode="spline"
dur="1.2s"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.6 0.4 0.8"
keyTimes="0;0.2;0.7;1"
repeatCount="indefinite"
values="0; 4; 0; 0"
/>
</circle>
<circle cx="0" cy="12" r="0" transform="translate(24 0)">
<animate
attributeName="r"
begin="0.6"
calcMode="spline"
dur="1.2s"
keySplines="0.2 0.2 0.4 0.8;0.2 0.6 0.4 0.8;0.2 0.6 0.4 0.8"
keyTimes="0;0.2;0.7;1"
repeatCount="indefinite"
values="0; 4; 0; 0"
/>
</circle>
</svg>
);

const BubblesLoading = () => {
const { colorTextTertiary } = useTheme();
return (
<div style={{ fill: colorTextTertiary, height: 24, width: 32 }}>
<Svg />
</div>
);
};

export default BubblesLoading;
9 changes: 9 additions & 0 deletions src/ProChat/components/ChatList/Messages/Assistant.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { RenderMessage } from '@lobehub/ui';
import { memo } from 'react';

import { DefaultMessage } from './Default';

export const AssistantMessage: RenderMessage = memo(({ id, content, ...props }) => {
// todo: need a custom render
return <DefaultMessage content={content} id={id} {...props} />;
});
12 changes: 12 additions & 0 deletions src/ProChat/components/ChatList/Messages/Default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RenderMessage } from '@lobehub/ui';
import { memo } from 'react';

import { LOADING_FLAT } from '@/ProChat/const/message';

import BubblesLoading from '../Loading';

export const DefaultMessage: RenderMessage = memo(({ id, editableContent, content }) => {
if (content === LOADING_FLAT) return <BubblesLoading />;

return <div id={id}>{editableContent}</div>;
});
9 changes: 9 additions & 0 deletions src/ProChat/components/ChatList/Messages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ChatListProps } from '@lobehub/ui';

import { AssistantMessage } from './Assistant';
import { DefaultMessage } from './Default';

export const renderMessages: ChatListProps['renderMessages'] = {
assistant: AssistantMessage,
default: DefaultMessage,
};
136 changes: 136 additions & 0 deletions src/ProChat/components/ChatList/OTPInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useControllableValue } from 'ahooks';
import { createStyles } from 'antd-style';
import React, { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

const useStyles = createStyles(
({ css, token }) => css`
width: ${token.controlHeight}px;
height: ${token.controlHeight}px;
font-size: 16px;
color: ${token.colorText};
text-align: center;
background: ${token.colorBgContainer};
border: 1px solid ${token.colorBorder};
border-radius: 8px;
&:focus,
&:focus-visible {
border-color: ${token.colorPrimary};
outline: none;
}
`,
);

/**
* Let's borrow some props from HTML "input". More info below:
*
* [Pick Documentation](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys)
*
* [How to extend HTML Elements](https://reacthustle.com/blog/how-to-extend-html-elements-in-react-typescript)
*/
type PartialInputProps = Pick<React.ComponentPropsWithoutRef<'input'>, 'className' | 'style'>;

interface OtpInputProps extends PartialInputProps {
onChange?: (value: string) => void;
/**
* Number of characters/input for this component
*/
size?: number;
/**
* Validation pattern for each input.
* e.g: /[0-9]{1}/ for digits only or /[0-9a-zA-Z]{1}/ for alphanumeric
*/
validationPattern?: RegExp;
/**
* full value of the otp input, up to {size} characters
*/
value?: string;
}

const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
const current = e.currentTarget;
if (e.key === 'ArrowLeft' || e.key === 'Backspace') {
const prev = current.previousElementSibling as HTMLInputElement | null;
prev?.focus();
prev?.setSelectionRange(0, 1);
return;
}

if (e.key === 'ArrowRight') {
const prev = current.nextSibling as HTMLInputElement | null;
prev?.focus();
prev?.setSelectionRange(0, 1);
return;
}
};

const OtpInput = memo<OtpInputProps>((props) => {
const {
//Set the default size to 6 characters
size = 6,
//Default validation is digits
validationPattern = /\d/,
value: outerValue,
onChange,
className,
...restProps
} = props;
const [value, setValue] = useControllableValue({ onChange, value: outerValue });

const { styles, cx } = useStyles();
// Create an array based on the size.
const arr = Array.from({ length: size }).fill('-');

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
const elem = e.target;
const val = e.target.value;

// check if the value is valid
if (!validationPattern.test(val) && val !== '') return;

// change the value using onChange props
const valueArr = value?.split('') || [];
valueArr[index] = val;
const newVal = valueArr.join('').slice(0, 6);
setValue(newVal);

//focus the next element if there's a value
if (val) {
const next = elem.nextElementSibling as HTMLInputElement | null;
next?.focus();
}
};

const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const val = e.clipboardData.getData('text').slice(0, Math.max(0, size));

setValue(val);
};

return (
<Flexbox gap={12} horizontal>
{arr.map((_, index) => {
return (
<input
key={index}
{...restProps}
className={cx(styles, className)}
maxLength={6}
onChange={(e) => handleInputChange(e, index)}
onKeyUp={handleKeyUp}
onPaste={handlePaste}
pattern={validationPattern.source}
type="text"
value={value?.at(index) ?? ''}
/>
);
})}
</Flexbox>
);
});

export default OtpInput;
Loading

0 comments on commit ec20b42

Please sign in to comment.