2 Commits

Author SHA1 Message Date
Ada
1e6c06bfef Merge branch 'main' into mobile-demo 2026-02-04 17:23:25 -08:00
Ada
d44ccc3ace feat(flow): add interactive AI puppet to FlowScreen
- Added puppet component modules: PuppetView, FlowPuppetSlot, and type definitions

- The puppet supports actions such as idle/smile/jump/shake/think, with a default smile.

- FlowScreen integrates puppet slots; it automatically uses the "think" function when sending messages and allows for interactive actions like Smile/Jump/Shake.

- The code is independent of the existing chat logic and does not affect existing functionality.
2026-02-04 16:57:28 -08:00
8 changed files with 878 additions and 553 deletions

798
README.md
View File

@@ -1,595 +1,287 @@
# Sentinel Frontend
# Sentinel App
A React Native mobile application for secure digital legacy management, built with Expo and TypeScript. Sentinel enables users to preserve, encrypt, and conditionally transfer their digital assets to designated heirs through a sophisticated cryptographic architecture.
[中文版](#中文版)
## Table of Contents
## Digital Legacy Management
- [Overview](#overview)
- [Business Features](#business-features)
- [Technical Architecture](#technical-architecture)
- [Security & Cryptography](#security--cryptography)
- [Getting Started](#getting-started)
- [Project Structure](#project-structure)
- [Configuration](#configuration)
- [Development](#development)
Sentinel is a mobile application that helps users securely manage their digital legacy. Built with React Native (Expo) and TypeScript.
## Overview
Sentinel is a comprehensive digital inheritance platform that addresses the critical challenge of preserving and transferring digital assets securely. The application combines advanced cryptographic techniques with an intuitive user interface, ensuring that sensitive information remains protected while enabling controlled access for designated beneficiaries.
### Core Value Proposition
- **Zero-Knowledge Architecture**: The platform cannot access user data without cryptographic keys
- **Conditional Release**: Assets are released only when specific conditions are met (e.g., subscription expiration, inactivity)
- **Multi-Layer Encryption**: Combines symmetric and asymmetric encryption for maximum security
- **Distributed Trust**: Uses secret sharing to prevent single points of failure
## Business Features
## Features
### 🗞️ Flow - Captain's Journal
An AI-powered journaling interface for daily thoughts, emotions, and reflections.
**Capabilities:**
- Multi-modal entry support: text, voice, and image inputs
- Record daily thoughts, emotions, and reflections
- AI-inferred emotional state tracking
- Conversational AI with multiple role configurations:
- Reflective Assistant: Deep introspection and emotional exploration
- Creative Spark: Brainstorming and creative writing
- Action Planner: Goal setting and productivity coaching
- Empathetic Guide: Emotional support and non-judgmental listening
- Archive entries to encrypted Vault for long-term preservation
- Conversation summarization for efficient review
**Use Cases:**
- Daily emotional tracking and mental health monitoring
- Creative writing and idea generation
- Goal planning and accountability
- Therapeutic journaling with AI support
- Archive entries to the encrypted Vault
- Support for text, voice, and image entries
### 📦 Vault - The Deep Vault
End-to-end encrypted storage for sensitive digital assets.
**Supported Asset Types:**
- **Game Accounts**: Credentials for gaming platforms (Steam, etc.)
- **Private Keys**: Cryptographic keys and wallet seeds
- **Documents**: Legal documents, contracts, and important files
- **Photos**: Personal memories and sensitive images
- **Wills**: Testamentary documents and final wishes
- **Custom**: User-defined asset categories
**Security Features:**
- Biometric authentication required for access (Face ID / Touch ID / Fingerprint)
- Shamir's Secret Sharing (3-of-2 threshold) for key management
- AES-256-GCM encryption for content protection
- User-isolated storage (multi-account support)
- Zero-knowledge architecture (server cannot decrypt without user shares)
**Workflow:**
1. User creates asset with plaintext content
2. System generates mnemonic phrase and splits into 3 shares
3. Content encrypted with derived AES key
4. One share stored on device, one on server, one for heir
5. Any 2 shares required to recover decryption key
- End-to-end encrypted asset storage
- Support for game accounts, private keys, documents, photos, wills
- Biometric authentication required for access
- Zero-knowledge architecture
### ⚓ Sentinel - Lighthouse Watch
Dead Man's Switch monitoring system for conditional asset release.
**Mechanism:**
- Continuous heartbeat monitoring of user activity
- Subscription status tracking
- Configurable grace periods before triggering release
- Activity logging and audit trail
- Status indicators: Normal, Warning, Releasing
**Trigger Conditions:**
- Subscription expiration without renewal
- Extended inactivity period
- Manual activation by user
- Administrative declaration (for testing/emergencies)
**Release Process:**
1. System detects trigger condition
2. Outer encryption layer removed (RSA gateway unlocked)
3. Heirs notified of available assets
4. Heirs can claim assets with their share + server share
- Dead Man's Switch monitoring system
- Heartbeat confirmation mechanism
- Subscription and activity tracking
- Configurable grace periods
### 🧭 Heritage - Fleet Legacy
Heir management and asset distribution system.
**Features:**
- **Heir Management**: Add, edit, and remove designated beneficiaries
- **Release Levels**: Configure priority tiers (1-3) for asset access
- **Release Order**: Define sequence for multi-heir scenarios
- **Payment Strategies**:
- Prepaid: Heir pays upfront for access
- Pay on Access: Payment required when claiming assets
- **Legal Document Interface**: Formal, testamentary-style presentation
- **Assignment Workflow**: Assign specific assets to specific heirs
**Heir Status:**
- **Invited**: Heir has been designated but not yet confirmed
- **Confirmed**: Heir has accepted invitation and verified identity
- Heir management with release levels
- Customizable release order and timing
- Payment strategy configuration
- Legal document-style interface
### ⛵ Me - Captain's Quarters
- Subscription and protocol status
- Sentinel configuration
- Security center
- Data export and backup
- Social responsibility program
User account management and system configuration.
## Tech Stack
**Sections:**
- **Subscription Status**: Current tier, expiration date, features enabled
- **Protocol Information**: Version tracking and update status
- **Sentinel Configuration**: Heartbeat intervals, grace periods, monitoring settings
- **Security Center**:
- Biometric settings
- Vault state management
- Key recovery options
- **Data Export**: Backup encrypted vault data
- **Social Responsibility**: Program information and participation
- **Framework**: React Native (Expo SDK 52)
- **Language**: TypeScript
- **Navigation**: React Navigation (Bottom Tabs)
- **Icons**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
- **Styling**: Custom nautical theme with gradients
- **State Management**: React Context (AuthContext)
- **Storage**: AsyncStorage for auth persistence
## Technical Architecture
## Configuration
### Technology Stack
The application uses a centralized configuration file located at `src/config/index.ts`.
| Category | Technology | Purpose |
|----------|-----------|---------|
| **Framework** | React Native (Expo SDK 52) | Cross-platform mobile development |
| **Language** | TypeScript 5.3+ | Type-safe development |
| **Navigation** | React Navigation 6 | Bottom tabs + stack navigation |
| **State Management** | React Context API | Authentication and app state |
| **Storage** | AsyncStorage | Local persistence (auth tokens, vault state) |
| **Icons** | @expo/vector-icons | Feather, Ionicons, FontAwesome5, Material |
| **Styling** | StyleSheet + LinearGradient | Custom nautical-themed UI |
| **Crypto** | Web Crypto API + Polyfills | Cryptographic operations |
| **AI Integration** | LangChain + LangGraph | AI conversation management |
| **Build System** | Metro Bundler | JavaScript bundling |
### Key Configuration Options
### Architecture Patterns
| Option | Description | Default |
|--------|-------------|---------|
| `NO_BACKEND_MODE` | Use mock data instead of real backend | `false` |
| `DEBUG_MODE` | Enable API debug logging | `true` |
| `API_BASE_URL` | Backend API server URL | `http://localhost:8000` |
| `API_TIMEOUT` | Request timeout (ms) | `30000` |
#### Service Layer Architecture
### API Endpoints
All backend API routes are defined in `API_ENDPOINTS`:
- **AUTH**: `/login`, `/register`
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
- **AI**: `/ai/proxy`
- **ADMIN**: `/admin/declare-guale`
### Environment Setup
For development, you may need to modify `API_BASE_URL` in the config file to match your backend server address.
## Project Structure
```
src/
├── components/
│ └── common/
│ ├── BiometricModal.tsx
│ ├── Icons.tsx
│ └── VaultDoorAnimation.tsx
├── config/
│ └── index.ts # Centralized configuration
├── context/
│ └── AuthContext.tsx # Authentication state management
├── navigation/
│ ├── AuthNavigator.tsx # Login/Register navigation
│ └── TabNavigator.tsx # Main app navigation
├── screens/
│ ├── FlowScreen.tsx # AI chat interface
│ ├── VaultScreen.tsx
│ ├── SentinelScreen.tsx
│ ├── HeritageScreen.tsx
│ ├── MeScreen.tsx
│ ├── LoginScreen.tsx
│ └── RegisterScreen.tsx
├── services/
│ ├── index.ts # Service exports
│ ├── ai.service.ts # AI API integration
│ ├── auth.service.ts # Authentication API
│ ├── assets.service.ts # Asset management API
│ └── admin.service.ts # Admin operations API
├── theme/
│ └── colors.ts
└── types/
└── index.ts
assets/
├── icon.png # App icon (1024x1024)
├── adaptive-icon.png # Android adaptive icon
├── splash.png # Splash screen
├── favicon.png # Web favicon (32x32)
├── favicon.svg # SVG favicon for web
├── logo.svg # Vector logo (512x512)
└── images/
└── captain-avatar.svg # Avatar placeholder
```
## Services
The application uses a modular service architecture for API communication:
```
src/services/
├── auth.service.ts # Authentication (login, register)
├── assets.service.ts # Asset CRUD and inheritance
├── vault.service.ts # Vault encryption/decryption
├── ai.service.ts # AI conversation proxy
├── admin.service.ts # Administrative operations
├── langgraph.service.ts # LangGraph workflow integration
└── storage.service.ts # AsyncStorage abstraction
- **AuthService**: User authentication (login, register)
- **AIService**: AI conversation proxy with support for text and image input
- **AssetsService**: Digital asset management
- **AdminService**: Administrative operations
## Icons & Branding
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
### Logo Elements
- **Anchor**: Symbolizes stability and anchoring your digital legacy
- **Star/Compass**: Represents guidance and direction for heirs
- **Teal Color**: Evokes ocean depth and calm security
### Generating Icons
```bash
# View icon specifications
node scripts/generate-icons.js
```
**Service Pattern:**
- Centralized API configuration
- Mock mode support for development
- Consistent error handling
- Debug logging integration
- Type-safe request/response interfaces
#### Context-Based State Management
**AuthContext** (`src/context/AuthContext.tsx`):
- Manages authentication state
- Handles token persistence
- Provides user information throughout app
- Handles initialization and loading states
**Usage Pattern:**
```typescript
const { user, token, login, logout, isInitializing } = useAuth();
```
#### Navigation Structure
```
App (Root)
├── AuthNavigator (if not authenticated)
│ ├── LoginScreen
│ └── RegisterScreen
└── TabNavigator (if authenticated)
├── FlowScreen (🗞️)
├── VaultScreen (📦)
├── SentinelScreen (⚓)
├── HeritageScreen (🧭)
└── MeScreen (⛵)
```
### Key Dependencies
**Core:**
- `react`: 18.3.1
- `react-native`: 0.76.9
- `expo`: ~52.0.0
**Navigation:**
- `@react-navigation/native`: ^6.1.18
- `@react-navigation/bottom-tabs`: ^6.6.1
- `@react-navigation/native-stack`: ^6.11.0
**Cryptography:**
- `@noble/ciphers`: ^1.3.0 (AES encryption)
- `@noble/hashes`: ^1.8.0 (Hash functions)
- `bip39`: ^3.1.0 (Mnemonic generation)
- `expo-crypto`: ~14.0.2 (Crypto polyfills)
**AI & Language Models:**
- `@langchain/core`: ^1.1.18
- `@langchain/langgraph`: ^1.1.3
**Storage & Utilities:**
- `@react-native-async-storage/async-storage`: ^2.2.0
- `buffer`: ^6.0.3 (Node.js Buffer polyfill)
- `readable-stream`: ^4.7.0
## Security & Cryptography
### Encryption Architecture
Sentinel implements a multi-layer encryption system:
#### Layer 1: Vault Encryption (User-Controlled)
**Process:**
1. Generate 12-word BIP-39 mnemonic phrase
2. Derive AES-256 key using PBKDF2:
- Input: Mnemonic phrase
- Salt: `Sentinel_Salt_2026`
- Iterations: 100,000
- Hash: SHA-256
- Output: 32-byte AES key
3. Encrypt plaintext with AES-256-GCM:
- Mode: Galois/Counter Mode (authenticated encryption)
- IV: 16-byte random nonce
- Tag: 16-byte authentication tag
- Output: `IV + Ciphertext + Tag` (hex encoded)
**Implementation:** `src/utils/vaultCrypto.ts`
#### Layer 2: Secret Sharing (Distributed Trust)
**Shamir's Secret Sharing (3-of-2 Threshold):**
1. Convert mnemonic to entropy (BigInt representation)
2. Split entropy into 3 shares using linear polynomial:
```
f(x) = secret + a*x (mod P)
```
Where:
- `secret`: Mnemonic entropy
- `a`: Random coefficient
- `P`: Prime modulus (2^127 - 1)
- Shares at x = 1, 2, 3
3. Share Distribution:
- **Device Share (S0)**: Stored locally on user's device
- **Cloud Share (S1)**: Stored on Sentinel server
- **Heir Share (S2)**: Provided to designated heir
4. Recovery: Any 2 shares can recover original entropy via Lagrange interpolation
**Implementation:** `src/utils/sss.ts`
#### Layer 3: Gateway Encryption (Server-Controlled)
**RSA Outer Encryption:**
- Server generates RSA-4096 key pair per asset
- Inner encrypted content encrypted again with RSA public key
- Private key held by server, released only on trigger conditions
- Prevents unauthorized access even if inner encryption is compromised
**Note:** Gateway encryption is handled by backend; frontend sends `content_inner_encrypted` which backend wraps with RSA.
### Key Management
**Storage Strategy:**
- User-isolated keys: `getVaultStorageKeys(userId)` generates per-user storage keys
- Device share (S0): Stored in AsyncStorage with user-scoped key
- Mnemonic backup: Optional local backup of mnemonic portion (encrypted)
- Multi-account support: Each user has independent vault state
**Storage Keys:**
```typescript
{
INITIALIZED: `sentinel_vault_initialized_u{userId}`,
SHARE_DEVICE: `sentinel_vault_s0_u{userId}`,
MNEMONIC_PART_LOCAL: `sentinel_mnemonic_part_local_u{userId}`,
AES_KEY: `sentinel_aes_key_u{userId}`,
SHARE_SERVER: `sentinel_share_server_u{userId}`,
SHARE_HEIR: `sentinel_share_heir_u{userId}`
}
```
### Security Properties
1. **Zero-Knowledge**: Server cannot decrypt user data without user's share
2. **Forward Secrecy**: Compromising one share reveals nothing about the secret
3. **Authenticated Encryption**: GCM mode ensures data integrity
4. **Key Derivation**: PBKDF2 with high iteration count prevents brute force
5. **Distributed Trust**: No single point of failure for key recovery
Use the `assets/logo.svg` as the source and export to required sizes.
## Getting Started
### Prerequisites
- Node.js 18+ and npm
- Expo CLI (installed globally or via npx)
- iOS Simulator (macOS) or Android Emulator / physical device
- Backend API server running (or use `NO_BACKEND_MODE`)
### Installation
```bash
# Install dependencies
npm install
# Start Expo development server
# Start the development server
npx expo start
```
### Platform-Specific Commands
```bash
# iOS Simulator (macOS only)
npm run ios
# Android Emulator / Device
npm run android
# Web Browser
npm run web
```
### Development Modes
**With Backend:**
1. Ensure backend server is running (default: `http://localhost:8000`)
2. Update `API_BASE_URL` in `src/config/index.ts` if needed
3. Start Expo: `npx expo start`
**Without Backend (Mock Mode):**
1. Set `NO_BACKEND_MODE = true` in `src/config/index.ts`
2. All API calls return mock data
3. Useful for UI development and testing
## Project Structure
```
frontend/
├── App.tsx # Root component with auth routing
├── app.json # Expo configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── assets/ # Static assets
│ ├── icon.png # App icon (1024x1024)
│ ├── adaptive-icon.png # Android adaptive icon
│ ├── splash.png # Splash screen
│ ├── favicon.png # Web favicon
│ ├── logo.svg # Vector logo
│ └── images/ # Additional images
├── scripts/ # Build scripts
│ └── generate-icons.js # Icon generation utility
└── src/
├── components/ # Reusable UI components
│ └── common/
│ ├── BiometricModal.tsx # Biometric auth modal
│ ├── Icons.tsx # Icon helper component
│ └── VaultDoorAnimation.tsx # Vault unlock animation
├── config/ # Configuration
│ └── index.ts # Centralized config (API, endpoints, etc.)
├── context/ # React Context providers
│ └── AuthContext.tsx # Authentication state management
├── hooks/ # Custom React hooks
│ ├── index.ts
│ └── useVaultAssets.ts # Vault asset management hook
├── navigation/ # Navigation configuration
│ ├── AuthNavigator.tsx # Login/Register navigation
│ └── TabNavigator.tsx # Main app tab navigation
├── screens/ # Screen components
│ ├── FlowScreen.tsx # AI journaling interface
│ ├── VaultScreen.tsx # Encrypted asset management
│ ├── SentinelScreen.tsx # Dead Man's Switch monitoring
│ ├── HeritageScreen.tsx # Heir management
│ ├── MeScreen.tsx # User settings and account
│ ├── LoginScreen.tsx # Authentication
│ └── RegisterScreen.tsx
├── services/ # API service layer
│ ├── index.ts # Service exports
│ ├── auth.service.ts # Authentication API
│ ├── assets.service.ts # Asset CRUD API
│ ├── vault.service.ts # Vault encryption utilities
│ ├── ai.service.ts # AI conversation API
│ ├── admin.service.ts # Admin operations
│ ├── langgraph.service.ts # LangGraph integration
│ └── storage.service.ts # AsyncStorage abstraction
├── theme/ # Design system
│ └── colors.ts # Color palette and typography
├── types/ # TypeScript type definitions
│ └── index.ts # Shared types and interfaces
├── utils/ # Utility functions
│ ├── index.ts
│ ├── crypto_core.ts # Crypto utilities (if needed)
│ ├── crypto_polyfill.ts # Crypto API polyfills
│ ├── sss.ts # Shamir's Secret Sharing
│ ├── vaultCrypto.ts # AES encryption/decryption
│ ├── vaultAssets.ts # Asset management utilities
│ ├── token_utils.ts # Token management
│ └── async_hooks_mock.ts # Async hooks polyfill
└── polyfills.ts # Global polyfills (Buffer, etc.)
```
## Configuration
### Environment Configuration
All configuration is centralized in `src/config/index.ts`:
```typescript
// Development mode
export const NO_BACKEND_MODE = false; // Use mock data
export const DEBUG_MODE = true; // API debug logging
// API Configuration
export const API_BASE_URL = 'http://localhost:8000';
export const API_TIMEOUT = 30000; // 30 seconds
```
### API Endpoints
Defined in `API_ENDPOINTS` constant:
| Category | Endpoint | Method | Purpose |
|----------|----------|--------|---------|
| **Auth** | `/login` | POST | User authentication |
| | `/register` | POST | User registration |
| **Assets** | `/assets/get` | GET | Fetch user's assets |
| | `/assets/create` | POST | Create new asset |
| | `/assets/claim` | POST | Claim inherited asset |
| | `/assets/assign` | POST | Assign asset to heir |
| | `/assets/delete` | POST | Delete asset |
| **AI** | `/ai/proxy` | POST | AI conversation proxy |
| | `/get_ai_roles` | GET | Fetch available AI roles |
| **Admin** | `/admin/declare-guale` | POST | Admin: Declare user deceased |
### Vault Storage Configuration
Vault storage uses user-scoped keys for multi-account support:
```typescript
// Get storage keys for a user
const keys = getVaultStorageKeys(userId);
// Keys are prefixed with user ID to prevent cross-user access
// Format: sentinel_vault_{key}_{suffix}
// Suffix: _u{userId} for authenticated users, _guest for guests
```
### AI Configuration
AI roles and system prompts are configurable:
```typescript
export const AI_CONFIG = {
DEFAULT_SYSTEM_PROMPT: '...',
MOCK_RESPONSE_DELAY: 500,
ROLES: [
{ id: 'reflective', name: 'Reflective Assistant', ... },
{ id: 'creative', name: 'Creative Spark', ... },
{ id: 'planner', name: 'Action Planner', ... },
{ id: 'empathetic', name: 'Empathetic Guide', ... },
]
};
```
## Development
### Code Style
- **TypeScript**: Strict mode enabled, all files typed
- **Components**: Functional components with hooks
- **Naming**: PascalCase for components, camelCase for functions/variables
- **Imports**: Absolute imports preferred (configured in tsconfig.json)
### Testing
**Manual Testing:**
- Use Expo Go app on physical device for real-world testing
- Test biometric authentication on actual devices
- Verify encryption/decryption flows with real backend
**Mock Mode Testing:**
- Enable `NO_BACKEND_MODE` for UI/UX testing
- Mock responses simulate real API behavior
- Useful for rapid iteration without backend dependency
### Debugging
**API Debug Logging:**
- Enabled when `DEBUG_MODE = true`
- Logs all API requests/responses to console
- Includes request URLs, headers, and response data
**React Native Debugger:**
- Use React Native Debugger or Chrome DevTools
- Inspect component state and props
- Monitor network requests
### Building for Production
```bash
# Build for iOS
eas build --platform ios
# Build for Android
eas build --platform android
# Build for Web
npx expo export:web
```
**Note:** Requires Expo Application Services (EAS) account for native builds.
### Common Issues
**Crypto API Not Available:**
- Ensure polyfills are loaded (`src/polyfills.ts`)
- Check that `crypto.subtle` is available in environment
- React Native requires polyfills for Web Crypto API
**AsyncStorage Errors:**
- Ensure `@react-native-async-storage/async-storage` is properly linked
- Check storage permissions on device
- Clear storage if corrupted: `AsyncStorage.clear()`
**Navigation Issues:**
- Ensure `NavigationContainer` wraps navigators
- Check that screens are properly registered
- Verify tab bar configuration matches screen names
## Design Philosophy
### Nautical Theme
The application uses a consistent nautical/maritime aesthetic:
- **Color Palette**: Teal (#459E9E) primary, ocean blues, mint gradients
- **Iconography**: Anchors, ship wheels, compasses, lighthouses
- **Terminology**: Captain, Fleet, Vault, Sentinel, Heritage
- **Typography**: Serif fonts for formal sections, sans-serif for UI
### User Experience Principles
1. **Privacy First**: Encryption happens locally, user controls keys
2. **Transparency**: Clear explanation of security mechanisms
3. **Accessibility**: Biometric auth for convenience, fallback options available
4. **Elegance**: Clean, modern UI with subtle animations
5. **Trust**: Visual indicators for security status and system health
## License
Private - All rights reserved.
## Support
For issues, questions, or contributions, please contact the development team.
- **Nautical Theme**: Captain's sanctum aesthetic with anchors, ship wheels, and ocean colors
- **Emotional Balance**: Warm and secure feeling across different tabs
- **Privacy First**: Zero-knowledge architecture, local encryption
- **Elegant UI**: Mint gradients, serif typography, subtle shadows
---
**Version**: 2.0.0
**Last Updated**: February 2026
# 中文版
[English Version](#sentinel-app)
## 数字遗产管理
Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。使用 React Native (Expo) 和 TypeScript 构建。
## 功能特性
### 🗞️ Flow - 船长日志
- 记录日常想法、情感和反思
- AI 推断情感状态追踪
- 将条目归档到加密保险库
- 支持文本、语音和图像条目
### 📦 Vault - 深海宝库
- 端到端加密资产存储
- 支持游戏账号、私钥、文档、照片、遗嘱
- 需要生物识别认证才能访问
- 零知识架构
### ⚓ Sentinel - 灯塔守望
- 死人开关监控系统
- 心跳确认机制
- 订阅和活动追踪
- 可配置的冷静期
### 🧭 Heritage - 舰队遗产
- 继承人管理与释放等级
- 可自定义释放顺序和时间
- 付款策略配置
- 法律文书风格界面
### ⛵ Me - 船长室
- 订阅和协议状态
- 哨兵配置
- 安全中心
- 数据导出和备份
- 社会责任计划
## 技术栈
- **框架**: React Native (Expo SDK 52)
- **语言**: TypeScript
- **导航**: React Navigation (底部标签)
- **图标**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
- **样式**: 自定义航海主题配渐变
- **状态管理**: React Context (AuthContext)
- **存储**: AsyncStorage 用于认证持久化
## 配置说明
应用使用位于 `src/config/index.ts` 的集中配置文件。
### 主要配置项
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `NO_BACKEND_MODE` | 使用模拟数据而非真实后端 | `false` |
| `DEBUG_MODE` | 启用 API 调试日志 | `true` |
| `API_BASE_URL` | 后端 API 服务器地址 | `http://localhost:8000` |
| `API_TIMEOUT` | 请求超时时间(毫秒) | `30000` |
### API 端点
所有后端 API 路由定义在 `API_ENDPOINTS` 中:
- **AUTH**: `/login`, `/register`
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
- **AI**: `/ai/proxy`
- **ADMIN**: `/admin/declare-guale`
### 环境配置
开发时,您可能需要修改配置文件中的 `API_BASE_URL` 以匹配您的后端服务器地址。
## 服务层
应用使用模块化的服务架构进行 API 通信:
- **AuthService**: 用户认证(登录、注册)
- **AIService**: AI 对话代理,支持文本和图片输入
- **AssetsService**: 数字资产管理
- **AdminService**: 管理员操作
## 运行项目
```bash
# 安装依赖
npm install
# 启动开发服务器
npx expo start
```
## 图标与品牌
Sentinel 品牌使用青色(#459E9E)背景上的航海锚与星星标志。
### 标志元素
- **锚**: 象征稳定性和锚定你的数字遗产
- **星星/指南针**: 代表对继承人的指引和方向
- **青色**: 唤起海洋深度和平静的安全感
### 生成图标
```bash
# 查看图标规格
node scripts/generate-icons.js
```
使用 `assets/logo.svg` 作为源文件并导出所需尺寸。
## 设计理念
- **航海主题**: 船长圣殿美学,配以锚、船舵和海洋色彩
- **情感平衡**: 不同标签页带来温暖而安全的感觉
- **隐私优先**: 零知识架构,本地加密
- **优雅界面**: 薄荷渐变、衬线字体、柔和阴影

157
package-lock.json generated
View File

@@ -36,6 +36,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.15.2",
"react-native-view-shot": "^3.8.0",
"react-native-web": "~0.19.13",
"readable-stream": "^4.7.0",
@@ -4705,6 +4706,12 @@
"@noble/hashes": "^1.2.0"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -5441,6 +5448,56 @@
"utrie": "^1.0.2"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -5609,6 +5666,61 @@
"node": ">=8"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -5692,6 +5804,18 @@
"once": "^1.4.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -8044,6 +8168,12 @@
"node": ">=0.10"
}
},
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -8834,6 +8964,18 @@
"node": ">=4"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -9832,6 +9974,21 @@
"react-native": "*"
}
},
"node_modules/react-native-svg": {
"version": "15.15.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.2.tgz",
"integrity": "sha512-lpaSwA2i+eLvcEdDZyGgMEInQW99K06zjJqfMFblE0yxI0SCN5E4x6in46f0IYi6i3w2t2aaq3oOnyYBe+bo4w==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-view-shot": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz",

View File

@@ -37,6 +37,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "^15.15.2",
"react-native-view-shot": "^3.8.0",
"react-native-web": "~0.19.13",
"readable-stream": "^4.7.0",

View File

@@ -0,0 +1,85 @@
/**
* FlowPuppetSlot - Slot for FlowScreen to show interactive AI puppet.
* Composes PuppetView and optional action buttons; does not depend on FlowScreen logic.
*/
import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { PuppetView } from './PuppetView';
import type { FlowPuppetSlotProps, PuppetAction } from './types';
import { colors } from '../../theme/colors';
import { borderRadius, spacing } from '../../theme/colors';
const ACTIONS: PuppetAction[] = ['smile', 'jump', 'shake'];
export function FlowPuppetSlot({
currentAction,
isTalking,
onAction,
showActionButtons = true,
}: FlowPuppetSlotProps) {
const [localAction, setLocalAction] = useState<PuppetAction>(currentAction);
const effectiveAction = currentAction !== 'idle' ? currentAction : localAction;
const handleAction = useCallback(
(action: PuppetAction) => {
setLocalAction(action);
onAction?.(action);
if (['smile', 'wave', 'nod', 'shake', 'jump'].includes(action)) {
setTimeout(() => {
setLocalAction((prev) => (prev === action ? 'idle' : prev));
onAction?.('idle');
}, 2600);
}
},
[onAction]
);
return (
<View style={styles.wrapper}>
<PuppetView action={effectiveAction} isTalking={isTalking} />
{showActionButtons && (
<View style={styles.actions}>
{ACTIONS.map((act) => (
<TouchableOpacity
key={act}
style={styles.actionBtn}
onPress={() => handleAction(act)}
activeOpacity={0.8}
>
<Text style={styles.actionLabel}>{act}</Text>
</TouchableOpacity>
))}
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.lg,
},
actions: {
flexDirection: 'row',
marginTop: spacing.lg,
gap: spacing.sm,
},
actionBtn: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.lg,
backgroundColor: colors.flow.cardBackground,
borderWidth: 1,
borderColor: colors.flow.cardBorder,
},
actionLabel: {
fontSize: 12,
fontWeight: '600',
color: colors.flow.primary,
textTransform: 'capitalize',
},
});

View File

@@ -0,0 +1,340 @@
/**
* PuppetView - Interactive blue spirit avatar (React Native).
* Port of airi---interactive-ai-puppet Puppet with same actions:
* idle, wave, nod, shake, jump, think; mouth reflects isTalking.
* Code isolated so FlowScreen stays unchanged except composition.
*/
import React, { useEffect, useRef } from 'react';
import { View, StyleSheet, Animated, Easing } from 'react-native';
import { PuppetViewProps } from './types';
const PUPPET_SIZE = 160;
export function PuppetView({ action, isTalking }: PuppetViewProps) {
const floatAnim = useRef(new Animated.Value(0)).current;
const bounceAnim = useRef(new Animated.Value(0)).current;
const shakeAnim = useRef(new Animated.Value(0)).current;
const thinkScale = useRef(new Animated.Value(1)).current;
const thinkOpacity = useRef(new Animated.Value(1)).current;
const smileScale = useRef(new Animated.Value(1)).current;
// Idle: gentle float
useEffect(() => {
if (action !== 'idle') return;
const loop = Animated.loop(
Animated.sequence([
Animated.timing(floatAnim, {
toValue: 1,
duration: 2000,
useNativeDriver: true,
easing: Easing.inOut(Easing.ease),
}),
Animated.timing(floatAnim, {
toValue: 0,
duration: 2000,
useNativeDriver: true,
easing: Easing.inOut(Easing.ease),
}),
])
);
loop.start();
return () => loop.stop();
}, [action, floatAnim]);
// Smile: exaggerated smile scale pulse
useEffect(() => {
if (action !== 'smile') {
smileScale.setValue(1);
return;
}
const loop = Animated.loop(
Animated.sequence([
Animated.timing(smileScale, {
toValue: 1.12,
duration: 400,
useNativeDriver: true,
easing: Easing.out(Easing.ease),
}),
Animated.timing(smileScale, {
toValue: 1,
duration: 400,
useNativeDriver: true,
easing: Easing.in(Easing.ease),
}),
]),
{ iterations: 3 }
);
loop.start();
return () => loop.stop();
}, [action, smileScale]);
// Wave / Jump: bounce
useEffect(() => {
if (action !== 'wave' && action !== 'jump') return;
const loop = Animated.loop(
Animated.sequence([
Animated.timing(bounceAnim, {
toValue: 1,
duration: 400,
useNativeDriver: true,
easing: Easing.out(Easing.ease),
}),
Animated.timing(bounceAnim, {
toValue: 0,
duration: 400,
useNativeDriver: true,
easing: Easing.in(Easing.ease),
}),
])
);
loop.start();
return () => loop.stop();
}, [action, bounceAnim]);
// Shake: wiggle
useEffect(() => {
if (action !== 'shake') return;
const loop = Animated.loop(
Animated.sequence([
Animated.timing(shakeAnim, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
Animated.timing(shakeAnim, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [action, shakeAnim]);
// Think: scale + opacity pulse
useEffect(() => {
if (action !== 'think') {
thinkScale.setValue(1);
thinkOpacity.setValue(1);
return;
}
const loop = Animated.loop(
Animated.sequence([
Animated.parallel([
Animated.timing(thinkScale, {
toValue: 0.92,
duration: 600,
useNativeDriver: true,
easing: Easing.inOut(Easing.ease),
}),
Animated.timing(thinkOpacity, {
toValue: 0.85,
duration: 600,
useNativeDriver: true,
}),
]),
Animated.parallel([
Animated.timing(thinkScale, {
toValue: 1,
duration: 600,
useNativeDriver: true,
easing: Easing.inOut(Easing.ease),
}),
Animated.timing(thinkOpacity, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}),
]),
])
);
loop.start();
return () => loop.stop();
}, [action, thinkScale, thinkOpacity]);
const floatY = floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, -8],
});
const bounceY = bounceAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, -20],
});
const shakeRotate = shakeAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '8deg'],
});
const isBounce = action === 'wave' || action === 'jump';
const isShake = action === 'shake';
const isSmile = action === 'smile';
const mouthStyle = isTalking
? [styles.mouth, styles.mouthOpen]
: isSmile
? [styles.mouth, styles.mouthBigSmile]
: [styles.mouth, styles.mouthSmile];
return (
<Animated.View
style={[
styles.container,
action === 'idle' && {
transform: [{ translateY: floatY }],
},
isBounce && {
transform: [{ translateY: bounceY }],
},
isShake && {
transform: [{ rotate: shakeRotate }],
},
action === 'think' && {
transform: [{ scale: thinkScale }],
opacity: thinkOpacity,
},
isSmile && {
transform: [{ scale: smileScale }],
},
]}
>
{/* Aura glow */}
<View style={styles.aura} />
{/* Body (droplet-like rounded rect) */}
<View style={styles.body}>
{/* Gloss */}
<View style={styles.gloss} />
{/* Cheeks */}
<View style={[styles.cheek, styles.cheekLeft]} />
<View style={[styles.cheek, styles.cheekRight]} />
{/* Eyes */}
<View style={styles.eyes}>
<View style={[styles.eye, styles.eyeLeft]}>
<View style={styles.eyeSparkle} />
</View>
<View style={[styles.eye, styles.eyeRight]}>
<View style={styles.eyeSparkle} />
</View>
</View>
{/* Mouth - default smile; open when talking; big smile when smile action */}
<View style={mouthStyle} />
</View>
</Animated.View>
);
}
const BODY_SIZE = PUPPET_SIZE * 0.9;
const EYE_SIZE = 10;
const EYE_OFFSET_X = 18;
const EYE_OFFSET_Y = -8;
const styles = StyleSheet.create({
container: {
width: PUPPET_SIZE,
height: PUPPET_SIZE,
alignItems: 'center',
justifyContent: 'center',
},
aura: {
position: 'absolute',
width: PUPPET_SIZE + 40,
height: PUPPET_SIZE + 40,
borderRadius: (PUPPET_SIZE + 40) / 2,
backgroundColor: 'rgba(14, 165, 233, 0.15)',
},
body: {
width: BODY_SIZE,
height: BODY_SIZE,
borderRadius: BODY_SIZE / 2,
backgroundColor: '#0ea5e9',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 0,
overflow: 'hidden',
shadowColor: '#0c4a6e',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 12,
elevation: 8,
},
gloss: {
position: 'absolute',
top: BODY_SIZE * 0.12,
left: BODY_SIZE * 0.2,
right: BODY_SIZE * 0.2,
height: 4,
borderRadius: 2,
backgroundColor: 'rgba(255,255,255,0.35)',
},
cheek: {
position: 'absolute',
width: 14,
height: 14,
borderRadius: 7,
backgroundColor: 'rgba(59, 130, 246, 0.35)',
},
cheekLeft: {
left: BODY_SIZE * 0.15,
top: BODY_SIZE * 0.42,
},
cheekRight: {
right: BODY_SIZE * 0.15,
top: BODY_SIZE * 0.42,
},
eyes: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: BODY_SIZE * 0.34,
},
eye: {
width: EYE_SIZE,
height: EYE_SIZE,
borderRadius: EYE_SIZE / 2,
backgroundColor: '#0c4a6e',
alignItems: 'center',
justifyContent: 'center',
},
eyeLeft: { marginRight: EYE_OFFSET_X },
eyeRight: { marginLeft: EYE_OFFSET_X },
eyeSparkle: {
width: 3,
height: 3,
borderRadius: 1.5,
backgroundColor: '#fff',
position: 'absolute',
top: 1,
left: 2,
},
mouth: {
position: 'absolute',
top: BODY_SIZE * 0.52,
backgroundColor: '#0c4a6e',
},
mouthSmile: {
width: 22,
height: 6,
borderBottomLeftRadius: 11,
borderBottomRightRadius: 11,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
mouthOpen: {
width: 18,
height: 6,
top: BODY_SIZE * 0.51,
borderRadius: 3,
backgroundColor: 'rgba(12, 74, 110, 0.9)',
},
mouthBigSmile: {
width: 32,
height: 10,
top: BODY_SIZE * 0.51,
borderBottomLeftRadius: 16,
borderBottomRightRadius: 16,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
});

View File

@@ -0,0 +1,3 @@
export { PuppetView } from './PuppetView';
export { FlowPuppetSlot } from './FlowPuppetSlot';
export type { PuppetAction, PuppetState, PuppetViewProps, FlowPuppetSlotProps } from './types';

View File

@@ -0,0 +1,28 @@
/**
* Puppet types - compatible with airi interactive AI puppet semantics.
* Used for FlowScreen multimodal avatar (action + talking state).
*/
export type PuppetAction = 'idle' | 'wave' | 'nod' | 'shake' | 'jump' | 'think' | 'talk' | 'smile';
export interface PuppetState {
currentAction: PuppetAction;
isTalking: boolean;
isThinking: boolean;
}
export interface PuppetViewProps {
action: PuppetAction;
isTalking: boolean;
}
export interface FlowPuppetSlotProps {
/** Current action (idle, wave, nod, shake, jump, think). */
currentAction: PuppetAction;
/** True when AI is "speaking" (e.g. streaming or responding). */
isTalking: boolean;
/** Optional: allow parent to set action (e.g. from AI tool call). */
onAction?: (action: PuppetAction) => void;
/** Show quick action buttons (wave, jump, shake) for interactivity. */
showActionButtons?: boolean;
}

View File

@@ -40,6 +40,8 @@ import { storageService } from '../services/storage.service';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SentinelVault } from '../utils/crypto_core';
import { Buffer } from 'buffer';
import { FlowPuppetSlot } from '../components/puppet';
import type { PuppetAction } from '../components/puppet';
// =============================================================================
// Type Definitions
@@ -98,6 +100,9 @@ export default function FlowScreen() {
const [saveResult, setSaveResult] = useState<{ success: boolean; message: string }>({ success: true, message: '' });
const [isSavingToVault, setIsSavingToVault] = useState(false);
// AI multimodal puppet (optional; does not affect existing chat logic)
const [puppetAction, setPuppetAction] = useState<PuppetAction>('idle');
const [chatHistory, setChatHistory] = useState<ChatSession[]>([
// Sample history data
{
@@ -208,6 +213,12 @@ export default function FlowScreen() {
}
}, [aiRoles]);
// Sync puppet action with sending state (think while AI is responding)
useEffect(() => {
if (isSending) setPuppetAction('think');
else setPuppetAction((prev) => (prev === 'think' ? 'idle' : prev));
}, [isSending]);
// Save current messages for the active role when they change
useEffect(() => {
if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing
@@ -754,6 +765,14 @@ export default function FlowScreen() {
</TouchableOpacity>
</View>
{/* AI multimodal puppet (optional slot; code in components/puppet) */}
<FlowPuppetSlot
currentAction={puppetAction}
isTalking={isSending}
onAction={setPuppetAction}
showActionButtons={true}
/>
{/* Chat Messages */}
<ScrollView
ref={scrollViewRef}