@webority/mobile-chat 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,11 +120,16 @@ package's, so a product passes one label set to both; `composerHint` is not show
120
120
  `title` (default "Assistant"), `headerActions` (before New chat, e.g. "All chats"), `empty`
121
121
  (shown before the first question), `disclaimer` (null hides it), `maxLength` (default 4000),
122
122
  `allowanceThreshold` (show the questions left at this many or fewer, default 10), `rating`
123
- (default true), `resolveLink`, `onLinkPress`, `confirmAction`, `onViewTicket`, `renderReply`.
123
+ (default true), `resolveLink`, `onLinkPress`, `confirmAction`, `onViewTicket`, `renderReply`,
124
+ `keyboardOffset`.
124
125
  New chat is disabled until there is something to clear; the question box is disabled with no
125
126
  questions left, and the thread follows the newest text while an answer streams.
126
- - **`ChatThread`**, **`ChatMessage`**, **`ChatComposer`**: its parts, for an app that lays out its
127
- own chat. The composer's return key adds a new line; only the send button sends.
127
+ - **`ChatThread`**, **`ChatMessage`**, **`ChatComposer`**: its parts, built on mobile-ui's
128
+ `MessageList`, `MessageBubble`, `TypingIndicator` and `Composer`, for an app that lays out its
129
+ own chat. The thread and the composer handle the keyboard themselves; pass
130
+ `keyboardAware={false}` to both inside a container that already does, and `keyboardOffset` (the
131
+ tab bar height) above a tab bar. The composer's return key adds a new line; only the send button
132
+ sends.
128
133
  - **`ChatMarkdown`**: renders a reply from a small, safe subset of Markdown: paragraphs, bold,
129
134
  italics, inline code, bullet and numbered lists, simple tables (scrolling sideways), headings,
