Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df76e2af6 | |||
| f80fe1df93 | |||
| cff421bc53 | |||
| bca925d385 | |||
| 9abde89805 | |||
| ea4f639fcb | |||
| 64cd5f7d52 | |||
| 843ebe1d8f | |||
| 764619f076 | |||
| e3a0cfb55a | |||
| 2929749314 | |||
| 51b9512f4e | |||
| ffcfa44eef | |||
| 6363da97b1 | |||
| 07ed2cdcf6 | |||
| 5ad68b7dfc | |||
| 8a6ee018ea | |||
| b42590ff95 | |||
| 056b579c47 | |||
| 576e612cd0 | |||
| c2faa06a15 | |||
| d3ed3556eb | |||
| d960d125c0 | |||
| 89d5d7ec0a | |||
| ea0c13936b | |||
| 773c976822 | |||
| cd05ed2379 |
@@ -79,8 +79,8 @@ android {
|
|||||||
applicationId "com.ariacockpit"
|
applicationId "com.ariacockpit"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 208
|
versionCode 306
|
||||||
versionName "0.0.2.8"
|
versionName "0.0.3.6"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
@@ -24,5 +25,15 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
import com.facebook.react.bridge.Promise
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ApkInstallerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||||
|
override fun getName() = "ApkInstaller"
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
fun install(filePath: String, promise: Promise) {
|
||||||
|
try {
|
||||||
|
val file = File(filePath)
|
||||||
|
if (!file.exists()) {
|
||||||
|
promise.reject("FILE_NOT_FOUND", "APK nicht gefunden: $filePath")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = reactApplicationContext
|
||||||
|
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
|
} else {
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
promise.resolve(true)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
promise.reject("INSTALL_ERROR", e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ariacockpit
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.bridge.NativeModule
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.uimanager.ViewManager
|
||||||
|
|
||||||
|
class ApkInstallerPackage : ReactPackage {
|
||||||
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||||
|
return listOf(ApkInstallerModule(reactContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
object : DefaultReactNativeHost(this) {
|
object : DefaultReactNativeHost(this) {
|
||||||
override fun getPackages(): List<ReactPackage> =
|
override fun getPackages(): List<ReactPackage> =
|
||||||
PackageList(this).packages.apply {
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
add(ApkInstallerPackage())
|
||||||
// add(MyReactNativePackage())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getJSMainModuleName(): String = "index"
|
override fun getJSMainModuleName(): String = "index"
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="cache" path="." />
|
||||||
|
</paths>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.2.8",
|
"version": "0.0.3.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Datei- und Kamera-Upload.
|
* Datei- und Kamera-Upload.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Image,
|
Image,
|
||||||
|
ScrollView,
|
||||||
Modal,
|
Modal,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
@@ -94,6 +95,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchVisible, setSearchVisible] = useState(false);
|
const [searchVisible, setSearchVisible] = useState(false);
|
||||||
|
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
|
||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const messageIdCounter = useRef(0);
|
const messageIdCounter = useRef(0);
|
||||||
@@ -273,12 +275,20 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => { unsubUpdate(); clearTimeout(timer); };
|
return () => { unsubUpdate(); clearTimeout(timer); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Wake Word: "ARIA" Erkennung → Auto-Aufnahme starten
|
// Gespraechsmodus: Nach TTS-Wiedergabe automatisch Aufnahme starten
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubPlayback = audioService.onPlaybackFinished(() => {
|
||||||
|
if (wakeWordService.isActive()) {
|
||||||
|
wakeWordService.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsubPlayback();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Wake Word / Gespraechsmodus: Auto-Aufnahme starten
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubWake = wakeWordService.onWakeWord(async () => {
|
const unsubWake = wakeWordService.onWakeWord(async () => {
|
||||||
console.log('[Chat] Wake Word erkannt — starte Auto-Aufnahme');
|
console.log('[Chat] Gespraechsmodus — starte Auto-Aufnahme');
|
||||||
// TTS stoppen damit ARIA sich nicht selbst hoert
|
|
||||||
audioService.stopPlayback();
|
|
||||||
// Aufnahme mit Auto-Stop (VAD) starten
|
// Aufnahme mit Auto-Stop (VAD) starten
|
||||||
const started = await audioService.startRecording(true);
|
const started = await audioService.startRecording(true);
|
||||||
if (!started) {
|
if (!started) {
|
||||||
@@ -359,22 +369,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
|
return () => { if (saveTimer.current) clearTimeout(saveTimer.current); };
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Auto-Scroll wird ueber onContentSizeChange der FlatList gesteuert
|
// Inverted FlatList: neueste Nachrichten unten, kein manuelles Scrollen noetig
|
||||||
const shouldAutoScroll = useRef(true);
|
const invertedMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||||||
const handleContentSizeChange = useCallback(() => {
|
|
||||||
if (shouldAutoScroll.current) {
|
|
||||||
flatListRef.current?.scrollToEnd({ animated: false });
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const handleScrollBeginDrag = useCallback(() => {
|
|
||||||
shouldAutoScroll.current = false;
|
|
||||||
}, []);
|
|
||||||
const handleScrollEndDrag = useCallback((e: any) => {
|
|
||||||
// Auto-Scroll wieder aktivieren wenn User ganz unten ist
|
|
||||||
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
||||||
const isAtBottom = contentOffset.y + layoutMeasurement.height >= contentSize.height - 50;
|
|
||||||
shouldAutoScroll.current = isAtBottom;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// GPS-Position holen (optional)
|
// GPS-Position holen (optional)
|
||||||
const getCurrentLocation = useCallback((): Promise<{ lat: number; lon: number } | null> => {
|
const getCurrentLocation = useCallback((): Promise<{ lat: number; lon: number } | null> => {
|
||||||
@@ -400,6 +396,13 @@ const ChatScreen: React.FC = () => {
|
|||||||
|
|
||||||
const sendTextMessage = useCallback(async () => {
|
const sendTextMessage = useCallback(async () => {
|
||||||
const text = inputText.trim();
|
const text = inputText.trim();
|
||||||
|
|
||||||
|
// Wenn pending Anhaenge vorhanden → Anhaenge + Text zusammen senden
|
||||||
|
if (pendingAttachments.length > 0) {
|
||||||
|
sendPendingAttachments(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
setInputText('');
|
setInputText('');
|
||||||
@@ -419,7 +422,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
text,
|
text,
|
||||||
...(location && { location }),
|
...(location && { location }),
|
||||||
});
|
});
|
||||||
}, [inputText, getCurrentLocation]);
|
}, [inputText, getCurrentLocation, pendingAttachments, sendPendingAttachments]);
|
||||||
|
|
||||||
// Sprachaufnahme abgeschlossen
|
// Sprachaufnahme abgeschlossen
|
||||||
const handleVoiceRecording = useCallback(async (result: RecordingResult) => {
|
const handleVoiceRecording = useCallback(async (result: RecordingResult) => {
|
||||||
@@ -441,88 +444,91 @@ const ChatScreen: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [getCurrentLocation]);
|
}, [getCurrentLocation]);
|
||||||
|
|
||||||
// Datei senden
|
// Datei auswaehlen → zur Pending-Liste hinzufuegen
|
||||||
const handleFileSelected = useCallback(async (file: FileData) => {
|
const handleFileSelected = useCallback(async (file: FileData) => {
|
||||||
setShowFileUpload(false);
|
setShowFileUpload(false);
|
||||||
const location = await getCurrentLocation();
|
setPendingAttachments(prev => [...prev, { file, isPhoto: false }]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isImage = file.type.startsWith('image/');
|
// Foto auswaehlen → zur Pending-Liste hinzufuegen
|
||||||
const msgId = nextId();
|
|
||||||
let imageUri = isImage && file.base64 ? `data:${file.type};base64,${file.base64}` : file.uri;
|
|
||||||
|
|
||||||
const userMsg: ChatMessage = {
|
|
||||||
id: msgId,
|
|
||||||
sender: 'user',
|
|
||||||
text: 'Anhang empfangen',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
attachments: [{
|
|
||||||
type: isImage ? 'image' : 'file',
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
uri: imageUri,
|
|
||||||
mimeType: file.type,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
setMessages(prev => [...prev, userMsg]);
|
|
||||||
|
|
||||||
// Anhang auf Disk speichern fuer Persistenz
|
|
||||||
if (file.base64) {
|
|
||||||
persistAttachment(file.base64, msgId, file.name).then(filePath => {
|
|
||||||
setMessages(prev => prev.map(m =>
|
|
||||||
m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m
|
|
||||||
));
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
rvs.send('file', {
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
base64: file.base64,
|
|
||||||
...(location && { location }),
|
|
||||||
});
|
|
||||||
}, [getCurrentLocation]);
|
|
||||||
|
|
||||||
// Foto senden
|
|
||||||
const handlePhotoSelected = useCallback(async (photo: PhotoData) => {
|
const handlePhotoSelected = useCallback(async (photo: PhotoData) => {
|
||||||
setShowCameraUpload(false);
|
setShowCameraUpload(false);
|
||||||
|
setPendingAttachments(prev => [...prev, { file: photo, isPhoto: true }]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Alle Pending Anhaenge + Text senden
|
||||||
|
const sendPendingAttachments = useCallback(async (messageText: string) => {
|
||||||
|
if (pendingAttachments.length === 0) return;
|
||||||
const location = await getCurrentLocation();
|
const location = await getCurrentLocation();
|
||||||
|
|
||||||
const msgId = nextId();
|
const msgId = nextId();
|
||||||
const dataUri = photo.base64 ? `data:${photo.type};base64,${photo.base64}` : undefined;
|
|
||||||
|
|
||||||
|
// Alle Attachments fuer die Chat-Nachricht sammeln
|
||||||
|
const attachments: Attachment[] = [];
|
||||||
|
for (const { file, isPhoto } of pendingAttachments) {
|
||||||
|
const isImage = isPhoto || (file.type && file.type.startsWith('image/'));
|
||||||
|
const name = isPhoto ? file.fileName : file.name;
|
||||||
|
const base64 = file.base64 || '';
|
||||||
|
const mimeType = file.type || '';
|
||||||
|
const imageUri = isImage && base64 ? `data:${mimeType};base64,${base64}` : file.uri;
|
||||||
|
|
||||||
|
attachments.push({
|
||||||
|
type: isImage ? 'image' : 'file',
|
||||||
|
name,
|
||||||
|
size: file.size,
|
||||||
|
uri: imageUri,
|
||||||
|
mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat-Nachricht mit allen Anhaengen
|
||||||
const userMsg: ChatMessage = {
|
const userMsg: ChatMessage = {
|
||||||
id: msgId,
|
id: msgId,
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
text: 'Anhang empfangen',
|
text: messageText || `${pendingAttachments.length} Anhang/Anhaenge`,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
attachments: [{
|
attachments,
|
||||||
type: 'image',
|
|
||||||
name: photo.fileName,
|
|
||||||
uri: dataUri,
|
|
||||||
mimeType: photo.type,
|
|
||||||
}],
|
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, userMsg]);
|
setMessages(prev => [...prev, userMsg]);
|
||||||
|
|
||||||
// Foto auf Disk speichern fuer Persistenz
|
// Alle Dateien an RVS senden + auf Disk speichern
|
||||||
if (photo.base64) {
|
for (const { file, isPhoto } of pendingAttachments) {
|
||||||
persistAttachment(photo.base64, msgId, photo.fileName).then(filePath => {
|
const name = isPhoto ? file.fileName : file.name;
|
||||||
setMessages(prev => prev.map(m =>
|
const base64 = file.base64 || '';
|
||||||
m.id === msgId ? { ...m, attachments: m.attachments?.map(a => ({ ...a, uri: filePath })) } : m
|
const mimeType = file.type || '';
|
||||||
));
|
|
||||||
}).catch(() => {});
|
// Auf Disk speichern
|
||||||
|
if (base64) {
|
||||||
|
persistAttachment(base64, msgId + '_' + name, name).then(filePath => {
|
||||||
|
setMessages(prev => prev.map(m =>
|
||||||
|
m.id === msgId ? { ...m, attachments: m.attachments?.map(a =>
|
||||||
|
a.name === name && !a.uri?.startsWith('file://') ? { ...a, uri: filePath } : a
|
||||||
|
)} : m
|
||||||
|
));
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// An RVS senden
|
||||||
|
rvs.send('file', {
|
||||||
|
name,
|
||||||
|
type: mimeType,
|
||||||
|
size: file.size,
|
||||||
|
base64,
|
||||||
|
...(isPhoto && file.width && { width: file.width, height: file.height }),
|
||||||
|
...(location && { location }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
rvs.send('file', {
|
// Text als separate Nachricht (damit ARIA weiss was zu tun ist)
|
||||||
name: photo.fileName,
|
if (messageText) {
|
||||||
type: photo.type,
|
rvs.send('chat', {
|
||||||
base64: photo.base64,
|
text: messageText,
|
||||||
width: photo.width,
|
...(location && { location }),
|
||||||
height: photo.height,
|
});
|
||||||
...(location && { location }),
|
}
|
||||||
});
|
|
||||||
}, [getCurrentLocation]);
|
setPendingAttachments([]);
|
||||||
|
setInputText('');
|
||||||
|
}, [pendingAttachments, getCurrentLocation]);
|
||||||
|
|
||||||
// --- Rendering ---
|
// --- Rendering ---
|
||||||
|
|
||||||
@@ -653,14 +659,12 @@ const ChatScreen: React.FC = () => {
|
|||||||
{/* Nachrichtenliste */}
|
{/* Nachrichtenliste */}
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={searchQuery ? messages.filter(m => m.text.toLowerCase().includes(searchQuery.toLowerCase())) : messages}
|
inverted
|
||||||
|
data={searchQuery ? messages.filter(m => m.text.toLowerCase().includes(searchQuery.toLowerCase())).reverse() : invertedMessages}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={renderMessage}
|
renderItem={renderMessage}
|
||||||
contentContainerStyle={styles.messageList}
|
contentContainerStyle={styles.messageList}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onContentSizeChange={handleContentSizeChange}
|
|
||||||
onScrollBeginDrag={handleScrollBeginDrag}
|
|
||||||
onScrollEndDrag={handleScrollEndDrag}
|
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<Text style={styles.emptyIcon}>{'\uD83E\uDD16'}</Text>
|
<Text style={styles.emptyIcon}>{'\uD83E\uDD16'}</Text>
|
||||||
@@ -670,6 +674,40 @@ const ChatScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Pending Anhaenge Vorschau */}
|
||||||
|
{pendingAttachments.length > 0 && (
|
||||||
|
<View style={styles.pendingBar}>
|
||||||
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{flex: 1}}>
|
||||||
|
{pendingAttachments.map((att, idx) => (
|
||||||
|
<View key={idx} style={styles.pendingItem}>
|
||||||
|
{att.file.type?.startsWith('image/') || att.isPhoto ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: att.file.base64
|
||||||
|
? `data:${att.file.type};base64,${att.file.base64}`
|
||||||
|
: att.file.uri }}
|
||||||
|
style={styles.pendingThumb}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.pendingThumb, {justifyContent: 'center', alignItems: 'center'}]}>
|
||||||
|
<Text style={{fontSize: 20}}>{'\uD83D\uDCC4'}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.pendingRemove}
|
||||||
|
onPress={() => setPendingAttachments(prev => prev.filter((_, i) => i !== idx))}
|
||||||
|
>
|
||||||
|
<Text style={{color: '#fff', fontSize: 10, fontWeight: 'bold'}}>X</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
<Text style={{color: '#8888AA', fontSize: 11, marginLeft: 8}}>{pendingAttachments.length}</Text>
|
||||||
|
<TouchableOpacity onPress={() => setPendingAttachments([])}>
|
||||||
|
<Text style={{color: '#FF3B30', fontSize: 14, paddingHorizontal: 8}}>Alle X</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Eingabebereich */}
|
{/* Eingabebereich */}
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
{/* Datei-Buttons */}
|
{/* Datei-Buttons */}
|
||||||
@@ -692,7 +730,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
style={styles.textInput}
|
style={styles.textInput}
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChangeText={setInputText}
|
onChangeText={setInputText}
|
||||||
placeholder="Nachricht an ARIA..."
|
placeholder={pendingAttachments.length > 0 ? "Text zu den Anhaengen (optional)..." : "Nachricht an ARIA..."}
|
||||||
placeholderTextColor="#555570"
|
placeholderTextColor="#555570"
|
||||||
multiline
|
multiline
|
||||||
maxLength={4000}
|
maxLength={4000}
|
||||||
@@ -701,7 +739,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Senden oder Sprache */}
|
{/* Senden oder Sprache */}
|
||||||
{inputText.trim() ? (
|
{inputText.trim() || pendingAttachments.length > 0 ? (
|
||||||
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
<TouchableOpacity style={styles.sendButton} onPress={sendTextMessage}>
|
||||||
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
<Text style={styles.sendIcon}>{'\u2B06\uFE0F'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -932,6 +970,36 @@ const styles = StyleSheet.create({
|
|||||||
wakeWordIcon: {
|
wakeWordIcon: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
|
pendingBar: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#1E1E2E',
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#2A2A3E',
|
||||||
|
},
|
||||||
|
pendingItem: {
|
||||||
|
position: 'relative',
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
pendingThumb: {
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: '#0D0D1A',
|
||||||
|
},
|
||||||
|
pendingRemove: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
backgroundColor: '#FF3B30',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
searchBar: {
|
searchBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -748,11 +748,21 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
<Text style={styles.sectionTitle}>{'\u00DC'}ber</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
<Text style={styles.aboutTitle}>ARIA Cockpit</Text>
|
||||||
<Text style={styles.aboutVersion}>Version 0.0.2.8 </Text>
|
<Text style={styles.aboutVersion}>Version {require('../../package.json').version}</Text>
|
||||||
<Text style={styles.aboutInfo}>
|
<Text style={styles.aboutInfo}>
|
||||||
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
Stefans Kommandozentrale f{'\u00FC'}r ARIA.{'\n'}
|
||||||
Gebaut mit React Native + TypeScript.
|
Gebaut mit React Native + TypeScript.
|
||||||
</Text>
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.connectButton, {marginTop: 12}]}
|
||||||
|
onPress={() => {
|
||||||
|
const updateService = require('../services/updater').default;
|
||||||
|
updateService.checkForUpdate();
|
||||||
|
Alert.alert('Update-Check', 'Pruefe auf neue Version...');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.connectButtonText}>Auf Updates pr{'\u00FC'}fen</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Platz am Ende */}
|
{/* Platz am Ende */}
|
||||||
|
|||||||
@@ -214,10 +214,22 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback wenn alle Audio-Teile abgespielt sind
|
||||||
|
private playbackFinishedListeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
onPlaybackFinished(callback: () => void): () => void {
|
||||||
|
this.playbackFinishedListeners.push(callback);
|
||||||
|
return () => {
|
||||||
|
this.playbackFinishedListeners = this.playbackFinishedListeners.filter(cb => cb !== callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Naechstes Audio aus der Queue abspielen */
|
/** Naechstes Audio aus der Queue abspielen */
|
||||||
private async _playNext(): Promise<void> {
|
private async _playNext(): Promise<void> {
|
||||||
if (this.audioQueue.length === 0) {
|
if (this.audioQueue.length === 0) {
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
|
// Alle Audio-Teile abgespielt → Listener benachrichtigen
|
||||||
|
this.playbackFinishedListeners.forEach(cb => cb());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
* 3. App zeigt Benachrichtigung → User bestaetigt → Download + Install
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Alert, Linking, Platform } from 'react-native';
|
import { Alert, Linking, Platform, NativeModules } from 'react-native';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import rvs, { RVSMessage } from './rvs';
|
import rvs, { RVSMessage } from './rvs';
|
||||||
|
|
||||||
// Aktuelle App-Version (aus package.json via Build)
|
// Version aus package.json (wird beim Build eingebettet)
|
||||||
const APP_VERSION = '0.0.2.3'; // TODO: aus nativer Build-Config lesen
|
const packageJson = require('../../package.json');
|
||||||
|
const APP_VERSION = packageJson.version || '0.0.0.0';
|
||||||
|
|
||||||
type UpdateCallback = (info: UpdateInfo) => void;
|
type UpdateCallback = (info: UpdateInfo) => void;
|
||||||
|
|
||||||
@@ -116,9 +117,17 @@ class UpdateService {
|
|||||||
const fileSize = await RNFS.stat(destPath);
|
const fileSize = await RNFS.stat(destPath);
|
||||||
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
console.log(`[Update] APK gespeichert: ${destPath} (${(parseInt(fileSize.size) / 1024 / 1024).toFixed(1)}MB)`);
|
||||||
|
|
||||||
// APK installieren (oeffnet Android-Installer)
|
// APK installieren via natives ApkInstaller Module (FileProvider + Intent)
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
await Linking.openURL(`file://${destPath}`);
|
try {
|
||||||
|
const { ApkInstaller } = NativeModules;
|
||||||
|
await ApkInstaller.install(destPath);
|
||||||
|
} catch (installErr: any) {
|
||||||
|
Alert.alert(
|
||||||
|
'APK heruntergeladen',
|
||||||
|
`Version ${info.version} gespeichert.\n\nBitte manuell installieren:\nDateimanager → ${apkData.fileName} antippen.\n\n(${installErr.message})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`[Update] Fehler: ${err.message}`);
|
console.error(`[Update] Fehler: ${err.message}`);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Wake Word Service — "ARIA" Erkennung
|
* Gespraechsmodus — "Ohr-Button"
|
||||||
*
|
*
|
||||||
* Phase 1: Deaktiviert — react-native-live-audio-stream hat native Bridge-Probleme.
|
* Wenn aktiv: Nach jeder ARIA-Antwort (TTS fertig) startet automatisch die Aufnahme.
|
||||||
* Nutzt stattdessen Tap-to-Talk (VoiceButton) als primaeren Eingabemodus.
|
* Wie ein Walkie-Talkie / natuerliches Gespraech:
|
||||||
|
* ARIA spricht → Aufnahme startet → User spricht → VAD stoppt → ARIA antwortet → ...
|
||||||
*
|
*
|
||||||
* Phase 2: Porcupine on-device "ARIA" Keyword (geplant).
|
* Phase 2 (geplant): Porcupine "ARIA" Wake Word fuer passives Lauschen.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type WakeWordCallback = () => void;
|
type WakeWordCallback = () => void;
|
||||||
@@ -17,30 +18,33 @@ class WakeWordService {
|
|||||||
private wakeCallbacks: WakeWordCallback[] = [];
|
private wakeCallbacks: WakeWordCallback[] = [];
|
||||||
private stateCallbacks: StateCallback[] = [];
|
private stateCallbacks: StateCallback[] = [];
|
||||||
|
|
||||||
/** Wake Word Erkennung starten */
|
/** Gespraechsmodus starten */
|
||||||
async start(): Promise<boolean> {
|
async start(): Promise<boolean> {
|
||||||
if (this.state === 'listening') return true;
|
if (this.state === 'listening') return true;
|
||||||
|
console.log('[WakeWord] Gespraechsmodus aktiviert — Aufnahme startet nach ARIA-Antwort');
|
||||||
try {
|
this.setState('listening');
|
||||||
// Phase 1: LiveAudioStream deaktiviert (native Bridge instabil)
|
return true;
|
||||||
// Stattdessen: Tap-to-Talk als primaerer Modus
|
|
||||||
console.log('[WakeWord] Wake Word ist in Phase 1 noch nicht verfuegbar — nutze Tap-to-Talk');
|
|
||||||
this.setState('listening');
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[WakeWord] Start fehlgeschlagen:', err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wake Word Erkennung stoppen */
|
/** Gespraechsmodus stoppen */
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
console.log('[WakeWord] Gespraechsmodus deaktiviert');
|
||||||
this.setState('off');
|
this.setState('off');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nach Aufnahme erneut starten */
|
/** Nach ARIA-Antwort (TTS fertig): Aufnahme automatisch starten */
|
||||||
async resume(): Promise<void> {
|
async resume(): Promise<void> {
|
||||||
// Nichts zu tun in Phase 1
|
if (this.state !== 'listening') return;
|
||||||
|
// Kurze Pause damit TTS-Audio nicht ins Mikrofon geht
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
if (this.state === 'listening') {
|
||||||
|
console.log('[WakeWord] TTS fertig — starte automatisch Aufnahme');
|
||||||
|
this.wakeCallbacks.forEach(cb => cb());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.state === 'listening';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Callbacks ---
|
// --- Callbacks ---
|
||||||
|
|||||||
+21
-4
@@ -201,11 +201,23 @@ class VoiceEngine:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Langen Text in Saetze aufteilen (Piper hat Limits bei langen Texten)
|
# Markdown + Sonderzeichen entfernen fuer natuerliche Sprache
|
||||||
import re
|
import re
|
||||||
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
|
clean = text.strip()
|
||||||
# Markdown-Formatierung entfernen
|
clean = re.sub(r'\*\*([^*]+)\*\*', r'\1', clean) # **fett**
|
||||||
sentences = [re.sub(r'\*\*([^*]+)\*\*', r'\1', s).strip() for s in sentences if s.strip()]
|
clean = re.sub(r'\*([^*]+)\*', r'\1', clean) # *kursiv*
|
||||||
|
clean = re.sub(r'`[^`]+`', '', clean) # `code`
|
||||||
|
clean = re.sub(r'```[\s\S]*?```', '', clean) # Code-Bloecke
|
||||||
|
clean = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', clean) # [text](url)
|
||||||
|
clean = re.sub(r'#{1,6}\s*', '', clean) # ### Ueberschriften
|
||||||
|
clean = re.sub(r'>\s*', '', clean) # > Zitate
|
||||||
|
clean = re.sub(r'[-*]\s+', '', clean) # Listen
|
||||||
|
clean = re.sub(r'\n{2,}', '. ', clean) # Absaetze
|
||||||
|
clean = re.sub(r'\n', ', ', clean) # Zeilenumbrueche
|
||||||
|
clean = re.sub(r'\s{2,}', ' ', clean) # Mehrfach-Leerzeichen
|
||||||
|
clean = re.sub(r'["""„]', '', clean) # Anfuehrungszeichen
|
||||||
|
sentences = re.split(r'(?<=[.!?])\s+', clean)
|
||||||
|
sentences = [s.strip() for s in sentences if s.strip()]
|
||||||
|
|
||||||
if not sentences:
|
if not sentences:
|
||||||
return None
|
return None
|
||||||
@@ -1045,6 +1057,11 @@ class ARIABridge:
|
|||||||
sender = payload.get("sender", "")
|
sender = payload.get("sender", "")
|
||||||
if sender in ("aria", "stt"):
|
if sender in ("aria", "stt"):
|
||||||
return
|
return
|
||||||
|
text = payload.get("text", "")
|
||||||
|
if text:
|
||||||
|
logger.info("[rvs] App-Chat: '%s'", text[:80])
|
||||||
|
await self.send_to_core(text, source="app")
|
||||||
|
return
|
||||||
|
|
||||||
elif msg_type == "xtts_response":
|
elif msg_type == "xtts_response":
|
||||||
# XTTS-Audio vom Gaming-PC empfangen → an App weiterleiten
|
# XTTS-Audio vom Gaming-PC empfangen → an App weiterleiten
|
||||||
|
|||||||
+81
-7
@@ -205,8 +205,14 @@
|
|||||||
<span><span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span></span>
|
<span><span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span></span>
|
||||||
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
<button class="btn secondary" onclick="cancelRequest()" style="padding:2px 10px;font-size:11px;color:#FF3B30;border-color:#FF3B30;">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="diag-pending-attachments" style="display:none;padding:6px 10px;background:#1E1E2E;border-radius:6px 6px 0 0;margin-bottom:-4px;display:flex;gap:6px;flex-wrap:wrap;align-items:center;">
|
||||||
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
|
<label class="btn secondary" style="padding:6px 10px;cursor:pointer;font-size:14px;" title="Datei anhaengen">
|
||||||
|
📎
|
||||||
|
<input type="file" id="diag-file-input" multiple accept="image/*,application/pdf,.doc,.docx,.txt" style="display:none;" onchange="handleDiagFileSelect(this.files)">
|
||||||
|
</label>
|
||||||
|
<input type="text" id="chat-input" placeholder="Nachricht an ARIA..." onpaste="handleDiagPaste(event)">
|
||||||
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
||||||
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
|
<button class="btn" id="btn-rvs" onclick="testRVS()">Via RVS senden</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -939,21 +945,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendDiagAttachments() {
|
||||||
|
// Alle pending Dateien an RVS senden
|
||||||
|
for (const f of diagPendingFiles) {
|
||||||
|
send({ action: 'send_file', name: f.name, type: f.type, size: f.size, base64: f.base64 });
|
||||||
|
}
|
||||||
|
if (diagPendingFiles.length > 0) {
|
||||||
|
addChat('sent', `${diagPendingFiles.length} Anhang/Anhaenge`, 'Datei');
|
||||||
|
}
|
||||||
|
diagPendingFiles = [];
|
||||||
|
renderDiagPending();
|
||||||
|
}
|
||||||
|
|
||||||
function testGateway() {
|
function testGateway() {
|
||||||
const input = document.getElementById('chat-input');
|
const input = document.getElementById('chat-input');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text) return;
|
if (!text && diagPendingFiles.length === 0) return;
|
||||||
addChat('sent', text, 'Gateway direkt');
|
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
||||||
send({ action: 'test_gateway', text });
|
if (text) {
|
||||||
|
addChat('sent', text, 'Gateway direkt');
|
||||||
|
send({ action: 'test_gateway', text });
|
||||||
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function testRVS() {
|
function testRVS() {
|
||||||
const input = document.getElementById('chat-input');
|
const input = document.getElementById('chat-input');
|
||||||
const text = input.value.trim();
|
const text = input.value.trim();
|
||||||
if (!text) return;
|
if (!text && diagPendingFiles.length === 0) return;
|
||||||
addChat('sent', text, 'via RVS');
|
if (diagPendingFiles.length > 0) sendDiagAttachments();
|
||||||
send({ action: 'test_rvs', text });
|
if (text) {
|
||||||
|
addChat('sent', text, 'via RVS');
|
||||||
|
send({ action: 'test_rvs', text });
|
||||||
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,6 +1326,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Diagnostic Anhang-Handling ─────────────
|
||||||
|
let diagPendingFiles = [];
|
||||||
|
|
||||||
|
function handleDiagFileSelect(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64 = reader.result.split(',')[1];
|
||||||
|
diagPendingFiles.push({ name: file.name, type: file.type, size: file.size, base64 });
|
||||||
|
renderDiagPending();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDiagPaste(event) {
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.kind === 'file') {
|
||||||
|
event.preventDefault();
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) handleDiagFileSelect([file]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiagPending() {
|
||||||
|
const container = document.getElementById('diag-pending-attachments');
|
||||||
|
if (diagPendingFiles.length === 0) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.style.display = 'flex';
|
||||||
|
container.innerHTML = diagPendingFiles.map((f, i) => {
|
||||||
|
const isImage = f.type.startsWith('image/');
|
||||||
|
const preview = isImage ? `<img src="data:${f.type};base64,${f.base64}" style="width:40px;height:40px;border-radius:4px;object-fit:cover;">` : `<span style="font-size:20px;">📄</span>`;
|
||||||
|
return `<div style="position:relative;display:inline-block;">
|
||||||
|
${preview}
|
||||||
|
<span onclick="removeDiagPending(${i})" style="position:absolute;top:-4px;right:-4px;width:16px;height:16px;border-radius:8px;background:#FF3B30;color:#fff;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;">X</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('') + `<span style="color:#8888AA;font-size:11px;margin-left:4px;">${diagPendingFiles.length} Datei(en)</span>
|
||||||
|
<span onclick="diagPendingFiles=[];renderDiagPending();" style="color:#FF3B30;font-size:11px;cursor:pointer;margin-left:8px;">Alle X</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDiagPending(idx) {
|
||||||
|
diagPendingFiles.splice(idx, 1);
|
||||||
|
renderDiagPending();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Abbrechen ──────────────────────────────
|
// ── Abbrechen ──────────────────────────────
|
||||||
function cancelRequest() {
|
function cancelRequest() {
|
||||||
send({ action: 'cancel_request' });
|
send({ action: 'cancel_request' });
|
||||||
|
|||||||
@@ -1181,6 +1181,14 @@ wss.on("connection", (ws) => {
|
|||||||
if (ws._sshSock) ws._sshSock.write(msg.data);
|
if (ws._sshSock) ws._sshSock.write(msg.data);
|
||||||
} else if (msg.action === "live_ssh_close") {
|
} else if (msg.action === "live_ssh_close") {
|
||||||
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
|
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
|
||||||
|
} else if (msg.action === "send_file") {
|
||||||
|
// Datei von Diagnostic an Bridge via RVS senden
|
||||||
|
sendToRVS_raw({
|
||||||
|
type: "file",
|
||||||
|
payload: { name: msg.name, type: msg.type, size: msg.size, base64: msg.base64 },
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
log("info", "server", `Datei gesendet: ${msg.name} (${msg.type})`);
|
||||||
} else if (msg.action === "cancel_request") {
|
} else if (msg.action === "cancel_request") {
|
||||||
// Laufende Anfrage abbrechen — doctor --fix beendet stuck runs
|
// Laufende Anfrage abbrechen — doctor --fix beendet stuck runs
|
||||||
log("warn", "server", "Anfrage abgebrochen — fuehre doctor --fix aus");
|
log("warn", "server", "Anfrage abgebrochen — fuehre doctor --fix aus");
|
||||||
|
|||||||
@@ -28,15 +28,17 @@
|
|||||||
|
|
||||||
### Bugs (Prioritaet)
|
### Bugs (Prioritaet)
|
||||||
- [ ] Session-Persistenz: Bei Container-Restart wird immer aria-bridge geladen statt die zuletzt gewaehlte Session. Wird nicht persistent gespeichert.
|
- [ ] Session-Persistenz: Bei Container-Restart wird immer aria-bridge geladen statt die zuletzt gewaehlte Session. Wird nicht persistent gespeichert.
|
||||||
- [ ] App: Textnachrichten, Bilder und Anhaenge werden von ARIA nicht beantwortet — nur Sprachnachrichten funktionieren.
|
- [x] App: Textnachrichten werden von ARIA beantwortet (Bridge chat handler fix)
|
||||||
- [ ] App: Audioausgabe hoert ab und zu einfach auf (mitten im Satz oder zwischen Chunks)
|
- [ ] App: Audioausgabe hoert ab und zu einfach auf (mitten im Satz oder zwischen Chunks)
|
||||||
- [ ] Auto-Update: release.sh kopiert APK nicht auf den RVS-Server (rvs/updates/ bleibt leer)
|
- [x] Auto-Update: APK-Installation via FileProvider (content:// URI)
|
||||||
- [ ] App: Kein Auto-Scroll zur letzten Nachricht beim App-Start (soll direkt springen, nicht animiert scrollen)
|
- [x] Auto-Update: "Auf Updates pruefen" Button in App-Einstellungen
|
||||||
- [ ] App: Bei neuen Nachrichten soll automatisch zur letzten Nachricht gescrollt werden
|
- [x] App: Auto-Scroll zur letzten Nachricht beim App-Start (direkt, ohne Animation)
|
||||||
|
- [x] App: Bei neuen Nachrichten automatisch zur letzten Nachricht scrollen
|
||||||
|
|
||||||
### App Features
|
### App Features
|
||||||
- [ ] App: Zu Anhaengen noch Text/Sprache hinzufuegen koennen (z.B. Bild senden + "Was siehst du?")
|
- [x] App: Zu Anhaengen Text hinzufuegen vor dem Senden (Pending-Vorschau + optionaler Text)
|
||||||
- [ ] Wake Word on-device (Porcupine "ARIA" Keyword, Phase 2)
|
- [x] Gespraechsmodus (Ohr-Button): Auto-Aufnahme nach ARIA-Antwort (Walkie-Talkie)
|
||||||
|
- [ ] Wake Word on-device (Porcupine "ARIA" Keyword, Phase 2 — passives Lauschen)
|
||||||
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
- [ ] Chat-History zuverlaessiger laden (AsyncStorage Race Condition)
|
||||||
- [ ] Background Audio Service (TTS auch bei minimierter App)
|
- [ ] Background Audio Service (TTS auch bei minimierter App)
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -76,8 +76,11 @@ echo -e " ${GREEN}✓${NC} SettingsScreen → Version $VERSION"
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── APK bauen ─────────────────────────────────
|
# ── APK bauen ─────────────────────────────────
|
||||||
echo -e "${GREEN}[2/5] APK bauen...${NC}"
|
echo -e "${GREEN}[2/5] APK bauen (Cache leeren + Build)...${NC}"
|
||||||
cd android
|
cd android
|
||||||
|
# Metro + Gradle Cache leeren damit neue Version sauber eingebettet wird
|
||||||
|
rm -rf node_modules/.cache 2>/dev/null
|
||||||
|
cd android && ./gradlew clean 2>/dev/null; cd ..
|
||||||
./build.sh release
|
./build.sh release
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
@@ -174,9 +177,11 @@ fi
|
|||||||
RVS_UPDATE_HOST="${RVS_UPDATE_HOST:-}"
|
RVS_UPDATE_HOST="${RVS_UPDATE_HOST:-}"
|
||||||
if [ -n "$RVS_UPDATE_HOST" ]; then
|
if [ -n "$RVS_UPDATE_HOST" ]; then
|
||||||
echo -e "${GREEN}[6/6] APK auf RVS-Server kopieren (Auto-Update)...${NC}"
|
echo -e "${GREEN}[6/6] APK auf RVS-Server kopieren (Auto-Update)...${NC}"
|
||||||
|
# Alte APKs auf dem RVS loeschen, dann neue hochladen
|
||||||
|
ssh "$RVS_UPDATE_HOST" "rm -f ~/ARIA-AGENT/rvs/updates/ARIA-*.apk" 2>/dev/null
|
||||||
scp "$APK_PATH" "${RVS_UPDATE_HOST}:~/ARIA-AGENT/rvs/updates/${APK_NAME}" 2>/dev/null
|
scp "$APK_PATH" "${RVS_UPDATE_HOST}:~/ARIA-AGENT/rvs/updates/${APK_NAME}" 2>/dev/null
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo -e " ${GREEN}✓${NC} APK auf RVS-Server kopiert — Apps werden benachrichtigt"
|
echo -e " ${GREEN}✓${NC} APK auf RVS-Server kopiert (alte Versionen geloescht)"
|
||||||
else
|
else
|
||||||
echo -e " ${YELLOW}APK konnte nicht auf RVS kopiert werden (RVS_UPDATE_HOST=$RVS_UPDATE_HOST)${NC}"
|
echo -e " ${YELLOW}APK konnte nicht auf RVS kopiert werden (RVS_UPDATE_HOST=$RVS_UPDATE_HOST)${NC}"
|
||||||
echo -e " ${YELLOW}Manuell: scp $APK_PATH $RVS_UPDATE_HOST:~/ARIA-AGENT/rvs/updates/${APK_NAME}${NC}"
|
echo -e " ${YELLOW}Manuell: scp $APK_PATH $RVS_UPDATE_HOST:~/ARIA-AGENT/rvs/updates/${APK_NAME}${NC}"
|
||||||
|
|||||||
+16
-2
@@ -97,8 +97,22 @@ async function handleTTSRequest(payload) {
|
|||||||
const { text, voice, requestId, language } = payload;
|
const { text, voice, requestId, language } = payload;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
// Markdown entfernen
|
// Markdown + Sonderzeichen entfernen fuer natuerliche Sprache
|
||||||
const cleanText = text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();
|
let cleanText = text
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, "$1") // **fett** → fett
|
||||||
|
.replace(/\*([^*]+)\*/g, "$1") // *kursiv* → kursiv
|
||||||
|
.replace(/`([^`]+)`/g, "$1") // `code` → code
|
||||||
|
.replace(/```[\s\S]*?```/g, "") // Code-Bloecke entfernen
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // [text](url) → text
|
||||||
|
.replace(/#{1,6}\s*/g, "") // ### Ueberschriften → entfernen
|
||||||
|
.replace(/>\s*/g, "") // > Zitate → entfernen
|
||||||
|
.replace(/[-*]\s+/g, "") // - Listen → entfernen
|
||||||
|
.replace(/\n{2,}/g, ". ") // Mehrere Newlines → Punkt
|
||||||
|
.replace(/\n/g, ", ") // Einzelne Newlines → Komma
|
||||||
|
.replace(/\s{2,}/g, " ") // Mehrfach-Leerzeichen
|
||||||
|
.replace(/["""„]/g, "") // Anfuehrungszeichen entfernen
|
||||||
|
.replace(/\(\)/g, "") // Leere Klammern
|
||||||
|
.trim();
|
||||||
|
|
||||||
// Text in Saetze aufteilen, dann zu Chunks von 2-3 Saetzen zusammenfassen
|
// Text in Saetze aufteilen, dann zu Chunks von 2-3 Saetzen zusammenfassen
|
||||||
// (mehr Kontext = konsistentere Stimme/Lautstaerke, aber nicht zu lang fuer WebSocket)
|
// (mehr Kontext = konsistentere Stimme/Lautstaerke, aber nicht zu lang fuer WebSocket)
|
||||||
|
|||||||
Reference in New Issue
Block a user