@vvedantb/native-ui 0.3.16 → 0.4.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.
@@ -0,0 +1,233 @@
1
+ // Reference screen for the AI chat parts (`@vvedantb/native-ui/ai`). Render
2
+ // `AiChatGallery` in a full-screen Expo route, with the same NativeWind setup
3
+ // as the rest of the package and a `SafeAreaProvider` above it. The thread is
4
+ // fixed sample text, not a model. It follows the app's larger-text setting.
5
+ // `ParityGallery` shows `AiChatSamples` in its "AI chat" section.
6
+ import { useState } from "react";
7
+ import { View } from "react-native";
8
+ import { IconFileText, IconPaperclip, IconSearch } from "@tabler/icons-react-native";
9
+ import {
10
+ ActivityStep,
11
+ AgentTrace,
12
+ AgentTraceCollapsible,
13
+ AgentTraceCollapsibleContent,
14
+ AgentTraceCollapsibleTrigger,
15
+ AgentTraceDescription,
16
+ AgentTraceIcon,
17
+ AgentTraceLine,
18
+ AgentTraceTitle,
19
+ Attachment,
20
+ Attachments,
21
+ CodeBlock,
22
+ Conversation,
23
+ ConversationContent,
24
+ ConversationScrollButton,
25
+ FileChip,
26
+ Message,
27
+ MessageAction,
28
+ MessageActionsBar,
29
+ MessageContent,
30
+ PromptInput,
31
+ PromptInputButton,
32
+ Reasoning,
33
+ ReasoningContent,
34
+ ReasoningTrigger,
35
+ ResponseText,
36
+ Source,
37
+ Sources,
38
+ SourcesContent,
39
+ SourcesTrigger,
40
+ Suggestion,
41
+ Suggestions,
42
+ formatElapsed,
43
+ useStreamingDisclosure,
44
+ type AttachmentData,
45
+ type ChatStatus,
46
+ } from "../ai";
47
+
48
+ const SUGGESTIONS = [
49
+ "Summarise this thread",
50
+ "Draft a reply",
51
+ "List the next steps",
52
+ "Explain it more simply",
53
+ ];
54
+
55
+ const SAMPLE_ATTACHMENTS: AttachmentData[] = [
56
+ { id: "notes", filename: "meeting-notes.pdf", mediaType: "application/pdf" },
57
+ { id: "sketch", filename: "sketch.png", mediaType: "image/png" },
58
+ ];
59
+
60
+ const FINISHED_REASONING =
61
+ "The question is how to keep one list the same on two devices.\nCompare a shared server copy with device-to-device sync, then suggest the simpler one.";
62
+
63
+ const FINISHED_REPLY =
64
+ "Keep one copy of the list on a server and have each device read and write it.\n\nWhen a device changes an item, it sends the change with a timestamp. The other device fetches changes it has not seen.";
65
+
66
+ const SAMPLE_CODE = `type Change = {\n itemId: string;\n updatedAt: number;\n};`;
67
+
68
+ const STREAMING_REASONING =
69
+ "Offline edits need to wait in a queue on the device.\nWhen the connection returns, send the queue in order and merge by timestamp…";
70
+
71
+ /**
72
+ * The sample thread: a user message, a finished assistant message (folded
73
+ * reasoning and trace, reply, code, sources and actions), a second user
74
+ * message, and an assistant message that is still working while `streaming`.
75
+ */
76
+ export function AiChatSamples({ streaming = true }: { streaming?: boolean }) {
77
+ const [feedback, setFeedback] = useState<"like" | "dislike" | null>(null);
78
+ const searchFold = useStreamingDisclosure(streaming);
79
+ const toggle = (kind: "like" | "dislike") => setFeedback(feedback === kind ? null : kind);
80
+
81
+ return (
82
+ <>
83
+ <Message from="user">
84
+ <MessageContent>How do I keep a list in sync across two devices?</MessageContent>
85
+ </Message>
86
+
87
+ <Message from="assistant">
88
+ <MessageContent>
89
+ <Reasoning duration={8}>
90
+ <ReasoningTrigger />
91
+ <ReasoningContent>{FINISHED_REASONING}</ReasoningContent>
92
+ </Reasoning>
93
+ <AgentTraceCollapsible>
94
+ <AgentTraceCollapsibleTrigger>
95
+ <AgentTraceLine>
96
+ <AgentTraceIcon icon={IconSearch} />
97
+ <AgentTraceTitle>Used 2 tools</AgentTraceTitle>
98
+ <AgentTraceDescription>{`Worked for ${formatElapsed(14)}`}</AgentTraceDescription>
99
+ </AgentTraceLine>
100
+ </AgentTraceCollapsibleTrigger>
101
+ <AgentTraceCollapsibleContent>
102
+ <AgentTrace>
103
+ <ActivityStep icon={IconSearch} label="Searched the web" description="5 results" />
104
+ <AgentTraceLine>
105
+ <AgentTraceIcon icon={IconFileText} />
106
+ <AgentTraceTitle>Read</AgentTraceTitle>
107
+ <FileChip name="sync-notes.md" />
108
+ </AgentTraceLine>
109
+ </AgentTrace>
110
+ </AgentTraceCollapsibleContent>
111
+ </AgentTraceCollapsible>
112
+ <ResponseText>{FINISHED_REPLY}</ResponseText>
113
+ {/* The gallery has no clipboard: the button only shows its copied state. */}
114
+ <CodeBlock language="ts" code={SAMPLE_CODE} onCopy={() => undefined} />
115
+ <Sources>
116
+ <SourcesTrigger count={2} />
117
+ <SourcesContent>
118
+ <Source href="https://example.com/sync" title="Syncing data between devices" />
119
+ <Source href="https://example.com/timestamps" title="Merging changes by timestamp" />
120
+ </SourcesContent>
121
+ </Sources>
122
+ </MessageContent>
123
+ <MessageActionsBar>
124
+ <MessageAction kind="copy" />
125
+ <MessageAction kind="retry" />
126
+ <MessageAction kind="like" selected={feedback === "like"} onPress={() => toggle("like")} />
127
+ <MessageAction kind="dislike" selected={feedback === "dislike"} onPress={() => toggle("dislike")} />
128
+ </MessageActionsBar>
129
+ </Message>
130
+
131
+ <Message from="user">
132
+ <MessageContent>And when one device is offline?</MessageContent>
133
+ </Message>
134
+
135
+ <Message from="assistant">
136
+ <MessageContent>
137
+ <Reasoning isStreaming={streaming}>
138
+ <ReasoningTrigger />
139
+ <ReasoningContent>{STREAMING_REASONING}</ReasoningContent>
140
+ </Reasoning>
141
+ <AgentTraceCollapsible {...searchFold}>
142
+ <AgentTraceCollapsibleTrigger>
143
+ <AgentTraceLine>
144
+ <AgentTraceIcon icon={IconSearch} />
145
+ <AgentTraceTitle shimmer={streaming}>
146
+ {streaming ? "Searching for offline sync" : "Searched for offline sync"}
147
+ </AgentTraceTitle>
148
+ </AgentTraceLine>
149
+ </AgentTraceCollapsibleTrigger>
150
+ <AgentTraceCollapsibleContent>
151
+ Offline-first sync, change queues, conflict handling
152
+ </AgentTraceCollapsibleContent>
153
+ </AgentTraceCollapsible>
154
+ <ActivityStep
155
+ icon={IconFileText}
156
+ label={streaming ? "Reading sync-notes.md" : "Read sync-notes.md"}
157
+ status={streaming ? "running" : "complete"}
158
+ description={streaming ? undefined : `Worked for ${formatElapsed(6)}`}
159
+ />
160
+ {streaming ? null : (
161
+ <ResponseText>
162
+ {"Queue each change on the device while it is offline.\n\nWhen it reconnects, send the queue in order. If both devices changed the same item, keep the newer change."}
163
+ </ResponseText>
164
+ )}
165
+ </MessageContent>
166
+ </Message>
167
+ </>
168
+ );
169
+ }
170
+
171
+ /**
172
+ * A full chat screen: the sample thread in a `Conversation` with its scroll
173
+ * button, suggestions, and the composer with sample attachments and an
174
+ * attach tool. The last message streams until Stop is pressed; then sent
175
+ * messages are added to the thread.
176
+ */
177
+ export function AiChatGallery() {
178
+ const [status, setStatus] = useState<ChatStatus>("streaming");
179
+ const [draft, setDraft] = useState("");
180
+ const [sent, setSent] = useState<string[]>([]);
181
+ const [attachments, setAttachments] = useState(SAMPLE_ATTACHMENTS);
182
+
183
+ return (
184
+ <View className="flex-1">
185
+ <Conversation>
186
+ <ConversationContent>
187
+ <AiChatSamples streaming={status === "streaming"} />
188
+ {sent.map((text, index) => (
189
+ <Message key={`${index}-${text}`} from="user">
190
+ <MessageContent>{text}</MessageContent>
191
+ </Message>
192
+ ))}
193
+ </ConversationContent>
194
+ <ConversationScrollButton />
195
+ </Conversation>
196
+ <Suggestions className="pt-2" contentContainerClassName="px-3">
197
+ {SUGGESTIONS.map((suggestion) => (
198
+ <Suggestion key={suggestion} suggestion={suggestion} onPress={setDraft} />
199
+ ))}
200
+ </Suggestions>
201
+ <PromptInput
202
+ value={draft}
203
+ onChangeText={setDraft}
204
+ status={status}
205
+ onStop={() => setStatus("ready")}
206
+ onSubmit={(text) => {
207
+ setSent([...sent, text]);
208
+ setAttachments([]);
209
+ }}
210
+ header={
211
+ attachments.length > 0 ? (
212
+ <Attachments>
213
+ {attachments.map((attachment) => (
214
+ <Attachment
215
+ key={attachment.id}
216
+ data={attachment}
217
+ onRemove={() => setAttachments(attachments.filter((item) => item.id !== attachment.id))}
218
+ />
219
+ ))}
220
+ </Attachments>
221
+ ) : null
222
+ }
223
+ toolbar={
224
+ <PromptInputButton
225
+ icon={IconPaperclip}
226
+ label="Attach files"
227
+ onPress={() => setAttachments(SAMPLE_ATTACHMENTS)}
228
+ />
229
+ }
230
+ />
231
+ </View>
232
+ );
233
+ }
@@ -85,6 +85,18 @@ import {
85
85
  } from "../index";