130
135
  code blocks and rules. A link shows only when its href has the form `scheme:kind/id` and
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatComposer=void 0;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_labels=require("../labels.js"),_jsxRuntime=require("react/jsx-runtime");const ChatComposer=({onSubmit:e,busy:t=!1,disabled:i=!1,maxLength:s=4e3,labels:r,testID:a="chat-composer"})=>{const l=(0,_labels.mergeLabels)(r),o=(0,_mobileUi.useTheme)(),[n,c]=(0,_react.useState)(""),m=n.trim(),u=t||i||""===m;return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{flexDirection:"row",alignItems:"flex-end",gap:o.spacing.sm},testID:a,children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:{flex:1},children:(0,_jsxRuntime.jsx)(_mobileUi.Input,{accessibilityLabel:l.composer,placeholder:l.placeholder,value:n,onChangeText:c,multiline:!0,maxLength:s,disabled:i,testID:`${a}-input`})}),(0,_jsxRuntime.jsx)(_mobileUi.IconButton,{icon:"send",accessibilityLabel:l.send,variant:"solid",tone:"primary",disabled:u,loading:t,onPress:()=>{u||(c(""),e(m))},testID:`${a}-send`})]})};exports.ChatComposer=ChatComposer;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatComposer=void 0;var _mobileUi=require("@webority/mobile-ui"),_labels=require("../labels.js"),_jsxRuntime=require("react/jsx-runtime");const ChatComposer=({onSubmit:e,busy:s=!1,disabled:o=!1,maxLength:r=4e3,above:a,below:t,keyboardAware:b,keyboardOffset:l,safeArea:i,labels:m,testID:d="chat-composer"})=>{const n=(0,_labels.mergeLabels)(m);return(0,_jsxRuntime.jsx)(_mobileUi.Composer,{onSend:e,busy:s,disabled:o,maxLength:r,placeholder:n.placeholder,accessibilityLabel:n.composer,sendLabel:n.send,above:a,below:t,keyboardAware:b,keyboardOffset:l,safeArea:i,testID:d})};exports.ChatComposer=ChatComposer;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatMessage=ChatMessage;var _mobileUi=require("@webority/mobile-ui"),_reactNative=require("react-native"),_labels=require("../labels.js"),_ActionCard=require("./ActionCard.js"),_ChatMarkdown=require("./ChatMarkdown.js"),_ReplyRating=require("./ReplyRating.js"),_jsxRuntime=require("react/jsx-runtime");function ChatMessage({turn:e,onRate:i,resolveLink:s,onLinkPress:t,confirmAction:a,onViewTicket:n,renderReply:r,labels:l,testID:o="chat-message"}){const c=(0,_labels.mergeLabels)(l),m=(0,_mobileUi.useTheme)(),u=e.streaming&&!e.answer;return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{gap:m.spacing.sm},testID:o,children:[""!==e.question&&(0,_jsxRuntime.jsx)(_reactNative.View,{accessible:!0,accessibilityLabel:`${c.you}: ${e.question}`,style:{alignSelf:"flex-end",maxWidth:"85%",backgroundColor:m.colors.primaryMuted,borderRadius:m.radius.lg,paddingHorizontal:m.spacing.md,paddingVertical:m.spacing.sm},children:(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"body",children:e.question})}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{gap:m.spacing.sm},children:[(0,_jsxRuntime.jsx)(_reactNative.View,{accessibilityLabel:u?c.thinking:void 0,accessibilityLiveRegion:"polite",accessibilityState:{busy:e.streaming},"aria-busy":e.streaming,children:u?(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{flexDirection:"row",alignItems:"center",gap:m.spacing.sm},children:[(0,_jsxRuntime.jsx)(_mobileUi.Spinner,{size:"sm",accessibilityLabel:c.thinking}),(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"bodySmall",color:"secondary",children:c.thinking})]}):""!==e.answer?r?r(e,{streaming:e.streaming}):(0,_jsxRuntime.jsx)(_ChatMarkdown.ChatMarkdown,{text:e.answer,resolveLink:s,onLinkPress:t}):null}),e.error&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"bodySmall",color:"error",accessibilityLiveRegion:"polite",children:e.error.message}),a&&!e.streaming&&e.actions.map(e=>(0,_jsxRuntime.jsx)(_ActionCard.ActionCard,{action:e,confirmAction:a,onViewTicket:n,labels:l},e.token)),i&&e.messageId&&!e.streaming&&(0,_jsxRuntime.jsx)(_ReplyRating.ReplyRating,{rating:e.rating,onRate:(s,t)=>i(e.id,s,t),labels:l})]})]})}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatMessage=ChatMessage;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_labels=require("../labels.js"),_markdownParse=require("../markdownParse.js"),_ActionCard=require("./ActionCard.js"),_ChatMarkdown=require("./ChatMarkdown.js"),_ReplyRating=require("./ReplyRating.js"),_jsxRuntime=require("react/jsx-runtime");const inlineText=e=>e.map(e=>"text"===e.type||"code"===e.type?e.text:inlineText(e.children)).join(""),spokenText=e=>(0,_markdownParse.parseBlocks)(e).map(e=>{switch(e.type){case"paragraph":return inlineText((0,_markdownParse.parseInline)(e.lines.join(" ")));case"heading":return inlineText((0,_markdownParse.parseInline)(e.text));case"list":return e.items.map(e=>inlineText((0,_markdownParse.parseInline)(e))).join(". ");case"table":return[e.header,...e.rows].map(e=>e.join(", ")).join(". ");case"code":return e.text;default:return""}}).filter(Boolean).join("\n");function ChatMessage({turn:e,onRate:t,resolveLink:i,onLinkPress:n,confirmAction:r,onViewTicket:s,renderReply:a,labels:o,testID:l="chat-message"}){const m=(0,_labels.mergeLabels)(o),u=(0,_mobileUi.useTheme)(),c=e.streaming&&!e.answer,x=(0,_react.useMemo)(()=>e.streaming?e.answer:spokenText(e.answer),[e.streaming,e.answer]),_=(0,_react.useMemo)(()=>({you:m.you}),[m.you]);return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{gap:u.spacing.sm},testID:l,children:[""!==e.question&&(0,_jsxRuntime.jsx)(_mobileUi.MessageBubble,{side:"mine",text:e.question,labels:_,testID:`${l}-question`}),c?(0,_jsxRuntime.jsx)(_mobileUi.TypingIndicator,{label:m.thinking,testID:`${l}-thinking`}):null,""!==e.answer?(0,_jsxRuntime.jsx)(_mobileUi.MessageBubble,{side:"theirs",author:m.assistant,text:x,streaming:e.streaming,testID:`${l}-answer`,children:a?a(e,{streaming:e.streaming}):(0,_jsxRuntime.jsx)(_ChatMarkdown.ChatMarkdown,{text:e.answer,resolveLink:i,onLinkPress:n})}):null,e.error&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"bodySmall",color:"error",accessibilityLiveRegion:"polite",children:e.error.message}),r&&!e.streaming&&e.actions.map(e=>(0,_jsxRuntime.jsx)(_ActionCard.ActionCard,{action:e,confirmAction:r,onViewTicket:s,labels:o},e.token)),t&&e.messageId&&!e.streaming&&(0,_jsxRuntime.jsx)(_ReplyRating.ReplyRating,{rating:e.rating,onRate:(i,n)=>t(e.id,i,n),labels:o})]})}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatScreen=ChatScreen;var _mobileUi=require("@webority/mobile-ui"),_reactNative=require("react-native"),_reactNativeKeyboardController=require("react-native-keyboard-controller"),_reactNativeSafeAreaContext=require("react-native-safe-area-context"),_labels=require("../labels.js"),_ChatComposer=require("./ChatComposer.js"),_ChatThread=require("./ChatThread.js"),_jsxRuntime=require("react/jsx-runtime");function ChatScreen({chat:e,title:t,headerActions:r,empty:i,disclaimer:a,maxLength:s=4e3,allowanceThreshold:o=10,rating:n=!0,testID:l="chat-screen",...d}){const c=(0,_labels.mergeLabels)(d.labels),m=(0,_mobileUi.useTheme)(),x=(0,_reactNativeSafeAreaContext.useSafeAreaInsets)(),u=e.messagesLeftToday,b=void 0===a?c.disclaimer:a;return(0,_jsxRuntime.jsxs)(_reactNativeKeyboardController.KeyboardAvoidingView,{behavior:"padding",style:{flex:1,backgroundColor:m.colors.background.primary},testID:l,children:[(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{flexDirection:"row",alignItems:"center",gap:m.spacing.sm,paddingHorizontal:m.spacing.md,paddingVertical:m.spacing.sm,borderBottomWidth:1,borderBottomColor:m.colors.border.secondary},children:[(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"title",accessibilityRole:"header",style:{flex:1},numberOfLines:1,children:t??c.panelTitle}),r,(0,_jsxRuntime.jsx)(_mobileUi.Button,{title:c.newChat,size:"sm",variant:"soft",leftIcon:"add",disabled:0===e.turns.length&&!e.loading,onPress:e.newConversation,testID:`${l}-new`})]}),e.loading?(0,_jsxRuntime.jsx)(_reactNative.View,{style:{flex:1,alignItems:"center",justifyContent:"center"},children:(0,_jsxRuntime.jsx)(_mobileUi.Spinner,{message:c.loadingConversation,testID:`${l}-loading`})}):(0,_jsxRuntime.jsx)(_ChatThread.ChatThread,{turns:e.turns,style:{flex:1},onRate:n?e.rate:void 0,empty:i??(0,_jsxRuntime.jsx)(_mobileUi.EmptyState,{fill:!0,size:"sm",title:c.emptyTitle,description:c.emptyText,testID:`${l}-empty`}),testID:`${l}-thread`,...d}),(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{gap:m.spacing.xs,paddingHorizontal:m.spacing.md,paddingTop:m.spacing.sm,paddingBottom:m.spacing.sm+x.bottom,borderTopWidth:1,borderTopColor:m.colors.border.secondary},children:[e.loadError&&(0,_jsxRuntime.jsx)(_mobileUi.Banner,{variant:"error",message:e.loadError.message,testID:`${l}-load-error`}),null!==u&&u<=o&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:0===u?"error":"secondary",accessibilityLiveRegion:"polite",testID:`${l}-allowance`,children:c.allowance(u)}),(0,_jsxRuntime.jsx)(_ChatComposer.ChatComposer,{onSubmit:t=>{e.submit(t)},busy:e.streaming,disabled:0===u||e.loading,maxLength:s,labels:d.labels,testID:`${l}-composer`}),b?(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:"tertiary",testID:`${l}-disclaimer`,children:b}):null]})]})}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatScreen=ChatScreen;var _mobileUi=require("@webority/mobile-ui"),_reactNative=require("react-native"),_labels=require("../labels.js"),_ChatComposer=require("./ChatComposer.js"),_ChatThread=require("./ChatThread.js"),_jsxRuntime=require("react/jsx-runtime");function ChatScreen({chat:e,title:t,headerActions:i,empty:s,disclaimer:r,maxLength:a=4e3,allowanceThreshold:n=10,rating:o=!0,keyboardOffset:l,testID:m="chat-screen",...c}){const d=(0,_labels.mergeLabels)(c.labels),u=(0,_mobileUi.useTheme)(),x=e.messagesLeftToday,b=void 0===r?d.disclaimer:r;return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{flex:1,backgroundColor:u.colors.background.primary},testID:m,children:[(0,_jsxRuntime.jsxs)(_reactNative.View,{style:{flexDirection:"row",alignItems:"center",gap:u.spacing.sm,paddingHorizontal:u.spacing.md,paddingVertical:u.spacing.sm,borderBottomWidth:1,borderBottomColor:u.colors.border.secondary},children:[(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"title",accessibilityRole:"header",style:{flex:1},numberOfLines:1,children:t??d.panelTitle}),i,(0,_jsxRuntime.jsx)(_mobileUi.Button,{title:d.newChat,size:"sm",variant:"soft",leftIcon:"add",disabled:0===e.turns.length&&!e.loading,onPress:e.newConversation,testID:`${m}-new`})]}),e.loading?(0,_jsxRuntime.jsx)(_reactNative.View,{style:{flex:1,alignItems:"center",justifyContent:"center"},children:(0,_jsxRuntime.jsx)(_mobileUi.Spinner,{message:d.loadingConversation,testID:`${m}-loading`})}):(0,_jsxRuntime.jsx)(_ChatThread.ChatThread,{turns:e.turns,style:{flex:1},keyboardOffset:l,onRate:o?e.rate:void 0,empty:s??(0,_jsxRuntime.jsx)(_mobileUi.EmptyState,{fill:!0,size:"sm",title:d.emptyTitle,description:d.emptyText,testID:`${m}-empty`}),testID:`${m}-thread`,...c}),(0,_jsxRuntime.jsx)(_ChatComposer.ChatComposer,{onSubmit:t=>{e.submit(t)},busy:e.streaming,disabled:0===x||e.loading,maxLength:a,keyboardOffset:l,labels:c.labels,testID:`${m}-composer`,above:(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[e.loadError&&(0,_jsxRuntime.jsx)(_mobileUi.Banner,{variant:"error",message:e.loadError.message,testID:`${m}-load-error`}),null!==x&&x<=n&&(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:0===x?"error":"secondary",accessibilityLiveRegion:"polite",testID:`${m}-allowance`,children:d.allowance(x)})]}),below:b?(0,_jsxRuntime.jsx)(_mobileUi.Text,{variant:"caption",color:"tertiary",testID:`${m}-disclaimer`,children:b}):null})]})}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatThread=ChatThread;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_labels=require("../labels.js"),_ChatMessage=require("./ChatMessage.js"),_jsxRuntime=require("react/jsx-runtime");function ChatThread({turns:e,empty:t,style:a,testID:r="chat-thread",...s}){const i=(0,_mobileUi.useTheme)(),l=(0,_labels.mergeLabels)(s.labels),n=(0,_react.useRef)(null);return(0,_jsxRuntime.jsx)(_reactNative.ScrollView,{ref:n,style:a,accessibilityLabel:l.thread,keyboardShouldPersistTaps:"handled",contentContainerStyle:{padding:i.spacing.md,gap:i.spacing.lg,flexGrow:1},onContentSizeChange:()=>n.current?.scrollToEnd({animated:!0}),testID:r,children:0===e.length?t:e.map(e=>(0,_jsxRuntime.jsx)(_ChatMessage.ChatMessage,{turn:e,testID:`${r}-${e.id}`,...s},e.id))})}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ChatThread=ChatThread;var _mobileUi=require("@webority/mobile-ui"),_react=require("react"),_reactNative=require("react-native"),_labels=require("../labels.js"),_ChatMessage=require("./ChatMessage.js"),_jsxRuntime=require("react/jsx-runtime");const turnKey=e=>e.id,followTurn=()=>!0,TurnGap=()=>{const e=(0,_mobileUi.useTheme)();return(0,_jsxRuntime.jsx)(_reactNative.View,{style:{height:e.spacing.lg}})};function ChatThread({turns:e,empty:t,keyboardAware:r,keyboardOffset:s,style:a,testID:i="chat-thread",...o}){const u=(0,_labels.mergeLabels)(o.labels),l=(0,_react.useMemo)(()=>[...e].reverse(),[e]);return(0,_jsxRuntime.jsx)(_mobileUi.MessageList,{messages:l,keyExtractor:turnKey,renderMessage:({item:e})=>(0,_jsxRuntime.jsx)(_ChatMessage.ChatMessage,{turn:e,testID:`${i}-${e.id}`,...o}),followNew:followTurn,empty:t,ItemSeparatorComponent:TurnGap,keyboardAware:r,keyboardOffset:s,accessibilityLabel:u.thread,style:a,testID:i})}
@@ -1 +1 @@
1
- "use strict";import{IconButton,Input,useTheme}from"@webority/mobile-ui";import{useState}from"react";import{View}from"react-native";import{mergeLabels}from"../labels.js";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export const ChatComposer=({onSubmit:e,busy:s=!1,disabled:t=!1,maxLength:i=4e3,labels:o,testID:r="chat-composer"})=>{const a=mergeLabels(o),n=useTheme(),[l,m]=useState(""),c=l.trim(),p=s||t||""===c;return _jsxs(View,{style:{flexDirection:"row",alignItems:"flex-end",gap:n.spacing.sm},testID:r,children:[_jsx(View,{style:{flex:1},children:_jsx(Input,{accessibilityLabel:a.composer,placeholder:a.placeholder,value:l,onChangeText:m,multiline:!0,maxLength:i,disabled:t,testID:`${r}-input`})}),_jsx(IconButton,{icon:"send",accessibilityLabel:a.send,variant:"solid",tone:"primary",disabled:p,loading:s,onPress:()=>{p||(m(""),e(c))},testID:`${r}-send`})]})};
1
+ "use strict";import{Composer}from"@webority/mobile-ui";import{mergeLabels}from"../labels.js";import{jsx as _jsx}from"react/jsx-runtime";export const ChatComposer=({onSubmit:e,busy:o=!1,disabled:s=!1,maxLength:r=4e3,above:a,below:t,keyboardAware:b,keyboardOffset:l,safeArea:m,labels:i,testID:d="chat-composer"})=>{const c=mergeLabels(i);return _jsx(Composer,{onSend:e,busy:o,disabled:s,maxLength:r,placeholder:c.placeholder,accessibilityLabel:c.composer,sendLabel:c.send,above:a,below:t,keyboardAware:b,keyboardOffset:l,safeArea:m,testID:d})};
@@ -1 +1 @@
1
- "use strict";import{Spinner,Text,useTheme}from"@webority/mobile-ui";import{View}from"react-native";import{mergeLabels}from"../labels.js";import{ActionCard}from"./ActionCard.js";import{ChatMarkdown}from"./ChatMarkdown.js";import{ReplyRating}from"./ReplyRating.js";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export function ChatMessage({turn:e,onRate:i,resolveLink:s,onLinkPress:r,confirmAction:n,onViewTicket:t,renderReply:a,labels:o,testID:l="chat-message"}){const c=mergeLabels(o),m=useTheme(),g=e.streaming&&!e.answer;return _jsxs(View,{style:{gap:m.spacing.sm},testID:l,children:[""!==e.question&&_jsx(View,{accessible:!0,accessibilityLabel:`${c.you}: ${e.question}`,style:{alignSelf:"flex-end",maxWidth:"85%",backgroundColor:m.colors.primaryMuted,borderRadius:m.radius.lg,paddingHorizontal:m.spacing.md,paddingVertical:m.spacing.sm},children:_jsx(Text,{variant:"body",children:e.question})}),_jsxs(View,{style:{gap:m.spacing.sm},children:[_jsx(View,{accessibilityLabel:g?c.thinking:void 0,accessibilityLiveRegion:"polite",accessibilityState:{busy:e.streaming},"aria-busy":e.streaming,children:g?_jsxs(View,{style:{flexDirection:"row",alignItems:"center",gap:m.spacing.sm},children:[_jsx(Spinner,{size:"sm",accessibilityLabel:c.thinking}),_jsx(Text,{variant:"bodySmall",color:"secondary",children:c.thinking})]}):""!==e.answer?a?a(e,{streaming:e.streaming}):_jsx(ChatMarkdown,{text:e.answer,resolveLink:s,onLinkPress:r}):null}),e.error&&_jsx(Text,{variant:"bodySmall",color:"error",accessibilityLiveRegion:"polite",children:e.error.message}),n&&!e.streaming&&e.actions.map(e=>_jsx(ActionCard,{action:e,confirmAction:n,onViewTicket:t,labels:o},e.token)),i&&e.messageId&&!e.streaming&&_jsx(ReplyRating,{rating:e.rating,onRate:(s,r)=>i(e.id,s,r),labels:o})]})]})}
1
+ "use strict";import{MessageBubble,Text,TypingIndicator,useTheme}from"@webority/mobile-ui";import{useMemo}from"react";import{View}from"react-native";import{mergeLabels}from"../labels.js";import{parseBlocks,parseInline}from"../markdownParse.js";import{ActionCard}from"./ActionCard.js";import{ChatMarkdown}from"./ChatMarkdown.js";import{ReplyRating}from"./ReplyRating.js";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";const inlineText=e=>e.map(e=>"text"===e.type||"code"===e.type?e.text:inlineText(e.children)).join(""),spokenText=e=>parseBlocks(e).map(e=>{switch(e.type){case"paragraph":return inlineText(parseInline(e.lines.join(" ")));case"heading":return inlineText(parseInline(e.text));case"list":return e.items.map(e=>inlineText(parseInline(e))).join(". ");case"table":return[e.header,...e.rows].map(e=>e.join(", ")).join(". ");case"code":return e.text;default:return""}}).filter(Boolean).join("\n");export function ChatMessage({turn:e,onRate:t,resolveLink:s,onLinkPress:n,confirmAction:r,onViewTicket:i,renderReply:a,labels:o,testID:l="chat-message"}){const m=mergeLabels(o),c=useTheme(),p=e.streaming&&!e.answer,g=useMemo(()=>e.streaming?e.answer:spokenText(e.answer),[e.streaming,e.answer]),u=useMemo(()=>({you:m.you}),[m.you]);return _jsxs(View,{style:{gap:c.spacing.sm},testID:l,children:[""!==e.question&&_jsx(MessageBubble,{side:"mine",text:e.question,labels:u,testID:`${l}-question`}),p?_jsx(TypingIndicator,{label:m.thinking,testID:`${l}-thinking`}):null,""!==e.answer?_jsx(MessageBubble,{side:"theirs",author:m.assistant,text:g,streaming:e.streaming,testID:`${l}-answer`,children:a?a(e,{streaming:e.streaming}):_jsx(ChatMarkdown,{text:e.answer,resolveLink:s,onLinkPress:n})}):null,e.error&&_jsx(Text,{variant:"bodySmall",color:"error",accessibilityLiveRegion:"polite",children:e.error.message}),r&&!e.streaming&&e.actions.map(e=>_jsx(ActionCard,{action:e,confirmAction:r,onViewTicket:i,labels:o},e.token)),t&&e.messageId&&!e.streaming&&_jsx(ReplyRating,{rating:e.rating,onRate:(s,n)=>t(e.id,s,n),labels:o})]})}
@@ -1 +1 @@
1
- "use strict";import{Banner,Button,EmptyState,Spinner,Text,useTheme}from"@webority/mobile-ui";import{View}from"react-native";import{KeyboardAvoidingView}from"react-native-keyboard-controller";import{useSafeAreaInsets}from"react-native-safe-area-context";import{mergeLabels}from"../labels.js";import{ChatComposer}from"./ChatComposer.js";import{ChatThread}from"./ChatThread.js";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export function ChatScreen({chat:e,title:t,headerActions:r,empty:s,disclaimer:a,maxLength:o=4e3,allowanceThreshold:i=10,rating:n=!0,testID:l="chat-screen",...d}){const m=mergeLabels(d.labels),c=useTheme(),p=useSafeAreaInsets(),g=e.messagesLeftToday,h=void 0===a?m.disclaimer:a;return _jsxs(KeyboardAvoidingView,{behavior:"padding",style:{flex:1,backgroundColor:c.colors.background.primary},testID:l,children:[_jsxs(View,{style:{flexDirection:"row",alignItems:"center",gap:c.spacing.sm,paddingHorizontal:c.spacing.md,paddingVertical:c.spacing.sm,borderBottomWidth:1,borderBottomColor:c.colors.border.secondary},children:[_jsx(Text,{variant:"title",accessibilityRole:"header",style:{flex:1},numberOfLines:1,children:t??m.panelTitle}),r,_jsx(Button,{title:m.newChat,size:"sm",variant:"soft",leftIcon:"add",disabled:0===e.turns.length&&!e.loading,onPress:e.newConversation,testID:`${l}-new`})]}),e.loading?_jsx(View,{style:{flex:1,alignItems:"center",justifyContent:"center"},children:_jsx(Spinner,{message:m.loadingConversation,testID:`${l}-loading`})}):_jsx(ChatThread,{turns:e.turns,style:{flex:1},onRate:n?e.rate:void 0,empty:s??_jsx(EmptyState,{fill:!0,size:"sm",title:m.emptyTitle,description:m.emptyText,testID:`${l}-empty`}),testID:`${l}-thread`,...d}),_jsxs(View,{style:{gap:c.spacing.xs,paddingHorizontal:c.spacing.md,paddingTop:c.spacing.sm,paddingBottom:c.spacing.sm+p.bottom,borderTopWidth:1,borderTopColor:c.colors.border.secondary},children:[e.loadError&&_jsx(Banner,{variant:"error",message:e.loadError.message,testID:`${l}-load-error`}),null!==g&&g<=i&&_jsx(Text,{variant:"caption",color:0===g?"error":"secondary",accessibilityLiveRegion:"polite",testID:`${l}-allowance`,children:m.allowance(g)}),_jsx(ChatComposer,{onSubmit:t=>{e.submit(t)},busy:e.streaming,disabled:0===g||e.loading,maxLength:o,labels:d.labels,testID:`${l}-composer`}),h?_jsx(Text,{variant:"caption",color:"tertiary",testID:`${l}-disclaimer`,children:h}):null]})]})}
1
+ "use strict";import{Banner,Button,EmptyState,Spinner,Text,useTheme}from"@webority/mobile-ui";import{View}from"react-native";import{mergeLabels}from"../labels.js";import{ChatComposer}from"./ChatComposer.js";import{ChatThread}from"./ChatThread.js";import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";export function ChatScreen({chat:e,title:t,headerActions:r,empty:s,disclaimer:a,maxLength:o=4e3,allowanceThreshold:i=10,rating:n=!0,keyboardOffset:l,testID:m="chat-screen",...d}){const c=mergeLabels(d.labels),p=useTheme(),h=e.messagesLeftToday,x=void 0===a?c.disclaimer:a;return _jsxs(View,{style:{flex:1,backgroundColor:p.colors.background.primary},testID:m,children:[_jsxs(View,{style:{flexDirection:"row",alignItems:"center",gap:p.spacing.sm,paddingHorizontal:p.spacing.md,paddingVertical:p.spacing.sm,borderBottomWidth:1,borderBottomColor:p.colors.border.secondary},children:[_jsx(Text,{variant:"title",accessibilityRole:"header",style:{flex:1},numberOfLines:1,children:t??c.panelTitle}),r,_jsx(Button,{title:c.newChat,size:"sm",variant:"soft",leftIcon:"add",disabled:0===e.turns.length&&!e.loading,onPress:e.newConversation,testID:`${m}-new`})]}),e.loading?_jsx(View,{style:{flex:1,alignItems:"center",justifyContent:"center"},children:_jsx(Spinner,{message:c.loadingConversation,testID:`${m}-loading`})}):_jsx(ChatThread,{turns:e.turns,style:{flex:1},keyboardOffset:l,onRate:n?e.rate:void 0,empty:s??_jsx(EmptyState,{fill:!0,size:"sm",title:c.emptyTitle,description:c.emptyText,testID:`${m}-empty`}),testID:`${m}-thread`,...d}),_jsx(ChatComposer,{onSubmit:t=>{e.submit(t)},busy:e.streaming,disabled:0===h||e.loading,maxLength:o,keyboardOffset:l,labels:d.labels,testID:`${m}-composer`,above:_jsxs(_Fragment,{children:[e.loadError&&_jsx(Banner,{variant:"error",message:e.loadError.message,testID:`${m}-load-error`}),null!==h&&h<=i&&_jsx(Text,{variant:"caption",color:0===h?"error":"secondary",accessibilityLiveRegion:"polite",testID:`${m}-allowance`,children:c.allowance(h)})]}),below:x?_jsx(Text,{variant:"caption",color:"tertiary",testID:`${m}-disclaimer`,children:x}):null})]})}
@@ -1 +1 @@
1
- "use strict";import{useTheme}from"@webority/mobile-ui";import{useRef}from"react";import{ScrollView}from"react-native";import{mergeLabels}from"../labels.js";import{ChatMessage}from"./ChatMessage.js";import{jsx as _jsx}from"react/jsx-runtime";export function ChatThread({turns:e,empty:t,style:r,testID:s="chat-thread",...a}){const i=useTheme(),o=mergeLabels(a.labels),l=useRef(null);return _jsx(ScrollView,{ref:l,style:r,accessibilityLabel:o.thread,keyboardShouldPersistTaps:"handled",contentContainerStyle:{padding:i.spacing.md,gap:i.spacing.lg,flexGrow:1},onContentSizeChange:()=>l.current?.scrollToEnd({animated:!0}),testID:s,children:0===e.length?t:e.map(e=>_jsx(ChatMessage,{turn:e,testID:`${s}-${e.id}`,...a},e.id))})}
1
+ "use strict";import{MessageList,useTheme}from"@webority/mobile-ui";import{useMemo}from"react";import{View}from"react-native";import{mergeLabels}from"../labels.js";import{ChatMessage}from"./ChatMessage.js";import{jsx as _jsx}from"react/jsx-runtime";const turnKey=e=>e.id,followTurn=()=>!0,TurnGap=()=>{const e=useTheme();return _jsx(View,{style:{height:e.spacing.lg}})};export function ChatThread({turns:e,empty:t,keyboardAware:s,keyboardOffset:r,style:a,testID:o="chat-thread",...m}){const i=mergeLabels(m.labels),n=useMemo(()=>[...e].reverse(),[e]);return _jsx(MessageList,{messages:n,keyExtractor:turnKey,renderMessage:({item:e})=>_jsx(ChatMessage,{turn:e,testID:`${o}-${e.id}`,...m}),followNew:followTurn,empty:t,ItemSeparatorComponent:TurnGap,keyboardAware:s,keyboardOffset:r,accessibilityLabel:i.thread,style:a,testID:o})}
@@ -9,12 +9,23 @@ export interface ChatComposerProps {
9
9
  disabled?: boolean;
10
10
  /** The server's message limit. Default 4000. */
11
11
  maxLength?: number;
12
+ /** Inside the bar, above the question box: a banner, the questions left. */
13
+ above?: React.ReactNode;
14
+ /** Inside the bar, below the question box: the disclaimer. */
15
+ below?: React.ReactNode;
16
+ /** Rides the keyboard; see mobile-ui `Composer`. Default true. */
17
+ keyboardAware?: boolean;
18
+ /** See mobile-ui `Composer`. Default: the bottom safe-area inset. */
19
+ keyboardOffset?: number;
20
+ /** Pads for the bottom safe area. Default true. */
21
+ safeArea?: boolean;
12
22
  labels?: Partial<ChatLabels>;
13
23
  testID?: string;
14
24
  }
