@10play/expo-air 0.10.0 → 0.10.2
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/LICENSE +661 -0
- package/cli/dist/server/promptServer.d.ts +7 -0
- package/cli/dist/server/promptServer.d.ts.map +1 -1
- package/cli/dist/server/promptServer.js +165 -30
- package/cli/dist/server/promptServer.js.map +1 -1
- package/cli/dist/types/messages.d.ts +1 -0
- package/cli/dist/types/messages.d.ts.map +1 -1
- package/ios/WidgetBridge.h +2 -1
- package/ios/WidgetBridge.mm +72 -3
- package/ios/widget.jsbundle +40 -8
- package/package.json +1 -1
- package/plugin/build/index.js +12 -0
- package/widget/BubbleContent.tsx +31 -5
- package/widget/components/PromptInput.tsx +214 -39
- package/widget/components/ResponseArea.tsx +23 -2
- package/widget/package.json +1 -0
- package/widget/services/websocket.ts +13 -2
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
import React, { useState, useRef, useImperativeHandle, forwardRef } from "react";
|
|
1
|
+
import React, { useState, useRef, useImperativeHandle, forwardRef, useEffect } from "react";
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
TextInput,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
StyleSheet,
|
|
7
|
+
Image,
|
|
8
|
+
ScrollView,
|
|
9
|
+
Alert,
|
|
10
|
+
NativeModules,
|
|
11
|
+
NativeEventEmitter,
|
|
7
12
|
} from "react-native";
|
|
13
|
+
import * as ImagePicker from "expo-image-picker";
|
|
8
14
|
import { SPACING, LAYOUT, COLORS, TYPOGRAPHY, SIZES } from "../constants/design";
|
|
15
|
+
import type { ImageAttachment } from "../services/websocket";
|
|
9
16
|
|
|
10
17
|
export interface PromptInputHandle {
|
|
11
18
|
focus: () => void;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
interface PromptInputProps {
|
|
15
|
-
onSubmit: (prompt: string) => void;
|
|
22
|
+
onSubmit: (prompt: string, images?: ImageAttachment[]) => void;
|
|
16
23
|
onStop?: () => void;
|
|
17
24
|
disabled?: boolean;
|
|
18
25
|
isProcessing?: boolean;
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
const MAX_IMAGES = 4;
|
|
29
|
+
|
|
21
30
|
export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(({
|
|
22
31
|
onSubmit,
|
|
23
32
|
onStop,
|
|
@@ -25,61 +34,149 @@ export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(({
|
|
|
25
34
|
isProcessing = false,
|
|
26
35
|
}, ref) => {
|
|
27
36
|
const [text, setText] = useState("");
|
|
37
|
+
const [images, setImages] = useState<ImageAttachment[]>([]);
|
|
28
38
|
const inputRef = useRef<TextInput>(null);
|
|
29
39
|
|
|
30
40
|
useImperativeHandle(ref, () => ({
|
|
31
41
|
focus: () => inputRef.current?.focus(),
|
|
32
42
|
}));
|
|
33
43
|
|
|
44
|
+
// Listen for native image paste events (UITextView paste: swizzle)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const emitter = new NativeEventEmitter(NativeModules.WidgetBridge);
|
|
47
|
+
const subscription = emitter.addListener('onClipboardImagePaste', (image: ImageAttachment) => {
|
|
48
|
+
setImages((prev) => {
|
|
49
|
+
if (prev.length >= MAX_IMAGES) return prev;
|
|
50
|
+
return [...prev, image];
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return () => subscription.remove();
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
34
56
|
const handleSubmit = () => {
|
|
35
57
|
const trimmed = text.trim();
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
const hasContent = trimmed.length > 0 || images.length > 0;
|
|
59
|
+
if (hasContent && !disabled && !isProcessing) {
|
|
60
|
+
onSubmit(trimmed, images.length > 0 ? images : undefined);
|
|
38
61
|
setText("");
|
|
62
|
+
setImages([]);
|
|
39
63
|
}
|
|
40
64
|
};
|
|
41
65
|
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
const handlePickImages = async () => {
|
|
67
|
+
if (images.length >= MAX_IMAGES) {
|
|
68
|
+
Alert.alert("Limit reached", `Maximum ${MAX_IMAGES} images per message.`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
74
|
+
mediaTypes: ["images"],
|
|
75
|
+
allowsMultipleSelection: true,
|
|
76
|
+
selectionLimit: MAX_IMAGES - images.length,
|
|
77
|
+
quality: 0.7,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.canceled && result.assets.length > 0) {
|
|
81
|
+
const picked: ImageAttachment[] = result.assets.map((asset) => ({
|
|
82
|
+
uri: asset.uri,
|
|
83
|
+
width: asset.width,
|
|
84
|
+
height: asset.height,
|
|
85
|
+
}));
|
|
86
|
+
setImages((prev) => [...prev, ...picked].slice(0, MAX_IMAGES));
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.warn("[expo-air] Image picker error:", e);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const removeImage = (index: number) => {
|
|
94
|
+
setImages((prev) => prev.filter((_, i) => i !== index));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const canSubmit = (text.trim().length > 0 || images.length > 0) && !disabled && !isProcessing;
|
|
44
98
|
|
|
45
99
|
return (
|
|
46
|
-
<View style={styles.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
onChangeText={setText}
|
|
54
|
-
onSubmitEditing={handleSubmit}
|
|
55
|
-
editable={!isProcessing}
|
|
56
|
-
multiline
|
|
57
|
-
maxLength={2000}
|
|
58
|
-
returnKeyType="send"
|
|
59
|
-
blurOnSubmit
|
|
60
|
-
/>
|
|
61
|
-
{isProcessing ? (
|
|
62
|
-
<TouchableOpacity
|
|
63
|
-
style={[styles.submitButton, styles.stopButton]}
|
|
64
|
-
onPress={onStop}
|
|
65
|
-
activeOpacity={0.7}
|
|
100
|
+
<View style={styles.outerContainer}>
|
|
101
|
+
{images.length > 0 && (
|
|
102
|
+
<ScrollView
|
|
103
|
+
horizontal
|
|
104
|
+
showsHorizontalScrollIndicator={false}
|
|
105
|
+
style={styles.previewStrip}
|
|
106
|
+
contentContainerStyle={styles.previewContent}
|
|
66
107
|
>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
{images.map((img, index) => (
|
|
109
|
+
<View key={index} style={styles.previewItem}>
|
|
110
|
+
<Image source={{ uri: img.uri }} style={styles.previewImage} />
|
|
111
|
+
<TouchableOpacity
|
|
112
|
+
style={styles.removeButton}
|
|
113
|
+
onPress={() => removeImage(index)}
|
|
114
|
+
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
|
|
115
|
+
>
|
|
116
|
+
<View style={styles.removeIcon}>
|
|
117
|
+
<View style={[styles.removeLine, styles.removeLineA]} />
|
|
118
|
+
<View style={[styles.removeLine, styles.removeLineB]} />
|
|
119
|
+
</View>
|
|
120
|
+
</TouchableOpacity>
|
|
121
|
+
</View>
|
|
122
|
+
))}
|
|
123
|
+
</ScrollView>
|
|
124
|
+
)}
|
|
125
|
+
<View style={styles.container}>
|
|
70
126
|
<TouchableOpacity
|
|
71
|
-
style={
|
|
72
|
-
onPress={
|
|
73
|
-
disabled={
|
|
74
|
-
activeOpacity={0.
|
|
127
|
+
style={styles.iconButton}
|
|
128
|
+
onPress={handlePickImages}
|
|
129
|
+
disabled={isProcessing}
|
|
130
|
+
activeOpacity={0.6}
|
|
75
131
|
>
|
|
76
|
-
<
|
|
132
|
+
<PlusIcon />
|
|
77
133
|
</TouchableOpacity>
|
|
78
|
-
|
|
134
|
+
<TextInput
|
|
135
|
+
ref={inputRef}
|
|
136
|
+
style={styles.input}
|
|
137
|
+
placeholder="Ask Claude..."
|
|
138
|
+
placeholderTextColor={COLORS.TEXT_TERTIARY}
|
|
139
|
+
value={text}
|
|
140
|
+
onChangeText={setText}
|
|
141
|
+
onSubmitEditing={handleSubmit}
|
|
142
|
+
editable={!isProcessing}
|
|
143
|
+
multiline
|
|
144
|
+
maxLength={2000}
|
|
145
|
+
returnKeyType="send"
|
|
146
|
+
blurOnSubmit
|
|
147
|
+
/>
|
|
148
|
+
{isProcessing ? (
|
|
149
|
+
<TouchableOpacity
|
|
150
|
+
style={[styles.submitButton, styles.stopButton]}
|
|
151
|
+
onPress={onStop}
|
|
152
|
+
activeOpacity={0.7}
|
|
153
|
+
>
|
|
154
|
+
<StopIcon />
|
|
155
|
+
</TouchableOpacity>
|
|
156
|
+
) : (
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
style={[styles.submitButton, !canSubmit && styles.submitButtonDisabled]}
|
|
159
|
+
onPress={handleSubmit}
|
|
160
|
+
disabled={!canSubmit}
|
|
161
|
+
activeOpacity={0.7}
|
|
162
|
+
>
|
|
163
|
+
<ArrowIcon />
|
|
164
|
+
</TouchableOpacity>
|
|
165
|
+
)}
|
|
166
|
+
</View>
|
|
79
167
|
</View>
|
|
80
168
|
);
|
|
81
169
|
});
|
|
82
170
|
|
|
171
|
+
function PlusIcon() {
|
|
172
|
+
return (
|
|
173
|
+
<View style={styles.plusIcon}>
|
|
174
|
+
<View style={styles.plusH} />
|
|
175
|
+
<View style={styles.plusV} />
|
|
176
|
+
</View>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
83
180
|
function ArrowIcon() {
|
|
84
181
|
return (
|
|
85
182
|
<View style={styles.arrowIcon}>
|
|
@@ -94,14 +191,71 @@ function StopIcon() {
|
|
|
94
191
|
}
|
|
95
192
|
|
|
96
193
|
const styles = StyleSheet.create({
|
|
194
|
+
outerContainer: {
|
|
195
|
+
borderTopWidth: 1,
|
|
196
|
+
borderTopColor: COLORS.BORDER,
|
|
197
|
+
backgroundColor: COLORS.BACKGROUND,
|
|
198
|
+
},
|
|
199
|
+
previewStrip: {
|
|
200
|
+
maxHeight: 72,
|
|
201
|
+
paddingHorizontal: LAYOUT.CONTENT_PADDING_H,
|
|
202
|
+
paddingTop: SPACING.SM,
|
|
203
|
+
},
|
|
204
|
+
previewContent: {
|
|
205
|
+
gap: SPACING.SM,
|
|
206
|
+
alignItems: "center",
|
|
207
|
+
},
|
|
208
|
+
previewItem: {
|
|
209
|
+
position: "relative",
|
|
210
|
+
},
|
|
211
|
+
previewImage: {
|
|
212
|
+
width: 56,
|
|
213
|
+
height: 56,
|
|
214
|
+
borderRadius: SPACING.SM,
|
|
215
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
216
|
+
},
|
|
217
|
+
removeButton: {
|
|
218
|
+
position: "absolute",
|
|
219
|
+
top: -3,
|
|
220
|
+
right: -3,
|
|
221
|
+
width: 18,
|
|
222
|
+
height: 18,
|
|
223
|
+
borderRadius: 9,
|
|
224
|
+
backgroundColor: "#1C1C1E",
|
|
225
|
+
alignItems: "center",
|
|
226
|
+
justifyContent: "center",
|
|
227
|
+
},
|
|
228
|
+
removeIcon: {
|
|
229
|
+
width: 8,
|
|
230
|
+
height: 8,
|
|
231
|
+
alignItems: "center",
|
|
232
|
+
justifyContent: "center",
|
|
233
|
+
},
|
|
234
|
+
removeLine: {
|
|
235
|
+
position: "absolute",
|
|
236
|
+
width: 9,
|
|
237
|
+
height: 1.2,
|
|
238
|
+
backgroundColor: "rgba(255,255,255,0.9)",
|
|
239
|
+
borderRadius: 1,
|
|
240
|
+
},
|
|
241
|
+
removeLineA: {
|
|
242
|
+
transform: [{ rotate: "45deg" }],
|
|
243
|
+
},
|
|
244
|
+
removeLineB: {
|
|
245
|
+
transform: [{ rotate: "-45deg" }],
|
|
246
|
+
},
|
|
97
247
|
container: {
|
|
98
248
|
flexDirection: "row",
|
|
99
249
|
alignItems: "flex-end",
|
|
100
250
|
paddingHorizontal: LAYOUT.CONTENT_PADDING_H,
|
|
101
251
|
paddingVertical: SPACING.MD,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
252
|
+
},
|
|
253
|
+
iconButton: {
|
|
254
|
+
width: 36,
|
|
255
|
+
height: SIZES.SUBMIT_BUTTON,
|
|
256
|
+
alignItems: "center",
|
|
257
|
+
justifyContent: "center",
|
|
258
|
+
marginRight: 4,
|
|
105
259
|
},
|
|
106
260
|
input: {
|
|
107
261
|
flex: 1,
|
|
@@ -120,7 +274,7 @@ const styles = StyleSheet.create({
|
|
|
120
274
|
backgroundColor: COLORS.BACKGROUND_BUTTON,
|
|
121
275
|
alignItems: "center",
|
|
122
276
|
justifyContent: "center",
|
|
123
|
-
marginLeft: SPACING.SM + 2,
|
|
277
|
+
marginLeft: SPACING.SM + 2,
|
|
124
278
|
},
|
|
125
279
|
submitButtonDisabled: {
|
|
126
280
|
opacity: 0.4,
|
|
@@ -158,4 +312,25 @@ const styles = StyleSheet.create({
|
|
|
158
312
|
borderRightColor: "transparent",
|
|
159
313
|
borderBottomColor: COLORS.TEXT_PRIMARY,
|
|
160
314
|
},
|
|
315
|
+
// Plus icon
|
|
316
|
+
plusIcon: {
|
|
317
|
+
width: 18,
|
|
318
|
+
height: 18,
|
|
319
|
+
alignItems: "center",
|
|
320
|
+
justifyContent: "center",
|
|
321
|
+
},
|
|
322
|
+
plusH: {
|
|
323
|
+
position: "absolute",
|
|
324
|
+
width: 16,
|
|
325
|
+
height: 2,
|
|
326
|
+
borderRadius: 1,
|
|
327
|
+
backgroundColor: COLORS.TEXT_MUTED,
|
|
328
|
+
},
|
|
329
|
+
plusV: {
|
|
330
|
+
position: "absolute",
|
|
331
|
+
width: 2,
|
|
332
|
+
height: 16,
|
|
333
|
+
borderRadius: 1,
|
|
334
|
+
backgroundColor: COLORS.TEXT_MUTED,
|
|
335
|
+
},
|
|
161
336
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
2
|
-
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, NativeScrollEvent, Keyboard, Platform, Linking } from "react-native";
|
|
2
|
+
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, NativeScrollEvent, Keyboard, Platform, Image, Linking } from "react-native";
|
|
3
3
|
import type { ServerMessage, ToolMessage, ResultMessage, UserPromptMessage, HistoryResultMessage, SystemDisplayMessage, AssistantPart, AssistantPartsMessage } from "../services/websocket";
|
|
4
4
|
import { SPACING, LAYOUT, COLORS, TYPOGRAPHY } from "../constants/design";
|
|
5
5
|
|
|
@@ -152,7 +152,16 @@ function MessageItem({ message }: { key?: React.Key; message: ServerMessage }) {
|
|
|
152
152
|
function UserPromptItem({ message }: { message: UserPromptMessage }) {
|
|
153
153
|
return (
|
|
154
154
|
<View style={styles.userPromptContainer}>
|
|
155
|
-
|
|
155
|
+
{message.images && message.images.length > 0 && (
|
|
156
|
+
<View style={styles.userImages}>
|
|
157
|
+
{message.images.map((img, i) => (
|
|
158
|
+
<Image key={i} source={{ uri: img.uri }} style={styles.userImageThumb} />
|
|
159
|
+
))}
|
|
160
|
+
</View>
|
|
161
|
+
)}
|
|
162
|
+
{message.content ? (
|
|
163
|
+
<Text style={styles.userPromptText} selectable>{message.content}</Text>
|
|
164
|
+
) : null}
|
|
156
165
|
</View>
|
|
157
166
|
);
|
|
158
167
|
}
|
|
@@ -716,6 +725,18 @@ const styles = StyleSheet.create({
|
|
|
716
725
|
fontSize: TYPOGRAPHY.SIZE_LG,
|
|
717
726
|
lineHeight: 20,
|
|
718
727
|
},
|
|
728
|
+
userImages: {
|
|
729
|
+
flexDirection: "row",
|
|
730
|
+
flexWrap: "wrap",
|
|
731
|
+
gap: SPACING.SM,
|
|
732
|
+
marginBottom: SPACING.SM,
|
|
733
|
+
},
|
|
734
|
+
userImageThumb: {
|
|
735
|
+
width: 80,
|
|
736
|
+
height: 80,
|
|
737
|
+
borderRadius: SPACING.SM,
|
|
738
|
+
backgroundColor: "rgba(255,255,255,0.1)",
|
|
739
|
+
},
|
|
719
740
|
scrollButton: {
|
|
720
741
|
position: "absolute",
|
|
721
742
|
bottom: LAYOUT.CONTENT_PADDING_H,
|
package/widget/package.json
CHANGED
|
@@ -92,10 +92,17 @@ export interface HistoryMessage {
|
|
|
92
92
|
timestamp: number;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export interface ImageAttachment {
|
|
96
|
+
uri: string;
|
|
97
|
+
width?: number;
|
|
98
|
+
height?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
// Local display-only message for showing user prompts in the UI
|
|
96
102
|
export interface UserPromptMessage {
|
|
97
103
|
type: "user_prompt";
|
|
98
104
|
content: string;
|
|
105
|
+
images?: ImageAttachment[];
|
|
99
106
|
timestamp: number;
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -341,18 +348,22 @@ export class WebSocketClient {
|
|
|
341
348
|
this.setStatus("disconnected");
|
|
342
349
|
}
|
|
343
350
|
|
|
344
|
-
sendPrompt(content: string): void {
|
|
351
|
+
sendPrompt(content: string, imagePaths?: string[]): void {
|
|
345
352
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
346
353
|
this.options.onError(new Error("Not connected"));
|
|
347
354
|
return;
|
|
348
355
|
}
|
|
349
356
|
|
|
350
|
-
const message = {
|
|
357
|
+
const message: Record<string, unknown> = {
|
|
351
358
|
type: "prompt",
|
|
352
359
|
content,
|
|
353
360
|
id: generateId(),
|
|
354
361
|
};
|
|
355
362
|
|
|
363
|
+
if (imagePaths && imagePaths.length > 0) {
|
|
364
|
+
message.imagePaths = imagePaths;
|
|
365
|
+
}
|
|
366
|
+
|
|
356
367
|
this.ws.send(JSON.stringify(message));
|
|
357
368
|
this.setStatus("processing");
|
|
358
369
|
}
|