opendesk-react-native 0.1.6 → 0.1.7
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/lib/ui/OpenDeskChat.js +37 -2
- package/lib/ui/RichText.d.ts +15 -0
- package/lib/ui/RichText.js +59 -0
- package/lib/ui/rich.d.ts +54 -0
- package/lib/ui/rich.js +444 -0
- package/package.json +1 -1
- package/src/ui/OpenDeskChat.tsx +70 -2
- package/src/ui/RichText.tsx +129 -0
- package/src/ui/rich.ts +507 -0
package/src/ui/OpenDeskChat.tsx
CHANGED
|
@@ -24,6 +24,8 @@ import type { Attachment, Conversation, Message, UploadFile, WidgetConfig } from
|
|
|
24
24
|
import { BotFace } from './BotFace';
|
|
25
25
|
import { ArrowIcon, ChevronIcon, CloseIcon, PaperclipIcon, PersonIcon, SparkIcon } from './icons';
|
|
26
26
|
import { useKeyboardInset } from './keyboard';
|
|
27
|
+
import { extractSources, hostOf, parseHtml, parseMarkdown, type Block, type Source } from './rich';
|
|
28
|
+
import { RichText } from './RichText';
|
|
27
29
|
import { defaultStrings, format, type ChatStrings } from './strings';
|
|
28
30
|
import { makeTheme, type ChatTheme } from './theme';
|
|
29
31
|
|
|
@@ -680,6 +682,7 @@ function Bubble({
|
|
|
680
682
|
const avatar = message ? (message.author.user?.avatar_url ?? bot?.avatar_url) : bot?.avatar_url;
|
|
681
683
|
const botLook = bot?.look ?? fallbackBot?.look;
|
|
682
684
|
const text = message ? (message.body_text ?? '') : (reply?.text ?? '');
|
|
685
|
+
const body = useMemo(() => bodyOf(message, reply, mine, ai), [message, reply, mine, ai]);
|
|
683
686
|
const local = message?.id.startsWith('local-') === true;
|
|
684
687
|
const action = message?.ai_action;
|
|
685
688
|
const tools = reply?.tools ?? [];
|
|
@@ -724,7 +727,7 @@ function Bubble({
|
|
|
724
727
|
</View>
|
|
725
728
|
) : null}
|
|
726
729
|
|
|
727
|
-
{text || reply ? (
|
|
730
|
+
{text || body.blocks.length > 0 || reply ? (
|
|
728
731
|
<View
|
|
729
732
|
style={[
|
|
730
733
|
styles.msg,
|
|
@@ -733,7 +736,40 @@ function Bubble({
|
|
|
733
736
|
local && styles.sendingMsg,
|
|
734
737
|
]}
|
|
735
738
|
>
|
|
736
|
-
{
|
|
739
|
+
{mine ? (
|
|
740
|
+
<Text style={[styles.msgText, { color: theme.accentFg }]}>{text}</Text>
|
|
741
|
+
) : body.blocks.length > 0 ? (
|
|
742
|
+
<RichText
|
|
743
|
+
blocks={body.blocks}
|
|
744
|
+
textStyle={styles.msgText}
|
|
745
|
+
codeBackground={ai ? theme.bg2 : theme.line}
|
|
746
|
+
quoteColor={theme.line2}
|
|
747
|
+
mono={theme.mono}
|
|
748
|
+
/>
|
|
749
|
+
) : (
|
|
750
|
+
<Dots color={theme.ai} />
|
|
751
|
+
)}
|
|
752
|
+
</View>
|
|
753
|
+
) : null}
|
|
754
|
+
|
|
755
|
+
{body.sources.length > 0 ? (
|
|
756
|
+
<View style={styles.sources}>
|
|
757
|
+
{body.sources.map((source) => (
|
|
758
|
+
<Pressable
|
|
759
|
+
key={source.href}
|
|
760
|
+
accessibilityRole="link"
|
|
761
|
+
accessibilityHint={hostOf(source.href)}
|
|
762
|
+
onPress={() => {
|
|
763
|
+
Linking.openURL(source.href).catch(() => undefined);
|
|
764
|
+
}}
|
|
765
|
+
style={({ pressed }) => [styles.src, pressed && { borderColor: theme.aiLine, backgroundColor: theme.aiSoft }]}
|
|
766
|
+
>
|
|
767
|
+
<ArrowIcon color={theme.ai} size={11} angle={-45} />
|
|
768
|
+
<Text style={styles.srcText} numberOfLines={1}>
|
|
769
|
+
{source.title}
|
|
770
|
+
</Text>
|
|
771
|
+
</Pressable>
|
|
772
|
+
))}
|
|
737
773
|
</View>
|
|
738
774
|
) : null}
|
|
739
775
|
|
|
@@ -775,6 +811,23 @@ function Bubble({
|
|
|
775
811
|
);
|
|
776
812
|
}
|
|
777
813
|
|
|
814
|
+
/**
|
|
815
|
+
* What a bubble shows: the visitor's own text as typed; stored replies from their HTML (links
|
|
816
|
+
* included), falling back to markdown in the text; AI answers still streaming as markdown. An AI
|
|
817
|
+
* answer's trailing list of links becomes its sources.
|
|
818
|
+
*/
|
|
819
|
+
function bodyOf(message: Message | undefined, reply: AIReply | undefined, mine: boolean, ai: boolean): { blocks: Block[]; sources: Source[] } {
|
|
820
|
+
if (mine) {
|
|
821
|
+
return { blocks: [], sources: [] };
|
|
822
|
+
}
|
|
823
|
+
if (reply && !message) {
|
|
824
|
+
return { blocks: parseMarkdown(reply.text, true), sources: [] };
|
|
825
|
+
}
|
|
826
|
+
const blocks = message?.body_html?.trim() ? parseHtml(message.body_html, ai) : parseMarkdown(message?.body_text ?? '');
|
|
827
|
+
|
|
828
|
+
return ai ? extractSources(blocks) : { blocks, sources: [] };
|
|
829
|
+
}
|
|
830
|
+
|
|
778
831
|
function ActionCard({
|
|
779
832
|
ui,
|
|
780
833
|
message,
|
|
@@ -1245,6 +1298,21 @@ function createStyles(t: ChatTheme) {
|
|
|
1245
1298
|
msgMe: { backgroundColor: t.accent },
|
|
1246
1299
|
msgThem: { backgroundColor: t.bg2 },
|
|
1247
1300
|
msgAi: { backgroundColor: t.bg3, borderWidth: 1, borderColor: t.line },
|
|
1301
|
+
sources: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 },
|
|
1302
|
+
src: {
|
|
1303
|
+
flexDirection: 'row',
|
|
1304
|
+
alignItems: 'center',
|
|
1305
|
+
gap: 6,
|
|
1306
|
+
maxWidth: 240,
|
|
1307
|
+
height: 26,
|
|
1308
|
+
paddingLeft: 8,
|
|
1309
|
+
paddingRight: 10,
|
|
1310
|
+
borderRadius: r.ctl,
|
|
1311
|
+
borderWidth: 1,
|
|
1312
|
+
borderColor: t.line,
|
|
1313
|
+
backgroundColor: t.bg,
|
|
1314
|
+
},
|
|
1315
|
+
srcText: { flexShrink: 1, fontSize: 12, color: t.fg2 },
|
|
1248
1316
|
msgText: { fontSize: 13.5, lineHeight: 19.5, color: t.fg },
|
|
1249
1317
|
sendingMsg: { opacity: 0.7 },
|
|
1250
1318
|
meta: { fontFamily: t.mono, fontSize: 10, color: t.fg4, marginTop: 3, marginHorizontal: 4 },
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Fragment } from 'react';
|
|
2
|
+
import { Linking, StyleSheet, Text, View, type TextStyle } from 'react-native';
|
|
3
|
+
import type { Block, Inline } from './rich';
|
|
4
|
+
|
|
5
|
+
interface RichTextProps {
|
|
6
|
+
blocks: Block[];
|
|
7
|
+
/** The bubble's text style; colour and size carry into every block. */
|
|
8
|
+
textStyle: TextStyle;
|
|
9
|
+
/** Fill behind inline code and code blocks. */
|
|
10
|
+
codeBackground: string;
|
|
11
|
+
/** Rule beside quotes. */
|
|
12
|
+
quoteColor: string;
|
|
13
|
+
mono: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Renders parsed message blocks like the widget's `.msg`: paragraphs, lists, quotes, code and underlined links. */
|
|
17
|
+
export function RichText({ blocks, textStyle, codeBackground, quoteColor, mono }: RichTextProps) {
|
|
18
|
+
const color = textStyle.color;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View style={styles.blocks}>
|
|
22
|
+
{blocks.map((block, index) => {
|
|
23
|
+
switch (block.kind) {
|
|
24
|
+
case 'paragraph':
|
|
25
|
+
return (
|
|
26
|
+
<Text key={index} style={textStyle}>
|
|
27
|
+
<Inlines inlines={block.inlines} codeStyle={{ fontFamily: mono, backgroundColor: codeBackground }} />
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
case 'heading':
|
|
31
|
+
return (
|
|
32
|
+
<Text key={index} style={[textStyle, styles.bold]}>
|
|
33
|
+
<Inlines inlines={block.inlines} codeStyle={{ fontFamily: mono, backgroundColor: codeBackground }} />
|
|
34
|
+
</Text>
|
|
35
|
+
);
|
|
36
|
+
case 'quote':
|
|
37
|
+
return (
|
|
38
|
+
<View key={index} style={[styles.quote, { borderLeftColor: quoteColor }]}>
|
|
39
|
+
<Text style={[textStyle, styles.quoteText]}>
|
|
40
|
+
<Inlines inlines={block.inlines} codeStyle={{ fontFamily: mono, backgroundColor: codeBackground }} />
|
|
41
|
+
</Text>
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
case 'code':
|
|
45
|
+
return (
|
|
46
|
+
<View key={index} style={[styles.pre, { backgroundColor: codeBackground }]}>
|
|
47
|
+
<Text style={[textStyle, styles.preText, { fontFamily: mono }]}>{block.text}</Text>
|
|
48
|
+
</View>
|
|
49
|
+
);
|
|
50
|
+
case 'list':
|
|
51
|
+
return (
|
|
52
|
+
<View key={index} style={styles.list}>
|
|
53
|
+
{block.items.map((item, itemIndex) => (
|
|
54
|
+
<View key={itemIndex} style={styles.item}>
|
|
55
|
+
<Text style={[textStyle, styles.marker, { color }]}>{block.ordered ? `${itemIndex + 1}.` : '•'}</Text>
|
|
56
|
+
<Text style={[textStyle, styles.itemText]}>
|
|
57
|
+
<Inlines inlines={item} codeStyle={{ fontFamily: mono, backgroundColor: codeBackground }} />
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
))}
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
})}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Inlines({ inlines, codeStyle }: { inlines: Inline[]; codeStyle: TextStyle }) {
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{inlines.map((inline, index) => {
|
|
73
|
+
switch (inline.kind) {
|
|
74
|
+
case 'text':
|
|
75
|
+
return <Fragment key={index}>{inline.text}</Fragment>;
|
|
76
|
+
case 'break':
|
|
77
|
+
return <Fragment key={index}>{'\n'}</Fragment>;
|
|
78
|
+
case 'bold':
|
|
79
|
+
return (
|
|
80
|
+
<Text key={index} style={styles.bold}>
|
|
81
|
+
<Inlines inlines={inline.children} codeStyle={codeStyle} />
|
|
82
|
+
</Text>
|
|
83
|
+
);
|
|
84
|
+
case 'italic':
|
|
85
|
+
return (
|
|
86
|
+
<Text key={index} style={styles.italic}>
|
|
87
|
+
<Inlines inlines={inline.children} codeStyle={codeStyle} />
|
|
88
|
+
</Text>
|
|
89
|
+
);
|
|
90
|
+
case 'code':
|
|
91
|
+
return (
|
|
92
|
+
<Text key={index} style={[styles.code, codeStyle]}>
|
|
93
|
+
{inline.text}
|
|
94
|
+
</Text>
|
|
95
|
+
);
|
|
96
|
+
case 'link':
|
|
97
|
+
return (
|
|
98
|
+
<Text
|
|
99
|
+
key={index}
|
|
100
|
+
accessibilityRole="link"
|
|
101
|
+
onPress={() => {
|
|
102
|
+
Linking.openURL(inline.href).catch(() => undefined);
|
|
103
|
+
}}
|
|
104
|
+
style={styles.link}
|
|
105
|
+
>
|
|
106
|
+
<Inlines inlines={inline.children} codeStyle={codeStyle} />
|
|
107
|
+
</Text>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
})}
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
blocks: { gap: 6 },
|
|
117
|
+
bold: { fontWeight: '600' },
|
|
118
|
+
italic: { fontStyle: 'italic' },
|
|
119
|
+
link: { textDecorationLine: 'underline', fontWeight: '500' },
|
|
120
|
+
code: { fontSize: 12 },
|
|
121
|
+
pre: { borderRadius: 6, paddingHorizontal: 8, paddingVertical: 6 },
|
|
122
|
+
preText: { fontSize: 12, lineHeight: 17 },
|
|
123
|
+
quote: { borderLeftWidth: 2, paddingLeft: 8 },
|
|
124
|
+
quoteText: { opacity: 0.8 },
|
|
125
|
+
list: { gap: 2 },
|
|
126
|
+
item: { flexDirection: 'row' },
|
|
127
|
+
marker: { minWidth: 16, paddingRight: 4 },
|
|
128
|
+
itemText: { flex: 1 },
|
|
129
|
+
});
|