15
25
  /**
16
- * The question box and its send button. The return key adds a new line, as phone keyboards
17
- * expect; only the button sends.
26
+ * The question box and its send button, on mobile-ui's `Composer`. The return
27
+ * key adds a new line, as phone keyboards expect; only the button sends. The
28
+ * bar rides the keyboard and pads for the bottom safe area by default.
18
29
  */
19
30
  export declare const ChatComposer: React.FC<ChatComposerProps>;
20
31
  //# sourceMappingURL=ChatComposer.d.ts.map
@@ -27,9 +27,10 @@ export interface ChatMessageProps<T = string> extends ChatReplyProps<T> {
27
27
  testID?: string;
28
28
  }
29
29
  /**
30
- * One turn: the person's question on the right, the assistant's answer on the left. The answer is
31
- * a polite live region, so a screen reader reads it as it settles. Support actions show under the
32
- * answer, and the rating once the reply is stored.
30
+ * One turn on mobile-ui's chat parts: the person's question in a `MessageBubble` on the trailing
31
+ * side, the typing dots while the answer has not started, then the answer in a bubble on the
32
+ * leading side. The answer is announced once, when it finishes arriving. Support actions show
33
+ * under the answer, and the rating once the reply is stored.
33
34
  */
34
35
  export declare function ChatMessage<T = string>({ turn, onRate, resolveLink, onLinkPress, confirmAction, onViewTicket, renderReply, labels: labelOverrides, testID }: ChatMessageProps<T>): React.ReactElement;
