Skip to content

Commit

Permalink
✨ feat: 支持流式响应
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Oct 21, 2023
1 parent 86e8283 commit 4e6d720
Show file tree
Hide file tree
Showing 25 changed files with 362 additions and 69 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ screenshot
example/.temp/*
.eslintcache
techUI*
.vercel
28 changes: 28 additions & 0 deletions api/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { OpenAIStream, StreamingTextResponse } from 'ai';
import OpenAI from 'openai';

export const config = {
runtime: 'edge',
};

export default async (req: Request) => {
const openai = new OpenAI();
const payload = (await req.json()) as OpenAIChatStreamPayload;
const { messages, ...params } = payload;

const formatMessages = messages.map((m) => ({
content: m.content,
name: m.name,
role: m.role,
}));
const response = await openai.chat.completions.create(
{
messages: formatMessages,
...params,
stream: true,
},
{ headers: { Accept: '*/*' } },
);
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
};
9 changes: 9 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "api",
"version": "0.0.0",
"description": "test openai api for pro chat",
"dependencies": {
"ai": "^2",
"openai": "^4"
}
}
9 changes: 9 additions & 0 deletions api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"strict": false,
"declaration": false,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"release": "semantic-release",
"setup": "dumi setup",
"start": "dumi dev",
"start-with-api": "vercel dev",
"test": "vitest --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"test:update": "vitest -u",
Expand Down Expand Up @@ -127,6 +128,7 @@
"semantic-release-config-gitmoji": "^1",
"stylelint": "^15",
"typescript": "^5",
"vercel": "^32",
"vitest": "latest"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/ProChat/components/ChatList/Actions/Assistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const AssistantActionsBar: RenderAction = memo(({ text, id, onActionClick
edit,
copy,
regenerate,
divider,
// divider,
// TODO: need a translate
divider,
del,
Expand Down
2 changes: 1 addition & 1 deletion src/ProChat/components/ChatList/Actions/User.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const UserActionsBar: RenderAction = memo(({ text, onActionClick }) => {
edit,
copy,
regenerate,
divider,
// divider,
// TODO: need a translate
divider,
del,
Expand Down
46 changes: 46 additions & 0 deletions src/ProChat/components/InputArea/ActionBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createStyles } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';

import ActionIcon from '@/ActionIcon';
import { ConfigProvider, Popconfirm } from 'antd';
import { Trash2 } from 'lucide-react';

import { useStore } from '../../store';

const useStyles = createStyles(({ css, token }) => ({
extra: css`
color: ${token.colorTextTertiary};
`,
}));

export const ActionBar = () => {
const [dispatchMessage] = useStore((s) => [s.dispatchMessage]);

const { styles, theme } = useStyles();

return (
<ConfigProvider theme={{ token: { colorText: theme.colorTextSecondary } }}>
<Flexbox
align={'center'}
direction={'horizontal-reverse'}
paddingInline={12}
className={styles.extra}
gap={8}
>
<Popconfirm
title={'你即将要清空会话,清空后将无法找回。是否清空当前会话?'}
okButtonProps={{ danger: true }}
okText={'清空会话'}
onConfirm={() => {
dispatchMessage({ type: 'resetMessages' });
}}
>
<ActionIcon title={'清空当前会话'} icon={Trash2} />
</Popconfirm>
</Flexbox>
</ConfigProvider>
);
};

export default memo(ActionBar);
116 changes: 116 additions & 0 deletions src/ProChat/components/InputArea/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { SendOutlined } from '@ant-design/icons';
import { Button, ConfigProvider, Input } from 'antd';
import { createStyles, useResponsive } from 'antd-style';
import { memo, useRef, useState } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useStore } from '../../store';

import ActionBar from './ActionBar';

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-radius: 8px;
`,
btn: css`
position: absolute;
z-index: 10;
right: 8px;
bottom: 8px;
color: ${token.colorTextTertiary};
&:hover {
color: ${token.colorTextSecondary};
}
`,
extra: css`
color: ${token.colorTextTertiary};
`,
}));

export const InputArea = ({}) => {
const [sendMessage, isLoading] = useStore((s) => [s.sendMessage, !!s.chatLoadingId]);
const [message, setMessage] = useState('');
const isChineseInput = useRef(false);

const { styles, theme } = useStyles();
const { mobile } = useResponsive();

const send = () => {
sendMessage(message);
setMessage('');
};

return (
<ConfigProvider
theme={{
token: {
borderRadius: 4,
colorBgContainer: theme.colorBgElevated,
controlHeightLG: 48,
colorBorder: 'transparent',
colorPrimaryHover: 'transparent',
},
}}
>
<Flexbox gap={8} padding={16} className={styles.container}>
<ActionBar />
<Flexbox horizontal gap={8} align={'center'} className={styles.boxShadow}>
<Input.TextArea
size={'large'}
value={message}
placeholder="请输入内容..."
onChange={(e) => {
setMessage(e.target.value);
}}
autoSize={{ maxRows: 8 }}
onCompositionStart={() => {
isChineseInput.current = true;
}}
onCompositionEnd={() => {
isChineseInput.current = false;
}}
className={styles.input}
onPressEnter={(e) => {
if (!isLoading && !e.shiftKey && !isChineseInput.current) {
e.preventDefault();
send();
}
}}
/>
{mobile ? null : (
<Button
loading={isLoading}
type="text"
className={styles.btn}
onClick={() => send()}
icon={<SendOutlined />}
/>
)}
</Flexbox>
</Flexbox>
</ConfigProvider>
);
};

export default memo(InputArea);
5 changes: 3 additions & 2 deletions src/ProChat/container/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { createStyles } from 'antd-style';
import { ReactNode, memo, useRef } from 'react';
import { Flexbox } from 'react-layout-kit';

import { useOverrideStyles } from '@/ProChat/container/OverrideStyle';
import ChatList from '../components/ChatList';
import InputArea from '../components/InputArea';
import ChatScrollAnchor from '../components/ScrollAnchor';
import { useOverrideStyles } from './OverrideStyle';

const useStyles = createStyles(
({ css, responsive, stylish }) => css`
Expand Down Expand Up @@ -36,7 +37,7 @@ const App = memo<ConversationProps>(({ chatInput, showTitle }) => {
</div>
<BackBottom target={ref} text={'返回底部'} />
</div>
{chatInput}
{chatInput ?? <InputArea />}
</Flexbox>
);
});
Expand Down
2 changes: 1 addition & 1 deletion src/ProChat/container/OverrideStyle/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default (token: FullToken) => css`
::selection {
color: #000;
background: ${token.yellow9};
background: ${token.blue3};
-webkit-text-fill-color: unset !important;
}
Expand Down
5 changes: 3 additions & 2 deletions src/ProChat/container/StoreUpdater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { createStoreUpdater } from 'zustand-utils';
import { ChatProps, ChatState, useStoreApi } from '../store';

export type StoreUpdaterProps = Partial<
Pick<ChatState, 'chats' | 'config' | 'init' | 'onChatsChange' | 'helloMessage'>
Pick<ChatState, 'chats' | 'config' | 'init' | 'onChatsChange' | 'helloMessage' | 'request'>
> &
Pick<ChatProps, 'userMeta' | 'assistantMeta'>;

const StoreUpdater = memo<StoreUpdaterProps>(
({ init, onChatsChange, userMeta, assistantMeta, helloMessage, chats, config }) => {
({ init, onChatsChange, request, userMeta, assistantMeta, helloMessage, chats, config }) => {
const storeApi = useStoreApi();
const useStoreUpdater = createStoreUpdater(storeApi);

Expand All @@ -24,6 +24,7 @@ const StoreUpdater = memo<StoreUpdaterProps>(
useStoreUpdater('chats', chats);
useStoreUpdater('onChatsChange', onChatsChange);

useStoreUpdater('request', request);
return null;
},
);
Expand Down
3 changes: 3 additions & 0 deletions src/ProChat/container/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ProChat = memo<ProChatProps>(
userMeta,
assistantMeta,
showTitle,
request,
...props
}) => {
return (
Expand All @@ -36,6 +37,7 @@ export const ProChat = memo<ProChatProps>(
helloMessage={helloMessage}
userMeta={userMeta}
assistantMeta={assistantMeta}
request={request}
{...props}
devtoolOptions={__PRO_CHAT_STORE_DEVTOOLS__}
>
Expand All @@ -47,6 +49,7 @@ export const ProChat = memo<ProChatProps>(
helloMessage={helloMessage}
chats={chats}
userMeta={userMeta}
request={request}
assistantMeta={assistantMeta}
onChatsChange={onChatsChange}
/>
Expand Down
20 changes: 20 additions & 0 deletions src/ProChat/demos/contolled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* compact: true
*/
import { ChatMessageMap, ProChat } from '@ant-design/pro-chat';

import { useTheme } from 'antd-style';
import { useState } from 'react';
import { Flexbox } from 'react-layout-kit';

export default () => {
const theme = useTheme();

const [chats, setChats] = useState<ChatMessageMap>();

return (
<Flexbox style={{ background: theme.colorBgLayout }}>
<ProChat chats={chats} onChatsChange={setChats} />
</Flexbox>
);
};
2 changes: 1 addition & 1 deletion src/ProChat/demos/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default () => {
const theme = useTheme();
return (
<Flexbox style={{ background: theme.colorBgLayout }}>
<ProChat initialChats={example.chats} config={example.config} />
<ProChat request={'https://chat.lobehub.com/api/openai/chat'} config={example.config} />
</Flexbox>
);
};
27 changes: 27 additions & 0 deletions src/ProChat/demos/request.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* compact: true
*/
import { ProChat } from '@ant-design/pro-chat';
import { useTheme } from 'antd-style';
import { Flexbox } from 'react-layout-kit';

import { example } from '../mocks/basic';
import { MockResponse } from '../mocks/streamResponse';

export default () => {
const theme = useTheme();

return (
<Flexbox style={{ background: theme.colorBgLayout }}>
<ProChat
request={async (messages) => {
const mockedData: string = `这是一段模拟的流式字符串数据。本次会话传入了${messages.length}条消息`;
const mockResponse = new MockResponse(mockedData);

return mockResponse.getResponse();
}}
config={example.config}
/>
</Flexbox>
);
};
Loading

0 comments on commit 4e6d720

Please sign in to comment.