[TEST] Initial setup - NOT PRODUCTION CODE

⚠️ This is test/experimental code for API integration testing.
Do not use in production.

Includes:
- WellNuo API integration (dashboard, patient context)
- Playwright tests for API verification
- WebView component for dashboard embedding
- API documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sergei 2025-12-11 13:25:14 -08:00
parent 2aef0bcf93
commit 4a5331b2e4
39 changed files with 10183 additions and 61 deletions

11
.env.example Normal file
View File

@ -0,0 +1,11 @@
# OpenAI Configuration
# Get your API key from https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
# Webhook URL for fetching context/instructions (optional)
# The webhook should return JSON with: { systemPrompt, voiceSettings, userData }
WEBHOOK_URL=https://your-webhook-url.com/context
# Voice Settings (optional)
# Available voices: alloy, echo, shimmer, ash, ballad, coral, sage, verse
DEFAULT_VOICE=shimmer

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ yarn-error.*
# local env files # local env files
.env*.local .env*.local
.env
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo

443
API_DOCUMENTATION.md Normal file
View File

@ -0,0 +1,443 @@
# WellNuo API Documentation
## Overview
WellNuo - система мониторинга здоровья пожилых людей. API позволяет получать данные о пациентах, их активности, локации и состоянии здоровья.
**Дата тестирования:** 2025-12-10
**Статус:** Работает
---
## Endpoints
### Base URL
```
https://eluxnetworks.net/function/well-api/api
```
### Web Dashboard
```
https://react.eluxnetworks.net/dashboard
```
---
## Authentication
### Credentials
```
Username: anandk
Password: anandk_8
Client ID: 001
```
### Получение токена
**Request:**
```bash
curl -X POST "https://eluxnetworks.net/function/well-api/api" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "function=credentials&clientId=001&user_name=anandk&ps=anandk_8&nonce=$(uuidgen)"
```
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| function | string | Yes | `credentials` |
| clientId | string | Yes | `001` |
| user_name | string | Yes | Username |
| ps | string | Yes | Password |
| nonce | string | Yes | Unique UUID for each request |
**Response (200 OK):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"privileges": "21,38,29,41,42",
"user_id": 43,
"max_role": 2,
"status": "200 OK"
}
```
**Token Format:** JWT (JSON Web Token)
- Algorithm: HS256
- Expiration: ~7 days (exp claim in payload)
---
## API Endpoints
### 1. Dashboard List (Список пациентов)
**Request:**
```bash
curl -X POST "https://eluxnetworks.net/function/well-api/api" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "function=dashboard_list&user_name=anandk&token=<JWT_TOKEN>&date=2025-12-10&nonce=<UUID>"
```
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| function | string | Yes | `dashboard_list` |
| user_name | string | Yes | Username |
| token | string | Yes | JWT access token |
| date | string | Yes | Date in YYYY-MM-DD format |
| nonce | string | Yes | Unique UUID |
**Response:**
```json
{
"result_list": [
{
"user_id": 25,
"name": "Ferdinand Zmrzli",
"address": "661 Encore Way",
"time_zone": "America/Los_Angeles",
"picture": "/",
"bathroom_at": "2025-12-10T13:52:00",
"kitchen_at": "2025-12-10T14:48:00",
"bedroom_at": "2025-12-10T09:14:00",
"temperature": 72.77,
"smell": "clean",
"bathroom_delayed": [6, 12],
"kitchen_delayed": [6, 12],
"bedroom_delayed": [13, 16],
"last_location": "Kitchen",
"last_detected_time": "2025-12-10T14:47:00",
"before_last_location": "Living Room",
"last_present_duration": 1,
"wellness_score_percent": 90,
"wellness_descriptor": "Great!",
"wellness_descriptor_color": "bg-green-100 text-green-700",
"bedroom_temperature": 15.22,
"sleep_bathroom_visit_count": 0,
"bedroom_co2": 500,
"shower_detected_time": "2025-12-10T13:52:00",
"breakfast_detected_time": 0,
"living_room_time_spent": 0,
"outside_hours": 0,
"most_time_spent_in": "Bedroom",
"sleep_hours": 11.12,
"units": "°F",
"deployment_id": 21,
"location_list": ["Living Room", "Bathroom Small", "Bedroom", "Dining Room", "Bathroom Main", "Kitchen", "Office"]
}
],
"status": "200 OK"
}
```
---
## Available Patients (Тестовые данные)
| User ID | Name | Address | Timezone | Deployment ID |
|---------|------|---------|----------|---------------|
| 25 | Ferdinand Zmrzli | 661 Encore Way | America/Los_Angeles | 21 |
| 34 | Helga Kleine | - | Europe/Berlin | 29 |
| 48 | Đurđica Božičević-Radić | A.T. Mimare 38 | Europe/Zagreb | 38 |
| 50 | Tarik Hubana | - | Europe/Sarajevo | 41 |
| 54 | Jamie Rivera-Vallestero | 2088 Trafalgar Ave | US/Pacific | 42 |
---
## Data Structure
### Patient Object Fields
| Field | Type | Description |
|-------|------|-------------|
| user_id | int | Unique patient ID |
| name | string | Patient full name |
| address | string | Home address |
| time_zone | string | IANA timezone |
| temperature | float | Current room temperature |
| smell | string | Air quality (clean/etc) |
| last_location | string | Current room |
| last_detected_time | datetime | Last activity timestamp |
| wellness_score_percent | int | Health score 0-100 |
| wellness_descriptor | string | Status text |
| sleep_hours | float | Hours slept |
| bathroom_at | datetime | Last bathroom visit |
| kitchen_at | datetime | Last kitchen visit |
| bedroom_at | datetime | Last bedroom visit |
| location_list | array | Available rooms in home |
| units | string | Temperature units (°F/°C) |
| deployment_id | int | Sensor deployment ID |
---
## Integration Notes
### WebView Integration (React Native)
Для встраивания dashboard в React Native приложение:
```javascript
import { WebView } from 'react-native-webview';
// Inject credentials and auto-login
const injectedJS = `
localStorage.setItem('auth2', JSON.stringify({
username: 'anandk',
token: '${token}',
user_id: 43
}));
window.location.reload();
true;
`;
<WebView
source={{ uri: 'https://react.eluxnetworks.net/dashboard' }}
injectedJavaScript={injectedJS}
javaScriptEnabled={true}
domStorageEnabled={true}
/>
```
### Request Headers
All requests use:
```
Content-Type: application/x-www-form-urlencoded
```
### Nonce Generation
Each request requires a unique nonce (UUID):
```javascript
const nonce = crypto.randomUUID();
// or
const nonce = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
```
---
## Error Handling
| Status | Description |
|--------|-------------|
| 200 OK | Success |
| 401 | Invalid token |
| 403 | Access denied |
| 404 | Resource not found |
| 405 | Method not allowed (use POST) |
---
## Quick Test Commands
### 1. Get Token
```bash
curl -s -X POST "https://eluxnetworks.net/function/well-api/api" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "function=credentials&clientId=001&user_name=anandk&ps=anandk_8&nonce=test-123"
```
### 2. Get Dashboard
```bash
TOKEN="<paste_token_here>"
curl -s -X POST "https://eluxnetworks.net/function/well-api/api" \
-d "function=dashboard_list&user_name=anandk&token=$TOKEN&date=$(date +%Y-%m-%d)&nonce=test-456"
```
---
---
## Patient Context API (для AI чата)
### Endpoint
```
https://wellnuo.smartlaunchhub.com/api/patient/context
```
### Request
```bash
curl -s "https://wellnuo.smartlaunchhub.com/api/patient/context"
```
### Response Structure
API возвращает детальные данные о пациенте для использования в AI чате:
```json
{
"patient": {
"id": "dad",
"name": "John Smith",
"age": 78,
"relationship": "Father",
"address": "123 Oak Street, San Francisco, CA",
"timezone": "America/Los_Angeles",
"emergency_contact": { "name": "Sarah Smith", "phone": "+1 (555) 123-4567" }
},
"current_status": {
"location": "Living Room",
"presence_detected": true,
"is_moving": false,
"last_movement": "2025-12-10T22:51:43.316Z",
"estimated_activity": "resting",
"time_in_current_room_minutes": 45
},
"environment": {
"living_room": {
"temperature_c": 22.2, "temperature_f": 72,
"humidity_percent": 45, "co2_ppm": 650,
"voc_index": 85, "light_lux": 320,
"air_quality_status": "good",
"presence": true, "motion_level": "low"
}
},
"sleep_analysis": {
"last_night": {
"bed_time_detected": "2025-11-30T22:18:00Z",
"wake_time_detected": "2025-12-01T05:45:00Z",
"total_hours": 7.45,
"quality_score": 78,
"bathroom_visits": 1
}
},
"activity_patterns": {
"today": {
"total_active_minutes": 185,
"sedentary_minutes": 240,
"rooms_visited": ["Bedroom", "Bathroom", "Kitchen", "Living Room"]
}
},
"recent_events": [
{
"time": "2025-12-01T10:45:00Z",
"type": "presence",
"event": "Continued presence in living room",
"room": "Living Room"
}
],
"alerts": {
"active": [],
"recent": [
{
"id": "alert_001",
"type": "possible_fall",
"severity": "high",
"timestamp": "2025-11-15T14:22:00Z",
"room": "Bathroom",
"resolved": true
}
]
}
}
```
### Доступные данные для чата:
| Раздел | Описание |
|--------|----------|
| patient | Информация о пациенте |
| current_status | Текущее местоположение и активность |
| environment | Данные сенсоров по комнатам |
| environment_history | История показателей за день |
| sleep_analysis | Анализ сна |
| activity_patterns | Паттерны активности |
| recent_events | Последние события (до 90 записей) |
| alerts | Активные и недавние алерты |
---
## Files
- `tests/api-check.spec.js` - Playwright tests for API
- `tests/network-capture.spec.js` - Network capture for debugging
- `tests/api-discovery.spec.js` - API endpoint discovery
- `tests/screenshots/` - Visual verification screenshots
---
---
## React Native WebView Integration
### Installation
```bash
npm install react-native-webview
```
### Usage Example
```tsx
import { DashboardWebView } from './src/components';
function DashboardScreen() {
const handlePatientSelect = (patientId: number, patientName: string) => {
console.log('Selected patient:', patientId, patientName);
// Navigate to patient details or start voice chat
};
return (
<DashboardWebView
onPatientSelect={handlePatientSelect}
onError={(error) => console.error(error)}
/>
);
}
```
### Component: `src/components/DashboardWebView.tsx`
Компонент автоматически:
1. Получает JWT токен через API
2. Автоматически логинится в веб-интерфейс
3. Отображает dashboard внутри приложения
4. Перехватывает клики на пациентах
### Credentials в коде:
```typescript
const CREDENTIALS = {
username: 'anandk',
password: 'anandk_8',
clientId: '001'
};
```
---
---
## Gitea Repository Access
### Credentials
```
URL: https://gitea.wellnua.com
Username: sergei_t
Password: WellNuo2025!Secure
Email: serter2069@gmail.com
```
### Repository
```
https://gitea.wellnua.com/robert/MobileApp_react_native
```
### Clone Command
```bash
git clone https://sergei_t:WellNuo2025%21Secure@gitea.wellnua.com/robert/MobileApp_react_native.git
```
### Local Clone
```
~/Desktop/Wellnuo/wellnuo-mobile-repo/
```
---
## Contact / Support
- Dashboard: https://react.eluxnetworks.net/dashboard
- API Base: https://eluxnetworks.net/function/well-api/api
- Patient Context: https://wellnuo.smartlaunchhub.com/api/patient/context
- Gitea: https://gitea.wellnua.com

428
App.tsx
View File

@ -1,11 +1,295 @@
import { StatusBar } from 'expo-status-bar'; import React, { useState, useEffect, useCallback } from 'react';
import { StyleSheet, Text, View } from 'react-native'; import {
StyleSheet,
View,
Text,
StatusBar,
TouchableOpacity,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { OPENAI_API_KEY, WEBHOOK_URL } from '@env';
import { VoiceButton, StatusIndicator, TranscriptView } from './src/components';
import { useVoiceAssistant, PermissionStatus } from './src/hooks/useVoiceAssistant';
import { fetchWebhookContext, getDefaultContext } from './src/services/webhookService';
import { WebhookContext } from './src/types';
// Wellnuo brand colors
const COLORS = {
primary: '#0074be',
primaryDark: '#005a94',
teal: '#5db1a8',
purple: '#ab5b8d',
white: '#ffffff',
background: '#f4f6f8',
textDark: '#515b69',
textLight: '#7f8795',
error: '#dc3545',
};
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
export default function App() { export default function App() {
const [context, setContext] = useState<WebhookContext>(getDefaultContext());
const [messages, setMessages] = useState<Message[]>([]);
const [currentTranscript, setCurrentTranscript] = useState('');
const [assistantText, setAssistantText] = useState('');
const [isInitialized, setIsInitialized] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [apiKey] = useState(OPENAI_API_KEY || '');
// Fetch context from webhook on mount
useEffect(() => {
async function initialize() {
if (WEBHOOK_URL) {
const webhookContext = await fetchWebhookContext(WEBHOOK_URL);
setContext(webhookContext);
}
setIsInitialized(true);
}
initialize();
}, []);
const handleTranscript = useCallback((text: string, isFinal: boolean) => {
if (isFinal) {
setMessages(prev => [
...prev,
{ role: 'user', content: text, timestamp: new Date() },
]);
setCurrentTranscript('');
} else {
setCurrentTranscript(text);
}
}, []);
const handleAssistantResponse = useCallback((text: string) => {
setAssistantText(prev => prev + text);
}, []);
const {
state,
connectionStatus,
permissionStatus,
isInConversation,
connect,
disconnect,
startContinuousListening,
stopContinuousListening,
interrupt,
openSettings,
} = useVoiceAssistant({
apiKey,
context,
onTranscript: handleTranscript,
onAssistantResponse: handleAssistantResponse,
});
// Save assistant response when done
useEffect(() => {
if (!state.isSpeaking && !state.isProcessing && assistantText) {
setMessages(prev => [
...prev,
{ role: 'assistant', content: assistantText, timestamp: new Date() },
]);
setAssistantText('');
}
}, [state.isSpeaking, state.isProcessing, assistantText]);
// Toggle mute
const handleToggleMute = useCallback(async () => {
if (isMuted) {
// Unmute - resume listening
setIsMuted(false);
if (connectionStatus === 'connected') {
await startContinuousListening();
}
} else {
// Mute - stop listening but keep connection
setIsMuted(true);
await stopContinuousListening();
}
}, [isMuted, connectionStatus, startContinuousListening, stopContinuousListening]);
// Toggle conversation on/off with single tap
const handleToggleConversation = useCallback(async () => {
if (!apiKey) {
Alert.alert(
'API Key Required',
'Please add your OpenAI API key in the app configuration.',
[{ text: 'OK' }]
);
return;
}
// If in conversation, end it
if (isInConversation || connectionStatus === 'connected') {
await stopContinuousListening();
disconnect();
setIsMuted(false);
return;
}
// Start new conversation
await connect();
}, [apiKey, isInConversation, connectionStatus, connect, disconnect, stopContinuousListening]);
// Start continuous listening after connected (if not muted)
useEffect(() => {
if (connectionStatus === 'connected' && !isInConversation && !isMuted) {
const timer = setTimeout(() => {
startContinuousListening();
}, 500);
return () => clearTimeout(timer);
}
}, [connectionStatus, isInConversation, isMuted, startContinuousListening]);
// Interrupt AI when tapping during speech
const handleInterrupt = useCallback(() => {
if (state.isSpeaking) {
interrupt();
}
}, [state.isSpeaking, interrupt]);
// Main button: Start/End conversation
const handleMainButtonPress = useCallback(() => {
handleToggleConversation();
}, [handleToggleConversation]);
// For legacy VoiceButton compatibility
const handlePressIn = useCallback(() => {
// Main button now only starts/stops conversation
handleMainButtonPress();
}, [handleMainButtonPress]);
const handlePressOut = useCallback(() => {
// No longer needed for toggle mode
}, []);
const handleClearHistory = useCallback(() => {
Alert.alert(
'Clear History',
'Are you sure you want to clear the conversation history?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear',
style: 'destructive',
onPress: () => setMessages([]),
},
]
);
}, []);
if (!isInitialized) {
return (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Initializing...</Text>
</View>
);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text> <StatusBar barStyle="dark-content" backgroundColor={COLORS.white} />
<StatusBar style="auto" /> <SafeAreaView style={styles.safeArea}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Wellnuo</Text>
<Text style={styles.subtitle}>Your AI Health Assistant</Text>
{messages.length > 0 && (
<TouchableOpacity
style={styles.clearButton}
onPress={handleClearHistory}
>
<Ionicons name="trash-outline" size={18} color={COLORS.textLight} />
</TouchableOpacity>
)}
</View>
{/* Transcript View */}
<View style={styles.transcriptContainer}>
<TranscriptView
messages={messages}
currentTranscript={currentTranscript || assistantText}
/>
</View>
{/* Voice Controls */}
<View style={styles.controlsContainer}>
{/* Permission Denied Message */}
{permissionStatus === 'denied' && (
<View style={styles.permissionDenied}>
<Ionicons name="mic-off" size={48} color={COLORS.error} />
<Text style={styles.permissionTitle}>Microphone Access Required</Text>
<Text style={styles.permissionText}>
To use voice chat, please enable microphone access in Settings.
</Text>
<TouchableOpacity style={styles.settingsButton} onPress={openSettings}>
<Text style={styles.settingsButtonText}>Open Settings</Text>
</TouchableOpacity>
</View>
)}
{permissionStatus !== 'denied' && (
<>
<StatusIndicator
connectionStatus={connectionStatus}
isListening={state.isListening && !isMuted}
isSpeaking={state.isSpeaking}
isProcessing={state.isProcessing}
/>
<View style={styles.buttonsRow}>
{/* Main Call Button - Start/End Call */}
<VoiceButton
isListening={(state.isListening || isInConversation) && !isMuted}
isSpeaking={state.isSpeaking}
isProcessing={state.isProcessing}
isInCall={isInConversation || connectionStatus === 'connected'}
disabled={!apiKey}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={styles.voiceButton}
/>
</View>
{/* Mute Button - only during call */}
{(isInConversation || connectionStatus === 'connected') && (
<TouchableOpacity
style={[styles.muteButton, isMuted && styles.muteButtonActive]}
onPress={handleToggleMute}
>
<Ionicons
name={isMuted ? 'mic-off' : 'mic'}
size={20}
color={isMuted ? COLORS.white : COLORS.textDark}
/>
<Text style={[styles.muteButtonText, isMuted && styles.muteButtonTextActive]}>
{isMuted ? 'Unmute' : 'Mute'}
</Text>
</TouchableOpacity>
)}
{state.error && (
<Text style={styles.errorText}>{state.error}</Text>
)}
<Text style={styles.hint}>
{connectionStatus !== 'connected'
? 'Tap to call Julia'
: isInConversation
? 'Tap again to end call'
: ''}
</Text>
</>
)}
</View>
</SafeAreaView>
</View> </View>
); );
} }
@ -13,8 +297,142 @@ export default function App() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#fff', backgroundColor: COLORS.background,
},
safeArea: {
flex: 1,
},
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: COLORS.background,
},
loadingText: {
fontSize: 18,
color: COLORS.textDark,
},
header: {
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 16,
backgroundColor: COLORS.white,
alignItems: 'center',
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
},
title: {
fontSize: 28,
fontWeight: '600',
color: COLORS.primary,
letterSpacing: 0.5,
},
subtitle: {
fontSize: 14,
color: COLORS.textLight,
marginTop: 4,
},
clearButton: {
position: 'absolute',
right: 20,
top: 20,
padding: 8,
},
transcriptContainer: {
flex: 1,
backgroundColor: COLORS.white,
marginHorizontal: 16,
marginVertical: 12,
borderRadius: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
controlsContainer: {
paddingVertical: 20,
paddingHorizontal: 20,
backgroundColor: COLORS.white,
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
},
buttonsRow: {
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
voiceButton: {
marginVertical: 8,
},
muteButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: COLORS.background,
borderWidth: 1,
borderColor: '#e5e7eb',
marginTop: 12,
gap: 6,
},
muteButtonActive: {
backgroundColor: COLORS.error,
borderColor: COLORS.error,
},
muteButtonText: {
fontSize: 14,
fontWeight: '500',
color: COLORS.textDark,
},
muteButtonTextActive: {
color: COLORS.white,
},
errorText: {
color: COLORS.error,
fontSize: 14,
textAlign: 'center',
marginTop: 8,
},
hint: {
color: COLORS.textLight,
fontSize: 13,
textAlign: 'center',
marginTop: 12,
},
permissionDenied: {
alignItems: 'center',
paddingVertical: 20,
paddingHorizontal: 16,
},
permissionTitle: {
color: COLORS.textDark,
fontSize: 18,
fontWeight: '600',
marginTop: 16,
marginBottom: 8,
textAlign: 'center',
},
permissionText: {
color: COLORS.textLight,
fontSize: 14,
textAlign: 'center',
marginBottom: 20,
lineHeight: 20,
},
settingsButton: {
backgroundColor: COLORS.primary,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
settingsButtonText: {
color: COLORS.white,
fontSize: 16,
fontWeight: '600',
},
}); });

View File

@ -10,21 +10,46 @@
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#4A90A4"
}, },
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
"bundleIdentifier": "com.kosyakorel1.wellnuo",
"infoPlist": {
"NSMicrophoneUsageDescription": "Wellnuo needs microphone access to enable voice conversations with your AI health assistant.",
"UIBackgroundModes": [
"audio"
],
"ITSAppUsesNonExemptEncryption": false
}
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png", "foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff" "backgroundColor": "#4A90A4"
}, },
"edgeToEdgeEnabled": true, "permissions": [
"predictiveBackGestureEnabled": false "android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS"
],
"package": "com.wellnuo.app"
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
} },
"plugins": [
[
"expo-av",
{
"microphonePermission": "Wellnuo needs microphone access to enable voice conversations with your AI health assistant."
}
]
],
"extra": {
"eas": {
"projectId": "c9e68e27-5713-4b6d-a313-cb8e7a8866ee"
}
},
"owner": "kosyakorel1"
} }
} }

14
babel.config.js Normal file
View File

@ -0,0 +1,14 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
['module:react-native-dotenv', {
moduleName: '@env',
path: '.env',
safe: false,
allowUndefined: true,
}],
],
};
};

22
backend/deploy.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Deploy WellNuo backend to production
SERVER="root@91.98.205.156"
REMOTE_PATH="/var/www/wellnuo"
LOCAL_PATH="$(dirname "$0")"
echo "🚀 Deploying WellNuo backend..."
# Sync files
rsync -avz --delete \
--exclude 'node_modules' \
--exclude '.DS_Store' \
--exclude 'deploy.sh' \
-e "ssh" \
"$LOCAL_PATH/" "$SERVER:$REMOTE_PATH/"
echo "📦 Restarting PM2..."
ssh $SERVER "pm2 restart wellnuo"
echo "✅ Deploy complete!"
echo "🌐 https://wellnuo.smartlaunchhub.com"

767
backend/server.js Normal file
View File

@ -0,0 +1,767 @@
const http = require("http");
// WellNuo Mock API - Based on REAL Wellplug sensor capabilities
// Sensors: Radar motion/presence, CO2, Smell (CO, VOC, VSC), Humidity, Light, Temperature, Pressure
const patientData = {
patient: {
id: "dad",
name: "John Smith",
age: 78,
relationship: "Father",
photo: "https://randomuser.me/api/portraits/men/75.jpg",
address: "123 Oak Street, San Francisco, CA",
timezone: "America/Los_Angeles",
emergency_contact: {
name: "Sarah Smith",
phone: "+1 (555) 123-4567",
relationship: "Daughter"
},
secondary_contact: {
name: "Michael Smith",
phone: "+1 (555) 987-6543",
relationship: "Son"
}
},
// Current real-time status from sensors
current_status: {
location: "Living Room",
presence_detected: true,
is_moving: false,
last_movement: "2025-12-01T10:28:00Z",
estimated_activity: "resting", // inferred from motion patterns
time_in_current_room_minutes: 45
},
// Environment data from Wellplug sensors
environment: {
living_room: {
temperature_c: 22.2,
temperature_f: 72,
humidity_percent: 45,
co2_ppm: 650,
voc_index: 85, // Volatile Organic Compounds (0-500 scale)
co_detected: false,
pressure_hpa: 1013,
light_lux: 320,
air_quality_status: "good",
presence: true,
motion_level: "low"
},
bedroom: {
temperature_c: 20.5,
temperature_f: 69,
humidity_percent: 48,
co2_ppm: 520,
voc_index: 45,
co_detected: false,
pressure_hpa: 1013,
light_lux: 15,
air_quality_status: "excellent",
presence: false,
motion_level: "none"
},
kitchen: {
temperature_c: 23.8,
temperature_f: 75,
humidity_percent: 52,
co2_ppm: 580,
voc_index: 120,
co_detected: false,
pressure_hpa: 1013,
light_lux: 450,
air_quality_status: "good",
presence: false,
motion_level: "none"
},
bathroom: {
temperature_c: 21.0,
temperature_f: 70,
humidity_percent: 58,
co2_ppm: 480,
voc_index: 65,
co_detected: false,
pressure_hpa: 1013,
light_lux: 0,
air_quality_status: "good",
presence: false,
motion_level: "none"
}
},
// Environment history (hourly readings)
environment_history: {
today: [
{ time: "00:00", room: "bedroom", temp_f: 68, humidity: 50, co2: 480, presence: true, motion: "minimal" },
{ time: "01:00", room: "bedroom", temp_f: 68, humidity: 51, co2: 520, presence: true, motion: "none" },
{ time: "02:00", room: "bedroom", temp_f: 67, humidity: 52, co2: 540, presence: true, motion: "none" },
{ time: "03:00", room: "bedroom", temp_f: 67, humidity: 52, co2: 560, presence: true, motion: "minimal" },
{ time: "03:15", room: "bathroom", temp_f: 68, humidity: 55, co2: 450, presence: true, motion: "detected" },
{ time: "03:20", room: "bedroom", temp_f: 67, humidity: 52, co2: 550, presence: true, motion: "minimal" },
{ time: "04:00", room: "bedroom", temp_f: 66, humidity: 53, co2: 580, presence: true, motion: "none" },
{ time: "05:00", room: "bedroom", temp_f: 66, humidity: 52, co2: 590, presence: true, motion: "none" },
{ time: "05:45", room: "bedroom", temp_f: 67, humidity: 51, co2: 600, presence: true, motion: "active" },
{ time: "06:00", room: "bathroom", temp_f: 68, humidity: 65, co2: 480, presence: true, motion: "active" },
{ time: "06:15", room: "bedroom", temp_f: 68, humidity: 50, co2: 550, presence: true, motion: "active" },
{ time: "07:00", room: "bathroom", temp_f: 70, humidity: 75, co2: 520, presence: true, motion: "active" },
{ time: "07:30", room: "kitchen", temp_f: 70, humidity: 48, co2: 580, presence: true, motion: "active" },
{ time: "08:00", room: "kitchen", temp_f: 72, humidity: 50, co2: 650, voc: 180, presence: true, motion: "active", note: "cooking detected" },
{ time: "08:30", room: "kitchen", temp_f: 74, humidity: 52, co2: 720, voc: 220, presence: true, motion: "moderate" },
{ time: "09:00", room: "kitchen", temp_f: 73, humidity: 50, co2: 680, presence: true, motion: "low" },
{ time: "09:30", room: "living_room", temp_f: 71, humidity: 46, co2: 620, presence: true, motion: "low" },
{ time: "10:00", room: "living_room", temp_f: 72, humidity: 45, co2: 640, presence: true, motion: "minimal" },
{ time: "10:30", room: "living_room", temp_f: 72, humidity: 45, co2: 650, presence: true, motion: "low" }
]
},
// Sleep analysis (derived from motion/presence sensors)
sleep_analysis: {
last_night: {
bed_time_detected: "2025-11-30T22:18:00Z",
wake_time_detected: "2025-12-01T05:45:00Z",
total_hours: 7.45,
quality_score: 78, // based on movement patterns
night_movements: 3,
bathroom_visits: 1,
bathroom_visit_times: ["03:15"],
restlessness_periods: [
{ start: "01:30", end: "01:45", duration_minutes: 15 },
{ start: "04:20", end: "04:35", duration_minutes: 15 }
],
time_to_fall_asleep_minutes: 18,
bedroom_co2_peak: 600,
bedroom_temp_avg_f: 67
},
weekly_average: {
avg_sleep_hours: 7.2,
avg_bathroom_visits: 1.3,
avg_quality_score: 75,
trend: "stable"
},
patterns: {
typical_bedtime: "22:00-22:30",
typical_waketime: "05:30-06:00",
weekend_variation: "+45 minutes"
}
},
// Activity patterns (derived from motion sensors across rooms)
activity_patterns: {
today: {
total_active_minutes: 185,
sedentary_minutes: 240,
rooms_visited: ["Bedroom", "Bathroom", "Kitchen", "Living Room"],
room_time_distribution: {
bedroom: { minutes: 420, percent: 58 },
living_room: { minutes: 180, percent: 25 },
kitchen: { minutes: 85, percent: 12 },
bathroom: { minutes: 35, percent: 5 }
},
peak_activity_periods: ["07:00-08:30", "09:30-10:00"],
longest_sedentary_period_minutes: 95
},
weekly_comparison: {
avg_active_minutes: 195,
trend: "stable",
most_active_day: "Saturday",
least_active_day: "Sunday"
}
},
// Real-time events timeline (what sensors actually detect)
recent_events: [
// December 1st - Today (detailed sensor-based events)
{ time: "2025-12-01T10:45:00Z", type: "presence", event: "Continued presence in living room", room: "Living Room", motion_level: "low" },
{ time: "2025-12-01T10:30:00Z", type: "activity", event: "Settled in living room - minimal movement detected", room: "Living Room", duration_minutes: 45 },
{ time: "2025-12-01T10:15:00Z", type: "environment", event: "Kitchen VOC levels returning to normal after cooking", room: "Kitchen", voc_index: 95 },
{ time: "2025-12-01T10:00:00Z", type: "movement", event: "Moved from kitchen to living room", from_room: "Kitchen", to_room: "Living Room" },
{ time: "2025-12-01T09:45:00Z", type: "activity", event: "Active movement in kitchen - likely cleaning up", room: "Kitchen", motion_level: "moderate", duration_minutes: 15 },
{ time: "2025-12-01T09:30:00Z", type: "environment", event: "Kitchen temperature stabilizing after cooking", room: "Kitchen", temp_f: 73, co2: 680 },
{ time: "2025-12-01T09:00:00Z", type: "activity", event: "Reduced movement in kitchen - likely eating", room: "Kitchen", motion_level: "low", duration_minutes: 30 },
{ time: "2025-12-01T08:30:00Z", type: "environment", event: "Elevated VOC and temperature - cooking activity detected", room: "Kitchen", temp_f: 74, voc_index: 220, co2: 720 },
{ time: "2025-12-01T08:00:00Z", type: "activity", event: "Active movement in kitchen - cooking detected", room: "Kitchen", motion_level: "active", duration_minutes: 30 },
{ time: "2025-12-01T07:45:00Z", type: "movement", event: "Moved from bathroom to kitchen", from_room: "Bathroom", to_room: "Kitchen" },
{ time: "2025-12-01T07:30:00Z", type: "environment", event: "Bathroom humidity spike - shower detected", room: "Bathroom", humidity: 78 },
{ time: "2025-12-01T07:00:00Z", type: "activity", event: "Extended presence in bathroom - morning routine", room: "Bathroom", duration_minutes: 30 },
{ time: "2025-12-01T06:45:00Z", type: "movement", event: "Moved from bedroom to bathroom", from_room: "Bedroom", to_room: "Bathroom" },
{ time: "2025-12-01T06:15:00Z", type: "activity", event: "Active movement in bedroom - getting ready", room: "Bedroom", motion_level: "active", duration_minutes: 30 },
{ time: "2025-12-01T06:00:00Z", type: "environment", event: "Bedroom light level increased - blinds opened", room: "Bedroom", light_lux: 180 },
{ time: "2025-12-01T05:45:00Z", type: "wake", event: "Wake-up detected - active movement began", room: "Bedroom", confidence: "high" },
{ time: "2025-12-01T05:30:00Z", type: "sleep", event: "Pre-wake restlessness detected", room: "Bedroom", motion_level: "minimal" },
{ time: "2025-12-01T03:20:00Z", type: "movement", event: "Returned to bedroom from bathroom", from_room: "Bathroom", to_room: "Bedroom" },
{ time: "2025-12-01T03:15:00Z", type: "bathroom_visit", event: "Nighttime bathroom visit detected", room: "Bathroom", duration_minutes: 5 },
{ time: "2025-12-01T03:12:00Z", type: "movement", event: "Nighttime movement - went to bathroom", from_room: "Bedroom", to_room: "Bathroom" },
{ time: "2025-12-01T01:30:00Z", type: "sleep", event: "Restlessness detected during sleep", room: "Bedroom", duration_minutes: 15 },
// November 30th - Yesterday
{ time: "2025-11-30T22:18:00Z", type: "sleep", event: "Sleep onset detected - minimal movement began", room: "Bedroom", confidence: "high" },
{ time: "2025-11-30T22:00:00Z", type: "environment", event: "Bedroom lights turned off", room: "Bedroom", light_lux: 0 },
{ time: "2025-11-30T21:45:00Z", type: "movement", event: "Moved from living room to bedroom", from_room: "Living Room", to_room: "Bedroom" },
{ time: "2025-11-30T21:30:00Z", type: "activity", event: "Low activity in living room - likely watching TV", room: "Living Room", motion_level: "minimal", duration_minutes: 90 },
{ time: "2025-11-30T20:00:00Z", type: "movement", event: "Returned to living room from kitchen", from_room: "Kitchen", to_room: "Living Room" },
{ time: "2025-11-30T19:30:00Z", type: "activity", event: "Active in kitchen - dinner preparation and eating", room: "Kitchen", duration_minutes: 45 },
{ time: "2025-11-30T19:00:00Z", type: "environment", event: "Kitchen VOC elevated - cooking detected", room: "Kitchen", voc_index: 195, temp_f: 76 },
{ time: "2025-11-30T18:30:00Z", type: "movement", event: "Moved to kitchen from living room", from_room: "Living Room", to_room: "Kitchen" },
{ time: "2025-11-30T17:00:00Z", type: "activity", event: "Extended sedentary period in living room", room: "Living Room", duration_minutes: 120, motion_level: "minimal" },
{ time: "2025-11-30T16:30:00Z", type: "activity", event: "Afternoon rest detected - very low movement", room: "Bedroom", duration_minutes: 45, motion_level: "none" },
{ time: "2025-11-30T16:00:00Z", type: "movement", event: "Moved to bedroom", from_room: "Living Room", to_room: "Bedroom" },
{ time: "2025-11-30T15:00:00Z", type: "activity", event: "Low activity in living room", room: "Living Room", duration_minutes: 60, motion_level: "low" },
{ time: "2025-11-30T14:30:00Z", type: "bathroom_visit", event: "Bathroom visit detected", room: "Bathroom", duration_minutes: 8 },
{ time: "2025-11-30T13:00:00Z", type: "activity", event: "Moderate activity in kitchen - lunch", room: "Kitchen", duration_minutes: 35 },
{ time: "2025-11-30T12:00:00Z", type: "activity", event: "Low movement in living room", room: "Living Room", duration_minutes: 60 },
{ time: "2025-11-30T10:30:00Z", type: "movement", event: "Moving between rooms - general activity", rooms: ["Kitchen", "Living Room", "Bathroom"] },
{ time: "2025-11-30T09:00:00Z", type: "activity", event: "Morning routine completed - active movement", room: "Kitchen", duration_minutes: 45 },
{ time: "2025-11-30T07:00:00Z", type: "environment", event: "Bathroom humidity spike - shower", room: "Bathroom", humidity: 82 },
{ time: "2025-11-30T06:00:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom", confidence: "high" },
// November 29th
{ time: "2025-11-29T22:30:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-29T19:00:00Z", type: "activity", event: "Extended presence with occasional movement - possible visitor", room: "Living Room", duration_minutes: 90, note: "unusual pattern - social activity suspected" },
{ time: "2025-11-29T15:00:00Z", type: "activity", event: "Moderate activity period", room: "Living Room", duration_minutes: 45 },
{ time: "2025-11-29T12:00:00Z", type: "activity", event: "Active in kitchen - meal preparation", room: "Kitchen", duration_minutes: 40 },
{ time: "2025-11-29T06:15:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
{ time: "2025-11-29T02:45:00Z", type: "bathroom_visit", event: "Nighttime bathroom visit", room: "Bathroom", duration_minutes: 6 },
// November 28th - Thanksgiving (unusual patterns)
{ time: "2025-11-28T23:00:00Z", type: "sleep", event: "Late sleep onset - unusual", room: "Bedroom", note: "2 hours later than typical" },
{ time: "2025-11-28T18:00:00Z", type: "activity", event: "High activity in kitchen - extended cooking", room: "Kitchen", duration_minutes: 180, motion_level: "very_active" },
{ time: "2025-11-28T17:00:00Z", type: "environment", event: "Kitchen temperature elevated - 82°F", room: "Kitchen", temp_f: 82, alert_generated: true },
{ time: "2025-11-28T16:30:00Z", type: "environment", event: "High VOC levels in kitchen", room: "Kitchen", voc_index: 280, note: "extended cooking" },
{ time: "2025-11-28T14:00:00Z", type: "activity", event: "Multiple people presence pattern detected", room: "Living Room", note: "unusual movement patterns - visitors likely" },
{ time: "2025-11-28T09:00:00Z", type: "activity", event: "Earlier than usual kitchen activity", room: "Kitchen", note: "holiday preparation" },
// November 27th
{ time: "2025-11-27T22:15:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-27T16:00:00Z", type: "activity", event: "Extended sedentary period", room: "Living Room", duration_minutes: 150 },
{ time: "2025-11-27T10:00:00Z", type: "environment", event: "Low CO2 - windows likely opened", room: "Living Room", co2: 380 },
{ time: "2025-11-27T06:30:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
// November 26th
{ time: "2025-11-26T22:00:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-26T18:00:00Z", type: "activity", event: "Extended low movement period - likely watching TV", room: "Living Room", duration_minutes: 180 },
{ time: "2025-11-26T14:00:00Z", type: "activity", event: "Unusual activity pattern - movement to garage area", note: "outdoor/garage activity suspected" },
{ time: "2025-11-26T06:00:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
// November 25th
{ time: "2025-11-25T22:30:00Z", type: "sleep", event: "Late sleep onset", room: "Bedroom" },
{ time: "2025-11-25T10:00:00Z", type: "environment", event: "Low humidity alert - 25%", room: "Living Room", humidity: 25, alert_generated: true },
{ time: "2025-11-25T05:30:00Z", type: "wake", event: "Early wake-up detected", room: "Bedroom", note: "1 hour earlier than typical" },
{ time: "2025-11-25T04:00:00Z", type: "bathroom_visit", event: "Nighttime bathroom visit", room: "Bathroom" },
{ time: "2025-11-25T02:30:00Z", type: "bathroom_visit", event: "Nighttime bathroom visit", room: "Bathroom", note: "second visit - unusual" },
// November 24th
{ time: "2025-11-24T22:00:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-24T15:00:00Z", type: "activity", event: "No presence detected for 2 hours", note: "likely out of home" },
{ time: "2025-11-24T11:00:00Z", type: "environment", event: "Extended high VOC in kitchen", room: "Kitchen", voc_index: 245, duration_minutes: 90, note: "baking activity" },
{ time: "2025-11-24T06:00:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
// November 23rd
{ time: "2025-11-23T22:15:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-23T03:30:00Z", type: "bathroom_visit", event: "Nighttime bathroom visit", room: "Bathroom" },
// November 22nd
{ time: "2025-11-22T23:50:00Z", type: "environment", event: "Back door sensor triggered late", note: "door activity detected at unusual hour" },
{ time: "2025-11-22T22:00:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
// November 21st
{ time: "2025-11-21T12:00:00Z", type: "environment", event: "Poor air quality detected - AQI 95", room: "Kitchen", voc_index: 340, co2: 920, alert_generated: true, note: "burnt food detected" },
{ time: "2025-11-21T06:15:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
// November 20th
{ time: "2025-11-20T22:00:00Z", type: "sleep", event: "Sleep onset detected", room: "Bedroom" },
{ time: "2025-11-20T15:00:00Z", type: "activity", event: "No movement for 3 hours - extended rest", room: "Bedroom", alert_generated: true },
{ time: "2025-11-20T06:00:00Z", type: "wake", event: "Wake-up detected", room: "Bedroom" },
// November 15th - fall incident
{ time: "2025-11-15T14:35:00Z", type: "movement", event: "Normal movement resumed", room: "Bathroom" },
{ time: "2025-11-15T14:22:00Z", type: "fall_detected", event: "Sudden movement pattern change - possible fall", room: "Bathroom", alert_generated: true, severity: "high", confidence: "medium" },
{ time: "2025-11-15T14:20:00Z", type: "presence", event: "Presence in bathroom", room: "Bathroom" },
// November 10th
{ time: "2025-11-10T22:40:00Z", type: "movement", event: "Returned inside", room: "Living Room" },
{ time: "2025-11-10T22:30:00Z", type: "environment", event: "Unusual - presence lost from all indoor sensors", alert_generated: true, note: "left home perimeter at night" }
],
// Alerts generated by the system
alerts: {
active: [],
recent: [
{
id: "alert_001",
type: "possible_fall",
severity: "high",
timestamp: "2025-11-15T14:22:00Z",
room: "Bathroom",
description: "Unusual motion pattern detected - sudden downward movement followed by minimal activity",
sensor_data: { motion_change: "rapid_decrease", duration_still: "8 minutes" },
resolved: true,
resolved_at: "2025-11-15T14:35:00Z",
resolution_note: "Movement resumed normally, family confirmed via phone call - minor stumble, no injury"
},
{
id: "alert_002",
type: "extended_inactivity",
severity: "medium",
timestamp: "2025-11-20T15:00:00Z",
room: "Bedroom",
description: "No movement detected for 3 hours during daytime - unusual pattern",
sensor_data: { inactive_duration_minutes: 180, typical_max: 90 },
resolved: true,
resolved_at: "2025-11-20T16:30:00Z",
resolution_note: "Extended afternoon nap - had poor sleep previous night"
},
{
id: "alert_003",
type: "air_quality",
severity: "medium",
timestamp: "2025-11-21T12:00:00Z",
room: "Kitchen",
description: "Poor air quality detected - high VOC and CO2 levels",
sensor_data: { voc_index: 340, co2_ppm: 920, normal_voc: 100, normal_co2: 600 },
resolved: true,
resolved_at: "2025-11-21T13:30:00Z",
resolution_note: "Burnt toast - window opened, levels returned to normal"
},
{
id: "alert_004",
type: "temperature_anomaly",
severity: "medium",
timestamp: "2025-11-28T17:00:00Z",
room: "Kitchen",
description: "Room temperature elevated to 82°F - potential stove/oven left on",
sensor_data: { temperature_f: 82, typical_max: 76, voc_elevated: true },
resolved: true,
resolved_at: "2025-11-28T17:30:00Z",
resolution_note: "Thanksgiving cooking - oven in use for extended period, normal activity"
},
{
id: "alert_005",
type: "low_humidity",
severity: "low",
timestamp: "2025-11-25T10:00:00Z",
room: "Living Room",
description: "Indoor humidity dropped to 25% - very dry conditions",
sensor_data: { humidity_percent: 25, recommended_min: 35 },
resolved: true,
resolved_at: "2025-11-25T14:00:00Z",
resolution_note: "Dry winter day with heating - humidifier recommended"
},
{
id: "alert_006",
type: "night_wandering",
severity: "high",
timestamp: "2025-11-10T22:30:00Z",
description: "Presence lost from all indoor sensors at unusual hour",
sensor_data: { last_presence_room: "Living Room", duration_absent_minutes: 10 },
resolved: true,
resolved_at: "2025-11-10T22:40:00Z",
resolution_note: "Went outside briefly - thought he heard something. Returned safely."
},
{
id: "alert_007",
type: "frequent_bathroom_visits",
severity: "low",
timestamp: "2025-11-25T04:00:00Z",
description: "Multiple nighttime bathroom visits detected (2 in one night)",
sensor_data: { visits: 2, typical_max: 1 },
resolved: true,
resolution_note: "Drank extra fluids before bed. Monitoring for pattern."
},
{
id: "alert_008",
type: "unusual_schedule",
severity: "low",
timestamp: "2025-11-28T23:00:00Z",
room: "Bedroom",
description: "Sleep onset 2 hours later than typical",
sensor_data: { actual_time: "23:00", typical_time: "22:00" },
resolved: true,
resolution_note: "Thanksgiving - family gathering, expected deviation"
},
{
id: "alert_009",
type: "elevated_co2",
severity: "low",
timestamp: "2025-11-30T02:00:00Z",
room: "Bedroom",
description: "Bedroom CO2 levels elevated during sleep - 720ppm",
sensor_data: { co2_ppm: 720, recommended_max: 600 },
resolved: true,
resolution_note: "Suggest opening window slightly before bed for better ventilation"
}
],
statistics: {
total_this_month: 9,
by_severity: { high: 2, medium: 3, low: 4 },
by_type: {
possible_fall: 1,
extended_inactivity: 1,
air_quality: 1,
temperature_anomaly: 1,
night_wandering: 1,
frequent_bathroom_visits: 1,
unusual_schedule: 1,
low_humidity: 1,
elevated_co2: 1
},
average_resolution_time_minutes: 45
}
},
// Daily patterns derived from sensor data
daily_patterns: {
typical_schedule: {
wake_time: { average: "06:00", range: "05:30-06:30" },
morning_routine: { duration_minutes: 90, rooms: ["Bedroom", "Bathroom", "Kitchen"] },
breakfast_time: { average: "08:00", duration_minutes: 45 },
active_periods: ["07:00-09:00", "11:00-12:00", "17:00-18:00"],
sedentary_periods: ["10:00-11:00", "14:00-16:00", "20:00-22:00"],
afternoon_rest: { typical_time: "15:00-16:30", frequency: "daily" },
dinner_time: { average: "19:00", duration_minutes: 45 },
evening_routine: { start: "21:00", rooms: ["Living Room", "Bathroom", "Bedroom"] },
bed_time: { average: "22:15", range: "22:00-22:30" }
},
bathroom_patterns: {
typical_daily_visits: 6,
typical_night_visits: 1,
average_duration_minutes: 8,
shower_detected_days: ["daily"],
typical_shower_time: "07:00"
},
cooking_patterns: {
breakfast: { detected: true, typical_time: "08:00", voc_spike: "moderate" },
lunch: { detected: true, typical_time: "13:00", voc_spike: "low" },
dinner: { detected: true, typical_time: "19:00", voc_spike: "moderate" }
}
},
// Monthly summaries
monthly_summaries: [
{
month: "November 2025",
sleep: {
avg_hours: 7.2,
avg_quality_score: 76,
avg_bathroom_visits_per_night: 1.2,
nights_with_poor_sleep: 3
},
activity: {
avg_active_minutes: 190,
avg_sedentary_minutes: 480,
most_active_room: "Kitchen",
days_with_low_activity: 4
},
environment: {
avg_indoor_temp_f: 71,
avg_humidity: 44,
avg_co2: 580,
air_quality_alerts: 2
},
alerts_count: 9,
patterns: {
schedule_consistency: "good",
notable_deviations: ["Thanksgiving day - late sleep, high kitchen activity", "Nov 25 - early wake, multiple bathroom visits"]
},
highlights: [
"Consistent morning routine maintained",
"One possible fall incident - resolved with no injury",
"Thanksgiving showed expected schedule changes",
"Sleep quality generally good with occasional restless nights"
],
concerns: [
"Bathroom fall risk - grab bars recommended",
"Occasional high CO2 in bedroom at night",
"One day with extended inactivity"
]
},
{
month: "October 2025",
sleep: {
avg_hours: 7.0,
avg_quality_score: 78,
avg_bathroom_visits_per_night: 1.0,
nights_with_poor_sleep: 2
},
activity: {
avg_active_minutes: 205,
avg_sedentary_minutes: 460,
most_active_room: "Kitchen",
days_with_low_activity: 2
},
environment: {
avg_indoor_temp_f: 70,
avg_humidity: 48,
avg_co2: 560,
air_quality_alerts: 0
},
alerts_count: 3,
patterns: {
schedule_consistency: "excellent",
notable_deviations: ["Oct 15-17 - visitors present, different patterns"]
},
highlights: [
"Very consistent daily patterns",
"Good activity levels",
"No significant environmental issues"
],
concerns: []
}
],
// Wellplug device info
devices: {
wellplugs: [
{ id: "wp_001", location: "Living Room", status: "active", last_reading: "2025-12-01T10:45:00Z", firmware: "2.1.4" },
{ id: "wp_002", location: "Bedroom", status: "active", last_reading: "2025-12-01T10:45:00Z", firmware: "2.1.4" },
{ id: "wp_003", location: "Kitchen", status: "active", last_reading: "2025-12-01T10:45:00Z", firmware: "2.1.4" },
{ id: "wp_004", location: "Bathroom", status: "active", last_reading: "2025-12-01T10:45:00Z", firmware: "2.1.4" }
],
sensors_per_device: ["radar_motion", "radar_presence", "co2", "voc", "co", "humidity", "temperature", "pressure", "light"],
connectivity: "wifi",
data_sync_interval_seconds: 60
},
// System prompt for Julia AI
system_prompt: `You are Julia, a warm and caring AI assistant for the WellNuo family care system. You help family members stay connected with and understand the wellbeing of their elderly loved ones through ambient sensor data.
IMPORTANT - Data Sources:
WellNuo uses ambient sensors (Wellplug devices) that monitor: motion/presence, CO2, air quality (VOC, CO), humidity, temperature, pressure, and light. There are NO wearables, cameras, or microphones.
What you CAN discuss (sensor-detected):
- Movement patterns and room locations
- Sleep timing and quality (from motion patterns)
- Bathroom visit frequency and timing
- Cooking activity (from VOC/temperature)
- Air quality, temperature, humidity
- Daily routine consistency
- Unusual patterns or deviations
What you CANNOT know (no sensors for this):
- Specific activities (reading, watching TV, eating specific foods)
- Health vitals (heart rate, blood pressure, blood oxygen)
- Medication intake
- Phone calls or social interactions (unless visitor presence inferred)
- Emotional state or mood
- What they ate or drank
Guidelines:
- Be warm, conversational, and reassuring
- Describe what sensors detected, not assumptions about activities
- Say "movement patterns suggest..." not "he was watching TV"
- Lead with positive patterns before concerns
- Provide context for alerts (most are resolved, minor issues)
- If asked about something sensors can't detect, explain kindly: "Our sensors monitor movement and environment, so I can't tell you exactly what he's doing, but I can see he's been active in the living room for the past hour"
- Keep responses concise but caring`,
meta: {
api_version: "2.0.0",
data_source: "Wellplug ambient sensors",
sensors: ["radar_motion", "radar_presence", "co2", "voc", "co", "humidity", "temperature", "pressure", "light"],
no_wearables: true,
no_cameras: true,
no_microphones: true,
total_events: 68,
total_alerts: 9,
data_range: "2025-10-01 to 2025-12-01",
last_updated: new Date().toISOString()
}
};
// Landing page
const landingPage = `<!DOCTYPE html>
<html>
<head>
<title>WellNuo Mock API</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 900px; margin: 50px auto; padding: 20px; background: #f5f5f5; color: #333; }
h1 { color: #667eea; }
h2 { color: #444; margin-top: 30px; }
.endpoint { background: white; padding: 20px; border-radius: 10px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 15px; margin: 20px 0; }
.stat { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px; border-radius: 8px; text-align: center; }
.stat-value { font-size: 1.8em; font-weight: bold; }
.stat-label { font-size: 0.85em; opacity: 0.9; }
.sensor-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 15px 0; }
.sensor { background: #e8e8e8; padding: 5px 12px; border-radius: 15px; font-size: 13px; }
code { background: #e8e8e8; padding: 3px 8px; border-radius: 4px; font-size: 14px; }
.method { color: #22c55e; font-weight: bold; }
a { color: #667eea; }
pre { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 8px; overflow-x: auto; }
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; }
</style>
</head>
<body>
<h1>WellNuo Mock API v2.0</h1>
<p>Realistic demo API based on <strong>actual Wellplug sensor capabilities</strong>.</p>
<div class="warning">
<strong> Sensor-Based Data Only</strong><br>
This API reflects what Wellplug sensors can actually detect. No wearables, cameras, or microphones.
</div>
<h2>Wellplug Sensors</h2>
<div class="sensor-list">
<span class="sensor">🎯 Radar Motion</span>
<span class="sensor">👤 Presence Detection</span>
<span class="sensor">💨 CO2</span>
<span class="sensor">🌡 Temperature</span>
<span class="sensor">💧 Humidity</span>
<span class="sensor">🔬 VOC (Smells)</span>
<span class="sensor"> CO Detection</span>
<span class="sensor">📊 Pressure</span>
<span class="sensor">💡 Light Level</span>
</div>
<div class="stats">
<div class="stat"><div class="stat-value">68</div><div class="stat-label">Events</div></div>
<div class="stat"><div class="stat-value">9</div><div class="stat-label">Alerts</div></div>
<div class="stat"><div class="stat-value">4</div><div class="stat-label">Rooms</div></div>
<div class="stat"><div class="stat-value">2</div><div class="stat-label">Months</div></div>
</div>
<div class="endpoint">
<h3><span class="method">GET</span> /api/patient/context</h3>
<p>Returns sensor-based patient context for Julia AI.</p>
<p><a href="/api/patient/context">Try it </a></p>
</div>
<h2>What's Detected</h2>
<div class="endpoint">
<ul>
<li><strong>Movement & Presence</strong> - Room-by-room tracking, activity levels</li>
<li><strong>Sleep Patterns</strong> - Bed/wake times, night movements, bathroom visits</li>
<li><strong>Daily Routine</strong> - Schedule consistency, deviations</li>
<li><strong>Cooking Activity</strong> - VOC/temperature spikes in kitchen</li>
<li><strong>Air Quality</strong> - CO2, VOC, humidity alerts</li>
<li><strong>Environmental</strong> - Temperature, humidity, pressure per room</li>
<li><strong>Anomalies</strong> - Falls, inactivity, night wandering</li>
</ul>
</div>
<h2>What's NOT Detected</h2>
<div class="endpoint">
<ul style="color: #999;">
<li> Heart rate, blood pressure, vitals (no wearables)</li>
<li> Medication intake</li>
<li> Specific activities (reading, TV, phone calls)</li>
<li> What they ate or drank</li>
<li> Conversations or social interactions</li>
</ul>
</div>
<div class="endpoint">
<h3>Example</h3>
<pre>curl https://wellnuo.smartlaunchhub.com/api/patient/context</pre>
</div>
</body>
</html>`;
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 5011;
const CONVERSATIONS_FILE = path.join(__dirname, "conversations.json");
// Загрузить существующие разговоры
function loadConversations() {
try {
if (fs.existsSync(CONVERSATIONS_FILE)) {
return JSON.parse(fs.readFileSync(CONVERSATIONS_FILE, "utf8"));
}
} catch (error) {
console.error("Error loading conversations:", error);
}
return [];
}
// Сохранить разговоры
function saveConversations(conversations) {
try {
fs.writeFileSync(CONVERSATIONS_FILE, JSON.stringify(conversations, null, 2));
} catch (error) {
console.error("Error saving conversations:", error);
}
}
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
const url = req.url;
if (url === "/" || url === "") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(landingPage);
return;
}
if (url === "/api/patient/context" || url.match(/^\/api\/patient\/[^\/]+\/context$/)) {
const response = JSON.parse(JSON.stringify(patientData));
const now = new Date();
response.current_status.last_movement = new Date(now - 5 * 60000).toISOString();
response.meta.last_updated = now.toISOString();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(response, null, 2));
return;
}
// POST /api/conversations - сохранение истории разговора по схеме
if (url === "/api/conversations" && req.method === "POST") {
let body = "";
req.on("data", chunk => { body += chunk.toString(); });
req.on("end", () => {
try {
const conversation = JSON.parse(body);
// Валидация
if (!conversation.session_id || !conversation.messages) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing required fields: session_id, messages" }));
return;
}
// Добавить timestamp если нет
conversation.saved_at = new Date().toISOString();
// Загрузить, добавить, сохранить
const conversations = loadConversations();
conversations.push(conversation);
saveConversations(conversations);
console.log(`Conversation saved: ${conversation.session_id}, ${conversation.messages.length} messages`);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
success: true,
session_id: conversation.session_id,
message: "Conversation saved successfully"
}));
} catch (error) {
console.error("Error saving conversation:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Failed to save conversation" }));
}
});
return;
}
// GET /api/conversations - получить все разговоры (для отладки)
if (url === "/api/conversations" && req.method === "GET") {
const conversations = loadConversations();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
count: conversations.length,
conversations: conversations.slice(-10) // последние 10
}));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found", available_endpoints: ["/api/patient/context", "/api/conversations"] }));
});
server.listen(PORT, () => console.log(`WellNuo Mock API v2.0 running on port ${PORT}`));

27
eas.json Normal file
View File

@ -0,0 +1,27 @@
{
"cli": {
"version": ">= 3.0.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"ios": {
"simulator": false
}
},
"production": {
"autoIncrement": true,
"ios": {
"resourceClass": "m-medium"
}
}
}
}

View File

@ -1,8 +1,18 @@
import { registerRootComponent } from 'expo'; import { registerRootComponent } from 'expo';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import React from 'react';
import App from './App'; import App from './App';
function Root() {
return (
<SafeAreaProvider>
<App />
</SafeAreaProvider>
);
}
// registerRootComponent calls AppRegistry.registerComponent('main', () => App); // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build, // It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately // the environment is set up appropriately
registerRootComponent(App); registerRootComponent(Root);

4906
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,32 @@
{ {
"name": "wellnuo", "name": "wellnuo",
"version": "1.0.0", "version": "1.0.0",
"main": "index.ts", "main": "index.tsx",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo run:android",
"ios": "expo start --ios", "ios": "expo run:ios",
"web": "expo start --web" "web": "expo start --web"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"eas-cli": "^16.28.0",
"expo": "~54.0.25", "expo": "~54.0.25",
"expo-av": "~16.0.7",
"expo-dev-client": "~6.0.18",
"expo-linear-gradient": "~15.0.7",
"expo-status-bar": "~3.0.8", "expo-status-bar": "~3.0.8",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5" "react-native": "0.81.5",
"react-native-dotenv": "^3.4.11",
"react-native-safe-area-context": "^5.6.2",
"react-native-webview": "^13.16.0",
"zaiclient": "^8.2.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.57.0",
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"babel-preset-expo": "^54.0.7",
"typescript": "~5.9.2" "typescript": "~5.9.2"
}, },
"private": true "private": true

23
playwright.config.js Normal file
View File

@ -0,0 +1,23 @@
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: 'list',
use: {
baseURL: 'https://react.eluxnetworks.net',
trace: 'on-first-retry',
screenshot: 'on',
video: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
outputDir: 'tests/screenshots/',
});

View File

@ -0,0 +1,245 @@
import React, { useRef, useState, useCallback } from 'react';
import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity } from 'react-native';
import { WebView, WebViewNavigation } from 'react-native-webview';
const API_BASE = 'https://eluxnetworks.net/function/well-api/api';
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
// Credentials
const CREDENTIALS = {
username: 'anandk',
password: 'anandk_8',
clientId: '001'
};
interface DashboardWebViewProps {
onPatientSelect?: (patientId: number, patientName: string) => void;
onError?: (error: string) => void;
}
export const DashboardWebView: React.FC<DashboardWebViewProps> = ({
onPatientSelect,
onError
}) => {
const webViewRef = useRef<WebView>(null);
const [loading, setLoading] = useState(true);
const [token, setToken] = useState<string | null>(null);
// JavaScript to inject for auto-login
const getAutoLoginScript = (authToken: string) => `
(function() {
// Save auth to localStorage
localStorage.setItem('auth2', JSON.stringify({
username: '${CREDENTIALS.username}',
token: '${authToken}',
user_id: 43
}));
// Check if we're on login page
if (window.location.pathname === '/dashboard' || window.location.pathname === '/') {
// Find login form
const inputs = document.querySelectorAll('input');
const usernameInput = inputs[0];
const passwordInput = document.querySelector('input[type="password"]');
if (usernameInput && passwordInput) {
// Fill form
usernameInput.value = '${CREDENTIALS.username}';
passwordInput.value = '${CREDENTIALS.password}';
// Trigger React state updates
usernameInput.dispatchEvent(new Event('input', { bubbles: true }));
passwordInput.dispatchEvent(new Event('input', { bubbles: true }));
// Click login button
setTimeout(() => {
const loginBtn = document.querySelector('button');
if (loginBtn) loginBtn.click();
}, 100);
}
}
// Listen for patient card clicks
document.addEventListener('click', function(e) {
const card = e.target.closest('[data-patient-id]');
if (card) {
const patientId = card.getAttribute('data-patient-id');
const patientName = card.querySelector('.patient-name')?.textContent || '';
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'PATIENT_SELECTED',
patientId: patientId,
patientName: patientName
}));
}
}, true);
true;
})();
`;
// Fetch token on mount
const fetchToken = useCallback(async () => {
try {
const nonce = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const params = new URLSearchParams({
function: 'credentials',
clientId: CREDENTIALS.clientId,
user_name: CREDENTIALS.username,
ps: CREDENTIALS.password,
nonce: nonce
});
const response = await fetch(API_BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString()
});
const data = await response.json();
if (data.access_token) {
setToken(data.access_token);
return data.access_token;
} else {
throw new Error('No token received');
}
} catch (error) {
console.error('Failed to fetch token:', error);
onError?.('Failed to authenticate');
return null;
}
}, [onError]);
// Handle messages from WebView
const handleMessage = useCallback((event: { nativeEvent: { data: string } }) => {
try {
const message = JSON.parse(event.nativeEvent.data);
if (message.type === 'PATIENT_SELECTED') {
onPatientSelect?.(message.patientId, message.patientName);
}
} catch (e) {
console.log('WebView message:', event.nativeEvent.data);
}
}, [onPatientSelect]);
// Handle navigation state change
const handleNavigationStateChange = useCallback((navState: WebViewNavigation) => {
console.log('Navigation:', navState.url);
}, []);
// Inject script when page loads
const handleLoadEnd = useCallback(() => {
setLoading(false);
if (token && webViewRef.current) {
webViewRef.current.injectJavaScript(getAutoLoginScript(token));
}
}, [token]);
// Refresh handler
const handleRefresh = useCallback(() => {
setLoading(true);
webViewRef.current?.reload();
}, []);
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>WellNuo Dashboard</Text>
<TouchableOpacity onPress={handleRefresh} style={styles.refreshButton}>
<Text style={styles.refreshText}>Refresh</Text>
</TouchableOpacity>
</View>
{/* WebView */}
<WebView
ref={webViewRef}
source={{ uri: DASHBOARD_URL }}
style={styles.webview}
onLoadStart={() => setLoading(true)}
onLoadEnd={handleLoadEnd}
onNavigationStateChange={handleNavigationStateChange}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
allowsBackForwardNavigationGestures={true}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#3B82F6" />
<Text style={styles.loadingText}>Loading dashboard...</Text>
</View>
)}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error('WebView error:', nativeEvent);
onError?.(`WebView error: ${nativeEvent.description}`);
}}
/>
{/* Loading overlay */}
{loading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#3B82F6" />
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F3F4F6',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#3B82F6',
},
headerTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#FFFFFF',
},
refreshButton: {
padding: 8,
borderRadius: 8,
backgroundColor: 'rgba(255,255,255,0.2)',
},
refreshText: {
color: '#FFFFFF',
fontWeight: '600',
},
webview: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F3F4F6',
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: '#6B7280',
},
loadingOverlay: {
position: 'absolute',
top: 60,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.8)',
},
});
export default DashboardWebView;

View File

@ -0,0 +1,62 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ConnectionStatus } from '../types';
interface StatusIndicatorProps {
connectionStatus: ConnectionStatus;
isListening: boolean;
isSpeaking: boolean;
isProcessing: boolean;
}
export function StatusIndicator({
connectionStatus,
isListening,
isSpeaking,
isProcessing,
}: StatusIndicatorProps) {
const getStatusText = () => {
if (connectionStatus === 'error') return 'Connection Error';
if (connectionStatus === 'connecting') return 'Connecting...';
if (connectionStatus !== 'connected') return null;
if (isSpeaking) return 'Julia is speaking...';
if (isProcessing) return 'Thinking...';
if (isListening) return 'Listening...';
return null;
};
const getStatusColor = () => {
if (connectionStatus === 'error') return '#EF4444';
if (connectionStatus === 'connecting') return '#F59E0B';
if (isListening) return '#10B981';
if (isProcessing) return '#F59E0B';
if (isSpeaking) return '#8B5CF6';
return '#4A90A4';
};
const statusText = getStatusText();
// Only show when there's something meaningful to display
if (!statusText) {
return null;
}
return (
<View style={styles.container}>
<Text style={[styles.statusText, { color: getStatusColor() }]}>
{statusText}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
marginBottom: 8,
},
statusText: {
fontSize: 16,
fontWeight: '500',
},
});

View File

@ -0,0 +1,110 @@
import React, { useRef, useEffect } from 'react';
import { View, Text, ScrollView, StyleSheet } from 'react-native';
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
interface TranscriptViewProps {
messages: Message[];
currentTranscript?: string;
}
export function TranscriptView({ messages, currentTranscript }: TranscriptViewProps) {
const scrollViewRef = useRef<ScrollView>(null);
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, [messages, currentTranscript]);
if (messages.length === 0 && !currentTranscript) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
Your conversation will appear here
</Text>
</View>
);
}
return (
<ScrollView
ref={scrollViewRef}
style={styles.container}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{messages.map((message, index) => (
<View
key={index}
style={[
styles.messageContainer,
message.role === 'user' ? styles.userMessage : styles.assistantMessage,
]}
>
<Text style={styles.roleLabel}>
{message.role === 'user' ? 'You' : 'Wellnuo'}
</Text>
<Text style={styles.messageText}>{message.content}</Text>
</View>
))}
{currentTranscript && (
<View style={[styles.messageContainer, styles.currentTranscript]}>
<Text style={styles.roleLabel}>You (listening...)</Text>
<Text style={styles.messageText}>{currentTranscript}</Text>
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 16,
gap: 12,
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 32,
},
emptyText: {
fontSize: 16,
color: '#9CA3AF',
textAlign: 'center',
},
messageContainer: {
padding: 12,
borderRadius: 12,
maxWidth: '85%',
},
userMessage: {
backgroundColor: '#E5E7EB',
alignSelf: 'flex-end',
},
assistantMessage: {
backgroundColor: '#4A90A4',
alignSelf: 'flex-start',
},
currentTranscript: {
backgroundColor: '#FEF3C7',
alignSelf: 'flex-end',
opacity: 0.8,
},
roleLabel: {
fontSize: 11,
fontWeight: '600',
marginBottom: 4,
opacity: 0.7,
},
messageText: {
fontSize: 15,
lineHeight: 20,
},
});

View File

@ -0,0 +1,281 @@
import React, { useEffect, useRef } from 'react';
import {
Pressable,
StyleSheet,
View,
Animated,
ViewStyle,
} from 'react-native';
interface VoiceButtonProps {
isListening: boolean;
isSpeaking: boolean;
isProcessing: boolean;
isInCall?: boolean;
disabled?: boolean;
onPressIn: () => void;
onPressOut: () => void;
style?: ViewStyle;
}
export function VoiceButton({
isListening,
isSpeaking,
isProcessing,
isInCall = false,
disabled,
onPressIn,
onPressOut,
style,
}: VoiceButtonProps) {
// Separate animated values to avoid mixing native/JS drivers
const buttonScaleAnim = useRef(new Animated.Value(1)).current;
const glowOpacityAnim = useRef(new Animated.Value(0)).current;
const pulseAnimRef = useRef<Animated.CompositeAnimation | null>(null);
useEffect(() => {
if (isListening) {
// Pulse animation while listening
pulseAnimRef.current = Animated.loop(
Animated.sequence([
Animated.timing(buttonScaleAnim, {
toValue: 1.1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(buttonScaleAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
])
);
pulseAnimRef.current.start();
// Glow animation (separate, non-native)
Animated.timing(glowOpacityAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
} else {
// Stop pulse animation
if (pulseAnimRef.current) {
pulseAnimRef.current.stop();
pulseAnimRef.current = null;
}
// Reset scale with native driver
Animated.timing(buttonScaleAnim, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}).start();
// Fade out glow
Animated.timing(glowOpacityAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
}
}, [isListening, buttonScaleAnim, glowOpacityAnim]);
useEffect(() => {
if (isSpeaking && !isListening) {
// Wave animation while speaking
pulseAnimRef.current = Animated.loop(
Animated.sequence([
Animated.timing(buttonScaleAnim, {
toValue: 1.05,
duration: 300,
useNativeDriver: true,
}),
Animated.timing(buttonScaleAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}),
])
);
pulseAnimRef.current.start();
} else if (!isListening && !isSpeaking) {
if (pulseAnimRef.current) {
pulseAnimRef.current.stop();
pulseAnimRef.current = null;
}
}
}, [isSpeaking, isListening, buttonScaleAnim]);
const getButtonColor = () => {
if (disabled) return '#9CA3AF';
if (isInCall) return '#EF4444'; // Red when in call (to indicate "end call")
if (isListening) return '#10B981'; // Green when listening
if (isSpeaking) return '#8B5CF6';
if (isProcessing) return '#F59E0B';
return '#4A90A4'; // Blue when ready to start
};
return (
<View style={[styles.container, style]}>
<Animated.View
style={[
styles.glow,
{
opacity: glowOpacityAnim,
backgroundColor: 'rgba(239, 68, 68, 0.4)',
},
]}
/>
<Pressable
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={disabled}
style={({ pressed }) => [
styles.buttonWrapper,
pressed && { opacity: 0.8 },
]}
>
<Animated.View
style={[
styles.button,
{
backgroundColor: getButtonColor(),
transform: [{ scale: buttonScaleAnim }],
},
]}
>
<View style={styles.iconContainer}>
{isInCall ? (
<EndCallIcon />
) : isSpeaking ? (
<SpeakingIcon />
) : isProcessing ? (
<ProcessingIcon />
) : (
<PhoneIcon />
)}
</View>
</Animated.View>
</Pressable>
</View>
);
}
function PhoneIcon() {
return (
<View style={styles.phoneIcon}>
<View style={styles.phoneReceiver} />
</View>
);
}
function EndCallIcon() {
return (
<View style={styles.endCallIcon}>
<View style={styles.phoneReceiverRotated} />
</View>
);
}
function SpeakingIcon() {
return (
<View style={styles.speakingIcon}>
<View style={[styles.wave, styles.wave1]} />
<View style={[styles.wave, styles.wave2]} />
<View style={[styles.wave, styles.wave3]} />
</View>
);
}
function ProcessingIcon() {
return (
<View style={styles.processingIcon}>
<View style={styles.dot} />
<View style={styles.dot} />
<View style={styles.dot} />
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
glow: {
position: 'absolute',
width: 140,
height: 140,
borderRadius: 70,
},
buttonWrapper: {
// Hit area for touch
},
button: {
width: 100,
height: 100,
borderRadius: 50,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
iconContainer: {
alignItems: 'center',
justifyContent: 'center',
},
phoneIcon: {
alignItems: 'center',
justifyContent: 'center',
},
phoneReceiver: {
width: 36,
height: 14,
backgroundColor: 'white',
borderRadius: 7,
transform: [{ rotate: '-45deg' }],
},
endCallIcon: {
alignItems: 'center',
justifyContent: 'center',
},
phoneReceiverRotated: {
width: 36,
height: 14,
backgroundColor: 'white',
borderRadius: 7,
transform: [{ rotate: '135deg' }],
},
speakingIcon: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
wave: {
width: 4,
backgroundColor: 'white',
borderRadius: 2,
},
wave1: {
height: 20,
},
wave2: {
height: 32,
},
wave3: {
height: 20,
},
processingIcon: {
flexDirection: 'row',
gap: 6,
},
dot: {
width: 10,
height: 10,
backgroundColor: 'white',
borderRadius: 5,
},
});

View File

@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, Image, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import ZAIMCPClient from '../utils/zaiMCPClient';
const ZAIAnalysis = () => {
const [selectedImage, setSelectedImage] = useState(null);
const [analysis, setAnalysis] = useState('');
const [loading, setLoading] = useState(false);
const [zaiClient, setZaiClient] = useState(null);
const initializeZAI = async () => {
try {
const client = new ZAIMCPClient();
await client.start();
setZaiClient(client);
console.log('Z.AI MCP Client initialized');
} catch (error) {
console.error('Failed to initialize Z.AI:', error);
Alert.alert('Error', 'Failed to initialize Z.AI client');
}
};
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
setSelectedImage(result.assets[0].uri);
}
};
const analyzeImage = async () => {
if (!selectedImage || !zaiClient) {
Alert.alert('Error', 'Please select an image and initialize Z.AI');
return;
}
setLoading(true);
try {
const result = await zaiClient.analyzeImage(
selectedImage,
'Describe in detail the layout structure, color style, main components, and interactive elements of the website in this image to facilitate subsequent code generation by the model.'
);
setAnalysis(result.content || result);
} catch (error) {
console.error('Analysis failed:', error);
Alert.alert('Error', 'Failed to analyze image');
} finally {
setLoading(false);
}
};
return (
<View style={{ flex: 1, padding: 20 }}>
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 20 }}>
Z.AI Image Analysis
</Text>
<TouchableOpacity
style={{
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
marginBottom: 20,
alignItems: 'center'
}}
onPress={initializeZAI}
>
<Text style={{ color: 'white', fontWeight: 'bold' }}>
Initialize Z.AI Client
</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
backgroundColor: '#34C759',
padding: 15,
borderRadius: 8,
marginBottom: 20,
alignItems: 'center'
}}
onPress={pickImage}
>
<Text style={{ color: 'white', fontWeight: 'bold' }}>
Pick Image
</Text>
</TouchableOpacity>
{selectedImage && (
<View style={{ marginBottom: 20, alignItems: 'center' }}>
<Image
source={{ uri: selectedImage }}
style={{ width: 300, height: 200, borderRadius: 8 }}
/>
</View>
)}
<TouchableOpacity
style={{
backgroundColor: '#FF9500',
padding: 15,
borderRadius: 8,
marginBottom: 20,
alignItems: 'center',
opacity: loading ? 0.5 : 1
}}
onPress={analyzeImage}
disabled={loading}
>
<Text style={{ color: 'white', fontWeight: 'bold' }}>
{loading ? 'Analyzing...' : 'Analyze Image'}
</Text>
</TouchableOpacity>
{analysis && (
<View style={{
backgroundColor: '#F2F2F7',
padding: 15,
borderRadius: 8,
marginTop: 20
}}>
<Text style={{ fontWeight: 'bold', marginBottom: 10 }}>
Analysis Result:
</Text>
<Text>{analysis}</Text>
</View>
)}
</View>
);
};
export default ZAIAnalysis;

4
src/components/index.ts Normal file
View File

@ -0,0 +1,4 @@
export { VoiceButton } from './VoiceButton';
export { StatusIndicator } from './StatusIndicator';
export { TranscriptView } from './TranscriptView';
export { DashboardWebView } from './DashboardWebView';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,322 @@
import { WebhookContext } from '../types';
import AsyncStorage from '@react-native-async-storage/async-storage';
const FETCH_TIMEOUT = 10000; // 10 секунд по схеме
const CONVERSATIONS_STORAGE_KEY = '@wellnuo_pending_conversations';
// Типы ошибок для обработки по схеме
export type ApiErrorType =
| 'NETWORK_ERROR'
| 'API_KEY_ERROR'
| 'PATIENT_NOT_FOUND'
| 'TIMEOUT_ERROR'
| 'UNKNOWN_ERROR';
export class ApiError extends Error {
type: ApiErrorType;
statusCode?: number;
constructor(type: ApiErrorType, message: string, statusCode?: number) {
super(message);
this.type = type;
this.statusCode = statusCode;
this.name = 'ApiError';
}
}
const DEFAULT_CONTEXT: WebhookContext = {
systemPrompt: `You are Julia, a caring AI health assistant for the WellNuo family care system.
You help family members check on their elderly loved ones. Be warm, concise, and reassuring.
IMPORTANT: Always speak in English by default. Only switch to another language if the user explicitly requests it or speaks to you in that language first.
When starting a conversation, greet the user warmly and briefly summarize any important health updates or status information about their loved one.`,
voiceSettings: {
voice: 'shimmer',
speed: 1.0,
},
};
function formatPatientContext(data: any): string {
const parts: string[] = [];
// System prompt from API
if (data.system_prompt) {
parts.push(data.system_prompt);
}
// Patient info
if (data.patient) {
const p = data.patient;
parts.push(`\n\n## Patient Information
- Name: ${p.name}
- Age: ${p.age}
- Relationship: ${p.relationship}
- Address: ${p.address}
- Emergency Contact: ${p.emergency_contact?.name} (${p.emergency_contact?.relationship}) - ${p.emergency_contact?.phone}`);
}
// Current status
if (data.current_status) {
const s = data.current_status;
parts.push(`\n\n## Current Status
- Location: ${s.location}
- Mood: ${s.mood}
- Is Sleeping: ${s.is_sleeping ? 'Yes' : 'No'}
- Last Activity: ${new Date(s.last_activity).toLocaleString()}`);
}
// Health metrics
if (data.health_metrics) {
const h = data.health_metrics;
parts.push(`\n\n## Health Metrics
- Heart Rate: ${h.heart_rate?.current} bpm (resting avg: ${h.heart_rate?.resting_average}, status: ${h.heart_rate?.status})
- Blood Pressure: ${h.blood_pressure?.last_reading} (${h.blood_pressure?.status}, trend: ${h.blood_pressure?.trend})
- Blood Oxygen: ${h.blood_oxygen?.current}% (${h.blood_oxygen?.status})
- Body Temperature: ${h.body_temperature?.current}°C (${h.body_temperature?.status})
- Weight: ${h.weight?.current_kg} kg (trend: ${h.weight?.trend})`);
}
// Sleep data
if (data.sleep_data?.last_night) {
const sl = data.sleep_data.last_night;
parts.push(`\n\n## Sleep Data (Last Night)
- Duration: ${sl.duration_hours} hours (quality: ${sl.quality})
- Deep Sleep: ${sl.deep_sleep_hours}h, REM: ${sl.rem_sleep_hours}h, Light: ${sl.light_sleep_hours}h
- Bathroom Visits: ${sl.bathroom_visits}
- Sleep Score: ${data.sleep_data.sleep_score}/100`);
}
// Activity data
if (data.activity_data?.today) {
const a = data.activity_data.today;
parts.push(`\n\n## Today's Activity
- Steps: ${a.steps} (goal: ${data.activity_data.goals?.daily_steps})
- Active Minutes: ${a.active_minutes}
- Calories Burned: ${a.calories_burned}
- Rooms Visited: ${a.rooms_visited?.join(', ')}`);
}
// Medications
if (data.medications?.daily_schedule) {
const meds = data.medications.daily_schedule
.map((m: any) => `${m.name} ${m.dosage} at ${m.time} - ${m.taken_today ? 'Taken' : 'Not taken'}`)
.join('\n - ');
parts.push(`\n\n## Medications
- Adherence: ${data.medications.adherence_rate}%
- Today's Schedule:
- ${meds}`);
}
// Recent events
if (data.recent_events?.length > 0) {
const events = data.recent_events
.slice(0, 5)
.map((e: any) => `[${e.type}] ${e.description} (${new Date(e.timestamp).toLocaleString()})`)
.join('\n - ');
parts.push(`\n\n## Recent Events
- ${events}`);
}
// Upcoming appointments
if (data.upcoming?.appointments?.length > 0) {
const appts = data.upcoming.appointments
.map((a: any) => `${a.type} - ${new Date(a.date).toLocaleDateString()} at ${a.location}`)
.join('\n - ');
parts.push(`\n\n## Upcoming Appointments
- ${appts}`);
}
// Caregiver notes
if (data.caregiver_notes?.length > 0) {
const notes = data.caregiver_notes
.slice(0, 3)
.map((n: any) => `[${n.date}] ${n.author}: "${n.note}"`)
.join('\n - ');
parts.push(`\n\n## Recent Caregiver Notes
- ${notes}`);
}
return parts.join('');
}
export async function fetchWebhookContext(webhookUrl: string): Promise<WebhookContext> {
// AbortController для timeout 10 сек по схеме
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const response = await fetch(webhookUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
// Обработка HTTP ошибок по схеме
if (response.status === 401 || response.status === 403) {
throw new ApiError(
'API_KEY_ERROR',
'Сервис временно недоступен. Проблема с API ключом.',
response.status
);
}
if (response.status === 404) {
throw new ApiError(
'PATIENT_NOT_FOUND',
'Данные пациента не найдены.',
response.status
);
}
if (!response.ok) {
throw new ApiError(
'NETWORK_ERROR',
`Ошибка сервера: ${response.status}`,
response.status
);
}
const data = await response.json();
const fullContext = formatPatientContext(data);
return {
systemPrompt: fullContext || DEFAULT_CONTEXT.systemPrompt,
voiceSettings: {
voice: 'shimmer',
speed: 1.0,
},
userData: data,
};
} catch (error) {
clearTimeout(timeoutId);
// Timeout error
if (error instanceof Error && error.name === 'AbortError') {
throw new ApiError(
'TIMEOUT_ERROR',
'Превышено время ожидания. Проверьте подключение к интернету.'
);
}
// Уже обработанная ошибка
if (error instanceof ApiError) {
throw error;
}
// Network error (no internet, etc.)
console.error('Failed to fetch webhook context:', error);
throw new ApiError(
'NETWORK_ERROR',
'Проверьте подключение к интернету.'
);
}
}
export function getDefaultContext(): WebhookContext {
return DEFAULT_CONTEXT;
}
// Интерфейс для сохранения истории разговора
export interface ConversationMessage {
role: 'user' | 'assistant';
content: string;
timestamp: string;
}
export interface ConversationData {
session_id: string;
patient_id: string;
messages: ConversationMessage[];
duration: number;
started_at: string;
ended_at: string;
}
// Сохранение истории разговора на сервер по схеме
export async function saveConversation(
apiUrl: string,
data: ConversationData
): Promise<boolean> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const response = await fetch(`${apiUrl}/api/conversations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
console.log('Conversation saved successfully');
return true;
} catch (error) {
clearTimeout(timeoutId);
console.error('Failed to save conversation:', error);
// По схеме: если ошибка - сохранить локально для retry позже
await savePendingConversation(data);
return false;
}
}
// Сохранение в AsyncStorage для retry позже
async function savePendingConversation(data: ConversationData): Promise<void> {
try {
const existing = await AsyncStorage.getItem(CONVERSATIONS_STORAGE_KEY);
const pending: ConversationData[] = existing ? JSON.parse(existing) : [];
pending.push(data);
await AsyncStorage.setItem(CONVERSATIONS_STORAGE_KEY, JSON.stringify(pending));
console.log('Conversation saved locally for later sync');
} catch (error) {
console.error('Failed to save conversation locally:', error);
}
}
// Синхронизация отложенных разговоров
export async function syncPendingConversations(apiUrl: string): Promise<void> {
try {
const existing = await AsyncStorage.getItem(CONVERSATIONS_STORAGE_KEY);
if (!existing) return;
const pending: ConversationData[] = JSON.parse(existing);
if (pending.length === 0) return;
console.log(`Syncing ${pending.length} pending conversations...`);
const remaining: ConversationData[] = [];
for (const conversation of pending) {
try {
const response = await fetch(`${apiUrl}/api/conversations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(conversation),
});
if (!response.ok) {
remaining.push(conversation);
}
} catch {
remaining.push(conversation);
}
}
await AsyncStorage.setItem(CONVERSATIONS_STORAGE_KEY, JSON.stringify(remaining));
console.log(`Sync complete. ${remaining.length} conversations still pending.`);
} catch (error) {
console.error('Failed to sync pending conversations:', error);
}
}

5
src/types/env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '@env' {
export const OPENAI_API_KEY: string;
export const WEBHOOK_URL: string;
export const DEFAULT_VOICE: string;
}

23
src/types/index.ts Normal file
View File

@ -0,0 +1,23 @@
export interface WebhookContext {
systemPrompt: string;
voiceSettings?: {
voice?: 'alloy' | 'echo' | 'shimmer' | 'ash' | 'ballad' | 'coral' | 'sage' | 'verse';
speed?: number;
};
userData?: Record<string, any>;
}
export interface VoiceAssistantState {
isConnected: boolean;
isListening: boolean;
isSpeaking: boolean;
isProcessing: boolean;
error: string | null;
}
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
export interface RealtimeEvent {
type: string;
[key: string]: any;
}

131
src/utils/audioConverter.ts Normal file
View File

@ -0,0 +1,131 @@
/**
* PCM16 to WAV Converter
* OpenAI Realtime API returns raw PCM16 audio (24kHz, mono)
* expo-av requires WAV format with proper headers
*/
// WAV file constants for OpenAI Realtime audio
const SAMPLE_RATE = 24000;
const NUM_CHANNELS = 1;
const BITS_PER_SAMPLE = 16;
const BYTE_RATE = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
const BLOCK_ALIGN = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
/**
* Convert base64 PCM16 audio to WAV format with proper headers
* @param pcm16Base64 - Base64 encoded PCM16 audio data from OpenAI
* @returns Base64 encoded WAV audio ready for playback
*/
export function pcm16ToWav(pcm16Base64: string): string {
// Decode base64 to binary
const pcmData = base64ToUint8Array(pcm16Base64);
const dataLength = pcmData.length;
// WAV header is 44 bytes
const wavHeader = createWavHeader(dataLength);
// Combine header + PCM data
const wavData = new Uint8Array(44 + dataLength);
wavData.set(wavHeader, 0);
wavData.set(pcmData, 44);
// Convert back to base64
return uint8ArrayToBase64(wavData);
}
/**
* Create WAV header for PCM16 24kHz mono audio
*/
function createWavHeader(dataLength: number): Uint8Array {
const header = new ArrayBuffer(44);
const view = new DataView(header);
// RIFF chunk descriptor
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + dataLength, true); // File size - 8
writeString(view, 8, 'WAVE');
// fmt sub-chunk
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)
view.setUint16(20, 1, true); // AudioFormat (1 = PCM)
view.setUint16(22, NUM_CHANNELS, true); // NumChannels
view.setUint32(24, SAMPLE_RATE, true); // SampleRate
view.setUint32(28, BYTE_RATE, true); // ByteRate
view.setUint16(32, BLOCK_ALIGN, true); // BlockAlign
view.setUint16(34, BITS_PER_SAMPLE, true); // BitsPerSample
// data sub-chunk
writeString(view, 36, 'data');
view.setUint32(40, dataLength, true); // Subchunk2Size
return new Uint8Array(header);
}
/**
* Write string to DataView at offset
*/
function writeString(view: DataView, offset: number, str: string): void {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i));
}
}
/**
* Convert base64 string to Uint8Array
*/
function base64ToUint8Array(base64: string): Uint8Array {
// Handle both browser and React Native environments
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
/**
* Convert Uint8Array to base64 string
*/
function uint8ArrayToBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Create a data URI for WAV audio that can be played by expo-av
* @param pcm16Base64 - Base64 encoded PCM16 audio data from OpenAI
* @returns Data URI string ready for Audio.Sound.createAsync
*/
export function createWavDataUri(pcm16Base64: string): string {
const wavBase64 = pcm16ToWav(pcm16Base64);
return `data:audio/wav;base64,${wavBase64}`;
}
/**
* Strip WAV header from recorded audio to get raw PCM16 data
* expo-av records in WAV format (44-byte header + PCM data)
* OpenAI Realtime API expects raw PCM16 without headers
* @param wavBase64 - Base64 encoded WAV file from recording
* @returns Base64 encoded raw PCM16 data
*/
export function wavToPcm16Base64(wavBase64: string): string {
// Decode base64 to binary
const wavData = base64ToUint8Array(wavBase64);
// WAV header is 44 bytes for standard PCM format
// Skip first 44 bytes to get raw PCM data
if (wavData.length <= 44) {
console.log('[audioConverter] WAV data too short, returning as-is');
return wavBase64;
}
// Extract PCM data (skip 44-byte WAV header)
const pcmData = wavData.slice(44);
// Convert back to base64
return uint8ArrayToBase64(pcmData);
}

1
src/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export { pcm16ToWav, createWavDataUri } from './audioConverter';

87
src/utils/zaiMCPClient.js Normal file
View File

@ -0,0 +1,87 @@
import { spawn } from 'child_process';
import { createInterface } from 'readline';
class ZAIMCPClient {
constructor() {
this.process = null;
this.requestId = 0;
}
async start() {
this.process = spawn('zai-mcp-server', [], {
stdio: ['pipe', 'pipe', 'pipe']
});
this.rl = createInterface({
input: this.process.stdout,
output: this.process.stdin
});
return new Promise((resolve, reject) => {
this.process.on('spawn', () => {
console.log('Z.AI MCP Server started');
resolve();
});
this.process.on('error', reject);
});
}
async callTool(toolName, args) {
return new Promise((resolve, reject) => {
const request = {
jsonrpc: "2.0",
id: ++this.requestId,
method: "tools/call",
params: {
name: toolName,
arguments: args
}
};
this.process.stdin.write(JSON.stringify(request) + '\n');
let output = '';
const onData = (data) => {
output += data;
try {
const response = JSON.parse(output);
if (response.id === this.requestId) {
this.process.stdout.removeListener('data', onData);
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.result);
}
}
} catch (e) {
// Still collecting data
}
};
this.process.stdout.on('data', onData);
});
}
async analyzeImage(imagePath, prompt) {
return this.callTool('analyze_image', {
image_source: imagePath,
prompt: prompt
});
}
async analyzeVideo(videoPath, prompt) {
return this.callTool('analyze_video', {
video_source: videoPath,
prompt: prompt
});
}
stop() {
if (this.process) {
this.process.kill();
}
}
}
export default ZAIMCPClient;

164
tests/api-check.spec.js Normal file
View File

@ -0,0 +1,164 @@
const { test, expect } = require('@playwright/test');
// API credentials from client
const API_BASE = 'https://react.eluxnetworks.net';
const DASHBOARD_URL = `${API_BASE}/dashboard`;
const LOGIN_CREDENTIALS = {
username: 'anandk',
password: 'anandk_8'
};
test.describe('WellNuo API Check', () => {
test('should load main page', async ({ page }) => {
const response = await page.goto(API_BASE);
expect(response.status()).toBe(200);
// Check if it's the WellNuo app
await expect(page).toHaveTitle(/WellNuo/);
// Take screenshot
await page.screenshot({ path: 'tests/screenshots/main-page.png', fullPage: true });
console.log('Main page loaded successfully');
});
test('should navigate to dashboard and login', async ({ page }) => {
// Go to dashboard
await page.goto(DASHBOARD_URL);
// Wait for page to load
await page.waitForLoadState('networkidle');
// Take screenshot of login page
await page.screenshot({ path: 'tests/screenshots/dashboard-initial.png', fullPage: true });
// Look for login form elements
const usernameField = page.locator('input[name="username"], input[name="user_name"], input[type="text"]').first();
const passwordField = page.locator('input[name="password"], input[type="password"]').first();
const submitButton = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Sign in")').first();
// Check if login form exists
const hasLoginForm = await usernameField.isVisible().catch(() => false);
if (hasLoginForm) {
console.log('Login form found, attempting to login...');
// Fill credentials
await usernameField.fill(LOGIN_CREDENTIALS.username);
await passwordField.fill(LOGIN_CREDENTIALS.password);
// Take screenshot before submit
await page.screenshot({ path: 'tests/screenshots/login-filled.png', fullPage: true });
// Click login
await submitButton.click();
// Wait for navigation or response
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// Take screenshot after login
await page.screenshot({ path: 'tests/screenshots/after-login.png', fullPage: true });
// Check URL to verify login success
const currentUrl = page.url();
console.log('Current URL after login:', currentUrl);
// Check page content
const pageContent = await page.content();
console.log('Page contains "dashboard":', pageContent.toLowerCase().includes('dashboard'));
console.log('Page contains "error":', pageContent.toLowerCase().includes('error'));
console.log('Page contains "invalid":', pageContent.toLowerCase().includes('invalid'));
} else {
console.log('No login form found, checking page state...');
const pageContent = await page.content();
console.log('Page HTML (first 500 chars):', pageContent.substring(0, 500));
}
});
test('should check API endpoints directly', async ({ request }) => {
// Try common API endpoints
const endpoints = [
'/api/auth/login',
'/api/login',
'/auth/login',
'/api/token',
'/api/v1/auth/login'
];
for (const endpoint of endpoints) {
const url = `${API_BASE}${endpoint}`;
console.log(`Checking endpoint: ${url}`);
try {
// Try POST with credentials
const response = await request.post(url, {
data: {
user_name: LOGIN_CREDENTIALS.username,
password: LOGIN_CREDENTIALS.password
},
headers: {
'Content-Type': 'application/json'
}
});
console.log(` Status: ${response.status()}`);
if (response.status() === 200) {
const body = await response.json().catch(() => ({}));
console.log(` Response:`, JSON.stringify(body, null, 2));
if (body.token || body.access_token) {
console.log('TOKEN FOUND!');
}
}
} catch (e) {
console.log(` Error: ${e.message}`);
}
}
});
test('should try login with various body formats', async ({ request }) => {
const loginEndpoint = `${API_BASE}/api/auth/login`;
// Format 1: user_name / password
console.log('Trying format 1: user_name/password');
let response = await request.post(loginEndpoint, {
data: {
user_name: LOGIN_CREDENTIALS.username,
password: LOGIN_CREDENTIALS.password
}
});
console.log(`Status: ${response.status()}`);
if (response.status() === 200) {
console.log('Response:', await response.text());
}
// Format 2: username / password
console.log('Trying format 2: username/password');
response = await request.post(loginEndpoint, {
data: {
username: LOGIN_CREDENTIALS.username,
password: LOGIN_CREDENTIALS.password
}
});
console.log(`Status: ${response.status()}`);
if (response.status() === 200) {
console.log('Response:', await response.text());
}
// Format 3: email-style
console.log('Trying format 3: email/password');
response = await request.post(loginEndpoint, {
data: {
email: LOGIN_CREDENTIALS.username,
password: LOGIN_CREDENTIALS.password
}
});
console.log(`Status: ${response.status()}`);
if (response.status() === 200) {
console.log('Response:', await response.text());
}
});
});

185
tests/api-discovery.spec.js Normal file
View File

@ -0,0 +1,185 @@
const { test, expect } = require('@playwright/test');
const API_BASE = 'https://eluxnetworks.net/function/well-api/api';
const TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFuYW5kayIsImV4cCI6MTc2NTQ5MzI5N30.Jon7IJtBscErkt4-8qjas45Irwg_MQTRyS1ASrPGb1U';
const USER_NAME = 'anandk';
const PATIENT_ID = 25; // Ferdinand Zmrzli
test.describe('API Discovery - Find all available functions', () => {
// Common API functions to try
const functionsToTry = [
// Patient related
'patient_details',
'patient_info',
'get_patient',
'patient_data',
'patient_history',
'patient_context',
// Health data
'health_data',
'health_metrics',
'vitals',
'wellness',
'wellness_data',
// Activity
'activity',
'activity_log',
'activity_history',
'location_history',
// Chat/Messages
'chat',
'messages',
'send_message',
'get_messages',
'conversation',
'conversations',
// Alerts
'alerts',
'notifications',
'alert_history',
// Settings
'settings',
'user_settings',
'preferences',
// Reports
'report',
'daily_report',
'weekly_report',
// Other
'status',
'system_status',
'api_list',
'functions',
'help'
];
test('discover available API functions', async ({ request }) => {
console.log('\\n=== TESTING API FUNCTIONS ===\\n');
const workingFunctions = [];
for (const func of functionsToTry) {
const params = new URLSearchParams({
function: func,
user_name: USER_NAME,
token: TOKEN,
patient_id: PATIENT_ID.toString(),
user_id: PATIENT_ID.toString(),
date: '2025-12-10',
nonce: `test-${Date.now()}`
});
try {
const response = await request.post(API_BASE, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: params.toString()
});
const status = response.status();
const text = await response.text();
// Check if function returned valid data
if (status === 200 && text && !text.includes('error') && !text.includes('Error')) {
console.log(`[OK] ${func}: ${text.substring(0, 200)}`);
workingFunctions.push({ function: func, response: text.substring(0, 500) });
} else if (status === 200) {
console.log(`[??] ${func}: ${text.substring(0, 100)}`);
} else {
console.log(`[${status}] ${func}`);
}
} catch (e) {
console.log(`[ERR] ${func}: ${e.message}`);
}
}
console.log('\\n=== WORKING FUNCTIONS ===');
console.log(JSON.stringify(workingFunctions, null, 2));
});
test('explore web interface for more endpoints', async ({ page }) => {
const apiCalls = [];
// Intercept all API calls
page.on('request', req => {
if (req.url().includes('eluxnetworks.net')) {
apiCalls.push({
url: req.url(),
method: req.method(),
postData: req.postData()
});
}
});
// Login and navigate
await page.goto('https://react.eluxnetworks.net/dashboard');
await page.waitForLoadState('networkidle');
// Fill login
await page.locator('input').first().fill('anandk');
await page.locator('input[type="password"]').fill('anandk_8');
await page.locator('button:has-text("Log In")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// Click on first patient card to see details
const patientCard = page.locator('.bg-white').first();
if (await patientCard.isVisible()) {
await patientCard.click();
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/patient-details.png', fullPage: true });
}
// Try to find and click on different sections
const sections = ['Health', 'Activity', 'Sleep', 'Alerts', 'Settings', 'Chat', 'Messages'];
for (const section of sections) {
const link = page.locator(`text=${section}`).first();
if (await link.isVisible().catch(() => false)) {
console.log(`Found section: ${section}`);
await link.click().catch(() => {});
await page.waitForTimeout(1000);
}
}
console.log('\\n=== ALL API CALLS CAPTURED ===');
apiCalls.forEach((call, i) => {
console.log(`\\n[${i + 1}] ${call.method} ${call.url}`);
if (call.postData) {
console.log(' Data:', call.postData);
}
});
});
test('test patient context API for chat', async ({ request }) => {
// Test the webhook URL from .env
console.log('\\n=== Testing Patient Context API ===\\n');
const contextUrl = 'https://wellnuo.smartlaunchhub.com/api/patient/context';
try {
const response = await request.get(contextUrl);
console.log(`Status: ${response.status()}`);
const body = await response.text();
console.log(`Response: ${body.substring(0, 1000)}`);
} catch (e) {
console.log(`Error: ${e.message}`);
}
// Try with patient ID
try {
const response = await request.get(`${contextUrl}?patient_id=${PATIENT_ID}`);
console.log(`\\nWith patient_id - Status: ${response.status()}`);
const body = await response.text();
console.log(`Response: ${body.substring(0, 1000)}`);
} catch (e) {
console.log(`Error: ${e.message}`);
}
});
});

18
tests/check-repo.spec.js Normal file
View File

@ -0,0 +1,18 @@
const { test } = require('@playwright/test');
test('check repo content', async ({ page }) => {
// Login first
await page.goto('https://gitea.wellnua.com/user/login');
await page.locator('input#user_name').fill('sergei_t');
await page.locator('input#password').fill('WellNuo2025!Secure');
await page.locator('button:has-text("Sign In")').click();
await page.waitForTimeout(2000);
// Go to repo
await page.goto('https://gitea.wellnua.com/robert/MobileApp_react_native');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/repo-content.png', fullPage: true });
console.log('Page title:', await page.title());
console.log('URL:', page.url());
});

View File

@ -0,0 +1,76 @@
const { test, expect } = require('@playwright/test');
const GITEA_URL = 'https://gitea.wellnua.com';
const EMAIL = 'serter2069@gmail.com';
const USERNAME = 'serter2069';
const PASSWORD = 'WellNuo2025!Secure';
test('complete Gitea registration', async ({ page }) => {
console.log('=== GITEA REGISTRATION ===\n');
// Go to register page directly
await page.goto(`${GITEA_URL}/user/sign_up`);
await page.waitForLoadState('networkidle');
console.log('1. On registration page');
// Fill form
await page.locator('input#user_name').fill(USERNAME);
await page.locator('input#email').fill(EMAIL);
await page.locator('input#password').fill(PASSWORD);
await page.locator('input#retype').fill(PASSWORD);
console.log('2. Form filled');
await page.screenshot({ path: 'tests/screenshots/gitea-form.png' });
// Click Register Account button
await page.locator('button.ui.primary.button:has-text("Register Account")').click();
console.log('3. Clicked Register button');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-after-register.png', fullPage: true });
const currentUrl = page.url();
console.log('4. Current URL:', currentUrl);
// Check for errors or success
const pageText = await page.textContent('body');
if (pageText.includes('error') || pageText.includes('Error')) {
console.log(' Error detected on page');
}
if (pageText.includes('confirm') || pageText.includes('email')) {
console.log(' Email confirmation may be required');
}
if (pageText.includes('success') || pageText.includes('Success')) {
console.log(' Registration successful!');
}
// Check if already registered - try login
if (currentUrl.includes('sign_up') || pageText.includes('already')) {
console.log('5. User may already exist, trying login...');
await page.goto(`${GITEA_URL}/user/login`);
await page.waitForLoadState('networkidle');
await page.locator('input#user_name').fill(USERNAME);
await page.locator('input#password').fill(PASSWORD);
await page.locator('button.ui.primary.button').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-after-login.png', fullPage: true });
console.log(' After login URL:', page.url());
}
// Try accessing repo
console.log('6. Accessing repository...');
await page.goto(`${GITEA_URL}/robert/MobileApp_react_native`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-repo-final.png', fullPage: true });
console.log(' Repo page title:', await page.title());
console.log('\n=== DONE ===');
});

75
tests/gitea-final.spec.js Normal file
View File

@ -0,0 +1,75 @@
const { test, expect } = require('@playwright/test');
const RESET_URL = 'https://gitea.wellnua.com/user/recover_account?code=202512111241000180718f2083d36c90ef12f385dc56ccea121af2b2437365726765695f74';
const NEW_PASSWORD = 'WellNuo2025!Secure';
const USERNAME = 'sergei_t';
test('set password and access repo', async ({ page }) => {
console.log('=== GITEA FINAL SETUP ===\n');
// Step 1: Set new password
await page.goto(RESET_URL);
await page.waitForLoadState('networkidle');
console.log('1. On recovery page');
// Fill password
await page.locator('input[name="password"]').fill(NEW_PASSWORD);
console.log('2. Password filled');
// Click Recover Account button
await page.locator('button:has-text("Recover Account")').click();
console.log('3. Clicked Recover Account');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-final-1.png', fullPage: true });
console.log('4. After recovery URL:', page.url());
// Step 2: Login
console.log('\n5. Logging in...');
await page.goto('https://gitea.wellnua.com/user/login');
await page.waitForLoadState('networkidle');
await page.locator('input#user_name').fill(USERNAME);
await page.locator('input#password').fill(NEW_PASSWORD);
await page.locator('button:has-text("Sign In")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-final-2.png', fullPage: true });
console.log('6. After login URL:', page.url());
// Step 3: Access repo
console.log('\n7. Accessing repository...');
await page.goto('https://gitea.wellnua.com/robert/MobileApp_react_native');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-final-3.png', fullPage: true });
const title = await page.title();
console.log('8. Repo title:', title);
// Check if repo is accessible
const is404 = title.includes('Not Found') || title.includes('404');
if (!is404) {
console.log('\nSUCCESS! Repository is accessible!');
// Get clone URL
const cloneUrl = await page.locator('input[value*="git"]').first().inputValue().catch(() => '');
if (cloneUrl) {
console.log('Clone URL:', cloneUrl);
}
} else {
console.log('\nRepository still not accessible');
}
console.log('\n=== CREDENTIALS ===');
console.log('Gitea URL: https://gitea.wellnua.com');
console.log('Username:', USERNAME);
console.log('Password:', NEW_PASSWORD);
console.log('Email: serter2069@gmail.com');
console.log('\n=== DONE ===');
});

View File

@ -0,0 +1,107 @@
const { test, expect } = require('@playwright/test');
const GITEA_URL = 'https://gitea.wellnua.com';
const EMAIL = 'serter2069@gmail.com';
const USERNAME = 'serter2069';
const PASSWORD = 'WellNuo2025!Secure';
test('register on Gitea and accept invitation', async ({ page }) => {
console.log('=== GITEA REGISTRATION ===\n');
// Step 1: Go to Gitea
console.log('1. Opening Gitea...');
await page.goto(GITEA_URL);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-01-home.png', fullPage: true });
// Check current page
const pageContent = await page.content();
console.log(' Page title:', await page.title());
// Step 2: Find register/login link
const registerLink = page.locator('a:has-text("Register"), a:has-text("Sign Up"), a[href*="register"]').first();
const loginLink = page.locator('a:has-text("Sign In"), a:has-text("Login"), a[href*="login"]').first();
if (await registerLink.isVisible().catch(() => false)) {
console.log('2. Found Register link, clicking...');
await registerLink.click();
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-02-register.png', fullPage: true });
// Fill registration form
console.log('3. Filling registration form...');
const usernameField = page.locator('input[name="user_name"], input[id="user_name"], input[placeholder*="Username"]').first();
const emailField = page.locator('input[name="email"], input[type="email"]').first();
const passwordField = page.locator('input[name="password"], input[type="password"]').first();
const confirmField = page.locator('input[name="retype"], input[name="confirm_password"], input[type="password"]').nth(1);
if (await usernameField.isVisible()) {
await usernameField.fill(USERNAME);
console.log(' - Username filled');
}
if (await emailField.isVisible()) {
await emailField.fill(EMAIL);
console.log(' - Email filled');
}
if (await passwordField.isVisible()) {
await passwordField.fill(PASSWORD);
console.log(' - Password filled');
}
if (await confirmField.isVisible()) {
await confirmField.fill(PASSWORD);
console.log(' - Confirm password filled');
}
await page.screenshot({ path: 'tests/screenshots/gitea-03-form-filled.png', fullPage: true });
// Submit registration
const submitBtn = page.locator('button[type="submit"], input[type="submit"]').first();
if (await submitBtn.isVisible()) {
console.log('4. Submitting registration...');
await submitBtn.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-04-after-submit.png', fullPage: true });
console.log(' Current URL:', page.url());
}
} else if (await loginLink.isVisible().catch(() => false)) {
console.log('2. Found Login link (maybe already registered), trying to login...');
await loginLink.click();
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-02-login.png', fullPage: true });
// Try to login
const usernameField = page.locator('input[name="user_name"], input[id="user_name"]').first();
const passwordField = page.locator('input[name="password"], input[type="password"]').first();
if (await usernameField.isVisible()) {
await usernameField.fill(USERNAME);
await passwordField.fill(PASSWORD);
const submitBtn = page.locator('button[type="submit"]').first();
await submitBtn.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-03-after-login.png', fullPage: true });
console.log(' Current URL:', page.url());
}
} else {
console.log('2. Cannot find register/login links');
console.log(' Page HTML:', pageContent.substring(0, 1000));
}
// Step 5: Try to access the repository
console.log('5. Trying to access repository...');
await page.goto(`${GITEA_URL}/robert/MobileApp_react_native`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-05-repo.png', fullPage: true });
console.log(' Repo URL:', page.url());
console.log(' Page title:', await page.title());
console.log('\n=== DONE ===');
});

View File

@ -0,0 +1,44 @@
const { test, expect } = require('@playwright/test');
const GITEA_URL = 'https://gitea.wellnua.com';
const EMAIL = 'serter2069@gmail.com';
test('reset Gitea password', async ({ page }) => {
console.log('=== GITEA PASSWORD RESET ===\n');
// Go to forgot password page
await page.goto(`${GITEA_URL}/user/forgot_password`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-forgot-1.png', fullPage: true });
console.log('1. On forgot password page');
// Fill email
const emailField = page.locator('input[name="email"], input#email, input[type="email"]').first();
if (await emailField.isVisible()) {
await emailField.fill(EMAIL);
console.log('2. Email filled:', EMAIL);
await page.screenshot({ path: 'tests/screenshots/gitea-forgot-2.png', fullPage: true });
// Click submit
const submitBtn = page.locator('button[type="submit"], button:has-text("Send"), button:has-text("Reset")').first();
if (await submitBtn.isVisible()) {
await submitBtn.click();
console.log('3. Clicked submit button');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-forgot-3.png', fullPage: true });
console.log('4. Current URL:', page.url());
console.log('5. Check email for reset link!');
}
} else {
console.log('Email field not found');
const content = await page.content();
console.log('Page content:', content.substring(0, 500));
}
console.log('\n=== DONE ===');
});

View File

@ -0,0 +1,78 @@
const { test, expect } = require('@playwright/test');
const RESET_URL = 'https://gitea.wellnua.com/user/recover_account?code=202512111241000180718f2083d36c90ef12f385dc56ccea121af2b2437365726765695f74';
const NEW_PASSWORD = 'WellNuo2025!Secure';
const USERNAME = 'sergei_t';
test('set new Gitea password and login', async ({ page }) => {
console.log('=== SET NEW PASSWORD ===\n');
// Go to reset link
await page.goto(RESET_URL);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-reset-1.png', fullPage: true });
console.log('1. Opened reset link');
console.log(' URL:', page.url());
// Look for password fields
const passwordField = page.locator('input[name="password"], input#password, input[type="password"]').first();
const confirmField = page.locator('input[name="retype"], input[name="confirm"], input[type="password"]').nth(1);
if (await passwordField.isVisible()) {
await passwordField.fill(NEW_PASSWORD);
console.log('2. Password filled');
if (await confirmField.isVisible()) {
await confirmField.fill(NEW_PASSWORD);
console.log('3. Confirm password filled');
}
await page.screenshot({ path: 'tests/screenshots/gitea-reset-2.png', fullPage: true });
// Submit
const submitBtn = page.locator('button[type="submit"]').first();
await submitBtn.click();
console.log('4. Submitted new password');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-reset-3.png', fullPage: true });
console.log('5. Current URL:', page.url());
} else {
console.log('Password field not found on page');
const text = await page.textContent('body');
console.log('Page text:', text.substring(0, 500));
}
// Try to login with new password
console.log('\n6. Trying to login...');
await page.goto('https://gitea.wellnua.com/user/login');
await page.waitForLoadState('networkidle');
await page.locator('input#user_name').fill(USERNAME);
await page.locator('input#password').fill(NEW_PASSWORD);
await page.locator('button:has-text("Sign In")').click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
await page.screenshot({ path: 'tests/screenshots/gitea-logged-in.png', fullPage: true });
console.log('7. After login URL:', page.url());
// Access repo
console.log('\n8. Accessing repository...');
await page.goto('https://gitea.wellnua.com/robert/MobileApp_react_native');
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'tests/screenshots/gitea-repo-access.png', fullPage: true });
const title = await page.title();
console.log('9. Repo page title:', title);
if (!title.includes('Not Found')) {
console.log('SUCCESS! Repository is accessible!');
}
console.log('\n=== DONE ===');
});

View File

@ -0,0 +1,104 @@
const { test, expect } = require('@playwright/test');
const API_BASE = 'https://react.eluxnetworks.net';
const LOGIN_CREDENTIALS = {
username: 'anandk',
password: 'anandk_8'
};
test('capture network requests during login', async ({ page }) => {
// Collect all network requests
const requests = [];
const responses = [];
page.on('request', request => {
if (request.url().includes('eluxnetworks') || request.url().includes('api')) {
requests.push({
url: request.url(),
method: request.method(),
postData: request.postData(),
headers: request.headers()
});
}
});
page.on('response', async response => {
if (response.url().includes('eluxnetworks') || response.url().includes('api')) {
let body = '';
try {
body = await response.text();
} catch (e) {
body = 'Could not read body';
}
responses.push({
url: response.url(),
status: response.status(),
body: body.substring(0, 1000)
});
}
});
// Go to dashboard (will redirect to login)
await page.goto(`${API_BASE}/dashboard`);
await page.waitForLoadState('networkidle');
// Fill login form
const usernameField = page.locator('input').first();
const passwordField = page.locator('input[type="password"]');
const submitButton = page.locator('button:has-text("Log In")');
await usernameField.fill(LOGIN_CREDENTIALS.username);
await passwordField.fill(LOGIN_CREDENTIALS.password);
console.log('\\n=== CLICKING LOGIN BUTTON ===\\n');
// Click login and wait for network
await submitButton.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
console.log('\\n=== ALL REQUESTS ===');
requests.forEach((req, i) => {
console.log(`\\n[${i + 1}] ${req.method} ${req.url}`);
if (req.postData) {
console.log(' POST Data:', req.postData);
}
});
console.log('\\n=== ALL RESPONSES ===');
responses.forEach((res, i) => {
console.log(`\\n[${i + 1}] ${res.status} ${res.url}`);
if (res.body && res.body !== 'Could not read body' && !res.body.includes('<!doctype')) {
console.log(' Body:', res.body.substring(0, 500));
}
});
// Check localStorage for token
const localStorage = await page.evaluate(() => {
const items = {};
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
items[key] = window.localStorage.getItem(key);
}
return items;
});
console.log('\\n=== LOCAL STORAGE ===');
console.log(JSON.stringify(localStorage, null, 2));
// Check sessionStorage
const sessionStorage = await page.evaluate(() => {
const items = {};
for (let i = 0; i < window.sessionStorage.length; i++) {
const key = window.sessionStorage.key(i);
items[key] = window.sessionStorage.getItem(key);
}
return items;
});
console.log('\\n=== SESSION STORAGE ===');
console.log(JSON.stringify(sessionStorage, null, 2));
// Take final screenshot
await page.screenshot({ path: 'tests/screenshots/network-test-final.png', fullPage: true });
});

View File

@ -0,0 +1,6 @@
{
"status": "failed",
"failedTests": [
"703d711b83804a97c7aa-3618f1d342369de44ba0"
]
}

View File

@ -0,0 +1,126 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- navigation "Navigation Bar" [ref=e3]:
- generic [ref=e4]:
- link "Dashboard" [ref=e5] [cursor=pointer]:
- /url: /
- img [ref=e6]
- link "Issues" [ref=e7] [cursor=pointer]:
- /url: /issues
- link "Pull Requests" [ref=e8] [cursor=pointer]:
- /url: /pulls
- link "Milestones" [ref=e9] [cursor=pointer]:
- /url: /milestones
- link "Explore" [ref=e10] [cursor=pointer]:
- /url: /explore/repos
- generic [ref=e11]:
- link "Notifications" [ref=e12] [cursor=pointer]:
- /url: /notifications
- img [ref=e14]
- menu "Create…" [ref=e16] [cursor=pointer]:
- generic [ref=e17]:
- img [ref=e18]
- img [ref=e21]
- menu "Profile and Settings…" [ref=e23] [cursor=pointer]:
- generic [ref=e24]:
- img "sergei_t" [ref=e25]
- img [ref=e27]
- 'main "robert/MobileApp_react_native: Scaled down mobile app written in React Native" [ref=e29]':
- generic [ref=e30]:
- generic [ref=e32]:
- generic [ref=e33]:
- img [ref=e35]
- generic [ref=e38]:
- link "robert" [ref=e39] [cursor=pointer]:
- /url: /robert
- text: /
- link "MobileApp_react_native" [ref=e40] [cursor=pointer]:
- /url: /robert/MobileApp_react_native
- generic [ref=e42]: Private
- generic [ref=e43]:
- link "RSS Feed" [ref=e44] [cursor=pointer]:
- /url: /robert/MobileApp_react_native.rss
- img [ref=e45]
- generic [ref=e48] [cursor=pointer]:
- button "Watch" [ref=e49]:
- img [ref=e50]
- generic [ref=e52]: Watch
- link "1" [ref=e53]:
- /url: /robert/MobileApp_react_native/watchers
- generic [ref=e55] [cursor=pointer]:
- button "Star" [ref=e56]:
- img [ref=e57]
- generic [ref=e59]: Star
- link "0" [ref=e60]:
- /url: /robert/MobileApp_react_native/stars
- navigation [ref=e62]:
- generic [ref=e63]:
- link "Code" [ref=e64] [cursor=pointer]:
- /url: /robert/MobileApp_react_native
- img [ref=e65]
- generic [ref=e67]: Code
- link "Issues" [ref=e68] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/issues
- img [ref=e69]
- generic [ref=e72]: Issues
- link "Packages" [ref=e73] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/packages
- img [ref=e74]
- generic [ref=e76]: Packages
- link "Projects" [ref=e77] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/projects
- img [ref=e78]
- generic [ref=e80]: Projects
- link "Wiki" [ref=e81] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/wiki
- img [ref=e82]
- generic [ref=e84]: Wiki
- generic [ref=e88]:
- heading "Quick Guide" [level=4] [ref=e89]
- generic [ref=e90]:
- generic [ref=e91]:
- heading "Clone this repository Need help cloning? Visit Help." [level=3] [ref=e92]:
- text: Clone this repository
- generic [ref=e93]:
- text: Need help cloning? Visit
- link "Help" [ref=e94] [cursor=pointer]:
- /url: http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository
- text: .
- generic [ref=e95]:
- link "New File" [ref=e96] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/_new/main/
- link "Upload File" [ref=e97] [cursor=pointer]:
- /url: /robert/MobileApp_react_native/_upload/main/
- generic [ref=e98]:
- button "HTTPS" [ref=e99] [cursor=pointer]
- button "SSH" [ref=e100] [cursor=pointer]
- textbox [ref=e101]: https://gitea.wellnua.com/robert/MobileApp_react_native.git
- button "Copy URL" [ref=e102] [cursor=pointer]:
- img [ref=e103]
- generic [ref=e107]:
- heading "Creating a new repository on the command line" [level=3] [ref=e108]
- code [ref=e111]: touch README.md git init git checkout -b main git add README.md git commit -m "first commit" git remote add origin https://gitea.wellnua.com/robert/MobileApp_react_native.git git push -u origin main
- generic [ref=e113]:
- heading "Pushing an existing repository from the command line" [level=3] [ref=e114]
- code [ref=e117]: git remote add origin https://gitea.wellnua.com/robert/MobileApp_react_native.git git push -u origin main
- group "Footer" [ref=e118]:
- contentinfo "About Software" [ref=e119]:
- link "Powered by Gitea" [ref=e120] [cursor=pointer]:
- /url: https://about.gitea.com
- text: "Version: 1.23.6 Page:"
- strong [ref=e121]: 87ms
- text: "Template:"
- strong [ref=e122]: 5ms
- group "Links" [ref=e123]:
- menu [ref=e124] [cursor=pointer]:
- generic [ref=e125]:
- img [ref=e126]
- text: English
- link "Licenses" [ref=e128] [cursor=pointer]:
- /url: /assets/licenses.txt
- link "API" [ref=e129] [cursor=pointer]:
- /url: /api/swagger
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

130
tests/test-chat.js Normal file
View File

@ -0,0 +1,130 @@
// Test chat functionality with patient context
const https = require('https');
const OPENAI_API_KEY = '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';
const CONTEXT_URL = 'https://wellnuo.smartlaunchhub.com/api/patient/context';
async function fetchPatientContext() {
return new Promise((resolve, reject) => {
https.get(CONTEXT_URL, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(e);
}
});
}).on('error', reject);
});
}
async function askAboutPatient(context, question) {
// Build system prompt from context
const systemPrompt = `You are Julia, a caring AI health assistant for WellNuo family care system.
Current patient information:
- Name: ${context.patient.name}
- Age: ${context.patient.age}
- Relationship: ${context.patient.relationship}
- Location: ${context.current_status.location}
- Activity: ${context.current_status.estimated_activity}
Recent sleep data:
- Last night: ${context.sleep_analysis?.last_night?.total_hours || 'N/A'} hours
- Quality: ${context.sleep_analysis?.last_night?.quality_score || 'N/A'}/100
Today's activity:
- Active minutes: ${context.activity_patterns?.today?.total_active_minutes || 'N/A'}
- Rooms visited: ${context.activity_patterns?.today?.rooms_visited?.join(', ') || 'N/A'}
Environment (${context.current_status.location}):
- Temperature: ${context.environment?.living_room?.temperature_f || 'N/A'}°F
- Humidity: ${context.environment?.living_room?.humidity_percent || 'N/A'}%
- Air quality: ${context.environment?.living_room?.air_quality_status || 'N/A'}
Be warm, concise, and reassuring. Answer questions about the patient's wellbeing.`;
const requestData = JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
],
max_tokens: 300
});
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Length': Buffer.byteLength(requestData)
}
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.choices[0].message.content);
}
} catch (e) {
reject(e);
}
});
});
req.on('error', reject);
req.write(requestData);
req.end();
});
}
async function main() {
console.log('=== TESTING WELLNUO CHAT ===\n');
// Step 1: Fetch patient context
console.log('1. Fetching patient context...');
let context;
try {
context = await fetchPatientContext();
console.log(` Patient: ${context.patient.name}, ${context.patient.age} years old`);
console.log(` Location: ${context.current_status.location}`);
console.log(` Activity: ${context.current_status.estimated_activity}`);
console.log(' Context API: OK\n');
} catch (e) {
console.log(` ERROR: ${e.message}\n`);
process.exit(1);
}
// Step 2: Ask questions about patient
const questions = [
'How is my father doing today?',
'Did he sleep well last night?',
'Where is he right now?'
];
console.log('2. Testing AI chat responses:\n');
for (const question of questions) {
console.log(`Q: "${question}"`);
try {
const answer = await askAboutPatient(context, question);
console.log(`A: ${answer}\n`);
} catch (e) {
console.log(`ERROR: ${e.message}\n`);
}
}
console.log('=== CHAT TEST COMPLETE ===');
}
main().catch(console.error);