35
36
  //# sourceMappingURL=ChatMessage.d.ts.map
@@ -18,13 +18,18 @@ export interface ChatScreenProps<T = string> extends Omit<ChatReplyProps<T>, 'on
18
18
  allowanceThreshold?: number;
19
19
  /** Offer ratings under stored replies. Default true. */
20
20
  rating?: boolean;
21
+ /**
22
+ * How far below the question box the keyboard's top edge starts. Default: the bottom safe-area
23
+ * inset; set it to the tab bar's height when the screen sits above one.
24
+ */
25
+ keyboardOffset?: number;
21
26
  testID?: string;
22
27
  }
23
28
  /**
24
29
  * The whole chat for one screen of the app's navigator: a header with New chat, the
25
30
  * conversation, the questions left today, the question box and the AI disclaimer. The question
26
- * box rises with the keyboard. Place it in a stack screen (or a full-height sheet) and keep the
27
- * `useChat` call above the navigator, so the conversation survives leaving the screen.
31
+ * box rides the keyboard and the newest turn stays above it. Place it in a stack screen and keep
32
+ * the `useChat` call above the navigator, so the conversation survives leaving the screen.
28
33
  */
29
- export declare function ChatScreen<T = string>({ chat, title, headerActions, empty, disclaimer, maxLength, allowanceThreshold, rating, testID, ...reply }: ChatScreenProps<T>): React.ReactElement;
34
+ export declare function ChatScreen<T = string>({ chat, title, headerActions, empty, disclaimer, maxLength, allowanceThreshold, rating, keyboardOffset, testID, ...reply }: ChatScreenProps<T>): React.ReactElement;
30
35
  //# sourceMappingURL=ChatScreen.d.ts.map
