@vvedantb/native-ui 0.8.0 → 0.10.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 +145 -7
- package/package.json +35 -8
- package/src/BulkActionBar.tsx +303 -0
- package/src/DividedList.tsx +37 -0
- package/src/DropdownMenu.tsx +439 -0
- package/src/FloatingFAB.tsx +4 -1
- package/src/ai/activity-step-detail.tsx +95 -0
- package/src/ai/choice-question.tsx +297 -0
- package/src/ai/code-block.tsx +50 -7
- package/src/ai/index.ts +10 -0
- package/src/ai/message.tsx +59 -12
- package/src/ai/prompt-input.tsx +79 -28
- package/src/ai/trace.tsx +32 -1
- package/src/auth/AuthScreen.tsx +32 -4
- package/src/auth/SecondFactorScreen.tsx +87 -0
- package/src/auth/SignInScreen.tsx +262 -1
- package/src/auth/index.ts +23 -1
- package/src/auth/totp.ts +202 -0
- package/src/configureTheme.ts +60 -0
- package/src/entries/markdown.ts +5 -0
- package/src/entries/primitives.ts +22 -0
- package/src/entries/theme.ts +2 -0
- package/src/examples/ParityGallery.tsx +56 -0
- package/src/floating.ts +2 -4
- package/src/highlight.ts +117 -0
- package/src/index.ts +24 -0
- package/src/markdown/Markdown.tsx +191 -0
- package/src/push.ts +186 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
3
|
+
import { IconArrowLeft, IconArrowRight, IconCheck, IconPencil } from "@tabler/icons-react-native";
|
|
4
|
+
import { Badge } from "../Badge";
|
|
5
|
+
import { Button } from "../Button";
|
|
6
|
+
import { Input } from "../Input";
|
|
7
|
+
import { sizeStyle, textStyle, typeStyle } from "../controlStyles";
|
|
8
|
+
import { palette } from "../palette";
|
|
9
|
+
import { useLargerText } from "../theme";
|
|
10
|
+
import { tokens } from "../tokens";
|
|
11
|
+
import { useScheme } from "../useScheme";
|
|
12
|
+
import { chatTextSize } from "./chat-text";
|
|
13
|
+
|
|
14
|
+
/** One question, as an agent's AskUserQuestion sends it. */
|
|
15
|
+
export type ChoiceQuestionItem = {
|
|
16
|
+
question: string;
|
|
17
|
+
/** Short tag shown as a badge beside the question. */
|
|
18
|
+
header: string;
|
|
19
|
+
multiSelect: boolean;
|
|
20
|
+
options: ReadonlyArray<{ label: string; description: string }>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ChoiceQuestionProps = {
|
|
24
|
+
questions: ReadonlyArray<ChoiceQuestionItem>;
|
|
25
|
+
/**
|
|
26
|
+
* Question text to the picked label, or the typed "Other" answer.
|
|
27
|
+
* Multi-select joins labels with ", " (the shape AskUserQuestion reads).
|
|
28
|
+
*/
|
|
29
|
+
onSubmit: (answers: Record<string, string>) => void | Promise<void>;
|
|
30
|
+
/** Shows a spinner on the submit button and blocks input. */
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
/** Default "Other...". */
|
|
33
|
+
otherLabel?: string;
|
|
34
|
+
/** Default "Type your answer...". */
|
|
35
|
+
otherPlaceholder?: string;
|
|
36
|
+
/** Inside the web dock card (`rounded-surface bg-card p-3`, scrolling past 60% of the screen). Default `true`. */
|
|
37
|
+
framed?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
41
|
+
|
|
42
|
+
/** The structured answer for `onSubmit`. Exported for tests. */
|
|
43
|
+
export function choiceAnswers(
|
|
44
|
+
questions: ReadonlyArray<ChoiceQuestionItem>,
|
|
45
|
+
picked: Readonly<Record<number, readonly string[]>>,
|
|
46
|
+
other: Readonly<Record<number, string>>,
|
|
47
|
+
otherOn: Readonly<Record<number, boolean>>,
|
|
48
|
+
): Record<string, string> {
|
|
49
|
+
const result: Record<string, string> = {};
|
|
50
|
+
questions.forEach((question, index) => {
|
|
51
|
+
if (otherOn[index]) {
|
|
52
|
+
result[question.question] = (other[index] ?? "").trim();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const selected = picked[index] ?? [];
|
|
56
|
+
result[question.question] = question.multiSelect ? selected.join(", ") : (selected[0] ?? "");
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* An agent's blocking multiple-choice question, as the web chat's question
|
|
63
|
+
* dock (`ChatQuestionDock` + `MultipleChoiceQuestion`): the question with its
|
|
64
|
+
* header badge and step count, lettered option rows with descriptions, an
|
|
65
|
+
* "Other..." row that opens a text field, then Back and Next or Submit. One
|
|
66
|
+
* question at a time.
|
|
67
|
+
*/
|
|
68
|
+
export function ChoiceQuestion({
|
|
69
|
+
questions,
|
|
70
|
+
onSubmit,
|
|
71
|
+
loading = false,
|
|
72
|
+
otherLabel = "Other...",
|
|
73
|
+
otherPlaceholder = "Type your answer...",
|
|
74
|
+
framed = true,
|
|
75
|
+
}: ChoiceQuestionProps) {
|
|
76
|
+
const scheme = useScheme();
|
|
77
|
+
const largerText = useLargerText();
|
|
78
|
+
const icon = largerText ? 18 : 15;
|
|
79
|
+
const [step, setStep] = useState(0);
|
|
80
|
+
const [picked, setPicked] = useState<Record<number, string[]>>({});
|
|
81
|
+
const [other, setOther] = useState<Record<number, string>>({});
|
|
82
|
+
const [otherOn, setOtherOn] = useState<Record<number, boolean>>({});
|
|
83
|
+
const question = questions[step];
|
|
84
|
+
if (!question) return null;
|
|
85
|
+
|
|
86
|
+
const total = questions.length;
|
|
87
|
+
const isLast = step === total - 1;
|
|
88
|
+
const selected = picked[step] ?? [];
|
|
89
|
+
const hasAnswer = otherOn[step] ? (other[step] ?? "").trim() !== "" : selected.length > 0;
|
|
90
|
+
|
|
91
|
+
const toggle = (label: string) => {
|
|
92
|
+
const next = question.multiSelect
|
|
93
|
+
? selected.includes(label)
|
|
94
|
+
? selected.filter((item) => item !== label)
|
|
95
|
+
: [...selected, label]
|
|
96
|
+
: [label];
|
|
97
|
+
setPicked({ ...picked, [step]: next });
|
|
98
|
+
setOtherOn({ ...otherOn, [step]: false });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const next = () => {
|
|
102
|
+
if (!hasAnswer || loading) return;
|
|
103
|
+
if (isLast) void onSubmit(choiceAnswers(questions, picked, other, otherOn));
|
|
104
|
+
else setStep(step + 1);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const body = (
|
|
108
|
+
<View style={{ gap: 16 }}>
|
|
109
|
+
<View className="flex-row items-center justify-between" style={{ gap: 8 }}>
|
|
110
|
+
<Text
|
|
111
|
+
className="flex-1 font-instrument-sans-semibold"
|
|
112
|
+
// Web `text-[15px] leading-snug`; larger text steps up to 18.
|
|
113
|
+
style={[{ fontSize: largerText ? 18 : 15, lineHeight: largerText ? 24 : 20 }, textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
114
|
+
>
|
|
115
|
+
{question.question}
|
|
116
|
+
</Text>
|
|
117
|
+
<View className="flex-row items-center" style={{ gap: 6 }}>
|
|
118
|
+
{/* Web `text-3xs`: a compact badge beside the question. */}
|
|
119
|
+
{question.header ? (
|
|
120
|
+
<Badge variant="secondary" className="px-2 py-0.5" textClassName={largerText ? "text-sm" : "text-xs"}>
|
|
121
|
+
{question.header}
|
|
122
|
+
</Badge>
|
|
123
|
+
) : null}
|
|
124
|
+
{total > 1 ? (
|
|
125
|
+
<Text className="font-instrument-sans-medium" style={[sizeStyle(chatTextSize("caption", largerText)), textStyle("muted", scheme), typeStyle("medium")]}>
|
|
126
|
+
{`${step + 1}/${total}`}
|
|
127
|
+
</Text>
|
|
128
|
+
) : null}
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
|
|
132
|
+
<View style={{ gap: 4 }}>
|
|
133
|
+
{question.options.map((option, index) => (
|
|
134
|
+
<ChoiceRow
|
|
135
|
+
key={`${option.label}-${index}`}
|
|
136
|
+
selected={!otherOn[step] && selected.includes(option.label)}
|
|
137
|
+
disabled={loading}
|
|
138
|
+
mark={LETTERS[index] ?? String(index + 1)}
|
|
139
|
+
label={option.label}
|
|
140
|
+
description={option.description}
|
|
141
|
+
onPress={() => toggle(option.label)}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
<ChoiceRow
|
|
145
|
+
selected={otherOn[step] ?? false}
|
|
146
|
+
disabled={loading}
|
|
147
|
+
mark="other"
|
|
148
|
+
label={otherLabel}
|
|
149
|
+
onPress={() => {
|
|
150
|
+
setOtherOn({ ...otherOn, [step]: true });
|
|
151
|
+
setPicked({ ...picked, [step]: [] });
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{otherOn[step] ? (
|
|
155
|
+
<View style={{ marginTop: 8, marginLeft: 36 }}>
|
|
156
|
+
<Input
|
|
157
|
+
value={other[step] ?? ""}
|
|
158
|
+
onChangeText={(text: string) => setOther({ ...other, [step]: text })}
|
|
159
|
+
placeholder={otherPlaceholder}
|
|
160
|
+
editable={!loading}
|
|
161
|
+
autoFocus
|
|
162
|
+
onSubmitEditing={next}
|
|
163
|
+
/>
|
|
164
|
+
</View>
|
|
165
|
+
) : null}
|
|
166
|
+
</ChoiceRow>
|
|
167
|
+
</View>
|
|
168
|
+
|
|
169
|
+
<View className="flex-row items-center" style={{ gap: 8 }}>
|
|
170
|
+
{total > 1 && step > 0 ? (
|
|
171
|
+
<View className="flex-1">
|
|
172
|
+
<Button
|
|
173
|
+
variant="ghost"
|
|
174
|
+
disabled={loading}
|
|
175
|
+
onPress={() => setStep(step - 1)}
|
|
176
|
+
startContent={<IconArrowLeft size={icon} strokeWidth={2.5} color={tokens.color.foreground[scheme]} />}
|
|
177
|
+
>
|
|
178
|
+
Back
|
|
179
|
+
</Button>
|
|
180
|
+
</View>
|
|
181
|
+
) : null}
|
|
182
|
+
<View className="flex-1">
|
|
183
|
+
<Button
|
|
184
|
+
disabled={!hasAnswer}
|
|
185
|
+
loading={loading}
|
|
186
|
+
onPress={next}
|
|
187
|
+
endContent={
|
|
188
|
+
loading ? null : <IconArrowRight size={icon} strokeWidth={2.5} color={palette.primaryForeground[scheme]} />
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{isLast ? "Submit" : "Next"}
|
|
192
|
+
</Button>
|
|
193
|
+
</View>
|
|
194
|
+
</View>
|
|
195
|
+
</View>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (!framed) return body;
|
|
199
|
+
return (
|
|
200
|
+
<View style={{ borderRadius: tokens.radius.lg, backgroundColor: tokens.color.surface[scheme], overflow: "hidden" }}>
|
|
201
|
+
<ScrollView style={{ maxHeight: 544 }} contentContainerStyle={{ padding: 12 }} keyboardShouldPersistTaps="handled">
|
|
202
|
+
{body}
|
|
203
|
+
</ScrollView>
|
|
204
|
+
</View>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One option row (web `Card` with `bg-secondary`, `py-2 px-2.5`, radius 16): a round 24 mark
|
|
210
|
+
* (30 with larger text; letter, or a pencil for "Other"; a check on the primary fill when picked),
|
|
211
|
+
* the label and the description. Picked: a 2-wide primary ring on the accent.
|
|
212
|
+
*/
|
|
213
|
+
function ChoiceRow({
|
|
214
|
+
selected,
|
|
215
|
+
disabled,
|
|
216
|
+
mark,
|
|
217
|
+
label,
|
|
218
|
+
description,
|
|
219
|
+
onPress,
|
|
220
|
+
children,
|
|
221
|
+
}: {
|
|
222
|
+
selected: boolean;
|
|
223
|
+
disabled: boolean;
|
|
224
|
+
mark: string;
|
|
225
|
+
label: string;
|
|
226
|
+
description?: string;
|
|
227
|
+
onPress: () => void;
|
|
228
|
+
children?: ReactNode;
|
|
229
|
+
}) {
|
|
230
|
+
const scheme = useScheme();
|
|
231
|
+
const largerText = useLargerText();
|
|
232
|
+
const muted = tokens.color.muted[scheme];
|
|
233
|
+
const markSize = largerText ? 30 : 24;
|
|
234
|
+
const glyph = largerText ? 16 : 13;
|
|
235
|
+
return (
|
|
236
|
+
<View
|
|
237
|
+
style={{
|
|
238
|
+
// Web `Card` (rounded-surface, 16) with `py-2 px-2.5` inside a 1 border + 1 ring.
|
|
239
|
+
borderRadius: tokens.radius.lg,
|
|
240
|
+
borderWidth: 2,
|
|
241
|
+
borderColor: selected ? palette.primary[scheme] : "transparent",
|
|
242
|
+
backgroundColor: selected ? tokens.color.background[scheme] : palette.default[scheme],
|
|
243
|
+
opacity: disabled ? 0.5 : 1,
|
|
244
|
+
paddingVertical: 8,
|
|
245
|
+
paddingHorizontal: 10,
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
<Pressable
|
|
249
|
+
accessibilityRole="button"
|
|
250
|
+
accessibilityLabel={label}
|
|
251
|
+
accessibilityState={{ selected, disabled }}
|
|
252
|
+
disabled={disabled}
|
|
253
|
+
onPress={onPress}
|
|
254
|
+
hitSlop={4}
|
|
255
|
+
className="flex-row"
|
|
256
|
+
style={{ gap: 12, alignItems: description ? "flex-start" : "center" }}
|
|
257
|
+
>
|
|
258
|
+
<View
|
|
259
|
+
style={{
|
|
260
|
+
width: markSize,
|
|
261
|
+
height: markSize,
|
|
262
|
+
// Web `rounded-md` (radius - 2 = 14 at the default 1rem) on a 24 box: round.
|
|
263
|
+
borderRadius: markSize / 2,
|
|
264
|
+
marginTop: description ? 2 : 0,
|
|
265
|
+
alignItems: "center",
|
|
266
|
+
justifyContent: "center",
|
|
267
|
+
backgroundColor: selected ? palette.primary[scheme] : palette.default[scheme],
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
{selected ? (
|
|
271
|
+
<IconCheck size={glyph} strokeWidth={3} color={palette.primaryForeground[scheme]} />
|
|
272
|
+
) : mark === "other" ? (
|
|
273
|
+
<IconPencil size={glyph} color={muted} />
|
|
274
|
+
) : (
|
|
275
|
+
<Text className="font-instrument-sans-semibold" style={[typeStyle("semibold"), { fontSize: largerText ? 14 : 11, color: muted, letterSpacing: 0.4 }]}>
|
|
276
|
+
{mark}
|
|
277
|
+
</Text>
|
|
278
|
+
)}
|
|
279
|
+
</View>
|
|
280
|
+
<View className="min-w-0 flex-1">
|
|
281
|
+
<Text
|
|
282
|
+
className="font-instrument-sans-medium"
|
|
283
|
+
style={[sizeStyle(chatTextSize("small", largerText), "snug"), typeStyle("medium"), { color: selected ? palette.primary[scheme] : muted }]}
|
|
284
|
+
>
|
|
285
|
+
{label}
|
|
286
|
+
</Text>
|
|
287
|
+
{description ? (
|
|
288
|
+
<Text className="font-instrument-sans" style={[sizeStyle(chatTextSize("caption", largerText)), { color: muted, marginTop: 2 }]}>
|
|
289
|
+
{description}
|
|
290
|
+
</Text>
|
|
291
|
+
) : null}
|
|
292
|
+
</View>
|
|
293
|
+
</Pressable>
|
|
294
|
+
{children}
|
|
295
|
+
</View>
|
|
296
|
+
);
|
|
297
|
+
}
|
package/src/ai/code-block.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ScrollView, Text, View } from "react-native";
|
|
3
3
|
import { IconCheck, IconCopy } from "@tabler/icons-react-native";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
5
|
import { sizeStyle, textStyle } from "../controlStyles";
|
|
6
|
+
import { GITHUB_CODE_COLORS, codeTokenColor, loadPrismReactRenderer } from "../highlight";
|
|
6
7
|
import { useLargerText } from "../theme";
|
|
7
8
|
import { tokens } from "../tokens";
|
|
8
9
|
import { useScheme } from "../useScheme";
|
|
@@ -54,20 +55,30 @@ export function CodeBlockCopyButton({ code, onCopy, timeout = 2000, label = "Cop
|
|
|
54
55
|
|
|
55
56
|
export type CodeBlockProps = {
|
|
56
57
|
code: string;
|
|
57
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Shown in the header, and the Prism language for highlighting (`tsx`,
|
|
60
|
+
* `python`, `json`…). An unknown language draws plain text.
|
|
61
|
+
*/
|
|
58
62
|
language?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Colour the code in the GitHub light and dark colours, as the web shiki
|
|
65
|
+
* block. Default `true`. Needs the optional peer `prism-react-renderer` and a
|
|
66
|
+
* `language`; without either the code is plain.
|
|
67
|
+
*/
|
|
68
|
+
highlight?: boolean;
|
|
59
69
|
/** With it, the header has a copy button. See `CodeBlockCopyButton`. */
|
|
60
70
|
onCopy?: (code: string) => void | Promise<void>;
|
|
61
71
|
className?: string;
|
|
62
72
|
};
|
|
63
73
|
|
|
64
74
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
75
|
+
* Code on the flat panel (web `flatPanelSubtle`: `bg-surface-secondary`, no
|
|
76
|
+
* border or shadow) at radius 16. A header with the language and a copy
|
|
67
77
|
* button when either is given. Long lines scroll sideways; the code is
|
|
68
|
-
* selectable.
|
|
78
|
+
* selectable. With `prism-react-renderer` installed and a `language`, tokens
|
|
79
|
+
* take the GitHub colours of the scheme.
|
|
69
80
|
*/
|
|
70
|
-
export function CodeBlock({ code, language, onCopy, className }: CodeBlockProps) {
|
|
81
|
+
export function CodeBlock({ code, language, highlight = true, onCopy, className }: CodeBlockProps) {
|
|
71
82
|
const scheme = useScheme();
|
|
72
83
|
const largerText = useLargerText();
|
|
73
84
|
const size = chatTextSize("small", largerText);
|
|
@@ -96,9 +107,41 @@ export function CodeBlock({ code, language, onCopy, className }: CodeBlockProps)
|
|
|
96
107
|
className={size}
|
|
97
108
|
style={[sizeStyle(size), textStyle("foreground", scheme), { fontFamily: MONO_FONT }]}
|
|
98
109
|
>
|
|
99
|
-
{code}
|
|
110
|
+
{highlight && language ? <HighlightedCode code={code} language={language} scheme={scheme} /> : code}
|
|
100
111
|
</Text>
|
|
101
112
|
</ScrollView>
|
|
102
113
|
</View>
|
|
103
114
|
);
|
|
104
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Prism tokens as nested `Text` spans, one colour each, lines joined by
|
|
119
|
+
* newlines. They inherit the size and font of the code `Text` around them.
|
|
120
|
+
* Without `prism-react-renderer` it is the plain code.
|
|
121
|
+
*/
|
|
122
|
+
function HighlightedCode({ code, language, scheme }: { code: string; language: string; scheme: "light" | "dark" }) {
|
|
123
|
+
const Highlight = loadPrismReactRenderer()?.Highlight;
|
|
124
|
+
if (!Highlight) return code;
|
|
125
|
+
return (
|
|
126
|
+
<Highlight
|
|
127
|
+
code={code.replace(/\n$/, "")}
|
|
128
|
+
language={language.toLowerCase()}
|
|
129
|
+
theme={{ plain: { color: GITHUB_CODE_COLORS[scheme].plain }, styles: [] }}
|
|
130
|
+
>
|
|
131
|
+
{({ tokens }) => (
|
|
132
|
+
<>
|
|
133
|
+
{tokens.map((line, lineIndex) => (
|
|
134
|
+
<Fragment key={lineIndex}>
|
|
135
|
+
{lineIndex > 0 ? "\n" : null}
|
|
136
|
+
{line.map((token, tokenIndex) => (
|
|
137
|
+
<Text key={tokenIndex} style={{ color: codeTokenColor(token.types, scheme) }}>
|
|
138
|
+
{token.content}
|
|
139
|
+
</Text>
|
|
140
|
+
))}
|
|
141
|
+
</Fragment>
|
|
142
|
+
))}
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
</Highlight>
|
|
146
|
+
);
|
|
147
|
+
}
|
package/src/ai/index.ts
CHANGED
|
@@ -34,14 +34,18 @@ export {
|
|
|
34
34
|
MessageContent,
|
|
35
35
|
MessageActionsBar,
|
|
36
36
|
MessageAction,
|
|
37
|
+
MessageMeta,
|
|
37
38
|
MESSAGE_ACTIONS,
|
|
38
39
|
userBubbleStyle,
|
|
40
|
+
tintBubbleStyle,
|
|
39
41
|
} from "./message";
|
|
40
42
|
export type {
|
|
41
43
|
MessageRole,
|
|
42
44
|
MessageProps,
|
|
43
45
|
MessageContentProps,
|
|
46
|
+
MessageBubble,
|
|
44
47
|
MessageActionsBarProps,
|
|
48
|
+
MessageMetaProps,
|
|
45
49
|
MessageActionKind,
|
|
46
50
|
MessageActionProps,
|
|
47
51
|
} from "./message";
|
|
@@ -62,6 +66,7 @@ export {
|
|
|
62
66
|
AgentTraceCollapsible,
|
|
63
67
|
AgentTraceCollapsibleTrigger,
|
|
64
68
|
AgentTraceCollapsibleContent,
|
|
69
|
+
AgentTraceFold,
|
|
65
70
|
traceLineColor,
|
|
66
71
|
} from "./trace";
|
|
67
72
|
export type {
|
|
@@ -70,6 +75,7 @@ export type {
|
|
|
70
75
|
AgentTraceIconProps,
|
|
71
76
|
AgentTraceTitleProps,
|
|
72
77
|
AgentTraceCollapsibleTriggerProps,
|
|
78
|
+
AgentTraceFoldProps,
|
|
73
79
|
} from "./trace";
|
|
74
80
|
export {
|
|
75
81
|
ChainOfThought,
|
|
@@ -121,3 +127,7 @@ export { CodeBlock, CodeBlockCopyButton } from "./code-block";
|
|
|
121
127
|
export type { CodeBlockProps, CodeBlockCopyButtonProps } from "./code-block";
|
|
122
128
|
export { ResponseText, ChatTextColorContext } from "./response-text";
|
|
123
129
|
export type { ResponseTextProps } from "./response-text";
|
|
130
|
+
export { ChoiceQuestion, choiceAnswers } from "./choice-question";
|
|
131
|
+
export type { ChoiceQuestionProps, ChoiceQuestionItem } from "./choice-question";
|
|
132
|
+
export { ActivityStepDetail } from "./activity-step-detail";
|
|
133
|
+
export type { ActivityStepDetailProps, ActivityStepDetailStep } from "./activity-step-detail";
|
package/src/ai/message.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createContext, useContext, type ComponentType } from "react";
|
|
2
|
-
import { View, type ViewProps } from "react-native";
|
|
1
|
+
import { createContext, useContext, type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import { Text, View, type ViewProps } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
IconCopy,
|
|
5
5
|
IconRefresh,
|
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
type IconProps,
|
|
9
9
|
} from "@tabler/icons-react-native";
|
|
10
10
|
import { cn } from "../cn";
|
|
11
|
-
import {
|
|
11
|
+
import { sizeStyle } from "../controlStyles";
|
|
12
|
+
import { palette, withAlpha } from "../palette";
|
|
13
|
+
import { useLargerText } from "../theme";
|
|
12
14
|
import { tokens } from "../tokens";
|
|
13
15
|
import { useScheme } from "../useScheme";
|
|
16
|
+
import { chatTextSize } from "./chat-text";
|
|
14
17
|
import { ChatIconButton } from "./icon-button";
|
|
15
18
|
import { ChatTextColorContext, ResponseText } from "./response-text";
|
|
16
19
|
|
|
@@ -56,24 +59,47 @@ export function userBubbleStyle(scheme: "light" | "dark") {
|
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
/**
|
|
63
|
+
* A user bubble's look. `primary` is the web default (`bg-primary` with the
|
|
64
|
+
* label colour). `tint` is the chat override Eva uses (`rounded-surface
|
|
65
|
+
* bg-primary/10 px-3 py-2 text-foreground`): a 10% wash of the primary colour
|
|
66
|
+
* with foreground text and every corner at 16.
|
|
67
|
+
*/
|
|
68
|
+
export type MessageBubble = "primary" | "tint";
|
|
69
|
+
|
|
70
|
+
/** The `tint` bubble: the primary colour at 10% alpha, radius 16 on every corner. */
|
|
71
|
+
export function tintBubbleStyle(scheme: "light" | "dark") {
|
|
72
|
+
return {
|
|
73
|
+
backgroundColor: withAlpha(palette.primary[scheme], 0.1),
|
|
74
|
+
borderRadius: tokens.radius.lg,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type MessageContentProps = ViewProps & {
|
|
79
|
+
/** User messages only. Default `"primary"`. */
|
|
80
|
+
bubble?: MessageBubble;
|
|
81
|
+
className?: string;
|
|
82
|
+
};
|
|
60
83
|
|
|
61
84
|
/**
|
|
62
|
-
* The message body. A user message is a filled bubble
|
|
63
|
-
* primary fill and label colour
|
|
64
|
-
*
|
|
65
|
-
*
|
|
85
|
+
* The message body. A user message is a filled bubble: by default `px-4 py-3`
|
|
86
|
+
* with the primary fill and label colour, or with `bubble="tint"` `px-3 py-2`
|
|
87
|
+
* on a 10% primary wash with the foreground colour. An assistant message has
|
|
88
|
+
* no fill and the foreground colour. A string child is drawn as
|
|
89
|
+
* `ResponseText`, and `ResponseText` inside takes the colour.
|
|
66
90
|
*/
|
|
67
|
-
export function MessageContent({ children, className, style, ...props }: MessageContentProps) {
|
|
91
|
+
export function MessageContent({ children, bubble = "primary", className, style, ...props }: MessageContentProps) {
|
|
68
92
|
const from = useContext(MessageContext);
|
|
69
93
|
const scheme = useScheme();
|
|
70
94
|
const user = from === "user";
|
|
71
|
-
const
|
|
95
|
+
const tint = user && bubble === "tint";
|
|
96
|
+
const color = user && !tint ? palette.primaryForeground[scheme] : tokens.color.foreground[scheme];
|
|
97
|
+
const bubbleStyle = !user ? null : tint ? tintBubbleStyle(scheme) : userBubbleStyle(scheme);
|
|
72
98
|
return (
|
|
73
99
|
<ChatTextColorContext.Provider value={color}>
|
|
74
100
|
<View
|
|
75
|
-
className={cn("min-w-0 max-w-full gap-2", user && "px-4 py-3", className)}
|
|
76
|
-
style={[
|
|
101
|
+
className={cn("min-w-0 max-w-full gap-2", user && (tint ? "px-3 py-2" : "px-4 py-3"), className)}
|
|
102
|
+
style={[bubbleStyle, style]}
|
|
77
103
|
{...props}
|
|
78
104
|
>
|
|
79
105
|
{typeof children === "string" ? <ResponseText>{children}</ResponseText> : children}
|
|
@@ -82,6 +108,27 @@ export function MessageContent({ children, className, style, ...props }: Message
|
|
|
82
108
|
);
|
|
83
109
|
}
|
|
84
110
|
|
|
111
|
+
export type MessageMetaProps = { children: ReactNode; className?: string };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The muted clock and duration after a message's actions, as Eva's web chat
|
|
115
|
+
* (`text-2xs tabular-nums text-muted-foreground/60`, "10:21 PM · 1m"): the
|
|
116
|
+
* caption size on phones, the muted colour at 60%, tabular figures.
|
|
117
|
+
*/
|
|
118
|
+
export function MessageMeta({ children, className }: MessageMetaProps) {
|
|
119
|
+
const scheme = useScheme();
|
|
120
|
+
const largerText = useLargerText();
|
|
121
|
+
const size = chatTextSize("caption", largerText);
|
|
122
|
+
return (
|
|
123
|
+
<Text
|
|
124
|
+
className={cn(size, "font-instrument-sans", className)}
|
|
125
|
+
style={[sizeStyle(size), { color: withAlpha(tokens.color.muted[scheme], 0.6), fontVariant: ["tabular-nums"] }]}
|
|
126
|
+
>
|
|
127
|
+
{children}
|
|
128
|
+
</Text>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
export type MessageActionsBarProps = ViewProps & { className?: string };
|
|
86
133
|
|
|
87
134
|
/** A row of `MessageAction`s under a message, as the web `flex items-center gap-1`. */
|
package/src/ai/prompt-input.tsx
CHANGED
|
@@ -93,6 +93,15 @@ export type PromptInputProps = {
|
|
|
93
93
|
header?: ReactNode;
|
|
94
94
|
/** Before the send button, such as `PromptInputButton`s. */
|
|
95
95
|
toolbar?: ReactNode;
|
|
96
|
+
/**
|
|
97
|
+
* `stacked` (default): the field over a footer of tools and send, the web
|
|
98
|
+
* `PromptInput`. `inline`: one row, as the web `Composer layout="inline"`
|
|
99
|
+
* (Eva's chat): `leading`, the field, `toolbar`, then send, in a 20-radius
|
|
100
|
+
* field with `p-1.5`, 44 high on one line.
|
|
101
|
+
*/
|
|
102
|
+
layout?: "stacked" | "inline";
|
|
103
|
+
/** `inline` only: before the field, such as a + (attach) `PromptInputButton`. */
|
|
104
|
+
leading?: ReactNode;
|
|
96
105
|
disabled?: boolean;
|
|
97
106
|
/** Pad the bottom safe-area inset (home indicator). Default `true`; turn it off when a tab bar below already does. */
|
|
98
107
|
safeBottom?: boolean;
|
|
@@ -102,6 +111,9 @@ export type PromptInputProps = {
|
|
|
102
111
|
testID?: string;
|
|
103
112
|
};
|
|
104
113
|
|
|
114
|
+
/** The web `Composer` inline radius, `rounded-[1.25rem]`. */
|
|
115
|
+
const INLINE_RADIUS = 20;
|
|
116
|
+
|
|
105
117
|
/** The field grows to this height, then scrolls: about 6 lines of `text-base` (24 each, with `py-2.5`), and about 8 of `text-xl` with larger text. */
|
|
106
118
|
const MAX_FIELD_HEIGHT = { regular: 160, larger: 240 } as const;
|
|
107
119
|
|
|
@@ -124,6 +136,8 @@ export function PromptInput({
|
|
|
124
136
|
accessibilityLabel = "Message",
|
|
125
137
|
header,
|
|
126
138
|
toolbar,
|
|
139
|
+
layout = "stacked",
|
|
140
|
+
leading,
|
|
127
141
|
disabled = false,
|
|
128
142
|
safeBottom = true,
|
|
129
143
|
keyboardVerticalOffset = 0,
|
|
@@ -156,35 +170,72 @@ export function PromptInput({
|
|
|
156
170
|
className={cn("pt-2", className)}
|
|
157
171
|
style={{ paddingBottom: bottom + 8, paddingLeft: insets.left + 12, paddingRight: insets.right + 12 }}
|
|
158
172
|
>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
{layout === "inline" ? (
|
|
174
|
+
// Web `Composer` inline: `rounded-[1.25rem]` and a `p-1.5` row, items at the bottom as the field grows.
|
|
175
|
+
<View className="border" style={[fieldStyle(scheme), { borderRadius: INLINE_RADIUS }]}>
|
|
176
|
+
{header ? <View className="flex-row flex-wrap gap-1 px-1.5 pt-1.5">{header}</View> : null}
|
|
177
|
+
<View className="flex-row items-end gap-1 p-1.5">
|
|
178
|
+
{leading ? <View className="shrink-0 flex-row items-center gap-1">{leading}</View> : null}
|
|
179
|
+
<FieldInput
|
|
180
|
+
multiline
|
|
181
|
+
value={text}
|
|
182
|
+
onChangeText={setText}
|
|
183
|
+
placeholder={placeholder}
|
|
184
|
+
accessibilityLabel={accessibilityLabel}
|
|
185
|
+
editable={!disabled}
|
|
186
|
+
textAlignVertical="center"
|
|
187
|
+
// One line to start (a multiline field is two rows on react-native-web); it grows from there.
|
|
188
|
+
numberOfLines={1}
|
|
189
|
+
className={cn(
|
|
190
|
+
"min-h-8 flex-1 border-0 px-2 py-1 text-base font-instrument-sans",
|
|
191
|
+
largerText && "min-h-11 py-2 text-xl font-instrument-sans-medium",
|
|
192
|
+
)}
|
|
193
|
+
style={[
|
|
194
|
+
fieldTextStyle(scheme),
|
|
195
|
+
largerText ? typeStyle("medium") : null,
|
|
196
|
+
{
|
|
197
|
+
backgroundColor: "transparent",
|
|
198
|
+
maxHeight: largerText ? MAX_FIELD_HEIGHT.larger : MAX_FIELD_HEIGHT.regular,
|
|
199
|
+
},
|
|
200
|
+
]}
|
|
201
|
+
/>
|
|
202
|
+
<View className="shrink-0 flex-row items-center gap-1">
|
|
203
|
+
{toolbar}
|
|
204
|
+
<PromptInputSubmit status={status} onSubmit={submit} onStop={onStop} canSubmit={canSubmit} />
|
|
205
|
+
</View>
|
|
206
|
+
</View>
|
|
207
|
+
</View>
|
|
208
|
+
) : (
|
|
209
|
+
<View className="border" style={fieldStyle(scheme)}>
|
|
210
|
+
{header ? <View className="flex-row flex-wrap gap-1 px-2 pt-2">{header}</View> : null}
|
|
211
|
+
{/* The `Textarea` field without its `numberOfLines` (default 4), which on Android sets the line count: this one starts at one line and grows. */}
|
|
212
|
+
<FieldInput
|
|
213
|
+
multiline
|
|
214
|
+
value={text}
|
|
215
|
+
onChangeText={setText}
|
|
216
|
+
placeholder={placeholder}
|
|
217
|
+
accessibilityLabel={accessibilityLabel}
|
|
218
|
+
editable={!disabled}
|
|
219
|
+
textAlignVertical="top"
|
|
220
|
+
className={cn(
|
|
221
|
+
"min-h-11 border-0 px-3.5 py-2.5 text-base font-instrument-sans",
|
|
222
|
+
largerText && "min-h-14 px-4 py-3 text-xl font-instrument-sans-medium",
|
|
223
|
+
)}
|
|
224
|
+
style={[
|
|
225
|
+
fieldTextStyle(scheme),
|
|
226
|
+
largerText ? typeStyle("medium") : null,
|
|
227
|
+
{
|
|
228
|
+
backgroundColor: "transparent",
|
|
229
|
+
maxHeight: largerText ? MAX_FIELD_HEIGHT.larger : MAX_FIELD_HEIGHT.regular,
|
|
230
|
+
},
|
|
231
|
+
]}
|
|
232
|
+
/>
|
|
233
|
+
<View className="flex-row items-center justify-between gap-1 px-2 pb-2">
|
|
234
|
+
<View className="min-w-0 shrink flex-row items-center gap-1">{toolbar}</View>
|
|
235
|
+
<PromptInputSubmit status={status} onSubmit={submit} onStop={onStop} canSubmit={canSubmit} />
|
|
236
|
+
</View>
|
|
186
237
|
</View>
|
|
187
|
-
|
|
238
|
+
)}
|
|
188
239
|
</View>
|
|
189
240
|
</KeyboardAvoidingView>
|
|
190
241
|
);
|