From ffee626c84d90f52b86f858d8abbba486ab22857 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:14:11 +0300 Subject: [PATCH 1/8] Chat: Add ToolCall types and TextMessage.toolCalls --- packages/devextreme/js/ui/chat.d.ts | 43 +++++++++++++++++++++++ packages/devextreme/js/ui/chat_types.d.ts | 2 ++ packages/devextreme/ts/dx.all.d.ts | 31 ++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/packages/devextreme/js/ui/chat.d.ts b/packages/devextreme/js/ui/chat.d.ts index 8f206c47dd45..7dab06242d47 100644 --- a/packages/devextreme/js/ui/chat.d.ts +++ b/packages/devextreme/js/ui/chat.d.ts @@ -249,6 +249,43 @@ export type Attachment = { [key: string]: any; }; +/** + * @docid + * @namespace DevExpress.ui.dxChat + * @public + */ +export type ToolCallStatus = 'success' | 'error'; + +/** + * @docid + * @namespace DevExpress.ui.dxChat + * @public + */ +export type ToolCall = { + /** + * @docid + * @public + */ + name: string; + /** + * @docid + * @public + */ + arguments?: Record; + /** + * @docid + * @public + */ + result?: any; + /** + * @docid + * @public + */ + status?: ToolCallStatus; + + [key: string]: any; +}; + /** * @docid * @namespace DevExpress.ui.dxChat @@ -315,6 +352,12 @@ export type TextMessage = MessageBase & { * @public */ isEdited?: boolean; + /** + * @docid + * @public + * @type Array + */ + toolCalls?: ToolCall[]; [key: string]: any; }; diff --git a/packages/devextreme/js/ui/chat_types.d.ts b/packages/devextreme/js/ui/chat_types.d.ts index 08de8b85118a..13c0b065323c 100644 --- a/packages/devextreme/js/ui/chat_types.d.ts +++ b/packages/devextreme/js/ui/chat_types.d.ts @@ -17,6 +17,8 @@ export { User, Alert, Attachment, + ToolCallStatus, + ToolCall, MessageType, TextMessage, ImageMessage, diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 30544588de29..10b66ddf338f 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -34987,9 +34987,40 @@ declare module DevExpress.ui.dxChat { * [descr:TextMessage.isEdited] */ isEdited?: boolean; + /** + * [descr:TextMessage.toolCalls] + */ + toolCalls?: ToolCall[]; + + [key: string]: any; + }; + /** + * [descr:ToolCall] + */ + export type ToolCall = { + /** + * [descr:ToolCall.name] + */ + name: string; + /** + * [descr:ToolCall.arguments] + */ + arguments?: Record; + /** + * [descr:ToolCall.result] + */ + result?: any; + /** + * [descr:ToolCall.status] + */ + status?: ToolCallStatus; [key: string]: any; }; + /** + * [descr:ToolCallStatus] + */ + export type ToolCallStatus = 'success' | 'error'; /** * [descr:User] */ From 15ef81e649f3b129b210e6da13dabc7a1ab38de3 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:14:56 +0300 Subject: [PATCH 2/8] Chat: Add tool call localization messages --- packages/devextreme/js/localization/messages/ar.json | 6 ++++++ packages/devextreme/js/localization/messages/bg.json | 6 ++++++ packages/devextreme/js/localization/messages/ca.json | 6 ++++++ packages/devextreme/js/localization/messages/cs.json | 6 ++++++ packages/devextreme/js/localization/messages/da.json | 6 ++++++ packages/devextreme/js/localization/messages/de.json | 6 ++++++ packages/devextreme/js/localization/messages/el.json | 6 ++++++ packages/devextreme/js/localization/messages/en.json | 6 ++++++ packages/devextreme/js/localization/messages/es.json | 6 ++++++ packages/devextreme/js/localization/messages/fa.json | 6 ++++++ packages/devextreme/js/localization/messages/fi.json | 6 ++++++ packages/devextreme/js/localization/messages/fr.json | 6 ++++++ packages/devextreme/js/localization/messages/hi.json | 6 ++++++ packages/devextreme/js/localization/messages/hu.json | 6 ++++++ packages/devextreme/js/localization/messages/it.json | 6 ++++++ packages/devextreme/js/localization/messages/ja.json | 6 ++++++ packages/devextreme/js/localization/messages/ko.json | 6 ++++++ packages/devextreme/js/localization/messages/lt.json | 6 ++++++ packages/devextreme/js/localization/messages/lv.json | 6 ++++++ packages/devextreme/js/localization/messages/nb.json | 6 ++++++ packages/devextreme/js/localization/messages/nl.json | 6 ++++++ packages/devextreme/js/localization/messages/pl.json | 6 ++++++ packages/devextreme/js/localization/messages/pt-BR.json | 6 ++++++ packages/devextreme/js/localization/messages/pt-PT.json | 6 ++++++ packages/devextreme/js/localization/messages/pt.json | 6 ++++++ packages/devextreme/js/localization/messages/ro.json | 6 ++++++ packages/devextreme/js/localization/messages/ru.json | 6 ++++++ packages/devextreme/js/localization/messages/sl.json | 6 ++++++ packages/devextreme/js/localization/messages/sv.json | 6 ++++++ packages/devextreme/js/localization/messages/tr.json | 6 ++++++ packages/devextreme/js/localization/messages/uk.json | 6 ++++++ packages/devextreme/js/localization/messages/vi.json | 6 ++++++ packages/devextreme/js/localization/messages/zh-CN.json | 6 ++++++ packages/devextreme/js/localization/messages/zh-Hans.json | 6 ++++++ packages/devextreme/js/localization/messages/zh-tw.json | 6 ++++++ packages/devextreme/js/localization/messages/zh.json | 6 ++++++ 36 files changed, 216 insertions(+) diff --git a/packages/devextreme/js/localization/messages/ar.json b/packages/devextreme/js/localization/messages/ar.json index 5481e4698ad8..4345f2ec3cf7 100644 --- a/packages/devextreme/js/localization/messages/ar.json +++ b/packages/devextreme/js/localization/messages/ar.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "قائمة الملفات", "dxChat-downloadButtonLabel": "تنزيل الملف {0}", "dxChat-fileLimitReachedWarning": "لقد حددت عددًا كبيرًا جدًا من الملفات. حدد {0} ملفات كحد أقصى وأعد المحاولة.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "أحمر", "dxColorView-ariaGreen": "أخضر", diff --git a/packages/devextreme/js/localization/messages/bg.json b/packages/devextreme/js/localization/messages/bg.json index ed237704ffed..7765295b6540 100644 --- a/packages/devextreme/js/localization/messages/bg.json +++ b/packages/devextreme/js/localization/messages/bg.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Списък с файлове", "dxChat-downloadButtonLabel": "Изтегляне на файл {0}", "dxChat-fileLimitReachedWarning": "Избрахте прекалено много файлове. Изберете не повече от {0} файла и опитайте отново.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Червен", "dxColorView-ariaGreen": "Зелен", diff --git a/packages/devextreme/js/localization/messages/ca.json b/packages/devextreme/js/localization/messages/ca.json index 36d3fc31030c..da8084da4035 100644 --- a/packages/devextreme/js/localization/messages/ca.json +++ b/packages/devextreme/js/localization/messages/ca.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Llista de fitxers", "dxChat-downloadButtonLabel": "Descarrega el fitxer {0}", "dxChat-fileLimitReachedWarning": "Heu seleccionat massa fitxers. Seleccioneu-ne com a màxim {0} i torneu-ho a provar.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Vermell", "dxColorView-ariaGreen": "Verd", diff --git a/packages/devextreme/js/localization/messages/cs.json b/packages/devextreme/js/localization/messages/cs.json index 67335ccae6c3..572ea011df1b 100644 --- a/packages/devextreme/js/localization/messages/cs.json +++ b/packages/devextreme/js/localization/messages/cs.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Seznam souborů", "dxChat-downloadButtonLabel": "Stáhnout soubor {0}", "dxChat-fileLimitReachedWarning": "Vybrali jste příliš mnoho souborů. Vyberte nejvýše {0} souborů a opakujte akci.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Červená", "dxColorView-ariaGreen": "Zelená", diff --git a/packages/devextreme/js/localization/messages/da.json b/packages/devextreme/js/localization/messages/da.json index f40d9f640af9..f33c46deddf5 100644 --- a/packages/devextreme/js/localization/messages/da.json +++ b/packages/devextreme/js/localization/messages/da.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Filliste", "dxChat-downloadButtonLabel": "Download fil {0}", "dxChat-fileLimitReachedWarning": "Du valgte for mange filer. Vælg højst {0} filer og prøv igen.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rød", "dxColorView-ariaGreen": "Grøn", diff --git a/packages/devextreme/js/localization/messages/de.json b/packages/devextreme/js/localization/messages/de.json index 03ca59bd785c..aeabe5446cb9 100644 --- a/packages/devextreme/js/localization/messages/de.json +++ b/packages/devextreme/js/localization/messages/de.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Dateiliste", "dxChat-downloadButtonLabel": "Datei {0} herunterladen", "dxChat-fileLimitReachedWarning": "Sie haben zu viele Dateien ausgewählt. Wählen Sie höchstens {0} Dateien aus und versuchen Sie es erneut.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rot", "dxColorView-ariaGreen": "Grün", diff --git a/packages/devextreme/js/localization/messages/el.json b/packages/devextreme/js/localization/messages/el.json index a7d5a18060a9..a4a0caf38d27 100644 --- a/packages/devextreme/js/localization/messages/el.json +++ b/packages/devextreme/js/localization/messages/el.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Λίστα αρχείων", "dxChat-downloadButtonLabel": "Λήψη αρχείου {0}", "dxChat-fileLimitReachedWarning": "Επιλέξατε πολλά αρχεία. Επιλέξτε έως {0} αρχεία και δοκιμάστε ξανά.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Κόκκινο", "dxColorView-ariaGreen": "Πράσινο", diff --git a/packages/devextreme/js/localization/messages/en.json b/packages/devextreme/js/localization/messages/en.json index b351d7dca066..edcaa87be0b5 100644 --- a/packages/devextreme/js/localization/messages/en.json +++ b/packages/devextreme/js/localization/messages/en.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "File list", "dxChat-downloadButtonLabel": "Download file {0}", "dxChat-fileLimitReachedWarning": "You selected too many files. Select no more than {0} files and retry.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Red", "dxColorView-ariaGreen": "Green", diff --git a/packages/devextreme/js/localization/messages/es.json b/packages/devextreme/js/localization/messages/es.json index c21f7180c064..ba3abf54abcd 100644 --- a/packages/devextreme/js/localization/messages/es.json +++ b/packages/devextreme/js/localization/messages/es.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista de archivos", "dxChat-downloadButtonLabel": "Descargar archivo {0}", "dxChat-fileLimitReachedWarning": "Ha seleccionado demasiados archivos. Seleccione no más de {0} archivos y reintente.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rojo", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/fa.json b/packages/devextreme/js/localization/messages/fa.json index c7a8f14662d9..312f7de501a3 100644 --- a/packages/devextreme/js/localization/messages/fa.json +++ b/packages/devextreme/js/localization/messages/fa.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "لیست فایل", "dxChat-downloadButtonLabel": "دانلود فایل {0}", "dxChat-fileLimitReachedWarning": "تعداد فایل‌های انتخاب شده زیاد است. حداکثر {0} فایل انتخاب کنید و دوباره امتحان کنید.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "قرمز", "dxColorView-ariaGreen": "سبز", diff --git a/packages/devextreme/js/localization/messages/fi.json b/packages/devextreme/js/localization/messages/fi.json index 85428935b970..2dd1b2525e86 100644 --- a/packages/devextreme/js/localization/messages/fi.json +++ b/packages/devextreme/js/localization/messages/fi.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Tiedostoluettelo", "dxChat-downloadButtonLabel": "Lataa tiedosto {0}", "dxChat-fileLimitReachedWarning": "Valitsit liian monta tiedostoa. Valitse enintään {0} tiedostoa ja yritä uudelleen.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Punainen", "dxColorView-ariaGreen": "Vihreä", diff --git a/packages/devextreme/js/localization/messages/fr.json b/packages/devextreme/js/localization/messages/fr.json index 75e73508a044..7b6f19ec2e80 100644 --- a/packages/devextreme/js/localization/messages/fr.json +++ b/packages/devextreme/js/localization/messages/fr.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Liste des fichiers", "dxChat-downloadButtonLabel": "Télécharger le fichier {0}", "dxChat-fileLimitReachedWarning": "Vous avez sélectionné trop de fichiers. Sélectionnez au maximum {0} fichiers et réessayez.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rouge", "dxColorView-ariaGreen": "Vert", diff --git a/packages/devextreme/js/localization/messages/hi.json b/packages/devextreme/js/localization/messages/hi.json index 44809ab806c5..ade59bd5beaf 100644 --- a/packages/devextreme/js/localization/messages/hi.json +++ b/packages/devextreme/js/localization/messages/hi.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "फ़ाइल सूची", "dxChat-downloadButtonLabel": "फ़ाइल डाउनलोड करें {0}", "dxChat-fileLimitReachedWarning": "आपने बहुत सारी फ़ाइलें चुनीं. {0} से अधिक फ़ाइलें न चुनें और पुनः प्रयास करें।", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "लाल", "dxColorView-ariaGreen": "हरा", diff --git a/packages/devextreme/js/localization/messages/hu.json b/packages/devextreme/js/localization/messages/hu.json index 62b96ecfa12a..40aba3cb1442 100644 --- a/packages/devextreme/js/localization/messages/hu.json +++ b/packages/devextreme/js/localization/messages/hu.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Fájllista", "dxChat-downloadButtonLabel": "{0} fájl letöltése", "dxChat-fileLimitReachedWarning": "Túl sok fájlt választott. Válasszon legfeljebb {0} fájlt, és próbálja újra.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Piros", "dxColorView-ariaGreen": "Zöld", diff --git a/packages/devextreme/js/localization/messages/it.json b/packages/devextreme/js/localization/messages/it.json index 77b233d386bc..c4526fad81d8 100644 --- a/packages/devextreme/js/localization/messages/it.json +++ b/packages/devextreme/js/localization/messages/it.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista file", "dxChat-downloadButtonLabel": "Scarica file {0}", "dxChat-fileLimitReachedWarning": "Hai selezionato troppi file. Seleziona al massimo {0} file e riprova.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rosso", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/ja.json b/packages/devextreme/js/localization/messages/ja.json index 8ba9516e581e..24b8f76456b6 100644 --- a/packages/devextreme/js/localization/messages/ja.json +++ b/packages/devextreme/js/localization/messages/ja.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "ファイル リスト", "dxChat-downloadButtonLabel": "ファイル {0} をダウンロード", "dxChat-fileLimitReachedWarning": "ファイルが多すぎます。{0} 件以下のファイルを選択してやり直してください。", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "赤", "dxColorView-ariaGreen": "緑", diff --git a/packages/devextreme/js/localization/messages/ko.json b/packages/devextreme/js/localization/messages/ko.json index 108dc23332a7..ff70efa3590c 100644 --- a/packages/devextreme/js/localization/messages/ko.json +++ b/packages/devextreme/js/localization/messages/ko.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "파일 목록", "dxChat-downloadButtonLabel": "파일 {0} 다운로드", "dxChat-fileLimitReachedWarning": "파일을 너무 많이 선택했습니다. {0}개 이하의 파일을 선택하고 다시 시도하세요.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "빨강", "dxColorView-ariaGreen": "초록", diff --git a/packages/devextreme/js/localization/messages/lt.json b/packages/devextreme/js/localization/messages/lt.json index ad1bb8a3bf1a..a88dfc73e445 100644 --- a/packages/devextreme/js/localization/messages/lt.json +++ b/packages/devextreme/js/localization/messages/lt.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Failų sąrašas", "dxChat-downloadButtonLabel": "Atsisiųsti failą {0}", "dxChat-fileLimitReachedWarning": "Pasirinkote per daug failų. Pasirinkite ne daugiau kaip {0} failų ir bandykite dar kartą.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Raudona", "dxColorView-ariaGreen": "Žalia", diff --git a/packages/devextreme/js/localization/messages/lv.json b/packages/devextreme/js/localization/messages/lv.json index 7e590c0f1fc0..bca283d579f5 100644 --- a/packages/devextreme/js/localization/messages/lv.json +++ b/packages/devextreme/js/localization/messages/lv.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Failu saraksts", "dxChat-downloadButtonLabel": "Lejupielādēt failu {0}", "dxChat-fileLimitReachedWarning": "Jūs izvēlējāties pārāk daudz failu. Izvēlieties ne vairāk kā {0} failus un mēģinājiet vēlreiz.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Sarkans", "dxColorView-ariaGreen": "Zaļš", diff --git a/packages/devextreme/js/localization/messages/nb.json b/packages/devextreme/js/localization/messages/nb.json index 58b47de71f6f..569d2dfa44fa 100644 --- a/packages/devextreme/js/localization/messages/nb.json +++ b/packages/devextreme/js/localization/messages/nb.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Filliste", "dxChat-downloadButtonLabel": "Last ned fil {0}", "dxChat-fileLimitReachedWarning": "Du valgte for mange filer. Velg ikke mer enn {0} filer og prøv igjen.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rød", "dxColorView-ariaGreen": "Grønn", diff --git a/packages/devextreme/js/localization/messages/nl.json b/packages/devextreme/js/localization/messages/nl.json index 2a8f64fcc2a1..2cbe90d67cd4 100644 --- a/packages/devextreme/js/localization/messages/nl.json +++ b/packages/devextreme/js/localization/messages/nl.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Bestandslijst", "dxChat-downloadButtonLabel": "Bestand {0} downloaden", "dxChat-fileLimitReachedWarning": "U heeft te veel bestanden geselecteerd. Selecteer maximaal {0} bestanden en probeer opnieuw.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rood", "dxColorView-ariaGreen": "Groen", diff --git a/packages/devextreme/js/localization/messages/pl.json b/packages/devextreme/js/localization/messages/pl.json index 75dd6186c99f..faa204a80c1d 100644 --- a/packages/devextreme/js/localization/messages/pl.json +++ b/packages/devextreme/js/localization/messages/pl.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista plików", "dxChat-downloadButtonLabel": "Pobierz plik {0}", "dxChat-fileLimitReachedWarning": "Wybrano zbyt wiele plików. Wybierz maksymalnie {0} plików i spróbuj ponownie.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "czerwony", "dxColorView-ariaGreen": "zielony", diff --git a/packages/devextreme/js/localization/messages/pt-BR.json b/packages/devextreme/js/localization/messages/pt-BR.json index 014c96fc3145..d5ed9a6a93d3 100644 --- a/packages/devextreme/js/localization/messages/pt-BR.json +++ b/packages/devextreme/js/localization/messages/pt-BR.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista de arquivos", "dxChat-downloadButtonLabel": "Baixar arquivo {0}", "dxChat-fileLimitReachedWarning": "Você selecionou arquivos demais. Selecione no máximo {0} arquivos e tente novamente.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Vermelho", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/pt-PT.json b/packages/devextreme/js/localization/messages/pt-PT.json index 466365cb4fea..67c637ccc489 100644 --- a/packages/devextreme/js/localization/messages/pt-PT.json +++ b/packages/devextreme/js/localization/messages/pt-PT.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista de ficheiros", "dxChat-downloadButtonLabel": "Transferir ficheiro {0}", "dxChat-fileLimitReachedWarning": "Selecionou demasiados ficheiros. Selecione, no máximo, {0} ficheiros e tente novamente.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Vermelho", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/pt.json b/packages/devextreme/js/localization/messages/pt.json index 6b598ae29762..8e2aae2e67cc 100644 --- a/packages/devextreme/js/localization/messages/pt.json +++ b/packages/devextreme/js/localization/messages/pt.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Lista de arquivos", "dxChat-downloadButtonLabel": "Baixar arquivo {0}", "dxChat-fileLimitReachedWarning": "Você selecionou arquivos demais. Selecione no máximo {0} arquivos e tente novamente.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Vermelho", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/ro.json b/packages/devextreme/js/localization/messages/ro.json index dd868ebe305d..89265d451021 100644 --- a/packages/devextreme/js/localization/messages/ro.json +++ b/packages/devextreme/js/localization/messages/ro.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Listă fișiere", "dxChat-downloadButtonLabel": "Descărcați fișierul {0}", "dxChat-fileLimitReachedWarning": "Ați selectat prea multe fișiere. Selectați cel mult {0} fișiere și încercați din nou.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Roșu", "dxColorView-ariaGreen": "Verde", diff --git a/packages/devextreme/js/localization/messages/ru.json b/packages/devextreme/js/localization/messages/ru.json index 1cd2c08413ad..351d78eb7bb4 100644 --- a/packages/devextreme/js/localization/messages/ru.json +++ b/packages/devextreme/js/localization/messages/ru.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Список файлов", "dxChat-downloadButtonLabel": "Скачать файл {0}", "dxChat-fileLimitReachedWarning": "Выбрано слишком много файлов. Выберите не более {0} файлов и повторите попытку.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Красный", "dxColorView-ariaGreen": "Зеленый", diff --git a/packages/devextreme/js/localization/messages/sl.json b/packages/devextreme/js/localization/messages/sl.json index c49db2f4affc..dddc652cb961 100644 --- a/packages/devextreme/js/localization/messages/sl.json +++ b/packages/devextreme/js/localization/messages/sl.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Seznam datotek", "dxChat-downloadButtonLabel": "Prenesi datoteko {0}", "dxChat-fileLimitReachedWarning": "Izbrali ste preveč datotek. Izberite največ {0} datotek in poskusite znova.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Rdeča", "dxColorView-ariaGreen": "Zelena", diff --git a/packages/devextreme/js/localization/messages/sv.json b/packages/devextreme/js/localization/messages/sv.json index 409cd11b3169..7de0f003d343 100644 --- a/packages/devextreme/js/localization/messages/sv.json +++ b/packages/devextreme/js/localization/messages/sv.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Fillista", "dxChat-downloadButtonLabel": "Ladda ner fil {0}", "dxChat-fileLimitReachedWarning": "Du valde för många filer. Välj högst {0} filer och försök igen.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Röd", "dxColorView-ariaGreen": "Grön", diff --git a/packages/devextreme/js/localization/messages/tr.json b/packages/devextreme/js/localization/messages/tr.json index ec6da10e1603..a717151d5b5d 100644 --- a/packages/devextreme/js/localization/messages/tr.json +++ b/packages/devextreme/js/localization/messages/tr.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Dosya listesi", "dxChat-downloadButtonLabel": "{0} dosyasını indir", "dxChat-fileLimitReachedWarning": "Çok fazla dosya seçtiniz. En fazla {0} dosya seçip tekrar deneyin.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Kırmızı", "dxColorView-ariaGreen": "Yeşil", diff --git a/packages/devextreme/js/localization/messages/uk.json b/packages/devextreme/js/localization/messages/uk.json index fedee25990ac..64a8c03634d2 100644 --- a/packages/devextreme/js/localization/messages/uk.json +++ b/packages/devextreme/js/localization/messages/uk.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Список файлів", "dxChat-downloadButtonLabel": "Завантажити файл {0}", "dxChat-fileLimitReachedWarning": "Ви вибрали забагато файлів. Виберіть не більше {0} файлів і повторіть спробу.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Червоний", "dxColorView-ariaGreen": "Зелений", diff --git a/packages/devextreme/js/localization/messages/vi.json b/packages/devextreme/js/localization/messages/vi.json index bf311b55f912..05a3d9462fca 100644 --- a/packages/devextreme/js/localization/messages/vi.json +++ b/packages/devextreme/js/localization/messages/vi.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "Danh sách tập tin", "dxChat-downloadButtonLabel": "Tải xuống tập tin {0}", "dxChat-fileLimitReachedWarning": "Bạn đã chọn quá nhiều tập tin. Chọn không quá {0} tập tin và thử lại.", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "Đỏ", "dxColorView-ariaGreen": "Xanh lá", diff --git a/packages/devextreme/js/localization/messages/zh-CN.json b/packages/devextreme/js/localization/messages/zh-CN.json index be5acebd67e0..5e45b321a7e2 100644 --- a/packages/devextreme/js/localization/messages/zh-CN.json +++ b/packages/devextreme/js/localization/messages/zh-CN.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "文件列表", "dxChat-downloadButtonLabel": "下载文件 {0}", "dxChat-fileLimitReachedWarning": "您选择的文件太多。请选择不超过 {0} 个文件后重试。", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "红色", "dxColorView-ariaGreen": "绿色", diff --git a/packages/devextreme/js/localization/messages/zh-Hans.json b/packages/devextreme/js/localization/messages/zh-Hans.json index 84f31725a97d..92a107cb5bbc 100644 --- a/packages/devextreme/js/localization/messages/zh-Hans.json +++ b/packages/devextreme/js/localization/messages/zh-Hans.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "文件列表", "dxChat-downloadButtonLabel": "下载文件 {0}", "dxChat-fileLimitReachedWarning": "您选择的文件太多。请选择不超过 {0} 个文件后重试。", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "红色", "dxColorView-ariaGreen": "绿色", diff --git a/packages/devextreme/js/localization/messages/zh-tw.json b/packages/devextreme/js/localization/messages/zh-tw.json index fa266a611d61..85120911a168 100644 --- a/packages/devextreme/js/localization/messages/zh-tw.json +++ b/packages/devextreme/js/localization/messages/zh-tw.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "檔案清單", "dxChat-downloadButtonLabel": "下載檔案 {0}", "dxChat-fileLimitReachedWarning": "您選擇的檔案過多。請選擇不超過 {0} 個檔案後重試。", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "紅色", "dxColorView-ariaGreen": "綠色", diff --git a/packages/devextreme/js/localization/messages/zh.json b/packages/devextreme/js/localization/messages/zh.json index 08beff591d81..a66b310d97ba 100644 --- a/packages/devextreme/js/localization/messages/zh.json +++ b/packages/devextreme/js/localization/messages/zh.json @@ -447,6 +447,12 @@ "dxChat-fileViewLabel": "文件列表", "dxChat-downloadButtonLabel": "下载文件 {0}", "dxChat-fileLimitReachedWarning": "您选择的文件太多。请选择不超过 {0} 个文件后重试。", + "dxChat-toolCallsGroupTitle": "{0} tools", + "dxChat-toolCallArgumentsLabel": "Arguments", + "dxChat-toolCallResultLabel": "Result", + "dxChat-toolCallErrorText": "Error", + "dxChat-copyButtonLabel": "Copy", + "dxChat-copiedButtonLabel": "Copied", "dxColorView-ariaRed": "红色", "dxColorView-ariaGreen": "绿色", From ba8a480f00a66f66201ca435af416474a35141fc Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:15:02 +0300 Subject: [PATCH 3/8] Chat: Render tool calls in the message bubble --- .../js/__internal/ui/chat/messagebubble.ts | 44 ++- .../js/__internal/ui/chat/messagegroup.ts | 5 +- .../ui/chat/tool_call_view/tool_call_view.ts | 290 ++++++++++++++++++ 3 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 packages/devextreme/js/__internal/ui/chat/tool_call_view/tool_call_view.ts diff --git a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts index 057437de38cc..b8aa92ee4a80 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagebubble.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagebubble.ts @@ -2,12 +2,15 @@ import messageLocalization from '@js/common/core/localization/message'; import { getPublicElement } from '@js/core/element'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { Attachment, AttachmentDownloadClickEvent, Message } from '@js/ui/chat'; +import type { + Attachment, AttachmentDownloadClickEvent, Message, ToolCall, +} from '@js/ui/chat'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import { ICON_CLASS } from '@ts/core/utils/icon'; import type { OptionChanged } from '@ts/core/widget/types'; import Widget from '@ts/core/widget/widget'; import FileView from '@ts/ui/chat/file_view/file_view'; +import ToolCallView from '@ts/ui/chat/tool_call_view/tool_call_view'; export const CHAT_MESSAGEBUBBLE_CLASS = 'dx-chat-messagebubble'; export const CHAT_MESSAGEBUBBLE_DELETED_CLASS = 'dx-chat-messagebubble-deleted'; @@ -26,6 +29,7 @@ export interface Properties extends WidgetOptions { src?: string; alt?: string; attachments?: Attachment[]; + toolCalls?: ToolCall[]; onAttachmentDownloadClick?: (e: AttachmentDownloadClickEvent) => void; template?: ((message: Message, container: Element) => void) | null; } @@ -35,6 +39,8 @@ class MessageBubble extends Widget { _$attachments?: dxElementWrapper; + _$toolCalls?: dxElementWrapper; + _getDefaultOptions(): Properties { return { ...super._getDefaultOptions(), @@ -53,9 +59,11 @@ class MessageBubble extends Widget { super._initMarkup(); this._renderContentContainer(); + this._renderToolCallsElement(); this._renderAttachmentsElement(); this._updateContent(); + this._renderToolCalls(); this._renderAttachments(); } @@ -65,6 +73,17 @@ class MessageBubble extends Widget { .appendTo(this.$element()); } + _renderToolCallsElement(): void { + const { toolCalls, isDeleted } = this.option(); + + this._$toolCalls?.remove(); + this._$toolCalls = undefined; + + if (toolCalls?.length && !isDeleted) { + this._$toolCalls = $('
').prependTo(this.$element()); + } + } + _renderAttachmentsElement(): void { const { attachments, isDeleted } = this.option(); @@ -132,6 +151,16 @@ class MessageBubble extends Widget { } } + _renderToolCalls(): void { + const { toolCalls } = this.option(); + + if (!this._$toolCalls) { + return; + } + + this._createComponent(this._$toolCalls, ToolCallView, { toolCalls }); + } + _renderAttachments(): void { const { attachments, @@ -163,10 +192,17 @@ class MessageBubble extends Widget { const { name, value } = args; switch (name) { + case 'isDeleted': + this._updateMessageData(name, value); + this._updateContent(); + this._renderToolCallsElement(); + this._renderToolCalls(); + this._renderAttachmentsElement(); + this._renderAttachments(); + break; case 'text': case 'src': case 'alt': - case 'isDeleted': this._updateMessageData(name, value); this._updateContent(); this._renderAttachmentsElement(); @@ -183,6 +219,10 @@ class MessageBubble extends Widget { case 'isEdited': this._updateMessageData(name, value); break; + case 'toolCalls': + this._renderToolCallsElement(); + this._renderToolCalls(); + break; case 'onAttachmentDownloadClick': case 'attachments': this._renderAttachmentsElement(); diff --git a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts index d4bef516f829..35d562c48463 100644 --- a/packages/devextreme/js/__internal/ui/chat/messagegroup.ts +++ b/packages/devextreme/js/__internal/ui/chat/messagegroup.ts @@ -125,12 +125,15 @@ class MessageGroup extends Widget { _getMessageBubbleOptions(message: Message): MessageBubbleProperties { const { messageTemplate, onAttachmentDownloadClick } = this.option(); - const { isDeleted, type, attachments } = message; + const { + isDeleted, type, attachments, toolCalls, + } = message; const options: MessageBubbleProperties = { isDeleted, type, attachments, + toolCalls, onAttachmentDownloadClick, }; diff --git a/packages/devextreme/js/__internal/ui/chat/tool_call_view/tool_call_view.ts b/packages/devextreme/js/__internal/ui/chat/tool_call_view/tool_call_view.ts new file mode 100644 index 000000000000..9560cde63087 --- /dev/null +++ b/packages/devextreme/js/__internal/ui/chat/tool_call_view/tool_call_view.ts @@ -0,0 +1,290 @@ +import { name as clickEventName } from '@js/common/core/events/click'; +import eventsEngine from '@js/common/core/events/core/events_engine'; +import { addNamespace } from '@js/common/core/events/utils'; +import messageLocalization from '@js/common/core/localization/message'; +import type { dxElementWrapper } from '@js/core/renderer'; +import $ from '@js/core/renderer'; +import { noop } from '@js/core/utils/common'; +import { isString } from '@js/core/utils/type'; +import type { Item as AccordionItem } from '@js/ui/accordion'; +import type { ButtonType } from '@js/ui/button'; +import type { ToolCall } from '@js/ui/chat'; +import { ICON_CLASS } from '@ts/core/utils/icon'; +import type { DOMComponentProperties } from '@ts/core/widget/dom_component'; +import DOMComponent from '@ts/core/widget/dom_component'; +import type { OptionChanged } from '@ts/core/widget/types'; +import Accordion from '@ts/ui/accordion'; +import TextArea from '@ts/ui/text_area'; + +export interface ToolCallViewProperties extends DOMComponentProperties { + toolCalls?: ToolCall[]; +} + +interface FieldConfig { + label: string; + value: string; + maxHeight: number; + isError?: boolean; +} + +interface CopyButtonState { + icon: string; + hint: string; + type: ButtonType; +} + +export const CHAT_TOOL_CALL_VIEW_CLASS = 'dx-chat-tool-call-view'; +export const CHAT_TOOL_CALL_TITLE_CLASS = 'dx-chat-tool-call-title'; +export const CHAT_TOOL_CALL_ICON_CLASS = `${ICON_CLASS}-toolbox`; +export const CHAT_TOOL_CALL_NAME_CLASS = 'dx-chat-tool-call-name'; +export const CHAT_TOOL_CALL_ERROR_BADGE_CLASS = 'dx-chat-tool-call-error-badge'; +export const CHAT_TOOL_CALL_CONTENT_CLASS = 'dx-chat-tool-call-content'; +export const CHAT_TOOL_CALL_FIELD_CLASS = 'dx-chat-tool-call-field'; +export const CHAT_TOOL_CALL_FIELD_LABEL_CLASS = 'dx-chat-tool-call-field-label'; +export const CHAT_TOOL_CALL_FIELD_ERROR_CLASS = 'dx-chat-tool-call-field-error'; + +export const COPY_BUTTON_NAME = 'copy'; +export const COPIED_STATE_DURATION = 2000; +const ARGUMENTS_MAX_HEIGHT = 320; +const RESULT_MAX_HEIGHT = 240; +const FIELD_CLICK_EVENT_NAME = addNamespace(clickEventName, 'dxChatToolCallView'); + +const formatValue = (value: unknown): string => (Array.isArray(value) + ? `[${value.map(formatValue).join(', ')}]` + : JSON.stringify(value)); + +const formatArguments = (args: ToolCall['arguments'] = {}): string => Object.entries(args) + .map(([key, value]) => `${key}: ${formatValue(value)}`) + .join(', '); + +const formatResult = (result: unknown): string => { + if (result === undefined) { + return ''; + } + + return isString(result) ? result : JSON.stringify(result, null, 2); +}; + +class ToolCallView extends DOMComponent { + _getDefaultOptions(): ToolCallViewProperties { + return { + ...super._getDefaultOptions(), + toolCalls: [], + }; + } + + _initMarkup(): void { + this.$element().addClass(CHAT_TOOL_CALL_VIEW_CLASS); + + super._initMarkup(); + + this._renderToolCalls(); + } + + _renderToolCalls(): void { + const { toolCalls = [] } = this.option(); + + if (toolCalls.length < 2) { + this._renderToolCallList(this.$element(), toolCalls); + + return; + } + + // @ts-expect-error + const groupTitle = messageLocalization.format('dxChat-toolCallsGroupTitle', toolCalls.length); + + this._renderAccordion(this.$element(), [{ + titleTemplate: (): Element => this._createTitle(groupTitle, false), + template: (_data, _index, element): Element => { + const $content = this._createContent(element); + + this._renderToolCallList($content, toolCalls); + + return $content.get(0); + }, + }]); + } + + _renderToolCallList($container: dxElementWrapper, toolCalls: ToolCall[]): void { + const items = toolCalls.map((toolCall): AccordionItem => ({ + titleTemplate: (): Element => this._createTitle(toolCall.name, toolCall.status === 'error'), + template: (_data, _index, element): Element => { + const $content = this._createContent(element); + + this._renderToolCallFields($content, toolCall); + + return $content.get(0); + }, + })); + + this._renderAccordion($container, items); + } + + _renderAccordion($container: dxElementWrapper, items: AccordionItem[]): void { + if (!items.length) { + return; + } + + const $accordion = $('
').appendTo($container); + + this._createComponent($accordion, Accordion, { + items, + collapsible: true, + multiple: true, + selectedIndex: -1, + }); + } + + _createTitle(text: string, isError: boolean): Element { + const $title = $('
').addClass(CHAT_TOOL_CALL_TITLE_CLASS); + + $('
') + .addClass(ICON_CLASS) + .addClass(CHAT_TOOL_CALL_ICON_CLASS) + .appendTo($title); + + $('
') + .addClass(CHAT_TOOL_CALL_NAME_CLASS) + .text(text) + .attr('title', text) + .appendTo($title); + + if (isError) { + $('
') + .addClass(CHAT_TOOL_CALL_ERROR_BADGE_CLASS) + .text(messageLocalization.format('dxChat-toolCallErrorText')) + .appendTo($title); + } + + return $title.get(0); + } + + _createContent(container: Element): dxElementWrapper { + return $('
') + .addClass(CHAT_TOOL_CALL_CONTENT_CLASS) + .appendTo(container); + } + + _renderToolCallFields($container: dxElementWrapper, toolCall: ToolCall): void { + const argumentsText = formatArguments(toolCall.arguments); + const resultText = formatResult(toolCall.result); + + if (argumentsText) { + this._renderField($container, { + label: messageLocalization.format('dxChat-toolCallArgumentsLabel'), + value: argumentsText, + maxHeight: ARGUMENTS_MAX_HEIGHT, + }); + } + + if (resultText) { + this._renderField($container, { + label: messageLocalization.format('dxChat-toolCallResultLabel'), + value: resultText, + maxHeight: RESULT_MAX_HEIGHT, + isError: toolCall.status === 'error', + }); + } + } + + _renderField($container: dxElementWrapper, config: FieldConfig): void { + const { + label, value, maxHeight, isError = false, + } = config; + + const $field = $('
') + .addClass(CHAT_TOOL_CALL_FIELD_CLASS) + .toggleClass(CHAT_TOOL_CALL_FIELD_ERROR_CLASS, isError) + .appendTo($container); + + $('
') + .addClass(CHAT_TOOL_CALL_FIELD_LABEL_CLASS) + .text(label) + .appendTo($field); + + const $editor = $('
').appendTo($field); + + const copyState: CopyButtonState = { + icon: 'copy', + hint: messageLocalization.format('dxChat-copyButtonLabel'), + type: 'normal', + }; + const copiedState: CopyButtonState = { + icon: 'check', + hint: messageLocalization.format('dxChat-copiedButtonLabel'), + type: 'success', + }; + + const textArea = this._createComponent($editor, TextArea, { + value, + readOnly: true, + stylingMode: 'outlined', + autoResizeEnabled: true, + maxHeight, + hoverStateEnabled: false, + focusStateEnabled: false, + inputAttr: { 'aria-label': label }, + buttons: [{ + name: COPY_BUTTON_NAME, + location: 'after', + options: { + ...copyState, + elementAttr: { 'aria-label': copyState.hint }, + stylingMode: 'text', + disabled: false, + }, + }], + }); + const copyButton = textArea.getButton(COPY_BUTTON_NAME); + const copiedStateTimer: { id?: ReturnType } = {}; + + const setCopyButtonState = (state: CopyButtonState): void => { + copyButton?.option({ + ...state, + elementAttr: { 'aria-label': state.hint }, + }); + }; + + const copyValue = (): void => { + navigator.clipboard?.writeText(value) + .then(() => { + setCopyButtonState(copiedState); + + clearTimeout(copiedStateTimer.id); + // eslint-disable-next-line no-restricted-globals + copiedStateTimer.id = setTimeout(() => { + setCopyButtonState(copyState); + }, COPIED_STATE_DURATION); + }) + .catch(noop); + }; + + copyButton?.option('onClick', copyValue); + + textArea.on('disposing', () => { + clearTimeout(copiedStateTimer.id); + }); + + eventsEngine.on($editor, FIELD_CLICK_EVENT_NAME, copyValue); + } + + _clean(): void { + this.$element().empty(); + + super._clean(); + } + + _optionChanged(args: OptionChanged): void { + const { name } = args; + + switch (name) { + case 'toolCalls': + this._invalidate(); + break; + default: + super._optionChanged(args); + } + } +} + +export default ToolCallView; From 570f9bdf25e17f08c7a00c2424d887d8c5e4b2aa Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:15:26 +0300 Subject: [PATCH 4/8] Chat: Cover tool calls with QUnit tests --- .../chat.markup.tests.js | 1 + .../tests/DevExpress.ui.widgets/chat.tests.js | 1 + .../chatParts/chat.tests.js | 46 +++ .../chatParts/messageBubble.markup.tests.js | 9 + .../chatParts/messageBubble.tests.js | 101 +++++ .../chatParts/toolCallView.markup.tests.js | 98 +++++ .../chatParts/toolCallView.tests.js | 382 ++++++++++++++++++ 7 files changed, 638 insertions(+) create mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js create mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js index f1374c9cfdf8..40ad637d2bb7 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.markup.tests.js @@ -21,3 +21,4 @@ import './chatParts/editingPreview.markup.tests.js'; import './chatParts/chatTextArea.markup.tests.js'; import './chatParts/chat.markup.tests.js'; import './chatParts/fileView.markup.tests.js'; +import './chatParts/toolCallView.markup.tests.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js index 3f1520ae2c8c..fc4d630a1888 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chat.tests.js @@ -21,3 +21,4 @@ import './chatParts/editingPreview.tests.js'; import './chatParts/chatTextArea.tests.js'; import './chatParts/chat.tests.js'; import './chatParts/fileView.tests.js'; +import './chatParts/toolCallView.tests.js'; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js index 3e3347e0d9be..05e386b96de2 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/chat.tests.js @@ -41,6 +41,7 @@ import { CHAT_CONFIRMATION_POPUP_WRAPPER_CLASS } from '__internal/ui/chat/confir import { POPUP_CLASS } from '__internal/ui/popup/popup'; import { BUTTON_CLASS } from '__internal/ui/button/button'; import { CHAT_FILE_CLASS } from '__internal/ui/chat/file_view/file'; +import { CHAT_TOOL_CALL_NAME_CLASS } from '__internal/ui/chat/tool_call_view/tool_call_view'; import MessageBubble from '__internal/ui/chat/messagebubble'; import ChatTextArea, { @@ -663,6 +664,21 @@ QUnit.module('Chat', () => { }); }); + QUnit.test('should render tool calls of messages passed in items', function(assert) { + this.reinit({ + items: [ + { text: 'question' }, + { text: 'answer', toolCalls: [{ name: 'getWeather' }, { name: 'getCurrentTime' }] }, + ], + }); + + const $bubbles = this.getBubbles(); + const toolCallNames = $bubbles.eq(1).find(`.${CHAT_TOOL_CALL_NAME_CLASS}`).map((_, element) => $(element).text()).get(); + + assert.strictEqual($bubbles.eq(0).find(`.${CHAT_TOOL_CALL_NAME_CLASS}`).length, 0, 'message without tool calls has none'); + assert.deepEqual(toolCallNames, ['2 tools'], 'message with tool calls renders them'); + }); + QUnit.module('messageTemplate', () => { QUnit.test('messageTemplate should set bubble content on init', function(assert) { const messageTemplate = (data, container) => { @@ -3446,6 +3462,36 @@ QUnit.module('Chat', () => { assert.strictEqual(this.getBubbles().eq(0).text(), 'updated text', 'message bubble text was updated'); }); + QUnit.test('it should be possible to update tool calls of a message using push api', function(assert) { + const messages = [{ id: 0, text: 'message_0' }, { id: 1, text: 'message_1' }]; + const timeout = 100; + + const store = new CustomStore({ + key: 'id', + load: function() { + const d = $.Deferred(); + setTimeout(function() { + d.resolve([...messages]); + }, timeout); + return d.promise(); + }, + }); + + this.reinit({ + dataSource: store, + reloadOnChange: false, + }); + + this.clock.tick(timeout); + store.push([{ type: 'update', key: 1, data: { toolCalls: [{ name: 'getWeather' }] } }]); + this.clock.tick(timeout * 2); + + const toolCallNames = this.getBubbles().eq(1).find(`.${CHAT_TOOL_CALL_NAME_CLASS}`).map((_, element) => $(element).text()).get(); + + assert.deepEqual(toolCallNames, ['getWeather'], 'tool calls are rendered in the updated bubble'); + assert.strictEqual(this.getBubbles().eq(0).find(`.${CHAT_TOOL_CALL_NAME_CLASS}`).length, 0, 'other bubbles have no tool calls'); + }); + QUnit.test('it should be possible to update isEdited state of first message in group using push api', function(assert) { const messages = [{ id: 0, text: 'message_0' }, { id: 1, text: 'message_1' }]; const timeout = 100; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js index 4a95d0a003e5..a14c75b04992 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.markup.tests.js @@ -10,6 +10,7 @@ import MessageBubble, { } from '__internal/ui/chat/messagebubble'; import { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; +import { CHAT_TOOL_CALL_VIEW_CLASS } from '__internal/ui/chat/tool_call_view/tool_call_view'; const moduleConfig = { beforeEach: function() { @@ -49,6 +50,14 @@ QUnit.module('MessageBubble', moduleConfig, () => { assert.strictEqual($content.parent().is(this.$element), true, 'attachments element is direct child of root element'); }); + QUnit.test('root element should have a first child tool calls element with correct class if tool calls provided', function(assert) { + this.reinit({ text: 'answer', toolCalls: [{ name: 'getWeather' }] }); + const $toolCalls = this.$element.find(`.${CHAT_TOOL_CALL_VIEW_CLASS}`); + + assert.strictEqual($toolCalls.length, 1, 'tool calls element exist'); + assert.strictEqual(this.$element.children().first().is($toolCalls), true, 'tool calls element is the first child of root element'); + }); + QUnit.test('root element should switch classes when deletion and undo happens', function(assert) { this.instance.option('isDeleted', true); assert.strictEqual(this.$element.hasClass(CHAT_MESSAGEBUBBLE_DELETED_CLASS), true, 'root element has deleted class'); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js index 507cc884c31b..8e2427f28f14 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/messageBubble.tests.js @@ -9,6 +9,21 @@ import MessageBubble, { import { BUTTON_CLASS } from '__internal/ui/button/button'; import FileView, { CHAT_FILE_VIEW_CLASS } from '__internal/ui/chat/file_view/file_view'; import { CHAT_FILE_CLASS, CHAT_FILE_NAME_CLASS, CHAT_FILE_SIZE_CLASS } from '__internal/ui/chat/file_view/file'; +import { + CHAT_TOOL_CALL_VIEW_CLASS, + CHAT_TOOL_CALL_NAME_CLASS, +} from '__internal/ui/chat/tool_call_view/tool_call_view'; +import fx from 'common/core/animation/fx'; + +const ACCORDION_ITEM_CLASS = 'dx-accordion-item'; +const ACCORDION_ITEM_TITLE_CLASS = 'dx-accordion-item-title'; +const ACCORDION_ITEM_OPENED_CLASS = 'dx-accordion-item-opened'; + +const toolCalls = [{ + name: 'getWeather', + arguments: { city: 'London' }, + result: '15°C', +}]; const moduleConfig = { beforeEach: function() { @@ -22,6 +37,11 @@ const moduleConfig = { const $fileView = this.$getAttachments(); return $fileView.length ? FileView.getInstance($fileView) : null; }; + this.$getToolCallView = () => this.$element.find(`.${CHAT_TOOL_CALL_VIEW_CLASS}`); + this.getToolCallNames = () => this.$getToolCallView() + .find(`.${CHAT_TOOL_CALL_NAME_CLASS}`) + .map((_, element) => $(element).text()) + .get(); }; this.reinit = (options) => { @@ -262,6 +282,87 @@ QUnit.module('MessageBubble', moduleConfig, () => { }); }); + QUnit.module('Tool calls', { + beforeEach() { + fx.off = true; + }, + afterEach() { + fx.off = false; + }, + }, () => { + QUnit.test('should render tool calls when toolCalls passed', function(assert) { + this.reinit({ text: 'answer', toolCalls }); + + assert.deepEqual(this.getToolCallNames(), ['getWeather']); + }); + + QUnit.test('should render tool calls before the message content', function(assert) { + this.reinit({ text: 'answer', toolCalls }); + + assert.strictEqual(this.$getToolCallView().next().hasClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS), true); + }); + + QUnit.test('should not render tool calls when toolCalls is empty', function(assert) { + this.reinit({ text: 'answer', toolCalls: [] }); + + assert.strictEqual(this.$getToolCallView().length, 0); + }); + + QUnit.test('should not render tool calls when isDeleted is true', function(assert) { + this.reinit({ isDeleted: true, toolCalls }); + + assert.strictEqual(this.$getToolCallView().length, 0); + }); + + QUnit.test('should render tool calls when toolCalls is set at runtime', function(assert) { + this.reinit({ text: 'answer' }); + + this.instance.option('toolCalls', toolCalls); + + assert.deepEqual(this.getToolCallNames(), ['getWeather'], 'tool calls are rendered'); + assert.strictEqual(this.$getToolCallView().next().hasClass(CHAT_MESSAGEBUBBLE_CONTENT_CLASS), true, 'tool calls are rendered before the content'); + }); + + QUnit.test('should remove tool calls when toolCalls is reset at runtime', function(assert) { + this.reinit({ text: 'answer', toolCalls }); + + this.instance.option('toolCalls', []); + + assert.strictEqual(this.$getToolCallView().length, 0); + }); + + QUnit.test('should remove tool calls when isDeleted is set at runtime', function(assert) { + this.reinit({ text: 'answer', toolCalls }); + + this.instance.option('isDeleted', true); + + assert.strictEqual(this.$getToolCallView().length, 0); + }); + + QUnit.test('should keep an expanded tool call expanded when the text changes at runtime', function(assert) { + this.reinit({ text: 'answer', toolCalls }); + + this.$getToolCallView().find(`.${ACCORDION_ITEM_TITLE_CLASS}`).trigger('dxclick'); + this.instance.option('text', 'updated answer'); + + assert.strictEqual(this.$content.text(), 'updated answer', 'text is updated'); + assert.strictEqual(this.$getToolCallView().find(`.${ACCORDION_ITEM_CLASS}`).hasClass(ACCORDION_ITEM_OPENED_CLASS), true, 'tool call stays expanded'); + }); + + QUnit.test('should render tool calls along with the message template', function(assert) { + this.reinit({ + text: 'answer', + toolCalls, + template: (data, container) => { + $('

').text(`template text: ${data.text}`).appendTo(container); + }, + }); + + assert.deepEqual(this.getToolCallNames(), ['getWeather'], 'tool calls are rendered'); + assert.strictEqual(this.$content.text(), 'template text: answer', 'template is rendered'); + }); + }); + QUnit.module('Proxy state options', () => { QUnit.test('MessageBubble state options should not affect FileView state options', function(assert) { const options = { diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js new file mode 100644 index 000000000000..c1f5129c8018 --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js @@ -0,0 +1,98 @@ +import $ from 'jquery'; +import fx from 'common/core/animation/fx'; +import ToolCallView, { + CHAT_TOOL_CALL_VIEW_CLASS, + CHAT_TOOL_CALL_TITLE_CLASS, + CHAT_TOOL_CALL_ICON_CLASS, + CHAT_TOOL_CALL_NAME_CLASS, + CHAT_TOOL_CALL_CONTENT_CLASS, + CHAT_TOOL_CALL_FIELD_CLASS, + CHAT_TOOL_CALL_FIELD_LABEL_CLASS, +} from '__internal/ui/chat/tool_call_view/tool_call_view'; + +const { test } = QUnit; + +const ACCORDION_CLASS = 'dx-accordion'; +const ACCORDION_ITEM_TITLE_CLASS = 'dx-accordion-item-title'; +const ACCORDION_ITEM_BODY_CLASS = 'dx-accordion-item-body'; +const ICON_CLASS = 'dx-icon'; +const TEXTAREA_CLASS = 'dx-textarea'; + +const toolCall = { + name: 'getWeather', + arguments: { city: 'London' }, + result: '15°C', +}; + +QUnit.module('ToolCallView', { + beforeEach() { + fx.off = true; + this.$element = $('
').appendTo('#qunit-fixture'); + + const init = (options) => { + this.instance = new ToolCallView(this.$element, options); + }; + + this.reinit = (options) => { + this.instance.dispose(); + + init(options); + }; + + init({ toolCalls: [toolCall] }); + }, + afterEach() { + this.instance.dispose(); + this.$element.remove(); + fx.off = false; + }, +}, () => { + test('root element should have correct class', function(assert) { + assert.strictEqual(this.$element.hasClass(CHAT_TOOL_CALL_VIEW_CLASS), true); + }); + + test('should render an accordion as a direct child of the root element', function(assert) { + const $accordion = this.$element.children(`.${ACCORDION_CLASS}`); + + assert.strictEqual($accordion.length, 1); + }); + + test('item title should contain the icon and the name elements', function(assert) { + const $title = this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS} > .${CHAT_TOOL_CALL_TITLE_CLASS}`); + const $children = $title.children(); + + assert.strictEqual($title.length, 1, 'title element exists'); + assert.strictEqual($children.eq(0).hasClass(ICON_CLASS), true, 'first child is an icon'); + assert.strictEqual($children.eq(0).hasClass(CHAT_TOOL_CALL_ICON_CLASS), true, 'icon is the toolbox icon'); + assert.strictEqual($children.eq(1).hasClass(CHAT_TOOL_CALL_NAME_CLASS), true, 'second child is the name'); + assert.strictEqual($children.eq(1).attr('title'), 'getWeather', 'name has a title attribute'); + }); + + test('expanded item body should contain the content element with fields', function(assert) { + this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS}`).trigger('dxclick'); + + const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); + const $fields = $content.children(`.${CHAT_TOOL_CALL_FIELD_CLASS}`); + + assert.strictEqual($content.length, 1, 'content element exists'); + assert.strictEqual($fields.length, 2, 'two fields are rendered'); + + $fields.each((_, field) => { + const $fieldChildren = $(field).children(); + + assert.strictEqual($fieldChildren.eq(0).hasClass(CHAT_TOOL_CALL_FIELD_LABEL_CLASS), true, 'first child is the label'); + assert.strictEqual($fieldChildren.eq(1).hasClass(TEXTAREA_CLASS), true, 'second child is the text area'); + }); + }); + + test('expanded group body should contain the content element with the nested accordion', function(assert) { + this.reinit({ toolCalls: [toolCall, toolCall] }); + + this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS}`).trigger('dxclick'); + + const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); + + assert.strictEqual($content.length, 1, 'content element exists'); + assert.strictEqual($content.children(`.${ACCORDION_CLASS}`).length, 1, 'nested accordion is a direct child of the content'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js new file mode 100644 index 000000000000..b1ea2f344294 --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js @@ -0,0 +1,382 @@ +import $ from 'jquery'; +import fx from 'common/core/animation/fx'; +import localization from 'localization'; + +import ToolCallView, { + CHAT_TOOL_CALL_ERROR_BADGE_CLASS, + CHAT_TOOL_CALL_FIELD_CLASS, + CHAT_TOOL_CALL_FIELD_ERROR_CLASS, + CHAT_TOOL_CALL_FIELD_LABEL_CLASS, + CHAT_TOOL_CALL_NAME_CLASS, + COPIED_STATE_DURATION, + COPY_BUTTON_NAME, +} from '__internal/ui/chat/tool_call_view/tool_call_view'; +import TextArea from '__internal/ui/text_area'; + +const ACCORDION_ITEM_CLASS = 'dx-accordion-item'; +const ACCORDION_ITEM_TITLE_CLASS = 'dx-accordion-item-title'; +const ACCORDION_ITEM_OPENED_CLASS = 'dx-accordion-item-opened'; +const TEXTAREA_CLASS = 'dx-textarea'; +const TEXTEDITOR_INPUT_CLASS = 'dx-texteditor-input'; + +const updateUserCall = { + name: 'updateUser', + arguments: { + name: 'John Doe', + email: 'john@example.com', + active: true, + count: 10, + }, + result: 'Completed', + status: 'success', +}; + +const currentTimeCall = { + name: 'getCurrentTime', + arguments: { locations: ['Barcelona', 'Los Angeles'] }, + result: 'Barcelona: 10:18', +}; + +const stockPriceCall = { + name: 'getStockPrice', + arguments: { symbol: 'CNTS' }, + result: 'Failed to execute the tool', + status: 'error', +}; + +const moduleConfig = { + beforeEach() { + fx.off = true; + + const init = (options = {}) => { + this.instance = new ToolCallView($('#component'), { + toolCalls: [updateUserCall], + ...options, + }); + this.$element = $(this.instance.$element()); + }; + + this.reinit = (options) => { + this.instance.dispose(); + init(options); + }; + + this.getItems = () => this.$element.find(`.${ACCORDION_ITEM_CLASS}`); + this.getTitles = () => this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS}`); + this.getNames = () => this.$element.find(`.${CHAT_TOOL_CALL_NAME_CLASS}`) + .map((_, element) => $(element).text()) + .get(); + this.expand = (index = 0) => { + this.getTitles().eq(index).trigger('dxclick'); + }; + this.getFields = () => this.$element.find(`.${CHAT_TOOL_CALL_FIELD_CLASS}`); + this.getField = (label) => this.getFields() + .filter((_, element) => $(element).find(`.${CHAT_TOOL_CALL_FIELD_LABEL_CLASS}`).text() === label); + this.getFieldValue = (label) => this.getField(label).find(`.${TEXTEDITOR_INPUT_CLASS}`).val(); + this.getCopyButton = (label) => TextArea + .getInstance(this.getField(label).find(`.${TEXTAREA_CLASS}`)) + .getButton(COPY_BUTTON_NAME); + + init(); + }, + afterEach() { + fx.off = false; + }, +}; + +QUnit.module('ToolCallView', moduleConfig, () => { + QUnit.module('Render', () => { + QUnit.test('should render a collapsed item titled with the tool name for a single tool call', function(assert) { + assert.strictEqual(this.getItems().length, 1, 'one accordion item'); + assert.deepEqual(this.getNames(), ['updateUser'], 'title contains the tool name'); + assert.strictEqual(this.getItems().hasClass(ACCORDION_ITEM_OPENED_CLASS), false, 'item is collapsed'); + }); + + QUnit.test('should render an error badge in the title of a failed tool call', function(assert) { + this.reinit({ toolCalls: [stockPriceCall] }); + + const $badge = this.getTitles().find(`.${CHAT_TOOL_CALL_ERROR_BADGE_CLASS}`); + + assert.strictEqual($badge.length, 1, 'badge is rendered'); + assert.strictEqual($badge.text(), 'Error', 'badge text is correct'); + }); + + QUnit.test('should not render an error badge for a tool call without the error status', function(assert) { + this.reinit({ toolCalls: [currentTimeCall] }); + + assert.strictEqual(this.$element.find(`.${CHAT_TOOL_CALL_ERROR_BADGE_CLASS}`).length, 0); + }); + + QUnit.test('should render a group item titled with the tool call count for several tool calls', function(assert) { + this.reinit({ toolCalls: [currentTimeCall, stockPriceCall] }); + + assert.strictEqual(this.getItems().length, 1, 'only the group item is rendered while collapsed'); + assert.deepEqual(this.getNames(), ['2 tools'], 'group title contains the count'); + + this.expand(); + + assert.deepEqual(this.getNames(), ['2 tools', 'getCurrentTime', 'getStockPrice'], 'tool calls are rendered inside the group'); + assert.strictEqual(this.getTitles().eq(0).find(`.${CHAT_TOOL_CALL_ERROR_BADGE_CLASS}`).length, 0, 'group title has no error badge'); + assert.strictEqual(this.getTitles().eq(2).find(`.${CHAT_TOOL_CALL_ERROR_BADGE_CLASS}`).length, 1, 'failed tool call title has an error badge'); + }); + + QUnit.test('should not render items for an empty toolCalls array', function(assert) { + this.reinit({ toolCalls: [] }); + + assert.strictEqual(this.getItems().length, 0); + }); + }); + + QUnit.module('Fields', () => { + QUnit.test('should not render fields until the item is expanded', function(assert) { + assert.strictEqual(this.getFields().length, 0); + }); + + QUnit.test('should render arguments as comma-separated key-value pairs', function(assert) { + this.expand(); + + assert.strictEqual( + this.getFieldValue('Arguments'), + 'name: "John Doe", email: "john@example.com", active: true, count: 10', + ); + }); + + QUnit.test('should separate array items with a comma and a space', function(assert) { + this.reinit({ toolCalls: [currentTimeCall] }); + this.expand(); + + assert.strictEqual(this.getFieldValue('Arguments'), 'locations: ["Barcelona", "Los Angeles"]'); + }); + + QUnit.test('should render a string result as is', function(assert) { + this.expand(); + + assert.strictEqual(this.getFieldValue('Result'), 'Completed'); + }); + + QUnit.test('should render an object result as indented JSON', function(assert) { + this.reinit({ + toolCalls: [{ name: 'getExchangeRate', result: { base: 'USD', rates: { EUR: 0.92 } } }], + }); + this.expand(); + + assert.strictEqual( + this.getFieldValue('Result'), + '{\n "base": "USD",\n "rates": {\n "EUR": 0.92\n }\n}', + ); + }); + + QUnit.test('should render a non-string primitive result as text', function(assert) { + this.reinit({ toolCalls: [{ name: 'getCount', result: 0 }] }); + this.expand(); + + assert.strictEqual(this.getFieldValue('Result'), '0'); + }); + + QUnit.test('should render only the result field when there are no arguments', function(assert) { + this.reinit({ toolCalls: [{ name: 'getTime', arguments: {}, result: '10:18' }] }); + this.expand(); + + const labels = this.getFields().find(`.${CHAT_TOOL_CALL_FIELD_LABEL_CLASS}`).map((_, element) => $(element).text()).get(); + + assert.deepEqual(labels, ['Result']); + }); + + QUnit.test('should render only the arguments field when there is no result', function(assert) { + this.reinit({ toolCalls: [{ name: 'getWeather', arguments: { city: 'London' } }] }); + this.expand(); + + const labels = this.getFields().find(`.${CHAT_TOOL_CALL_FIELD_LABEL_CLASS}`).map((_, element) => $(element).text()).get(); + + assert.deepEqual(labels, ['Arguments']); + }); + + QUnit.test('should mark only the result field of a failed tool call as an error', function(assert) { + this.reinit({ toolCalls: [stockPriceCall] }); + this.expand(); + + assert.strictEqual(this.getField('Result').hasClass(CHAT_TOOL_CALL_FIELD_ERROR_CLASS), true, 'result field is an error'); + assert.strictEqual(this.getField('Arguments').hasClass(CHAT_TOOL_CALL_FIELD_ERROR_CLASS), false, 'arguments field is not an error'); + }); + + QUnit.test('should not mark the result field of a successful tool call as an error', function(assert) { + this.expand(); + + assert.strictEqual(this.getField('Result').hasClass(CHAT_TOOL_CALL_FIELD_ERROR_CLASS), false); + }); + + QUnit.test('should render read-only fields', function(assert) { + this.expand(); + + const $inputs = this.getFields().find(`.${TEXTEDITOR_INPUT_CLASS}`); + + assert.strictEqual($inputs.length, 2, 'two fields are rendered'); + assert.strictEqual($inputs.filter('[readonly]').length, 2, 'both fields are read-only'); + }); + }); + + QUnit.module('Copy', { + beforeEach() { + this.clock = sinon.useFakeTimers(); + this.writeText = sinon.stub(navigator.clipboard, 'writeText').returns(Promise.resolve()); + this.waitForWrite = () => this.writeText.lastCall.returnValue; + }, + afterEach() { + this.writeText.restore(); + this.clock.restore(); + }, + }, () => { + QUnit.test('should copy the field value to the clipboard on a field click', function(assert) { + this.expand(); + + this.getField('Arguments').find(`.${TEXTEDITOR_INPUT_CLASS}`).trigger('dxclick'); + + assert.strictEqual(this.writeText.callCount, 1, 'clipboard is written once'); + assert.strictEqual( + this.writeText.firstCall.args[0], + 'name: "John Doe", email: "john@example.com", active: true, count: 10', + 'formatted arguments are copied', + ); + }); + + QUnit.test('should copy the field value once on a copy button click', function(assert) { + this.expand(); + + $(this.getCopyButton('Result').element()).trigger('dxclick'); + + assert.strictEqual(this.writeText.callCount, 1, 'clipboard is written once'); + assert.strictEqual(this.writeText.firstCall.args[0], 'Completed', 'result is copied'); + }); + + QUnit.test('should switch the copy button to the copied state after the value is copied', async function(assert) { + this.expand(); + + const copyButton = this.getCopyButton('Result'); + + this.getField('Result').find(`.${TEXTEDITOR_INPUT_CLASS}`).trigger('dxclick'); + await this.waitForWrite(); + + assert.strictEqual(copyButton.option('icon'), 'check', 'icon is correct'); + assert.strictEqual(copyButton.option('type'), 'success', 'type is correct'); + assert.strictEqual(copyButton.option('hint'), 'Copied', 'hint is correct'); + assert.strictEqual($(copyButton.element()).attr('aria-label'), 'Copied', 'aria-label is correct'); + }); + + QUnit.test('should restore the copy state of the button after the copied state duration', async function(assert) { + this.expand(); + + const copyButton = this.getCopyButton('Result'); + + this.getField('Result').find(`.${TEXTEDITOR_INPUT_CLASS}`).trigger('dxclick'); + await this.waitForWrite(); + + this.clock.tick(COPIED_STATE_DURATION - 1); + assert.strictEqual(copyButton.option('icon'), 'check', 'copied state is shown before the duration ends'); + + this.clock.tick(1); + assert.strictEqual(copyButton.option('icon'), 'copy', 'icon is restored'); + assert.strictEqual(copyButton.option('type'), 'normal', 'type is restored'); + assert.strictEqual(copyButton.option('hint'), 'Copy', 'hint is restored'); + assert.strictEqual($(copyButton.element()).attr('aria-label'), 'Copy', 'aria-label is restored'); + }); + + QUnit.test('should restart the copied state duration on a repeated copy', async function(assert) { + this.expand(); + + const copyButton = this.getCopyButton('Result'); + const $input = this.getField('Result').find(`.${TEXTEDITOR_INPUT_CLASS}`); + + $input.trigger('dxclick'); + await this.waitForWrite(); + this.clock.tick(COPIED_STATE_DURATION - 1); + + $input.trigger('dxclick'); + await this.waitForWrite(); + this.clock.tick(COPIED_STATE_DURATION - 1); + + assert.strictEqual(copyButton.option('icon'), 'check'); + }); + + QUnit.test('should keep the copy state of the button when writing to the clipboard fails', async function(assert) { + this.writeText.returns(Promise.reject(new Error('Write permission denied'))); + this.expand(); + + const copyButton = this.getCopyButton('Result'); + + this.getField('Result').find(`.${TEXTEDITOR_INPUT_CLASS}`).trigger('dxclick'); + await this.waitForWrite().catch(() => {}); + + assert.strictEqual(copyButton.option('icon'), 'copy'); + }); + + QUnit.test('should clear the copied state timer on dispose', async function(assert) { + this.expand(); + + this.getField('Result').find(`.${TEXTEDITOR_INPUT_CLASS}`).trigger('dxclick'); + await this.waitForWrite(); + + const timerCountWithCopiedState = this.clock.countTimers(); + + this.instance.dispose(); + + assert.strictEqual(this.clock.countTimers(), timerCountWithCopiedState - 1); + }); + }); + + QUnit.module('Options', () => { + QUnit.test('should render new tool calls after the toolCalls option is changed at runtime', function(assert) { + this.instance.option('toolCalls', [currentTimeCall, stockPriceCall]); + + assert.deepEqual(this.getNames(), ['2 tools']); + }); + }); + + QUnit.module('Accessibility', () => { + QUnit.test('field inputs should be labelled with the field labels', function(assert) { + this.expand(); + + const labels = this.getFields().find(`.${TEXTEDITOR_INPUT_CLASS}`).map((_, element) => $(element).attr('aria-label')).get(); + + assert.deepEqual(labels, ['Arguments', 'Result']); + }); + + QUnit.test('copy buttons should have a localized label', function(assert) { + this.expand(); + + const copyButton = this.getCopyButton('Arguments'); + + assert.strictEqual($(copyButton.element()).attr('aria-label'), 'Copy', 'aria-label is correct'); + assert.strictEqual(copyButton.option('hint'), 'Copy', 'hint is correct'); + }); + }); + + QUnit.module('Localization', () => { + QUnit.test('should localize the group title, the field labels and the error badge', function(assert) { + const defaultLocale = localization.locale(); + + try { + localization.loadMessages({ + 'ee': { + 'dxChat-toolCallsGroupTitle': '{0} tööriista', + 'dxChat-toolCallArgumentsLabel': 'Argumendid', + 'dxChat-toolCallResultLabel': 'Tulemus', + 'dxChat-toolCallErrorText': 'Viga', + }, + }); + localization.locale('ee'); + + this.reinit({ toolCalls: [currentTimeCall, stockPriceCall] }); + this.expand(); + this.expand(2); + + const labels = this.getFields().find(`.${CHAT_TOOL_CALL_FIELD_LABEL_CLASS}`).map((_, element) => $(element).text()).get(); + + assert.strictEqual(this.getNames()[0], '2 tööriista', 'group title is localized'); + assert.deepEqual(labels, ['Argumendid', 'Tulemus'], 'field labels are localized'); + assert.strictEqual(this.$element.find(`.${CHAT_TOOL_CALL_ERROR_BADGE_CLASS}`).text(), 'Viga', 'error badge is localized'); + } finally { + localization.locale(defaultLocale); + } + }); + }); +}); From 5cfc7844295a388b59d3de2f11b54e56627acb75 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:15:35 +0300 Subject: [PATCH 5/8] Chat: Add tool call layout and legacy theme styles --- .../scss/widgets/base/chat/_index.scss | 1 + .../scss/widgets/base/chat/_mixins.scss | 1 + .../chat/layout/chat-toolcallview/_index.scss | 88 +++++++++++++++++++ .../layout/chat-toolcallview/_mixins.scss | 73 +++++++++++++++ .../scss/widgets/fluent/chat/_colors.scss | 5 ++ .../scss/widgets/fluent/chat/_index.scss | 19 ++++ .../scss/widgets/fluent/chat/_sizes.scss | 24 +++++ .../scss/widgets/generic/chat/_colors.scss | 5 ++ .../scss/widgets/generic/chat/_index.scss | 19 ++++ .../scss/widgets/generic/chat/_sizes.scss | 24 +++++ .../scss/widgets/material/chat/_colors.scss | 5 ++ .../scss/widgets/material/chat/_index.scss | 19 ++++ .../scss/widgets/material/chat/_sizes.scss | 24 +++++ 13 files changed, 307 insertions(+) create mode 100644 packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_index.scss create mode 100644 packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_mixins.scss diff --git a/packages/devextreme-scss/scss/widgets/base/chat/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/_index.scss index 46256faab87d..d687d471393d 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/_index.scss @@ -33,3 +33,4 @@ $chat-messagebubble-max-height-image: 400px !default; @use "layout/chat-messagelist" as *; @use "layout/chat-suggestions" as *; @use "layout/chat-typingindicator" as *; +@use "layout/chat-toolcallview" as *; diff --git a/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss index 0cabd4e5533d..c014c82a5c5e 100644 --- a/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss +++ b/packages/devextreme-scss/scss/widgets/base/chat/_mixins.scss @@ -9,4 +9,5 @@ @forward "layout/chat-messagelist/mixins"; @forward "layout/chat-suggestions/mixins"; @forward "layout/chat-typingindicator/mixins"; +@forward "layout/chat-toolcallview/mixins"; @forward "layout/chat-confirmationpopup/mixins"; diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_index.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_index.scss new file mode 100644 index 000000000000..c71997e1afe2 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_index.scss @@ -0,0 +1,88 @@ +@use "sass:map"; +@use "../../../mixins" as *; +@use "../../../icon_fonts" as *; + +.dx-chat-messagegroup-alignment-start .dx-chat-messagegroup-content:has(.dx-chat-tool-call-view) { + justify-self: stretch; +} + +.dx-chat-tool-call-view { + .dx-accordion { + margin: 0; + background-color: transparent; + } + + .dx-accordion-item { + margin-block: 0; + box-shadow: none; + background-color: transparent; + } + + .dx-accordion-item:not(.dx-state-focused) { + border-color: transparent; + } + + .dx-accordion-item-title { + justify-content: flex-start; + font-size: inherit; + background-color: transparent; + + &::before { + position: static; + order: 1; + } + } + + .dx-accordion-item-closed > .dx-accordion-item-title::before { + content: map.get($icons, "chevronright"); + } + + .dx-accordion-item-opened > .dx-accordion-item-title::before { + content: map.get($icons, "chevrondown"); + } + + .dx-accordion-item-body { + padding: 0; + } +} + +.dx-rtl .dx-chat-tool-call-view .dx-accordion-item-closed > .dx-accordion-item-title::before { + content: map.get($icons, "chevronleft"); +} + +.dx-chat-tool-call-title { + display: flex; + align-items: center; + min-width: 0; +} + +.dx-chat-tool-call-name { + min-width: 0; + + @include dx-overflow(); +} + +.dx-chat-tool-call-error-badge { + flex-shrink: 0; +} + +.dx-chat-tool-call-content { + display: flex; + flex-direction: column; + border-inline-start-style: solid; +} + +.dx-chat-tool-call-field { + display: flex; + flex-direction: column; + + .dx-textarea, + .dx-texteditor-input, + .dx-button { + cursor: copy; + } + + .dx-button.dx-button-mode-text:not(:focus-visible) { + background-color: transparent; + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_mixins.scss b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_mixins.scss new file mode 100644 index 000000000000..e7b25c26b2f1 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/chat/layout/chat-toolcallview/_mixins.scss @@ -0,0 +1,73 @@ +@mixin chat-toolcallview( + $min-width, + $title-gap, + $title-color, + $icon-size, + $badge-padding-block, + $badge-padding-inline, + $badge-border-radius, + $content-gap, + $content-padding-block, + $content-margin-inline-start, + $content-padding-inline-start, + $content-border-width, + $content-border-color, + $field-gap, + $field-font-family, + $error-color, + $error-background-color, +) { + .dx-chat-messagebubble:has(> .dx-chat-tool-call-view) { + min-width: min($min-width, 100%); + } + + .dx-chat-tool-call-view .dx-accordion-item-title::before { + margin-inline: $title-gap 0; + color: $title-color; + } + + .dx-chat-tool-call-title { + gap: $title-gap; + color: $title-color; + + .dx-icon { + flex-shrink: 0; + font-size: $icon-size; + } + } + + .dx-chat-tool-call-error-badge { + padding: $badge-padding-block $badge-padding-inline; + border-radius: $badge-border-radius; + color: $error-color; + background-color: $error-background-color; + } + + .dx-chat-tool-call-content { + gap: $content-gap; + padding-block: $content-padding-block; + margin-inline-start: $content-margin-inline-start; + padding-inline-start: $content-padding-inline-start; + border-inline-start-width: $content-border-width; + border-inline-start-color: $content-border-color; + } + + .dx-chat-tool-call-field { + gap: $field-gap; + + .dx-textarea { + font-family: $field-font-family; + } + } + + .dx-chat-tool-call-field-error { + .dx-texteditor.dx-editor-outlined.dx-state-readonly { + border-color: transparent; + background-color: $error-background-color; + } + + .dx-texteditor-input { + color: $error-color; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss index 7f53705ade4e..bf5317d013f7 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_colors.scss @@ -188,3 +188,8 @@ $chat-typingindicator-bubble-bg-color: $chat-bubble-background-color-secondary ! $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-hover-bg !default; + +$chat-tool-call-title-color: $base-icon-color !default; +$chat-tool-call-content-border-color: $base-border-color !default; +$chat-tool-call-error-color: $base-danger !default; +$chat-tool-call-error-background-color: color.change($base-danger, $alpha: 0.1) !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss index 3f8905151866..3357083f5c41 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_index.scss @@ -65,6 +65,25 @@ $chat-bubble-delete-icon-size, $chat-bubble-gap, ); +@include chat-toolcallview( + $chat-tool-call-min-width, + $chat-tool-call-title-gap, + $chat-tool-call-title-color, + $chat-tool-call-icon-size, + $chat-tool-call-badge-padding-block, + $chat-tool-call-badge-padding-inline, + $chat-tool-call-badge-border-radius, + $chat-tool-call-content-gap, + $chat-tool-call-content-padding-block, + $chat-tool-call-content-margin-inline-start, + $chat-tool-call-content-padding-inline-start, + $chat-tool-call-content-border-width, + $chat-tool-call-content-border-color, + $chat-tool-call-field-gap, + $chat-tool-call-field-font-family, + $chat-tool-call-error-color, + $chat-tool-call-error-background-color, +); @include chat-messagegroup( $chat-messagegroup-gap, $chat-messagegroup-alignment-start-template, diff --git a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss index e34e11d75886..c8b8fec445a5 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/chat/_sizes.scss @@ -3,6 +3,7 @@ @use "../typography/sizes" as *; @use "../popup/sizes" as *; @use "../button/colors" as *; +@use "../accordion/sizes" as accordionSizes; $chat-border-radius: $base-border-radius * 3 !default; $chat-bubble-border-radius: null !default; @@ -80,6 +81,10 @@ $chat-file-icon-font-size: null !default; $chat-file-view-items-gap: 8px !default; $chat-bubble-gap: 8px !default; +$chat-tool-call-badge-padding-inline: null !default; +$chat-tool-call-content-gap: null !default; +$chat-tool-call-content-padding-block: null !default; + @if $size == "default" { $chat-bubble-border-radius: $fluent-base-border-radius * 3 !default; $chat-bubble-padding: 8px 12px !default; @@ -114,6 +119,10 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 8px !default; $chat-file-icon-font-size: 20px !default; + + $chat-tool-call-badge-padding-inline: 8px !default; + $chat-tool-call-content-gap: 16px !default; + $chat-tool-call-content-padding-block: 8px !default; } @else if $size == "compact" { @@ -150,7 +159,22 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 4px !default; $chat-file-icon-font-size: 16px !default; + + $chat-tool-call-badge-padding-inline: 6px !default; + $chat-tool-call-content-gap: 12px !default; + $chat-tool-call-content-padding-block: 6px !default; } $chat-typingindicator-padding: $chat-messagegroup-padding !default; $chat-typingindicator-bubble-border-radius: $chat-bubble-border-radius !default; + +$chat-tool-call-min-width: 360px !default; +$chat-tool-call-icon-size: accordionSizes.$accordion-title-icon-size !default; +$chat-tool-call-title-gap: accordionSizes.$accordion-title-icon-margin !default; +$chat-tool-call-badge-padding-block: 2px !default; +$chat-tool-call-badge-border-radius: $base-border-radius !default; +$chat-tool-call-content-border-width: 1px !default; +$chat-tool-call-content-margin-inline-start: accordionSizes.$fluent-accordion-title-horizontal-padding + math.div($chat-tool-call-icon-size, 2) !default; +$chat-tool-call-content-padding-inline-start: math.div($chat-tool-call-icon-size, 2) + $chat-tool-call-title-gap - $chat-tool-call-content-border-width !default; +$chat-tool-call-field-gap: 4px !default; +$chat-tool-call-field-font-family: Consolas, "Cascadia Mono", Menlo, monospace !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss index 73d08b5ebadb..75cdb73dce0a 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_colors.scss @@ -329,3 +329,8 @@ $chat-typingindicator-bubble-bg-color: $chat-bubble-background-color-secondary ! $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-inverted-text-color !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-danger !default; + +$chat-tool-call-title-color: $base-icon-color !default; +$chat-tool-call-content-border-color: $base-border-color !default; +$chat-tool-call-error-color: $base-danger !default; +$chat-tool-call-error-background-color: color.change($base-danger, $alpha: 0.1) !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss index 66b7c36452a2..7ac8edd602f5 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_index.scss @@ -65,6 +65,25 @@ $chat-bubble-delete-icon-size, $chat-bubble-gap, ); +@include chat-toolcallview( + $chat-tool-call-min-width, + $chat-tool-call-title-gap, + $chat-tool-call-title-color, + $chat-tool-call-icon-size, + $chat-tool-call-badge-padding-block, + $chat-tool-call-badge-padding-inline, + $chat-tool-call-badge-border-radius, + $chat-tool-call-content-gap, + $chat-tool-call-content-padding-block, + $chat-tool-call-content-margin-inline-start, + $chat-tool-call-content-padding-inline-start, + $chat-tool-call-content-border-width, + $chat-tool-call-content-border-color, + $chat-tool-call-field-gap, + $chat-tool-call-field-font-family, + $chat-tool-call-error-color, + $chat-tool-call-error-background-color, +); @include chat-messagegroup( $chat-messagegroup-gap, $chat-messagegroup-alignment-start-template, diff --git a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss index c1037584a25e..f19d10572fe1 100644 --- a/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/generic/chat/_sizes.scss @@ -4,6 +4,7 @@ @use "../sizes" as *; @use "../popup/sizes" as *; @use "../button/colors" as *; +@use "../accordion/sizes" as accordionSizes; $chat-border-radius: $base-border-radius !default; $chat-bubble-border-radius: $base-border-radius !default; @@ -84,6 +85,10 @@ $chat-file-icon-font-size: null !default; $chat-file-view-items-gap: 8px !default; $chat-bubble-gap: 8px !default; +$chat-tool-call-badge-padding-inline: null !default; +$chat-tool-call-content-gap: null !default; +$chat-tool-call-content-padding-block: null !default; + @if $size == "default" { $chat-bubble-padding: 8px 12px !default; $chat-bubble-delete-gap: 4px !default; @@ -122,6 +127,10 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 8px !default; $chat-file-icon-font-size: 18px !default; + + $chat-tool-call-badge-padding-inline: 8px !default; + $chat-tool-call-content-gap: 16px !default; + $chat-tool-call-content-padding-block: 8px !default; } @else if $size == "compact" { @@ -162,6 +171,21 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 4px !default; $chat-file-icon-font-size: 14 !default; + + $chat-tool-call-badge-padding-inline: 6px !default; + $chat-tool-call-content-gap: 12px !default; + $chat-tool-call-content-padding-block: 6px !default; } $chat-typingindicator-padding: $chat-messagegroup-padding !default; + +$chat-tool-call-min-width: 360px !default; +$chat-tool-call-icon-size: accordionSizes.$generic-accordion-title-icon-size !default; +$chat-tool-call-title-gap: accordionSizes.$accordion-title-icon-margin !default; +$chat-tool-call-badge-padding-block: 2px !default; +$chat-tool-call-badge-border-radius: $base-border-radius !default; +$chat-tool-call-content-border-width: 1px !default; +$chat-tool-call-content-margin-inline-start: accordionSizes.$generic-accordion-title-horizontal-padding + math.div($chat-tool-call-icon-size, 2) !default; +$chat-tool-call-content-padding-inline-start: math.div($chat-tool-call-icon-size, 2) + $chat-tool-call-title-gap - $chat-tool-call-content-border-width !default; +$chat-tool-call-field-gap: 4px !default; +$chat-tool-call-field-font-family: Consolas, "Cascadia Mono", Menlo, monospace !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss index 625265b3ee6d..858a3f028fcd 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_colors.scss @@ -149,3 +149,8 @@ $chat-suggestions-box-shadow: null !default; $chat-messagelist-contextmenu-delete-button-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-color: $base-danger !default; $chat-messagelist-contextmenu-delete-button-focused-bg: $base-hover-bg !default; + +$chat-tool-call-title-color: $base-icon-color !default; +$chat-tool-call-content-border-color: $base-border-color !default; +$chat-tool-call-error-color: $base-danger !default; +$chat-tool-call-error-background-color: color.change($base-danger, $alpha: 0.1) !default; diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss index afe41c08361f..150295ffe36f 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_index.scss @@ -65,6 +65,25 @@ $chat-bubble-delete-icon-size, $chat-bubble-gap, ); +@include chat-toolcallview( + $chat-tool-call-min-width, + $chat-tool-call-title-gap, + $chat-tool-call-title-color, + $chat-tool-call-icon-size, + $chat-tool-call-badge-padding-block, + $chat-tool-call-badge-padding-inline, + $chat-tool-call-badge-border-radius, + $chat-tool-call-content-gap, + $chat-tool-call-content-padding-block, + $chat-tool-call-content-margin-inline-start, + $chat-tool-call-content-padding-inline-start, + $chat-tool-call-content-border-width, + $chat-tool-call-content-border-color, + $chat-tool-call-field-gap, + $chat-tool-call-field-font-family, + $chat-tool-call-error-color, + $chat-tool-call-error-background-color, +); @include chat-messagegroup( $chat-messagegroup-gap, $chat-messagegroup-alignment-start-template, diff --git a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss index 5ced22e92a53..5799c32eade2 100644 --- a/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/material/chat/_sizes.scss @@ -2,6 +2,7 @@ @use "../sizes" as *; @use "../typography/sizes" as *; @use "../button/colors" as *; +@use "../accordion/sizes" as accordionSizes; $chat-border-radius: $base-border-radius * 2 !default; // 8px $chat-bubble-border-radius: $base-border-radius * 2 !default; @@ -82,6 +83,10 @@ $chat-file-icon-font-size: null !default; $chat-file-view-items-gap: 8px !default; $chat-bubble-gap: 8px !default; +$chat-tool-call-badge-padding-inline: null !default; +$chat-tool-call-content-gap: null !default; +$chat-tool-call-content-padding-block: null !default; + @if $size == "default" { $chat-bubble-border-radius: 8px !default; $chat-bubble-padding: 8px 16px !default; @@ -120,6 +125,10 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 8px !default; $chat-file-icon-font-size: 18px !default; + + $chat-tool-call-badge-padding-inline: 8px !default; + $chat-tool-call-content-gap: 16px !default; + $chat-tool-call-content-padding-block: 8px !default; } @else if $size == "compact" { @@ -160,6 +169,21 @@ $chat-bubble-gap: 8px !default; $chat-confirmation-popup-toolbar-gap: 4px !default; $chat-file-icon-font-size: 16px !default; + + $chat-tool-call-badge-padding-inline: 6px !default; + $chat-tool-call-content-gap: 12px !default; + $chat-tool-call-content-padding-block: 6px !default; } $chat-typingindicator-padding: $chat-messagegroup-padding !default; + +$chat-tool-call-min-width: 360px !default; +$chat-tool-call-icon-size: accordionSizes.$accordion-title-icon-size !default; +$chat-tool-call-title-gap: accordionSizes.$accordion-title-icon-margin !default; +$chat-tool-call-badge-padding-block: 2px !default; +$chat-tool-call-badge-border-radius: $base-border-radius !default; +$chat-tool-call-content-border-width: 1px !default; +$chat-tool-call-content-margin-inline-start: accordionSizes.$material-accordion-title-horizontal-padding + math.div($chat-tool-call-icon-size, 2) !default; +$chat-tool-call-content-padding-inline-start: math.div($chat-tool-call-icon-size, 2) + $chat-tool-call-title-gap - $chat-tool-call-content-border-width !default; +$chat-tool-call-field-gap: 4px !default; +$chat-tool-call-field-font-family: Consolas, "Cascadia Mono", Menlo, monospace !default; From f7698103e97df3befee764ca491c39be5266f213 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:15:51 +0300 Subject: [PATCH 6/8] Chat: Style tool calls with Fluent Next tokens --- .../widgets/fluent-next/chat/_colors.scss | 5 ++++ .../scss/widgets/fluent-next/chat/_index.scss | 25 +++++++++++++++- .../widgets/fluent-next/chat/_public.scss | 17 +++++++++++ .../scss/widgets/fluent-next/chat/_sizes.scss | 30 +++++++++++++++++++ .../tests/public-surface.baseline.json | 17 +++++++++++ .../tools/naming/derive-registries.ts | 2 +- .../tools/naming/registries.json | 6 +++- .../tools/review/nested-scopes.json | 6 ++++ 8 files changed, 105 insertions(+), 3 deletions(-) diff --git a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_colors.scss index 0da5aaa57b34..fa4b5b87cc2b 100644 --- a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_colors.scss +++ b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_colors.scss @@ -59,3 +59,8 @@ $chat-suggestions-box-shadow: ds.$box-shadow-md !default; $chat-message-list-context-menu-delete-button-content: ds.$color-content-danger !default; $chat-message-list-context-menu-delete-button-content-focused: ds.$color-content-danger !default; $chat-message-list-context-menu-delete-button-bg-focused: ds.$color-bg-hovered !default; + +$chat-tool-call-title-content: ds.$color-content-subtle !default; +$chat-tool-call-content-border: ds.$color-border !default; +$chat-tool-call-error-content: ds.$color-content-danger !default; +$chat-tool-call-error-bg: ds.$color-bg-danger-subtler !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_index.scss b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_index.scss index 09dc5e708198..ef50841f77d6 100644 --- a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_index.scss @@ -72,6 +72,25 @@ var(--dx-chat-bubble-delete-icon-size), var(--dx-chat-bubble-gap), ); +@include chat-toolcallview( + var(--dx-chat-tool-call-min-width), + var(--dx-chat-tool-call-title-gap), + var(--dx-chat-tool-call-title-content), + var(--dx-chat-tool-call-icon-size), + var(--dx-chat-tool-call-badge-padding-block), + var(--dx-chat-tool-call-badge-padding-inline), + var(--dx-chat-tool-call-badge-border-radius), + var(--dx-chat-tool-call-content-gap), + var(--dx-chat-tool-call-content-padding-block), + var(--dx-chat-tool-call-content-margin-inline-start), + var(--dx-chat-tool-call-content-padding-inline-start), + var(--dx-chat-tool-call-content-border-width), + var(--dx-chat-tool-call-content-border), + var(--dx-chat-tool-call-field-gap), + var(--dx-chat-tool-call-field-font-family), + var(--dx-chat-tool-call-error-content), + var(--dx-chat-tool-call-error-bg), +); @include chat-messagegroup( var(--dx-chat-message-group-gap), var(--dx-chat-message-group-start-grid-template-columns), @@ -179,7 +198,11 @@ .dx-chat-message-edited, .dx-chat-messagelist-empty-message, .dx-chat-messagelist-empty-prompt, - .dx-chat-messagelist-empty-image { + .dx-chat-messagelist-empty-image, + .dx-chat-tool-call-title, + .dx-chat-tool-call-error-badge, + .dx-chat-tool-call-field-error .dx-texteditor-input, + .dx-chat-tool-call-view .dx-accordion-item-title::before { color: var(--dx-chat-information-content-disabled); } } diff --git a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_public.scss b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_public.scss index dc842297914b..c5298fe0dba1 100644 --- a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_public.scss +++ b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_public.scss @@ -101,6 +101,23 @@ --dx-chat-suggestions-button-max-width: #{$chat-suggestions-button-max-width}; --dx-chat-suggestions-gap: #{$chat-suggestions-gap}; --dx-chat-suggestions-padding: #{$chat-suggestions-padding}; + --dx-chat-tool-call-badge-border-radius: #{$chat-tool-call-badge-border-radius}; + --dx-chat-tool-call-badge-padding-block: #{$chat-tool-call-badge-padding-block}; + --dx-chat-tool-call-badge-padding-inline: #{$chat-tool-call-badge-padding-inline}; + --dx-chat-tool-call-content-border: #{$chat-tool-call-content-border}; + --dx-chat-tool-call-content-border-width: #{$chat-tool-call-content-border-width}; + --dx-chat-tool-call-content-gap: #{$chat-tool-call-content-gap}; + --dx-chat-tool-call-content-margin-inline-start: #{$chat-tool-call-content-margin-inline-start}; + --dx-chat-tool-call-content-padding-block: #{$chat-tool-call-content-padding-block}; + --dx-chat-tool-call-content-padding-inline-start: #{$chat-tool-call-content-padding-inline-start}; + --dx-chat-tool-call-error-bg: #{$chat-tool-call-error-bg}; + --dx-chat-tool-call-error-content: #{$chat-tool-call-error-content}; + --dx-chat-tool-call-field-font-family: #{$chat-tool-call-field-font-family}; + --dx-chat-tool-call-field-gap: #{$chat-tool-call-field-gap}; + --dx-chat-tool-call-icon-size: #{$chat-tool-call-icon-size}; + --dx-chat-tool-call-min-width: #{$chat-tool-call-min-width}; + --dx-chat-tool-call-title-content: #{$chat-tool-call-title-content}; + --dx-chat-tool-call-title-gap: #{$chat-tool-call-title-gap}; --dx-chat-typing-indicator-bubble-column-gap: #{$chat-typing-indicator-bubble-column-gap}; --dx-chat-typing-indicator-bubble-padding-block: #{$chat-typing-indicator-bubble-padding-block}; --dx-chat-typing-indicator-bubble-padding-inline: #{$chat-typing-indicator-bubble-padding-inline}; diff --git a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_sizes.scss index 2cb3d2b84a33..2f65bb4261f2 100644 --- a/packages/devextreme-scss/scss/widgets/fluent-next/chat/_sizes.scss +++ b/packages/devextreme-scss/scss/widgets/fluent-next/chat/_sizes.scss @@ -76,6 +76,20 @@ $chat-file-icon-font-size: null !default; $chat-file-view-items-gap: ds.$spacing-80 !default; $chat-bubble-gap: ds.$spacing-80 !default; +$chat-tool-call-min-width: 360px !default; +$chat-tool-call-title-gap: null !default; +$chat-tool-call-icon-size: null !default; +$chat-tool-call-badge-padding-block: ds.$spacing-20 !default; +$chat-tool-call-badge-padding-inline: null !default; +$chat-tool-call-badge-border-radius: ds.$border-radius-40 !default; +$chat-tool-call-content-gap: null !default; +$chat-tool-call-content-padding-block: null !default; +$chat-tool-call-content-margin-inline-start: null !default; +$chat-tool-call-content-padding-inline-start: null !default; +$chat-tool-call-content-border-width: ds.$border-width-10 !default; +$chat-tool-call-field-gap: ds.$spacing-40 !default; +$chat-tool-call-field-font-family: ds.$font-family-system-mono !default; + @if $size == "default" { $chat-bubble-border-radius: ds.$border-radius-120 !default; $chat-bubble-padding: ds.$spacing-80 ds.$spacing-120 !default; @@ -110,6 +124,14 @@ $chat-bubble-gap: ds.$spacing-80 !default; $chat-confirmation-popup-toolbar-gap: ds.$spacing-80 !default; $chat-file-icon-font-size: ds.$spacing-200 !default; + + $chat-tool-call-title-gap: ds.$spacing-80 !default; + $chat-tool-call-icon-size: ds.$spacing-200 !default; + $chat-tool-call-badge-padding-inline: ds.$spacing-80 !default; + $chat-tool-call-content-gap: ds.$spacing-160 !default; + $chat-tool-call-content-padding-block: ds.$spacing-80 !default; + $chat-tool-call-content-margin-inline-start: ds.$spacing-220 !default; + $chat-tool-call-content-padding-inline-start: ds.$spacing-170 !default; } @else if $size == "compact" { @@ -146,6 +168,14 @@ $chat-bubble-gap: ds.$spacing-80 !default; $chat-confirmation-popup-toolbar-gap: ds.$spacing-40 !default; $chat-file-icon-font-size: ds.$spacing-160 !default; + + $chat-tool-call-title-gap: ds.$spacing-60 !default; + $chat-tool-call-icon-size: ds.$spacing-160 !default; + $chat-tool-call-badge-padding-inline: ds.$spacing-60 !default; + $chat-tool-call-content-gap: ds.$spacing-120 !default; + $chat-tool-call-content-padding-block: ds.$spacing-60 !default; + $chat-tool-call-content-margin-inline-start: ds.$spacing-160 !default; + $chat-tool-call-content-padding-inline-start: ds.$spacing-130 !default; } $chat-bubble-image-padding: ds.$spacing-40 !default; diff --git a/packages/devextreme-scss/tests/public-surface.baseline.json b/packages/devextreme-scss/tests/public-surface.baseline.json index b0824c633357..b6c188e381b3 100644 --- a/packages/devextreme-scss/tests/public-surface.baseline.json +++ b/packages/devextreme-scss/tests/public-surface.baseline.json @@ -474,6 +474,23 @@ "--dx-chat-suggestions-button-max-width", "--dx-chat-suggestions-gap", "--dx-chat-suggestions-padding", + "--dx-chat-tool-call-badge-border-radius", + "--dx-chat-tool-call-badge-padding-block", + "--dx-chat-tool-call-badge-padding-inline", + "--dx-chat-tool-call-content-border", + "--dx-chat-tool-call-content-border-width", + "--dx-chat-tool-call-content-gap", + "--dx-chat-tool-call-content-margin-inline-start", + "--dx-chat-tool-call-content-padding-block", + "--dx-chat-tool-call-content-padding-inline-start", + "--dx-chat-tool-call-error-bg", + "--dx-chat-tool-call-error-content", + "--dx-chat-tool-call-field-font-family", + "--dx-chat-tool-call-field-gap", + "--dx-chat-tool-call-icon-size", + "--dx-chat-tool-call-min-width", + "--dx-chat-tool-call-title-content", + "--dx-chat-tool-call-title-gap", "--dx-chat-typing-indicator-bubble-bg", "--dx-chat-typing-indicator-bubble-border-radius", "--dx-chat-typing-indicator-bubble-column-gap", diff --git a/packages/devextreme-scss/tools/naming/derive-registries.ts b/packages/devextreme-scss/tools/naming/derive-registries.ts index d9292f319069..9f0a6b9be3d8 100644 --- a/packages/devextreme-scss/tools/naming/derive-registries.ts +++ b/packages/devextreme-scss/tools/naming/derive-registries.ts @@ -794,7 +794,7 @@ const OVERRIDES: Overrides = { 'alert-list', 'prompt', 'textarea', 'toolbar', 'information', 'author-name', 'timestamp', 'typing-indicator', 'circle', 'bubble', 'group', // .dx-chat-messagebubble-image is a real box inside a bubble - 'image'], + 'image', 'tool-call', 'title', 'badge', 'field'], /* * `alpha` alone was doing three jobs — the alpha row, the alpha cell and the alpha scale are * three different elements with three different sizes, so each gets its own segment and the diff --git a/packages/devextreme-scss/tools/naming/registries.json b/packages/devextreme-scss/tools/naming/registries.json index e43b87d18bba..37c759808081 100644 --- a/packages/devextreme-scss/tools/naming/registries.json +++ b/packages/devextreme-scss/tools/naming/registries.json @@ -1266,7 +1266,11 @@ "circle", "bubble", "group", - "image" + "image", + "tool-call", + "title", + "badge", + "field" ], "color-view": [ "palette", diff --git a/packages/devextreme-scss/tools/review/nested-scopes.json b/packages/devextreme-scss/tools/review/nested-scopes.json index 9c6b4b53728c..dee02cbf42fd 100644 --- a/packages/devextreme-scss/tools/review/nested-scopes.json +++ b/packages/devextreme-scss/tools/review/nested-scopes.json @@ -94,6 +94,12 @@ "dx-chat-suggestions", "dx-chat-textarea", "dx-chat-textarea-toolbar", + "dx-chat-tool-call-content", + "dx-chat-tool-call-error-badge", + "dx-chat-tool-call-field", + "dx-chat-tool-call-field-error", + "dx-chat-tool-call-title", + "dx-chat-tool-call-view", "dx-chat-typingindicator-bubble", "dx-chat-typingindicator-circle", "dx-chat-typingindicator-content", From 3eccbc301eafc987884e179f30567e7d49f19de9 Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 13:16:01 +0300 Subject: [PATCH 7/8] Wrappers: Add the Chat toolCalls nested option --- .../src/core/tokens/index.ts | 1 + .../devextreme-angular/src/ui/chat/index.ts | 9 ++ .../src/ui/chat/nested/chat-item-dxi.ts | 16 +++- .../src/ui/chat/nested/index.ts | 1 + .../src/ui/chat/nested/item-dxi.ts | 16 +++- .../src/ui/chat/nested/tool-call-dxi.ts | 92 +++++++++++++++++++ packages/devextreme-react/src/chat.ts | 34 ++++++- packages/devextreme-vue/src/chat.ts | 37 +++++++- 8 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 packages/devextreme-angular/src/ui/chat/nested/tool-call-dxi.ts diff --git a/packages/devextreme-angular/src/core/tokens/index.ts b/packages/devextreme-angular/src/core/tokens/index.ts index b4a8073203e5..e6d1937c2f98 100644 --- a/packages/devextreme-angular/src/core/tokens/index.ts +++ b/packages/devextreme-angular/src/core/tokens/index.ts @@ -42,3 +42,4 @@ export const PROPERTY_TOKEN_views = new InjectionToken('property-token-v export const PROPERTY_TOKEN_layers = new InjectionToken('property-token-layers'); export const PROPERTY_TOKEN_legends = new InjectionToken('property-token-legends'); export const PROPERTY_TOKEN_center = new InjectionToken('property-token-center'); +export const PROPERTY_TOKEN_toolCalls = new InjectionToken('property-token-toolCalls'); diff --git a/packages/devextreme-angular/src/ui/chat/index.ts b/packages/devextreme-angular/src/ui/chat/index.ts index 353e80f69ded..fd96bbd4fca1 100644 --- a/packages/devextreme-angular/src/ui/chat/index.ts +++ b/packages/devextreme-angular/src/ui/chat/index.ts @@ -64,12 +64,14 @@ import { DxoChatSpeechRecognitionConfigModule } from 'devextreme-angular/ui/chat import { DxoChatSpeechToTextOptionsModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatSuggestionsModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatSuggestionsItemModule } from 'devextreme-angular/ui/chat/nested'; +import { DxiChatToolCallModule } from 'devextreme-angular/ui/chat/nested'; import { DxiChatTypingUserModule } from 'devextreme-angular/ui/chat/nested'; import { DxoChatUserModule } from 'devextreme-angular/ui/chat/nested'; import { PROPERTY_TOKEN_alerts, PROPERTY_TOKEN_attachments, PROPERTY_TOKEN_items, + PROPERTY_TOKEN_toolCalls, PROPERTY_TOKEN_typingUsers, } from 'devextreme-angular/core/tokens'; @@ -106,6 +108,11 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges this.setChildren('items', value); } + @ContentChildren(PROPERTY_TOKEN_toolCalls) + set _toolCallsContentChildren(value: QueryList) { + this.setChildren('toolCalls', value); + } + @ContentChildren(PROPERTY_TOKEN_typingUsers) set _typingUsersContentChildren(value: QueryList) { this.setChildren('typingUsers', value); @@ -817,6 +824,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatSpeechToTextOptionsModule, DxoChatSuggestionsModule, DxiChatSuggestionsItemModule, + DxiChatToolCallModule, DxiChatTypingUserModule, DxoChatUserModule, DxIntegrationModule, @@ -847,6 +855,7 @@ export class DxChatComponent extends DxComponent implements OnDestroy, OnChanges DxoChatSpeechToTextOptionsModule, DxoChatSuggestionsModule, DxiChatSuggestionsItemModule, + DxiChatToolCallModule, DxiChatTypingUserModule, DxoChatUserModule, DxTemplateModule diff --git a/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts index 332d1c5ad1f8..82a2b6003467 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/chat-item-dxi.ts @@ -8,7 +8,7 @@ import { ContentChildren, QueryList } from '@angular/core'; -import type { Attachment, User, MessageType } from 'devextreme/ui/chat'; +import type { Attachment, User, ToolCall, MessageType } from 'devextreme/ui/chat'; import { DxIntegrationModule, @@ -20,6 +20,7 @@ import { PROPERTY_TOKEN_items } from 'devextreme-angular/core/tokens'; import { PROPERTY_TOKEN_attachments, + PROPERTY_TOKEN_toolCalls, } from 'devextreme-angular/core/tokens'; @Component({ @@ -40,6 +41,11 @@ export class DxiChatChatItemComponent extends CollectionNestedOption { set _attachmentsContentChildren(value: QueryList) { this.setChildren('attachments', value); } + + @ContentChildren(PROPERTY_TOKEN_toolCalls) + set _toolCallsContentChildren(value: QueryList) { + this.setChildren('toolCalls', value); + } @Input() get alt(): string { @@ -113,6 +119,14 @@ export class DxiChatChatItemComponent extends CollectionNestedOption { this._setOption('timestamp', value); } + @Input() + get toolCalls(): Array { + return this._getOption('toolCalls'); + } + set toolCalls(value: Array) { + this._setOption('toolCalls', value); + } + @Input() get type(): MessageType { return this._getOption('type'); diff --git a/packages/devextreme-angular/src/ui/chat/nested/index.ts b/packages/devextreme-angular/src/ui/chat/nested/index.ts index f0db937f8d34..5de28fe9f0d3 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/index.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/index.ts @@ -13,6 +13,7 @@ export * from './speech-recognition-config'; export * from './speech-to-text-options'; export * from './suggestions-item-dxi'; export * from './suggestions'; +export * from './tool-call-dxi'; export * from './typing-user-dxi'; export * from './user'; diff --git a/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts index 73eaa9bb7efc..d34bc6002ec9 100644 --- a/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts +++ b/packages/devextreme-angular/src/ui/chat/nested/item-dxi.ts @@ -13,7 +13,7 @@ import { QueryList } from '@angular/core'; import { DOCUMENT } from '@angular/common'; -import type { Attachment, User, MessageType } from 'devextreme/ui/chat'; +import type { Attachment, User, ToolCall, MessageType } from 'devextreme/ui/chat'; import type { ButtonType } from 'devextreme/common'; import { @@ -30,6 +30,7 @@ import { PROPERTY_TOKEN_items } from 'devextreme-angular/core/tokens'; import { PROPERTY_TOKEN_attachments, + PROPERTY_TOKEN_toolCalls, } from 'devextreme-angular/core/tokens'; @Component({ @@ -52,6 +53,11 @@ export class DxiChatItemComponent extends CollectionNestedOption implements Afte set _attachmentsContentChildren(value: QueryList) { this.setChildren('attachments', value); } + + @ContentChildren(PROPERTY_TOKEN_toolCalls) + set _toolCallsContentChildren(value: QueryList) { + this.setChildren('toolCalls', value); + } @Input() get alt(): string { @@ -125,6 +131,14 @@ export class DxiChatItemComponent extends CollectionNestedOption implements Afte this._setOption('timestamp', value); } + @Input() + get toolCalls(): Array { + return this._getOption('toolCalls'); + } + set toolCalls(value: Array) { + this._setOption('toolCalls', value); + } + @Input() get type(): MessageType | ButtonType | string { return this._getOption('type'); diff --git a/packages/devextreme-angular/src/ui/chat/nested/tool-call-dxi.ts b/packages/devextreme-angular/src/ui/chat/nested/tool-call-dxi.ts new file mode 100644 index 000000000000..d8f9e282c03f --- /dev/null +++ b/packages/devextreme-angular/src/ui/chat/nested/tool-call-dxi.ts @@ -0,0 +1,92 @@ +/* tslint:disable:max-line-length */ +import { + Component, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; +import type { ToolCallStatus } from 'devextreme/ui/chat'; + +import { + DxIntegrationModule, + NestedOptionHost, +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + +import { PROPERTY_TOKEN_toolCalls } from 'devextreme-angular/core/tokens'; + +@Component({ + selector: 'dxi-chat-tool-call', + template: '', + styles: [''], + imports: [ DxIntegrationModule ], + providers: [ + NestedOptionHost, + { + provide: PROPERTY_TOKEN_toolCalls, + useExisting: DxiChatToolCallComponent, + } + ] +}) +export class DxiChatToolCallComponent extends CollectionNestedOption { + @Input() + get arguments(): Record { + return this._getOption('arguments'); + } + set arguments(value: Record) { + this._setOption('arguments', value); + } + + @Input() + get name(): string { + return this._getOption('name'); + } + set name(value: string) { + this._setOption('name', value); + } + + @Input() + get result(): any { + return this._getOption('result'); + } + set result(value: any) { + this._setOption('result', value); + } + + @Input() + get status(): ToolCallStatus { + return this._getOption('status'); + } + set status(value: ToolCallStatus) { + this._setOption('status', value); + } + + + protected get _optionPath() { + return 'toolCalls'; + } + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } +} + +@NgModule({ + imports: [ + DxiChatToolCallComponent + ], + exports: [ + DxiChatToolCallComponent + ], +}) +export class DxiChatToolCallModule { } diff --git a/packages/devextreme-react/src/chat.ts b/packages/devextreme-react/src/chat.ts index b536ca714887..ee628161f088 100644 --- a/packages/devextreme-react/src/chat.ts +++ b/packages/devextreme-react/src/chat.ts @@ -8,7 +8,7 @@ import dxChat, { import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component"; import NestedOption from "./core/nested-option"; -import type { Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, TypingEndEvent, TypingStartEvent, Attachment as ChatAttachment, User as ChatUser, MessageType, SendButtonAction, SendButtonClickEvent } from "devextreme/ui/chat"; +import type { Message, AttachmentDownloadClickEvent, DisposingEvent, InitializedEvent, MessageDeletedEvent, MessageDeletingEvent, MessageEditCanceledEvent, MessageEditingStartEvent, MessageEnteredEvent, MessageUpdatedEvent, MessageUpdatingEvent, TypingEndEvent, TypingStartEvent, Attachment as ChatAttachment, User as ChatUser, ToolCall as ChatToolCall, MessageType, SendButtonAction, SendButtonClickEvent, ToolCallStatus } from "devextreme/ui/chat"; import type { DisposingEvent as FileUploaderDisposingEvent, InitializedEvent as FileUploaderInitializedEvent, BeforeSendEvent, ContentReadyEvent, DropZoneEnterEvent, DropZoneLeaveEvent, FilesUploadedEvent, OptionChangedEvent, ProgressEvent, UploadAbortedEvent, UploadedEvent, UploadErrorEvent, UploadStartedEvent, ValueChangedEvent, UploadHttpMethod, FileUploadMode } from "devextreme/ui/file_uploader"; import type { DisposingEvent as SpeechToTextDisposingEvent, InitializedEvent as SpeechToTextInitializedEvent, ContentReadyEvent as SpeechToTextContentReadyEvent, OptionChangedEvent as SpeechToTextOptionChangedEvent, CustomSpeechRecognizer as SpeechToTextCustomSpeechRecognizer, EndEvent, ErrorEvent, ResultEvent, StartClickEvent, StopClickEvent, SpeechRecognitionConfig as SpeechToTextSpeechRecognitionConfig } from "devextreme/ui/speech_to_text"; import type { DisposingEvent as ButtonGroupDisposingEvent, InitializedEvent as ButtonGroupInitializedEvent, ContentReadyEvent as ButtonGroupContentReadyEvent, OptionChangedEvent as ButtonGroupOptionChangedEvent, dxButtonGroupItem, ItemClickEvent, SelectionChangedEvent } from "devextreme/ui/button_group"; @@ -187,6 +187,7 @@ type IChatItemProps = React.PropsWithChildren<{ src?: string; text?: string; timestamp?: Date | number | string; + toolCalls?: Array; type?: MessageType; }> const _componentChatItem = (props: IChatItemProps) => { @@ -197,7 +198,8 @@ const _componentChatItem = (props: IChatItemProps) => { IsCollectionItem: true, ExpectedChildren: { attachment: { optionName: "attachments", isCollectionItem: true }, - author: { optionName: "author", isCollectionItem: false } + author: { optionName: "author", isCollectionItem: false }, + toolCall: { optionName: "toolCalls", isCollectionItem: true } }, }, }); @@ -367,6 +369,7 @@ type IItemProps = React.PropsWithChildren<{ src?: string; text?: string; timestamp?: Date | number | string; + toolCalls?: Array; type?: MessageType | ButtonType | string; disabled?: boolean; elementAttr?: Record; @@ -385,7 +388,8 @@ const _componentItem = (props: IItemProps) => { IsCollectionItem: true, ExpectedChildren: { attachment: { optionName: "attachments", isCollectionItem: true }, - author: { optionName: "author", isCollectionItem: false } + author: { optionName: "author", isCollectionItem: false }, + toolCall: { optionName: "toolCalls", isCollectionItem: true } }, TemplateProps: [{ tmplOption: "template", @@ -609,6 +613,28 @@ const SuggestionsItem = Object.assign; + name?: string; + result?: any; + status?: ToolCallStatus; +}> +const _componentToolCall = (props: IToolCallProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "toolCalls", + IsCollectionItem: true, + }, + }); +}; + +const ToolCall = Object.assign(_componentToolCall, { + componentType: "option", +}); + // owners: // Chat type ITypingUserProps = React.PropsWithChildren<{ @@ -687,6 +713,8 @@ export { ISuggestionsProps, SuggestionsItem, ISuggestionsItemProps, + ToolCall, + IToolCallProps, TypingUser, ITypingUserProps, User, diff --git a/packages/devextreme-vue/src/chat.ts b/packages/devextreme-vue/src/chat.ts index 62b453875482..619ac7d7778d 100644 --- a/packages/devextreme-vue/src/chat.ts +++ b/packages/devextreme-vue/src/chat.ts @@ -25,9 +25,11 @@ import { SendButtonProperties, User, Attachment, + ToolCall, MessageType, SendButtonAction, SendButtonClickEvent, + ToolCallStatus, } from "devextreme/ui/chat"; import { DataSourceOptions, @@ -348,6 +350,7 @@ const DxChatItemConfig = { "update:src": null, "update:text": null, "update:timestamp": null, + "update:toolCalls": null, "update:type": null, }, props: { @@ -360,6 +363,7 @@ const DxChatItemConfig = { src: String, text: String, timestamp: [Date, Number, String], + toolCalls: Array as PropType>, type: String as PropType } }; @@ -372,7 +376,8 @@ const DxChatItem = defineComponent(DxChatItemConfig); (DxChatItem as any).$_isCollectionItem = true; (DxChatItem as any).$_expectedChildren = { attachment: { isCollectionItem: true, optionName: "attachments" }, - author: { isCollectionItem: false, optionName: "author" } + author: { isCollectionItem: false, optionName: "author" }, + toolCall: { isCollectionItem: true, optionName: "toolCalls" } }; const DxCustomSpeechRecognizerConfig = { @@ -601,6 +606,7 @@ const DxItemConfig = { "update:template": null, "update:text": null, "update:timestamp": null, + "update:toolCalls": null, "update:type": null, "update:visible": null, }, @@ -619,6 +625,7 @@ const DxItemConfig = { template: {}, text: String, timestamp: [Date, Number, String], + toolCalls: Array as PropType>, type: String as PropType, visible: Boolean } @@ -632,7 +639,8 @@ const DxItem = defineComponent(DxItemConfig); (DxItem as any).$_isCollectionItem = true; (DxItem as any).$_expectedChildren = { attachment: { isCollectionItem: true, optionName: "attachments" }, - author: { isCollectionItem: false, optionName: "author" } + author: { isCollectionItem: false, optionName: "author" }, + toolCall: { isCollectionItem: true, optionName: "toolCalls" } }; const DxMessageTimestampFormatConfig = { @@ -886,6 +894,30 @@ const DxSuggestionsItem = defineComponent(DxSuggestionsItemConfig); (DxSuggestionsItem as any).$_optionName = "items"; (DxSuggestionsItem as any).$_isCollectionItem = true; +const DxToolCallConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:arguments": null, + "update:name": null, + "update:result": null, + "update:status": null, + }, + props: { + arguments: Object as PropType>, + name: String, + result: {}, + status: String as PropType + } +}; + +prepareConfigurationComponentConfig(DxToolCallConfig); + +const DxToolCall = defineComponent(DxToolCallConfig); + +(DxToolCall as any).$_optionName = "toolCalls"; +(DxToolCall as any).$_isCollectionItem = true; + const DxTypingUserConfig = { emits: { "update:isActive": null, @@ -951,6 +983,7 @@ export { DxSpeechToTextOptions, DxSuggestions, DxSuggestionsItem, + DxToolCall, DxTypingUser, DxUser }; From 29854bbeb87c0b6fa66c92a394de9bec4bc8a21d Mon Sep 17 00:00:00 2001 From: Raushen Date: Fri, 16 Oct 2026 17:15:01 +0300 Subject: [PATCH 8/8] Chat: Move expanded tool call tests to the behavior suite --- .../chatParts/toolCallView.markup.tests.js | 49 +------------------ .../chatParts/toolCallView.tests.js | 32 ++++++++++++ 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js index c1f5129c8018..ed93e780c131 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.markup.tests.js @@ -1,22 +1,16 @@ import $ from 'jquery'; -import fx from 'common/core/animation/fx'; import ToolCallView, { CHAT_TOOL_CALL_VIEW_CLASS, CHAT_TOOL_CALL_TITLE_CLASS, CHAT_TOOL_CALL_ICON_CLASS, CHAT_TOOL_CALL_NAME_CLASS, - CHAT_TOOL_CALL_CONTENT_CLASS, - CHAT_TOOL_CALL_FIELD_CLASS, - CHAT_TOOL_CALL_FIELD_LABEL_CLASS, } from '__internal/ui/chat/tool_call_view/tool_call_view'; const { test } = QUnit; const ACCORDION_CLASS = 'dx-accordion'; const ACCORDION_ITEM_TITLE_CLASS = 'dx-accordion-item-title'; -const ACCORDION_ITEM_BODY_CLASS = 'dx-accordion-item-body'; const ICON_CLASS = 'dx-icon'; -const TEXTAREA_CLASS = 'dx-textarea'; const toolCall = { name: 'getWeather', @@ -26,25 +20,12 @@ const toolCall = { QUnit.module('ToolCallView', { beforeEach() { - fx.off = true; this.$element = $('
').appendTo('#qunit-fixture'); - - const init = (options) => { - this.instance = new ToolCallView(this.$element, options); - }; - - this.reinit = (options) => { - this.instance.dispose(); - - init(options); - }; - - init({ toolCalls: [toolCall] }); + this.instance = new ToolCallView(this.$element, { toolCalls: [toolCall] }); }, afterEach() { this.instance.dispose(); this.$element.remove(); - fx.off = false; }, }, () => { test('root element should have correct class', function(assert) { @@ -67,32 +48,4 @@ QUnit.module('ToolCallView', { assert.strictEqual($children.eq(1).hasClass(CHAT_TOOL_CALL_NAME_CLASS), true, 'second child is the name'); assert.strictEqual($children.eq(1).attr('title'), 'getWeather', 'name has a title attribute'); }); - - test('expanded item body should contain the content element with fields', function(assert) { - this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS}`).trigger('dxclick'); - - const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); - const $fields = $content.children(`.${CHAT_TOOL_CALL_FIELD_CLASS}`); - - assert.strictEqual($content.length, 1, 'content element exists'); - assert.strictEqual($fields.length, 2, 'two fields are rendered'); - - $fields.each((_, field) => { - const $fieldChildren = $(field).children(); - - assert.strictEqual($fieldChildren.eq(0).hasClass(CHAT_TOOL_CALL_FIELD_LABEL_CLASS), true, 'first child is the label'); - assert.strictEqual($fieldChildren.eq(1).hasClass(TEXTAREA_CLASS), true, 'second child is the text area'); - }); - }); - - test('expanded group body should contain the content element with the nested accordion', function(assert) { - this.reinit({ toolCalls: [toolCall, toolCall] }); - - this.$element.find(`.${ACCORDION_ITEM_TITLE_CLASS}`).trigger('dxclick'); - - const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); - - assert.strictEqual($content.length, 1, 'content element exists'); - assert.strictEqual($content.children(`.${ACCORDION_CLASS}`).length, 1, 'nested accordion is a direct child of the content'); - }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js index b1ea2f344294..b1a486961a51 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/chatParts/toolCallView.tests.js @@ -3,6 +3,7 @@ import fx from 'common/core/animation/fx'; import localization from 'localization'; import ToolCallView, { + CHAT_TOOL_CALL_CONTENT_CLASS, CHAT_TOOL_CALL_ERROR_BADGE_CLASS, CHAT_TOOL_CALL_FIELD_CLASS, CHAT_TOOL_CALL_FIELD_ERROR_CLASS, @@ -13,7 +14,9 @@ import ToolCallView, { } from '__internal/ui/chat/tool_call_view/tool_call_view'; import TextArea from '__internal/ui/text_area'; +const ACCORDION_CLASS = 'dx-accordion'; const ACCORDION_ITEM_CLASS = 'dx-accordion-item'; +const ACCORDION_ITEM_BODY_CLASS = 'dx-accordion-item-body'; const ACCORDION_ITEM_TITLE_CLASS = 'dx-accordion-item-title'; const ACCORDION_ITEM_OPENED_CLASS = 'dx-accordion-item-opened'; const TEXTAREA_CLASS = 'dx-textarea'; @@ -127,6 +130,35 @@ QUnit.module('ToolCallView', moduleConfig, () => { }); }); + QUnit.module('Structure', () => { + QUnit.test('expanded item body should contain the content element with fields', function(assert) { + this.expand(); + + const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); + const $fields = $content.children(`.${CHAT_TOOL_CALL_FIELD_CLASS}`); + + assert.strictEqual($content.length, 1, 'content element exists'); + assert.strictEqual($fields.length, 2, 'two fields are rendered'); + + $fields.each((_, field) => { + const $fieldChildren = $(field).children(); + + assert.strictEqual($fieldChildren.eq(0).hasClass(CHAT_TOOL_CALL_FIELD_LABEL_CLASS), true, 'first child is the label'); + assert.strictEqual($fieldChildren.eq(1).hasClass(TEXTAREA_CLASS), true, 'second child is the text area'); + }); + }); + + QUnit.test('expanded group body should contain the content element with the nested accordion', function(assert) { + this.reinit({ toolCalls: [currentTimeCall, stockPriceCall] }); + this.expand(); + + const $content = this.$element.find(`.${ACCORDION_ITEM_BODY_CLASS} > .${CHAT_TOOL_CALL_CONTENT_CLASS}`); + + assert.strictEqual($content.length, 1, 'content element exists'); + assert.strictEqual($content.children(`.${ACCORDION_CLASS}`).length, 1, 'nested accordion is a direct child of the content'); + }); + }); + QUnit.module('Fields', () => { QUnit.test('should not render fields until the item is expanded', function(assert) { assert.strictEqual(this.getFields().length, 0);