@@ -6,12 +6,17 @@ export interface ChatThreadProps<T = string> extends ChatReplyProps<T> {
6
6
  turns: ChatTurn[];
7
7
  /** Shown before the first question. */
8
8
  empty?: React.ReactNode;
9
+ /** Lifts the newest turn above the keyboard; see mobile-ui `MessageList`. Default true. */
10
+ keyboardAware?: boolean;
11
+ /** The composer's `keyboardOffset`. Default: the bottom safe-area inset. */
12
+ keyboardOffset?: number;
9
13
  style?: StyleProp<ViewStyle>;
10
14
  testID?: string;
11
15
  }
12
16
  /**
13
- * The conversation, oldest first, in a scroll view that follows the newest text while an answer
14
- * streams and when a question is added.
17
+ * The conversation on mobile-ui's `MessageList`: newest at the bottom, the
18
+ * streaming answer growing in place, a jump to the latest once scrolled up,
19
+ * and the newest turn kept above the keyboard.
15
20
  */
16
- export declare function ChatThread<T = string>({ turns, empty, style, testID, ...reply }: ChatThreadProps<T>): React.ReactElement;
21
+ export declare function ChatThread<T = string>({ turns, empty, keyboardAware, keyboardOffset, style, testID, ...reply }: ChatThreadProps<T>): React.ReactElement;
17
22
  //# sourceMappingURL=ChatThread.d.ts.map
