@ateam-ai-mobile/ui 0.1.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,134 @@
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ TextInput,
5
+ Pressable,
6
+ StyleSheet,
7
+ Platform,
8
+ } from 'react-native';
9
+ import Svg, { Path, Line, Polygon } from 'react-native-svg';
10
+ import { colors, borderRadius } from '@ateam-ai-mobile/core';
11
+ import { getHaptics } from '@ateam-ai-mobile/plugin-runtime';
12
+
13
+ interface Props {
14
+ onSend: (text: string) => void;
15
+ onCallStart?: () => void;
16
+ loading: boolean;
17
+ /** Pre-fill the composer (e.g. from bridge.prepareMessage). The text is
18
+ * placed in the input and focused — not sent. */
19
+ prefill?: string;
20
+ placeholder?: string;
21
+ }
22
+
23
+ export default function ChatInput({ onSend, onCallStart, loading, prefill, placeholder }: Props) {
24
+ const [text, setText] = useState('');
25
+ const [height, setHeight] = useState(40);
26
+ const inputRef = useRef<TextInput>(null);
27
+
28
+ useEffect(() => {
29
+ if (prefill) {
30
+ setText(prefill);
31
+ inputRef.current?.focus();
32
+ }
33
+ }, [prefill]);
34
+
35
+ // NOTE: no `loading` guard and no editable={!loading} — sending WHILE a
36
+ // chain runs is a feature (backend Chain Additions router decides
37
+ // attach/cancel/new), and the input must never be disabled mid-run.
38
+ const handleSend = useCallback(() => {
39
+ const trimmed = text.trim();
40
+ if (!trimmed) return;
41
+ getHaptics()?.impact('light');
42
+ onSend(trimmed);
43
+ setText('');
44
+ setHeight(40);
45
+ }, [text, loading, onSend]);
46
+
47
+ const canSend = text.trim().length > 0;
48
+
49
+ return (
50
+ <View style={styles.dock}>
51
+ <View style={styles.inputRow}>
52
+ {onCallStart && (
53
+ <Pressable style={styles.callBtn} onPress={onCallStart}>
54
+ <Svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke={colors.light.accent} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
55
+ <Path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.63A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z" />
56
+ </Svg>
57
+ </Pressable>
58
+ )}
59
+ <TextInput
60
+ ref={inputRef}
61
+ style={[styles.input, { height: Math.min(Math.max(40, height), 120) }]}
62
+ value={text}
63
+ onChangeText={setText}
64
+ onContentSizeChange={e => setHeight(e.nativeEvent.contentSize.height)}
65
+ placeholder={placeholder || 'Message A-Team...'}
66
+ placeholderTextColor={colors.light.textMuted}
67
+ multiline
68
+ returnKeyType="default"
69
+ blurOnSubmit={false}
70
+ onSubmitEditing={() => {
71
+ if (Platform.OS === 'web') handleSend();
72
+ }}
73
+ />
74
+ <Pressable
75
+ style={[styles.sendBtn, canSend && styles.sendBtnActive]}
76
+ onPress={handleSend}
77
+ disabled={!canSend}
78
+ >
79
+ <Svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke={canSend ? '#fff' : colors.light.textMuted} strokeWidth={2}>
80
+ <Line x1="22" y1="2" x2="11" y2="13" />
81
+ <Polygon points="22 2 15 22 11 13 2 9 22 2" />
82
+ </Svg>
83
+ </Pressable>
84
+ </View>
85
+ </View>
86
+ );
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ dock: {
91
+ paddingHorizontal: 12,
92
+ paddingVertical: 8,
93
+ backgroundColor: colors.light.bgPrimary,
94
+ borderTopWidth: 0.5,
95
+ borderTopColor: colors.light.border,
96
+ },
97
+ inputRow: {
98
+ flexDirection: 'row',
99
+ alignItems: 'flex-end',
100
+ backgroundColor: colors.light.bgInput,
101
+ borderRadius: borderRadius.xl,
102
+ borderWidth: 1,
103
+ borderColor: colors.light.border,
104
+ paddingHorizontal: 4,
105
+ paddingVertical: 4,
106
+ gap: 4,
107
+ },
108
+ callBtn: {
109
+ width: 36,
110
+ height: 36,
111
+ borderRadius: 18,
112
+ justifyContent: 'center',
113
+ alignItems: 'center',
114
+ },
115
+ input: {
116
+ flex: 1,
117
+ fontSize: 16,
118
+ color: colors.light.textPrimary,
119
+ paddingHorizontal: 8,
120
+ paddingVertical: 8,
121
+ maxHeight: 120,
122
+ },
123
+ sendBtn: {
124
+ width: 36,
125
+ height: 36,
126
+ borderRadius: 18,
127
+ justifyContent: 'center',
128
+ alignItems: 'center',
129
+ backgroundColor: colors.light.bgTertiary,
130
+ },
131
+ sendBtnActive: {
132
+ backgroundColor: colors.light.accent,
133
+ },
134
+ });
@@ -0,0 +1,221 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { View, Text, Animated, StyleSheet } from 'react-native';
3
+ import type { JobProgress } from '@ateam-ai-mobile/core';
4
+ import { ada, fontSize, borderRadius } from '@ateam-ai-mobile/core';
5
+
6
+ // Semantic warm-palette colors for terminal states (kept local — Ada palette
7
+ // is mostly orange/ink, but progress states need a small warm green/red/amber
8
+ // vocabulary to stay readable).
9
+ const WARM_GREEN = '#5A8A5C';
10
+ const WARM_GREEN_SOFT = 'rgba(90, 138, 92, 0.14)';
11
+ const WARM_RED = '#C15545';
12
+ const WARM_RED_SOFT = 'rgba(193, 85, 69, 0.14)';
13
+ const WARM_AMBER = '#D49A4E';
14
+ const WARM_AMBER_SOFT = 'rgba(212, 154, 78, 0.16)';
15
+
16
+ interface Props {
17
+ progress: JobProgress;
18
+ }
19
+
20
+ export default function JobProgressBar({ progress }: Props) {
21
+ const pulseAnim = useRef(new Animated.Value(0.5)).current;
22
+ const [elapsed, setElapsed] = useState(0);
23
+
24
+ const isRunning = progress.status === 'running';
25
+ const isDone = progress.status === 'done';
26
+ const isError = progress.status === 'error';
27
+ const isPaused = progress.status === 'paused';
28
+
29
+ // Pulse animation while running
30
+ useEffect(() => {
31
+ if (!isRunning) {
32
+ pulseAnim.setValue(1);
33
+ return;
34
+ }
35
+ const anim = Animated.loop(
36
+ Animated.sequence([
37
+ Animated.timing(pulseAnim, { toValue: 1, duration: 800, useNativeDriver: true }),
38
+ Animated.timing(pulseAnim, { toValue: 0.5, duration: 800, useNativeDriver: true }),
39
+ ]),
40
+ );
41
+ anim.start();
42
+ return () => anim.stop();
43
+ }, [isRunning, pulseAnim]);
44
+
45
+ // Elapsed timer — ticks every second while running, clears on terminal state
46
+ useEffect(() => {
47
+ if (!isRunning || !progress.chainStartedAt) {
48
+ setElapsed(0);
49
+ return;
50
+ }
51
+ const start = progress.chainStartedAt;
52
+ setElapsed(Math.floor((Date.now() - start) / 1000));
53
+ const id = setInterval(() => {
54
+ setElapsed(Math.floor((Date.now() - start) / 1000));
55
+ }, 1000);
56
+ return () => clearInterval(id);
57
+ }, [isRunning, progress.chainStartedAt]);
58
+
59
+ const statusColor = isDone
60
+ ? WARM_GREEN
61
+ : isError
62
+ ? WARM_RED
63
+ : isPaused
64
+ ? WARM_AMBER
65
+ : ada.orange;
66
+
67
+ const statusBg = isDone
68
+ ? WARM_GREEN_SOFT
69
+ : isError
70
+ ? WARM_RED_SOFT
71
+ : isPaused
72
+ ? WARM_AMBER_SOFT
73
+ : ada.orangeSoft;
74
+
75
+ // Terminal states — simple one-liner
76
+ if (isDone || isError) {
77
+ return (
78
+ <View style={styles.container}>
79
+ <View style={styles.row}>
80
+ <View style={[styles.dot, { backgroundColor: statusColor }]} />
81
+ <Text style={[styles.label, { color: statusColor }]}>
82
+ {isDone ? 'Completed' : 'Failed'}
83
+ </Text>
84
+ </View>
85
+ </View>
86
+ );
87
+ }
88
+
89
+ if (isPaused) {
90
+ return (
91
+ <View style={styles.container}>
92
+ <View style={styles.row}>
93
+ <View style={[styles.dot, { backgroundColor: statusColor }]} />
94
+ <Text style={[styles.label, { color: statusColor }]}>
95
+ Waiting for your reply
96
+ </Text>
97
+ </View>
98
+ </View>
99
+ );
100
+ }
101
+
102
+ // Running — voice-agent-style compact line:
103
+ // ● messaging-agent → whatsapp.send · iter 3 · 0:42
104
+ const elapsedStr = progress.chainStartedAt
105
+ ? `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, '0')}`
106
+ : null;
107
+
108
+ const metaParts = [
109
+ progress.iteration != null && progress.iteration > 0 ? `iter ${progress.iteration}` : null,
110
+ elapsedStr,
111
+ ].filter(Boolean) as string[];
112
+
113
+ const lp = progress.liveProgress;
114
+
115
+ return (
116
+ <View style={styles.container}>
117
+ <View style={styles.row}>
118
+ <Animated.View
119
+ style={[styles.dot, { backgroundColor: ada.orange, opacity: pulseAnim }]}
120
+ />
121
+ <Text style={styles.skill} numberOfLines={1}>
122
+ {progress.skillSlug || 'Processing...'}
123
+ </Text>
124
+ {!!progress.currentTool && (
125
+ <Text style={styles.tool} numberOfLines={1}>
126
+ {` → ${progress.currentTool}`}
127
+ </Text>
128
+ )}
129
+ {metaParts.length > 0 && (
130
+ <Text style={styles.meta} numberOfLines={1}>
131
+ {` · ${metaParts.join(' · ')}`}
132
+ </Text>
133
+ )}
134
+ </View>
135
+ {/* Live progress from adas_emit_progress() — shown during fat tool execution */}
136
+ {!!lp?.message && (
137
+ <View style={styles.liveProgressRow}>
138
+ {lp.step != null && lp.total != null && (
139
+ <Text style={styles.liveProgressStep}>[{lp.step}/{lp.total}]</Text>
140
+ )}
141
+ <Text style={styles.liveProgressMsg} numberOfLines={1}>{lp.message}</Text>
142
+ </View>
143
+ )}
144
+ <View style={[styles.progressTrack, { backgroundColor: statusBg }]}>
145
+ <Animated.View
146
+ style={[styles.progressFill, { backgroundColor: ada.orange, opacity: pulseAnim }]}
147
+ />
148
+ </View>
149
+ </View>
150
+ );
151
+ }
152
+
153
+ const styles = StyleSheet.create({
154
+ container: {
155
+ marginTop: 8,
156
+ paddingTop: 8,
157
+ borderTopWidth: 1,
158
+ borderTopColor: ada.border,
159
+ },
160
+ row: {
161
+ flexDirection: 'row',
162
+ alignItems: 'center',
163
+ flexWrap: 'nowrap',
164
+ overflow: 'hidden',
165
+ },
166
+ dot: {
167
+ width: 7,
168
+ height: 7,
169
+ borderRadius: 4,
170
+ marginRight: 6,
171
+ flexShrink: 0,
172
+ },
173
+ label: {
174
+ fontSize: fontSize.xs,
175
+ fontWeight: '600',
176
+ },
177
+ // Running state text segments
178
+ skill: {
179
+ fontSize: fontSize.xs,
180
+ fontWeight: '600',
181
+ color: ada.orange,
182
+ flexShrink: 1,
183
+ },
184
+ tool: {
185
+ fontSize: fontSize.xs,
186
+ color: ada.inkMuted,
187
+ flexShrink: 2,
188
+ },
189
+ meta: {
190
+ fontSize: fontSize.xs,
191
+ color: ada.inkMuted,
192
+ flexShrink: 0,
193
+ },
194
+ liveProgressRow: {
195
+ flexDirection: 'row',
196
+ alignItems: 'center',
197
+ marginTop: 4,
198
+ gap: 4,
199
+ },
200
+ liveProgressStep: {
201
+ fontSize: fontSize.xs,
202
+ fontWeight: '600',
203
+ color: ada.inkMuted,
204
+ },
205
+ liveProgressMsg: {
206
+ fontSize: fontSize.xs,
207
+ color: ada.orange,
208
+ flex: 1,
209
+ },
210
+ progressTrack: {
211
+ height: 3,
212
+ borderRadius: 2,
213
+ marginTop: 6,
214
+ overflow: 'hidden',
215
+ },
216
+ progressFill: {
217
+ height: '100%',
218
+ width: '60%',
219
+ borderRadius: 2,
220
+ },
221
+ });