Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff03d8ce62 | |||
| 8281131432 | |||
| 8a6bd4e0e7 | |||
| 1b4df0565a | |||
| eb3692ef81 | |||
| 46a9ac9f84 | |||
| a012ec65ef | |||
| b86c4a0d1a | |||
| 11de9a01b9 | |||
| 80dec2daf9 | |||
| da591bb53c | |||
| 7545c9c823 | |||
| ecc3d59a8f | |||
| b8862f025b | |||
| db20a07b27 |
@@ -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 1
|
versionCode 109
|
||||||
versionName "0.0.1.7"
|
versionName "0.0.1.9"
|
||||||
// Fallback fuer Libraries mit Product Flavors
|
// Fallback fuer Libraries mit Product Flavors
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aria-cockpit",
|
"name": "aria-cockpit",
|
||||||
"version": "0.0.1.7",
|
"version": "0.0.1.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
const [showCameraUpload, setShowCameraUpload] = useState(false);
|
const [showCameraUpload, setShowCameraUpload] = useState(false);
|
||||||
const [gpsEnabled, setGpsEnabled] = useState(false);
|
const [gpsEnabled, setGpsEnabled] = useState(false);
|
||||||
const [wakeWordActive, setWakeWordActive] = useState(false);
|
const [wakeWordActive, setWakeWordActive] = useState(false);
|
||||||
|
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
||||||
|
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const messageIdCounter = useRef(0);
|
const messageIdCounter = useRef(0);
|
||||||
@@ -525,12 +526,12 @@ const ChatScreen: React.FC = () => {
|
|||||||
{item.attachments?.map((att, idx) => (
|
{item.attachments?.map((att, idx) => (
|
||||||
<View key={idx}>
|
<View key={idx}>
|
||||||
{att.type === 'image' && att.uri ? (
|
{att.type === 'image' && att.uri ? (
|
||||||
|
<TouchableOpacity onPress={() => setFullscreenImage(att.uri || null)} activeOpacity={0.8}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: att.uri }}
|
source={{ uri: att.uri }}
|
||||||
style={styles.attachmentImage}
|
style={styles.attachmentImage}
|
||||||
resizeMode="contain"
|
resizeMode="cover"
|
||||||
onError={() => {
|
onError={() => {
|
||||||
// Bild nicht mehr verfuegbar — Placeholder setzen
|
|
||||||
setMessages(prev => prev.map(m =>
|
setMessages(prev => prev.map(m =>
|
||||||
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
|
m.id === item.id ? { ...m, attachments: m.attachments?.map((a, i) =>
|
||||||
i === idx ? { ...a, uri: undefined } : a
|
i === idx ? { ...a, uri: undefined } : a
|
||||||
@@ -538,6 +539,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
));
|
));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
) : att.type === 'image' && !att.uri ? (
|
) : att.type === 'image' && !att.uri ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.attachmentFile}
|
style={styles.attachmentFile}
|
||||||
@@ -675,6 +677,23 @@ const ChatScreen: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Bild-Vollbild Modal */}
|
||||||
|
<Modal visible={!!fullscreenImage} transparent animationType="fade" onRequestClose={() => setFullscreenImage(null)}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.fullscreenOverlay}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setFullscreenImage(null)}
|
||||||
|
>
|
||||||
|
{fullscreenImage && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: fullscreenImage }}
|
||||||
|
style={styles.fullscreenImage}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Datei-Upload Modal */}
|
{/* Datei-Upload Modal */}
|
||||||
<Modal visible={showFileUpload} transparent animationType="slide">
|
<Modal visible={showFileUpload} transparent animationType="slide">
|
||||||
<View style={styles.modalOverlay}>
|
<View style={styles.modalOverlay}>
|
||||||
@@ -757,7 +776,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
attachmentImage: {
|
attachmentImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 200,
|
minHeight: 200,
|
||||||
|
maxHeight: 400,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
backgroundColor: '#0D0D1A',
|
backgroundColor: '#0D0D1A',
|
||||||
@@ -867,6 +887,16 @@ const styles = StyleSheet.create({
|
|||||||
wakeWordIcon: {
|
wakeWordIcon: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
|
fullscreenOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.95)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
fullscreenImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
modalOverlay: {
|
modalOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
|
const [storagePath, setStoragePath] = useState(DEFAULT_STORAGE_PATH);
|
||||||
const [autoDownload, setAutoDownload] = useState(true);
|
const [autoDownload, setAutoDownload] = useState(true);
|
||||||
const [storageSize, setStorageSize] = useState('...');
|
const [storageSize, setStorageSize] = useState('...');
|
||||||
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||||
|
const [defaultVoice, setDefaultVoice] = useState('ramona');
|
||||||
|
const [highlightVoice, setHighlightVoice] = useState('thorsten');
|
||||||
const [editingPath, setEditingPath] = useState(false);
|
const [editingPath, setEditingPath] = useState(false);
|
||||||
const [tempPath, setTempPath] = useState('');
|
const [tempPath, setTempPath] = useState('');
|
||||||
|
|
||||||
@@ -91,6 +94,15 @@ const SettingsScreen: React.FC = () => {
|
|||||||
AsyncStorage.getItem('aria_auto_download').then(saved => {
|
AsyncStorage.getItem('aria_auto_download').then(saved => {
|
||||||
if (saved !== null) setAutoDownload(saved === 'true');
|
if (saved !== null) setAutoDownload(saved === 'true');
|
||||||
});
|
});
|
||||||
|
AsyncStorage.getItem('aria_tts_enabled').then(saved => {
|
||||||
|
if (saved !== null) setTtsEnabled(saved === 'true');
|
||||||
|
});
|
||||||
|
AsyncStorage.getItem('aria_default_voice').then(saved => {
|
||||||
|
if (saved) setDefaultVoice(saved);
|
||||||
|
});
|
||||||
|
AsyncStorage.getItem('aria_highlight_voice').then(saved => {
|
||||||
|
if (saved) setHighlightVoice(saved);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Speichergroesse berechnen
|
// Speichergroesse berechnen
|
||||||
@@ -442,6 +454,83 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* === Sprachausgabe === */}
|
||||||
|
<Text style={styles.sectionTitle}>Sprachausgabe</Text>
|
||||||
|
<View style={styles.card}>
|
||||||
|
{/* TTS An/Aus */}
|
||||||
|
<View style={styles.toggleRow}>
|
||||||
|
<View style={styles.toggleInfo}>
|
||||||
|
<Text style={styles.toggleLabel}>Sprachausgabe</Text>
|
||||||
|
<Text style={styles.toggleHint}>ARIA antwortet per Sprache (TTS)</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={ttsEnabled}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setTtsEnabled(val);
|
||||||
|
AsyncStorage.setItem('aria_tts_enabled', String(val));
|
||||||
|
rvs.send('config' as any, { ttsEnabled: val });
|
||||||
|
}}
|
||||||
|
trackColor={{ false: '#2A2A3E', true: '#0096FF' }}
|
||||||
|
thumbColor={ttsEnabled ? '#FFFFFF' : '#666680'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Standard-Stimme */}
|
||||||
|
<View style={{marginTop: 16}}>
|
||||||
|
<Text style={styles.toggleLabel}>Standard-Stimme</Text>
|
||||||
|
<Text style={styles.toggleHint}>Fuer normale Antworten und Gespraeche</Text>
|
||||||
|
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, defaultVoice === 'ramona' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, defaultVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
|
||||||
|
<Text style={styles.voiceBtnHint}>Weiblich, warm</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, defaultVoice === 'thorsten' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, defaultVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
|
||||||
|
<Text style={styles.voiceBtnHint}>Maennlich, tief</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Highlight-Stimme */}
|
||||||
|
<View style={{marginTop: 16}}>
|
||||||
|
<Text style={styles.toggleLabel}>Highlight-Stimme</Text>
|
||||||
|
<Text style={styles.toggleHint}>Fuer besondere Ereignisse (Deploy, Alarm, Erfolg)</Text>
|
||||||
|
<View style={{flexDirection: 'row', gap: 8, marginTop: 8}}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, highlightVoice === 'thorsten' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83E\uDDD4'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, highlightVoice === 'thorsten' && styles.voiceBtnTextActive]}>Thorsten</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.voiceBtn, highlightVoice === 'ramona' && styles.voiceBtnActive]}
|
||||||
|
onPress={() => { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); }}
|
||||||
|
>
|
||||||
|
<Text style={styles.voiceBtnIcon}>{'\uD83D\uDE4E\u200D\u2640\uFE0F'}</Text>
|
||||||
|
<Text style={[styles.voiceBtnText, highlightVoice === 'ramona' && styles.voiceBtnTextActive]}>Ramona</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Highlight-Trigger Info */}
|
||||||
|
<View style={{marginTop: 16, padding: 10, backgroundColor: '#1E1E2E', borderRadius: 8}}>
|
||||||
|
<Text style={styles.toggleLabel}>{'\u26A1'} Highlight-Trigger</Text>
|
||||||
|
<Text style={[styles.toggleHint, {marginTop: 4}]}>
|
||||||
|
Die Highlight-Stimme wird automatisch bei diesen Woertern verwendet:{'\n'}
|
||||||
|
deploy, erfolgreich, alarm, so soll es sein, kritisch, server down, sicherheitswarnung, ticket geloest, aufgabe abgeschlossen
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* === Speicher === */}
|
{/* === Speicher === */}
|
||||||
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
|
<Text style={styles.sectionTitle}>Anhang-Speicher</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
@@ -601,7 +690,7 @@ 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.1.6 </Text>
|
<Text style={styles.aboutVersion}>Version 0.0.1.9 </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.
|
||||||
@@ -744,6 +833,38 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Stimmen
|
||||||
|
voiceBtn: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: '#1E1E2E',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
},
|
||||||
|
voiceBtnActive: {
|
||||||
|
borderColor: '#0096FF',
|
||||||
|
backgroundColor: '#0D1A2E',
|
||||||
|
},
|
||||||
|
voiceBtnIcon: {
|
||||||
|
fontSize: 28,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
voiceBtnText: {
|
||||||
|
color: '#8888AA',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
voiceBtnTextActive: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
voiceBtnHint: {
|
||||||
|
color: '#555570',
|
||||||
|
fontSize: 11,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
|
||||||
// Speicher
|
// Speicher
|
||||||
storagePathText: {
|
storagePathText: {
|
||||||
color: '#0096FF',
|
color: '#0096FF',
|
||||||
|
|||||||
+12
-5
@@ -184,6 +184,9 @@ class VoiceEngine:
|
|||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
with wave.open(tmp_path, "wb") as wav_file:
|
with wave.open(tmp_path, "wb") as wav_file:
|
||||||
|
wav_file.setnchannels(1)
|
||||||
|
wav_file.setsampwidth(2) # 16-bit
|
||||||
|
wav_file.setframerate(voice.config.sample_rate)
|
||||||
voice.synthesize(text, wav_file)
|
voice.synthesize(text, wav_file)
|
||||||
|
|
||||||
audio_data = Path(tmp_path).read_bytes()
|
audio_data = Path(tmp_path).read_bytes()
|
||||||
@@ -927,7 +930,7 @@ class ARIABridge:
|
|||||||
if msg_type == "chat":
|
if msg_type == "chat":
|
||||||
# Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!)
|
# Nur User-Nachrichten weiterleiten — ARIA/Diagnostic-Antworten ignorieren (sonst Loop!)
|
||||||
sender = payload.get("sender", "")
|
sender = payload.get("sender", "")
|
||||||
if sender in ("aria", "diagnostic", "stt"):
|
if sender in ("aria", "stt"):
|
||||||
return
|
return
|
||||||
text = payload.get("text", "")
|
text = payload.get("text", "")
|
||||||
if text:
|
if text:
|
||||||
@@ -984,7 +987,8 @@ class ARIABridge:
|
|||||||
text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
|
text = (f"Stefan hat dir ein Bild geschickt: {file_name}"
|
||||||
f"{f' ({width}x{height}px)' if width else ''}"
|
f"{f' ({width}x{height}px)' if width else ''}"
|
||||||
f", {size_kb}KB."
|
f", {size_kb}KB."
|
||||||
f" Das Bild liegt unter: {file_path}")
|
f" Das Bild liegt unter: {file_path}"
|
||||||
|
f" Warte auf Stefans Anweisung was du damit tun sollst.")
|
||||||
await self.send_to_core(text, source="app-file")
|
await self.send_to_core(text, source="app-file")
|
||||||
# Dann App informieren (optional, darf nicht crashen)
|
# Dann App informieren (optional, darf nicht crashen)
|
||||||
try:
|
try:
|
||||||
@@ -1006,7 +1010,8 @@ class ARIABridge:
|
|||||||
# ERST an aria-core senden
|
# ERST an aria-core senden
|
||||||
text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
|
text = (f"Stefan hat dir eine Datei geschickt: {file_name}"
|
||||||
f" ({file_type}, {size_kb}KB)."
|
f" ({file_type}, {size_kb}KB)."
|
||||||
f" Die Datei liegt unter: {file_path}")
|
f" Die Datei liegt unter: {file_path}"
|
||||||
|
f" Warte auf Stefans Anweisung was du damit tun sollst.")
|
||||||
await self.send_to_core(text, source="app-file")
|
await self.send_to_core(text, source="app-file")
|
||||||
try:
|
try:
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
@@ -1216,8 +1221,10 @@ class ARIABridge:
|
|||||||
logger.info("Keine Sprache erkannt — ignoriert")
|
logger.info("Keine Sprache erkannt — ignoriert")
|
||||||
|
|
||||||
except sd.PortAudioError:
|
except sd.PortAudioError:
|
||||||
logger.error("Audio-Geraet nicht verfuegbar — warte 5 Sekunden")
|
if not hasattr(self, '_audio_warned'):
|
||||||
await asyncio.sleep(5)
|
logger.warning("Audio-Geraet nicht verfuegbar — lokales Mikrofon deaktiviert (kein Spam mehr)")
|
||||||
|
self._audio_warned = True
|
||||||
|
await asyncio.sleep(60) # 60s statt 5s — spart Log-Spam
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Fehler in der Audio-Schleife")
|
logger.exception("Fehler in der Audio-Schleife")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|||||||
@@ -201,6 +201,9 @@
|
|||||||
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
|
<button class="btn secondary" onclick="toggleChatFullscreen()" id="btn-chat-fs" style="padding:4px 10px;font-size:11px;">Vollbild</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-box" id="chat-box"></div>
|
<div class="chat-box" id="chat-box"></div>
|
||||||
|
<div id="thinking-indicator" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:0 0 6px 6px;margin-top:-8px;margin-bottom:8px;">
|
||||||
|
<span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text">ARIA denkt...</span>
|
||||||
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
|
<input type="text" id="chat-input" placeholder="Nachricht an ARIA...">
|
||||||
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
<button class="btn" id="btn-gw" onclick="testGateway()">Gateway senden</button>
|
||||||
@@ -216,6 +219,9 @@
|
|||||||
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
|
<button class="btn secondary" onclick="toggleChatFullscreen()" style="padding:6px 14px;">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
|
<div id="chat-box-fs" class="chat-box" style="flex:1;max-height:none;min-height:0;overflow-y:auto;"></div>
|
||||||
|
<div id="thinking-indicator-fs" style="display:none;padding:6px 10px;font-size:12px;color:#FFD60A;background:#1E1E2E;border-radius:6px;margin-top:4px;">
|
||||||
|
<span style="animation:pulse 1s infinite;">💭</span> <span id="thinking-text-fs">ARIA denkt...</span>
|
||||||
|
</div>
|
||||||
<div class="input-row" style="margin-top:8px;">
|
<div class="input-row" style="margin-top:8px;">
|
||||||
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
|
<input type="text" id="chat-input-fs" placeholder="Nachricht an ARIA..." onkeydown="if(event.key==='Enter'){testRVSFS();event.preventDefault();}">
|
||||||
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
|
<button class="btn" onclick="testGatewayFS()">Gateway senden</button>
|
||||||
@@ -507,6 +513,11 @@
|
|||||||
if (msg.type === 'state') { updateState(msg.state); return; }
|
if (msg.type === 'state') { updateState(msg.state); return; }
|
||||||
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
|
if (msg.type === 'log') { addLog(msg.entry.level, msg.entry.source, msg.entry.message, msg.entry.ts); return; }
|
||||||
|
|
||||||
|
if (msg.type === 'agent_activity') {
|
||||||
|
updateThinkingIndicator(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === 'chat_final') {
|
if (msg.type === 'chat_final') {
|
||||||
addChat('received', msg.text, 'chat:final');
|
addChat('received', msg.text, 'chat:final');
|
||||||
return;
|
return;
|
||||||
@@ -883,6 +894,10 @@
|
|||||||
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
return `<a href="${match}" target="_blank">${match}</a><img src="${match}" class="chat-media" onclick="openLightbox('image','${match}')" onerror="this.style.display='none'">`;
|
||||||
});
|
});
|
||||||
const html = `${linked}<div class="meta">${escapeHtml(meta)} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
const html = `${linked}<div class="meta">${escapeHtml(meta)} — ${new Date().toLocaleTimeString('de-DE')}</div>`;
|
||||||
|
|
||||||
|
// Thinking-Indikator ausblenden bei neuer Nachricht
|
||||||
|
updateThinkingIndicator({ activity: 'idle' });
|
||||||
|
|
||||||
// In beide Chat-Boxen schreiben (normal + Vollbild)
|
// In beide Chat-Boxen schreiben (normal + Vollbild)
|
||||||
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
for (const box of [chatBox, document.getElementById('chat-box-fs')]) {
|
||||||
if (!box) continue;
|
if (!box) continue;
|
||||||
@@ -930,6 +945,52 @@
|
|||||||
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
|
if (e.key === 'Escape' && chatFullscreen) toggleChatFullscreen();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Thinking-Indikator ─────────────────────────────
|
||||||
|
let thinkingTimeout = null;
|
||||||
|
const TOOL_LABELS = {
|
||||||
|
'Bash': '\uD83D\uDDA5\uFE0F Shell-Befehl',
|
||||||
|
'WebFetch': '\uD83C\uDF10 Webseite abrufen',
|
||||||
|
'WebSearch': '\uD83D\uDD0D Suche',
|
||||||
|
'Read': '\uD83D\uDCC4 Datei lesen',
|
||||||
|
'Write': '\u270D\uFE0F Datei schreiben',
|
||||||
|
'Edit': '\u270D\uFE0F Datei bearbeiten',
|
||||||
|
'Grep': '\uD83D\uDD0D Code durchsuchen',
|
||||||
|
'Glob': '\uD83D\uDCC1 Dateien suchen',
|
||||||
|
'Agent': '\uD83E\uDD16 Sub-Agent',
|
||||||
|
};
|
||||||
|
function updateThinkingIndicator(msg) {
|
||||||
|
const indicators = [
|
||||||
|
document.getElementById('thinking-indicator'),
|
||||||
|
document.getElementById('thinking-indicator-fs'),
|
||||||
|
];
|
||||||
|
const texts = [
|
||||||
|
document.getElementById('thinking-text'),
|
||||||
|
document.getElementById('thinking-text-fs'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (msg.activity === 'idle') {
|
||||||
|
indicators.forEach(el => { if (el) el.style.display = 'none'; });
|
||||||
|
if (thinkingTimeout) { clearTimeout(thinkingTimeout); thinkingTimeout = null; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = 'ARIA denkt...';
|
||||||
|
if (msg.activity === 'tool' && msg.tool) {
|
||||||
|
label = TOOL_LABELS[msg.tool] || `\uD83D\uDD27 ${msg.tool}`;
|
||||||
|
} else if (msg.activity === 'assistant') {
|
||||||
|
label = 'ARIA schreibt...';
|
||||||
|
}
|
||||||
|
|
||||||
|
indicators.forEach(el => { if (el) el.style.display = 'block'; });
|
||||||
|
texts.forEach(el => { if (el) el.textContent = label; });
|
||||||
|
|
||||||
|
// Auto-Hide nach 2min (falls idle Event verpasst wird — ARIA arbeitet max 15min)
|
||||||
|
if (thinkingTimeout) clearTimeout(thinkingTimeout);
|
||||||
|
thinkingTimeout = setTimeout(() => {
|
||||||
|
indicators.forEach(el => { if (el) el.style.display = 'none'; });
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
|
||||||
function openLightbox(mediaType, url) {
|
function openLightbox(mediaType, url) {
|
||||||
const lb = document.getElementById('lightbox');
|
const lb = document.getElementById('lightbox');
|
||||||
if (mediaType === 'video') {
|
if (mediaType === 'video') {
|
||||||
|
|||||||
+115
-30
@@ -74,8 +74,8 @@ function pipelineStart(method, text) {
|
|||||||
pipelineStartTime = Date.now();
|
pipelineStartTime = Date.now();
|
||||||
if (pipelineTimeout) clearTimeout(pipelineTimeout);
|
if (pipelineTimeout) clearTimeout(pipelineTimeout);
|
||||||
pipelineTimeout = setTimeout(() => {
|
pipelineTimeout = setTimeout(() => {
|
||||||
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s");
|
if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 10min");
|
||||||
}, 60000);
|
}, 600000);
|
||||||
plog(`━━━ Pipeline Start: ${method} ━━━`);
|
plog(`━━━ Pipeline Start: ${method} ━━━`);
|
||||||
plog(`Nachricht: "${text}"`);
|
plog(`Nachricht: "${text}"`);
|
||||||
}
|
}
|
||||||
@@ -319,10 +319,23 @@ function handleGatewayMessage(msg) {
|
|||||||
if (event === "agent") {
|
if (event === "agent") {
|
||||||
const data = payload.data || {};
|
const data = payload.data || {};
|
||||||
const delta = data.delta || "";
|
const delta = data.delta || "";
|
||||||
if (delta && payload.stream === "assistant") {
|
const stream = payload.stream || "";
|
||||||
|
|
||||||
|
if (delta && stream === "assistant") {
|
||||||
broadcast({ type: "chat_delta", delta, payload });
|
broadcast({ type: "chat_delta", delta, payload });
|
||||||
}
|
}
|
||||||
// agent Events nicht einzeln loggen (zu viele)
|
|
||||||
|
// Tool-Nutzung erkennen und broadcasten
|
||||||
|
if (stream === "tool_use" || data.type === "tool_use") {
|
||||||
|
const toolName = data.name || data.tool || payload.tool || "";
|
||||||
|
if (toolName) {
|
||||||
|
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
|
||||||
|
log("info", "gateway", `Tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genereller Activity-Heartbeat (ARIA denkt)
|
||||||
|
broadcast({ type: "agent_activity", activity: stream || "thinking" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +351,7 @@ function handleGatewayMessage(msg) {
|
|||||||
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
||||||
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
||||||
broadcast({ type: "chat_final", text, payload });
|
broadcast({ type: "chat_final", text, payload });
|
||||||
|
broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,14 +427,7 @@ function sendToGateway(text, isPipeline) {
|
|||||||
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
|
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
|
||||||
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
|
if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`);
|
||||||
|
|
||||||
// Nachricht auch an RVS senden damit die App sie sieht
|
// Gateway-Nachrichten NICHT an RVS senden (sonst doppelter ARIA-Request via Bridge)
|
||||||
if (rvsWs && rvsWs.readyState === WebSocket.OPEN) {
|
|
||||||
rvsWs.send(JSON.stringify({
|
|
||||||
type: "chat",
|
|
||||||
payload: { text, sender: "diagnostic" },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +441,13 @@ function connectRVS(forcePlain) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt)
|
// Alte Verbindung sauber schliessen
|
||||||
|
if (rvsWs) {
|
||||||
|
try { rvsWs.removeAllListeners(); rvsWs.close(); } catch (_) {}
|
||||||
|
rvsWs = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS-Logik: wss zuerst, bei Fehler Fallback auf ws
|
||||||
const useTls = RVS_TLS === "true" && !forcePlain;
|
const useTls = RVS_TLS === "true" && !forcePlain;
|
||||||
const proto = useTls ? "wss" : "ws";
|
const proto = useTls ? "wss" : "ws";
|
||||||
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
||||||
@@ -443,7 +456,18 @@ function connectRVS(forcePlain) {
|
|||||||
broadcastState();
|
broadcastState();
|
||||||
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
|
log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`);
|
||||||
|
|
||||||
const ws = new WebSocket(url);
|
let ws;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
} catch (err) {
|
||||||
|
log("error", "rvs", `WebSocket erstellen fehlgeschlagen: ${err.message}`);
|
||||||
|
if (useTls && RVS_TLS_FALLBACK === "true") {
|
||||||
|
connectRVS(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackTriggered = false;
|
||||||
|
|
||||||
ws.on("open", () => {
|
ws.on("open", () => {
|
||||||
log("info", "rvs", `Verbunden (${proto})`);
|
log("info", "rvs", `Verbunden (${proto})`);
|
||||||
@@ -451,6 +475,16 @@ function connectRVS(forcePlain) {
|
|||||||
state.rvs.lastError = null;
|
state.rvs.lastError = null;
|
||||||
rvsWs = ws;
|
rvsWs = ws;
|
||||||
broadcastState();
|
broadcastState();
|
||||||
|
|
||||||
|
// Keepalive: alle 25s ein Ping senden damit die Verbindung nicht stirbt
|
||||||
|
const keepalive = setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
try { ws.ping(); } catch (_) {}
|
||||||
|
} else {
|
||||||
|
clearInterval(keepalive);
|
||||||
|
}
|
||||||
|
}, 25000);
|
||||||
|
ws._keepalive = keepalive;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("message", (raw) => {
|
ws.on("message", (raw) => {
|
||||||
@@ -458,11 +492,24 @@ function connectRVS(forcePlain) {
|
|||||||
const msg = JSON.parse(raw.toString());
|
const msg = JSON.parse(raw.toString());
|
||||||
if (msg.type === "chat" && msg.payload) {
|
if (msg.type === "chat" && msg.payload) {
|
||||||
const sender = msg.payload.sender || "?";
|
const sender = msg.payload.sender || "?";
|
||||||
|
// Eigene Nachrichten ignorieren (Echo)
|
||||||
|
if (sender === "diagnostic") return;
|
||||||
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
|
log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`);
|
||||||
if (pipelineActive && sender !== "diagnostic") {
|
if (pipelineActive) {
|
||||||
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
|
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`);
|
||||||
}
|
}
|
||||||
broadcast({ type: "rvs_chat", msg });
|
broadcast({ type: "rvs_chat", msg });
|
||||||
|
} else if (msg.type === "file_saved" && msg.payload) {
|
||||||
|
// Bild/Datei-Upload von der App — im Chat anzeigen
|
||||||
|
const name = msg.payload.name || "?";
|
||||||
|
const serverPath = msg.payload.serverPath || "";
|
||||||
|
const mimeType = msg.payload.mimeType || "";
|
||||||
|
log("info", "rvs", `Datei empfangen: ${name} (${serverPath})`);
|
||||||
|
// Als User-Nachricht mit Pfad broadcasten (Diagnostic zeigt Bilder inline)
|
||||||
|
broadcast({ type: "rvs_chat", msg: {
|
||||||
|
type: "chat",
|
||||||
|
payload: { text: `Anhang: ${name}\n${serverPath}`, sender: "user" }
|
||||||
|
}});
|
||||||
} else if (msg.type === "heartbeat") {
|
} else if (msg.type === "heartbeat") {
|
||||||
// ignorieren
|
// ignorieren
|
||||||
} else {
|
} else {
|
||||||
@@ -473,10 +520,13 @@ function connectRVS(forcePlain) {
|
|||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
log("warn", "rvs", "Verbindung geschlossen");
|
log("warn", "rvs", "Verbindung geschlossen");
|
||||||
|
if (ws._keepalive) clearInterval(ws._keepalive);
|
||||||
state.rvs.status = "disconnected";
|
state.rvs.status = "disconnected";
|
||||||
rvsWs = null;
|
if (rvsWs === ws) rvsWs = null;
|
||||||
broadcastState();
|
broadcastState();
|
||||||
setTimeout(() => connectRVS(), 5000);
|
if (!fallbackTriggered) {
|
||||||
|
setTimeout(() => connectRVS(), 5000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", (err) => {
|
ws.on("error", (err) => {
|
||||||
@@ -484,29 +534,64 @@ function connectRVS(forcePlain) {
|
|||||||
state.rvs.lastError = err.message;
|
state.rvs.lastError = err.message;
|
||||||
broadcastState();
|
broadcastState();
|
||||||
|
|
||||||
// TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen
|
// TLS Fallback
|
||||||
if (useTls && RVS_TLS_FALLBACK === "true") {
|
if (useTls && RVS_TLS_FALLBACK === "true" && !fallbackTriggered) {
|
||||||
|
fallbackTriggered = true;
|
||||||
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
|
log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://");
|
||||||
ws.removeAllListeners();
|
try { ws.removeAllListeners(); ws.close(); } catch (_) {}
|
||||||
try { ws.close(); } catch (_) {}
|
if (rvsWs === ws) rvsWs = null;
|
||||||
connectRVS(true);
|
connectRVS(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToRVS(text, isPipeline) {
|
function sendToRVS(text, isPipeline) {
|
||||||
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) {
|
if (!RVS_HOST || !RVS_TOKEN) {
|
||||||
log("error", "rvs", "Nicht verbunden");
|
log("error", "rvs", "Nicht konfiguriert");
|
||||||
if (isPipeline) pipelineEnd(false, "RVS nicht verbunden");
|
if (isPipeline) pipelineEnd(false, "RVS nicht konfiguriert");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rvsWs.send(JSON.stringify({
|
// Frische WebSocket-Verbindung fuer jede Nachricht (Zombie-Schutz)
|
||||||
|
const proto = RVS_TLS === "true" ? "wss" : "ws";
|
||||||
|
const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`;
|
||||||
|
const msg = JSON.stringify({
|
||||||
type: "chat",
|
type: "chat",
|
||||||
payload: { text, sender: "diagnostic" },
|
payload: { text, sender: "diagnostic" },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}));
|
});
|
||||||
log("info", "rvs", `Gesendet via RVS: "${text}"`);
|
|
||||||
|
log("info", "rvs", `Sende via frische Verbindung: ${url.split('?')[0]}`);
|
||||||
|
|
||||||
|
const freshWs = new WebSocket(url);
|
||||||
|
freshWs.on("open", () => {
|
||||||
|
freshWs.send(msg);
|
||||||
|
log("info", "rvs", `Gesendet via RVS: "${text}"`);
|
||||||
|
// Verbindung offen lassen fuer Antwort-Empfang, nach 5min schliessen
|
||||||
|
setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 300000);
|
||||||
|
});
|
||||||
|
freshWs.on("message", (raw) => {
|
||||||
|
try {
|
||||||
|
const resp = JSON.parse(raw.toString());
|
||||||
|
if (resp.type === "chat" && resp.payload) {
|
||||||
|
const sender = resp.payload.sender || "?";
|
||||||
|
// Eigene Nachrichten und STT ignorieren (werden von persistenter Verbindung gehandelt)
|
||||||
|
if (sender === "diagnostic" || sender === "stt") return;
|
||||||
|
log("info", "rvs", `Chat von ${sender}: "${(resp.payload.text || "").slice(0, 100)}"`);
|
||||||
|
if (pipelineActive && sender !== "diagnostic") {
|
||||||
|
pipelineEnd(true, `Antwort via RVS von ${sender}: "${(resp.payload.text || "").slice(0, 120)}"`);
|
||||||
|
}
|
||||||
|
broadcast({ type: "rvs_chat", msg: resp });
|
||||||
|
} else if (resp.type !== "heartbeat") {
|
||||||
|
log("debug", "rvs", `Nachricht: ${JSON.stringify(resp).slice(0, 150)}`);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
freshWs.on("error", (err) => {
|
||||||
|
log("error", "rvs", `Sende-Fehler: ${err.message}`);
|
||||||
|
if (isPipeline) pipelineEnd(false, `RVS Fehler: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
|
if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -526,7 +611,7 @@ async function testProxy(prompt) {
|
|||||||
|
|
||||||
const modelsRes = await fetch(healthUrl, {
|
const modelsRes = await fetch(healthUrl, {
|
||||||
headers: { "Authorization": "Bearer not-needed" },
|
headers: { "Authorization": "Bearer not-needed" },
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(30000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!modelsRes.ok) {
|
if (!modelsRes.ok) {
|
||||||
@@ -553,7 +638,7 @@ async function testProxy(prompt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Schritt 2: Chat Completion testen (kurzer Prompt)
|
// Schritt 2: Chat Completion testen (kurzer Prompt)
|
||||||
const testPrompt = prompt || "Antworte mit genau einem Wort: Ping";
|
const testPrompt = prompt || "Antworte in einem Satz: Wer bist du und funktionierst du?";
|
||||||
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
|
log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`);
|
||||||
|
|
||||||
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
|
const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, {
|
||||||
@@ -567,7 +652,7 @@ async function testProxy(prompt) {
|
|||||||
messages: [{ role: "user", content: testPrompt }],
|
messages: [{ role: "user", content: testPrompt }],
|
||||||
max_tokens: 200,
|
max_tokens: 200,
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(30000),
|
signal: AbortSignal.timeout(120000), // 2min — Cold Start braucht Zeit
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!chatRes.ok) {
|
if (!chatRes.ok) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
- ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json)
|
||||||
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
|
- ./aria-data/ssh:/root/.ssh:ro # SSH Keys fuer VM-Zugriff (aria-wohnung)
|
||||||
|
- aria-shared:/shared # Shared Volume fuer Datei-Austausch (Uploads von App)
|
||||||
environment:
|
environment:
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
|
- SHELL=/bin/bash # Claude Code Bash-Tool braucht bash (nicht nur sh/ash)
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
bildupload ghet noch nicht.
|
bildupload ghet noch nicht.
|
||||||
|
|
||||||
|
#erledigt
|
||||||
sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat
|
sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß was man gesendet hat
|
||||||
|
# ende
|
||||||
|
|
||||||
|
|
||||||
cache leeren, bilder werden nicht neu geladen beim antippen.
|
cache leeren, bilder werden nicht neu geladen beim antippen.
|
||||||
autoload geht nicht
|
autoload geht nicht
|
||||||
|
|
||||||
wenn man auf das ohr zum hören klickt stürzt ab
|
wenn man auf das ohr zum hören klickt stürzt ab
|
||||||
|
|
||||||
aria liest die nachrichten nicht vor
|
aria liest die nachrichten nicht vor
|
||||||
|
|
||||||
|
|
||||||
|
# erledigt autoscroll geht doch noch nicht zur letzten nachricht
|
||||||
|
unserer memory brain
|
||||||
|
# ende
|
||||||
|
|
||||||
|
bilder im chat größer darstellen
|
||||||
|
# ende
|
||||||
|
|
||||||
|
|
||||||
|
die viper voices downloaden über die diagnostic
|
||||||
|
# ende
|
||||||
+9
-7
@@ -58,17 +58,19 @@ echo -e "${GREEN}[1/5] Versionsnummern auf $VERSION setzen...${NC}"
|
|||||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" android/package.json
|
||||||
echo -e " ${GREEN}✓${NC} package.json → $VERSION"
|
echo -e " ${GREEN}✓${NC} package.json → $VERSION"
|
||||||
|
|
||||||
# build.gradle: versionName + versionCode (aus Major.Minor.Patch berechnen)
|
# build.gradle: versionName + versionCode (aus Version berechnen)
|
||||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
# Unterstuetzt 3-stellig (1.2.3) und 4-stellig (0.0.1.7)
|
||||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
IFS='.' read -ra VER_PARTS <<< "$VERSION"
|
||||||
PATCH=$(echo "$VERSION" | cut -d. -f3)
|
V1=${VER_PARTS[0]:-0}; V2=${VER_PARTS[1]:-0}; V3=${VER_PARTS[2]:-0}; V4=${VER_PARTS[3]:-0}
|
||||||
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
|
VERSION_CODE=$((V1 * 1000000 + V2 * 10000 + V3 * 100 + V4))
|
||||||
|
# Mindestens 1 (Android erfordert versionCode >= 1)
|
||||||
|
[ "$VERSION_CODE" -lt 1 ] && VERSION_CODE=1
|
||||||
sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle
|
sed -i "s/versionName \"[^\"]*\"/versionName \"$VERSION\"/" android/android/app/build.gradle
|
||||||
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle
|
sed -i "s/versionCode [0-9]*/versionCode $VERSION_CODE/" android/android/app/build.gradle
|
||||||
echo -e " ${GREEN}✓${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE"
|
echo -e " ${GREEN}✓${NC} build.gradle → versionName $VERSION, versionCode $VERSION_CODE"
|
||||||
|
|
||||||
# SettingsScreen: Anzeige-Version
|
# SettingsScreen: Anzeige-Version (beliebiges Versionsformat)
|
||||||
sed -i "s/Version [0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]* [^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
|
sed -i "s/Version [0-9][0-9.]*[^<]*/Version $VERSION /" android/src/screens/SettingsScreen.tsx
|
||||||
echo -e " ${GREEN}✓${NC} SettingsScreen → Version $VERSION"
|
echo -e " ${GREEN}✓${NC} SettingsScreen → Version $VERSION"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user