From e1092a5fe61d1f1c23697a64eac424349952349f Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Mon, 6 Apr 2026 17:17:32 +0300 Subject: [PATCH 1/8] Chat: Add Message Streaming jQuery demo --- .../Chat/MessageStreaming/jQuery/data.js | 27 ++ .../Chat/MessageStreaming/jQuery/index.html | 37 +++ .../Chat/MessageStreaming/jQuery/index.js | 298 ++++++++++++++++++ .../Chat/MessageStreaming/jQuery/styles.css | 110 +++++++ apps/demos/menuMeta.json | 14 + 5 files changed, 486 insertions(+) create mode 100644 apps/demos/Demos/Chat/MessageStreaming/jQuery/data.js create mode 100644 apps/demos/Demos/Chat/MessageStreaming/jQuery/index.html create mode 100644 apps/demos/Demos/Chat/MessageStreaming/jQuery/index.js create mode 100644 apps/demos/Demos/Chat/MessageStreaming/jQuery/styles.css diff --git a/apps/demos/Demos/Chat/MessageStreaming/jQuery/data.js b/apps/demos/Demos/Chat/MessageStreaming/jQuery/data.js new file mode 100644 index 000000000000..5201b7e439d7 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/jQuery/data.js @@ -0,0 +1,27 @@ +const deployment = 'gpt-4o-mini'; +const apiVersion = '2024-02-01'; +const endpoint = 'https://public-api.devexpress.com/demo-openai'; +const apiKey = 'DEMO'; +const CHAT_DISABLED_CLASS = 'chat-disabled'; +const ALERT_TIMEOUT = 1000 * 60; +const user = { + id: 'user', +}; +const assistant = { + id: 'assistant', + name: 'AI Assistant', +}; +const suggestionCards = [ + { + title: '💡 What is DevExtreme?', + prompt: 'What is DevExtreme and how can it help me build modern web apps?', + }, + { + title: '🚀 Get Started with DevExtreme', + prompt: 'How do I get started with DevExtreme in my project?', + }, + { + title: '📄 DevExtreme Licensing', + prompt: 'What are the licensing options for DevExtreme?', + }, +]; diff --git a/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.html b/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.html new file mode 100644 index 000000000000..28c36ea0c0a9 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.html @@ -0,0 +1,37 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.js b/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.js new file mode 100644 index 000000000000..fa29c36deada --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/jQuery/index.js @@ -0,0 +1,298 @@ +$(() => { + const store = []; + const messages = []; + let abortController = null; + + DevExpress.localization.loadMessages({ + en: { + 'dxChat-emptyListMessage': 'Chat is Empty', + 'dxChat-emptyListPrompt': + 'AI Assistant is ready to answer your questions.', + 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', + }, + }); + + const chatService = new AzureOpenAI({ + dangerouslyAllowBrowser: true, + deployment, + endpoint, + apiVersion, + apiKey, + }); + + async function getAIResponseStream(messages, { onDelta, onError, signal }) { + const params = { + messages, + model: deployment, + max_tokens: 1000, + temperature: 0.7, + stream: true, + }; + + try { + const stream = await chatService.chat.completions.create(params, { + signal, + }); + + // eslint-disable-next-line no-restricted-syntax + for await (const event of stream) { + const delta = event.choices?.[0]?.delta?.content; + if (delta) { + onDelta(delta); + } + } + } catch (e) { + onError?.(e); + throw e; + } + } + + function alertLimitReached() { + instance.option({ + alerts: [ + { + message: 'Request limit reached, try again in a minute.', + }, + ], + }); + + setTimeout(() => { + instance.option({ alerts: [] }); + }, ALERT_TIMEOUT); + } + + function toggleDisabledState(disabled, event) { + instance.element().toggleClass(CHAT_DISABLED_CLASS, disabled); + + if (disabled) { + event?.target.blur(); + } else { + event?.target.focus(); + } + } + + function setMainButtonToDefault() { + instance.option({ + sendButtonOptions: { + action: 'send', + icon: 'arrowright', + }, + }); + } + + function setMainButtonToStop() { + instance.option({ + sendButtonOptions: { + action: 'custom', + icon: 'stopfilled', + onClick: stopStreaming, + }, + }); + } + + function stopStreaming() { + if (abortController) { + abortController.abort(); + setMainButtonToDefault(); + } + } + + async function processMessageSending(message, event) { + abortController = new AbortController(); + setMainButtonToStop(); + + messages.push({ role: 'user', content: message.text }); + instance.option({ typingUsers: [assistant] }); + + let assistantId; + let buffer = ''; + let typingCleared = false; + + const onDelta = (chunk) => { + if (!typingCleared) { + instance.option({ typingUsers: [] }); + typingCleared = true; + } + + if (!assistantId) { + assistantId = insertAssistantPlaceholder(); + } + + buffer += chunk; + + updateMessageText(assistantId, buffer); + }; + + try { + await getAIResponseStream(messages, { + onDelta, + signal: abortController.signal, + }); + + instance.option({ typingUsers: [] }); + messages.push({ role: 'assistant', content: buffer }); + } catch { + instance.option({ typingUsers: [] }); + + messages.pop(); + + updateMessageText(assistantId, ''); + alertLimitReached(); + } finally { + abortController = null; + setMainButtonToDefault(); + toggleDisabledState(false, event); + } + } + + function insertAssistantPlaceholder() { + const id = Date.now(); + + dataSource.store().push([ + { + type: 'insert', + data: { + id, + timestamp: new Date(), + author: assistant, + text: '', + }, + }, + ]); + + return id; + } + + function updateMessageText(id, text) { + dataSource.store().push([ + { + type: 'update', + key: id, + data: { text }, + }, + ]); + } + + function convertToHtml(value) { + const result = unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeMinifyWhitespace) + .use(rehypeStringify) + .processSync(value) + .toString(); + + return result; + } + + function renderMessageContent(message, element) { + $('
') + .addClass('chat-messagebubble-text') + .html(convertToHtml(message.text)) + .appendTo(element); + } + + const customStore = new DevExpress.data.CustomStore({ + key: 'id', + load: () => { + const d = $.Deferred(); + + setTimeout(() => { + d.resolve([...store]); + }); + + return d.promise(); + }, + insert: (message) => { + const d = $.Deferred(); + + setTimeout(() => { + store.push(message); + d.resolve(); + }); + + return d.promise(); + }, + }); + + const dataSource = new DevExpress.data.DataSource({ + store: customStore, + paginate: false, + }); + + function sendSuggestion(prompt, event) { + const message = { + id: Date.now(), + timestamp: new Date(), + author: user, + text: prompt, + }; + + dataSource.store().push([{ type: 'insert', data: message }]); + + if (!instance.option('alerts').length) { + processMessageSending(message, event); + } + } + + function createSuggestionCard(card) { + return $(' + } +
+ + + diff --git a/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.component.ts b/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.component.ts new file mode 100644 index 000000000000..fec17c9febf7 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.component.ts @@ -0,0 +1,84 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { DxChatModule } from 'devextreme-angular'; +import type { DxChatTypes } from 'devextreme-angular/ui/chat'; +import { Observable, map } from 'rxjs'; +import { loadMessages } from 'devextreme-angular/common/core/localization'; +import { DataSource } from 'devextreme-angular/common/data'; +import { AppService, suggestionCards } from './app.service'; +import { AiService } from './ai/ai.service'; + +if (!/localhost/.test(document.location.host)) { + enableProdMode(); +} + +let modulePrefix = ''; +// @ts-ignore +if (window && window.config?.packageConfigPaths) { + modulePrefix = '/app'; +} + +@Component({ + selector: 'demo-app', + templateUrl: `.${modulePrefix}/app.component.html`, + styleUrls: [`.${modulePrefix}/app.component.css`], + imports: [ + DxChatModule, + AsyncPipe, + ], +}) +export class AppComponent { + dataSource: DataSource; + + user: DxChatTypes.User; + + typingUsers$: Observable; + + alerts$: Observable; + + sendButtonOptions$: Observable; + + readonly suggestionCards = suggestionCards; + + constructor(private readonly appService: AppService) { + loadMessages(this.appService.getDictionary()); + + this.dataSource = this.appService.dataSource; + this.user = this.appService.user; + this.alerts$ = this.appService.alerts$; + this.typingUsers$ = this.appService.typingUsers$; + + this.sendButtonOptions$ = this.appService.isStreaming$.pipe( + map((isStreaming) => (isStreaming ? { + action: 'custom' as const, + icon: 'stopfilled', + onClick: () => this.appService.stopStreaming(), + } : { + action: 'send' as const, + icon: 'arrowright', + onClick: () => {}, + })), + ); + } + + convertToHtml(message: DxChatTypes.Message): string { + return this.appService.convertToHtml(message.text); + } + + onMessageEntered(e: DxChatTypes.MessageEnteredEvent): void { + this.appService.onMessageEntered(e); + } + + onSuggestionClick(prompt: string): void { + this.appService.sendSuggestion(prompt); + } +} + +bootstrapApplication(AppComponent, { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), + AppService, + AiService, + ], +}); diff --git a/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.service.ts b/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.service.ts new file mode 100644 index 000000000000..07b186a910a2 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Angular/app/app.service.ts @@ -0,0 +1,269 @@ +import { Injectable } from '@angular/core'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeStringify from 'rehype-stringify'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; +import { type DxChatTypes } from 'devextreme-angular/ui/chat'; +import { DataSource, CustomStore } from 'devextreme-angular/common/data'; +import { AiService, type AIMessage } from './ai/ai.service'; + +export const suggestionCards = [ + { + title: '💡 What is DevExtreme?', + description: 'What is DevExtreme and how can it help me build modern web apps?', + prompt: 'What is DevExtreme, and which components and frameworks does it support?', + }, + { + title: '🚀 Get Started with DevExtreme', + description: 'How do I get started with DevExtreme in my project?', + prompt: 'How can I get started with DevExtreme? Include instructions for library installation, linking required CSS/assets, applying an application theme, and coding a simple working app.', + }, + { + title: '📄 DevExtreme Licensing', + description: 'What are the licensing options for DevExtreme?', + prompt: 'Which DevExtreme license do I need for a commercial project? What licensing options are available?', + }, +]; + +interface DelayedRendererOptions { + delay?: number; + onRender: (chunk: string) => void; +} + +function createDelayedRenderer({ delay = 20, onRender }: DelayedRendererOptions) { + let queue: string[] = []; + let rendering = false; + + function processQueue() { + if (!queue.length) { + rendering = false; + return; + } + + rendering = true; + const chunk = queue.shift(); + if (chunk !== undefined) { + onRender(chunk); + } + + setTimeout(processQueue, delay); + } + + function pushChunk(chunk: string) { + queue.push(chunk); + + if (!rendering) { + processQueue(); + } + } + + function stop() { + queue = []; + rendering = false; + } + + return { pushChunk, stop }; +} + +@Injectable() +export class AppService { + readonly ALERT_TIMEOUT = 1000 * 60; + + readonly user: DxChatTypes.User = { id: 'user' }; + + readonly assistant: DxChatTypes.User = { id: 'assistant', name: 'AI Assistant' }; + + private store: DxChatTypes.Message[] = []; + + private messages: AIMessage[] = []; + + private abortController: AbortController | null = null; + + private typingUsersSubject = new BehaviorSubject([]); + + private alertsSubject = new BehaviorSubject([]); + + private isStreamingSubject = new BehaviorSubject(false); + + readonly dataSource: DataSource; + + get alerts(): DxChatTypes.Alert[] { + return this.alertsSubject.getValue(); + } + + get typingUsers$(): Observable { + return this.typingUsersSubject.asObservable(); + } + + get alerts$(): Observable { + return this.alertsSubject.asObservable(); + } + + get isStreaming$(): Observable { + return this.isStreamingSubject.asObservable(); + } + + constructor(private readonly aiService: AiService) { + const customStore = new CustomStore({ + key: 'id', + load: () => new Promise((resolve) => { + setTimeout(() => { + resolve([...this.store]); + }, 0); + }), + insert: (message) => new Promise((resolve) => { + setTimeout(() => { + this.store.push(message); + resolve(message); + }); + }), + }); + + this.dataSource = new DataSource({ + store: customStore, + paginate: false, + }); + } + + getDictionary() { + return { + en: { + 'dxChat-emptyListMessage': 'Chat is Empty', + 'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.', + 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', + }, + }; + } + + private insertMessage(data: DxChatTypes.Message): void { + this.dataSource.store().push([{ type: 'insert', data }]); + } + + private updateMessageText(id: number, text: string): void { + this.dataSource.store().push([{ + type: 'update', + key: id, + data: { text }, + }]); + } + + private insertAssistantPlaceholder(): number { + const id = Date.now(); + this.dataSource.store().push([{ + type: 'insert', + data: { + id, + timestamp: new Date(), + author: this.assistant, + text: '', + }, + }]); + return id; + } + + private alertLimitReached(): void { + this.alertsSubject.next([{ message: 'Request limit reached, try again in a minute.' }]); + + setTimeout(() => { + this.alertsSubject.next([]); + }, this.ALERT_TIMEOUT); + } + + stopStreaming(): void { + if (this.abortController) { + this.abortController.abort(); + } + } + + async fetchAIResponse(message: DxChatTypes.Message): Promise { + const dataItemToMessage = (item: DxChatTypes.Message): AIMessage => ({ + role: item.author?.id as AIMessage['role'], + content: item.text, + }); + + this.messages = [...this.dataSource.items().map(dataItemToMessage), dataItemToMessage(message)]; + this.abortController = new AbortController(); + + setTimeout(() => this.isStreamingSubject.next(true), 0); + this.typingUsersSubject.next([this.assistant]); + + let assistantId: number | undefined; + let buffer = ''; + let typingCleared = false; + + const delayedRenderer = createDelayedRenderer({ + onRender: (chunk: string) => { + if (!typingCleared) { + this.typingUsersSubject.next([]); + typingCleared = true; + } + + if (assistantId === undefined) { + assistantId = this.insertAssistantPlaceholder(); + } + + buffer += chunk; + this.updateMessageText(assistantId, buffer); + }, + }); + + const onAborted = () => { + delayedRenderer.stop(); + }; + + try { + await this.aiService.getAIResponseStream(this.messages, { + onAborted, + onDelta: delayedRenderer.pushChunk, + signal: this.abortController.signal, + }); + + this.typingUsersSubject.next([]); + } catch (e: unknown) { + this.typingUsersSubject.next([]); + + if ((e as Error)?.name !== 'AbortError' && assistantId !== undefined) { + this.updateMessageText(assistantId, ''); + this.alertLimitReached(); + } + } finally { + this.abortController = null; + this.isStreamingSubject.next(false); + } + } + + onMessageEntered({ message }: DxChatTypes.MessageEnteredEvent): void { + this.insertMessage({ id: Date.now(), ...message }); + + if (!this.alerts.length) { + this.fetchAIResponse(message); + } + } + + sendSuggestion(prompt: string): void { + const message: DxChatTypes.Message = { + id: Date.now(), + timestamp: new Date(), + author: this.user, + text: prompt, + }; + + this.insertMessage(message); + + if (!this.alerts.length) { + this.fetchAIResponse(message); + } + } + + convertToHtml(value: string): string { + return unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeMinifyWhitespace) + .use(rehypeStringify) + .processSync(value) + .toString(); + } +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/Angular/index.html b/apps/demos/Demos/Chat/MessageStreaming/Angular/index.html new file mode 100644 index 000000000000..1ab1fb54a1df --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Angular/index.html @@ -0,0 +1,26 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + + +
+ Loading... +
+ + diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/App.tsx b/apps/demos/Demos/Chat/MessageStreaming/React/App.tsx new file mode 100644 index 000000000000..d2c39b9f55e1 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/App.tsx @@ -0,0 +1,86 @@ +import React, { useCallback, useMemo } from 'react'; +import Chat from 'devextreme-react/chat'; +import type { ChatTypes } from 'devextreme-react/chat'; +import { loadMessages } from 'devextreme-react/common/core/localization'; +import { + user, +} from './data.ts'; +import Message from './Message.tsx'; +import EmptyView from './EmptyView.tsx'; +import { dataSource, useApi } from './useApi.ts'; + +loadMessages({ + en: { + 'dxChat-emptyListMessage': 'Chat is Empty', + 'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.', + 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', + }, +}); + +export default function App() { + const { + alerts, + typingUsers, + isStreaming, + insertMessage, + fetchAIResponse, + stopStreaming, + } = useApi(); + + const onMessageEntered = useCallback(async ({ message }: ChatTypes.MessageEnteredEvent): Promise => { + insertMessage({ id: Date.now(), ...message }); + + if (!alerts.length) { + await fetchAIResponse(message); + } + }, [insertMessage, alerts.length, fetchAIResponse]); + + const sendSuggestion = useCallback((prompt: string): void => { + const message: ChatTypes.Message = { + id: Date.now(), + timestamp: new Date(), + author: user, + text: prompt, + }; + + insertMessage(message); + + if (!alerts.length) { + fetchAIResponse(message); + } + }, [insertMessage, alerts.length, fetchAIResponse]); + + const messageRender = useCallback(({ message }: { message: ChatTypes.Message }) => , []); + + const emptyViewRender = useCallback(({ texts }: ChatTypes.EmptyViewTemplateData) => ( + + ), [sendSuggestion]); + + const sendButtonOptions = useMemo(() => (isStreaming ? { + action: 'custom', + icon: 'stopfilled', + onClick: stopStreaming, + } : { + action: 'send', + icon: 'arrowright', + onClick: () => {}, + }), [isStreaming, stopStreaming]); + + return ( + + ); +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/EmptyView.tsx b/apps/demos/Demos/Chat/MessageStreaming/React/EmptyView.tsx new file mode 100644 index 000000000000..7a25264473a6 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/EmptyView.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type { FC } from 'react'; +import type { ChatTypes } from 'devextreme-react/chat'; +import { suggestionCards } from './data.ts'; + +interface EmptyViewProps { + texts: ChatTypes.EmptyViewTemplateData['texts']; + onSuggestionClick: (prompt: string) => void; +} + +const EmptyView: FC = ({ texts, onSuggestionClick }: EmptyViewProps) => ( +
+
{texts.message}
+
{texts.prompt}
+
+ {suggestionCards.map((card) => ( + + ))} +
+
+); + +export default EmptyView; diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/Message.tsx b/apps/demos/Demos/Chat/MessageStreaming/React/Message.tsx new file mode 100644 index 000000000000..f4fb48ef524c --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/Message.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import type { FC } from 'react'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; +import rehypeStringify from 'rehype-stringify'; +import HTMLReactParser from 'html-react-parser'; + +function convertToHtml(value: string): string { + return unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeMinifyWhitespace) + .use(rehypeStringify) + .processSync(value) + .toString(); +} + +interface MessageProps { + text: string; +} + +const Message: FC = ({ text }: MessageProps) => ( +
+ {HTMLReactParser(convertToHtml(text))} +
+); + +export default Message; diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/data.ts b/apps/demos/Demos/Chat/MessageStreaming/React/data.ts new file mode 100644 index 000000000000..980ebab8945b --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/data.ts @@ -0,0 +1,30 @@ +import type { ChatTypes } from 'devextreme-react/chat'; + +export const ALERT_TIMEOUT = 1000 * 60; + +export const user: ChatTypes.User = { + id: 'user', +}; + +export const assistant: ChatTypes.User = { + id: 'assistant', + name: 'AI Assistant', +}; + +export const suggestionCards = [ + { + title: '💡 What is DevExtreme?', + description: 'What is DevExtreme and how can it help me build modern web apps?', + prompt: 'What is DevExtreme, and which components and frameworks does it support?', + }, + { + title: '🚀 Get Started with DevExtreme', + description: 'How do I get started with DevExtreme in my project?', + prompt: 'How can I get started with DevExtreme? Include instructions for library installation, linking required CSS/assets, applying an application theme, and coding a simple working app.', + }, + { + title: '📄 DevExtreme Licensing', + description: 'What are the licensing options for DevExtreme?', + prompt: 'Which DevExtreme license do I need for a commercial project? What licensing options are available?', + }, +]; diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/index.html b/apps/demos/Demos/Chat/MessageStreaming/React/index.html new file mode 100644 index 000000000000..ee451f8288ff --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/index.html @@ -0,0 +1,24 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/index.tsx b/apps/demos/Demos/Chat/MessageStreaming/React/index.tsx new file mode 100644 index 000000000000..8acbec4b6179 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App.tsx'; + +ReactDOM.render( + , + document.getElementById('app'), +); diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/service.ts b/apps/demos/Demos/Chat/MessageStreaming/React/service.ts new file mode 100644 index 000000000000..6d478e57bef6 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/service.ts @@ -0,0 +1,59 @@ +import { AzureOpenAI, OpenAI } from 'openai'; + +export type AIMessage = ( + OpenAI.ChatCompletionUserMessageParam + | OpenAI.ChatCompletionSystemMessageParam + | OpenAI.ChatCompletionAssistantMessageParam) & { + content: string; + }; + +interface GetAIResponseStreamOptions { + onAborted: () => void; + onDelta: (delta: string) => void; + onError?: (error: unknown) => void; + signal: AbortSignal; +} + +const AzureOpenAIConfig = { + dangerouslyAllowBrowser: true, + deployment: 'gpt-4o-mini', + apiVersion: '2024-02-01', + endpoint: 'https://public-api.devexpress.com/demo-openai', + apiKey: 'DEMO', +}; + +const chatService = new AzureOpenAI(AzureOpenAIConfig); + +export async function getAIResponseStream( + messages: AIMessage[], + { onAborted, onDelta, onError, signal }: GetAIResponseStreamOptions, +): Promise { + const params = { + messages, + model: AzureOpenAIConfig.deployment, + max_tokens: 1000, + temperature: 0.7, + stream: true as const, + }; + + try { + const stream = await chatService.chat.completions.create(params, { signal }); + + for await (const event of stream) { + const delta = event.choices?.[0]?.delta?.content; + if (delta) { + onDelta(delta); + } + } + + if (signal.aborted) { + onAborted(); + } + } catch (e) { + if ((e as Error)?.name === 'AbortError') { + onAborted(); + } + onError?.(e); + throw e; + } +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/styles.css b/apps/demos/Demos/Chat/MessageStreaming/React/styles.css new file mode 100644 index 000000000000..22eac67b2934 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/styles.css @@ -0,0 +1,110 @@ +#app { + display: flex; + justify-content: center; +} + +.dx-chat { + max-width: 900px; +} + +.dx-chat-messagelist-empty-image { + display: none; +} + +.dx-chat-messagelist-empty-message { + font-size: var(--dx-font-size-heading-5); +} + +.dx-chat-messagebubble-content, +.chat-messagebubble-text { + display: flex; + flex-direction: column; +} + +.bubble-button-container { + display: none; +} + +.dx-button { + display: inline-block; + color: var(--dx-color-icon); +} + +.dx-chat-messagegroup-alignment-start:last-child .dx-chat-messagebubble:last-child .bubble-button-container { + display: flex; + gap: 4px; + margin-top: 8px; +} + +.dx-chat-messagebubble-content > div > p:first-child { + margin-top: 0; +} + +.dx-chat-messagebubble-content > div > p:last-child { + margin-bottom: 0; +} + +.dx-chat-messagebubble-content h1, +.dx-chat-messagebubble-content h2, +.dx-chat-messagebubble-content h3, +.dx-chat-messagebubble-content h4, +.dx-chat-messagebubble-content h5, +.dx-chat-messagebubble-content h6 { + font-size: revert; + font-weight: revert; +} + +.chat-disabled .dx-chat-messagebox { + opacity: 0.5; + pointer-events: none; +} + +.dx-chat-suggestion-cards { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 16px; + margin-top: 32px; + width: 100%; +} + +.dx-chat-suggestion-card { + border-radius: 12px; + padding: 16px; + border: 1px solid #EBEBEB; + background: #FAFAFA; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + flex: 0 1 230px; + max-width: 230px; + text-align: left; + cursor: pointer; + transition: 0.2s ease; + width: 230px; +} + +.dx-chat-suggestion-card:hover { + border: 1px solid #E0E0E0; + background: #F5F5F5; + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.04), 0 4px 24px 0 rgba(0, 0, 0, 0.02); +} + +.dx-chat-suggestion-card-title { + color: #242424; + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +.dx-chat-suggestion-card-prompt { + color: #616161; + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +.dx-chat-messagelist-empty-prompt { + margin-top: 4px; +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/React/useApi.ts b/apps/demos/Demos/Chat/MessageStreaming/React/useApi.ts new file mode 100644 index 000000000000..fd5d459b3553 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/React/useApi.ts @@ -0,0 +1,188 @@ +import { useCallback, useRef, useState } from 'react'; +import type { ChatTypes } from 'devextreme-react/chat'; +import { CustomStore, DataSource } from 'devextreme-react/common/data'; +import { + ALERT_TIMEOUT, + assistant, +} from './data.ts'; +import { getAIResponseStream } from './service.ts'; +import type { AIMessage } from './service.ts'; + +const store: ChatTypes.Message[] = []; + +const customStore = new CustomStore({ + key: 'id', + load: (): Promise => new Promise((resolve) => { + setTimeout(() => { + resolve([...store]); + }, 0); + }), + insert: (message: ChatTypes.Message): Promise => new Promise((resolve) => { + setTimeout(() => { + store.push(message); + resolve(message); + }); + }), +}); + +export const dataSource = new DataSource({ + store: customStore, + paginate: false, +}); + +const dataItemToMessage = (item: ChatTypes.Message): AIMessage => ({ + role: item.author?.id as AIMessage['role'], + content: item.text, +}); + +const getMessageHistory = (): AIMessage[] => [...dataSource.items()].map(dataItemToMessage); + +interface DelayedRendererOptions { + delay?: number; + onRender: (chunk: string) => void; +} + +function createDelayedRenderer({ delay = 20, onRender }: DelayedRendererOptions) { + let queue: string[] = []; + let rendering = false; + + function processQueue() { + if (!queue.length) { + rendering = false; + return; + } + + rendering = true; + const chunk = queue.shift(); + if (chunk !== undefined) { + onRender(chunk); + } + + setTimeout(processQueue, delay); + } + + function pushChunk(chunk: string) { + queue.push(chunk); + + if (!rendering) { + processQueue(); + } + } + + function stop() { + queue = []; + rendering = false; + } + + return { pushChunk, stop }; +} + +export const useApi = () => { + const [alerts, setAlerts] = useState([]); + const [typingUsers, setTypingUsers] = useState([]); + const [isStreaming, setIsStreaming] = useState(false); + const abortControllerRef = useRef(null); + + const insertMessage = useCallback((data: ChatTypes.Message): void => { + dataSource.store().push([{ type: 'insert', data }]); + }, []); + + const updateMessageText = useCallback((id: number, text: string): void => { + dataSource.store().push([{ + type: 'update', + key: id, + data: { text }, + }]); + }, []); + + const insertAssistantPlaceholder = useCallback((): number => { + const id = Date.now(); + dataSource.store().push([{ + type: 'insert', + data: { + id, + timestamp: new Date(), + author: assistant, + text: '', + }, + }]); + return id; + }, []); + + const alertLimitReached = useCallback((): void => { + setAlerts([{ + message: 'Request limit reached, try again in a minute.', + }]); + + setTimeout(() => { + setAlerts([]); + }, ALERT_TIMEOUT); + }, []); + + const stopStreaming = useCallback((): void => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }, []); + + const fetchAIResponse = useCallback(async (message: ChatTypes.Message): Promise => { + const messages = [...getMessageHistory(), dataItemToMessage(message)]; + abortControllerRef.current = new AbortController(); + + setTimeout(() => setIsStreaming(true), 0); + setTypingUsers([assistant]); + + let assistantId: number | undefined; + let buffer = ''; + let typingCleared = false; + + const delayedRenderer = createDelayedRenderer({ + onRender: (chunk: string) => { + if (!typingCleared) { + setTypingUsers([]); + typingCleared = true; + } + + if (assistantId === undefined) { + assistantId = insertAssistantPlaceholder(); + } + + buffer += chunk; + updateMessageText(assistantId, buffer); + }, + }); + + const onAborted = () => { + delayedRenderer.stop(); + }; + + try { + await getAIResponseStream(messages, { + onAborted, + onDelta: delayedRenderer.pushChunk, + signal: abortControllerRef.current.signal, + }); + + setTypingUsers([]); + } catch (e: unknown) { + setTypingUsers([]); + + if ((e as Error)?.name !== 'AbortError' && assistantId !== undefined) { + updateMessageText(assistantId, ''); + alertLimitReached(); + } + } finally { + abortControllerRef.current = null; + setIsStreaming(false); + } + }, [alertLimitReached, insertAssistantPlaceholder, updateMessageText]); + + return { + alerts, + typingUsers, + isStreaming, + insertMessage, + fetchAIResponse, + stopStreaming, + }; +}; diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/App.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/App.js new file mode 100644 index 000000000000..677e16e02f95 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/App.js @@ -0,0 +1,87 @@ +import React, { useCallback, useMemo } from 'react'; +import Chat from 'devextreme-react/chat'; +import { loadMessages } from 'devextreme-react/common/core/localization'; +import { user } from './data.js'; +import Message from './Message.js'; +import EmptyView from './EmptyView.js'; +import { dataSource, useApi } from './useApi.js'; + +loadMessages({ + en: { + 'dxChat-emptyListMessage': 'Chat is Empty', + 'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.', + 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', + }, +}); +export default function App() { + const { + alerts, typingUsers, isStreaming, insertMessage, fetchAIResponse, stopStreaming, + } = + useApi(); + const onMessageEntered = useCallback( + async ({ message }) => { + insertMessage({ id: Date.now(), ...message }); + if (!alerts.length) { + await fetchAIResponse(message); + } + }, + [insertMessage, alerts.length, fetchAIResponse], + ); + const sendSuggestion = useCallback( + (prompt) => { + const message = { + id: Date.now(), + timestamp: new Date(), + author: user, + text: prompt, + }; + insertMessage(message); + if (!alerts.length) { + fetchAIResponse(message); + } + }, + [insertMessage, alerts.length, fetchAIResponse], + ); + const messageRender = useCallback(({ message }) => , []); + const emptyViewRender = useCallback( + ({ texts }) => ( + + ), + [sendSuggestion], + ); + const sendButtonOptions = useMemo( + () => + isStreaming + ? { + action: 'custom', + icon: 'stopfilled', + onClick: stopStreaming, + } + : { + action: 'send', + icon: 'arrowright', + onClick: () => {}, + }, + [isStreaming, stopStreaming], + ); + return ( + + ); +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/EmptyView.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/EmptyView.js new file mode 100644 index 000000000000..4baf9f6eaee0 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/EmptyView.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { suggestionCards } from './data.js'; + +const EmptyView = ({ texts, onSuggestionClick }) => ( +
+
{texts.message}
+
{texts.prompt}
+
+ {suggestionCards.map((card) => ( + + ))} +
+
+); +export default EmptyView; diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/Message.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/Message.js new file mode 100644 index 000000000000..bea9a2bff9cf --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/Message.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; +import rehypeStringify from 'rehype-stringify'; +import HTMLReactParser from 'html-react-parser'; + +function convertToHtml(value) { + return unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeMinifyWhitespace) + .use(rehypeStringify) + .processSync(value) + .toString(); +} +const Message = ({ text }) => ( +
{HTMLReactParser(convertToHtml(text))}
+); +export default Message; diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/data.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/data.js new file mode 100644 index 000000000000..9573773eae88 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/data.js @@ -0,0 +1,27 @@ +export const ALERT_TIMEOUT = 1000 * 60; +export const user = { + id: 'user', +}; +export const assistant = { + id: 'assistant', + name: 'AI Assistant', +}; +export const suggestionCards = [ + { + title: '💡 What is DevExtreme?', + description: 'What is DevExtreme and how can it help me build modern web apps?', + prompt: 'What is DevExtreme, and which components and frameworks does it support?', + }, + { + title: '🚀 Get Started with DevExtreme', + description: 'How do I get started with DevExtreme in my project?', + prompt: + 'How can I get started with DevExtreme? Include instructions for library installation, linking required CSS/assets, applying an application theme, and coding a simple working app.', + }, + { + title: '📄 DevExtreme Licensing', + description: 'What are the licensing options for DevExtreme?', + prompt: + 'Which DevExtreme license do I need for a commercial project? What licensing options are available?', + }, +]; diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.html b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.html new file mode 100644 index 000000000000..db31b0fd60c6 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.html @@ -0,0 +1,44 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.js new file mode 100644 index 000000000000..b853e0be8242 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App.js'; + +ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/service.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/service.js new file mode 100644 index 000000000000..55d10b9d72e4 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/service.js @@ -0,0 +1,39 @@ +import { AzureOpenAI } from 'openai'; + +const AzureOpenAIConfig = { + dangerouslyAllowBrowser: true, + deployment: 'gpt-4o-mini', + apiVersion: '2024-02-01', + endpoint: 'https://public-api.devexpress.com/demo-openai', + apiKey: 'DEMO', +}; +const chatService = new AzureOpenAI(AzureOpenAIConfig); +export async function getAIResponseStream(messages, { + onAborted, onDelta, onError, signal, +}) { + const params = { + messages, + model: AzureOpenAIConfig.deployment, + max_tokens: 1000, + temperature: 0.7, + stream: true, + }; + try { + const stream = await chatService.chat.completions.create(params, { signal }); + for await (const event of stream) { + const delta = event.choices?.[0]?.delta?.content; + if (delta) { + onDelta(delta); + } + } + if (signal.aborted) { + onAborted(); + } + } catch (e) { + if (e?.name === 'AbortError') { + onAborted(); + } + onError?.(e); + throw e; + } +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/styles.css b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/styles.css new file mode 100644 index 000000000000..22eac67b2934 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/styles.css @@ -0,0 +1,110 @@ +#app { + display: flex; + justify-content: center; +} + +.dx-chat { + max-width: 900px; +} + +.dx-chat-messagelist-empty-image { + display: none; +} + +.dx-chat-messagelist-empty-message { + font-size: var(--dx-font-size-heading-5); +} + +.dx-chat-messagebubble-content, +.chat-messagebubble-text { + display: flex; + flex-direction: column; +} + +.bubble-button-container { + display: none; +} + +.dx-button { + display: inline-block; + color: var(--dx-color-icon); +} + +.dx-chat-messagegroup-alignment-start:last-child .dx-chat-messagebubble:last-child .bubble-button-container { + display: flex; + gap: 4px; + margin-top: 8px; +} + +.dx-chat-messagebubble-content > div > p:first-child { + margin-top: 0; +} + +.dx-chat-messagebubble-content > div > p:last-child { + margin-bottom: 0; +} + +.dx-chat-messagebubble-content h1, +.dx-chat-messagebubble-content h2, +.dx-chat-messagebubble-content h3, +.dx-chat-messagebubble-content h4, +.dx-chat-messagebubble-content h5, +.dx-chat-messagebubble-content h6 { + font-size: revert; + font-weight: revert; +} + +.chat-disabled .dx-chat-messagebox { + opacity: 0.5; + pointer-events: none; +} + +.dx-chat-suggestion-cards { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 16px; + margin-top: 32px; + width: 100%; +} + +.dx-chat-suggestion-card { + border-radius: 12px; + padding: 16px; + border: 1px solid #EBEBEB; + background: #FAFAFA; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + flex: 0 1 230px; + max-width: 230px; + text-align: left; + cursor: pointer; + transition: 0.2s ease; + width: 230px; +} + +.dx-chat-suggestion-card:hover { + border: 1px solid #E0E0E0; + background: #F5F5F5; + box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 0.04), 0 4px 24px 0 rgba(0, 0, 0, 0.02); +} + +.dx-chat-suggestion-card-title { + color: #242424; + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +.dx-chat-suggestion-card-prompt { + color: #616161; + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +.dx-chat-messagelist-empty-prompt { + margin-top: 4px; +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/ReactJs/useApi.js b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/useApi.js new file mode 100644 index 000000000000..8ad6561ef71e --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/ReactJs/useApi.js @@ -0,0 +1,159 @@ +import { useCallback, useRef, useState } from 'react'; +import { CustomStore, DataSource } from 'devextreme-react/common/data'; +import { ALERT_TIMEOUT, assistant } from './data.js'; +import { getAIResponseStream } from './service.js'; + +const store = []; +const customStore = new CustomStore({ + key: 'id', + load: () => + new Promise((resolve) => { + setTimeout(() => { + resolve([...store]); + }, 0); + }), + insert: (message) => + new Promise((resolve) => { + setTimeout(() => { + store.push(message); + resolve(message); + }); + }), +}); +export const dataSource = new DataSource({ + store: customStore, + paginate: false, +}); +const dataItemToMessage = (item) => ({ + role: item.author?.id, + content: item.text, +}); +const getMessageHistory = () => [...dataSource.items()].map(dataItemToMessage); +function createDelayedRenderer({ delay = 20, onRender }) { + let queue = []; + let rendering = false; + function processQueue() { + if (!queue.length) { + rendering = false; + return; + } + rendering = true; + const chunk = queue.shift(); + if (chunk !== undefined) { + onRender(chunk); + } + setTimeout(processQueue, delay); + } + function pushChunk(chunk) { + queue.push(chunk); + if (!rendering) { + processQueue(); + } + } + function stop() { + queue = []; + rendering = false; + } + return { pushChunk, stop }; +} +export const useApi = () => { + const [alerts, setAlerts] = useState([]); + const [typingUsers, setTypingUsers] = useState([]); + const [isStreaming, setIsStreaming] = useState(false); + const abortControllerRef = useRef(null); + const insertMessage = useCallback((data) => { + dataSource.store().push([{ type: 'insert', data }]); + }, []); + const updateMessageText = useCallback((id, text) => { + dataSource.store().push([ + { + type: 'update', + key: id, + data: { text }, + }, + ]); + }, []); + const insertAssistantPlaceholder = useCallback(() => { + const id = Date.now(); + dataSource.store().push([ + { + type: 'insert', + data: { + id, + timestamp: new Date(), + author: assistant, + text: '', + }, + }, + ]); + return id; + }, []); + const alertLimitReached = useCallback(() => { + setAlerts([ + { + message: 'Request limit reached, try again in a minute.', + }, + ]); + setTimeout(() => { + setAlerts([]); + }, ALERT_TIMEOUT); + }, []); + const stopStreaming = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }, []); + const fetchAIResponse = useCallback( + async (message) => { + const messages = [...getMessageHistory(), dataItemToMessage(message)]; + abortControllerRef.current = new AbortController(); + setTimeout(() => setIsStreaming(true), 0); + setTypingUsers([assistant]); + let assistantId; + let buffer = ''; + let typingCleared = false; + const delayedRenderer = createDelayedRenderer({ + onRender: (chunk) => { + if (!typingCleared) { + setTypingUsers([]); + typingCleared = true; + } + if (assistantId === undefined) { + assistantId = insertAssistantPlaceholder(); + } + buffer += chunk; + updateMessageText(assistantId, buffer); + }, + }); + const onAborted = () => { + delayedRenderer.stop(); + }; + try { + await getAIResponseStream(messages, { + onAborted, + onDelta: delayedRenderer.pushChunk, + signal: abortControllerRef.current.signal, + }); + setTypingUsers([]); + } catch (e) { + setTypingUsers([]); + if (e?.name !== 'AbortError' && assistantId !== undefined) { + updateMessageText(assistantId, ''); + alertLimitReached(); + } + } finally { + abortControllerRef.current = null; + setIsStreaming(false); + } + }, + [alertLimitReached, insertAssistantPlaceholder, updateMessageText], + ); + return { + alerts, + typingUsers, + isStreaming, + insertMessage, + fetchAIResponse, + stopStreaming, + }; +}; diff --git a/apps/demos/Demos/Chat/MessageStreaming/Vue/App.vue b/apps/demos/Demos/Chat/MessageStreaming/Vue/App.vue new file mode 100644 index 000000000000..af8e023dc224 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Vue/App.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/apps/demos/Demos/Chat/MessageStreaming/Vue/data.ts b/apps/demos/Demos/Chat/MessageStreaming/Vue/data.ts new file mode 100644 index 000000000000..436e3c8f6174 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Vue/data.ts @@ -0,0 +1,76 @@ +import { CustomStore, DataSource } from 'devextreme-vue/common/data'; +import { type DxChatTypes } from 'devextreme-vue/chat'; +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeStringify from 'rehype-stringify'; +import rehypeMinifyWhitespace from 'rehype-minify-whitespace'; + +export const dictionary = { + en: { + 'dxChat-emptyListMessage': 'Chat is Empty', + 'dxChat-emptyListPrompt': 'AI Assistant is ready to answer your questions.', + 'dxChat-textareaPlaceholder': 'Ask AI Assistant...', + }, +}; + +export const ALERT_TIMEOUT = 1000 * 60; + +export const user: DxChatTypes.User = { + id: 'user', +}; + +export const assistant: DxChatTypes.User = { + id: 'assistant', + name: 'AI Assistant', +}; + +export const suggestionCards = [ + { + title: '💡 What is DevExtreme?', + description: 'What is DevExtreme and how can it help me build modern web apps?', + prompt: 'What is DevExtreme, and which components and frameworks does it support?', + }, + { + title: '🚀 Get Started with DevExtreme', + description: 'How do I get started with DevExtreme in my project?', + prompt: 'How can I get started with DevExtreme? Include instructions for library installation, linking required CSS/assets, applying an application theme, and coding a simple working app.', + }, + { + title: '📄 DevExtreme Licensing', + description: 'What are the licensing options for DevExtreme?', + prompt: 'Which DevExtreme license do I need for a commercial project? What licensing options are available?', + }, +]; + +const store: DxChatTypes.Message[] = []; + +const customStore = new CustomStore({ + key: 'id', + load: () => new Promise((resolve) => { + setTimeout(() => { + resolve([...store]); + }, 0); + }), + insert: (message: DxChatTypes.Message) => new Promise((resolve) => { + setTimeout(() => { + store.push(message); + resolve(message); + }); + }), +}); + +export const dataSource = new DataSource({ + store: customStore, + paginate: false, +}); + +export function convertToHtml(value: string): string { + return unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeMinifyWhitespace) + .use(rehypeStringify) + .processSync(value) + .toString(); +} diff --git a/apps/demos/Demos/Chat/MessageStreaming/Vue/index.html b/apps/demos/Demos/Chat/MessageStreaming/Vue/index.html new file mode 100644 index 000000000000..2413f2254bf1 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Vue/index.html @@ -0,0 +1,29 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/Chat/MessageStreaming/Vue/index.ts b/apps/demos/Demos/Chat/MessageStreaming/Vue/index.ts new file mode 100644 index 000000000000..684d04215d72 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Vue/index.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); diff --git a/apps/demos/Demos/Chat/MessageStreaming/Vue/service.ts b/apps/demos/Demos/Chat/MessageStreaming/Vue/service.ts new file mode 100644 index 000000000000..cd195ada0646 --- /dev/null +++ b/apps/demos/Demos/Chat/MessageStreaming/Vue/service.ts @@ -0,0 +1,59 @@ +import { AzureOpenAI, OpenAI } from 'openai'; + +export type AIMessage = ( + OpenAI.ChatCompletionUserMessageParam + | OpenAI.ChatCompletionSystemMessageParam + | OpenAI.ChatCompletionAssistantMessageParam) & { + content: string; + }; + +export interface GetAIResponseStreamOptions { + onAborted: () => void; + onDelta: (delta: string) => void; + onError?: (error: unknown) => void; + signal: AbortSignal; +} + +const AzureOpenAIConfig = { + dangerouslyAllowBrowser: true, + deployment: 'gpt-4o-mini', + apiVersion: '2024-02-01', + endpoint: 'https://public-api.devexpress.com/demo-openai', + apiKey: 'DEMO', +}; + +const chatService = new AzureOpenAI(AzureOpenAIConfig); + +export async function getAIResponseStream( + messages: AIMessage[], + { onAborted, onDelta, onError, signal }: GetAIResponseStreamOptions, +): Promise { + const params = { + messages, + model: AzureOpenAIConfig.deployment, + max_tokens: 1000, + temperature: 0.7, + stream: true as const, + }; + + try { + const stream = await chatService.chat.completions.create(params, { signal }); + + for await (const event of stream) { + const delta = event.choices?.[0]?.delta?.content; + if (delta) { + onDelta(delta); + } + } + + if (signal.aborted) { + onAborted(); + } + } catch (e) { + if ((e as Error)?.name === 'AbortError') { + onAborted(); + } + onError?.(e); + throw e; + } +} diff --git a/apps/demos/testing/etalons/Chat-MessageStreaming (fluent.blue.light).png b/apps/demos/testing/etalons/Chat-MessageStreaming (fluent.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..acca37b04bf6654aa2e3c544c96356c6570c6b47 GIT binary patch literal 43845 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 A6aWAK literal 0 HcmV?d00001 diff --git a/apps/demos/testing/etalons/Chat-MessageStreaming (material.blue.light).png b/apps/demos/testing/etalons/Chat-MessageStreaming (material.blue.light).png new file mode 100644 index 0000000000000000000000000000000000000000..88af99c9caf6d7ad74ebc68946c75ce0dc960695 GIT binary patch literal 44465 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