@@ -9,12 +9,23 @@ export interface ChatComposerProps {
9
9
  disabled?: boolean;
10
10
  /** The server's message limit. Default 4000. */
11
11
  maxLength?: number;
12
+ /** Inside the bar, above the question box: a banner, the questions left. */
13
+ above?: React.ReactNode;
14
+ /** Inside the bar, below the question box: the disclaimer. */
15
+ below?: React.ReactNode;
16
+ /** Rides the keyboard; see mobile-ui `Composer`. Default true. */
17
+ keyboardAware?: boolean;
18
+ /** See mobile-ui `Composer`. Default: the bottom safe-area inset. */
19
+ keyboardOffset?: number;
20
+ /** Pads for the bottom safe area. Default true. */
21
+ safeArea?: boolean;
12
22
  labels?: Partial<ChatLabels>;
13
23
  testID?: string;
14
24
  }
15
25
  /**
16
- * The question box and its send button. The return key adds a new line, as phone keyboards
17
- * expect; only the button sends.
26
+ * The question box and its send button, on mobile-ui's `Composer`. The return
27
+ * key adds a new line, as phone keyboards expect; only the button sends. The
28
+ * bar rides the keyboard and pads for the bottom safe area by default.
18
29
  */
19
30
  export declare const ChatComposer: React.FC<ChatComposerProps>;
