diff --git a/packages/@react-spectrum/ai/exports/index.ts b/packages/@react-spectrum/ai/exports/index.ts index a9d8a39c46c..3c9f7dd3008 100644 --- a/packages/@react-spectrum/ai/exports/index.ts +++ b/packages/@react-spectrum/ai/exports/index.ts @@ -3,18 +3,21 @@ export {MessageFeedback} from '../src/MessageFeedback'; export {MessageSource, SourceList, SourceListItem} from '../src/MessageSource'; export {MessageSuggestion, MessageSuggestionList} from '../src/MessageSuggestion'; export { + AutoLinkingTokenFieldValue, PromptField, PromptFieldSubmitButton, PromptTokenField, AttachFileMenuItem, + CommandMenuItem, InsertMenuButton, + InsertTextMenuItem, InsertTokenMenuItem, PromptFieldAttachmentList, PromptFieldToolbar, PromptToken } from '../src/PromptField'; export {ResponseStatus, ResponseStatusTitle, ResponseStatusPanel} from '../src/ResponseStatus'; -export {Chat, Thread, ThreadItem, ThreadScrollButton} from '../src/Chat'; +export {Chat, Thread, ThreadItem, ThreadScrollButton, PromptFocusContext} from '../src/Chat'; export {TokenFieldValue} from 'react-aria-components/TokenField'; export {UserMessage} from '../src/UserMessage'; diff --git a/packages/@react-spectrum/ai/intl/ar-AE.json b/packages/@react-spectrum/ai/intl/ar-AE.json index 53c8bab8068..b2fca80eaa6 100644 --- a/packages/@react-spectrum/ai/intl/ar-AE.json +++ b/packages/@react-spectrum/ai/intl/ar-AE.json @@ -2,5 +2,17 @@ "chat.newMessage": "رسالة جديدة", "messagefeedback.thumbDown": "استجابة سيئة", "messagefeedback.thumbUp": "استجابة جيدة", - "responsestatus.loading": "جارٍ التحميل" + "responsestatus.loading": "جارٍ التحميل", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/bg-BG.json b/packages/@react-spectrum/ai/intl/bg-BG.json index 063adbb66b2..d2f54a78bbd 100644 --- a/packages/@react-spectrum/ai/intl/bg-BG.json +++ b/packages/@react-spectrum/ai/intl/bg-BG.json @@ -2,5 +2,17 @@ "chat.newMessage": "Ново съобщение", "messagefeedback.thumbDown": "Лош отговор", "messagefeedback.thumbUp": "Добър отговор", - "responsestatus.loading": "Зареждане" + "responsestatus.loading": "Зареждане", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/cs-CZ.json b/packages/@react-spectrum/ai/intl/cs-CZ.json index 29d937d46b9..14bdfe9af42 100644 --- a/packages/@react-spectrum/ai/intl/cs-CZ.json +++ b/packages/@react-spectrum/ai/intl/cs-CZ.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nová zpráva", "messagefeedback.thumbDown": "Špatná odpověď", "messagefeedback.thumbUp": "Dobrá odpověď", - "responsestatus.loading": "Načítání" + "responsestatus.loading": "Načítání", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/da-DK.json b/packages/@react-spectrum/ai/intl/da-DK.json index f95ac272ee3..89da2f0990a 100644 --- a/packages/@react-spectrum/ai/intl/da-DK.json +++ b/packages/@react-spectrum/ai/intl/da-DK.json @@ -2,5 +2,17 @@ "chat.newMessage": "Ny besked", "messagefeedback.thumbDown": "Dårligt svar", "messagefeedback.thumbUp": "Godt svar", - "responsestatus.loading": "Indlæser" + "responsestatus.loading": "Indlæser", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/de-DE.json b/packages/@react-spectrum/ai/intl/de-DE.json index f91861a66dc..1a306440fb1 100644 --- a/packages/@react-spectrum/ai/intl/de-DE.json +++ b/packages/@react-spectrum/ai/intl/de-DE.json @@ -2,5 +2,17 @@ "chat.newMessage": "Neue Nachricht", "messagefeedback.thumbDown": "Fehlerhafte Antwort", "messagefeedback.thumbUp": "Gute Antwort", - "responsestatus.loading": "Ladevorgang läuft" + "responsestatus.loading": "Ladevorgang läuft", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Antworten werden mit KI generiert und sind möglicherweise ungenau. Vor der Verwendung prüfen.", + "promptfield.aiUserGuidlines": "AI-Benutzerrichtlinien", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/el-GR.json b/packages/@react-spectrum/ai/intl/el-GR.json index 1cd35614f1e..c579f8d8fd1 100644 --- a/packages/@react-spectrum/ai/intl/el-GR.json +++ b/packages/@react-spectrum/ai/intl/el-GR.json @@ -2,5 +2,17 @@ "chat.newMessage": "Νέο μήνυμα", "messagefeedback.thumbDown": "Κακή απάντηση", "messagefeedback.thumbUp": "Καλή απάντηση", - "responsestatus.loading": "Φόρτωση" + "responsestatus.loading": "Φόρτωση", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/en-US.json b/packages/@react-spectrum/ai/intl/en-US.json index 6584405209f..eed12ebd480 100644 --- a/packages/@react-spectrum/ai/intl/en-US.json +++ b/packages/@react-spectrum/ai/intl/en-US.json @@ -1,8 +1,18 @@ { "chat.newMessage": "New message", + "chat.scrollToBottom": "Scroll to bottom", "messagefeedback.thumbDown": "Bad response", "messagefeedback.thumbUp": "Good response", "responsestatus.loading": "Loading", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", "promptfield.label": "Prompt", - "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task." + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/es-ES.json b/packages/@react-spectrum/ai/intl/es-ES.json index ec3f94e8b81..9c97df7b313 100644 --- a/packages/@react-spectrum/ai/intl/es-ES.json +++ b/packages/@react-spectrum/ai/intl/es-ES.json @@ -2,5 +2,17 @@ "chat.newMessage": "Mensaje nuevo", "messagefeedback.thumbDown": "Respuesta incorrecta", "messagefeedback.thumbUp": "Respuesta correcta", - "responsestatus.loading": "Cargando" + "responsestatus.loading": "Cargando", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Las respuestas se generan mediante IA y pueden ser imprecisas. Compruebe antes de utilizar.", + "promptfield.aiUserGuidlines": "Directrices de usuario de AI", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/et-EE.json b/packages/@react-spectrum/ai/intl/et-EE.json index 714df3d9364..3742fa0aed1 100644 --- a/packages/@react-spectrum/ai/intl/et-EE.json +++ b/packages/@react-spectrum/ai/intl/et-EE.json @@ -2,5 +2,17 @@ "chat.newMessage": "Uus sõnum", "messagefeedback.thumbDown": "Halb vastus", "messagefeedback.thumbUp": "Hea vastus", - "responsestatus.loading": "Laadimine" + "responsestatus.loading": "Laadimine", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/fi-FI.json b/packages/@react-spectrum/ai/intl/fi-FI.json index 5f21d9f4628..322bbf6365e 100644 --- a/packages/@react-spectrum/ai/intl/fi-FI.json +++ b/packages/@react-spectrum/ai/intl/fi-FI.json @@ -2,5 +2,17 @@ "chat.newMessage": "Uusi viesti", "messagefeedback.thumbDown": "Huono vastaus", "messagefeedback.thumbUp": "Hyvä vastaus", - "responsestatus.loading": "Ladataan" + "responsestatus.loading": "Ladataan", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/fr-FR.json b/packages/@react-spectrum/ai/intl/fr-FR.json index 6c52462a01c..ba072a226c2 100644 --- a/packages/@react-spectrum/ai/intl/fr-FR.json +++ b/packages/@react-spectrum/ai/intl/fr-FR.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nouveau message", "messagefeedback.thumbDown": "Réponse incorrecte", "messagefeedback.thumbUp": "Réponse correcte", - "responsestatus.loading": "Chargement" + "responsestatus.loading": "Chargement", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Les réponses sont générées à l'aide de l'IA et peuvent être inexactes. Vérifiez avant d'utiliser.", + "promptfield.aiUserGuidlines": "Instructions d'utilisation IA", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/he-IL.json b/packages/@react-spectrum/ai/intl/he-IL.json index 00e9782d945..c276dde4345 100644 --- a/packages/@react-spectrum/ai/intl/he-IL.json +++ b/packages/@react-spectrum/ai/intl/he-IL.json @@ -2,5 +2,17 @@ "chat.newMessage": "הודעה חדשה", "messagefeedback.thumbDown": "תגובה גרועה", "messagefeedback.thumbUp": "תגובה טובה", - "responsestatus.loading": "בטעינה…" + "responsestatus.loading": "בטעינה…", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/hr-HR.json b/packages/@react-spectrum/ai/intl/hr-HR.json index a0450369a9e..b488f882281 100644 --- a/packages/@react-spectrum/ai/intl/hr-HR.json +++ b/packages/@react-spectrum/ai/intl/hr-HR.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nova poruka", "messagefeedback.thumbDown": "Odgovor nije dobar", "messagefeedback.thumbUp": "Dobar odgovor", - "responsestatus.loading": "Učitavanje" + "responsestatus.loading": "Učitavanje", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/hu-HU.json b/packages/@react-spectrum/ai/intl/hu-HU.json index 29de4b0ea03..33f68cd73a1 100644 --- a/packages/@react-spectrum/ai/intl/hu-HU.json +++ b/packages/@react-spectrum/ai/intl/hu-HU.json @@ -2,5 +2,17 @@ "chat.newMessage": "Új üzenet", "messagefeedback.thumbDown": "Rossz válasz", "messagefeedback.thumbUp": "Jó válasz", - "responsestatus.loading": "Betöltés folyamatban" + "responsestatus.loading": "Betöltés folyamatban", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/it-IT.json b/packages/@react-spectrum/ai/intl/it-IT.json index 2906d5f6c73..86dbd814cf5 100644 --- a/packages/@react-spectrum/ai/intl/it-IT.json +++ b/packages/@react-spectrum/ai/intl/it-IT.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nuovo messaggio", "messagefeedback.thumbDown": "Risposta scadente", "messagefeedback.thumbUp": "Risposta buona", - "responsestatus.loading": "Caricamento" + "responsestatus.loading": "Caricamento", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Le risposte vengono generate utilizzando l'intelligenza artificiale e potrebbero essere imprecise. Controllare prima di utilizzare.", + "promptfield.aiUserGuidlines": "Linee guida utente IA", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/ja-JP.json b/packages/@react-spectrum/ai/intl/ja-JP.json index faf42fa1395..9d3c2c8d35f 100644 --- a/packages/@react-spectrum/ai/intl/ja-JP.json +++ b/packages/@react-spectrum/ai/intl/ja-JP.json @@ -2,5 +2,17 @@ "chat.newMessage": "新規メッセージ", "messagefeedback.thumbDown": "無効な応答", "messagefeedback.thumbUp": "有効な応答", - "responsestatus.loading": "読み込み中" + "responsestatus.loading": "読み込み中", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "回答は AI を使用して生成されますが、不正確になる場合があります。使用する前に確認してください。", + "promptfield.aiUserGuidlines": "AI ユーザーガイドライン", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/ko-KR.json b/packages/@react-spectrum/ai/intl/ko-KR.json index 945e161b430..4abb72fc758 100644 --- a/packages/@react-spectrum/ai/intl/ko-KR.json +++ b/packages/@react-spectrum/ai/intl/ko-KR.json @@ -2,5 +2,17 @@ "chat.newMessage": "새 메시지", "messagefeedback.thumbDown": "잘못된 응답", "messagefeedback.thumbUp": "양호한 응답", - "responsestatus.loading": "로드 중" + "responsestatus.loading": "로드 중", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "응답은 AI를 이용하여 생성되며, 부정확할 수 있다. 사용하기 전에 확인하십시오.", + "promptfield.aiUserGuidlines": "AI 사용자 지침", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/lt-LT.json b/packages/@react-spectrum/ai/intl/lt-LT.json index f7d18bc49e6..6671d294242 100644 --- a/packages/@react-spectrum/ai/intl/lt-LT.json +++ b/packages/@react-spectrum/ai/intl/lt-LT.json @@ -2,5 +2,17 @@ "chat.newMessage": "Naujas pranešimas", "messagefeedback.thumbDown": "Blogas atsakas", "messagefeedback.thumbUp": "Geras atsakas", - "responsestatus.loading": "Įkeliama" + "responsestatus.loading": "Įkeliama", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/lv-LV.json b/packages/@react-spectrum/ai/intl/lv-LV.json index 56fff13710f..07efc451c99 100644 --- a/packages/@react-spectrum/ai/intl/lv-LV.json +++ b/packages/@react-spectrum/ai/intl/lv-LV.json @@ -2,5 +2,17 @@ "chat.newMessage": "Jauns ziņojums", "messagefeedback.thumbDown": "Slikta atbilde", "messagefeedback.thumbUp": "Laba atbilde", - "responsestatus.loading": "Notiek ielāde" + "responsestatus.loading": "Notiek ielāde", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/nb-NO.json b/packages/@react-spectrum/ai/intl/nb-NO.json index bb9bb720567..3fe4208539c 100644 --- a/packages/@react-spectrum/ai/intl/nb-NO.json +++ b/packages/@react-spectrum/ai/intl/nb-NO.json @@ -2,5 +2,17 @@ "chat.newMessage": "Ny melding", "messagefeedback.thumbDown": "Dårlig respons", "messagefeedback.thumbUp": "Bra respons", - "responsestatus.loading": "Laster inn" + "responsestatus.loading": "Laster inn", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/nl-NL.json b/packages/@react-spectrum/ai/intl/nl-NL.json index 07d750ea70e..210f9ec281b 100644 --- a/packages/@react-spectrum/ai/intl/nl-NL.json +++ b/packages/@react-spectrum/ai/intl/nl-NL.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nieuw bericht", "messagefeedback.thumbDown": "Slecht antwoord", "messagefeedback.thumbUp": "Goed antwoord", - "responsestatus.loading": "Laden" + "responsestatus.loading": "Laden", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/pl-PL.json b/packages/@react-spectrum/ai/intl/pl-PL.json index a4f9051ea83..6b4f07e8fd6 100644 --- a/packages/@react-spectrum/ai/intl/pl-PL.json +++ b/packages/@react-spectrum/ai/intl/pl-PL.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nowa wiadomość", "messagefeedback.thumbDown": "Zła odpowiedź", "messagefeedback.thumbUp": "Dobra odpowiedź", - "responsestatus.loading": "Wczytywanie" + "responsestatus.loading": "Wczytywanie", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/pt-BR.json b/packages/@react-spectrum/ai/intl/pt-BR.json index 40dac8c8c41..a509d4471d3 100644 --- a/packages/@react-spectrum/ai/intl/pt-BR.json +++ b/packages/@react-spectrum/ai/intl/pt-BR.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nova mensagem", "messagefeedback.thumbDown": "Resposta ruim", "messagefeedback.thumbUp": "Resposta boa", - "responsestatus.loading": "Carregando" + "responsestatus.loading": "Carregando", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "As respostas são geradas usando IA e podem ser imprecisas. Verifique antes de usar.", + "promptfield.aiUserGuidlines": "Diretrizes de usuário de IA", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/pt-PT.json b/packages/@react-spectrum/ai/intl/pt-PT.json index 5ba09b983dd..e0cdc2ca6b3 100644 --- a/packages/@react-spectrum/ai/intl/pt-PT.json +++ b/packages/@react-spectrum/ai/intl/pt-PT.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nova mensagem", "messagefeedback.thumbDown": "Má resposta", "messagefeedback.thumbUp": "Boa resposta", - "responsestatus.loading": "A carregar" + "responsestatus.loading": "A carregar", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/ro-RO.json b/packages/@react-spectrum/ai/intl/ro-RO.json index 34a8f804b8d..d20b60e7aad 100644 --- a/packages/@react-spectrum/ai/intl/ro-RO.json +++ b/packages/@react-spectrum/ai/intl/ro-RO.json @@ -2,5 +2,17 @@ "chat.newMessage": "Mesaj nou", "messagefeedback.thumbDown": "Răspuns greșit", "messagefeedback.thumbUp": "Răspuns bun", - "responsestatus.loading": "Se încarcă" + "responsestatus.loading": "Se încarcă", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/ru-RU.json b/packages/@react-spectrum/ai/intl/ru-RU.json index 45a155a1aa8..e256b6afe54 100644 --- a/packages/@react-spectrum/ai/intl/ru-RU.json +++ b/packages/@react-spectrum/ai/intl/ru-RU.json @@ -2,5 +2,17 @@ "chat.newMessage": "Новое сообщение", "messagefeedback.thumbDown": "Плохой ответ", "messagefeedback.thumbUp": "Хороший ответ", - "responsestatus.loading": "Загрузка" + "responsestatus.loading": "Загрузка", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/sk-SK.json b/packages/@react-spectrum/ai/intl/sk-SK.json index 65a00c5a501..5dd74367b00 100644 --- a/packages/@react-spectrum/ai/intl/sk-SK.json +++ b/packages/@react-spectrum/ai/intl/sk-SK.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nová správa", "messagefeedback.thumbDown": "Zlá odozva", "messagefeedback.thumbUp": "Dobrá odozva", - "responsestatus.loading": "Načítava sa" + "responsestatus.loading": "Načítava sa", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/sl-SI.json b/packages/@react-spectrum/ai/intl/sl-SI.json index 7deee250a34..12e7045e8c5 100644 --- a/packages/@react-spectrum/ai/intl/sl-SI.json +++ b/packages/@react-spectrum/ai/intl/sl-SI.json @@ -2,5 +2,17 @@ "chat.newMessage": "Novo sporočilo", "messagefeedback.thumbDown": "Slab odziv", "messagefeedback.thumbUp": "Dober odziv", - "responsestatus.loading": "Nalaganje" + "responsestatus.loading": "Nalaganje", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/sr-SP.json b/packages/@react-spectrum/ai/intl/sr-SP.json index b6ca2d44e09..be81838c5ce 100644 --- a/packages/@react-spectrum/ai/intl/sr-SP.json +++ b/packages/@react-spectrum/ai/intl/sr-SP.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nova poruka", "messagefeedback.thumbDown": "Loš odgovor", "messagefeedback.thumbUp": "Dobar odgovor", - "responsestatus.loading": "Učitavanje" + "responsestatus.loading": "Učitavanje", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/sv-SE.json b/packages/@react-spectrum/ai/intl/sv-SE.json index f7e3d630a3a..0387c272639 100644 --- a/packages/@react-spectrum/ai/intl/sv-SE.json +++ b/packages/@react-spectrum/ai/intl/sv-SE.json @@ -2,5 +2,17 @@ "chat.newMessage": "Nytt meddelande", "messagefeedback.thumbDown": "Dåligt svar", "messagefeedback.thumbUp": "Bra svar", - "responsestatus.loading": "Läser in" + "responsestatus.loading": "Läser in", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/tr-TR.json b/packages/@react-spectrum/ai/intl/tr-TR.json index 0cdfa0ebf0b..a15863421c6 100644 --- a/packages/@react-spectrum/ai/intl/tr-TR.json +++ b/packages/@react-spectrum/ai/intl/tr-TR.json @@ -2,5 +2,17 @@ "chat.newMessage": "Yeni mesaj", "messagefeedback.thumbDown": "Yanıt kötüydü", "messagefeedback.thumbUp": "Yanıt iyiydi", - "responsestatus.loading": "Yükleniyor" + "responsestatus.loading": "Yükleniyor", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/uk-UA.json b/packages/@react-spectrum/ai/intl/uk-UA.json index fdc04c17932..684b89586d0 100644 --- a/packages/@react-spectrum/ai/intl/uk-UA.json +++ b/packages/@react-spectrum/ai/intl/uk-UA.json @@ -2,5 +2,17 @@ "chat.newMessage": "Нове повідомлення", "messagefeedback.thumbDown": "Погана відповідь", "messagefeedback.thumbUp": "Гарна відповідь", - "responsestatus.loading": "Завантаження" + "responsestatus.loading": "Завантаження", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "Responses are generated using AI, and may be inaccurate. Check before using.", + "promptfield.aiUserGuidlines": "AI User Guidelines", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/zh-CN.json b/packages/@react-spectrum/ai/intl/zh-CN.json index a291059a58c..6391a0e6874 100644 --- a/packages/@react-spectrum/ai/intl/zh-CN.json +++ b/packages/@react-spectrum/ai/intl/zh-CN.json @@ -2,5 +2,17 @@ "chat.newMessage": "新消息", "messagefeedback.thumbDown": "错误响应", "messagefeedback.thumbUp": "良好响应", - "responsestatus.loading": "加载" + "responsestatus.loading": "加载", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "响应使用人工智能生成,可能不准确。使用前检查。", + "promptfield.aiUserGuidlines": "AI用户准则", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/intl/zh-TW.json b/packages/@react-spectrum/ai/intl/zh-TW.json index 4cead4c10b2..eac1e1c8f92 100644 --- a/packages/@react-spectrum/ai/intl/zh-TW.json +++ b/packages/@react-spectrum/ai/intl/zh-TW.json @@ -2,5 +2,17 @@ "chat.newMessage": "新訊息", "messagefeedback.thumbDown": "不良回答", "messagefeedback.thumbUp": "良好回答", - "responsestatus.loading": "載入中" + "responsestatus.loading": "載入中", + "chat.scrollToBottom": "Scroll to bottom", + "voicebutton.stopListening": "Stop recording", + "voicebutton.startListening": "Start recording", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task.", + "promptfield.aiDisclaimer": "回應是使用AI產生,可能不準確。使用前先檢查。", + "promptfield.aiUserGuidlines": "AI使用者指引", + "promptfield.attachments": "Attachments", + "promptfield.label": "Prompt", + "promptfield.stopButton": "Stop", + "promptfield.submitButton": "Send", + "promptfield.insertButton": "Add", + "promptfield.uploading": "Uploading" } diff --git a/packages/@react-spectrum/ai/package.json b/packages/@react-spectrum/ai/package.json index 4d32a79c534..fb09f74a77d 100644 --- a/packages/@react-spectrum/ai/package.json +++ b/packages/@react-spectrum/ai/package.json @@ -47,6 +47,7 @@ "dependencies": { "@internationalized/date": "^3.12.2", "@internationalized/number": "^3.6.7", + "@react-aria/utils": "^3.8.0", "@react-spectrum/s2": "1.5.1", "@react-types/shared": "^3.36.0", "react-aria": "3.50.0", diff --git a/packages/@react-spectrum/ai/src/AttachmentList.tsx b/packages/@react-spectrum/ai/src/AttachmentList.tsx index 63d2ce94249..2856c8cfe15 100644 --- a/packages/@react-spectrum/ai/src/AttachmentList.tsx +++ b/packages/@react-spectrum/ai/src/AttachmentList.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import AlertTriangle from '@react-spectrum/s2/icons/AlertTriangle'; import { AriaLabelingProps, DOMProps, @@ -25,6 +26,8 @@ import Close from '@react-spectrum/s2/icons/Close'; import {forwardRef, ReactNode, useRef} from 'react'; import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'}; import {ImageContext} from '@react-spectrum/s2/Image'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {mergeStyles} from '@react-spectrum/s2/mergeStyles'; import {pressScale} from '@react-spectrum/s2/pressScale'; import {ProgressCircle} from '@react-spectrum/s2/ProgressCircle'; @@ -38,6 +41,7 @@ import { TagProps } from 'react-aria-components/TagGroup'; import {useDOMRef} from './useDOMRef'; +import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; interface AttachmentRenderProps { /** The size of the Card. */ @@ -186,6 +190,8 @@ export interface AttachmentProps /** The children of the Attachment. */ children: ReactNode | ((renderProps: AttachmentRenderProps) => ReactNode); uploadProgress?: number; + /** Whether the attachment has an error. */ + isInvalid?: boolean; /** * Spectrum-defined styles, returned by the `style()` macro. */ @@ -200,6 +206,17 @@ const tagStyles = style({ borderRadius: 'default' }); +const attachmentErrorStyles = style({ + display: 'flex', + flexShrink: 0, + alignItems: 'center', + paddingStart: 8, + '--iconPrimary': { + type: 'color', + value: 'negative' + } +}); + export const Attachment = forwardRef(function Attachment( props: AttachmentProps, ref: DOMRef @@ -211,9 +228,13 @@ export const Attachment = forwardRef(function Attachment( 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, styles, + isInvalid, + children, + size = 'M', ...otherProps } = props; let domRef = useDOMRef(ref); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); return ( mergeStyles(tagStyles({...renderProps}), styles)}> - + {props.uploadProgress != null && props.uploadProgress < 100 && (
- +
)} {/* Reduce opacity of the thumbnail if upload is in progress */} @@ -257,12 +285,15 @@ export const Attachment = forwardRef(function Attachment( } } }}> - {typeof props.children === 'function' - ? props.children({size: otherProps.size || 'M'}) - : props.children} + {typeof children === 'function' ? children({size}) : children} )} + {isInvalid && ( + + )}
{/** Definitely not a close button, though looks like one. */}
); }); + +function AlertTriangleIcon({size}) { + switch (size) { + case 'XS': + return ; + case 'S': + return ; + case 'M': + return ; + case 'L': + return ; + case 'XL': + return ; + } +} diff --git a/packages/@react-spectrum/ai/src/Chat.tsx b/packages/@react-spectrum/ai/src/Chat.tsx index d0b11bde5be..ec6bef5761f 100644 --- a/packages/@react-spectrum/ai/src/Chat.tsx +++ b/packages/@react-spectrum/ai/src/Chat.tsx @@ -83,11 +83,13 @@ const InternalChatContext = createContext({ interface ThreadScrollButtonContextValue { isNearBottom: boolean; scrollToBottom: () => void; + 'aria-label': string; } const ThreadScrollButtonContext = createContext({ isNearBottom: true, - scrollToBottom: () => {} + scrollToBottom: () => {}, + 'aria-label': '' }); // TODO: make this more RAC like (aka default class name and other RAC prop) @@ -189,7 +191,14 @@ export const Chat = /*#__PURE__*/ (forwardRef as forwardRefType)(function Chat( (null); let isVisible = !isNearBottom; let isExiting = useExitAnimation(ref, isVisible); @@ -304,7 +313,7 @@ export function ThreadScrollButton({children}: ThreadScrollButtonProps) { return ( + value={{slots: {[DEFAULT_SLOT]: {}, scroll: {onPress: scrollToBottom, ...buttonProps}}}}> {children} @@ -334,7 +343,7 @@ const threadItemBase = style({ export interface ThreadItemProps extends Pick< GridListItemProps, - 'children' | 'textValue' | 'focusMode' | 'allowsArrowNavigation' + 'children' | 'textValue' | 'focusMode' | 'allowsArrowNavigation' | 'id' > { /** * Spectrum-defined styles, returned by the `style()` macro. diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx index a9f71686e8a..bdbd2e80f9e 100644 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ b/packages/@react-spectrum/ai/src/HorizontalCard.tsx @@ -89,6 +89,8 @@ export interface BasicCardProps extends Omit { * @default 'primary' */ variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet'; + /** Whether the card is in an error state. */ + isInvalid?: boolean; } const borderRadius = { @@ -119,7 +121,11 @@ let card = style({ default: lightDark('transparent-white-300', 'transparent-black-300'), forcedColors: 'ButtonFace' }, - boxShadow: `[inset 0 0 0 1px light-dark(${color('transparent-black-300')}, ${color('transparent-white-300')})]`, + // TODO: waiting for design to investigate thumbnail card/attachement error state + boxShadow: { + default: `[inset 0 0 0 1px light-dark(${color('transparent-black-300')}, ${color('transparent-white-300')})]`, + isInvalid: `[inset 0 0 0 1px ${color('negative-900')}]` + }, forcedColorAdjust: 'none', transition: 'default', fontFamily: 'sans', @@ -422,6 +428,7 @@ const actionButtonSize = { const Card = forwardRef(function Card( props: Omit & { isBasic?: boolean; + isInvalid?: boolean; variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet'; }, ref: DOMRef @@ -430,6 +437,7 @@ const Card = forwardRef(function Card( let domRef = useDOMRef(ref); let { isBasic = false, + isInvalid = false, density = 'regular', size = 'M', variant = 'primary', @@ -498,6 +506,7 @@ const Card = forwardRef(function Card( density, variant, isBasic, + isInvalid, isCardView: false, isLink: true }), @@ -523,11 +532,12 @@ const Card = forwardRef(function Card(
mergeStyles( card({ @@ -559,7 +570,8 @@ const Card = forwardRef(function Card( size, density, variant, - isBasic + isBasic, + isInvalid }), styles ) @@ -809,6 +821,7 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard( styles: style({ position: 'relative', alignSelf: 'center', + flexShrink: 0, pointerEvents: 'none', userSelect: 'none', size: '--basic-thumb-size', diff --git a/packages/@react-spectrum/ai/src/MessageFeedback.tsx b/packages/@react-spectrum/ai/src/MessageFeedback.tsx index 2f692b858b8..a2dde90e5ad 100644 --- a/packages/@react-spectrum/ai/src/MessageFeedback.tsx +++ b/packages/@react-spectrum/ai/src/MessageFeedback.tsx @@ -20,13 +20,14 @@ import {StylesPropWithHeight} from '@react-spectrum/s2'; import ThumbDown from '@react-spectrum/s2/icons/ThumbDown'; import ThumbUp from '@react-spectrum/s2/icons/ThumbUp'; import {ToggleButton} from '@react-spectrum/s2/ToggleButton'; -import {ToggleButtonGroup} from '@react-spectrum/s2/ToggleButtonGroup'; +import {ToggleButtonGroup, ToggleButtonGroupProps} from '@react-spectrum/s2/ToggleButtonGroup'; import {useDOMRef} from './useDOMRef'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; export type MessageFeedbackValue = 'up' | 'down' | null; -export interface MessageFeedbackProps extends DOMProps, AriaLabelingProps, SlotProps { +export interface MessageFeedbackProps + extends DOMProps, AriaLabelingProps, SlotProps, Pick { /** The selected feedback value (controlled). */ value?: MessageFeedbackValue; /** The default feedback value (uncontrolled). */ @@ -57,7 +58,16 @@ export const MessageFeedback = forwardRef(function MessageFeedback( ) { let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); let domRef = useDOMRef(ref); - let {value, defaultValue, onChange, isDisabled, thumbUpLabel, thumbDownLabel, styles} = props; + let { + value, + defaultValue, + onChange, + isDisabled, + thumbUpLabel, + thumbDownLabel, + styles, + size = 'M' + } = props; let handleSelectionChange = (selection: Selection): void => { onChange?.(selectionToValue(selection)); @@ -76,6 +86,7 @@ export const MessageFeedback = forwardRef(function MessageFeedback( void; + attachments?: PromptFieldAttachment[]; + defaultAttachments?: PromptFieldAttachment[]; + onAttachmentsChange?: (attachments: PromptFieldAttachment[]) => void; onSubmit?: (prompt: TokenFieldValue, attachments: PromptFieldAttachment[]) => void; isGenerating?: boolean; onStop?: () => void; @@ -96,34 +111,40 @@ interface PromptFieldState { isGenerating: boolean; onAddAttachments?: (attachments: PromptFieldAttachment[]) => void; onRemoveAttachments?: (attachments: PromptFieldAttachment[]) => void; + isListening: boolean; + setListening: React.Dispatch>; } // TODO: make this customizable const tokenRegex = /(?<=\s|^)(https?:\/\/)?(www\.)?([^/\s]+\.[a-z]{2,}(\/\S+)?)(?=\s)/g; -class AutoLinkingTokenFieldValue extends TokenFieldValue { - tokenize(text: string): TokenFieldSegment[] { - if (text.length === 0) { - return [{type: 'text', text}]; - } +function tokenizeURLs(text: string): TokenFieldSegment[] { + if (text.length === 0) { + return [{type: 'text', text}]; + } - tokenRegex.lastIndex = 0; + tokenRegex.lastIndex = 0; - let match: RegExpExecArray | null = null; - let start = 0; - let segments: TokenFieldSegment[] = []; - while ((match = tokenRegex.exec(text))) { - if (match.index > start) { - segments.push({type: 'text', text: text.slice(start, match.index)}); - } - segments.push({type: 'token', text: match[3], value: {type: 'url', url: match[0]}}); - start = match.index + match[0].length; + let match: RegExpExecArray | null = null; + let start = 0; + let segments: TokenFieldSegment[] = []; + while ((match = tokenRegex.exec(text))) { + if (match.index > start) { + segments.push({type: 'text', text: text.slice(start, match.index)}); } + segments.push({type: 'token', text: match[3], value: {type: 'url', url: match[0]}}); + start = match.index + match[0].length; + } - if (start < text.length) { - segments.push({type: 'text', text: text.slice(start)}); - } + if (start < text.length) { + segments.push({type: 'text', text: text.slice(start)}); + } - return segments; + return segments; +} + +export class AutoLinkingTokenFieldValue extends TokenFieldValue { + tokenize(text: string): TokenFieldSegment[] { + return tokenizeURLs(text); } } @@ -133,9 +154,16 @@ const PromptFieldContext = createContext({ prompt: new AutoLinkingTokenFieldValue([]), setPrompt: () => {}, inputRef: createRef(), - isGenerating: false + isGenerating: false, + isListening: false, + setListening: () => {} }); +// to communicate the anchor position to the menu items in the completion popover +// need this so we can replace the inline filter text rather than inserting it at the current caret +// aka the difference between a slash command and using the + menu which won't have filter text +const PromptCompletionAnchorContext = createContext(null); + function matchMimeType(mimeType: string, acceptedMimeTypes: string[]): boolean { return acceptedMimeTypes.some(type => { if (type === '*/*') { @@ -159,8 +187,17 @@ export function PromptField(props: PromptFieldProps) { onRemoveAttachments, variant = 'balanced' } = props; - let [prompt, setPrompt] = useState(new AutoLinkingTokenFieldValue([])); - let [attachments, setAttachments] = useState([]); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); + let [prompt, setPrompt] = useControlledState( + props.value, + props.defaultValue ?? new AutoLinkingTokenFieldValue([]), + props.onChange + ); + let [attachments, setAttachments] = useControlledState( + props.attachments, + props.defaultAttachments ?? [], + props.onAttachmentsChange + ); // Not using RAC DropZone because it adds its own focusable button, // and we want to avoid an extra tab. We support pasting files directly into the input. @@ -188,17 +225,24 @@ export function PromptField(props: PromptFieldProps) { } }); + let [isListening, setListening] = useState(false); let {onFocusChange} = useContext(PromptFocusContext); let {focusWithinProps} = useFocusWithin({onFocusWithinChange: onFocusChange}); + let isPromptControlled = props.value !== undefined; + let isAttachmentsControlled = props.attachments !== undefined; let onSubmit = () => { if (prompt.segments.length === 0) { return; } props.onSubmit?.(prompt, attachments); - setPrompt(new AutoLinkingTokenFieldValue([])); - setAttachments([]); + if (!isPromptControlled) { + setPrompt(new AutoLinkingTokenFieldValue([])); + } + if (!isAttachmentsControlled) { + setAttachments([]); + } inputRef.current?.focus(); }; @@ -213,6 +257,8 @@ export function PromptField(props: PromptFieldProps) { inputRef, onSubmit, isGenerating: isGenerating ?? false, + isListening, + setListening, onStop, onAddAttachments, onRemoveAttachments @@ -229,12 +275,12 @@ export function PromptField(props: PromptFieldProps) { {children}

- Responses are generated using AI, and may be inaccurate. Check before using.{' '} + {stringFormatter.format('promptfield.aiDisclaimer')}{' '} - AI User Guidelines + {stringFormatter.format('promptfield.aiUserGuidlines')}

@@ -249,6 +295,7 @@ export interface PromptFieldAttachmentListProps extends AttachmentListProps { let removedAttachments = attachments.filter(attachment => keys.has(attachment.id)); onRemoveAttachments?.(removedAttachments); @@ -284,10 +331,19 @@ export interface PromptTokenFieldProps { children?: (segment: TokenSegment) => React.ReactElement; pixelLoader?: Cell[] | Cell[][]; placeholder?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; } export function PromptTokenField(props: PromptTokenFieldProps) { - let {completionTrigger, renderCompletions, children, pixelLoader, placeholder} = props; + let { + completionTrigger, + renderCompletions, + children, + pixelLoader, + placeholder, + onKeyDown: onKeyDownProp + } = props; + let {keyboardProps} = useKeyboard({onKeyDown: onKeyDownProp}); let { prompt, setPrompt, @@ -296,7 +352,8 @@ export function PromptTokenField(props: PromptTokenFieldProps) { onAddAttachments, inputRef, onSubmit, - isGenerating + isGenerating, + isListening } = useContext(PromptFieldContext); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); let [isFocused, setFocused] = useState(false); @@ -345,6 +402,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) { allowsNewlines className={style({flexGrow: 1})} aria-label={stringFormatter.format('promptfield.label')} + isReadOnly={isListening} onSubmit={onSubmit} onFocus={e => { if (e.isTrusted) { @@ -379,6 +437,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) { : undefined }> @@ -418,7 +477,7 @@ export interface PromptTokenFieldPopoverProps extends PopoverProps { function PromptTokenFieldPopover(props: PromptTokenFieldPopoverProps) { let {filterAnchor, items, isFocused} = props; - let {inputRef, setPrompt} = useContext(PromptFieldContext); + let {inputRef} = useContext(PromptFieldContext); let resolvedItems = items instanceof Promise ? use(items) : items; let isOpen = @@ -430,24 +489,6 @@ function PromptTokenFieldPopover(props: PromptTokenFieldPopoverProps) { setMenuItems(resolvedItems); } - let onAction = (item: any) => { - setPrompt(value => - value.replaceRangeWithSegments( - filterAnchor!, - value.caretPosition, - [ - { - type: 'token', - text: 'command' in item ? item.command : item.title, - value: item - }, - {type: 'text', text: ' '} - ], - false // Don't coalesce in undo/redo history. - ) - ); - }; - return ( { return positionToDOMRange(target, filterAnchor!).getBoundingClientRect(); }}> - onAction(value)}>{menuItems} + + {menuItems} + ); } @@ -536,28 +579,137 @@ export interface PromptFieldSubmitButtonProps {} // eslint-disable-next-line @typescript-eslint/no-unused-vars export function PromptFieldSubmitButton(props: PromptFieldSubmitButtonProps) { let {prompt, isGenerating, onSubmit, onStop} = useContext(PromptFieldContext); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); return ( ); } +export interface PromptFieldVoiceButtonProps { + lang?: string; + isDisabled?: boolean; + onError?: (code: VoiceInputErrorCode) => void; +} + +export function PromptFieldVoiceButton(props: PromptFieldVoiceButtonProps) { + let {lang, isDisabled: isDisabledProp, onError} = props; + let {prompt, setPrompt, inputRef, setListening} = useContext(PromptFieldContext); + let isDisabled = isDisabledProp; + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); + + let basePromptRef = useRef(prompt); + let updateBasePrompt = useEffectEvent(() => { + basePromptRef.current = prompt; + }); + + let { + isSupported, + isListening: isVoiceListening, + transcript, + toggle, + stop + } = useVoiceInput({lang, onError, onListeningChange: setListening}); + + let restoreFocus = useEffectEvent(() => { + if (!inputRef.current) { + return; + } + // similar to useInsertPromptSegment, calling programatic focus on the input causes the caret positioning + // to be inaccurate + let finalPrompt = buildVoicePrompt(basePromptRef.current, transcript); + inputRef.current.focus(); + setSelection(inputRef.current, finalPrompt.caretPosition, finalPrompt.caretPosition); + setPrompt(finalPrompt); + }); + + let wasListeningRef = useRef(false); + useEffect(() => { + if (isVoiceListening) { + updateBasePrompt(); + wasListeningRef.current = true; + } else if (wasListeningRef.current) { + wasListeningRef.current = false; + restoreFocus(); + } + }, [isVoiceListening]); + + let applyVoiceTranscript = useEffectEvent(() => { + if (!transcript || !isVoiceListening) { + return; + } + + setPrompt(buildVoicePrompt(basePromptRef.current, transcript)); + }); + useEffect(() => { + applyVoiceTranscript(); + }, [transcript, isVoiceListening]); + + useEffect(() => { + if (isDisabled && isVoiceListening) { + stop(); + } + }, [isDisabled, isVoiceListening, stop]); + + if (!isSupported) { + return null; + } + + let label = isVoiceListening + ? stringFormatter.format('voicebutton.stopListening') + : stringFormatter.format('voicebutton.startListening'); + + return ( + + + + + {label} + + ); +} + +function buildVoicePrompt(base: TokenFieldValue, voiceText: string): AutoLinkingTokenFieldValue { + if (!voiceText) { + return base as AutoLinkingTokenFieldValue; + } + return base.replaceRange( + base.caretPosition, + base.caretPosition, + voiceText + ) as AutoLinkingTokenFieldValue; +} + export interface InsertMenuItemProps { children: React.ReactNode; } export function InsertMenuButton(props: InsertMenuItemProps) { let {children} = props; + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); return ( - + {children} @@ -600,30 +752,73 @@ export function AttachFileMenuItem() { ); } -export function InsertTokenMenuItem(props: MenuItemProps) { +// either replace the filter text (aka token replace) or insert value at current caret position (aka plain text inject) +function useInsertPromptSegment(buildSegments: (item: any) => TokenFieldSegment[]) { let {setPrompt, inputRef} = useContext(PromptFieldContext); - let onAction = (item: any) => { - setPrompt(value => - value.replaceRangeWithSegments( + let anchor = useContext(PromptCompletionAnchorContext); + let pendingCaret = useRef(null); + return (item: any) => { + setPrompt(value => { + let newValue = value.replaceRangeWithSegments( + anchor ?? value.caretPosition, value.caretPosition, - value.caretPosition, - [ - { - type: 'token', - text: 'command' in item ? item.command : item.title, - value: item - }, - {type: 'text', text: ' '} - ], + buildSegments(item), false // Don't coalesce in undo/redo history. - ) - ); - - // Wait for popover animation - setTimeout(() => { - inputRef.current?.focus(); - }, 400); + ); + pendingCaret.current = newValue.caretPosition; + return newValue; + }); + + if (anchor == null) { + // Wait for popover animation, then restore cursor to after the inserted content. + setTimeout(() => { + if (inputRef.current && pendingCaret.current) { + let position = pendingCaret.current; + pendingCaret.current = null; + inputRef.current.focus(); + // we need to update the position manually since TokenField's update caret logic only happens if the field is focused + // but this insert can happen from the + menu aka the field isn't focused until this gets called which is too late + setSelection(inputRef.current, position, position); + // the above focus and setCursor call can cause the internally tracked caret position to be reset incorrectly + // seemingly due to TokenField's isProgrammaticSelectionChange being flipped to false by setCursor and thus reset to 0 by the .focus + // fix this by resetting to proper position below + // happens when injecting multiple tokens one after another via + menu + setPrompt(value => value.withCaretPosition(position)); + } + }, 400); + } }; +} + +export function InsertTokenMenuItem(props: MenuItemProps) { + let insert = useInsertPromptSegment(item => [ + {type: 'token', text: 'command' in item ? item.command : item.title, value: item}, + {type: 'text', text: ' '} + ]); + + return insert(props.value)} />; +} - return onAction(props.value)} />; +export function InsertTextMenuItem(props: MenuItemProps) { + let insert = useInsertPromptSegment(item => [ + {type: 'text', text: `${'command' in item ? item.command : item.title} `} + ]); + + return insert(props.value)} />; +} + +// specifically for menu items that only trigger a callback in the autocomplete menu +// since they dont end up inserting a token or text, we need to clear the partial text that the user used +// to filter the menu +export function CommandMenuItem(props: MenuItemProps) { + let insert = useInsertPromptSegment(() => []); + return ( + { + insert(undefined); + props.onAction?.(); + }} + /> + ); } diff --git a/packages/@react-spectrum/ai/src/ResponseStatus.tsx b/packages/@react-spectrum/ai/src/ResponseStatus.tsx index 9e0a66cc424..7d854c7ae3a 100644 --- a/packages/@react-spectrum/ai/src/ResponseStatus.tsx +++ b/packages/@react-spectrum/ai/src/ResponseStatus.tsx @@ -23,6 +23,7 @@ import {Button} from 'react-aria-components/Button'; import {CenterBaseline} from '@react-spectrum/s2/CenterBaseline'; import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle'; import ChevronRight from '@react-spectrum/s2/icons/ChevronRight'; +import CloseCircle from '@react-spectrum/s2/icons/CloseCircle'; import { DisclosureStateContext, Disclosure as RACDisclosure, @@ -38,11 +39,20 @@ import intlMessages from '../intl/*.json'; import {mergeStyles} from '@react-spectrum/s2/mergeStyles'; import {ProgressCircle} from '@react-spectrum/s2/ProgressCircle'; import {Provider} from 'react-aria-components/slots'; -import React, {createContext, forwardRef, ReactNode, useContext} from 'react'; +import React, { + createContext, + forwardRef, + ReactNode, + useCallback, + useContext, + useState +} from 'react'; import {StyleString} from '@react-spectrum/s2/style' with {type: 'macro'}; import {useDOMRef} from './useDOMRef'; +import {useLayoutEffect} from '@react-aria/utils'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; + export interface ResponseStatusProps extends Omit< RACDisclosureProps, 'className' | 'style' | 'render' | 'children' | keyof GlobalDOMAttributes @@ -60,10 +70,11 @@ export interface ResponseStatusProps extends Omit< */ density?: 'compact' | 'regular' | 'spacious'; /** - * Whether the response is still being generated. When true, a ProgressCircle replaces - * the chevron and the panel cannot be expanded. The trigger remains focusable. + * The current status of the response. + * + * @default 'loading' */ - isLoading?: boolean; + status?: 'loading' | 'failed' | 'success'; /** * The contents of the response status, consisting of a ResponseStatusTitle and * ResponseStatusPanel. @@ -78,8 +89,14 @@ export interface ResponseStatusProps extends Omit< const ResponseStatusContext = createContext<{ size?: 'S' | 'M' | 'L' | 'XL'; density?: 'compact' | 'regular' | 'spacious'; - isLoading?: boolean; -}>({}); + status: 'loading' | 'failed' | 'success'; + hasPanelContent: boolean; + registerPanel: (mounted: boolean) => void; +}>({ + status: 'loading', + hasPanelContent: false, + registerPanel: () => {} +}); const responseStatus = style({ color: 'heading', @@ -87,24 +104,28 @@ const responseStatus = style({ }); /** - * A ResponseStatus indicates the progress of a system response while it is begin generated and when - * it is complete. + * A ResponseStatus indicates the progress of a system response while it is being generated and when + * it is complete. If a ResponseStatusPanel is provided, the title can be pressed to expand and + * collapse it. */ export const ResponseStatus = forwardRef(function ResponseStatus( props: ResponseStatusProps, ref: DOMRef ) { - let {size = 'M', density = 'regular', isLoading, styles} = props; + let {size = 'M', density = 'regular', status = 'loading', styles} = props; let domRef = useDOMRef(ref); + let [hasPanelContent, setHasPanelContent] = useState(false); + let registerPanel = useCallback((mounted: boolean) => setHasPanelContent(mounted), []); let disclosureProps: Partial = {}; - if (isLoading) { + if (status === 'loading') { disclosureProps.isExpanded = false; disclosureProps.onExpandedChange = () => {}; } return ( - + - ); @@ -352,16 +389,22 @@ const panelInner = style({ /** * A response status panel is a collapsible section of content that is hidden until the - * response status is expanded. The panel cannot be expanded while `isLoading` is true. + * response status is expanded. The panel cannot be expanded while `status` is `'loading'`. */ export const ResponseStatusPanel = forwardRef(function ResponseStatusPanel( props: ResponseStatusPanelProps, ref: DOMRef ) { let {styles} = props; - let {size = 'M'} = useContext(ResponseStatusContext)!; + let {size = 'M', registerPanel} = useContext(ResponseStatusContext)!; const domProps = filterDOMProps(props); let panelRef = useDOMRef(ref); + + useLayoutEffect(() => { + registerPanel(true); + return () => registerPanel(false); + }, [registerPanel]); + return (
{props.children}
diff --git a/packages/@react-spectrum/ai/src/speech-recognition.d.ts b/packages/@react-spectrum/ai/src/speech-recognition.d.ts new file mode 100644 index 00000000000..9f4c79cc463 --- /dev/null +++ b/packages/@react-spectrum/ai/src/speech-recognition.d.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// Ambient declarations for parts of the Web Speech API missing from TypeScript's +// lib.dom.d.ts: the recognizer constructor, its vendor-prefixed alias, event +// types, and the User-Agent Client Hints surface used for the Chromium browser +// allowlist. SpeechRecognitionAlternative, SpeechRecognitionResult, and +// SpeechRecognitionResultList ARE provided by lib.dom and are not redeclared. +// Spec: https://wicg.github.io/speech-api/ + +type SpeechRecognitionErrorCode = + | 'no-speech' + | 'aborted' + | 'audio-capture' + | 'network' + | 'not-allowed' + | 'service-not-allowed' + | 'language-not-supported' + | 'phrases-not-supported'; + +interface SpeechRecognitionEvent extends Event { + readonly resultIndex: number; + readonly results: SpeechRecognitionResultList; +} + +interface SpeechRecognitionErrorEvent extends Event { + readonly error: SpeechRecognitionErrorCode; + readonly message: string; +} + +interface SpeechRecognition extends EventTarget { + lang: string; + continuous: boolean; + interimResults: boolean; + maxAlternatives: number; + start(): void; + stop(): void; + abort(): void; + onaudiostart: ((this: SpeechRecognition, ev: Event) => void) | null; + onsoundstart: ((this: SpeechRecognition, ev: Event) => void) | null; + onspeechstart: ((this: SpeechRecognition, ev: Event) => void) | null; + onspeechend: ((this: SpeechRecognition, ev: Event) => void) | null; + onsoundend: ((this: SpeechRecognition, ev: Event) => void) | null; + onaudioend: ((this: SpeechRecognition, ev: Event) => void) | null; + onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null; + onnomatch: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null; + onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null; + onstart: ((this: SpeechRecognition, ev: Event) => void) | null; + onend: ((this: SpeechRecognition, ev: Event) => void) | null; +} + +interface SpeechRecognitionConstructor { + new (): SpeechRecognition; +} + +interface Window { + SpeechRecognition?: SpeechRecognitionConstructor; + webkitSpeechRecognition?: SpeechRecognitionConstructor; +} + +// User-Agent Client Hints — present in Chromium, absent in Safari/Firefox. +interface NavigatorUABrandVersion { + readonly brand: string; + readonly version: string; +} + +interface NavigatorUAData { + readonly brands: ReadonlyArray; + readonly platform?: string; +} + +interface Navigator { + readonly userAgentData?: NavigatorUAData; +} diff --git a/packages/@react-spectrum/ai/src/useVoiceInput.ts b/packages/@react-spectrum/ai/src/useVoiceInput.ts new file mode 100644 index 00000000000..3030bcaa8c7 --- /dev/null +++ b/packages/@react-spectrum/ai/src/useVoiceInput.ts @@ -0,0 +1,312 @@ +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { + Dispatch, + RefObject, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import {useEffectEvent} from 'react-aria/private/utils/useEffectEvent'; + +/** + * Chromium brands whose Web Speech backend is known to work. Other Chromium + * forks (Arc, Brave, Edge) expose the API but route to a backend that does not + * return results, so they are excluded. Safari has no `userAgentData` and is + * trusted directly. Add a brand here to enable another Chromium fork. + */ +const VOICE_INPUT_CHROMIUM_BROWSERS = new Set(['Google Chrome']); + +/** Normalised voice-recognition error. `null` is never surfaced (e.g. aborts). */ +export type VoiceInputErrorCode = + | 'not-allowed' + | 'network' + | 'no-speech' + | 'audio-capture' + | 'language-not-supported' + | 'phrases-not-supported' + | 'unknown'; + +// doesn't have onStart/onInterim/etc, didn't seem like they were needed since coworker just used +// it to contruct the transcript while still allowing the field be editable by the user and it was quite buggy +export interface VoiceInputProps { + lang?: string; + onError?: (code: VoiceInputErrorCode) => void; + onListeningChange?: (isListening: boolean) => void; +} + +// TODO: make a accompanying aria hook later, this is more stately like +export interface VoiceInputResult { + isSupported: boolean; + isListening: boolean; + errorCode: VoiceInputErrorCode | null; + // TODO: this returns the full voice transcript rather than offering a interim and final one like cooworker's does + // can alway offer that later + transcript: string; + start: () => void; + stop: () => void; + toggle: () => void; +} + +/** Resolves the recognizer constructor, applying the Chromium brand allowlist. */ +const getRecognitionCtor = (): SpeechRecognitionConstructor | null => { + if (typeof window === 'undefined') { + return null; + } + const ctor = window.SpeechRecognition ?? window.webkitSpeechRecognition ?? null; + if (!ctor) { + return null; + } + const brands = navigator.userAgentData?.brands; + if (brands && !brands.some(b => VOICE_INPUT_CHROMIUM_BROWSERS.has(b.brand))) { + return null; + } + return ctor; +}; + +/** SpeechRecognition requires a secure context (HTTPS / localhost). */ +const isSecureVoiceContext = (): boolean => + typeof window !== 'undefined' && window.isSecureContext === true; + +/** Maps a raw recognition error to a normalised code, or `null` to ignore it. */ +const mapErrorCode = (error: string): VoiceInputErrorCode | null => { + switch (error) { + case 'aborted': + return null; + case 'not-allowed': + case 'service-not-allowed': + return 'not-allowed'; + case 'network': + return 'network'; + case 'no-speech': + return 'no-speech'; + case 'audio-capture': + return 'audio-capture'; + case 'language-not-supported': + return 'language-not-supported'; + case 'phrases-not-supported': + return 'phrases-not-supported'; + default: + return 'unknown'; + } +}; + +/** Maps a getUserMedia rejection to a normalised error code. */ +const mapMediaError = (err: unknown): VoiceInputErrorCode => { + const name = err instanceof DOMException ? err.name : ''; + if (name === 'NotAllowedError' || name === 'SecurityError') { + return 'not-allowed'; + } + if (name === 'NotFoundError' || name === 'NotReadableError') { + return 'audio-capture'; + } + return 'unknown'; +}; + +/** + * Requests microphone access so the browser permission prompt reliably opens and + * a denial is detected before recognition starts. Returns `null` on success (or + * when getUserMedia is unavailable, deferring to the recognizer's own prompt), + * or an error code on failure. The granted stream is released immediately — + * SpeechRecognition opens its own. + */ +const requestMicrophone = async (): Promise => { + const media = typeof navigator !== 'undefined' ? navigator.mediaDevices : undefined; + if (!media?.getUserMedia) { + return null; + } + try { + const stream = await media.getUserMedia({audio: true}); + stream.getTracks().forEach(track => track.stop()); + return null; + } catch (err) { + return mapMediaError(err); + } +}; + +/** Accumulates interim vs final transcripts from a recognition result event. */ +const readTranscript = (event: SpeechRecognitionEvent): {interim: string; final: string} => { + let interim = ''; + let final = ''; + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i]; + const alternative = result?.[0]; + if (result && alternative) { + if (result.isFinal) { + final += alternative.transcript; + } else { + interim += alternative.transcript; + } + } + } + return {interim, final}; +}; + +interface RecognizerSetup { + recognitionRef: RefObject; + setListening: Dispatch>; + onListeningChange: (isListening: boolean) => void; + onError: (code: VoiceInputErrorCode) => void; + setError: Dispatch>; + setInterim: Dispatch>; + appendFinal: (s: string) => void; +} + +/** + * Creates and wires a SpeechRecognition instance. On error/end the ref is cleared, + * but only when this instance is still the active one, so a late event from a + * previous session can't wipe out a newly-started recognizer. + */ +const createRecognizer = ( + ctor: SpeechRecognitionConstructor, + lang: string, + setup: RecognizerSetup +): SpeechRecognition => { + const recognition = new ctor(); + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = lang; + + // Ignore events from a session that is no longer the active recognizer (e.g. a + // late onend/onerror after a new session started), so they can't flip the + // listening state, surface a st ale error, or clear a newer recognizer. + const isActive = () => setup.recognitionRef.current === recognition; + + recognition.onstart = () => { + setup.setListening(true); + setup.onListeningChange(true); + }; + recognition.onresult = event => { + if (!isActive()) { + return; + } + const {interim, final} = readTranscript(event); + if (final) { + setup.appendFinal(final); + } + setup.setInterim(interim); + }; + recognition.onerror = event => { + if (!isActive()) { + return; + } + const code = mapErrorCode(event.error); + if (code) { + setup.setError(code); + setup.onError(code); + } + setup.setListening(false); + setup.onListeningChange(false); + setup.setInterim(''); + // Clear so the next press starts a fresh session instead of calling stop(). + setup.recognitionRef.current = null; + }; + recognition.onend = () => { + if (!isActive()) { + return; + } + setup.setListening(false); + setup.onListeningChange(false); + setup.setInterim(''); + setup.recognitionRef.current = null; + }; + + return recognition; +}; + +export const useVoiceInput = (options: VoiceInputProps): VoiceInputResult => { + const {lang, onError, onListeningChange} = options; + + // Resolve support once — it cannot change for the lifetime of the page. + const ctor = useMemo(() => (isSecureVoiceContext() ? getRecognitionCtor() : null), []); + const isSupported = ctor !== null; + + const [isListening, setIsListening] = useState(false); + const [errorCode, setErrorCode] = useState(null); + const [interimTranscript, setInterimTranscript] = useState(''); + const [finalTranscript, setFinalTranscript] = useState(''); + const transcript = finalTranscript + interimTranscript; + + const recognitionRef = useRef(null); + const startingRef = useRef(false); + const handleError = useEffectEvent((code: VoiceInputErrorCode) => { + onError?.(code); + }); + const handleListeningChange = useEffectEvent((v: boolean) => { + onListeningChange?.(v); + }); + + const stop = useCallback(() => { + recognitionRef.current?.stop(); + }, []); + + const start = useCallback(() => { + // Guard against double-start (a started recognizer throws InvalidStateError) + // and against re-entry while the permission prompt is open. + if (!ctor || recognitionRef.current || startingRef.current) { + return; + } + startingRef.current = true; + setErrorCode(null); + setInterimTranscript(''); + setFinalTranscript(''); + + // Request mic access first so the browser permission prompt reliably opens + // and a denial is reported before recognition starts. + void requestMicrophone().then(micError => { + startingRef.current = false; + if (micError) { + setErrorCode(micError); + handleError(micError); + return; + } + // Bail if already started while the permission prompt was open. + if (recognitionRef.current) { + return; + } + + const recognition = createRecognizer(ctor, lang ?? navigator.language ?? 'en-US', { + recognitionRef, + setListening: setIsListening, + onListeningChange: handleListeningChange, + onError: handleError, + setError: setErrorCode, + setInterim: setInterimTranscript, + appendFinal: s => setFinalTranscript(prev => prev + s) + }); + recognitionRef.current = recognition; + recognition.start(); + }); + // oxlint-disable-next-line react-hooks/exhaustive-deps + }, [ctor, lang, handleError, handleListeningChange]); + + const toggle = useCallback(() => { + if (recognitionRef.current) { + stop(); + } else { + start(); + } + }, [start, stop]); + + // Abort on unmount so dangling callbacks never fire after teardown. + useEffect(() => { + return () => { + recognitionRef.current?.abort(); + }; + }, []); + + return {isSupported, isListening, errorCode, transcript, start, stop, toggle}; +}; diff --git a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx index f66030ce120..cae286fa369 100644 --- a/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx +++ b/packages/@react-spectrum/ai/stories/AttachmentList.stories.tsx @@ -28,9 +28,15 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { ...categorizeArgTypes('Events', events), - children: {table: {disable: true}} + children: {table: {disable: true}}, + isInvalid: {control: 'boolean'}, + uploadProgress: {control: 'number', min: 0, max: 100}, + size: { + control: 'radio', + options: ['XS', 'S', 'M', 'L', 'XL'] + } }, - args: {...getActionArgs(events)}, + args: {isInvalid: false, size: 'M', ...getActionArgs(events)}, title: 'AI/AttachmentList' }; @@ -38,28 +44,45 @@ export default meta; type Story = StoryObj; -export const AIAttachmentList: Story = { - render: args => ( - - +function AttachmentListRender(args) { + let {isInvalid, size, uploadProgress, ...listArgs} = args; + return ( + + - + - + - + - ) + ); +} + +export const AIAttachmentList: Story = { + render: args => }; diff --git a/packages/@react-spectrum/ai/stories/Chat.stories.tsx b/packages/@react-spectrum/ai/stories/Chat.stories.tsx index ad4d84e2084..d3bc9040b06 100644 --- a/packages/@react-spectrum/ai/stories/Chat.stories.tsx +++ b/packages/@react-spectrum/ai/stories/Chat.stories.tsx @@ -10,19 +10,12 @@ * governing permissions and limitations under the License. */ +import {action} from 'storybook/actions'; import {ActionButton} from '@react-spectrum/s2/ActionButton'; import {ActionMenu} from '@react-spectrum/s2/ActionMenu'; import {AssetCard, CardPreview} from '@react-spectrum/s2/Card'; -import {Chat} from '../src/Chat'; -import ChatIcon from '@react-spectrum/s2/icons/Chat'; -import ChevronDown from '@react-spectrum/s2/icons/ChevronDown'; -import {Content} from '@react-spectrum/s2/Content'; -import {DialogTrigger, Popover} from '@react-spectrum/s2/Popover'; -import {GridList} from 'react-aria-components'; -import {Image} from '@react-spectrum/s2/Image'; -import {ListLayout} from 'react-stately/useVirtualizerState'; -import {MenuItem} from '@react-spectrum/s2/Menu'; import { + AutoLinkingTokenFieldValue, MessageFeedback, MessageSource, MessageSuggestion, @@ -41,9 +34,18 @@ import { TokenFieldValue, UserMessage } from '@react-spectrum/ai'; -import type {Meta} from '@storybook/react'; +import {Chat} from '../src/Chat'; +import ChatIcon from '@react-spectrum/s2/icons/Chat'; +import ChevronDown from '@react-spectrum/s2/icons/ChevronDown'; +import {Content} from '@react-spectrum/s2/Content'; +import {DialogTrigger, Popover} from '@react-spectrum/s2/Popover'; +import {GridList} from 'react-aria-components'; +import {Image} from '@react-spectrum/s2/Image'; +import {ListLayout} from 'react-stately/useVirtualizerState'; +import {MenuItem} from '@react-spectrum/s2/Menu'; +import type {Meta, StoryObj} from '@storybook/react'; import {prose} from '../src/style/prose' with {type: 'macro'}; -import {ReactNode, useRef, useState} from 'react'; +import {ReactNode, useEffect, useRef, useState} from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Text} from '@react-spectrum/s2/Text'; import {Virtualizer} from 'react-aria-components/Virtualizer'; @@ -65,6 +67,7 @@ const meta: Meta = { }; export default meta; +type Story = StoryObj; let dummyResponses = [ "Sure! Here's a summary of the key points based on the assets you shared. The main themes revolve around brand consistency, audience engagement, and clear calls to action across all touchpoints.", @@ -212,13 +215,15 @@ function CardMessage({ ); } -export function StreamingChat() { +function StreamingChatRender() { let [messages, setMessages] = useState( initialResponses as StreamingMessage[] ); let nextId = useRef(initialResponses.length); let [isGenerating, setGenerating] = useState(false); let timeouts = useRef([]); + let [promptValue, setPromptValue] = useState(new AutoLinkingTokenFieldValue([])); + let followUpMessage = useRef(null); function handleSend(prompt: TokenFieldValue) { setGenerating(true); @@ -415,6 +420,29 @@ export function StreamingChat() { }, streamEndTimestamp + 1000); } + useEffect(() => { + if (!isGenerating && followUpMessage.current) { + let followup = followUpMessage.current; + followUpMessage.current = null; + handleSend(followup); + } + }, [isGenerating]); + + // TODO: maybe also have it finalize any in progress tool calls and what not, but do it later + function handleStop() { + followUpMessage.current = null; + timeouts.current.forEach(clearTimeout); + timeouts.current = []; + setMessages(prev => + prev.map(m => + (m.type === 'system' || m.type === 'status') && m.isStreaming + ? {...m, isStreaming: false} + : m + ) + ); + setGenerating(false); + } + return ( // TODO: these extra div wrappers would need to be implemented by the RAC user, maybe we can internalize some more? // of particular note is the scroll button. Same for the other styles @@ -498,7 +526,7 @@ export function StreamingChat() { textValue={announcement} isStreaming={msg.isStreaming} shouldAnnounceOnMount> - + {title} {msg.details && ( @@ -546,15 +574,47 @@ export function StreamingChat() {
{ + setPromptValue(new AutoLinkingTokenFieldValue([])); + handleSend(prompt); + }} isGenerating={isGenerating} - onStop={() => { - setGenerating(false); - timeouts.current.forEach(clearTimeout); - timeouts.current = []; - }}> + onStop={handleStop}>
- + { + if (!isGenerating) { + return; + } + + // TODO: we could make this even more realistic but for now just fire storybook event + // and add follow up message to queue + if (e.key === 'Enter' && !e.altKey) { + e.preventDefault(); + if (promptValue.segments.length > 0) { + action('onSteer')(promptValue.toString()); + setPromptValue(new AutoLinkingTokenFieldValue([])); + } + } else if (e.key === 'Enter' && e.altKey) { + e.preventDefault(); + if (promptValue.segments.length > 0) { + action('onFollowUp')(promptValue.toString()); + followUpMessage.current = promptValue; + setPromptValue(new AutoLinkingTokenFieldValue([])); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + handleStop(); + } + }} + />
@@ -563,6 +623,10 @@ export function StreamingChat() { ); } +export const StreamingChat: Story = { + render: () => +}; + // Ignore this story, just here for local testing export function VirtualizedChat() { let [messages, setMessages] = useState(initialResponses); @@ -627,7 +691,7 @@ export function VirtualizedChat() { return ( - + {message} diff --git a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx index f5f7ea02b66..466f13f9e15 100644 --- a/packages/@react-spectrum/ai/stories/PromptField.stories.tsx +++ b/packages/@react-spectrum/ai/stories/PromptField.stories.tsx @@ -10,14 +10,20 @@ * governing permissions and limitations under the License. */ +import {action} from 'storybook/actions'; import { AttachFileMenuItem, + AutoLinkingTokenFieldValue, + CommandMenuItem, InsertMenuButton, + InsertTextMenuItem, InsertTokenMenuItem, PromptField, + PromptFieldAttachment, PromptFieldAttachmentList, PromptFieldSubmitButton, PromptFieldToolbar, + PromptFieldVoiceButton, PromptToken, PromptTokenField } from '../src/PromptField'; @@ -40,12 +46,14 @@ import * as data from '../src/loader/data'; import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Image} from '@react-spectrum/s2/Image'; import LinkIcon from '@react-spectrum/s2/icons/Link'; -import type {Meta} from '@storybook/react'; +import {MessageSuggestion, MessageSuggestionList} from '../src/MessageSuggestion'; +import type {Meta, StoryObj} from '@storybook/react'; import Plugin from '@react-spectrum/s2/icons/Plugin'; import Prompt from '@react-spectrum/s2/icons/Prompt'; import SocialNetwork from '@react-spectrum/s2/icons/SocialNetwork'; +import {TokenFieldValue} from 'react-aria-components'; +import {useRef, useState} from 'react'; import UserGroup from '@react-spectrum/s2/icons/UserGroup'; -import {useState} from 'react'; const events = ['onSubmit', 'onStop', 'onAddAttachments', 'onRemoveAttachments']; @@ -73,12 +81,22 @@ const meta: Meta = { attachmentVariant: { control: 'radio', options: ['thumbnail', 'card'] + }, + attachmentInvalid: { + control: 'boolean', + description: 'Sets attachments to an invalid state.' + }, + placeholder: { + control: 'text', + table: {category: 'PromptTokenField'} } }, args: { brand: 'rgb(236, 105, 255)', pixelLoader: 'aiLogo', attachmentVariant: 'thumbnail', + attachmentInvalid: false, + placeholder: undefined, ...getActionArgs(events) }, title: 'AI/PromptField', @@ -99,6 +117,7 @@ const meta: Meta = { }; export default meta; +type Story = StoryObj; const slashCommands = [ { @@ -106,9 +125,11 @@ const slashCommands = [ type: 'skill', description: 'Explain an AEP audience in english' }, + {command: '/btw', type: 'command', description: 'Ask a side question'}, {command: '/clear', type: 'command', description: 'Clear the context'}, {command: '/compact', type: 'command', description: 'Summarize conversation history'}, {command: '/dataset-usage', type: 'skill', description: 'Explain how to use a dataset'}, + {command: '/feedback', type: 'command', description: 'Submit feedback'}, {command: '/plan', type: 'command', description: 'Create a plan before executing'}, {command: '/visual-artifact', type: 'skill', description: 'Generate a chart or graph'} ]; @@ -155,26 +176,52 @@ const objects = [ } ]; -function renderCompletions(filterValue: string) { +interface CompletionCallbacks { + onClear?: () => void; + onCompact?: () => void; +} + +function renderCompletions(filterValue: string, callbacks?: CompletionCallbacks) { if (filterValue.startsWith('/')) { return slashCommands .filter(item => item.command.includes(filterValue.slice(1))) - .map(item => ( - - {item.type === 'skill' ? : } - {item.command} - {item.description} - - )); + .map(item => + item.command === '/clear' ? ( + + + {item.command} + {item.description} + + ) : item.command === '/compact' ? ( + + + {item.command} + {item.description} + + ) : item.command === '/feedback' || item.command === '/btw' ? ( + // coworker doesn't seem to have any text insertion commands anymore, so I added these for testing + + + {item.command} + {item.description} + + ) : ( + + {item.type === 'skill' ? : } + {item.command} + {item.description} + + ) + ); } else if (filterValue.startsWith('@')) { return objects .map(section => { let matchingItems = section.items .filter(item => item.title.toLowerCase().includes(filterValue.slice(1).toLowerCase())) .map(item => ( - + {item.title} - + )); if (matchingItems.length > 0) { @@ -200,8 +247,40 @@ interface UploadState { progress?: number; } -export const Everything = args => { +let prompt3Base = new AutoLinkingTokenFieldValue([ + {type: 'text', text: 'Summarize the '}, + {type: 'token', text: 'Welcome Flow', value: {type: 'journey', title: 'Welcome Flow'}} +]); +let prompt3End = { + index: 1, + offset: prompt3Base.segments[1].text.length +}; + +let prompts = [ + new AutoLinkingTokenFieldValue([ + {type: 'text', text: 'Analyze '}, + {type: 'token', text: 'New Customers', value: {type: 'audience', title: 'New Customers'}}, + {type: 'text', text: ' and suggest targeting strategies'} + ]), + new AutoLinkingTokenFieldValue([ + {type: 'text', text: 'Write a brief for '}, + { + type: 'token', + text: 'Spring Launch 2026', + value: {type: 'campaign', title: 'Spring Launch 2026'} + } + ]), + prompt3Base.replaceRange(prompt3End, prompt3End, ' journey performance from test.com ') +]; + +function EverythingRender(args) { + let {placeholder, ...otherArgs} = args; + let [value, setValue] = useState(() => new AutoLinkingTokenFieldValue([])); + let [attachments, setAttachments] = useState([]); let [attachmentState, setAttachmentState] = useState>(new Map()); + let historyRef = useRef([]); + let historyIndexRef = useRef(-1); + let isHistoryNavigating = useRef(false); let mockUpload = async (id: string) => { await new Promise(resolve => setTimeout(resolve, Math.random() * 30)); @@ -222,124 +301,242 @@ export const Everything = args => { }); }; + let isFieldEmpty = (prompt: TokenFieldValue) => { + let text = prompt.toString(); + return text === '' && !text.includes('\n'); + }; + + // logic for using up/down arrow keys to fill field with previous prompts + let onKeyDown = (e: React.KeyboardEvent) => { + let canNavigate = historyIndexRef.current !== -1 || isFieldEmpty(value); + let history = historyRef.current; + if (!canNavigate || history.length === 0) { + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + let nextIndex = + historyIndexRef.current === -1 + ? history.length - 1 + : Math.max(0, historyIndexRef.current - 1); + historyIndexRef.current = nextIndex; + isHistoryNavigating.current = true; + setValue(history[nextIndex]); + } else if (e.key === 'ArrowDown') { + if (historyIndexRef.current === -1) { + return; + } + e.preventDefault(); + let nextIndex = historyIndexRef.current + 1; + if (nextIndex >= history.length) { + historyIndexRef.current = -1; + isHistoryNavigating.current = true; + setValue(new AutoLinkingTokenFieldValue([])); + } else { + historyIndexRef.current = nextIndex; + isHistoryNavigating.current = true; + setValue(history[nextIndex]); + } + } + }; + + let handleChange = (newValue: TokenFieldValue) => { + if (!isHistoryNavigating.current) { + // if user edits the field, then we want to reset the index so up arrow starts from latest prompt again + historyIndexRef.current = -1; + } + isHistoryNavigating.current = false; + setValue(newValue); + }; + return ( - { - setAttachmentState(prev => { - let newState = new Map(prev); - attachments.forEach(attachment => { - newState.set(attachment.id, {status: 'uploading', progress: 0}); - mockUpload(attachment.id); - }); - return newState; - }); - }} - onRemoveAttachments={attachments => { - setAttachmentState(prev => { - let newState = new Map(prev); - attachments.forEach(attachment => { - newState.delete(attachment.id); +
+ + {prompts.map((prompt, i) => ( + setValue(prompt)}> + {prompt.toString()} + + ))} + + { + action('onSubmit')(prompt.toString()); + historyRef.current = [...historyRef.current, prompt]; + historyIndexRef.current = -1; + setValue(new AutoLinkingTokenFieldValue([])); + setAttachments([]); + setAttachmentState(new Map()); + }} + acceptedAttachmentTypes={['image/*']} + onAddAttachments={newAttachments => { + setAttachmentState(prev => { + let newState = new Map(prev); + newAttachments.forEach(attachment => { + newState.set(attachment.id, {status: 'uploading', progress: 0}); + mockUpload(attachment.id); + }); + return newState; }); - return newState; - }); - }}> - - {attachment => { - let state = attachmentState.get(attachment.id); - return ( - - {/* TODO: what about non-image attachments? */} - {attachment.image && } - {args.attachmentVariant === 'card' && ( - - {attachment.file.name} - {attachment.file.type} - - )} - - ); }} - - - {segment => ( - - {icons[segment.value?.type]} - {segment.text} - - )} - - - - - - - - Commands - - item.type === 'command')}> - {item => ( - - {item.command} - {item.description} - - )} - - - - - - Skills - - item.type === 'skill')}> - {item => ( - - {item.command} - {item.description} - - )} - - - - - - Reference an object - - - {item => ( - -
- {item.section} -
- - {item => ( - {item.title} - )} - -
- )} -
-
-
+ onRemoveAttachments={removedAttachments => { + setAttachmentState(prev => { + let newState = new Map(prev); + removedAttachments.forEach(attachment => { + newState.delete(attachment.id); + }); + return newState; + }); + }}> + + {attachment => { + let state = attachmentState.get(attachment.id); + return ( + + {/* TODO: what about non-image attachments? */} + {attachment.image && } + {args.attachmentVariant === 'card' && ( + + {attachment.file.name} + {attachment.file.type} + + )} + + ); + }} + + + renderCompletions(filterValue, { + onClear: () => { + setValue(new AutoLinkingTokenFieldValue([])); + setAttachments([]); + }, + onCompact: action('onCompact') + }) + } + pixelLoader={data[args.pixelLoader]} + placeholder={placeholder} + onKeyDown={onKeyDown}> + {segment => ( + + {icons[segment.value?.type]} + {segment.text} + + )} + + + + + + + + Commands + + item.type === 'command')}> + {item => + item.command === '/clear' ? ( + { + setValue(new AutoLinkingTokenFieldValue([])); + setAttachments([]); + }}> + {item.command} + {item.description} + + ) : item.command === '/compact' ? ( + + {item.command} + {item.description} + + ) : item.command === '/feedback' || item.command === '/btw' ? ( + + {item.command} + {item.description} + + ) : ( + + {item.command} + {item.description} + + ) + } + + + + + + Skills + + item.type === 'skill')}> + {item => ( + + {item.command} + {item.description} + + )} + + + + + + Reference an object + + + {item => ( + +
+ {item.section} +
+ + {item => ( + {item.title} + )} + +
+ )} +
+
+
+ {/* TODO is this kind of styling expected from the user? Or should we have a slot that places the mic button next to the submit button? */} +
+ + +
+
+
+
+ ); +} + +export const Everything: Story = { + render: args => +}; + +function BasicRender({placeholder, ...args}: any) { + return ( + +
+ - +
); -}; +} -export const Basic = () => ( - -
- - -
-
-); +export const Basic: Story = { + render: args => +}; export const AsyncCompletions = () => ( diff --git a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx index 3b68f172946..18b4f2b8544 100644 --- a/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx +++ b/packages/@react-spectrum/ai/stories/ResponseStatus.stories.tsx @@ -34,13 +34,14 @@ const meta: Meta = { control: 'radio', options: ['compact', 'regular', 'spacious'] }, - isLoading: { - control: {type: 'boolean'} + status: { + control: 'radio', + options: ['loading', 'failed', 'success'] }, children: {table: {disable: true}} }, args: { - isLoading: true, + status: 'loading', ...getActionArgs(events) }, title: 'AI/ResponseStatus' @@ -54,7 +55,11 @@ export const Example: Story = {
- {args.isLoading ? 'Generating response' : 'Response generated'} + {args.status === 'loading' + ? 'Generating response' + : args.status === 'success' + ? 'Response generated' + : 'Response failed'} Here is the generated response content. This area is hidden until the disclosure is @@ -64,3 +69,19 @@ export const Example: Story = {
) }; + +export const NoResponseContent: Story = { + render: args => ( +
+ + + {args.status === 'loading' + ? 'Generating response' + : args.status === 'success' + ? 'Response generated' + : 'Response failed'} + + +
+ ) +}; diff --git a/packages/dev/s2-docs/pages/react-aria/Tree.mdx b/packages/dev/s2-docs/pages/react-aria/Tree.mdx index 7024eb44420..590a4ce1c26 100644 --- a/packages/dev/s2-docs/pages/react-aria/Tree.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Tree.mdx @@ -341,26 +341,26 @@ import {TextField} from 'vanilla-starter/TextField'; aria-label="Shared files"> - + - + - + - + - + diff --git a/packages/dev/s2-docs/pages/s2/TreeView.mdx b/packages/dev/s2-docs/pages/s2/TreeView.mdx index eb93e63c91a..42f4344300e 100644 --- a/packages/dev/s2-docs/pages/s2/TreeView.mdx +++ b/packages/dev/s2-docs/pages/s2/TreeView.mdx @@ -410,20 +410,20 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - + - + - + @@ -431,13 +431,13 @@ import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - + - + diff --git a/packages/react-aria/exports/useTokenField.ts b/packages/react-aria/exports/useTokenField.ts index 15b0091b6d2..b57c1c8c7b8 100644 --- a/packages/react-aria/exports/useTokenField.ts +++ b/packages/react-aria/exports/useTokenField.ts @@ -10,7 +10,7 @@ * governing */ -export {useTokenField, positionToDOMRange} from '../src/tokenfield/useTokenField'; +export {useTokenField, positionToDOMRange, setSelection} from '../src/tokenfield/useTokenField'; export {useToken} from '../src/tokenfield/useToken'; export type {TokenFieldAria, AriaTokenFieldProps} from '../src/tokenfield/useTokenField'; export type {TokenAria, TokenProps} from '../src/tokenfield/useToken'; diff --git a/packages/react-aria/src/selection/useSelectableCollection.ts b/packages/react-aria/src/selection/useSelectableCollection.ts index a97b8ddf02a..6cb2e4c7772 100644 --- a/packages/react-aria/src/selection/useSelectableCollection.ts +++ b/packages/react-aria/src/selection/useSelectableCollection.ts @@ -475,7 +475,7 @@ export function useSelectableCollection( }; // we need the "virtual" modality case checks here because shift tabbing from the prompt field's attachment card back into the - // thread is a virtual focus event (the tab handler in onKeyDown focuses the ref of the AttachementList aka TagGroup via a focus() call, hence the virtual modality) + // thread is a virtual focus event (the tab handler in onKeyDown focuses the ref of the attachmentList aka TagGroup via a focus() call, hence the virtual modality) if (UNSTABLE_focusOnEntry && (modality === 'keyboard' || modality === 'virtual')) { // always go to the first item in the Thread when tabbing forwards/backwards into the collection // since it is probably more important to the user to see the new prompt reply rather than go to the last focused key diff --git a/packages/react-aria/src/tokenfield/useTokenField.ts b/packages/react-aria/src/tokenfield/useTokenField.ts index 54113fe6f9b..9359385b7a8 100644 --- a/packages/react-aria/src/tokenfield/useTokenField.ts +++ b/packages/react-aria/src/tokenfield/useTokenField.ts @@ -56,6 +56,10 @@ export interface AriaTokenFieldProps void; + /** Handler that is called when a key is pressed. */ + onKeyDown?: (e: React.KeyboardEvent) => void; + /** Handler that is called when a key is released. */ + onKeyUp?: (e: React.KeyboardEvent) => void; /** * Handler that is called when the user copies text. See * [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/oncopy). @@ -533,6 +537,8 @@ export function useTokenField( } }; + // TODO: user provided onKeyDown currently relies on user provided preventDefault to stop submit + // maybe can have them specify a format like shortcuts and merge into above? let {keyboardProps} = useKeyboard({ isDisabled: isDisabled || isReadOnly, onKeyDown: props.onKeyDown, diff --git a/yarn.lock b/yarn.lock index 67c78467c9a..9e0d3af73db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7377,6 +7377,7 @@ __metadata: "@internationalized/date": "npm:^3.12.2" "@internationalized/number": "npm:^3.6.7" "@react-aria/test-utils": "npm:^1.0.0-alpha.8" + "@react-aria/utils": "npm:^3.8.0" "@react-spectrum/s2": "npm:1.5.1" "@react-types/shared": "npm:^3.36.0" "@storybook/jest": "npm:^0.2.3"