86
86
  import { typeStyle } from "../controlStyles";
87
87
  import { useScheme } from "../useScheme";
88
+ import {
89
+ ChainOfThought,
90
+ ChainOfThoughtContent,
91
+ ChainOfThoughtHeader,
92
+ ChainOfThoughtSearchResult,
93
+ ChainOfThoughtSearchResults,
94
+ ChainOfThoughtStep,
95
+ ConversationEmptyState,
96
+ Suggestion,
97
+ Suggestions,
98
+ } from "../ai";
99
+ import { AiChatSamples } from "./AiChatGallery";
88
100
 
89
101
  const BUTTON_VARIANTS: ButtonVariant[] = [
90
102
  "default",
@@ -502,6 +514,29 @@ function GalleryScreen() {
502
514
  <EmptyState.Description>New messages land here.</EmptyState.Description>
503
515
  </EmptyState.Root>
504
516
  </Section>
517
+
518
+ {/* The full chat screen, with the composer and keyboard, is `AiChatGallery` (`@vvedantb/native-ui/ai-gallery`). */}
519
+ <Section title="AI chat">
520
+ <AiChatSamples />
521
+ <ChainOfThought defaultOpen>
522
+ <ChainOfThoughtHeader />
523
+ <ChainOfThoughtContent>
524
+ <ChainOfThoughtStep label="Read the question" />
525
+ <ChainOfThoughtStep label="Search for sync options" status="active">
526
+ <ChainOfThoughtSearchResults>
527
+ <ChainOfThoughtSearchResult>example.com</ChainOfThoughtSearchResult>
528
+ <ChainOfThoughtSearchResult>example.org</ChainOfThoughtSearchResult>
529
+ </ChainOfThoughtSearchResults>
530
+ </ChainOfThoughtStep>
531
+ <ChainOfThoughtStep label="Write the answer" status="pending" />
532
+ </ChainOfThoughtContent>
533
+ </ChainOfThought>
534
+ <Suggestions>
535
+ <Suggestion suggestion="Summarise this thread" onPress={(text) => toast(text)} />
536
+ <Suggestion suggestion="Draft a reply" onPress={(text) => toast(text)} />
537
+ </Suggestions>
538
+ <ConversationEmptyState className="flex-none" />
539
+ </Section>
505
540
  </ScrollView>
506
541
 
507
542
  <PricingModal