@qafka/react-native 2.0.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/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +92 -0
- package/LICENSE +22 -0
- package/README.md +109 -0
- package/SECURITY.md +67 -0
- package/android/build.gradle +35 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
- package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
- package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
- package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
- package/app.plugin.js +1 -0
- package/dist/QafkaSDK.d.ts +174 -0
- package/dist/QafkaSDK.js +461 -0
- package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
- package/dist/cards/bindings/resolveFieldName.js +82 -0
- package/dist/cards/cta/CardContext.d.ts +16 -0
- package/dist/cards/cta/CardContext.js +58 -0
- package/dist/cards/cta/dispatcher.d.ts +7 -0
- package/dist/cards/cta/dispatcher.js +90 -0
- package/dist/cards/cta/types.d.ts +66 -0
- package/dist/cards/cta/types.js +2 -0
- package/dist/cards/index.d.ts +20 -0
- package/dist/cards/index.js +34 -0
- package/dist/cards/primitives/QButton.d.ts +10 -0
- package/dist/cards/primitives/QButton.js +115 -0
- package/dist/cards/primitives/QDivider.d.ts +7 -0
- package/dist/cards/primitives/QDivider.js +17 -0
- package/dist/cards/primitives/QIcon.d.ts +13 -0
- package/dist/cards/primitives/QIcon.js +26 -0
- package/dist/cards/primitives/QImage.d.ts +9 -0
- package/dist/cards/primitives/QImage.js +22 -0
- package/dist/cards/primitives/QText.d.ts +9 -0
- package/dist/cards/primitives/QText.js +30 -0
- package/dist/cards/primitives/QView.d.ts +8 -0
- package/dist/cards/primitives/QView.js +19 -0
- package/dist/cards/renderer/CardRenderer.d.ts +19 -0
- package/dist/cards/renderer/CardRenderer.js +64 -0
- package/dist/cards/renderer/renderNode.d.ts +13 -0
- package/dist/cards/renderer/renderNode.js +42 -0
- package/dist/cards/types.d.ts +110 -0
- package/dist/cards/types.js +6 -0
- package/dist/components/ActionResultBadge.d.ts +12 -0
- package/dist/components/ActionResultBadge.js +58 -0
- package/dist/components/ChatPage.d.ts +44 -0
- package/dist/components/ChatPage.js +84 -0
- package/dist/components/DataChip.d.ts +8 -0
- package/dist/components/DataChip.js +80 -0
- package/dist/components/DataChipList.d.ts +13 -0
- package/dist/components/DataChipList.js +21 -0
- package/dist/components/FloatingButton.d.ts +11 -0
- package/dist/components/FloatingButton.js +162 -0
- package/dist/components/InputArea.d.ts +57 -0
- package/dist/components/InputArea.js +142 -0
- package/dist/components/MarkdownText.d.ts +15 -0
- package/dist/components/MarkdownText.js +283 -0
- package/dist/components/MessageBubble.d.ts +134 -0
- package/dist/components/MessageBubble.js +384 -0
- package/dist/components/NavigationSuggestion.d.ts +11 -0
- package/dist/components/NavigationSuggestion.js +109 -0
- package/dist/components/Qafka.d.ts +39 -0
- package/dist/components/Qafka.handlers.d.ts +21 -0
- package/dist/components/Qafka.handlers.js +54 -0
- package/dist/components/Qafka.js +493 -0
- package/dist/components/Qafka.styles.d.ts +19 -0
- package/dist/components/Qafka.styles.js +101 -0
- package/dist/components/Qafka.types.d.ts +744 -0
- package/dist/components/Qafka.types.js +2 -0
- package/dist/components/Qafka.utils.d.ts +7 -0
- package/dist/components/Qafka.utils.js +34 -0
- package/dist/components/QafkaProvider.d.ts +12 -0
- package/dist/components/QafkaProvider.js +87 -0
- package/dist/components/QuickReplies.d.ts +14 -0
- package/dist/components/QuickReplies.js +48 -0
- package/dist/components/StepProgressIndicator.d.ts +12 -0
- package/dist/components/StepProgressIndicator.js +48 -0
- package/dist/components/SuggestionButton.d.ts +42 -0
- package/dist/components/SuggestionButton.js +67 -0
- package/dist/components/ToolStatusPill.d.ts +20 -0
- package/dist/components/ToolStatusPill.js +43 -0
- package/dist/components/TypingIndicator.d.ts +28 -0
- package/dist/components/TypingIndicator.js +109 -0
- package/dist/components/VoicePage.d.ts +48 -0
- package/dist/components/VoicePage.js +683 -0
- package/dist/components/defaults/DefaultCard.d.ts +14 -0
- package/dist/components/defaults/DefaultCard.js +156 -0
- package/dist/components/defaults/DefaultDetail.d.ts +14 -0
- package/dist/components/defaults/DefaultDetail.js +138 -0
- package/dist/components/defaults/DefaultList.d.ts +12 -0
- package/dist/components/defaults/DefaultList.js +98 -0
- package/dist/components/defaults/DefaultTable.d.ts +14 -0
- package/dist/components/defaults/DefaultTable.js +204 -0
- package/dist/components/defaults/index.d.ts +14 -0
- package/dist/components/defaults/index.js +25 -0
- package/dist/components/index.d.ts +22 -0
- package/dist/components/index.js +36 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +13 -0
- package/dist/hooks/useChatMessages.d.ts +72 -0
- package/dist/hooks/useChatMessages.js +505 -0
- package/dist/hooks/useContextManager.d.ts +12 -0
- package/dist/hooks/useContextManager.js +46 -0
- package/dist/hooks/useProjectTheme.d.ts +19 -0
- package/dist/hooks/useProjectTheme.js +163 -0
- package/dist/hooks/useSDK.d.ts +31 -0
- package/dist/hooks/useSDK.js +103 -0
- package/dist/hooks/useVoiceChat.d.ts +110 -0
- package/dist/hooks/useVoiceChat.js +436 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +59 -0
- package/dist/native/QafkaAttestation.d.ts +23 -0
- package/dist/native/QafkaAttestation.js +70 -0
- package/dist/native/QafkaAudio.d.ts +14 -0
- package/dist/native/QafkaAudio.js +31 -0
- package/dist/native/QafkaClipboard.d.ts +11 -0
- package/dist/native/QafkaClipboard.js +14 -0
- package/dist/native/QafkaStorage.d.ts +15 -0
- package/dist/native/QafkaStorage.js +12 -0
- package/dist/resolve-project-config.d.ts +35 -0
- package/dist/resolve-project-config.js +41 -0
- package/dist/runtime-config-loader.d.ts +37 -0
- package/dist/runtime-config-loader.js +53 -0
- package/dist/services/AttestationManager.d.ts +38 -0
- package/dist/services/AttestationManager.js +296 -0
- package/dist/services/BackendService.d.ts +156 -0
- package/dist/services/BackendService.js +755 -0
- package/dist/services/ConversationManager.d.ts +43 -0
- package/dist/services/ConversationManager.js +96 -0
- package/dist/services/NavigationHandler.d.ts +29 -0
- package/dist/services/NavigationHandler.js +70 -0
- package/dist/services/RealtimeService.d.ts +83 -0
- package/dist/services/RealtimeService.js +203 -0
- package/dist/services/storage.d.ts +11 -0
- package/dist/services/storage.js +15 -0
- package/dist/services/storageCore.d.ts +17 -0
- package/dist/services/storageCore.js +46 -0
- package/dist/themes/dark.d.ts +5 -0
- package/dist/themes/dark.js +129 -0
- package/dist/themes/index.d.ts +12 -0
- package/dist/themes/index.js +33 -0
- package/dist/themes/light.d.ts +5 -0
- package/dist/themes/light.js +129 -0
- package/dist/themes/types.d.ts +155 -0
- package/dist/themes/types.js +5 -0
- package/dist/types/chat.d.ts +126 -0
- package/dist/types/chat.js +5 -0
- package/dist/types/components.d.ts +56 -0
- package/dist/types/components.js +16 -0
- package/dist/types/external-navigation.d.ts +19 -0
- package/dist/types/external-navigation.js +8 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +25 -0
- package/dist/types/navigation.d.ts +86 -0
- package/dist/types/navigation.js +5 -0
- package/dist/types/sdk.d.ts +36 -0
- package/dist/types/sdk.js +5 -0
- package/dist/utils/deepMerge.d.ts +46 -0
- package/dist/utils/deepMerge.js +70 -0
- package/dist/utils/fontUtils.d.ts +8 -0
- package/dist/utils/fontUtils.js +16 -0
- package/dist/validate-end-user.d.ts +18 -0
- package/dist/validate-end-user.js +74 -0
- package/expo-plugin/withQafkaAttestation.js +57 -0
- package/ios/QafkaAttestation.m +25 -0
- package/ios/QafkaAttestation.swift +128 -0
- package/ios/QafkaAudio.m +23 -0
- package/ios/QafkaAudio.swift +519 -0
- package/ios/QafkaClipboard.m +10 -0
- package/ios/QafkaClipboard.swift +21 -0
- package/ios/QafkaReactImports.h +2 -0
- package/ios/QafkaStorage.m +26 -0
- package/ios/QafkaStorage.swift +118 -0
- package/package.json +82 -0
- package/qafka.config.d.ts +9 -0
- package/qafka.config.js +9 -0
- package/react-native-qafka.podspec +28 -0
- package/react-native.config.js +14 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
package com.qafka.audio
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.media.AudioFormat
|
|
6
|
+
import android.media.AudioRecord
|
|
7
|
+
import android.media.AudioTrack
|
|
8
|
+
import android.media.MediaRecorder
|
|
9
|
+
import android.media.audiofx.AcousticEchoCanceler
|
|
10
|
+
import android.media.audiofx.AutomaticGainControl
|
|
11
|
+
import android.media.audiofx.NoiseSuppressor
|
|
12
|
+
import android.util.Base64
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import com.facebook.react.bridge.*
|
|
16
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
17
|
+
|
|
18
|
+
class QafkaAudioModule(reactContext: ReactApplicationContext) :
|
|
19
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
20
|
+
|
|
21
|
+
override fun getName(): String = "QafkaAudio"
|
|
22
|
+
|
|
23
|
+
private var audioRecord: AudioRecord? = null
|
|
24
|
+
private var audioTrack: AudioTrack? = null
|
|
25
|
+
private var captureThread: Thread? = null
|
|
26
|
+
@Volatile private var isCapturing = false
|
|
27
|
+
private var listenerCount = 0
|
|
28
|
+
|
|
29
|
+
// iOS VPIO equivalent — hardware AEC + NS + AGC (android.media.audiofx).
|
|
30
|
+
// Enables barge-in by cancelling playback echo at mic level.
|
|
31
|
+
// Device-dependent; isAvailable() + null checks keep it safe.
|
|
32
|
+
private var echoCanceler: AcousticEchoCanceler? = null
|
|
33
|
+
private var noiseSuppressor: NoiseSuppressor? = null
|
|
34
|
+
private var automaticGainControl: AutomaticGainControl? = null
|
|
35
|
+
|
|
36
|
+
companion object {
|
|
37
|
+
private const val TAG = "QafkaAudio"
|
|
38
|
+
private const val CAPTURE_SAMPLE_RATE = 16000
|
|
39
|
+
private const val PLAYBACK_SAMPLE_RATE = 24000
|
|
40
|
+
private const val CHANNEL_IN = AudioFormat.CHANNEL_IN_MONO
|
|
41
|
+
private const val CHANNEL_OUT = AudioFormat.CHANNEL_OUT_MONO
|
|
42
|
+
private const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* True when the consumer app is built in debug mode. Used to gate noisy
|
|
47
|
+
* info/warn logs so they never reach release builds. Real errors still
|
|
48
|
+
* call Log.e directly.
|
|
49
|
+
*/
|
|
50
|
+
private val isDebugBuild: Boolean by lazy {
|
|
51
|
+
(reactApplicationContext.applicationInfo.flags and
|
|
52
|
+
android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun dlogI(message: String) {
|
|
56
|
+
if (isDebugBuild) Log.i(TAG, message)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun dlogW(message: String) {
|
|
60
|
+
if (isDebugBuild) Log.w(TAG, message)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Event Emitter support
|
|
64
|
+
|
|
65
|
+
@ReactMethod
|
|
66
|
+
fun addListener(eventName: String) {
|
|
67
|
+
listenerCount++
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@ReactMethod
|
|
71
|
+
fun removeListeners(count: Int) {
|
|
72
|
+
listenerCount -= count
|
|
73
|
+
if (listenerCount < 0) listenerCount = 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private fun sendEvent(eventName: String, params: WritableMap) {
|
|
77
|
+
if (listenerCount > 0) {
|
|
78
|
+
reactApplicationContext
|
|
79
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
80
|
+
.emit(eventName, params)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - Capture
|
|
85
|
+
|
|
86
|
+
@ReactMethod
|
|
87
|
+
fun startCapture(promise: Promise) {
|
|
88
|
+
if (isCapturing) {
|
|
89
|
+
promise.resolve(true)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check permission
|
|
94
|
+
val permission = ContextCompat.checkSelfPermission(
|
|
95
|
+
reactApplicationContext,
|
|
96
|
+
Manifest.permission.RECORD_AUDIO
|
|
97
|
+
)
|
|
98
|
+
if (permission != PackageManager.PERMISSION_GRANTED) {
|
|
99
|
+
promise.reject("PERMISSION_DENIED", "RECORD_AUDIO permission not granted")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
val minBufferSize = AudioRecord.getMinBufferSize(
|
|
104
|
+
CAPTURE_SAMPLE_RATE, CHANNEL_IN, AUDIO_ENCODING
|
|
105
|
+
)
|
|
106
|
+
if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
|
|
107
|
+
promise.reject("AUDIO_ERROR", "Failed to get minimum buffer size for AudioRecord")
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
val bufferSize = maxOf(minBufferSize, 4096)
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
val record = AudioRecord(
|
|
115
|
+
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
|
|
116
|
+
CAPTURE_SAMPLE_RATE,
|
|
117
|
+
CHANNEL_IN,
|
|
118
|
+
AUDIO_ENCODING,
|
|
119
|
+
bufferSize
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (record.state != AudioRecord.STATE_INITIALIZED) {
|
|
123
|
+
record.release()
|
|
124
|
+
promise.reject("AUDIO_ERROR", "Failed to initialize AudioRecord")
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// iOS VPIO equivalent — hardware AEC + NS + AGC, bound to AudioRecord sessionId.
|
|
129
|
+
val sessionId = record.audioSessionId
|
|
130
|
+
|
|
131
|
+
if (AcousticEchoCanceler.isAvailable()) {
|
|
132
|
+
echoCanceler = AcousticEchoCanceler.create(sessionId)?.also {
|
|
133
|
+
it.enabled = true
|
|
134
|
+
dlogI("✅ AcousticEchoCanceler enabled (sessionId=$sessionId)")
|
|
135
|
+
}
|
|
136
|
+
if (echoCanceler == null) dlogW("⚠️ AcousticEchoCanceler.create returned null")
|
|
137
|
+
} else {
|
|
138
|
+
dlogW("⚠️ AcousticEchoCanceler NOT available on this device")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (NoiseSuppressor.isAvailable()) {
|
|
142
|
+
noiseSuppressor = NoiseSuppressor.create(sessionId)?.also {
|
|
143
|
+
it.enabled = true
|
|
144
|
+
dlogI("✅ NoiseSuppressor enabled")
|
|
145
|
+
}
|
|
146
|
+
if (noiseSuppressor == null) dlogW("⚠️ NoiseSuppressor.create returned null")
|
|
147
|
+
} else {
|
|
148
|
+
dlogW("⚠️ NoiseSuppressor NOT available on this device")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (AutomaticGainControl.isAvailable()) {
|
|
152
|
+
automaticGainControl = AutomaticGainControl.create(sessionId)?.also {
|
|
153
|
+
it.enabled = true
|
|
154
|
+
dlogI("✅ AutomaticGainControl enabled")
|
|
155
|
+
}
|
|
156
|
+
if (automaticGainControl == null) dlogW("⚠️ AutomaticGainControl.create returned null")
|
|
157
|
+
} else {
|
|
158
|
+
dlogW("⚠️ AutomaticGainControl NOT available on this device")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Initialize AudioTrack for playback
|
|
162
|
+
val playMinBuffer = AudioTrack.getMinBufferSize(
|
|
163
|
+
PLAYBACK_SAMPLE_RATE, CHANNEL_OUT, AUDIO_ENCODING
|
|
164
|
+
)
|
|
165
|
+
val playBufferSize = maxOf(playMinBuffer, 4096)
|
|
166
|
+
|
|
167
|
+
val track = AudioTrack.Builder()
|
|
168
|
+
.setAudioFormat(
|
|
169
|
+
AudioFormat.Builder()
|
|
170
|
+
.setSampleRate(PLAYBACK_SAMPLE_RATE)
|
|
171
|
+
.setChannelMask(CHANNEL_OUT)
|
|
172
|
+
.setEncoding(AUDIO_ENCODING)
|
|
173
|
+
.build()
|
|
174
|
+
)
|
|
175
|
+
.setBufferSizeInBytes(playBufferSize)
|
|
176
|
+
.setTransferMode(AudioTrack.MODE_STREAM)
|
|
177
|
+
.build()
|
|
178
|
+
|
|
179
|
+
track.play()
|
|
180
|
+
|
|
181
|
+
audioRecord = record
|
|
182
|
+
audioTrack = track
|
|
183
|
+
isCapturing = true
|
|
184
|
+
|
|
185
|
+
record.startRecording()
|
|
186
|
+
|
|
187
|
+
// Start capture thread
|
|
188
|
+
captureThread = Thread {
|
|
189
|
+
val readBuffer = ShortArray(bufferSize / 2)
|
|
190
|
+
|
|
191
|
+
while (isCapturing) {
|
|
192
|
+
val shortsRead = record.read(readBuffer, 0, readBuffer.size)
|
|
193
|
+
if (shortsRead > 0) {
|
|
194
|
+
// Convert shorts to bytes (little-endian PCM 16-bit)
|
|
195
|
+
val byteBuffer = ByteArray(shortsRead * 2)
|
|
196
|
+
for (i in 0 until shortsRead) {
|
|
197
|
+
byteBuffer[i * 2] = (readBuffer[i].toInt() and 0xFF).toByte()
|
|
198
|
+
byteBuffer[i * 2 + 1] = (readBuffer[i].toInt() shr 8 and 0xFF).toByte()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
val base64 = Base64.encodeToString(byteBuffer, Base64.NO_WRAP)
|
|
202
|
+
val params = Arguments.createMap().apply {
|
|
203
|
+
putString("audio", base64)
|
|
204
|
+
}
|
|
205
|
+
sendEvent("onAudioData", params)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}.apply {
|
|
209
|
+
name = "QafkaAudioCapture"
|
|
210
|
+
start()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
promise.resolve(true)
|
|
214
|
+
} catch (e: Exception) {
|
|
215
|
+
promise.reject("AUDIO_ERROR", "Failed to start capture: ${e.message}", e)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// MARK: - Stop Capture
|
|
220
|
+
|
|
221
|
+
@ReactMethod
|
|
222
|
+
fun stopCapture(promise: Promise) {
|
|
223
|
+
isCapturing = false
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
captureThread?.join(2000)
|
|
227
|
+
} catch (_: InterruptedException) {}
|
|
228
|
+
captureThread = null
|
|
229
|
+
|
|
230
|
+
// Release audio effects BEFORE AudioRecord — they depend on its sessionId
|
|
231
|
+
try { echoCanceler?.release() } catch (_: Exception) {}
|
|
232
|
+
echoCanceler = null
|
|
233
|
+
try { noiseSuppressor?.release() } catch (_: Exception) {}
|
|
234
|
+
noiseSuppressor = null
|
|
235
|
+
try { automaticGainControl?.release() } catch (_: Exception) {}
|
|
236
|
+
automaticGainControl = null
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
audioRecord?.stop()
|
|
240
|
+
audioRecord?.release()
|
|
241
|
+
} catch (_: Exception) {}
|
|
242
|
+
audioRecord = null
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
audioTrack?.stop()
|
|
246
|
+
audioTrack?.release()
|
|
247
|
+
} catch (_: Exception) {}
|
|
248
|
+
audioTrack = null
|
|
249
|
+
|
|
250
|
+
promise.resolve(true)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// MARK: - Playback
|
|
254
|
+
|
|
255
|
+
@ReactMethod
|
|
256
|
+
fun playAudioChunk(base64Data: String, promise: Promise) {
|
|
257
|
+
val track = audioTrack
|
|
258
|
+
if (track == null) {
|
|
259
|
+
promise.reject("NOT_RUNNING", "Audio is not running. Call startCapture first.")
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
val audioBytes = Base64.decode(base64Data, Base64.DEFAULT)
|
|
265
|
+
track.write(audioBytes, 0, audioBytes.size)
|
|
266
|
+
promise.resolve(true)
|
|
267
|
+
} catch (e: Exception) {
|
|
268
|
+
promise.reject("PLAYBACK_ERROR", "Failed to play audio chunk: ${e.message}", e)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MARK: - Stop Playback
|
|
273
|
+
|
|
274
|
+
@ReactMethod
|
|
275
|
+
fun stopPlayback(promise: Promise) {
|
|
276
|
+
val track = audioTrack ?: run {
|
|
277
|
+
promise.resolve(true)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
track.pause()
|
|
283
|
+
track.flush()
|
|
284
|
+
track.play()
|
|
285
|
+
promise.resolve(true)
|
|
286
|
+
} catch (e: Exception) {
|
|
287
|
+
promise.reject("PLAYBACK_ERROR", "Failed to stop playback: ${e.message}", e)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.qafka.clipboard
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
8
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
9
|
+
import com.facebook.react.bridge.ReactMethod
|
|
10
|
+
|
|
11
|
+
class QafkaClipboardModule(reactContext: ReactApplicationContext) :
|
|
12
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
13
|
+
|
|
14
|
+
override fun getName(): String = "QafkaClipboard"
|
|
15
|
+
|
|
16
|
+
@ReactMethod
|
|
17
|
+
fun setString(value: String, promise: Promise) {
|
|
18
|
+
try {
|
|
19
|
+
val clipboard = reactApplicationContext.getSystemService(
|
|
20
|
+
Context.CLIPBOARD_SERVICE
|
|
21
|
+
) as ClipboardManager
|
|
22
|
+
clipboard.setPrimaryClip(ClipData.newPlainText("Qafka", value))
|
|
23
|
+
promise.resolve(null)
|
|
24
|
+
} catch (e: Exception) {
|
|
25
|
+
promise.reject("CLIPBOARD_WRITE_FAILED", e.message, e)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package com.qafka.storage
|
|
2
|
+
|
|
3
|
+
import android.content.SharedPreferences
|
|
4
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
5
|
+
import androidx.security.crypto.MasterKey
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.facebook.react.bridge.Promise
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
10
|
+
import com.facebook.react.bridge.ReactMethod
|
|
11
|
+
import com.facebook.react.bridge.ReadableArray
|
|
12
|
+
|
|
13
|
+
class QafkaStorageModule(reactContext: ReactApplicationContext) :
|
|
14
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
15
|
+
|
|
16
|
+
override fun getName(): String = "QafkaStorage"
|
|
17
|
+
|
|
18
|
+
private val prefs: SharedPreferences by lazy {
|
|
19
|
+
val masterKey = MasterKey.Builder(reactApplicationContext)
|
|
20
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
21
|
+
.build()
|
|
22
|
+
|
|
23
|
+
EncryptedSharedPreferences.create(
|
|
24
|
+
reactApplicationContext,
|
|
25
|
+
FILE_NAME,
|
|
26
|
+
masterKey,
|
|
27
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
28
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun getItem(key: String, promise: Promise) {
|
|
34
|
+
try {
|
|
35
|
+
val value = prefs.getString(key, null)
|
|
36
|
+
promise.resolve(value)
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
promise.reject("STORAGE_READ_FAILED", e.message, e)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@ReactMethod
|
|
43
|
+
fun setItem(key: String, value: String, promise: Promise) {
|
|
44
|
+
try {
|
|
45
|
+
prefs.edit().putString(key, value).apply()
|
|
46
|
+
promise.resolve(null)
|
|
47
|
+
} catch (e: Exception) {
|
|
48
|
+
promise.reject("STORAGE_WRITE_FAILED", e.message, e)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@ReactMethod
|
|
53
|
+
fun removeItem(key: String, promise: Promise) {
|
|
54
|
+
try {
|
|
55
|
+
prefs.edit().remove(key).apply()
|
|
56
|
+
promise.resolve(null)
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
promise.reject("STORAGE_DELETE_FAILED", e.message, e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@ReactMethod
|
|
63
|
+
fun multiRemove(keys: ReadableArray, promise: Promise) {
|
|
64
|
+
try {
|
|
65
|
+
val editor = prefs.edit()
|
|
66
|
+
for (i in 0 until keys.size()) {
|
|
67
|
+
val key = keys.getString(i) ?: continue
|
|
68
|
+
editor.remove(key)
|
|
69
|
+
}
|
|
70
|
+
editor.apply()
|
|
71
|
+
promise.resolve(null)
|
|
72
|
+
} catch (e: Exception) {
|
|
73
|
+
promise.reject("STORAGE_DELETE_FAILED", e.message, e)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
companion object {
|
|
78
|
+
private const val FILE_NAME = "qafka_secure_prefs"
|
|
79
|
+
}
|
|
80
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./expo-plugin/withQafkaAttestation');
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { SDKConfig, SDKStatusType, ChatMessage, ChatResponse } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* QafkaSDK - Main SDK class
|
|
4
|
+
* Provides AI-powered conversational interface for React Native apps
|
|
5
|
+
*/
|
|
6
|
+
export declare class QafkaSDK {
|
|
7
|
+
private static instance;
|
|
8
|
+
private config;
|
|
9
|
+
private status;
|
|
10
|
+
private subProjectId;
|
|
11
|
+
private backendService;
|
|
12
|
+
private attestationManager;
|
|
13
|
+
private conversationManager;
|
|
14
|
+
private navigationHandler;
|
|
15
|
+
private themePrefetchPromise;
|
|
16
|
+
private constructor();
|
|
17
|
+
/**
|
|
18
|
+
* Get singleton instance
|
|
19
|
+
*/
|
|
20
|
+
static getInstance(): QafkaSDK;
|
|
21
|
+
/**
|
|
22
|
+
* Initialize SDK
|
|
23
|
+
*/
|
|
24
|
+
initialize(config: SDKConfig): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* developer-driven locale (BCP 47 — e.g. "tr", "en", "tr-TR").
|
|
27
|
+
* Forwarded to BackendService so it lands in `sdkContext.locale` on every
|
|
28
|
+
* outgoing chat request, which the server then echoes into the user-message
|
|
29
|
+
* context prefix. Same value should drive UI strings on the SDK side.
|
|
30
|
+
*
|
|
31
|
+
* Pass `null` to clear (e.g. when switching back to instructions-default).
|
|
32
|
+
* Calling before initialize() is a no-op; pass `locale` via SDKConfig instead.
|
|
33
|
+
*/
|
|
34
|
+
setLocale(locale: string | null): void;
|
|
35
|
+
/**
|
|
36
|
+
* Tool Data Channel: register the partner-provided resolver
|
|
37
|
+
* that supplies opaque PII bags for `{{tooldata.X}}` substitution on
|
|
38
|
+
* the backend. Called from `Qafka.tsx` whenever the `onToolDataRequested`
|
|
39
|
+
* prop changes. Pass `null` to clear.
|
|
40
|
+
*/
|
|
41
|
+
setOnToolDataRequested(resolver: ((tool: {
|
|
42
|
+
key: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}) => Record<string, unknown> | Promise<Record<string, unknown>>) | null): void;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the current device-attestation session token (or `null` when
|
|
47
|
+
* attestation is unavailable). Voice mode passes this in the auth frame so
|
|
48
|
+
* the realtime WS opens against an attested session.
|
|
49
|
+
*/
|
|
50
|
+
getSessionToken(): Promise<string | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Propagate the validated end-user identity from the React component
|
|
53
|
+
* into the transport layer. Called by `<Qafka />` in an effect whenever
|
|
54
|
+
* the `endUserId` / `endUserData` props change. Validation happens at
|
|
55
|
+
* the component boundary (`validate-end-user.ts`).
|
|
56
|
+
*/
|
|
57
|
+
setEndUser(endUserId: string, endUserData?: Record<string, unknown>): void;
|
|
58
|
+
/**
|
|
59
|
+
* Send a chat message
|
|
60
|
+
*/
|
|
61
|
+
sendMessage(message: string, context?: Record<string, any>, contextDescription?: string, isInitialMessage?: boolean): Promise<ChatResponse>;
|
|
62
|
+
/**
|
|
63
|
+
* Send message with streaming response
|
|
64
|
+
*/
|
|
65
|
+
sendMessageStream(message: string, onChunk: (chunk: string) => void, onComplete?: (response: ChatResponse) => void, onError?: (error: Error) => void, context?: Record<string, any>, contextDescription?: string, onToolSuggestions?: (tools: any[], conversationId?: string, messageId?: string) => void, // Tool suggestions callback
|
|
66
|
+
isInitialMessage?: boolean, onActionResult?: (results: Array<{
|
|
67
|
+
actionType: string;
|
|
68
|
+
success: boolean;
|
|
69
|
+
message: string;
|
|
70
|
+
}>) => void, // Action result callback
|
|
71
|
+
onStepCompleted?: (result: {
|
|
72
|
+
tool: string;
|
|
73
|
+
step: string;
|
|
74
|
+
data: Record<string, any>;
|
|
75
|
+
actionResults?: Array<{
|
|
76
|
+
actionType: string;
|
|
77
|
+
success: boolean;
|
|
78
|
+
message: string;
|
|
79
|
+
}>;
|
|
80
|
+
}) => void, // Step completed callback
|
|
81
|
+
onFileUploadRequest?: (request: any) => void, // File upload request callback
|
|
82
|
+
onExtractionResult?: (result: any) => void, // Extraction result callback
|
|
83
|
+
onToolStatus?: (event: {
|
|
84
|
+
toolKey: string;
|
|
85
|
+
message: string;
|
|
86
|
+
stage: string;
|
|
87
|
+
}) => void, onToolResultPayload?: (event: {
|
|
88
|
+
toolKey: string;
|
|
89
|
+
data: unknown;
|
|
90
|
+
uiConfig?: any;
|
|
91
|
+
}) => void, onFinalChunk?: (content: string) => void): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Get conversation history
|
|
94
|
+
*/
|
|
95
|
+
getConversationHistory(): Promise<ChatMessage[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Clear conversation history
|
|
98
|
+
*/
|
|
99
|
+
clearConversation(): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Start a new conversation
|
|
102
|
+
* Clears history and returns the new session ID
|
|
103
|
+
*/
|
|
104
|
+
startNewConversation(): Promise<string>;
|
|
105
|
+
/**
|
|
106
|
+
* Add tool response to conversation
|
|
107
|
+
* Use this after executing a tool to display the results
|
|
108
|
+
*/
|
|
109
|
+
addToolResponse(toolKey: string, data: any, tool: any): ChatMessage;
|
|
110
|
+
/**
|
|
111
|
+
* Post a tool result back to the backend so the AI can continue its turn.
|
|
112
|
+
*/
|
|
113
|
+
postToolResult(payload: {
|
|
114
|
+
conversationId: string;
|
|
115
|
+
messageId: string;
|
|
116
|
+
toolKey: string;
|
|
117
|
+
data: unknown;
|
|
118
|
+
sdkContext?: Record<string, unknown>;
|
|
119
|
+
}, onFinalChunk: (content: string) => void, onDone: () => void, onError: (err: Error) => void, onCard?: (card: {
|
|
120
|
+
templateId: string;
|
|
121
|
+
templateSlug: string;
|
|
122
|
+
definition: any;
|
|
123
|
+
data: any;
|
|
124
|
+
}) => void): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Upload a file attached to a tool flow.
|
|
127
|
+
*/
|
|
128
|
+
uploadFile(toolId: string, file: {
|
|
129
|
+
uri: string;
|
|
130
|
+
name: string;
|
|
131
|
+
type: string;
|
|
132
|
+
}, conversationId?: string): Promise<any>;
|
|
133
|
+
/**
|
|
134
|
+
* Get project theme configuration
|
|
135
|
+
*/
|
|
136
|
+
getProjectTheme(): Promise<any>;
|
|
137
|
+
/**
|
|
138
|
+
* Returns the in-flight theme prefetch (kicked off in initialize() parallel
|
|
139
|
+
* with attestation). Used by `useProjectTheme` to paint the branded greeting
|
|
140
|
+
* without waiting for `status === 'ready'`. Returns `null` if initialize()
|
|
141
|
+
* hasn't been called yet or if prefetch already settled.
|
|
142
|
+
*/
|
|
143
|
+
getThemePrefetch(): Promise<any> | null;
|
|
144
|
+
/**
|
|
145
|
+
* Fetches the project theme via BackendService and writes the result to
|
|
146
|
+
* persistent cache so subsequent chat opens paint instantly.
|
|
147
|
+
*/
|
|
148
|
+
private prefetchAndCacheTheme;
|
|
149
|
+
/**
|
|
150
|
+
* Get SDK status
|
|
151
|
+
*/
|
|
152
|
+
getStatus(): SDKStatusType;
|
|
153
|
+
/**
|
|
154
|
+
* Get SDK configuration
|
|
155
|
+
*/
|
|
156
|
+
getConfig(): SDKConfig | null;
|
|
157
|
+
/**
|
|
158
|
+
* Navigate to screen
|
|
159
|
+
*/
|
|
160
|
+
navigate(screenName: string, params?: any): void;
|
|
161
|
+
/**
|
|
162
|
+
* Generate unique ID
|
|
163
|
+
*/
|
|
164
|
+
private generateId;
|
|
165
|
+
/**
|
|
166
|
+
* Resolve API key from config
|
|
167
|
+
*/
|
|
168
|
+
private resolveApiKey;
|
|
169
|
+
/**
|
|
170
|
+
* Destroy SDK instance
|
|
171
|
+
*/
|
|
172
|
+
destroy(): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
export declare const getSDK: () => QafkaSDK;
|