yaver-feedback-react-native 0.8.12 → 0.9.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/app.plugin.js +13 -0
- package/dist/DeployPanel.d.ts +7 -0
- package/dist/DeployPanel.js +354 -0
- package/dist/FeedbackModal.js +408 -63
- package/dist/FloatingButton.js +25 -3
- package/dist/MachinePickerScreen.js +14 -5
- package/dist/P2PClient.d.ts +102 -1
- package/dist/P2PClient.js +313 -0
- package/dist/VibeChatScreen.d.ts +25 -0
- package/dist/VibeChatScreen.js +531 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +13 -0
- package/dist/_core/buildFeedbackPrompt.js +77 -0
- package/dist/capture.d.ts +20 -0
- package/dist/capture.js +82 -0
- package/dist/preferences.d.ts +4 -0
- package/dist/preferences.js +62 -0
- package/dist/types.d.ts +66 -2
- package/dist/voice.d.ts +61 -0
- package/dist/voice.js +246 -0
- package/package.json +26 -3
- package/src/DeployPanel.tsx +403 -0
- package/src/FeedbackModal.tsx +510 -76
- package/src/FloatingButton.tsx +27 -3
- package/src/MachinePickerScreen.tsx +12 -3
- package/src/P2PClient.ts +326 -1
- package/src/VibeChatScreen.tsx +581 -0
- package/src/_core/buildFeedbackPrompt.ts +102 -0
- package/src/capture.ts +82 -0
- package/src/preferences.ts +56 -0
- package/src/types.ts +62 -2
- package/src/voice.ts +270 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// VibeChatScreen — the converged chat UI for the standalone feedback
|
|
3
|
+
// SDK. Mirrors Yaver mobile's Tasks tab + the in-Yaver native pane:
|
|
4
|
+
//
|
|
5
|
+
// 1. User sees a live SSE transcript of agent stdout (PhaseStatusLine
|
|
6
|
+
// style "searching… / compiling…" while running, full markdown
|
|
7
|
+
// output once it lands).
|
|
8
|
+
// 2. User can keep vibing — type a follow-up after the first turn
|
|
9
|
+
// lands and POST a /tasks/{id}/resume to multi-turn the same
|
|
10
|
+
// coding session.
|
|
11
|
+
// 3. Reload button at the bottom hits client.reloadApp() so the user
|
|
12
|
+
// can see the change without leaving the chat.
|
|
13
|
+
//
|
|
14
|
+
// State machine:
|
|
15
|
+
// idle — empty, waiting for first prompt (handled by parent screen)
|
|
16
|
+
// running — task is live, transcript streams, follow-up disabled
|
|
17
|
+
// done — task finished, follow-up enabled, Reload prominent
|
|
18
|
+
// failed — same as done but error tinted
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.VibeChatScreen = VibeChatScreen;
|
|
54
|
+
const react_1 = __importStar(require("react"));
|
|
55
|
+
const react_native_1 = require("react-native");
|
|
56
|
+
const voice_1 = require("./voice");
|
|
57
|
+
const capture_1 = require("./capture");
|
|
58
|
+
function VibeChatScreen({ client, initialTaskId, initialUserPrompt, onClose, onReload, project, model, runner, }) {
|
|
59
|
+
const [taskId, setTaskId] = (0, react_1.useState)(initialTaskId);
|
|
60
|
+
const [turns, setTurns] = (0, react_1.useState)(() => [
|
|
61
|
+
{
|
|
62
|
+
id: `user-${Date.now()}`,
|
|
63
|
+
role: 'user',
|
|
64
|
+
text: initialUserPrompt,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: `status-${Date.now()}`,
|
|
69
|
+
role: 'status',
|
|
70
|
+
text: 'starting…',
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
const [streamBuffer, setStreamBuffer] = (0, react_1.useState)('');
|
|
75
|
+
const [status, setStatus] = (0, react_1.useState)('running');
|
|
76
|
+
const [followUp, setFollowUp] = (0, react_1.useState)('');
|
|
77
|
+
const [isResuming, setIsResuming] = (0, react_1.useState)(false);
|
|
78
|
+
const [isReloading, setIsReloading] = (0, react_1.useState)(false);
|
|
79
|
+
const scrollRef = (0, react_1.useRef)(null);
|
|
80
|
+
const abortRef = (0, react_1.useRef)(null);
|
|
81
|
+
// ── Voice vibe coding ──────────────────────────────────────────────
|
|
82
|
+
const [voiceState, setVoiceState] = (0, react_1.useState)('idle');
|
|
83
|
+
const [voiceAvailable, setVoiceAvailable] = (0, react_1.useState)(false);
|
|
84
|
+
// "local" = whisper.cpp on the host (free, private); "flux" = Deepgram
|
|
85
|
+
// nova-3 streaming. fluxAvailable gates the toggle on whether the agent
|
|
86
|
+
// has a Deepgram key. activeEngine is echoed back by the agent.
|
|
87
|
+
const [voiceMode, setVoiceMode] = (0, react_1.useState)('local');
|
|
88
|
+
const [fluxAvailable, setFluxAvailable] = (0, react_1.useState)(false);
|
|
89
|
+
const [activeEngine, setActiveEngine] = (0, react_1.useState)('');
|
|
90
|
+
const voiceSessionRef = (0, react_1.useRef)(null);
|
|
91
|
+
// Subscribe to the current task's SSE stream. Re-runs whenever the
|
|
92
|
+
// taskId changes (resumeTask reuses the same id, so this only fires
|
|
93
|
+
// once per task — which is fine).
|
|
94
|
+
(0, react_1.useEffect)(() => {
|
|
95
|
+
let live = true;
|
|
96
|
+
const acc = [];
|
|
97
|
+
const close = client.streamTaskOutput(taskId, (line) => {
|
|
98
|
+
if (!live)
|
|
99
|
+
return;
|
|
100
|
+
// Filter our internal error sentinel from the SSE helper.
|
|
101
|
+
if (line.startsWith('__error__:')) {
|
|
102
|
+
setStatus('failed');
|
|
103
|
+
setStreamBuffer((prev) => prev + (prev ? '\n' : '') + line.slice('__error__:'.length).trim());
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
acc.push(line);
|
|
107
|
+
// Throttle re-renders: flush every ~100ms.
|
|
108
|
+
setStreamBuffer(acc.join('\n'));
|
|
109
|
+
}, (terminal) => {
|
|
110
|
+
if (!live)
|
|
111
|
+
return;
|
|
112
|
+
setStatus(terminal === 'completed' ? 'done' : 'failed');
|
|
113
|
+
// Move the buffered stream into a real assistant turn so the
|
|
114
|
+
// user sees a stable render and can scroll back, then clear
|
|
115
|
+
// the buffer for any follow-up.
|
|
116
|
+
setTurns((prev) => {
|
|
117
|
+
const collapsed = acc.join('\n').trim();
|
|
118
|
+
if (!collapsed)
|
|
119
|
+
return prev.filter((t) => t.role !== 'status');
|
|
120
|
+
const next = prev.filter((t) => t.role !== 'status');
|
|
121
|
+
next.push({
|
|
122
|
+
id: `assistant-${taskId}-${Date.now()}`,
|
|
123
|
+
role: 'assistant',
|
|
124
|
+
text: collapsed,
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
return next;
|
|
128
|
+
});
|
|
129
|
+
setStreamBuffer('');
|
|
130
|
+
});
|
|
131
|
+
abortRef.current = close;
|
|
132
|
+
return () => {
|
|
133
|
+
live = false;
|
|
134
|
+
try {
|
|
135
|
+
close();
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
};
|
|
139
|
+
}, [client, taskId]);
|
|
140
|
+
// Auto-scroll the transcript when new content lands.
|
|
141
|
+
(0, react_1.useEffect)(() => {
|
|
142
|
+
const t = setTimeout(() => {
|
|
143
|
+
scrollRef.current?.scrollToEnd({ animated: true });
|
|
144
|
+
}, 50);
|
|
145
|
+
return () => clearTimeout(t);
|
|
146
|
+
}, [streamBuffer, turns]);
|
|
147
|
+
const handleSendFollowUp = (0, react_1.useCallback)(async () => {
|
|
148
|
+
const text = followUp.trim();
|
|
149
|
+
if (!text || isResuming)
|
|
150
|
+
return;
|
|
151
|
+
setIsResuming(true);
|
|
152
|
+
// Add user turn immediately for snappy UX.
|
|
153
|
+
setTurns((prev) => [
|
|
154
|
+
...prev,
|
|
155
|
+
{ id: `user-${Date.now()}`, role: 'user', text, timestamp: Date.now() },
|
|
156
|
+
{ id: `status-${Date.now()}`, role: 'status', text: 'thinking…', timestamp: Date.now() },
|
|
157
|
+
]);
|
|
158
|
+
setFollowUp('');
|
|
159
|
+
setStatus('running');
|
|
160
|
+
setStreamBuffer('');
|
|
161
|
+
try {
|
|
162
|
+
await client.resumeTask({ taskId, userPrompt: text });
|
|
163
|
+
// resumeTask reuses the same taskId, so the SSE subscription
|
|
164
|
+
// above will pick up the new output stream automatically. To
|
|
165
|
+
// force a fresh subscription we momentarily flip taskId to a
|
|
166
|
+
// sentinel and back; cleaner than tearing down + re-attaching
|
|
167
|
+
// the SSE manually.
|
|
168
|
+
const same = taskId;
|
|
169
|
+
setTaskId(`${same}#`);
|
|
170
|
+
setTimeout(() => setTaskId(same), 0);
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
setStatus('failed');
|
|
174
|
+
setTurns((prev) => [
|
|
175
|
+
...prev.filter((t) => t.role !== 'status'),
|
|
176
|
+
{
|
|
177
|
+
id: `assistant-err-${Date.now()}`,
|
|
178
|
+
role: 'assistant',
|
|
179
|
+
text: `Failed to send follow-up: ${e instanceof Error ? e.message : String(e)}`,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
setIsResuming(false);
|
|
186
|
+
}
|
|
187
|
+
}, [client, followUp, isResuming, taskId]);
|
|
188
|
+
const handleReload = (0, react_1.useCallback)(async () => {
|
|
189
|
+
if (isReloading || !onReload)
|
|
190
|
+
return;
|
|
191
|
+
setIsReloading(true);
|
|
192
|
+
try {
|
|
193
|
+
await onReload();
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
setTurns((prev) => [
|
|
197
|
+
...prev,
|
|
198
|
+
{
|
|
199
|
+
id: `assistant-reload-err-${Date.now()}`,
|
|
200
|
+
role: 'assistant',
|
|
201
|
+
text: `Reload failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
setIsReloading(false);
|
|
208
|
+
}
|
|
209
|
+
}, [isReloading, onReload]);
|
|
210
|
+
// Probe whether voice is usable: deps present (expo-av + expo-file-
|
|
211
|
+
// system + buffer) AND the agent reports STT/TTS ready. Hide the mic
|
|
212
|
+
// entirely otherwise so users never tap a dead button.
|
|
213
|
+
(0, react_1.useEffect)(() => {
|
|
214
|
+
let cancelled = false;
|
|
215
|
+
(async () => {
|
|
216
|
+
if (!(0, capture_1.isVoiceCaptureSupported)() || !(0, voice_1.isVoiceStreamSupported)())
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetch(`${client.agentBaseUrl}/voice/status`, { headers: client.voiceAuthHeaders() });
|
|
220
|
+
if (!res.ok)
|
|
221
|
+
return;
|
|
222
|
+
const body = await res.json();
|
|
223
|
+
if (cancelled)
|
|
224
|
+
return;
|
|
225
|
+
// Local whisper is always usable when voice is enabled; Flux needs
|
|
226
|
+
// a Deepgram key on the agent. Show the mic if either path works.
|
|
227
|
+
const localOk = !!body?.enabled;
|
|
228
|
+
const fluxOk = !!body?.enabled && !!body?.deepgramSet;
|
|
229
|
+
if (localOk || fluxOk)
|
|
230
|
+
setVoiceAvailable(true);
|
|
231
|
+
setFluxAvailable(fluxOk);
|
|
232
|
+
if (!localOk && fluxOk)
|
|
233
|
+
setVoiceMode('flux');
|
|
234
|
+
}
|
|
235
|
+
catch { /* leave hidden */ }
|
|
236
|
+
})();
|
|
237
|
+
return () => { cancelled = true; };
|
|
238
|
+
}, [client]);
|
|
239
|
+
(0, react_1.useEffect)(() => () => { voiceSessionRef.current?.close(); }, []);
|
|
240
|
+
// Local TTS: the agent streams no audio for "local"/"device" engines,
|
|
241
|
+
// so the client speaks the result text with the device synthesizer.
|
|
242
|
+
// expo-speech is optional — if absent, the text is still shown.
|
|
243
|
+
const speakLocalText = (0, react_1.useCallback)((text) => {
|
|
244
|
+
if (!text)
|
|
245
|
+
return;
|
|
246
|
+
try {
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
248
|
+
const Speech = require('expo-speech');
|
|
249
|
+
const headline = text.length > 280 ? `${text.slice(0, 280)} — see screen for the rest.` : text;
|
|
250
|
+
Speech.stop?.();
|
|
251
|
+
Speech.speak?.(headline);
|
|
252
|
+
}
|
|
253
|
+
catch { /* expo-speech not installed — text remains visible */ }
|
|
254
|
+
}, []);
|
|
255
|
+
const playTTS = (0, react_1.useCallback)(async (pcm, sampleRate) => {
|
|
256
|
+
try {
|
|
257
|
+
const wavUri = await (0, voice_1.pcmToTempWavURI)(pcm, sampleRate);
|
|
258
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
259
|
+
const { Audio } = require('expo-av');
|
|
260
|
+
const { sound } = await Audio.Sound.createAsync({ uri: wavUri }, { shouldPlay: true });
|
|
261
|
+
sound.setOnPlaybackStatusUpdate((st) => {
|
|
262
|
+
if (st.didJustFinish)
|
|
263
|
+
sound.unloadAsync().catch(() => { });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
catch { /* playback best-effort */ }
|
|
267
|
+
}, []);
|
|
268
|
+
const stopVoiceAndProcess = (0, react_1.useCallback)(async () => {
|
|
269
|
+
setVoiceState('uploading');
|
|
270
|
+
let uri = null;
|
|
271
|
+
try {
|
|
272
|
+
uri = await (0, capture_1.stopPcmRecording)();
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
setVoiceState('idle');
|
|
276
|
+
setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!uri) {
|
|
280
|
+
setVoiceState('idle');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const useFlux = voiceMode === 'flux' && fluxAvailable;
|
|
284
|
+
const session = new voice_1.SDKVoiceSession({
|
|
285
|
+
onProviders: (stt, tts) => setActiveEngine(stt === 'deepgram' ? 'Flux (Deepgram)' : stt === 'local' ? 'Local (whisper)' : stt),
|
|
286
|
+
onTranscriptPartial: (t) => {
|
|
287
|
+
setTurns((prev) => {
|
|
288
|
+
const next = prev.filter((x) => x.id !== 'voice-partial');
|
|
289
|
+
next.push({ id: 'voice-partial', role: 'status', text: `🎙 ${t}`, timestamp: Date.now() });
|
|
290
|
+
return next;
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
onTranscriptFinal: (t) => {
|
|
294
|
+
setVoiceState('thinking');
|
|
295
|
+
setTurns((prev) => [
|
|
296
|
+
...prev.filter((x) => x.id !== 'voice-partial'),
|
|
297
|
+
{ id: `user-voice-${Date.now()}`, role: 'user', text: t, timestamp: Date.now() },
|
|
298
|
+
{ id: `status-${Date.now()}`, role: 'status', text: 'thinking…', timestamp: Date.now() },
|
|
299
|
+
]);
|
|
300
|
+
},
|
|
301
|
+
onTaskCreated: (id) => {
|
|
302
|
+
// Hand the chat's SSE subscription the new task so its agent
|
|
303
|
+
// output streams into the transcript exactly like a typed turn.
|
|
304
|
+
if (id) {
|
|
305
|
+
setStatus('running');
|
|
306
|
+
setStreamBuffer('');
|
|
307
|
+
setTaskId(id);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
onTaskResult: (_id, text) => {
|
|
311
|
+
setVoiceState('speaking');
|
|
312
|
+
// Local TTS path: agent sends no audio frames, so speak here.
|
|
313
|
+
if (!useFlux)
|
|
314
|
+
speakLocalText(text);
|
|
315
|
+
},
|
|
316
|
+
onTTSReady: (pcm, sr) => { void playTTS(pcm, sr); },
|
|
317
|
+
onDone: () => setTimeout(() => setVoiceState('idle'), 1200),
|
|
318
|
+
onError: (msg) => {
|
|
319
|
+
setVoiceState('idle');
|
|
320
|
+
setTurns((prev) => [...prev.filter((x) => x.id !== 'voice-partial'), { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${msg}`, timestamp: Date.now() }]);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
voiceSessionRef.current = session;
|
|
324
|
+
try {
|
|
325
|
+
await session.start({
|
|
326
|
+
wsUrl: client.voiceStreamUrl(),
|
|
327
|
+
headers: client.voiceAuthHeaders(),
|
|
328
|
+
project,
|
|
329
|
+
model,
|
|
330
|
+
runner,
|
|
331
|
+
surface: 'feedback-sdk',
|
|
332
|
+
ttsBudget: 280,
|
|
333
|
+
// Local: whisper.cpp on the host + device synth. Flux: Deepgram
|
|
334
|
+
// nova-3 STT + Aura TTS streamed back as PCM.
|
|
335
|
+
sttProvider: useFlux ? 'deepgram' : 'local',
|
|
336
|
+
ttsProvider: useFlux ? 'deepgram' : 'local',
|
|
337
|
+
});
|
|
338
|
+
await session.streamAudioFile(uri, { skipWavHeader: true });
|
|
339
|
+
session.finalize();
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
setVoiceState('idle');
|
|
343
|
+
session.close();
|
|
344
|
+
setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
|
|
345
|
+
}
|
|
346
|
+
}, [client, project, model, runner, playTTS, voiceMode, fluxAvailable, speakLocalText]);
|
|
347
|
+
const handleVoicePress = (0, react_1.useCallback)(async () => {
|
|
348
|
+
if (voiceState === 'recording') {
|
|
349
|
+
void stopVoiceAndProcess();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (voiceState !== 'idle') {
|
|
353
|
+
// Mid-flow tap cancels.
|
|
354
|
+
voiceSessionRef.current?.close();
|
|
355
|
+
voiceSessionRef.current = null;
|
|
356
|
+
setVoiceState('idle');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await (0, capture_1.startPcmRecording)();
|
|
361
|
+
setVoiceState('recording');
|
|
362
|
+
}
|
|
363
|
+
catch (e) {
|
|
364
|
+
setTurns((prev) => [...prev, { id: `status-verr-${Date.now()}`, role: 'status', text: `voice: ${e instanceof Error ? e.message : String(e)}`, timestamp: Date.now() }]);
|
|
365
|
+
}
|
|
366
|
+
}, [voiceState, stopVoiceAndProcess]);
|
|
367
|
+
const voiceLabel = {
|
|
368
|
+
idle: '🎙 speak',
|
|
369
|
+
recording: '■ stop',
|
|
370
|
+
uploading: 'sending…',
|
|
371
|
+
thinking: 'thinking…',
|
|
372
|
+
speaking: 'speaking…',
|
|
373
|
+
};
|
|
374
|
+
return (<react_native_1.View style={styles.container}>
|
|
375
|
+
<react_native_1.View style={styles.header}>
|
|
376
|
+
<react_native_1.Text style={styles.title}>Vibe</react_native_1.Text>
|
|
377
|
+
{onClose && (<react_native_1.TouchableOpacity onPress={onClose} accessibilityLabel="Close vibe chat">
|
|
378
|
+
<react_native_1.Text style={styles.close}>✕</react_native_1.Text>
|
|
379
|
+
</react_native_1.TouchableOpacity>)}
|
|
380
|
+
</react_native_1.View>
|
|
381
|
+
|
|
382
|
+
<react_native_1.ScrollView ref={scrollRef} style={styles.transcript} contentContainerStyle={styles.transcriptContent} keyboardShouldPersistTaps="handled">
|
|
383
|
+
{turns.map((turn) => (<react_native_1.View key={turn.id} style={[
|
|
384
|
+
styles.turn,
|
|
385
|
+
turn.role === 'user' && styles.turnUser,
|
|
386
|
+
turn.role === 'assistant' && styles.turnAssistant,
|
|
387
|
+
turn.role === 'status' && styles.turnStatus,
|
|
388
|
+
]}>
|
|
389
|
+
<react_native_1.Text style={styles.turnText}>{turn.text}</react_native_1.Text>
|
|
390
|
+
</react_native_1.View>))}
|
|
391
|
+
{/* Live streaming buffer rendered as a single trailing
|
|
392
|
+
assistant block while the task is running. Once the task
|
|
393
|
+
terminates the stream is moved into a real turn (above)
|
|
394
|
+
and this block clears. */}
|
|
395
|
+
{streamBuffer && status === 'running' && (<react_native_1.View style={[styles.turn, styles.turnAssistant]}>
|
|
396
|
+
<react_native_1.Text style={styles.turnText}>{streamBuffer}</react_native_1.Text>
|
|
397
|
+
</react_native_1.View>)}
|
|
398
|
+
{status === 'running' && (<react_native_1.View style={styles.spinnerRow}>
|
|
399
|
+
<react_native_1.ActivityIndicator size="small" color="#9ca3af"/>
|
|
400
|
+
<react_native_1.Text style={styles.spinnerText}>working…</react_native_1.Text>
|
|
401
|
+
</react_native_1.View>)}
|
|
402
|
+
</react_native_1.ScrollView>
|
|
403
|
+
|
|
404
|
+
<react_native_1.View style={styles.footer}>
|
|
405
|
+
{voiceAvailable && voiceState !== 'idle' && (<react_native_1.Text style={styles.engineCaption}>
|
|
406
|
+
{voiceState === 'recording' ? 'listening' : voiceState === 'uploading' ? 'sending' : voiceState === 'thinking' ? 'agent working' : 'speaking'}
|
|
407
|
+
{activeEngine ? ` · ${activeEngine}` : ` · ${voiceMode === 'flux' ? 'Flux (Deepgram)' : 'Local (whisper)'}`}
|
|
408
|
+
</react_native_1.Text>)}
|
|
409
|
+
<react_native_1.TextInput style={styles.input} value={followUp} onChangeText={setFollowUp} placeholder={status === 'running' ? 'wait for the agent…' : 'follow up…'} placeholderTextColor="#666" editable={status !== 'running' && !isResuming} multiline/>
|
|
410
|
+
<react_native_1.View style={styles.actions}>
|
|
411
|
+
{voiceAvailable && (<>
|
|
412
|
+
{/* Local ↔ Flux engine toggle. Only shows Flux when the
|
|
413
|
+
agent has a Deepgram key; otherwise the label just
|
|
414
|
+
states "Local" so the active engine is always clear. */}
|
|
415
|
+
{fluxAvailable ? (<react_native_1.TouchableOpacity style={[styles.actionBtn, styles.engineToggle]} onPress={() => setVoiceMode((m) => (m === 'local' ? 'flux' : 'local'))} disabled={voiceState !== 'idle'} accessibilityLabel="Toggle voice engine">
|
|
416
|
+
<react_native_1.Text style={styles.engineToggleText}>{voiceMode === 'flux' ? '⚡ Flux' : '🔒 Local'}</react_native_1.Text>
|
|
417
|
+
</react_native_1.TouchableOpacity>) : (<react_native_1.View style={[styles.actionBtn, styles.engineToggle]}>
|
|
418
|
+
<react_native_1.Text style={styles.engineToggleText}>🔒 Local</react_native_1.Text>
|
|
419
|
+
</react_native_1.View>)}
|
|
420
|
+
<react_native_1.TouchableOpacity style={[
|
|
421
|
+
styles.actionBtn,
|
|
422
|
+
styles.voiceBtn,
|
|
423
|
+
voiceState === 'recording' && styles.voiceBtnActive,
|
|
424
|
+
(voiceState === 'uploading' || voiceState === 'thinking' || voiceState === 'speaking') && styles.actionBtnDisabled,
|
|
425
|
+
]} onPress={handleVoicePress} disabled={voiceState === 'uploading' || voiceState === 'thinking' || voiceState === 'speaking'} accessibilityLabel="Vibe code by voice">
|
|
426
|
+
<react_native_1.Text style={styles.actionText}>{voiceLabel[voiceState]}</react_native_1.Text>
|
|
427
|
+
</react_native_1.TouchableOpacity>
|
|
428
|
+
</>)}
|
|
429
|
+
{onReload && (<react_native_1.TouchableOpacity style={[
|
|
430
|
+
styles.actionBtn,
|
|
431
|
+
styles.reloadBtn,
|
|
432
|
+
(isReloading || status === 'running') && styles.actionBtnDisabled,
|
|
433
|
+
]} onPress={handleReload} disabled={isReloading || status === 'running'}>
|
|
434
|
+
<react_native_1.Text style={styles.actionText}>
|
|
435
|
+
{isReloading ? 'reloading…' : '⟳ reload'}
|
|
436
|
+
</react_native_1.Text>
|
|
437
|
+
</react_native_1.TouchableOpacity>)}
|
|
438
|
+
<react_native_1.TouchableOpacity style={[
|
|
439
|
+
styles.actionBtn,
|
|
440
|
+
styles.sendBtn,
|
|
441
|
+
(isResuming || status === 'running' || !followUp.trim()) && styles.actionBtnDisabled,
|
|
442
|
+
]} onPress={handleSendFollowUp} disabled={isResuming || status === 'running' || !followUp.trim()}>
|
|
443
|
+
<react_native_1.Text style={styles.actionText}>
|
|
444
|
+
{isResuming ? '…' : '↑ send'}
|
|
445
|
+
</react_native_1.Text>
|
|
446
|
+
</react_native_1.TouchableOpacity>
|
|
447
|
+
</react_native_1.View>
|
|
448
|
+
</react_native_1.View>
|
|
449
|
+
</react_native_1.View>);
|
|
450
|
+
}
|
|
451
|
+
const styles = react_native_1.StyleSheet.create({
|
|
452
|
+
container: { flex: 1, backgroundColor: '#0a0a0a' },
|
|
453
|
+
header: {
|
|
454
|
+
flexDirection: 'row',
|
|
455
|
+
alignItems: 'center',
|
|
456
|
+
justifyContent: 'space-between',
|
|
457
|
+
paddingHorizontal: 16,
|
|
458
|
+
paddingTop: 14,
|
|
459
|
+
paddingBottom: 8,
|
|
460
|
+
borderBottomWidth: 1,
|
|
461
|
+
borderBottomColor: 'rgba(255,255,255,0.08)',
|
|
462
|
+
},
|
|
463
|
+
title: { color: '#fff', fontSize: 17, fontWeight: '600' },
|
|
464
|
+
close: { color: '#9ca3af', fontSize: 18 },
|
|
465
|
+
transcript: { flex: 1 },
|
|
466
|
+
transcriptContent: { padding: 12, paddingBottom: 24 },
|
|
467
|
+
turn: {
|
|
468
|
+
marginVertical: 4,
|
|
469
|
+
padding: 10,
|
|
470
|
+
borderRadius: 12,
|
|
471
|
+
maxWidth: '92%',
|
|
472
|
+
},
|
|
473
|
+
turnUser: {
|
|
474
|
+
backgroundColor: '#7582f5',
|
|
475
|
+
alignSelf: 'flex-end',
|
|
476
|
+
},
|
|
477
|
+
turnAssistant: {
|
|
478
|
+
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
479
|
+
borderColor: 'rgba(255,255,255,0.10)',
|
|
480
|
+
borderWidth: 1,
|
|
481
|
+
alignSelf: 'flex-start',
|
|
482
|
+
},
|
|
483
|
+
turnStatus: {
|
|
484
|
+
backgroundColor: 'transparent',
|
|
485
|
+
alignSelf: 'flex-start',
|
|
486
|
+
paddingHorizontal: 4,
|
|
487
|
+
},
|
|
488
|
+
turnText: { color: '#f1f5f9', fontSize: 14, lineHeight: 20 },
|
|
489
|
+
spinnerRow: {
|
|
490
|
+
flexDirection: 'row',
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
marginTop: 8,
|
|
493
|
+
paddingHorizontal: 4,
|
|
494
|
+
},
|
|
495
|
+
spinnerText: { color: '#9ca3af', fontSize: 12, marginLeft: 8 },
|
|
496
|
+
footer: {
|
|
497
|
+
borderTopWidth: 1,
|
|
498
|
+
borderTopColor: 'rgba(255,255,255,0.08)',
|
|
499
|
+
padding: 10,
|
|
500
|
+
},
|
|
501
|
+
input: {
|
|
502
|
+
minHeight: 40,
|
|
503
|
+
maxHeight: 120,
|
|
504
|
+
color: '#f1f5f9',
|
|
505
|
+
fontSize: 14,
|
|
506
|
+
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
507
|
+
borderRadius: 10,
|
|
508
|
+
paddingHorizontal: 12,
|
|
509
|
+
paddingVertical: 8,
|
|
510
|
+
},
|
|
511
|
+
actions: {
|
|
512
|
+
flexDirection: 'row',
|
|
513
|
+
justifyContent: 'flex-end',
|
|
514
|
+
marginTop: 8,
|
|
515
|
+
},
|
|
516
|
+
actionBtn: {
|
|
517
|
+
paddingHorizontal: 14,
|
|
518
|
+
paddingVertical: 8,
|
|
519
|
+
borderRadius: 10,
|
|
520
|
+
marginLeft: 8,
|
|
521
|
+
},
|
|
522
|
+
actionBtnDisabled: { opacity: 0.5 },
|
|
523
|
+
reloadBtn: { backgroundColor: 'rgba(255,255,255,0.08)' },
|
|
524
|
+
voiceBtn: { backgroundColor: 'rgba(16,185,129,0.18)' },
|
|
525
|
+
voiceBtnActive: { backgroundColor: '#ef4444' },
|
|
526
|
+
engineToggle: { backgroundColor: 'rgba(255,255,255,0.06)', marginRight: 'auto', marginLeft: 0 },
|
|
527
|
+
engineToggleText: { color: '#cbd5e1', fontSize: 12, fontWeight: '600' },
|
|
528
|
+
engineCaption: { color: '#9ca3af', fontSize: 11, marginBottom: 6, marginLeft: 4 },
|
|
529
|
+
sendBtn: { backgroundColor: '#7582f5' },
|
|
530
|
+
actionText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
|
531
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface BuildFeedbackPromptInput {
|
|
2
|
+
userPrompt: string;
|
|
3
|
+
/** Hot-Reload project name when running inside Yaver mobile, OR
|
|
4
|
+
* the host app's bundle/package name when running standalone. */
|
|
5
|
+
projectName?: string;
|
|
6
|
+
/** Absolute path on the host where the project lives (only known
|
|
7
|
+
* when running inside Yaver mobile via Hot Reload). */
|
|
8
|
+
projectPath?: string;
|
|
9
|
+
/** True when the caller has attached a screenshot of the current
|
|
10
|
+
* screen as the first image in the task's images array. */
|
|
11
|
+
hasScreenshot: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildFeedbackPrompt(input: BuildFeedbackPromptInput): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// buildFeedbackPrompt — shared prompt enrichment used by every Yaver
|
|
3
|
+
// feedback surface, in-Yaver native pane (mirrored in Swift + Kotlin)
|
|
4
|
+
// AND the standalone RN feedback SDK (this file). Keep all three
|
|
5
|
+
// implementations in lockstep — the wording is what the AI on the
|
|
6
|
+
// remote is conditioned to expect.
|
|
7
|
+
//
|
|
8
|
+
// The bare user text on its own loses crucial context: WHICH app the
|
|
9
|
+
// user is testing, WHICH screen they're looking at, and whether a
|
|
10
|
+
// screenshot is attached for visual reference. Without that the agent
|
|
11
|
+
// guesses, edits the wrong project, or asks clarifying questions
|
|
12
|
+
// instead of acting. The wrapper below tells the agent:
|
|
13
|
+
// - this feedback comes from the in-app drawer while the user is
|
|
14
|
+
// mid-test,
|
|
15
|
+
// - which project the user is in (when known),
|
|
16
|
+
// - that the FIRST attached image (when present) is a snapshot of
|
|
17
|
+
// the current screen — open it to see what the user is pointing
|
|
18
|
+
// at,
|
|
19
|
+
// - that changes should be applied to that project's source +
|
|
20
|
+
// saved so the user can trigger a Hermes reload to see them.
|
|
21
|
+
//
|
|
22
|
+
// Cross-reference: mobile/ios/Yaver/YaverFeedbackPane.swift's
|
|
23
|
+
// `buildFeedbackPrompt` and mobile/android/.../YaverFeedbackPane.kt's
|
|
24
|
+
// `buildFeedbackPrompt`. All three must match.
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.buildFeedbackPrompt = buildFeedbackPrompt;
|
|
27
|
+
function buildFeedbackPrompt(input) {
|
|
28
|
+
const userPrompt = input.userPrompt ?? "";
|
|
29
|
+
const projectName = (input.projectName ?? "").trim();
|
|
30
|
+
const projectPath = (input.projectPath ?? "").trim();
|
|
31
|
+
const hasScreenshot = !!input.hasScreenshot;
|
|
32
|
+
const lines = [];
|
|
33
|
+
lines.push("[Mobile feedback from inside Yaver]");
|
|
34
|
+
lines.push("The user is providing this feedback while running a mobile app inside the Yaver mobile container " +
|
|
35
|
+
"and is currently looking at a specific screen of that app.");
|
|
36
|
+
lines.push("");
|
|
37
|
+
if (projectName || projectPath) {
|
|
38
|
+
lines.push("App being tested:");
|
|
39
|
+
if (projectName)
|
|
40
|
+
lines.push(` name: ${projectName}`);
|
|
41
|
+
if (projectPath)
|
|
42
|
+
lines.push(` path: ${projectPath}`);
|
|
43
|
+
lines.push("");
|
|
44
|
+
}
|
|
45
|
+
if (hasScreenshot) {
|
|
46
|
+
lines.push("A screenshot of the current screen is attached as the first image. " +
|
|
47
|
+
"Open it before deciding what to change — the user is pointing at what they SEE, " +
|
|
48
|
+
"not necessarily what is named most prominently in the source.");
|
|
49
|
+
lines.push("");
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
lines.push("(The user chose not to attach a screenshot for this round.)");
|
|
53
|
+
lines.push("");
|
|
54
|
+
}
|
|
55
|
+
lines.push("Operation contract:");
|
|
56
|
+
lines.push("1. Locate the file(s) responsible for what the user described and EDIT them in place. " +
|
|
57
|
+
"Save the changes — that is the deliverable.");
|
|
58
|
+
lines.push("2. Stream a CONCISE Claude-Code / Codex-style narration as you work: " +
|
|
59
|
+
"one short line per step (e.g. \"Reading app/index.tsx\", " +
|
|
60
|
+
"\"Editing safe.backgroundColor\", \"Saved app/index.tsx\"). Show small diffs only — " +
|
|
61
|
+
"never dump entire files, never paste node_modules contents, never echo build / install logs.");
|
|
62
|
+
lines.push("3. Do NOT run npm install / yarn / pnpm / git clone / cargo build / docker pull or any other " +
|
|
63
|
+
"long-running install / fetch command. The repo is already prepared on this machine. " +
|
|
64
|
+
"If a dependency is genuinely missing, say so in one line and stop — the user will install it.");
|
|
65
|
+
lines.push("4. Do NOT trigger a Hermes reload yourself. The user has a Reload button in the drawer " +
|
|
66
|
+
"and decides when to refresh.");
|
|
67
|
+
lines.push("5. Keep total output under a few hundred lines. Heavy ripgrep / find / cat with no filter " +
|
|
68
|
+
"are usually the wrong tool — use targeted reads.");
|
|
69
|
+
if (!projectName && !projectPath) {
|
|
70
|
+
lines.push("6. If you can identify the project from the prompt or the screenshot, work there. " +
|
|
71
|
+
"Otherwise ask the user briefly which project to target — one short line, no exhaustive list.");
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("User feedback:");
|
|
75
|
+
lines.push(userPrompt);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
package/dist/capture.d.ts
CHANGED
|
@@ -22,6 +22,20 @@
|
|
|
22
22
|
* modal. See `FeedbackModal.handleScreenshotForFix`.
|
|
23
23
|
*/
|
|
24
24
|
export declare function captureScreenshot(): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Capture a screenshot AND return it as base64 + mime type, ready to
|
|
27
|
+
* embed in a `/tasks` payload's `images` array. Used by the converged
|
|
28
|
+
* vibe-feedback flow (FeedbackModal → P2PClient.createFeedbackTask).
|
|
29
|
+
*
|
|
30
|
+
* Returns null when capture isn't possible (peer dep missing / user
|
|
31
|
+
* permission denied / running in a context where view-shot can't
|
|
32
|
+
* grab the screen). Caller should treat null as "send without
|
|
33
|
+
* screenshot" rather than aborting the whole feedback.
|
|
34
|
+
*/
|
|
35
|
+
export declare function captureScreenshotBase64(): Promise<{
|
|
36
|
+
base64: string;
|
|
37
|
+
mimeType: string;
|
|
38
|
+
} | null>;
|
|
25
39
|
export interface PickedFeedbackFile {
|
|
26
40
|
path: string;
|
|
27
41
|
name: string;
|
|
@@ -74,3 +88,9 @@ export declare function stopAudioRecording(): Promise<{
|
|
|
74
88
|
} | null>;
|
|
75
89
|
/** Whether a voice-note recording is currently active. */
|
|
76
90
|
export declare function isAudioRecording(): boolean;
|
|
91
|
+
/** Begin a raw-PCM recording for the voice stream. */
|
|
92
|
+
export declare function startPcmRecording(): Promise<void>;
|
|
93
|
+
/** Stop the voice recording; returns the WAV file:// URI (or null). */
|
|
94
|
+
export declare function stopPcmRecording(): Promise<string | null>;
|
|
95
|
+
/** Whether a voice-stream recording is currently active. */
|
|
96
|
+
export declare function isPcmRecording(): boolean;
|