20
31
  //# sourceMappingURL=ChatComposer.d.ts.map
@@ -27,9 +27,10 @@ export interface ChatMessageProps<T = string> extends ChatReplyProps<T> {
27
27
  testID?: string;
28
28
  }
29
29
  /**
30
- * One turn: the person's question on the right, the assistant's answer on the left. The answer is
31
- * a polite live region, so a screen reader reads it as it settles. Support actions show under the
32
- * answer, and the rating once the reply is stored.
30
+ * One turn on mobile-ui's chat parts: the person's question in a `MessageBubble` on the trailing
31
+ * side, the typing dots while the answer has not started, then the answer in a bubble on the
32
+ * leading side. The answer is announced once, when it finishes arriving. Support actions show
33
+ * under the answer, and the rating once the reply is stored.
33
34
  */
34
35
  export declare function ChatMessage<T = string>({ turn, onRate, resolveLink, onLinkPress, confirmAction, onViewTicket, renderReply, labels: labelOverrides, testID }: ChatMessageProps<T>): React.ReactElement;
35
36
  //# sourceMappingURL=ChatMessage.d.ts.map
@@ -18,13 +18,18 @@ export interface ChatScreenProps<T = string> extends Omit<ChatReplyProps<T>, 'on
18
18
  allowanceThreshold?: number;
