Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7c2f07361 | ||
|
|
ccf6dd84fb | ||
|
|
75675ed3aa | ||
|
|
73fee27e90 | ||
|
|
03021a6787 | ||
|
|
b6a5d7029f | ||
|
|
3a8202d2de | ||
|
|
7bbb75481c |
@@ -11,6 +11,8 @@
|
|||||||
#
|
#
|
||||||
# Optionen:
|
# Optionen:
|
||||||
# --up am Ende den xtts-Stack starten (Default-Profil, OHNE voxtral)
|
# --up am Ende den xtts-Stack starten (Default-Profil, OHNE voxtral)
|
||||||
|
# --upgrade-driver NVIDIA-Treiber aus trixie-backports (modernes CUDA fuer Voxtral).
|
||||||
|
# Danach REBOOT noetig. Default-Bootstrap laesst 550 unangetastet.
|
||||||
# --token <TOK> RVS_TOKEN in xtts/.env eintragen (alternativ: env RVS_TOKEN=...)
|
# --token <TOK> RVS_TOKEN in xtts/.env eintragen (alternativ: env RVS_TOKEN=...)
|
||||||
# --rvs-host <H> RVS_HOST setzen (Default aus .env.example)
|
# --rvs-host <H> RVS_HOST setzen (Default aus .env.example)
|
||||||
#
|
#
|
||||||
@@ -21,11 +23,13 @@ set -euo pipefail
|
|||||||
|
|
||||||
# ── CLI ──
|
# ── CLI ──
|
||||||
DO_UP=0
|
DO_UP=0
|
||||||
|
DO_UPGRADE_DRIVER=0
|
||||||
RVS_TOKEN_ARG="${RVS_TOKEN:-}"
|
RVS_TOKEN_ARG="${RVS_TOKEN:-}"
|
||||||
RVS_HOST_ARG="${RVS_HOST:-}"
|
RVS_HOST_ARG="${RVS_HOST:-}"
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--up) DO_UP=1; shift ;;
|
--up) DO_UP=1; shift ;;
|
||||||
|
--upgrade-driver) DO_UPGRADE_DRIVER=1; shift ;;
|
||||||
--token) RVS_TOKEN_ARG="${2:-}"; shift 2 ;;
|
--token) RVS_TOKEN_ARG="${2:-}"; shift 2 ;;
|
||||||
--rvs-host) RVS_HOST_ARG="${2:-}"; shift 2 ;;
|
--rvs-host) RVS_HOST_ARG="${2:-}"; shift 2 ;;
|
||||||
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
@@ -90,6 +94,45 @@ fi
|
|||||||
[[ $ENABLED_ANY -eq 0 ]] && ok "non-free schon aktiv"
|
[[ $ENABLED_ANY -eq 0 ]] && ok "non-free schon aktiv"
|
||||||
apt-get update -qq # immer neu einlesen, damit der Kandidat sicher da ist
|
apt-get update -qq # immer neu einlesen, damit der Kandidat sicher da ist
|
||||||
|
|
||||||
|
# ── 2b. (opt-in) Treiber-Upgrade via trixie-backports — fuer Voxtral/modernes CUDA ──
|
||||||
|
# Der Trixie-Standardtreiber (550, CUDA 12.4) ist zu alt fuer den modernen Stack
|
||||||
|
# (torch 2.13, vLLM). Backports bringt einen neueren, apt-verwalteten Treiber.
|
||||||
|
# NUR mit --upgrade-driver, damit ein laufendes Setup nicht ungewollt angefasst wird.
|
||||||
|
if [[ "$DO_UPGRADE_DRIVER" -eq 1 ]]; then
|
||||||
|
step "NVIDIA-Treiber-Upgrade (trixie-backports)"
|
||||||
|
BP_FILE="/etc/apt/sources.list.d/backports.sources"
|
||||||
|
if ! grep -rqs "trixie-backports" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null; then
|
||||||
|
cat > "$BP_FILE" <<'EOF'
|
||||||
|
Types: deb
|
||||||
|
URIs: http://deb.debian.org/debian
|
||||||
|
Suites: trixie-backports
|
||||||
|
Components: main contrib non-free non-free-firmware
|
||||||
|
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||||
|
EOF
|
||||||
|
ok "trixie-backports hinzugefuegt"
|
||||||
|
else
|
||||||
|
ok "trixie-backports bereits aktiv"
|
||||||
|
fi
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y linux-headers-amd64 || true
|
||||||
|
apt-get install -y "linux-headers-$(uname -r)" || true
|
||||||
|
if apt-get install -y -t trixie-backports nvidia-driver; then
|
||||||
|
dkms autoinstall >/dev/null 2>&1 || true
|
||||||
|
NEWV="$(dpkg-query -W -f='${Version}' nvidia-driver 2>/dev/null || echo '?')"
|
||||||
|
ok "nvidia-driver aus backports installiert: ${NEWV}"
|
||||||
|
echo
|
||||||
|
echo -e "${c_y}==> REBOOT noetig, damit der neue Treiber laedt:${c_0}"
|
||||||
|
echo -e "${c_y} sudo reboot && danach: cd ai-box && sudo ./bootstrap.sh --up --token <TOKEN>${c_0}"
|
||||||
|
echo -e "${c_y} (nvidia-smi zeigt dann die neue Version + CUDA-Level)${c_0}"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
warn "backports-Install fehlgeschlagen — 550er bleibt aktiv."
|
||||||
|
warn "Alternative fuer den neuesten Treiber: NVIDIAs CUDA-Repo fuer debian13"
|
||||||
|
warn " (developer.download.nvidia.com/compute/cuda/repos/debian13/x86_64) → Paket 'cuda-drivers'."
|
||||||
|
die "Treiber-Upgrade nicht moeglich — siehe oben."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 3. NVIDIA-Treiber ──
|
# ── 3. NVIDIA-Treiber ──
|
||||||
step "NVIDIA-Treiber"
|
step "NVIDIA-Treiber"
|
||||||
if nvidia-smi >/dev/null 2>&1; then
|
if nvidia-smi >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -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 20301
|
versionCode 20303
|
||||||
versionName "0.2.3.1"
|
versionName "0.2.3.3"
|
||||||
// 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.2.3.1",
|
"version": "0.2.3.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import VoiceButton from '../components/VoiceButton';
|
|||||||
import FileUpload, { FileData } from '../components/FileUpload';
|
import FileUpload, { FileData } from '../components/FileUpload';
|
||||||
import CameraUpload, { PhotoData } from '../components/CameraUpload';
|
import CameraUpload, { PhotoData } from '../components/CameraUpload';
|
||||||
import MessageText from '../components/MessageText';
|
import MessageText from '../components/MessageText';
|
||||||
import { loadConvWindowMs, loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs } from '../services/audio';
|
import { loadConvWindowMs, loadTtsSpeed, TTS_SPEED_DEFAULT, loadSttEndpointMs, loadMaxRecordingMs } from '../services/audio';
|
||||||
import Geolocation from '@react-native-community/geolocation';
|
import Geolocation from '@react-native-community/geolocation';
|
||||||
|
|
||||||
// --- Typen ---
|
// --- Typen ---
|
||||||
@@ -93,6 +93,9 @@ interface ChatMessage {
|
|||||||
* gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen,
|
* gespiegelt damit wir die EXAKT richtige Placeholder-Bubble ersetzen,
|
||||||
* auch wenn mehrere Aufnahmen parallel offen sind. */
|
* auch wenn mehrere Aufnahmen parallel offen sind. */
|
||||||
audioRequestId?: string;
|
audioRequestId?: string;
|
||||||
|
/** Laenge der Sprachaufnahme in Sekunden (aus dem stt_endpoint) — fuer die
|
||||||
|
* Dauer-Anzeige an der Voice-Bubble. */
|
||||||
|
durationS?: number;
|
||||||
/** Skill-Created-Bubble: ARIA hat einen neuen Skill angelegt */
|
/** Skill-Created-Bubble: ARIA hat einen neuen Skill angelegt */
|
||||||
skillCreated?: {
|
skillCreated?: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -184,6 +187,14 @@ function stripSystemHints(text: string): string {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
/** Sekunden → "M:SS" fuer die Sprachnachricht-Dauer. */
|
||||||
|
function formatDur(sec: number): string {
|
||||||
|
const s = Math.max(0, Math.round(sec));
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const r = s % 60;
|
||||||
|
return `${m}:${r.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
const DEFAULT_ATTACHMENT_DIR = `${RNFS.DocumentDirectoryPath}/chat_attachments`;
|
||||||
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
const STORAGE_PATH_KEY = 'aria_attachment_storage_path';
|
||||||
|
|
||||||
@@ -1668,7 +1679,9 @@ const ChatScreen: React.FC = () => {
|
|||||||
location: location || null,
|
location: location || null,
|
||||||
noSpeechTimeoutMs: windowMs,
|
noSpeechTimeoutMs: windowMs,
|
||||||
endpointMs: await loadSttEndpointMs(),
|
endpointMs: await loadSttEndpointMs(),
|
||||||
hardCapMs: 60000,
|
// Notbremse 5 min (nicht 1 min) — der Stille-Endpoint beendet normale
|
||||||
|
// Turns eh sofort; der Cap darf lange Diktate nicht mitten drin kappen.
|
||||||
|
hardCapMs: await loadMaxRecordingMs(),
|
||||||
projectId: focusedProjectIdRef.current,
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startStreamingRecording returned ok=${ok}`)).catch(()=>{});
|
import('../services/logger').then(m => m.reportAppDebug('wake.cb', `startStreamingRecording returned ok=${ok}`)).catch(()=>{});
|
||||||
@@ -1696,6 +1709,13 @@ const ChatScreen: React.FC = () => {
|
|||||||
if (ev.text && ev.text.trim()) {
|
if (ev.text && ev.text.trim()) {
|
||||||
console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)',
|
console.log('[Chat] STT-Endpoint: %r (reason=%s, %dms, %.1fs Audio)',
|
||||||
ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS);
|
ev.text.slice(0, 80), ev.reason, ev.sttMs, ev.durationS);
|
||||||
|
// Aufnahme-Dauer an die passende Voice-Bubble haengen (Anzeige). Der
|
||||||
|
// spaetere STT-Text-Update spreadet die Message, die Dauer bleibt.
|
||||||
|
if (ev.audioRequestId && typeof ev.durationS === 'number' && ev.durationS > 0) {
|
||||||
|
const dur = ev.durationS;
|
||||||
|
setMessages(prev => prev.map(m =>
|
||||||
|
m.audioRequestId === ev.audioRequestId ? { ...m, durationS: dur } : m));
|
||||||
|
}
|
||||||
// Wenn passive lauschend: User hat tatsaechlich was gesagt → uebergang
|
// Wenn passive lauschend: User hat tatsaechlich was gesagt → uebergang
|
||||||
// zu 'conversing' damit der normale Flow greift (TTS, resume, etc.)
|
// zu 'conversing' damit der normale Flow greift (TTS, resume, etc.)
|
||||||
if (wakeWordService.getState() === 'listening') {
|
if (wakeWordService.getState() === 'listening') {
|
||||||
@@ -1771,7 +1791,8 @@ const ChatScreen: React.FC = () => {
|
|||||||
location: location || null,
|
location: location || null,
|
||||||
noSpeechTimeoutMs: windowMs,
|
noSpeechTimeoutMs: windowMs,
|
||||||
endpointMs: await loadSttEndpointMs(),
|
endpointMs: await loadSttEndpointMs(),
|
||||||
hardCapMs: 60000,
|
// Notbremse 5 min (s.o.) — lange Diktate nicht bei 1 min abschneiden.
|
||||||
|
hardCapMs: await loadMaxRecordingMs(),
|
||||||
projectId: focusedProjectIdRef.current,
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -2255,7 +2276,7 @@ const ChatScreen: React.FC = () => {
|
|||||||
// die Session auch app-seitig haben wir +2s Toleranz.
|
// die Session auch app-seitig haben wir +2s Toleranz.
|
||||||
noSpeechTimeoutMs: 0,
|
noSpeechTimeoutMs: 0,
|
||||||
endpointMs: await loadSttEndpointMs(),
|
endpointMs: await loadSttEndpointMs(),
|
||||||
hardCapMs: 300000,
|
hardCapMs: await loadMaxRecordingMs(),
|
||||||
projectId: focusedProjectIdRef.current,
|
projectId: focusedProjectIdRef.current,
|
||||||
});
|
});
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -2647,6 +2668,16 @@ const ChatScreen: React.FC = () => {
|
|||||||
{att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'}
|
{att.serverPath ? '(tippen zum Laden)' : '(nicht verfuegbar)'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
) : att.type === 'audio' ? (
|
||||||
|
<View style={styles.attachmentFile}>
|
||||||
|
<Text style={styles.attachmentFileIcon}>{'🎙'}</Text>
|
||||||
|
<Text style={styles.attachmentFileName} numberOfLines={1}>
|
||||||
|
{att.name || 'Sprachaufnahme'}
|
||||||
|
</Text>
|
||||||
|
{typeof item.durationS === 'number' && item.durationS > 0 ? (
|
||||||
|
<Text style={styles.attachmentFileSize}>{formatDur(item.durationS)}</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.attachmentFile}
|
style={styles.attachmentFile}
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ import {
|
|||||||
CONV_WINDOW_MIN_SEC,
|
CONV_WINDOW_MIN_SEC,
|
||||||
CONV_WINDOW_MAX_SEC,
|
CONV_WINDOW_MAX_SEC,
|
||||||
CONV_WINDOW_STORAGE_KEY,
|
CONV_WINDOW_STORAGE_KEY,
|
||||||
|
STT_ENDPOINT_DEFAULT_MS,
|
||||||
|
STT_ENDPOINT_MIN_MS,
|
||||||
|
STT_ENDPOINT_MAX_MS,
|
||||||
|
STT_ENDPOINT_STORAGE_KEY,
|
||||||
MAX_RECORDING_DEFAULT_SEC,
|
MAX_RECORDING_DEFAULT_SEC,
|
||||||
MAX_RECORDING_MIN_SEC,
|
MAX_RECORDING_MIN_SEC,
|
||||||
MAX_RECORDING_MAX_SEC,
|
MAX_RECORDING_MAX_SEC,
|
||||||
@@ -195,6 +199,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||||
const [ttsPrerollSec, setTtsPrerollSec] = useState<number>(TTS_PREROLL_DEFAULT_SEC);
|
const [ttsPrerollSec, setTtsPrerollSec] = useState<number>(TTS_PREROLL_DEFAULT_SEC);
|
||||||
const [vadSilenceSec, setVadSilenceSec] = useState<number>(VAD_SILENCE_DEFAULT_SEC);
|
const [vadSilenceSec, setVadSilenceSec] = useState<number>(VAD_SILENCE_DEFAULT_SEC);
|
||||||
|
// Aktive Streaming-Pausen-Toleranz (STT_ENDPOINT) — der "Stille-Toleranz"-Regler
|
||||||
|
// steuert jetzt DIESEN Wert (der alte vadSilenceSec war der tote Legacy-dB-Pfad).
|
||||||
|
const [sttEndpointSec, setSttEndpointSec] = useState<number>(STT_ENDPOINT_DEFAULT_MS / 1000);
|
||||||
const [convWindowSec, setConvWindowSec] = useState<number>(CONV_WINDOW_DEFAULT_SEC);
|
const [convWindowSec, setConvWindowSec] = useState<number>(CONV_WINDOW_DEFAULT_SEC);
|
||||||
const [maxRecordingSec, setMaxRecordingSec] = useState<number>(MAX_RECORDING_DEFAULT_SEC);
|
const [maxRecordingSec, setMaxRecordingSec] = useState<number>(MAX_RECORDING_DEFAULT_SEC);
|
||||||
// null = automatisch (adaptive Baseline), sonst manueller dB-Override
|
// null = automatisch (adaptive Baseline), sonst manueller dB-Override
|
||||||
@@ -306,6 +313,14 @@ const SettingsScreen: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
AsyncStorage.getItem(STT_ENDPOINT_STORAGE_KEY).then(saved => {
|
||||||
|
if (saved != null) {
|
||||||
|
const n = parseInt(saved, 10);
|
||||||
|
if (isFinite(n) && n >= STT_ENDPOINT_MIN_MS && n <= STT_ENDPOINT_MAX_MS) {
|
||||||
|
setSttEndpointSec(n / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
AsyncStorage.getItem(MAX_RECORDING_STORAGE_KEY).then(saved => {
|
AsyncStorage.getItem(MAX_RECORDING_STORAGE_KEY).then(saved => {
|
||||||
if (saved != null) {
|
if (saved != null) {
|
||||||
const n = parseFloat(saved);
|
const n = parseFloat(saved);
|
||||||
@@ -1655,30 +1670,30 @@ const SettingsScreen: React.FC = () => {
|
|||||||
<Text style={styles.toggleHint}>
|
<Text style={styles.toggleHint}>
|
||||||
Wie lange du eine Sprechpause machen darfst, bevor die Aufnahme
|
Wie lange du eine Sprechpause machen darfst, bevor die Aufnahme
|
||||||
automatisch beendet und gesendet wird. Hoeher = mehr Zeit zum
|
automatisch beendet und gesendet wird. Hoeher = mehr Zeit zum
|
||||||
Nachdenken; niedriger = schnelleres Senden.
|
Nachdenken (z.B. im Auto); niedriger = schnelleres Senden.
|
||||||
Default: {VAD_SILENCE_DEFAULT_SEC.toFixed(1)}s.
|
Default: {(STT_ENDPOINT_DEFAULT_MS / 1000).toFixed(1)}s.
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.prerollRow}>
|
<View style={styles.prerollRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.prerollButton}
|
style={styles.prerollButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const next = Math.max(VAD_SILENCE_MIN_SEC, Math.round((vadSilenceSec - 0.5) * 10) / 10);
|
const next = Math.max(STT_ENDPOINT_MIN_MS / 1000, Math.round((sttEndpointSec - 0.5) * 10) / 10);
|
||||||
setVadSilenceSec(next);
|
setSttEndpointSec(next);
|
||||||
AsyncStorage.setItem(VAD_SILENCE_STORAGE_KEY, String(next));
|
AsyncStorage.setItem(STT_ENDPOINT_STORAGE_KEY, String(Math.round(next * 1000)));
|
||||||
}}
|
}}
|
||||||
disabled={vadSilenceSec <= VAD_SILENCE_MIN_SEC}
|
disabled={sttEndpointSec <= STT_ENDPOINT_MIN_MS / 1000}
|
||||||
>
|
>
|
||||||
<Text style={styles.prerollButtonText}>−0.5</Text>
|
<Text style={styles.prerollButtonText}>−0.5</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.prerollValue}>{vadSilenceSec.toFixed(1)} s</Text>
|
<Text style={styles.prerollValue}>{sttEndpointSec.toFixed(1)} s</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.prerollButton}
|
style={styles.prerollButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const next = Math.min(VAD_SILENCE_MAX_SEC, Math.round((vadSilenceSec + 0.5) * 10) / 10);
|
const next = Math.min(STT_ENDPOINT_MAX_MS / 1000, Math.round((sttEndpointSec + 0.5) * 10) / 10);
|
||||||
setVadSilenceSec(next);
|
setSttEndpointSec(next);
|
||||||
AsyncStorage.setItem(VAD_SILENCE_STORAGE_KEY, String(next));
|
AsyncStorage.setItem(STT_ENDPOINT_STORAGE_KEY, String(Math.round(next * 1000)));
|
||||||
}}
|
}}
|
||||||
disabled={vadSilenceSec >= VAD_SILENCE_MAX_SEC}
|
disabled={sttEndpointSec >= STT_ENDPOINT_MAX_MS / 1000}
|
||||||
>
|
>
|
||||||
<Text style={styles.prerollButtonText}>+0.5</Text>
|
<Text style={styles.prerollButtonText}>+0.5</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -1749,56 +1764,10 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: 24, gap: 8}}>
|
{/* "Stille-Pegel (dB)"-Regler entfernt: der aktive Streaming-STT nutzt
|
||||||
<Text style={styles.toggleLabel}>Stille-Pegel (dB)</Text>
|
einen adaptiven Rausch-Boden (automatisch), ein manueller dB-Wert war
|
||||||
<TouchableOpacity onPress={() => setShowVadInfo(true)} style={styles.infoBtn}>
|
wirkungslos. Rauschen-als-Wort verhindert das STT-Modell selbst
|
||||||
<Text style={styles.infoBtnText}>i</Text>
|
(no_speech_prob-Filter), nicht die dB-Schwelle. */}
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.toggleHint}>
|
|
||||||
Welcher Mikro-Pegel als "Stille" gilt. Standard: automatisch (Baseline aus
|
|
||||||
den ersten 500ms). Manuell setzen wenn Auto nicht zuverlaessig greift.
|
|
||||||
</Text>
|
|
||||||
<View style={styles.prerollRow}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.prerollButton}
|
|
||||||
onPress={() => {
|
|
||||||
const next = vadSilenceDb == null
|
|
||||||
? VAD_SILENCE_DB_DEFAULT - 1
|
|
||||||
: Math.max(VAD_SILENCE_DB_MIN, vadSilenceDb - 1);
|
|
||||||
setVadSilenceDb(next);
|
|
||||||
AsyncStorage.setItem(VAD_SILENCE_DB_OVERRIDE_KEY, String(next));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.prerollButtonText}>−1</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<Text style={styles.prerollValue}>
|
|
||||||
{vadSilenceDb == null ? 'auto' : `${vadSilenceDb} dB`}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.prerollButton}
|
|
||||||
onPress={() => {
|
|
||||||
const next = vadSilenceDb == null
|
|
||||||
? VAD_SILENCE_DB_DEFAULT + 1
|
|
||||||
: Math.min(VAD_SILENCE_DB_MAX, vadSilenceDb + 1);
|
|
||||||
setVadSilenceDb(next);
|
|
||||||
AsyncStorage.setItem(VAD_SILENCE_DB_OVERRIDE_KEY, String(next));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.prerollButtonText}>+1</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
{vadSilenceDb != null && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => {
|
|
||||||
setVadSilenceDb(null);
|
|
||||||
AsyncStorage.removeItem(VAD_SILENCE_DB_OVERRIDE_KEY);
|
|
||||||
}}
|
|
||||||
style={{alignSelf: 'center', marginTop: 8, paddingVertical: 6, paddingHorizontal: 12}}
|
|
||||||
>
|
|
||||||
<Text style={{color: '#0096FF', fontSize: 13}}>↻ Auf automatisch zuruecksetzen</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* AriaViewCanvas — die pannbare Flaeche, auf der ARIAs komponierte Ansicht
|
||||||
|
* (aria_view) MATERIALISIERT: Orb oben, darunter die Karten. Erscheint als
|
||||||
|
* Overlay ueber dem Chat, sobald ARIA present_view aufruft ("sag was → Orb denkt
|
||||||
|
* → Karte fliegt rein"). Der erste, greifbare Vorgeschmack aufs generative
|
||||||
|
* Cockpit (M1).
|
||||||
|
*
|
||||||
|
* Bedienung (NoMachine-Prinzip): 2-Finger halten + schieben bewegt die Welt,
|
||||||
|
* Pinch zoomt. Ein-Finger-Touch geht an die Karten durch (Scrollen). Die Welt
|
||||||
|
* traegt gerenderte/gestreamte Inhalte — interaktive native Panels rasten
|
||||||
|
* spaeter bei Scale 1 ein (Chat bleibt separat darunter).
|
||||||
|
*
|
||||||
|
* Geraete-agnostisch gehalten: liest nur die ViewSpec, damit ein spaeterer Web-/
|
||||||
|
* AR-Renderer dieselbe Spec konsumieren kann.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
FadeInDown,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||||
|
import { ViewSpec } from '../services/ariaView';
|
||||||
|
import Orb from './Orb';
|
||||||
|
import CardView from './CardView';
|
||||||
|
|
||||||
|
const MIN_SCALE = 0.5;
|
||||||
|
const MAX_SCALE = 3;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
view: ViewSpec;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AriaViewCanvas: React.FC<Props> = ({ view, onClose }) => {
|
||||||
|
const tx = useSharedValue(0);
|
||||||
|
const ty = useSharedValue(0);
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const savedTx = useSharedValue(0);
|
||||||
|
const savedTy = useSharedValue(0);
|
||||||
|
const savedScale = useSharedValue(1);
|
||||||
|
|
||||||
|
const pan = Gesture.Pan()
|
||||||
|
.minPointers(2)
|
||||||
|
.maxPointers(2)
|
||||||
|
.onUpdate((e) => {
|
||||||
|
tx.value = savedTx.value + e.translationX;
|
||||||
|
ty.value = savedTy.value + e.translationY;
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
savedTx.value = tx.value;
|
||||||
|
savedTy.value = ty.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinch = Gesture.Pinch()
|
||||||
|
.onUpdate((e) => {
|
||||||
|
const next = savedScale.value * e.scale;
|
||||||
|
scale.value = Math.max(MIN_SCALE, Math.min(MAX_SCALE, next));
|
||||||
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
savedScale.value = scale.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const composed = Gesture.Simultaneous(pan, pinch);
|
||||||
|
|
||||||
|
const worldStyle = useAnimatedStyle(() => ({
|
||||||
|
transform: [
|
||||||
|
{ translateX: tx.value },
|
||||||
|
{ translateY: ty.value },
|
||||||
|
{ scale: scale.value },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetCamera = () => {
|
||||||
|
tx.value = withTiming(0);
|
||||||
|
ty.value = withTiming(0);
|
||||||
|
scale.value = withTiming(1);
|
||||||
|
savedTx.value = 0;
|
||||||
|
savedTy.value = 0;
|
||||||
|
savedScale.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cards = Array.isArray(view.cards) ? view.cards : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.overlay}>
|
||||||
|
<GestureDetector gesture={composed}>
|
||||||
|
<Animated.View style={[styles.world, worldStyle]}>
|
||||||
|
<View style={styles.orbWrap}>
|
||||||
|
<Orb state={view.orb} size={110} />
|
||||||
|
</View>
|
||||||
|
{!!view.title && <Text style={styles.worldTitle}>{view.title}</Text>}
|
||||||
|
<View style={styles.cards}>
|
||||||
|
{cards.map((c, i) => (
|
||||||
|
<Animated.View
|
||||||
|
key={i}
|
||||||
|
entering={FadeInDown.duration(420).delay(120 + i * 90)}
|
||||||
|
>
|
||||||
|
<CardView card={c} />
|
||||||
|
</Animated.View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
|
|
||||||
|
{/* Steuerung — ausserhalb des Transforms, immer bei Scale 1 bedienbar */}
|
||||||
|
<View style={styles.topBar} pointerEvents="box-none">
|
||||||
|
<TouchableOpacity style={styles.iconBtn} onPress={resetCamera}>
|
||||||
|
<Text style={styles.icon}>⤢</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.iconBtn} onPress={onClose}>
|
||||||
|
<Text style={styles.icon}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View style={styles.hintWrap} pointerEvents="none">
|
||||||
|
<Text style={styles.hint}>2 Finger: schieben · Pinch: zoomen</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(6,6,16,0.94)',
|
||||||
|
zIndex: 50,
|
||||||
|
},
|
||||||
|
world: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 48,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
},
|
||||||
|
orbWrap: { marginTop: 8, marginBottom: 6 },
|
||||||
|
worldTitle: {
|
||||||
|
color: '#C9C9FF',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
cards: { width: '100%', maxWidth: 560 },
|
||||||
|
topBar: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 10,
|
||||||
|
right: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
iconBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
marginLeft: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(30,30,60,0.9)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(123,92,255,0.4)',
|
||||||
|
},
|
||||||
|
icon: { color: '#C9C9FF', fontSize: 18 },
|
||||||
|
hintWrap: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 14,
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
color: '#6A6A90',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AriaViewCanvas;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* CardView — rendert EINE Karte einer aria_view-Spec (M1). Schaltet nach
|
||||||
|
* card.type auf den passenden Renderer. Unbekannte Typen werden als Text-
|
||||||
|
* Fallback gezeigt (nie crashen).
|
||||||
|
*
|
||||||
|
* Bewusst dependency-leicht (v1): Markdown wird als Klartext dargestellt, Map
|
||||||
|
* als Marker-Liste (kein Karten-Lib), Code als Monospace-Block. Spaeter koennen
|
||||||
|
* einzelne Renderer aufgebohrt werden, ohne die Spec/den Fluss zu aendern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||||
|
import { ViewCard, ViewMarker } from '../services/ariaView';
|
||||||
|
|
||||||
|
const ImageBody: React.FC<{ src?: string }> = ({ src }) => {
|
||||||
|
const isUrl = !!src && /^https?:\/\//i.test(src);
|
||||||
|
if (isUrl) {
|
||||||
|
return <Image source={{ uri: src }} style={styles.image} resizeMode="contain" />;
|
||||||
|
}
|
||||||
|
return <Text style={styles.muted}>🖼️ {src || '(kein Bild)'}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListBody: React.FC<{ md?: string }> = ({ md }) => {
|
||||||
|
const lines = (md || '')
|
||||||
|
.split('\n')
|
||||||
|
.map((l) => l.replace(/^\s*[-*•]\s?/, '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (lines.length === 0) return <Text style={styles.muted}>(leer)</Text>;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{lines.map((l, i) => (
|
||||||
|
<View key={i} style={styles.listRow}>
|
||||||
|
<Text style={styles.bullet}>•</Text>
|
||||||
|
<Text style={styles.text}>{l}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MapBody: React.FC<{ markers?: ViewMarker[] }> = ({ markers }) => {
|
||||||
|
const ms = Array.isArray(markers) ? markers : [];
|
||||||
|
return (
|
||||||
|
<View style={styles.map}>
|
||||||
|
<Text style={styles.mapHint}>🗺️ Karte ({ms.length} Orte)</Text>
|
||||||
|
{ms.map((m, i) => (
|
||||||
|
<Text key={i} style={styles.text}>
|
||||||
|
📍 {m.label || `${m.lat?.toFixed?.(4)}, ${m.lon?.toFixed?.(4)}`}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBody: React.FC<{ md?: string; path?: string; lang?: string }> = ({ md, path, lang }) => (
|
||||||
|
<View>
|
||||||
|
{(path || lang) && (
|
||||||
|
<Text style={styles.codeCaption}>
|
||||||
|
{path || ''}{lang ? ` · ${lang}` : ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<ScrollView horizontal style={styles.codeScroll}>
|
||||||
|
<Text style={styles.code}>{md || ''}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CardView: React.FC<{ card: ViewCard }> = ({ card }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.card}>
|
||||||
|
{!!card.title && <Text style={styles.cardTitle}>{card.title}</Text>}
|
||||||
|
{card.type === 'image' ? (
|
||||||
|
<ImageBody src={card.src} />
|
||||||
|
) : card.type === 'list' ? (
|
||||||
|
<ListBody md={card.md} />
|
||||||
|
) : card.type === 'map' ? (
|
||||||
|
<MapBody markers={card.markers} />
|
||||||
|
) : card.type === 'code' ? (
|
||||||
|
<CodeBody md={card.md} path={card.path} lang={card.lang} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.text}>{card.md || ''}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
backgroundColor: 'rgba(18,18,42,0.92)',
|
||||||
|
borderColor: 'rgba(123,92,255,0.35)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 14,
|
||||||
|
padding: 14,
|
||||||
|
marginVertical: 8,
|
||||||
|
shadowColor: '#7B5CFF',
|
||||||
|
shadowOpacity: 0.25,
|
||||||
|
shadowRadius: 12,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
elevation: 6,
|
||||||
|
},
|
||||||
|
cardTitle: { color: '#C9C9FF', fontSize: 15, fontWeight: '700', marginBottom: 8 },
|
||||||
|
text: { color: '#E6E6F0', fontSize: 14, lineHeight: 20, flexShrink: 1 },
|
||||||
|
muted: { color: '#8A8AB0', fontSize: 13, fontStyle: 'italic' },
|
||||||
|
image: { width: '100%', height: 200, borderRadius: 8, backgroundColor: '#0D0D1A' },
|
||||||
|
listRow: { flexDirection: 'row', alignItems: 'flex-start', marginVertical: 2 },
|
||||||
|
bullet: { color: '#7B5CFF', marginRight: 8, fontSize: 14, lineHeight: 20 },
|
||||||
|
map: { backgroundColor: '#0D0D1A', borderRadius: 8, padding: 10 },
|
||||||
|
mapHint: { color: '#00B4D8', fontSize: 13, fontWeight: '600', marginBottom: 6 },
|
||||||
|
codeCaption: { color: '#8A8AB0', fontSize: 12, marginBottom: 6 },
|
||||||
|
codeScroll: { backgroundColor: '#0A0A14', borderRadius: 8, padding: 10 },
|
||||||
|
code: { color: '#B9F5C9', fontFamily: 'monospace', fontSize: 12.5, lineHeight: 18 },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(CardView);
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* Orb — ARIAs Praesenz-Avatar (M1). Zeigt ihren Zustand (idle/listening/
|
||||||
|
* thinking/speaking/working) als pulsierender Leucht-Kern und ist das
|
||||||
|
* verbindende Element ueber alle Oberflaechen (App/Web/spaeter Brille).
|
||||||
|
*
|
||||||
|
* Reine Optik, keine Logik — der Zustand kommt von aussen (aria_view.orb bzw.
|
||||||
|
* spaeter direkt von Audio/Wake-Word-Signalen). Dependency-leicht: nur
|
||||||
|
* reanimated (schon installiert), kein SVG/Gradient noetig.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
cancelAnimation,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { OrbState } from '../services/ariaView';
|
||||||
|
|
||||||
|
const COLORS: Record<OrbState, string> = {
|
||||||
|
idle: '#3A6EA5',
|
||||||
|
listening: '#00B4D8',
|
||||||
|
thinking: '#7B5CFF',
|
||||||
|
speaking: '#34C759',
|
||||||
|
working: '#FF9500',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
state?: OrbState;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Orb: React.FC<Props> = ({ state = 'idle', size = 120 }) => {
|
||||||
|
const pulse = useSharedValue(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fast = state === 'thinking' || state === 'working';
|
||||||
|
cancelAnimation(pulse);
|
||||||
|
pulse.value = 1;
|
||||||
|
pulse.value = withRepeat(
|
||||||
|
withTiming(fast ? 1.14 : 1.07, {
|
||||||
|
duration: fast ? 620 : 1500,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
}),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return () => cancelAnimation(pulse);
|
||||||
|
}, [state, pulse]);
|
||||||
|
|
||||||
|
const animStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulse.value }] }));
|
||||||
|
const color = COLORS[state] || COLORS.idle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.wrap, { width: size, height: size }]}>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.glow,
|
||||||
|
{ width: size, height: size, borderRadius: size / 2, backgroundColor: color },
|
||||||
|
animStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.ring,
|
||||||
|
{
|
||||||
|
width: size * 0.72,
|
||||||
|
height: size * 0.72,
|
||||||
|
borderRadius: size * 0.36,
|
||||||
|
borderColor: color,
|
||||||
|
},
|
||||||
|
animStyle,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.core,
|
||||||
|
{
|
||||||
|
width: size * 0.44,
|
||||||
|
height: size * 0.44,
|
||||||
|
borderRadius: size * 0.22,
|
||||||
|
backgroundColor: color,
|
||||||
|
shadowColor: color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrap: { alignItems: 'center', justifyContent: 'center' },
|
||||||
|
glow: { position: 'absolute', opacity: 0.22 },
|
||||||
|
ring: { position: 'absolute', borderWidth: 2, opacity: 0.55 },
|
||||||
|
core: {
|
||||||
|
shadowOpacity: 0.9,
|
||||||
|
shadowRadius: 16,
|
||||||
|
shadowOffset: { width: 0, height: 0 },
|
||||||
|
elevation: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default React.memo(Orb);
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
import projectFocus, { FocusSnapshot } from '../services/projectFocus';
|
||||||
import codeFile from '../services/codeFile';
|
import codeFile from '../services/codeFile';
|
||||||
import brainApi from '../services/brainApi';
|
import brainApi from '../services/brainApi';
|
||||||
@@ -16,6 +17,8 @@ import viewMode, { ViewModeValue } from '../services/viewMode';
|
|||||||
import ChatScreen from '../screens/ChatScreen';
|
import ChatScreen from '../screens/ChatScreen';
|
||||||
import { TileId } from './layout';
|
import { TileId } from './layout';
|
||||||
import WorkspaceDeck from './WorkspaceDeck';
|
import WorkspaceDeck from './WorkspaceDeck';
|
||||||
|
import ariaView, { AriaView } from '../services/ariaView';
|
||||||
|
import AriaViewCanvas from './AriaViewCanvas';
|
||||||
|
|
||||||
const COCKPIT_PANELS: TileId[] = ['chat', 'files', 'editor', 'vnc'];
|
const COCKPIT_PANELS: TileId[] = ['chat', 'files', 'editor', 'vnc'];
|
||||||
|
|
||||||
@@ -24,12 +27,21 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
const [focus, setFocus] = useState<FocusSnapshot>(projectFocus.get());
|
const [focus, setFocus] = useState<FocusSnapshot>(projectFocus.get());
|
||||||
const [hasCode, setHasCode] = useState(false);
|
const [hasCode, setHasCode] = useState(false);
|
||||||
const [hasDesktop, setHasDesktop] = useState(false);
|
const [hasDesktop, setHasDesktop] = useState(false);
|
||||||
|
const [view, setView] = useState<AriaView | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => viewMode.subscribe(setMode), []);
|
useEffect(() => viewMode.subscribe(setMode), []);
|
||||||
useEffect(() => projectFocus.subscribe(setFocus), []);
|
useEffect(() => projectFocus.subscribe(setFocus), []);
|
||||||
|
|
||||||
const pid = focus.focusedProjectId;
|
const pid = focus.focusedProjectId;
|
||||||
|
|
||||||
|
// aria_view: ARIAs komponierte Ansicht fuers fokussierte Projekt spiegeln.
|
||||||
|
useEffect(() => {
|
||||||
|
setView(ariaView.getView(pid));
|
||||||
|
return ariaView.subscribe((v) => {
|
||||||
|
if ((v.projectId || '') === (pid || '')) setView(v);
|
||||||
|
});
|
||||||
|
}, [pid]);
|
||||||
|
|
||||||
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
// Code-Signal: hat der Spiegel schon Dateien fuer dieses Projekt?
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasCode(codeFile.getFiles(pid).length > 0);
|
setHasCode(codeFile.getFiles(pid).length > 0);
|
||||||
@@ -59,13 +71,32 @@ const WorkspaceScreen: React.FC = () => {
|
|||||||
vnc: hasDesktop ? '#34C759' : undefined,
|
vnc: hasDesktop ? '#34C759' : undefined,
|
||||||
} as Partial<Record<TileId, string>>), [hasCode, hasDesktop]);
|
} as Partial<Record<TileId, string>>), [hasCode, hasDesktop]);
|
||||||
|
|
||||||
// Kompakt-Ansicht: klassischer Vollbild-Chat, exakt wie vor dem Umbau.
|
// Kompakt-Ansicht: klassischer Vollbild-Chat; Cockpit: Workbench mit Dock.
|
||||||
if (mode === 'compact') {
|
const content =
|
||||||
return <ChatScreen />;
|
mode === 'compact' ? (
|
||||||
}
|
<ChatScreen />
|
||||||
|
) : (
|
||||||
|
<WorkspaceDeck projectId={pid} panels={COCKPIT_PANELS} badges={badges} />
|
||||||
|
);
|
||||||
|
|
||||||
// Cockpit: Workbench mit Dock.
|
// Generative Flaeche als Overlay, sobald ARIA fuer dieses Projekt eine Ansicht
|
||||||
return <WorkspaceDeck projectId={pid} panels={COCKPIT_PANELS} badges={badges} />;
|
// komponiert hat (present_view → aria_view). Chat/Cockpit bleiben darunter.
|
||||||
|
const showView = !!view && (view.projectId || '') === (pid || '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
{content}
|
||||||
|
{showView && view && (
|
||||||
|
<AriaViewCanvas
|
||||||
|
view={view.view}
|
||||||
|
onClose={() => {
|
||||||
|
ariaView.clear(pid);
|
||||||
|
setView(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WorkspaceScreen;
|
export default WorkspaceScreen;
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# PyTorch CUDA-Wheels zuerst (f5-tts zieht sonst CPU-only Torch rein)
|
# torch FEST auf cu124 (Treiber 550 = CUDA 12.4). 2.6.0 ist der NEUESTE cu124-
|
||||||
RUN pip3 install --no-cache-dir torch==2.3.1 torchaudio==2.3.1 \
|
# Build — torch 2.7+ gibt es nur noch fuer cu126+, was 550 nicht unterstuetzt
|
||||||
--index-url https://download.pytorch.org/whl/cu121
|
# ("NVIDIA driver too old, found 12040"). Der Constraint haelt f5-tts davon ab,
|
||||||
|
# torch beim Dependency-Aufloesen wieder auf eine zu neue CUDA-Version zu ziehen.
|
||||||
|
RUN pip3 install --no-cache-dir torch==2.6.0 torchaudio==2.6.0 \
|
||||||
|
--index-url https://download.pytorch.org/whl/cu124
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip3 install --no-cache-dir -r requirements.txt
|
RUN printf 'torch==2.6.0\ntorchaudio==2.6.0\n' > /tmp/torch-constraint.txt && \
|
||||||
|
pip3 install --no-cache-dir -c /tmp/torch-constraint.txt -r requirements.txt
|
||||||
|
|
||||||
COPY bridge.py .
|
COPY bridge.py .
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user