Merge pull request #4695 from pnp/react-azure-openai-api-stream_updates
This commit is contained in:
commit
e550779692
|
@ -1,10 +1,10 @@
|
|||
# Calling Azure OpenAI API in Stream mode
|
||||
# Calling Azure OpenAI API in Streaming Mode
|
||||
|
||||
## Summary
|
||||
|
||||
This web part shows how you can call Azure OpenAI API in Streaming mode, so the web part shows the data coming from the API in chunks, giving a much better user experience, so you are not waiting for the entire response. It also shows how you can cancel the streaming response at any point, which is useful to safe some tokens (hence money), if the generating response does not look good to you (like when getting AI hallucinations).
|
||||
This web part shows how you can call Azure OpenAI API in Streaming mode. The web part will show the data coming from the API in chunks, giving a much better user experience, so you are not waiting for the entire response. It also shows how you can cancel the streaming response at any point, which is useful to save some tokens (hence money), if the generating response does not look good to you (like when getting AI hallucinations). AI responses render Markdown but there is a toggle to disable so you can compare Markdown rendering of responses with Streaming and without.
|
||||
|
||||
![./assets/react-azure-openai-api-stream.gif](./assets/react-azure-openai-api-stream.gif)
|
||||
![Sample in action](./assets/screenshot.gif)
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
@ -44,13 +44,15 @@ This sample is optimally compatible with the following environment configuration
|
|||
|
||||
## Contributors
|
||||
|
||||
- [Luis Mañez](https://github.com/luismanez) |
|
||||
- [Luis Mañez](https://github.com/luismanez)
|
||||
- [Chris Kent](https://twitter.com/thechriskent)
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Comments |
|
||||
| ------- | ---------------- | --------------- |
|
||||
| 1.0 | January 2, 2024 | Initial release |
|
||||
| 1.1 | February 8, 2024 | Theme enhancements & markdown support |
|
||||
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
@ -73,10 +75,7 @@ This sample illustrates the following concepts:
|
|||
- How to cancel an streaming request
|
||||
- Using MS Graph toolkit with the Person component
|
||||
- Multiple FluentUI components
|
||||
|
||||
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
|
||||
|
||||
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
|
||||
- Markdown rendering of AI responses
|
||||
|
||||
## References
|
||||
|
||||
|
@ -85,3 +84,26 @@ This sample illustrates the following concepts:
|
|||
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
||||
|
||||
## Help
|
||||
|
||||
|
||||
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
|
||||
|
||||
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
|
||||
|
||||
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-azure-openai-api-stream%22) to see if anybody else is having the same issues.
|
||||
|
||||
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-azure-openai-api-stream) and see what the community is saying.
|
||||
|
||||
If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-azure-openai-api-stream&template=bug-report.yml&sample=react-azure-openai-api-stream&authors=@luismanez%20@thechriskent&title=react-azure-openai-api-stream%20-%20).
|
||||
|
||||
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-azure-openai-api-stream&template=question.yml&sample=react-azure-openai-api-stream&authors=@luismanez%20@thechriskent&title=react-azure-openai-api-stream%20-%20).
|
||||
|
||||
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-azure-openai-api-stream&template=suggestion.yml&sample=react-azure-openai-api-stream&authors=@luismanez%20@thechriskent&title=react-azure-openai-api-stream%20-%20).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
<img src="https://m365-visitor-stats.azurewebsites.net/sp-dev-fx-webparts/samples/react-azure-openai-api-stream" />
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
{
|
||||
"name": "pnp-sp-dev-spfx-web-parts-react-azure-openai-api-stream",
|
||||
"source": "pnp",
|
||||
"title": "Calling Azure OpenAI API in stream mode",
|
||||
"title": "Calling Azure OpenAI API in streaming mode",
|
||||
"shortDescription": "This web part shows how you can call Azure OpenAI API in Streaming mode",
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-azure-openai-api-stream",
|
||||
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-azure-openai-api-stream",
|
||||
"longDescription": [
|
||||
"This web part shows how you can call Azure OpenAI API in Streaming mode, so the web part shows the data coming from the API in chunks, giving a much better user experience, so you are not waiting for the entire response. It also shows how you can cancel the streaming response at any point, which is useful to safe some tokens (hence money), if the generating response does not look good to you (like when getting AI hallucinations)."
|
||||
"This web part shows how you can call Azure OpenAI API in Streaming mode. The web part will show the data coming from the API in chunks, giving a much better user experience, so you are not waiting for the entire response. It also shows how you can cancel the streaming response at any point, which is useful to save some tokens (hence money), if the generating response does not look good to you (like when getting AI hallucinations). AI responses render Markdown but there is a toggle to disable so you can compare Markdown rendering of responses with Streaming and without."
|
||||
],
|
||||
"creationDateTime": "2024-01-02",
|
||||
"updateDateTime": "2024-01-02",
|
||||
"updateDateTime": "2024-02-08",
|
||||
"products": [
|
||||
"SharePoint"
|
||||
],
|
||||
|
@ -28,8 +28,14 @@
|
|||
{
|
||||
"type": "image",
|
||||
"order": 100,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/blob/1c6a0a60236cd8637c880406b8a30fec66b798ba/samples/react-azure-openai-api-stream/assets/react-azure-openai-api-stream.gif",
|
||||
"alt": "Web Part Preview"
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/blob/main/samples/react-azure-openai-api-stream/assets/screenshot.gif",
|
||||
"alt": "Sample in action"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"order": 101,
|
||||
"url": "https://github.com/pnp/sp-dev-fx-webparts/blob/main/samples/react-azure-openai-api-stream/assets/screenshot.png",
|
||||
"alt": "Screenshot"
|
||||
}
|
||||
],
|
||||
"authors": [
|
||||
|
@ -37,6 +43,11 @@
|
|||
"gitHubAccount": "luismanez",
|
||||
"pictureUrl": "https://github.com/luismanez.png",
|
||||
"name": "Luis Mañez"
|
||||
},
|
||||
{
|
||||
"gitHubAccount": "thechriskent",
|
||||
"pictureUrl": "https://github.com/thechriskent.png",
|
||||
"name": "Chris Kent"
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -13,4 +13,10 @@ build.rig.getTasks = function () {
|
|||
return result;
|
||||
};
|
||||
|
||||
/* fast-serve */
|
||||
const { addFastServe } = require("spfx-fast-serve-helpers");
|
||||
addFastServe(build);
|
||||
/* end of fast-serve */
|
||||
|
||||
build.initialize(require('gulp'));
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,8 @@
|
|||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
"test": "gulp test",
|
||||
"serve": "fast-serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react": "^8.106.4",
|
||||
|
@ -25,6 +26,8 @@
|
|||
"@pnp/spfx-property-controls": "3.15.1",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-remark": "^2.1.0",
|
||||
"remark-gfm": "1.0.0",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -41,6 +44,7 @@
|
|||
"eslint": "8.7.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"gulp": "4.0.2",
|
||||
"spfx-fast-serve-helpers": "~1.18.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"group": { "default": "Advanced" },
|
||||
"title": { "default": "OpenAi Chat in streaming" },
|
||||
"description": { "default": "This webpart provides an OpenAI chat in streaming mode." },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"officeFabricIconFontName": "OfficeChat",
|
||||
"properties": {
|
||||
"openAiApiKey": "",
|
||||
"openAiApiEndpoint": "",
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { Stack } from "@fluentui/react";
|
||||
import { Icon } from '@fluentui/react';
|
||||
import * as React from "react";
|
||||
import styles from "./ChatStreaming.module.scss";
|
||||
import MarkdownContent from "./MarkdownContent";
|
||||
import ThinkingIndicator from "./ThinkingIndicator";
|
||||
|
||||
export interface IAssistantResponseProps {
|
||||
message: string;
|
||||
disableMarkdown?: boolean;
|
||||
thinking?: boolean;
|
||||
}
|
||||
|
||||
export default class AssistantResponse extends React.Component<
|
||||
|
@ -12,46 +17,22 @@ export default class AssistantResponse extends React.Component<
|
|||
> {
|
||||
public render(): React.ReactElement<IAssistantResponseProps> {
|
||||
return (
|
||||
<Stack horizontal tokens={{ childrenGap: 30, padding: 10 }}>
|
||||
<Stack.Item>
|
||||
<Icon iconName="Robot" styles={{ root: { fontSize: '22px' } }} />
|
||||
</Stack.Item>
|
||||
<Stack.Item
|
||||
grow
|
||||
styles={{ root: { display: "flex", justifyContent: "flex-start" } }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
backgroundColor: "white",
|
||||
fontFamily:
|
||||
'"Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;',
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
maxWidth: "85%",
|
||||
minWidth: "350px"
|
||||
}}
|
||||
>
|
||||
<p style={{ minWidth: '100px' }}>{this.props.message}</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
left: "-10px",
|
||||
top: "10px", // changed right to left
|
||||
width: "0",
|
||||
height: "0",
|
||||
borderTop: "10px solid transparent",
|
||||
borderBottom: "10px solid transparent",
|
||||
borderRight: "10px solid white", // changed borderLeft to borderRight
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack.Item>
|
||||
<Stack horizontal className={styles.assistantResponse}>
|
||||
<div className={styles.avatar}>
|
||||
<Icon iconName="Robot" />
|
||||
</div>
|
||||
<div className={styles.messageBox}>
|
||||
{this.props.thinking && this.props.message.length === 0 &&
|
||||
<ThinkingIndicator />
|
||||
}
|
||||
{this.props.disableMarkdown && this.props.message.length > 0 &&
|
||||
<p className={styles.message}>{this.props.message}</p>
|
||||
}
|
||||
{!this.props.disableMarkdown && this.props.message.length > 0 &&
|
||||
<MarkdownContent className={styles.message}>{this.props.message}</MarkdownContent>
|
||||
}
|
||||
<div className={styles.beak}/>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,30 +4,220 @@
|
|||
overflow: hidden;
|
||||
color: "[theme:bodyText, default: #323130]";
|
||||
color: var(--bodyText);
|
||||
min-height: 100%;
|
||||
gap: 20px;
|
||||
|
||||
&.teams {
|
||||
font-family: $ms-font-family-fallbacks;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.welcomeImage {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.links {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: "[theme:link, default:#03787c]";
|
||||
color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: "[theme:linkHovered, default: #014446]";
|
||||
color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only
|
||||
}
|
||||
.messagesContainer {
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.messagesList {
|
||||
background-color: "[theme:neutralLighter, default: #f3f2f1]";
|
||||
min-height: 400px;
|
||||
border-radius: 8px;
|
||||
|
||||
.scrollablePane {
|
||||
|
||||
:global {
|
||||
.ms-ScrollablePane--contentContainer {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.userQuestion {
|
||||
padding-bottom: 8px;
|
||||
gap: 12px;
|
||||
|
||||
.avatar {
|
||||
--person-avatar-size: 32px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.beak {
|
||||
right: -15px;
|
||||
border-left: 15px solid "[theme:white, default: #ffffff]";
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.assistantResponse {
|
||||
padding-bottom: 8px;
|
||||
gap: 12px;
|
||||
|
||||
.avatar {
|
||||
margin-top: 8px;
|
||||
background-color: "[theme:white, default: #ffffff]";
|
||||
color: "[theme:neutralPrimary, default: #323130]";
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
font-size: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.beak {
|
||||
left: -15px;
|
||||
border-right: 15px solid "[theme:white, default: #ffffff]";
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageBox {
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 0 12px;
|
||||
background-color: "[theme:white, default: #ffffff]";
|
||||
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 85%;
|
||||
min-width: 350px;
|
||||
|
||||
.message {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.beak {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 15px solid transparent;
|
||||
border-bottom: 15px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.userMessage {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.markdownContent {
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
margin: 14px 0;
|
||||
width: max-content;
|
||||
max-width: fit-content;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: "[theme: white, default: #ffffff]";
|
||||
border-top: 1px solid "[theme: neutralTertiaryAlt, default: #c8c6c4]";
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: "[theme: neutralLight, default: #edebe9]";
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid "[theme: neutralTertiaryAlt, default: #c8c6c4]";
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
table img {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: "[theme: neutralLight, default: #edebe9]";
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
$dotSize: 8px;
|
||||
$jumpHeight: 14px;
|
||||
|
||||
.thinkingIndicator {
|
||||
padding-top: 6px;
|
||||
|
||||
.allDots {
|
||||
margin-top: $jumpHeight;
|
||||
display: flex;
|
||||
gap: calc($dotSize / 2);
|
||||
|
||||
.dot {
|
||||
position: relative;
|
||||
width: $dotSize;
|
||||
height: $dotSize;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
animation: bouncedelay 2.0s infinite cubic-bezier(.62, .28, .23, .99) both;
|
||||
}
|
||||
|
||||
.dot1 {
|
||||
animation-delay: -.16s;
|
||||
}
|
||||
|
||||
.dot2 {
|
||||
animation-delay: -.08s;
|
||||
}
|
||||
|
||||
.dot3 {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0% {
|
||||
bottom: 0;
|
||||
background-color: "[theme: themePrimary, default: #0078d4]";
|
||||
}
|
||||
|
||||
16.66% {
|
||||
bottom: $jumpHeight;
|
||||
background-color: "[theme: themeDark, default: #005a9e]";
|
||||
}
|
||||
|
||||
33.33% {
|
||||
bottom: 0px;
|
||||
background-color: "[theme: themeDark, default: #005a9e]";
|
||||
}
|
||||
|
||||
50% {
|
||||
bottom: $jumpHeight;
|
||||
background-color: "[theme: themeLight, default: #c7e0f4]";
|
||||
}
|
||||
|
||||
66.66% {
|
||||
bottom: 0px;
|
||||
background-color: "[theme: themeLight, default: #c7e0f4]";
|
||||
}
|
||||
|
||||
83.33% {
|
||||
bottom: $jumpHeight;
|
||||
background-color: "[theme: themePrimary, default: #0078d4]";
|
||||
}
|
||||
|
||||
100% {
|
||||
bottom: 0;
|
||||
background-color: "[theme: themePrimary, default: #0078d4]";
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react';
|
||||
//import styles from './ChatStreaming.module.scss';
|
||||
import styles from './ChatStreaming.module.scss';
|
||||
import type { IChatStreamingProps } from './IChatStreamingProps';
|
||||
import { IChatStreamingState } from './IChatStreamingState';
|
||||
import { cloneDeep } from '@microsoft/sp-lodash-subset';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import CompletionsRequestBuilder from '../models/CompletionsRequestBuilder';
|
||||
import { Spinner, SpinnerSize, Stack } from '@fluentui/react';
|
||||
import { Stack, css } from '@fluentui/react';
|
||||
import MessagesList from './MessagesList';
|
||||
import UserMessage from './UserMessage';
|
||||
import { IChatMessage } from '../models/IChatMessage';
|
||||
|
@ -21,8 +21,12 @@ export default class ChatStreaming extends React.Component<IChatStreamingProps,
|
|||
|
||||
this.state = {
|
||||
userQuery: '',
|
||||
sessionMessages: [],
|
||||
thinking: false
|
||||
sessionMessages: [{
|
||||
role: 'assistant',
|
||||
text: 'Hello! I am your AI assistant. How can I help you today?'
|
||||
}],
|
||||
thinking: false,
|
||||
disableMarkdown: false,
|
||||
}
|
||||
|
||||
this._controller = new AbortController();
|
||||
|
@ -32,31 +36,18 @@ export default class ChatStreaming extends React.Component<IChatStreamingProps,
|
|||
public render(): React.ReactElement<IChatStreamingProps> {
|
||||
|
||||
const content = this._validateWebPartProperties() ? (
|
||||
<Stack tokens={{ childrenGap: 20 }} style={{ minHeight: "100%" }}>
|
||||
<Stack.Item
|
||||
grow={1}
|
||||
styles={{
|
||||
root: { minHeight: "200px", height: "100%", position: "relative" },
|
||||
}}
|
||||
>
|
||||
<MessagesList messages={this.state.sessionMessages} />
|
||||
<Stack className={css(styles.chatStreaming, this.props.hasTeamsContext && styles.teams)}>
|
||||
<Stack.Item grow className={styles.messagesContainer}>
|
||||
<MessagesList messages={this.state.sessionMessages} disableMarkdown={this.state.disableMarkdown} thinking={this.state.thinking} />
|
||||
</Stack.Item>
|
||||
{this.state.thinking && (
|
||||
<Stack.Item>
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
label="Wait till our super cool AI system answers your question..."
|
||||
ariaLive="assertive"
|
||||
labelPosition="right"
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<UserMessage
|
||||
textFieldValue={this.state.userQuery}
|
||||
onMessageChange={this._onUserQueryChange}
|
||||
sendQuery={this._onQuerySent}
|
||||
onMessageChange={this._onUserQueryChange.bind(this)}
|
||||
sendQuery={this._onQuerySent.bind(this)}
|
||||
controller={this._controller}
|
||||
disableMarkdown={this.state.disableMarkdown}
|
||||
toggleMarkdown={this._toggleMarkdown.bind(this)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
@ -78,6 +69,10 @@ export default class ChatStreaming extends React.Component<IChatStreamingProps,
|
|||
role: 'user', text: this.state.userQuery
|
||||
});
|
||||
|
||||
this.state.sessionMessages.push({
|
||||
role: 'assistant', text: ''
|
||||
});
|
||||
|
||||
await this._chatAsStream();
|
||||
|
||||
this.setState({
|
||||
|
@ -177,4 +172,10 @@ export default class ChatStreaming extends React.Component<IChatStreamingProps,
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _toggleMarkdown(): void {
|
||||
this.setState({
|
||||
disableMarkdown: !this.state.disableMarkdown
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,5 @@ export interface IChatStreamingState {
|
|||
userQuery: string;
|
||||
sessionMessages: IChatMessage[];
|
||||
thinking: boolean;
|
||||
disableMarkdown: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { css } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import styles from "./ChatStreaming.module.scss";
|
||||
import { Remark } from "react-remark";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
export interface IMarkdownContentProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default class MarkdownContent extends React.Component<
|
||||
IMarkdownContentProps,
|
||||
{}
|
||||
> {
|
||||
public render(): React.ReactElement<IMarkdownContentProps> {
|
||||
return (
|
||||
<div className={css(styles.markdownContent, this.props.className)}>
|
||||
<Remark remarkPlugins={[remarkGfm]}>{this.props.children}</Remark>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,11 +2,14 @@ import * as React from "react";
|
|||
import UserQuestion from "./UserQuestion";
|
||||
import AssistantResponse from "./AssistantResponse";
|
||||
import { IChatMessage } from "../models/IChatMessage";
|
||||
import styles from "./ChatStreaming.module.scss";
|
||||
|
||||
import { ScrollablePane, ScrollbarVisibility } from '@fluentui/react';
|
||||
|
||||
export interface IMessagesListProps {
|
||||
messages: IChatMessage[];
|
||||
disableMarkdown?: boolean;
|
||||
thinking?: boolean;
|
||||
}
|
||||
|
||||
export default class MessagesList extends React.Component<IMessagesListProps, {}> {
|
||||
|
@ -26,12 +29,12 @@ export default class MessagesList extends React.Component<IMessagesListProps, {}
|
|||
if (m.role === 'user') {
|
||||
return <UserQuestion key={i} message={m.text} />
|
||||
}
|
||||
return <AssistantResponse key={i} message={m.text} />
|
||||
return <AssistantResponse key={i} message={m.text} disableMarkdown={this.props.disableMarkdown} thinking={this.props.thinking} />
|
||||
});
|
||||
|
||||
return (
|
||||
<div style= {{ backgroundColor: '#f3f3f3', minHeight: '200px' }}>
|
||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto} style={{ padding: '20px' }}>
|
||||
<div className={styles.messagesList}>
|
||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto} className={styles.scrollablePane}>
|
||||
{output}
|
||||
</ScrollablePane>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from "react";
|
||||
import styles from "./ChatStreaming.module.scss";
|
||||
import { css } from "@fluentui/react";
|
||||
|
||||
export interface IThinkingIndicatorProps {}
|
||||
|
||||
export default class ThinkingIndicator extends React.Component<
|
||||
IThinkingIndicatorProps,
|
||||
{}
|
||||
> {
|
||||
public render(): React.ReactElement<IThinkingIndicatorProps> {
|
||||
return (
|
||||
<div className={styles.thinkingIndicator}>
|
||||
<div className={styles.allDots}>
|
||||
<div className={css(styles.dot, styles.dot1)} />
|
||||
<div className={css(styles.dot, styles.dot2)} />
|
||||
<div className={css(styles.dot, styles.dot3)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
import { IconButton, Stack, TextField } from '@fluentui/react';
|
||||
import * as React from 'react';
|
||||
import styles from './ChatStreaming.module.scss';
|
||||
|
||||
export interface IUserMessageProps {
|
||||
onMessageChange: (query: string) => void;
|
||||
sendQuery: () => Promise<void>;
|
||||
controller: AbortController;
|
||||
textFieldValue: string;
|
||||
disableMarkdown?: boolean;
|
||||
toggleMarkdown: () => void;
|
||||
}
|
||||
|
||||
export default class UserMessage extends React.Component<IUserMessageProps, {}> {
|
||||
|
@ -20,34 +23,30 @@ export default class UserMessage extends React.Component<IUserMessageProps, {}>
|
|||
await this.props.sendQuery();
|
||||
};
|
||||
|
||||
private _keyDownHandler = async (e: KeyboardEvent): Promise<void> => {
|
||||
if (e.ctrlKey && e.code === "Enter") {
|
||||
await this._handleClick();
|
||||
|
||||
private _keyDownHandler = async (event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>): Promise<void> => {
|
||||
if(!event.shiftKey && event.key === "Enter") {
|
||||
//Submits message when a user hits return (but still allows newlines for shift+enter)
|
||||
event.preventDefault();
|
||||
return this._handleClick();
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
window.addEventListener("keydown", this._keyDownHandler);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
window.removeEventListener("keydown", this._keyDownHandler);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IUserMessageProps> {
|
||||
return (
|
||||
<Stack horizontal tokens={{ childrenGap: 5 }}>
|
||||
<Stack.Item grow={1}>
|
||||
<Stack horizontal className={styles.userMessage}>
|
||||
<Stack.Item grow>
|
||||
<TextField
|
||||
multiline
|
||||
autoAdjustHeight
|
||||
rows={5}
|
||||
value={this.props.textFieldValue}
|
||||
onChange={this._onChange}
|
||||
label="User message"
|
||||
placeholder="Type user query here."
|
||||
onKeyDown={this._keyDownHandler}
|
||||
placeholder={'Talk to our super cool AI system!\n(Shift + Enter for new line)'}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item align="end">
|
||||
<Stack verticalAlign='end'>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
title="Send"
|
||||
|
@ -60,7 +59,14 @@ export default class UserMessage extends React.Component<IUserMessageProps, {}>
|
|||
ariaLabel="Stop"
|
||||
onClick={() => this.props.controller.abort()}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<IconButton
|
||||
toggle
|
||||
checked={!this.props.disableMarkdown}
|
||||
onClick={this.props.toggleMarkdown}
|
||||
title="Toggle Markdown"
|
||||
ariaLabel="Markdown"
|
||||
iconProps={{iconName:'MarkDownLanguage'}}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Stack } from "@fluentui/react";
|
|||
import { Person } from "@microsoft/mgt-react/dist/es6/spfx";
|
||||
import { ViewType } from "@microsoft/mgt-spfx";
|
||||
import * as React from "react";
|
||||
import styles from "./ChatStreaming.module.scss";
|
||||
|
||||
export interface IUserQuestionProps {
|
||||
message: string;
|
||||
|
@ -13,45 +14,13 @@ export default class UserQuestion extends React.Component<
|
|||
> {
|
||||
public render(): React.ReactElement<IUserQuestionProps> {
|
||||
return (
|
||||
<Stack horizontal horizontalAlign="end" tokens={{ childrenGap: 30, padding: 5 }}>
|
||||
<Stack.Item
|
||||
styles={{ root: { display: "flex", justifyContent: 'flex-end', alignItems: 'flex-end' } }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
borderRadius: "5px",
|
||||
padding: "5px",
|
||||
backgroundColor: "white",
|
||||
fontFamily:
|
||||
'"Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;',
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
maxWidth: "85%",
|
||||
minWidth: "350px"
|
||||
}}
|
||||
>
|
||||
<p style={{ minWidth: '100px' }}>{this.props.message}</p>
|
||||
<div
|
||||
style={{
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
right: "-10px",
|
||||
top: "10px",
|
||||
width: "0",
|
||||
height: "0",
|
||||
borderTop: "10px solid transparent",
|
||||
borderBottom: "10px solid transparent",
|
||||
borderLeft: "10px solid white",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Person personQuery="me" view={ViewType.image} avatarSize="auto" />
|
||||
</Stack.Item>
|
||||
<Stack horizontal horizontalAlign="end" className={styles.userQuestion}>
|
||||
<div className={styles.messageBox}>
|
||||
<p className={styles.message}>{this.props.message}</p>
|
||||
<div className={styles.beak} />
|
||||
</div>
|
||||
<Person className={styles.avatar} personQuery="me" view={ViewType.image} avatarSize="auto" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
|
|
Loading…
Reference in New Issue