19
19
  /** Offer ratings under stored replies. Default true. */
20
20
  rating?: boolean;
21
+ /**
22
+ * How far below the question box the keyboard's top edge starts. Default: the bottom safe-area
23
+ * inset; set it to the tab bar's height when the screen sits above one.
24
+ */
25
+ keyboardOffset?: number;
21
26
  testID?: string;
22
27
  }
23
28
  /**
24
29
  * The whole chat for one screen of the app's navigator: a header with New chat, the
25
30
  * conversation, the questions left today, the question box and the AI disclaimer. The question
26
- * box rises with the keyboard. Place it in a stack screen (or a full-height sheet) and keep the
27
- * `useChat` call above the navigator, so the conversation survives leaving the screen.
31
+ * box rides the keyboard and the newest turn stays above it. Place it in a stack screen and keep
32
+ * the `useChat` call above the navigator, so the conversation survives leaving the screen.
28
33
  */
29
- export declare function ChatScreen<T = string>({ chat, title, headerActions, empty, disclaimer, maxLength, allowanceThreshold, rating, testID, ...reply }: ChatScreenProps<T>): React.ReactElement;
34
+ export declare function ChatScreen<T = string>({ chat, title, headerActions, empty, disclaimer, maxLength, allowanceThreshold, rating, keyboardOffset, testID, ...reply }: ChatScreenProps<T>): React.ReactElement;
30
35
  //# sourceMappingURL=ChatScreen.d.ts.map
@@ -6,12 +6,17 @@ export interface ChatThreadProps<T = string> extends ChatReplyProps<T> {
6
6
  turns: ChatTurn[];
7
7
  /** Shown before the first question. */
8
8
  empty?: React.ReactNode;
9
+ /** Lifts the newest turn above the keyboard; see mobile-ui `MessageList`. Default true. */
10
+ keyboardAware?: boolean;
11
+ /** The composer's `keyboardOffset`. Default: the bottom safe-area inset. */
12
+ keyboardOffset?: number;
9
13
  style?: StyleProp<ViewStyle>;
10
14
  testID?: string;
11
15
  }
12
16
  /**
13
- * The conversation, oldest first, in a scroll view that follows the newest text while an answer
14
- * streams and when a question is added.
17
+ * The conversation on mobile-ui's `MessageList`: newest at the bottom, the
18
+ * streaming answer growing in place, a jump to the latest once scrolled up,
19
+ * and the newest turn kept above the keyboard.
15
20
  */
16
- export declare function ChatThread<T = string>({ turns, empty, style, testID, ...reply }: ChatThreadProps<T>): React.ReactElement;
21
+ export declare function ChatThread<T = string>({ turns, empty, keyboardAware, keyboardOffset, style, testID, ...reply }: ChatThreadProps<T>): React.ReactElement;
17
22
  //# sourceMappingURL=ChatThread.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-chat",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "AI assistant chat for Webority React Native apps: a streaming chat client, a store-agnostic chat hook, and the chat screen, thread, composer, past chats, reply rating and support action cards, built on @webority/mobile-ui.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -42,8 +42,8 @@
42
42
  "!**/*.map"
43
43
  ],
44
44
  "peerDependencies": {
45
- "@webority/mobile-core": ">=0.9.0 <0.10.0",
46
- "@webority/mobile-ui": ">=0.9.0 <0.10.0",
45
+ "@webority/mobile-core": ">=0.11.0 <0.12.0",
46
+ "@webority/mobile-ui": ">=0.11.0 <0.12.0",
47
47
  "react": "^19.1.0",
48
48
  "react-native": ">=0.81.0",
49
49
  "react-native-keyboard-controller": ">=1.21.9",
@@ -81,8 +81,8 @@
81
81
  ]
82
82
  },
83
83
  "devDependencies": {
84
- "@webority/mobile-core": "0.9.1",
85
- "@webority/mobile-ui": "0.9.1"
84
+ "@webority/mobile-core": "0.11.0",
85
+ "@webority/mobile-ui": "0.11.0"
86
86
  },
87
87
  "scripts": {
88
88
  "build": "bob build && node ../../scripts/minify-lib.mjs",