diff --git a/App.tsx b/App.tsx
index 86623ba..2c913dc 100644
--- a/App.tsx
+++ b/App.tsx
@@ -1,28 +1,54 @@
+/**
+ * App Entry Point
+ *
+ * Main application component with authentication routing.
+ * Shows loading screen while restoring auth state.
+ */
+
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { StyleSheet, View, ActivityIndicator } from 'react-native';
+import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';
import TabNavigator from './src/navigation/TabNavigator';
import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors';
+/**
+ * Loading screen shown while restoring auth state
+ */
+function LoadingScreen() {
+ return (
+
+
+ Loading...
+
+ );
+}
+
+/**
+ * Main app content with auth-based routing
+ */
function AppContent() {
- const { user } = useAuth();
+ const { user, isInitializing } = useAuth();
+
+ // Show loading screen while restoring auth state
+ if (isInitializing) {
+ return ;
+ }
return (
- {user ? (
-
- ) : (
-
- )}
+ {user ? : }
);
}
+/**
+ * Root App component
+ */
export default function App() {
return (
@@ -42,7 +68,11 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
- backgroundColor: colors.sentinel.background,
+ backgroundColor: colors.flow.backgroundGradientStart,
+ },
+ loadingText: {
+ marginTop: 16,
+ fontSize: 16,
+ color: colors.flow.textSecondary,
},
});
-
diff --git a/README.md b/README.md
index 4d6d5eb..44378df 100644
--- a/README.md
+++ b/README.md
@@ -44,8 +44,35 @@ Sentinel is a mobile application that helps users securely manage their digital
- **Framework**: React Native (Expo SDK 52)
- **Language**: TypeScript
- **Navigation**: React Navigation (Bottom Tabs)
-- **Icons**: @expo/vector-icons (Feather, Ionicons, MaterialCommunityIcons, FontAwesome5)
+- **Icons**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
- **Styling**: Custom nautical theme with gradients
+- **State Management**: React Context (AuthContext)
+- **Storage**: AsyncStorage for auth persistence
+
+## Configuration
+
+The application uses a centralized configuration file located at `src/config/index.ts`.
+
+### Key Configuration Options
+
+| 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` |
+
+### 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
@@ -56,14 +83,27 @@ src/
│ ├── BiometricModal.tsx
│ ├── Icons.tsx
│ └── VaultDoorAnimation.tsx
+├── config/
+│ └── index.ts # Centralized configuration
+├── context/
+│ └── AuthContext.tsx # Authentication state management
├── navigation/
-│ └── TabNavigator.tsx
+│ ├── AuthNavigator.tsx # Login/Register navigation
+│ └── TabNavigator.tsx # Main app navigation
├── screens/
-│ ├── FlowScreen.tsx
+│ ├── FlowScreen.tsx # AI chat interface
│ ├── VaultScreen.tsx
│ ├── SentinelScreen.tsx
│ ├── HeritageScreen.tsx
-│ └── MeScreen.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/
@@ -80,6 +120,15 @@ assets/
└── captain-avatar.svg # Avatar placeholder
```
+## Services
+
+The application uses a modular service architecture for API communication:
+
+- **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.
@@ -163,8 +212,44 @@ Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。
- **框架**: React Native (Expo SDK 52)
- **语言**: TypeScript
- **导航**: React Navigation (底部标签)
-- **图标**: @expo/vector-icons
+- **图标**: @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**: 管理员操作
## 运行项目
diff --git a/package-lock.json b/package-lock.json
index ab285f0..ead1426 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
@@ -18,6 +19,7 @@
"expo-constants": "~17.0.8",
"expo-font": "~13.0.4",
"expo-haptics": "~14.0.0",
+ "expo-image-picker": "^17.0.10",
"expo-linear-gradient": "~14.0.2",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
@@ -3275,6 +3277,18 @@
"node": ">=14"
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
+ "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
+ "license": "MIT",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.65 <1.0"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
@@ -5722,6 +5736,27 @@
"expo": "*"
}
},
+ "node_modules/expo-image-loader": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
+ "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-image-picker": {
+ "version": "17.0.10",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
+ "integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-image-loader": "~6.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-linear-gradient": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
@@ -6712,6 +6747,15 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -7591,6 +7635,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
+ "node_modules/merge-options": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
diff --git a/package.json b/package.json
index a3691be..3b220f6 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4",
+ "@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
@@ -19,6 +20,7 @@
"expo-constants": "~17.0.8",
"expo-font": "~13.0.4",
"expo-haptics": "~14.0.0",
+ "expo-image-picker": "^17.0.10",
"expo-linear-gradient": "~14.0.2",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 0000000..d892ac0
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,146 @@
+/**
+ * Application Configuration
+ *
+ * This file contains all configuration settings for the frontend application.
+ * Centralized configuration makes it easier to manage environment-specific settings.
+ */
+
+// =============================================================================
+// Environment Configuration
+// =============================================================================
+
+/**
+ * Set to true to use mock data instead of real backend calls.
+ * Useful for development and testing without a running backend.
+ */
+export const NO_BACKEND_MODE = false;
+
+/**
+ * Enable debug logging for API calls
+ */
+export const DEBUG_MODE = true;
+
+// =============================================================================
+// API Configuration
+// =============================================================================
+
+/**
+ * Base URL for the backend API server
+ */
+export const API_BASE_URL = 'http://localhost:8000';
+
+/**
+ * API request timeout in milliseconds
+ */
+export const API_TIMEOUT = 30000;
+
+/**
+ * API Endpoints
+ * All backend API routes are defined here for easy reference and maintenance.
+ */
+export const API_ENDPOINTS = {
+ // Authentication
+ AUTH: {
+ LOGIN: '/login',
+ REGISTER: '/register',
+ },
+
+ // Assets Management
+ ASSETS: {
+ GET: '/assets/get',
+ CREATE: '/assets/create',
+ CLAIM: '/assets/claim',
+ ASSIGN: '/assets/assign',
+ },
+
+ // AI Services
+ AI: {
+ PROXY: '/ai/proxy',
+ },
+
+ // Admin Operations
+ ADMIN: {
+ DECLARE_GUALE: '/admin/declare-guale',
+ },
+} as const;
+
+// =============================================================================
+// Helper Functions
+// =============================================================================
+
+/**
+ * Build full API URL from endpoint
+ * @param endpoint - API endpoint path (e.g., '/login')
+ * @returns Full URL (e.g., 'http://192.168.56.103:8000/login')
+ */
+export function buildApiUrl(endpoint: string): string {
+ return `${API_BASE_URL}${endpoint}`;
+}
+
+/**
+ * Get default headers for API requests
+ * @param token - Optional JWT token for authenticated requests
+ * @returns Headers object
+ */
+export function getApiHeaders(token?: string): Record {
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ };
+
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+
+ return headers;
+}
+
+/**
+ * Log API debug information
+ * Only logs when DEBUG_MODE is enabled
+ */
+export function logApiDebug(label: string, data: unknown): void {
+ if (DEBUG_MODE) {
+ console.log(`[API Debug] ${label}:`, data);
+ }
+}
+
+// =============================================================================
+// Mock User Configuration (for NO_BACKEND_MODE)
+// =============================================================================
+
+export const MOCK_CONFIG = {
+ USER: {
+ id: 999,
+ username: 'MockCaptain',
+ public_key: 'mock_public_key',
+ is_admin: true,
+ guale: false,
+ tier: 'premium',
+ tier_expires_at: '2026-12-31T23:59:59Z',
+ last_active_at: new Date().toISOString(),
+ },
+ ACCESS_TOKEN: 'mock_access_token',
+ RESPONSE_DELAY: 200, // milliseconds
+} as const;
+
+// =============================================================================
+// AI Service Configuration
+// =============================================================================
+
+export const AI_CONFIG = {
+ /**
+ * Default system prompt for AI conversations
+ */
+ DEFAULT_SYSTEM_PROMPT: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
+
+ /**
+ * Mock response delay in milliseconds (for NO_BACKEND_MODE)
+ */
+ MOCK_RESPONSE_DELAY: 500,
+} as const;
+
+// =============================================================================
+// Export Type Definitions
+// =============================================================================
+
+export type ApiEndpoint = typeof API_ENDPOINTS;
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
index 891de9c..32d298b 100644
--- a/src/context/AuthContext.tsx
+++ b/src/context/AuthContext.tsx
@@ -1,66 +1,175 @@
-import React, { createContext, useContext, useState, ReactNode } from 'react';
+/**
+ * AuthContext - Authentication State Management
+ *
+ * Provides authentication state and methods throughout the app.
+ * Uses AsyncStorage for persistent login state.
+ */
+
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import AsyncStorage from '@react-native-async-storage/async-storage';
import { User, LoginRequest, RegisterRequest } from '../types';
import { authService } from '../services/auth.service';
-import { Alert } from 'react-native';
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
interface AuthContextType {
- user: User | null;
- token: string | null;
- isLoading: boolean;
- signIn: (credentials: LoginRequest) => Promise;
- signUp: (data: RegisterRequest) => Promise;
- signOut: () => void;
+ user: User | null;
+ token: string | null;
+ isLoading: boolean;
+ isInitializing: boolean;
+ signIn: (credentials: LoginRequest) => Promise;
+ signUp: (data: RegisterRequest) => Promise;
+ signOut: () => void;
}
+// Storage keys
+const STORAGE_KEYS = {
+ TOKEN: '@auth_token',
+ USER: '@auth_user',
+};
+
+// =============================================================================
+// Context
+// =============================================================================
+
const AuthContext = createContext(undefined);
+// =============================================================================
+// Provider Component
+// =============================================================================
+
export function AuthProvider({ children }: { children: ReactNode }) {
- const [user, setUser] = useState(null);
- const [token, setToken] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isInitializing, setIsInitializing] = useState(true);
- const signIn = async (credentials: LoginRequest) => {
- setIsLoading(true);
- try {
- const response = await authService.login(credentials);
- setToken(response.access_token);
- setUser(response.user);
- } catch (error) {
- throw error;
- } finally {
- setIsLoading(false);
- }
- };
+ // Load saved auth state on app start
+ useEffect(() => {
+ loadStoredAuth();
+ }, []);
- const signUp = async (data: RegisterRequest) => {
- setIsLoading(true);
- try {
- await authService.register(data);
- // After successful registration, sign in automatically
- await signIn({ username: data.username, password: data.password });
- } catch (error) {
- throw error;
- } finally {
- setIsLoading(false);
- }
- };
+ /**
+ * Load stored authentication from AsyncStorage
+ */
+ const loadStoredAuth = async () => {
+ try {
+ const [storedToken, storedUser] = await Promise.all([
+ AsyncStorage.getItem(STORAGE_KEYS.TOKEN),
+ AsyncStorage.getItem(STORAGE_KEYS.USER),
+ ]);
- const signOut = () => {
- setUser(null);
- setToken(null);
- };
+ if (storedToken && storedUser) {
+ setToken(storedToken);
+ setUser(JSON.parse(storedUser));
+ console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
+ }
+ } catch (error) {
+ console.error('[Auth] Failed to load stored auth:', error);
+ } finally {
+ setIsInitializing(false);
+ }
+ };
- return (
-
- {children}
-
- );
+ /**
+ * Save authentication to AsyncStorage
+ */
+ const saveAuth = async (authToken: string, authUser: User) => {
+ try {
+ await Promise.all([
+ AsyncStorage.setItem(STORAGE_KEYS.TOKEN, authToken),
+ AsyncStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(authUser)),
+ ]);
+ console.log('[Auth] Session saved for user:', authUser.username);
+ } catch (error) {
+ console.error('[Auth] Failed to save auth:', error);
+ }
+ };
+
+ /**
+ * Clear authentication from AsyncStorage
+ */
+ const clearAuth = async () => {
+ try {
+ await Promise.all([
+ AsyncStorage.removeItem(STORAGE_KEYS.TOKEN),
+ AsyncStorage.removeItem(STORAGE_KEYS.USER),
+ ]);
+ console.log('[Auth] Session cleared');
+ } catch (error) {
+ console.error('[Auth] Failed to clear auth:', error);
+ }
+ };
+
+ /**
+ * Sign in with username and password
+ */
+ const signIn = async (credentials: LoginRequest) => {
+ setIsLoading(true);
+ try {
+ const response = await authService.login(credentials);
+ setToken(response.access_token);
+ setUser(response.user);
+ await saveAuth(response.access_token, response.user);
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ /**
+ * Sign up and automatically sign in
+ */
+ const signUp = async (data: RegisterRequest) => {
+ setIsLoading(true);
+ try {
+ await authService.register(data);
+ // After successful registration, sign in automatically
+ await signIn({ username: data.username, password: data.password });
+ } catch (error) {
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ /**
+ * Sign out and clear stored auth
+ */
+ const signOut = () => {
+ setUser(null);
+ setToken(null);
+ clearAuth();
+ };
+
+ return (
+
+ {children}
+
+ );
}
+// =============================================================================
+// Hook
+// =============================================================================
+
export function useAuth() {
- const context = useContext(AuthContext);
- if (context === undefined) {
- throw new Error('useAuth must be used within an AuthProvider');
- }
- return context;
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
}
diff --git a/src/screens/FlowScreen.tsx b/src/screens/FlowScreen.tsx
index d2c14af..dd693bb 100644
--- a/src/screens/FlowScreen.tsx
+++ b/src/screens/FlowScreen.tsx
@@ -1,76 +1,102 @@
-import React, { useState } from 'react';
+/**
+ * FlowScreen - AI Chat Interface
+ *
+ * Main chat screen for AI conversations with history management.
+ * Features:
+ * - Current conversation displayed in main window
+ * - Chat history accessible from top-right button
+ * - ChatGPT-style input bar at bottom
+ */
+
+import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
+ TouchableWithoutFeedback,
Modal,
TextInput,
SafeAreaView,
+ ActivityIndicator,
+ Alert,
+ FlatList,
+ Animated,
+ Image,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
-import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
+import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
+import * as ImagePicker from 'expo-image-picker';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
-import { FlowRecord } from '../types';
-import BiometricModal from '../components/common/BiometricModal';
-import VaultDoorAnimation from '../components/common/VaultDoorAnimation';
+import { aiService } from '../services/ai.service';
+import { useAuth } from '../context/AuthContext';
-// Mock data
-const initialRecords: FlowRecord[] = [
- {
- id: '1',
- type: 'text',
- content: 'Beautiful sunny day today. Went to the park with family. Felt the simple joy of life.',
- createdAt: new Date('2024-01-18T10:30:00'),
- emotion: 'Calm',
- isArchived: false,
- },
- {
- id: '2',
- type: 'text',
- content: 'Completed an important project. The process was challenging, but the result is satisfying.',
- createdAt: new Date('2024-01-17T16:45:00'),
- emotion: 'Content',
- isArchived: false,
- },
- {
- id: '3',
- type: 'text',
- content: 'Archived a reflection',
- createdAt: new Date('2024-01-15T09:20:00'),
- isArchived: true,
- archivedAt: new Date('2024-01-16T14:00:00'),
- },
- {
- id: '4',
- type: 'voice',
- content: '[Voice recording 2:30]',
- createdAt: new Date('2024-01-14T20:15:00'),
- emotion: 'Grateful',
- isArchived: false,
- },
-];
+// =============================================================================
+// Type Definitions
+// =============================================================================
-// Emotion config
-const emotionConfig: Record = {
- 'Calm': { color: colors.nautical.seafoam, icon: 'water' },
- 'Content': { color: colors.nautical.teal, icon: 'checkmark-circle' },
- 'Grateful': { color: colors.nautical.gold, icon: 'star' },
- 'Nostalgic': { color: colors.nautical.sage, icon: 'time' },
- 'Hopeful': { color: colors.nautical.mint, icon: 'sunny' },
-};
+interface ChatMessage {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ imageUri?: string;
+ createdAt: Date;
+}
+
+interface ChatSession {
+ id: string;
+ title: string;
+ messages: ChatMessage[];
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+// =============================================================================
+// Component
+// =============================================================================
export default function FlowScreen() {
- const [records, setRecords] = useState(initialRecords);
- const [showAddModal, setShowAddModal] = useState(false);
+ const { token, signOut } = useAuth();
+ const scrollViewRef = useRef(null);
+
+ // Current conversation state
+ const [messages, setMessages] = useState([]);
const [newContent, setNewContent] = useState('');
- const [selectedRecord, setSelectedRecord] = useState(null);
- const [showArchiveWarning, setShowArchiveWarning] = useState(false);
- const [showBiometric, setShowBiometric] = useState(false);
- const [showVaultAnimation, setShowVaultAnimation] = useState(false);
- const [selectedInputType, setSelectedInputType] = useState<'text' | 'voice' | 'image'>('text');
+ const [isSending, setIsSending] = useState(false);
+ const [isRecording, setIsRecording] = useState(false);
+ const [selectedImage, setSelectedImage] = useState(null);
+
+ // History modal state
+ const [showHistoryModal, setShowHistoryModal] = useState(false);
+ const modalSlideAnim = useRef(new Animated.Value(0)).current;
+ const [chatHistory, setChatHistory] = useState([
+ // Sample history data
+ {
+ id: '1',
+ title: 'Morning reflection',
+ messages: [],
+ createdAt: new Date('2024-01-18T10:30:00'),
+ updatedAt: new Date('2024-01-18T10:45:00'),
+ },
+ {
+ id: '2',
+ title: 'Project brainstorm',
+ messages: [],
+ createdAt: new Date('2024-01-17T14:00:00'),
+ updatedAt: new Date('2024-01-17T15:30:00'),
+ },
+ {
+ id: '3',
+ title: 'Evening thoughts',
+ messages: [],
+ createdAt: new Date('2024-01-16T20:00:00'),
+ updatedAt: new Date('2024-01-16T20:30:00'),
+ },
+ ]);
+
+ // Header date display
const today = new Date();
const dateStr = today.toLocaleDateString('en-US', {
weekday: 'long',
@@ -78,50 +104,285 @@ export default function FlowScreen() {
day: 'numeric'
});
- const handleAddRecord = () => {
- if (!newContent.trim()) return;
- const newRecord: FlowRecord = {
- id: Date.now().toString(),
- type: selectedInputType,
- content: newContent,
- createdAt: new Date(),
- emotion: 'Calm',
- isArchived: false,
- };
- setRecords([newRecord, ...records]);
- setNewContent('');
- setShowAddModal(false);
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ if (messages.length > 0) {
+ setTimeout(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ }
+ }, [messages]);
+
+ // Modal animation control
+ const openHistoryModal = () => {
+ setShowHistoryModal(true);
+ Animated.spring(modalSlideAnim, {
+ toValue: 1,
+ useNativeDriver: true,
+ tension: 65,
+ friction: 11,
+ }).start();
};
- const handleSendToVault = (record: FlowRecord) => {
- setSelectedRecord(record);
- setShowArchiveWarning(true);
+ const closeHistoryModal = () => {
+ Animated.timing(modalSlideAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }).start(() => {
+ setShowHistoryModal(false);
+ });
};
- const handleConfirmArchive = () => {
- setShowArchiveWarning(false);
- setShowBiometric(true);
- };
+ // =============================================================================
+ // Event Handlers
+ // =============================================================================
- const handleBiometricSuccess = () => {
- setShowBiometric(false);
- setShowVaultAnimation(true);
- };
-
- const handleVaultAnimationComplete = () => {
- setShowVaultAnimation(false);
- if (selectedRecord) {
- setRecords(
- records.map((r) =>
- r.id === selectedRecord.id
- ? { ...r, content: 'Archived a reflection', isArchived: true, archivedAt: new Date() }
- : r
- )
+ /**
+ * Handle sending a message to AI
+ */
+ const handleSendMessage = async () => {
+ if (!newContent.trim() || isSending) return;
+
+ // Check authentication
+ if (!token) {
+ Alert.alert(
+ 'Login Required',
+ 'Please login to send messages',
+ [{ text: 'OK', onPress: () => signOut() }]
);
- setSelectedRecord(null);
+ return;
+ }
+
+ const userMessage = newContent.trim();
+ setIsSending(true);
+ setNewContent('');
+
+ // Add user message immediately
+ const userMsg: ChatMessage = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: userMessage,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, userMsg]);
+
+ try {
+ // Call AI proxy
+ const aiResponse = await aiService.sendMessage(userMessage, token);
+
+ // Add AI response
+ const aiMsg: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: aiResponse,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, aiMsg]);
+
+ } catch (error) {
+ console.error('AI request failed:', error);
+
+ const errorMessage = error instanceof Error ? error.message : String(error);
+
+ // Handle authentication errors (401, credentials, unauthorized)
+ const isAuthError =
+ errorMessage.includes('401') ||
+ errorMessage.includes('credentials') ||
+ errorMessage.includes('Unauthorized') ||
+ errorMessage.includes('Not authenticated') ||
+ errorMessage.includes('validate');
+
+ if (isAuthError) {
+ signOut();
+ Alert.alert(
+ 'Session Expired',
+ 'Your login session has expired. Please login again.',
+ [{ text: 'OK' }]
+ );
+ return;
+ }
+
+ // Show error as AI message
+ const errorMsg: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: `Error: ${errorMessage}`,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, errorMsg]);
+ } finally {
+ setIsSending(false);
}
};
+ /**
+ * Handle voice recording toggle
+ */
+ const handleVoiceRecord = () => {
+ setIsRecording(!isRecording);
+ // TODO: Implement voice recording functionality
+ };
+
+ /**
+ * Handle image attachment - pick image and analyze with AI
+ */
+ const handleAddImage = async () => {
+ // Request permission
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (status !== 'granted') {
+ Alert.alert('Permission Required', 'Please grant permission to access photos');
+ return;
+ }
+
+ // Pick image
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
+ allowsEditing: true,
+ quality: 0.8,
+ base64: true,
+ });
+
+ if (!result.canceled && result.assets[0]) {
+ const imageAsset = result.assets[0];
+ setSelectedImage(imageAsset.uri);
+
+ // Check authentication
+ if (!token) {
+ Alert.alert(
+ 'Login Required',
+ 'Please login to analyze images',
+ [{ text: 'OK', onPress: () => signOut() }]
+ );
+ return;
+ }
+
+ setIsSending(true);
+
+ // Add user message with image
+ const userMsg: ChatMessage = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: 'Analyze this image',
+ imageUri: imageAsset.uri,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, userMsg]);
+
+ try {
+ // Call AI with image (using base64)
+ const aiResponse = await aiService.sendMessageWithImage(
+ 'Please describe and analyze this image in detail.',
+ imageAsset.base64 || '',
+ token
+ );
+
+ const aiMsg: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: aiResponse,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, aiMsg]);
+ } catch (error) {
+ console.error('AI image analysis failed:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
+
+ // Handle authentication errors
+ const isAuthError =
+ errorMessage.includes('401') ||
+ errorMessage.includes('Unauthorized') ||
+ errorMessage.includes('credentials') ||
+ errorMessage.includes('validate');
+
+ if (isAuthError) {
+ signOut();
+ Alert.alert(
+ 'Session Expired',
+ 'Your login session has expired. Please login again.',
+ [{ text: 'OK' }]
+ );
+ return;
+ }
+
+ const errorMsg: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: `⚠️ Error analyzing image: ${errorMessage}`,
+ createdAt: new Date(),
+ };
+ setMessages(prev => [...prev, errorMsg]);
+ } finally {
+ setIsSending(false);
+ setSelectedImage(null);
+ }
+ }
+ };
+
+ /**
+ * Start a new conversation
+ */
+ const handleNewChat = () => {
+ // Save current conversation to history if it has messages
+ if (messages.length > 0) {
+ const newSession: ChatSession = {
+ id: Date.now().toString(),
+ title: messages[0]?.content.substring(0, 30) + '...' || 'New conversation',
+ messages: [...messages],
+ createdAt: messages[0]?.createdAt || new Date(),
+ updatedAt: new Date(),
+ };
+ setChatHistory(prev => [newSession, ...prev]);
+ }
+
+ // Clear current messages
+ setMessages([]);
+ closeHistoryModal();
+ };
+
+ /**
+ * Load a conversation from history
+ */
+ const handleLoadHistory = (session: ChatSession) => {
+ // Save current conversation first if it has messages
+ if (messages.length > 0) {
+ const currentSession: ChatSession = {
+ id: Date.now().toString(),
+ title: messages[0]?.content.substring(0, 30) + '...' || 'Conversation',
+ messages: [...messages],
+ createdAt: messages[0]?.createdAt || new Date(),
+ updatedAt: new Date(),
+ };
+ setChatHistory(prev => [currentSession, ...prev.filter(s => s.id !== session.id)]);
+ }
+
+ // Load selected conversation
+ setMessages(session.messages);
+ closeHistoryModal();
+ };
+
+ /**
+ * Delete a conversation from history
+ */
+ const handleDeleteHistory = (sessionId: string) => {
+ Alert.alert(
+ 'Delete Conversation',
+ 'Are you sure you want to delete this conversation?',
+ [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Delete',
+ style: 'destructive',
+ onPress: () => setChatHistory(prev => prev.filter(s => s.id !== sessionId))
+ },
+ ]
+ );
+ };
+
+ // =============================================================================
+ // Helper Functions
+ // =============================================================================
+
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
};
@@ -130,6 +391,101 @@ export default function FlowScreen() {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
+ // =============================================================================
+ // Render Functions
+ // =============================================================================
+
+ /**
+ * Render a single chat message bubble
+ */
+ const renderMessage = (message: ChatMessage, index: number) => {
+ const isUser = message.role === 'user';
+
+ return (
+
+ {!isUser && (
+
+
+
+ )}
+
+ {/* Show image if present */}
+ {message.imageUri && (
+
+ )}
+
+ {message.content}
+
+
+ {formatTime(message.createdAt)}
+
+
+
+ );
+ };
+
+ /**
+ * Render empty state when no messages
+ */
+ const renderEmptyState = () => (
+
+
+
+
+ Start a conversation
+
+ Ask me anything or share your thoughts
+
+
+ );
+
+ /**
+ * Render history item in modal
+ */
+ const renderHistoryItem = ({ item }: { item: ChatSession }) => (
+ handleLoadHistory(item)}
+ onLongPress={() => handleDeleteHistory(item.id)}
+ >
+
+
+
+
+
+ {item.title}
+
+
+ {formatDate(item.updatedAt)} • {item.messages.length} messages
+
+
+
+
+ );
+
+ // =============================================================================
+ // Main Render
+ // =============================================================================
+
return (
- Journal
+ Flow
{dateStr}
-
-
- Serene
-
+
+ {/* History Button */}
+
+
+
- {/* Timeline */}
+ {/* Chat Messages */}
- {records.map((record, index) => {
- const emotionInfo = record.emotion ? emotionConfig[record.emotion] : null;
- const showDateHeader = index === 0 ||
- formatDate(record.createdAt) !== formatDate(records[index - 1].createdAt);
-
- return (
-
- {showDateHeader && (
-
-
- {formatDate(record.createdAt)}
-
-
- )}
-
-
-
- {formatTime(record.createdAt)}
-
-
- {emotionInfo && !record.isArchived && (
-
-
- {record.emotion}
-
- )}
- {record.type === 'voice' && !record.isArchived && (
-
- )}
-
-
-
-
- {record.isArchived
- ? `📦 ${record.archivedAt?.toLocaleDateString('en-US')} — ${record.content}`
- : record.content
- }
-
-
- {!record.isArchived && (
- handleSendToVault(record)}
- >
-
- Seal in Vault
-
- )}
-
+ {messages.length === 0 ? (
+ renderEmptyState()
+ ) : (
+ messages.map((message, index) => renderMessage(message, index))
+ )}
+
+ {/* Loading indicator when sending */}
+ {isSending && (
+
+
+
- );
- })}
-
+
+
+
+
+ )}
+
+
- {/* FAB */}
- setShowAddModal(true)}
- activeOpacity={0.9}
- >
-
-
-
-
+ {/* Bottom Input Bar */}
+
+
+ {/* Image attachment button */}
+
+
+
+
+ {/* Text Input */}
+
+
+
+
+ {/* Send or Voice button */}
+ {newContent.trim() || isSending ? (
+
+
+ {isSending ? (
+
+ ) : (
+
+ )}
+
+
+ ) : (
+
+
+
+ )}
+
+
- {/* Add Modal */}
- setShowAddModal(false)}>
-
-
+ {/* History Modal - Background appears instantly, content slides up */}
+
+
+
+ e.stopPropagation()}>
+
- New Entry
-
+ Chat History
+
+
+ New Chat
+
+
+
+ {/* History List */}
+ item.id}
+ style={styles.historyList}
+ ListEmptyComponent={
+
+
+ No chat history yet
+
+ }
/>
-
-
- {[
- { type: 'text', icon: 'type', label: 'Text' },
- { type: 'voice', icon: 'mic', label: 'Voice' },
- { type: 'image', icon: 'image', label: 'Photo' },
- ].map((item) => (
- setSelectedInputType(item.type as any)}
- >
-
- {item.label}
-
- ))}
-
-
-
- { setShowAddModal(false); setNewContent(''); }}>
- Cancel
-
-
-
-
- Save
-
-
-
+
+ {/* Close Button */}
+
+ Close
+
+
+
-
+
-
- {/* Archive Warning */}
- setShowArchiveWarning(false)}>
-
-
-
-
-
- Seal Entry?
-
- This entry will be encrypted and stored in the vault. The original will be replaced with a sealed record.
-
-
- setShowArchiveWarning(false)}>
- Cancel
-
-
-
- Seal
-
-
-
-
-
-
- { setShowBiometric(false); setSelectedRecord(null); }}
- title="Verify Identity"
- message="Authenticate to seal this entry"
- />
-
-
);
}
+// =============================================================================
+// Styles
+// =============================================================================
+
const styles = StyleSheet.create({
- container: { flex: 1 },
- gradient: { flex: 1 },
- safeArea: { flex: 1 },
+ // Container styles
+ container: {
+ flex: 1
+ },
+ gradient: {
+ flex: 1
+ },
+ safeArea: {
+ flex: 1
+ },
+
+ // Header styles
header: {
flexDirection: 'row',
justifyContent: 'space-between',
@@ -352,111 +722,179 @@ const styles = StyleSheet.create({
fontSize: typography.fontSize.sm,
color: colors.flow.textSecondary,
},
- moodBadge: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- backgroundColor: `${colors.nautical.gold}15`,
- paddingHorizontal: spacing.sm,
- paddingVertical: 6,
- borderRadius: borderRadius.full,
- },
- moodText: {
- fontSize: typography.fontSize.sm,
- fontWeight: '600',
- color: colors.nautical.gold,
- },
- timeline: { flex: 1 },
- timelineContent: { padding: spacing.base, paddingTop: 0 },
- dateHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- marginVertical: spacing.md,
- },
- dateLine: {
- flex: 1,
- height: 1,
- backgroundColor: colors.flow.cardBorder,
- },
- dateHeaderText: {
- fontSize: typography.fontSize.xs,
- fontWeight: '600',
- color: colors.flow.textSecondary,
- paddingHorizontal: spacing.md,
- },
- card: {
+ historyButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 14,
backgroundColor: colors.flow.cardBackground,
- borderRadius: borderRadius.xl,
- padding: spacing.base,
- marginBottom: spacing.sm,
+ justifyContent: 'center',
+ alignItems: 'center',
...shadows.soft,
},
- archivedCard: {
- backgroundColor: colors.flow.archived,
- shadowOpacity: 0,
+
+ // Messages container styles
+ messagesContainer: {
+ flex: 1
},
- cardHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
+ messagesContent: {
+ padding: spacing.base,
+ paddingTop: 0
+ },
+ emptyContent: {
+ flex: 1,
+ justifyContent: 'center',
+ },
+
+ // Empty state styles
+ emptyState: {
alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: spacing.xxl,
+ },
+ emptyIcon: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: colors.flow.cardBackground,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ ...shadows.soft,
+ },
+ emptyTitle: {
+ fontSize: typography.fontSize.lg,
+ fontWeight: '600',
+ color: colors.flow.text,
marginBottom: spacing.sm,
},
- cardTime: {
- fontSize: typography.fontSize.xs,
- color: colors.flow.textSecondary,
- fontWeight: '500',
- },
- archivedText: { color: colors.flow.archivedText },
- cardBadges: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: spacing.sm,
- },
- emotionBadge: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 3,
- paddingHorizontal: 8,
- paddingVertical: 3,
- borderRadius: borderRadius.full,
- },
- emotionText: {
- fontSize: 10,
- fontWeight: '600',
- },
- cardContent: {
+ emptySubtitle: {
fontSize: typography.fontSize.base,
- color: colors.flow.text,
- lineHeight: typography.fontSize.base * 1.6,
+ color: colors.flow.textSecondary,
+ textAlign: 'center',
},
- vaultButton: {
+
+ // Message bubble styles
+ messageBubble: {
flexDirection: 'row',
- alignItems: 'center',
+ marginBottom: spacing.md,
+ alignItems: 'flex-end',
+ },
+ userBubble: {
justifyContent: 'flex-end',
- gap: 4,
- marginTop: spacing.md,
+ },
+ aiBubble: {
+ justifyContent: 'flex-start',
+ },
+ aiAvatar: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: colors.flow.cardBackground,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: spacing.sm,
+ ...shadows.soft,
+ },
+ messageContent: {
+ maxWidth: '75%',
+ borderRadius: borderRadius.xl,
+ padding: spacing.md,
+ ...shadows.soft,
+ },
+ userContent: {
+ backgroundColor: colors.nautical.teal,
+ borderBottomRightRadius: 4,
+ marginLeft: 'auto',
+ },
+ aiContent: {
+ backgroundColor: colors.flow.cardBackground,
+ borderBottomLeftRadius: 4,
+ },
+ messageImage: {
+ width: '100%',
+ height: 200,
+ borderRadius: borderRadius.lg,
+ marginBottom: spacing.sm,
+ },
+ messageText: {
+ fontSize: typography.fontSize.base,
+ lineHeight: typography.fontSize.base * 1.5,
+ },
+ userText: {
+ color: '#fff',
+ },
+ aiText: {
+ color: colors.flow.text,
+ },
+ messageTime: {
+ fontSize: typography.fontSize.xs,
+ marginTop: spacing.xs,
+ textAlign: 'right',
+ },
+ userMessageTime: {
+ color: 'rgba(255, 255, 255, 0.7)',
+ },
+ aiMessageTime: {
+ color: colors.flow.textSecondary,
+ },
+
+ // Input bar styles
+ inputBarContainer: {
+ paddingHorizontal: spacing.base,
+ paddingBottom: spacing.lg,
paddingTop: spacing.sm,
- borderTopWidth: 1,
- borderTopColor: colors.flow.cardBorder,
+ backgroundColor: 'transparent',
},
- vaultButtonText: {
- fontSize: typography.fontSize.sm,
- color: colors.flow.primary,
- fontWeight: '600',
+ inputBar: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ backgroundColor: colors.flow.cardBackground,
+ borderRadius: borderRadius.xl,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ ...shadows.soft,
+ gap: spacing.xs,
},
- fab: {
- position: 'absolute',
- bottom: 100,
- right: spacing.base,
- ...shadows.medium,
- },
- fabGradient: {
- width: 56,
- height: 56,
- borderRadius: 18,
+ inputBarButton: {
+ width: 40,
+ height: 40,
+ borderRadius: borderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
},
+ recordingButton: {
+ backgroundColor: colors.nautical.coral,
+ },
+ inputWrapper: {
+ flex: 1,
+ position: 'relative',
+ justifyContent: 'center',
+ },
+ inputBarText: {
+ fontSize: typography.fontSize.base,
+ color: colors.flow.text,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.sm,
+ maxHeight: 100,
+ minHeight: 40,
+ },
+ sendButton: {
+ width: 40,
+ height: 40,
+ borderRadius: borderRadius.lg,
+ overflow: 'hidden',
+ },
+ sendButtonDisabled: {
+ opacity: 0.7,
+ },
+ sendButtonGradient: {
+ width: '100%',
+ height: '100%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
+ // Modal styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(26, 58, 74, 0.4)',
@@ -468,6 +906,7 @@ const styles = StyleSheet.create({
borderTopRightRadius: borderRadius.xxl,
padding: spacing.lg,
paddingBottom: spacing.xxl,
+ maxHeight: '80%',
},
modalHandle: {
width: 36,
@@ -477,149 +916,85 @@ const styles = StyleSheet.create({
alignSelf: 'center',
marginBottom: spacing.md,
},
+ modalHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: spacing.lg,
+ },
modalTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.flow.text,
- marginBottom: spacing.md,
},
- input: {
- backgroundColor: colors.nautical.paleAqua,
- borderRadius: borderRadius.lg,
- padding: spacing.base,
- fontSize: typography.fontSize.base,
- color: colors.flow.text,
- minHeight: 100,
- marginBottom: spacing.md,
- },
- inputTypeRow: {
- flexDirection: 'row',
- gap: spacing.sm,
- marginBottom: spacing.lg,
- },
- inputTypeButton: {
- flex: 1,
- alignItems: 'center',
- paddingVertical: spacing.md,
- borderRadius: borderRadius.lg,
- backgroundColor: colors.nautical.paleAqua,
- borderWidth: 2,
- borderColor: 'transparent',
- },
- inputTypeActive: {
- borderColor: colors.flow.primary,
- backgroundColor: colors.nautical.lightMint,
- },
- inputTypeLabel: {
- fontSize: typography.fontSize.xs,
- color: colors.flow.textSecondary,
- marginTop: 4,
- fontWeight: '500',
- },
- inputTypeLabelActive: {
- color: colors.flow.primary,
- fontWeight: '600',
- },
- modalButtons: {
- flexDirection: 'row',
- gap: spacing.md,
- },
- cancelButton: {
- flex: 1,
- paddingVertical: spacing.md,
- borderRadius: borderRadius.lg,
- backgroundColor: colors.nautical.paleAqua,
- alignItems: 'center',
- },
- cancelButtonText: {
- fontSize: typography.fontSize.base,
- color: colors.flow.textSecondary,
- fontWeight: '600',
- },
- confirmButton: {
- flex: 1,
- borderRadius: borderRadius.lg,
- overflow: 'hidden',
- },
- confirmButtonGradient: {
+ newChatButton: {
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: spacing.md,
- gap: spacing.sm,
- },
- confirmButtonText: {
- fontSize: typography.fontSize.base,
- color: '#fff',
- fontWeight: '600',
- },
- warningOverlay: {
- flex: 1,
- backgroundColor: 'rgba(26, 58, 74, 0.6)',
- justifyContent: 'center',
- alignItems: 'center',
- padding: spacing.lg,
- },
- warningModal: {
- backgroundColor: colors.flow.cardBackground,
- borderRadius: borderRadius.xxl,
- padding: spacing.xl,
- alignItems: 'center',
- width: '100%',
- ...shadows.medium,
- },
- warningIcon: {
- width: 72,
- height: 72,
- borderRadius: 24,
- backgroundColor: colors.nautical.lightMint,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: spacing.md,
- },
- warningTitle: {
- fontSize: typography.fontSize.lg,
- fontWeight: '700',
- color: colors.flow.text,
- marginBottom: spacing.sm,
- },
- warningText: {
- fontSize: typography.fontSize.base,
- color: colors.flow.textSecondary,
- textAlign: 'center',
- lineHeight: typography.fontSize.base * 1.5,
- marginBottom: spacing.lg,
- },
- warningButtons: {
- flexDirection: 'row',
- gap: spacing.md,
- width: '100%',
- },
- warningCancelButton: {
- flex: 1,
- paddingVertical: spacing.md,
- borderRadius: borderRadius.lg,
- backgroundColor: colors.nautical.paleAqua,
- alignItems: 'center',
- },
- warningCancelText: {
- fontSize: typography.fontSize.base,
- color: colors.flow.textSecondary,
- fontWeight: '600',
- },
- warningConfirmButton: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: spacing.md,
- borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.teal,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ borderRadius: borderRadius.lg,
gap: spacing.xs,
},
- warningConfirmText: {
- fontSize: typography.fontSize.base,
+ newChatText: {
+ fontSize: typography.fontSize.sm,
+ fontWeight: '600',
color: '#fff',
+ },
+
+ // History list styles
+ historyList: {
+ flex: 1,
+ },
+ historyItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: spacing.md,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.flow.cardBorder,
+ },
+ historyItemIcon: {
+ width: 40,
+ height: 40,
+ borderRadius: borderRadius.lg,
+ backgroundColor: colors.nautical.lightMint,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginRight: spacing.md,
+ },
+ historyItemContent: {
+ flex: 1,
+ },
+ historyItemTitle: {
+ fontSize: typography.fontSize.base,
+ fontWeight: '600',
+ color: colors.flow.text,
+ marginBottom: 2,
+ },
+ historyItemDate: {
+ fontSize: typography.fontSize.sm,
+ color: colors.flow.textSecondary,
+ },
+ historyEmpty: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: spacing.xxl,
+ },
+ historyEmptyText: {
+ fontSize: typography.fontSize.base,
+ color: colors.flow.textSecondary,
+ marginTop: spacing.md,
+ },
+ closeButton: {
+ paddingVertical: spacing.md,
+ borderRadius: borderRadius.lg,
+ backgroundColor: colors.nautical.paleAqua,
+ alignItems: 'center',
+ marginTop: spacing.md,
+ },
+ closeButtonText: {
+ fontSize: typography.fontSize.base,
+ color: colors.flow.textSecondary,
fontWeight: '600',
},
});
diff --git a/src/services/admin.service.ts b/src/services/admin.service.ts
new file mode 100644
index 0000000..db9f649
--- /dev/null
+++ b/src/services/admin.service.ts
@@ -0,0 +1,79 @@
+/**
+ * Admin Service
+ *
+ * Handles admin-only API operations.
+ */
+
+import {
+ NO_BACKEND_MODE,
+ API_ENDPOINTS,
+ MOCK_CONFIG,
+ buildApiUrl,
+ getApiHeaders,
+ logApiDebug,
+} from '../config';
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
+
+export interface DeclareGualeRequest {
+ username: string;
+}
+
+export interface DeclareGualeResponse {
+ message: string;
+ username: string;
+ guale: boolean;
+}
+
+// =============================================================================
+// Admin Service
+// =============================================================================
+
+export const adminService = {
+ /**
+ * Declare a user as deceased (guale)
+ * Admin only operation
+ * @param request - Username to declare as deceased
+ * @param token - JWT token for authentication (must be admin)
+ * @returns Success response
+ */
+ async declareGuale(request: DeclareGualeRequest, token: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Declare Guale', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ message: `User ${request.username} has been declared as deceased`,
+ username: request.username,
+ guale: true,
+ });
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.ADMIN.DECLARE_GUALE);
+ logApiDebug('Declare Guale URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify(request),
+ });
+
+ logApiDebug('Declare Guale Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to declare user as deceased');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Declare guale error:', error);
+ throw error;
+ }
+ },
+};
diff --git a/src/services/ai.service.ts b/src/services/ai.service.ts
new file mode 100644
index 0000000..bdd0636
--- /dev/null
+++ b/src/services/ai.service.ts
@@ -0,0 +1,243 @@
+/**
+ * AI Service
+ *
+ * Handles communication with the AI proxy endpoint for chat completions.
+ */
+
+import {
+ NO_BACKEND_MODE,
+ API_ENDPOINTS,
+ AI_CONFIG,
+ buildApiUrl,
+ getApiHeaders,
+ logApiDebug,
+} from '../config';
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
+
+export interface AIMessage {
+ role: 'system' | 'user' | 'assistant';
+ content: string;
+}
+
+export interface AIRequest {
+ messages: AIMessage[];
+ model?: string;
+}
+
+export interface AIResponse {
+ id: string;
+ object: string;
+ created: number;
+ model: string;
+ choices: Array<{
+ index: number;
+ message: AIMessage;
+ finish_reason: string;
+ }>;
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ };
+}
+
+// =============================================================================
+// Mock Response Generator
+// =============================================================================
+
+const createMockResponse = (userMessage: string): AIResponse => {
+ return {
+ id: `mock-${Date.now()}`,
+ object: 'chat.completion',
+ created: Math.floor(Date.now() / 1000),
+ model: 'mock-model',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: `I received your message: "${userMessage}". This is a mock response since the backend is not connected.`,
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ usage: {
+ prompt_tokens: 10,
+ completion_tokens: 20,
+ total_tokens: 30,
+ },
+ };
+};
+
+// =============================================================================
+// AI Service
+// =============================================================================
+
+export const aiService = {
+ /**
+ * Send chat messages to the AI proxy
+ * @param messages - Array of chat messages
+ * @param token - JWT token for authentication
+ * @returns AI response
+ */
+ async chat(messages: AIMessage[], token?: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('AI Chat', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
+ resolve(createMockResponse(lastUserMessage?.content || 'Hello'));
+ }, AI_CONFIG.MOCK_RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
+
+ logApiDebug('AI Request', {
+ url,
+ hasToken: !!token,
+ messageCount: messages.length,
+ });
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify({ messages } as AIRequest),
+ });
+
+ logApiDebug('AI Response Status', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ logApiDebug('AI Error Response', errorText);
+
+ let errorDetail = 'AI request failed';
+ try {
+ const errorData = JSON.parse(errorText);
+ errorDetail = errorData.detail || errorDetail;
+ } catch {
+ errorDetail = errorText || errorDetail;
+ }
+ throw new Error(`${response.status}: ${errorDetail}`);
+ }
+
+ const data = await response.json();
+ logApiDebug('AI Success', {
+ id: data.id,
+ model: data.model,
+ choicesCount: data.choices?.length,
+ });
+
+ return data;
+ } catch (error) {
+ console.error('AI proxy error:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Simple helper for single message chat
+ * @param content - User message content
+ * @param token - JWT token for authentication
+ * @returns AI response text
+ */
+ async sendMessage(content: string, token?: string): Promise {
+ const messages: AIMessage[] = [
+ {
+ role: 'system',
+ content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
+ },
+ {
+ role: 'user',
+ content,
+ },
+ ];
+
+ const response = await this.chat(messages, token);
+ return response.choices[0]?.message?.content || 'No response';
+ },
+
+ /**
+ * Send a message with an image to AI for analysis
+ * @param content - User message content
+ * @param imageBase64 - Base64 encoded image data
+ * @param token - JWT token for authentication
+ * @returns AI response text
+ */
+ async sendMessageWithImage(content: string, imageBase64: string, token?: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('AI Image Analysis', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve('This is a mock image analysis response. The image appears to show an interesting scene. In production, this would be analyzed by Gemini AI.');
+ }, AI_CONFIG.MOCK_RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
+
+ logApiDebug('AI Image Request', {
+ url,
+ hasToken: !!token,
+ hasImage: !!imageBase64,
+ });
+
+ // Gemini vision format - using multimodal content
+ const messages = [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: content,
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `data:image/jpeg;base64,${imageBase64}`,
+ },
+ },
+ ],
+ },
+ ];
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify({ messages }),
+ });
+
+ logApiDebug('AI Image Response Status', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ logApiDebug('AI Image Error Response', errorText);
+
+ let errorDetail = 'AI image request failed';
+ try {
+ const errorData = JSON.parse(errorText);
+ errorDetail = errorData.detail || errorDetail;
+ } catch {
+ errorDetail = errorText || errorDetail;
+ }
+ throw new Error(`${response.status}: ${errorDetail}`);
+ }
+
+ const data = await response.json();
+ logApiDebug('AI Image Success', {
+ id: data.id,
+ model: data.model,
+ });
+
+ return data.choices[0]?.message?.content || 'No response';
+ } catch (error) {
+ console.error('AI image proxy error:', error);
+ throw error;
+ }
+ },
+};
diff --git a/src/services/assets.service.ts b/src/services/assets.service.ts
new file mode 100644
index 0000000..84e30de
--- /dev/null
+++ b/src/services/assets.service.ts
@@ -0,0 +1,243 @@
+/**
+ * Assets Service
+ *
+ * Handles all asset-related API operations including CRUD and inheritance.
+ */
+
+import {
+ NO_BACKEND_MODE,
+ API_ENDPOINTS,
+ MOCK_CONFIG,
+ buildApiUrl,
+ getApiHeaders,
+ logApiDebug,
+} from '../config';
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
+
+export interface Asset {
+ id: number;
+ title: string;
+ author_id: number;
+ private_key_shard: string;
+ content_outer_encrypted: string;
+}
+
+export interface AssetCreate {
+ title: string;
+ private_key_shard: string;
+ content_inner_encrypted: string;
+}
+
+export interface AssetClaim {
+ asset_id: number;
+ private_key_shard: string;
+}
+
+export interface AssetClaimResponse {
+ asset_id: number;
+ title: string;
+ decrypted_content: string;
+ server_shard_key: string;
+}
+
+export interface AssetAssign {
+ asset_id: number;
+ heir_name: string;
+}
+
+// =============================================================================
+// Mock Data
+// =============================================================================
+
+const MOCK_ASSETS: Asset[] = [
+ {
+ id: 1,
+ title: 'Mock Asset 1',
+ author_id: MOCK_CONFIG.USER.id,
+ private_key_shard: 'mock_shard_1',
+ content_outer_encrypted: 'mock_encrypted_content_1',
+ },
+ {
+ id: 2,
+ title: 'Mock Asset 2',
+ author_id: MOCK_CONFIG.USER.id,
+ private_key_shard: 'mock_shard_2',
+ content_outer_encrypted: 'mock_encrypted_content_2',
+ },
+];
+
+// =============================================================================
+// Assets Service
+// =============================================================================
+
+export const assetsService = {
+ /**
+ * Get all assets for the current user
+ * @param token - JWT token for authentication
+ * @returns Array of user's assets
+ */
+ async getMyAssets(token: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Get Assets', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => resolve(MOCK_ASSETS), MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.ASSETS.GET);
+ logApiDebug('Get Assets URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: getApiHeaders(token),
+ });
+
+ logApiDebug('Get Assets Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to fetch assets');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Get assets error:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Create a new asset
+ * @param asset - Asset creation data
+ * @param token - JWT token for authentication
+ * @returns Created asset
+ */
+ async createAsset(asset: AssetCreate, token: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Create Asset', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ id: Date.now(),
+ title: asset.title,
+ author_id: MOCK_CONFIG.USER.id,
+ private_key_shard: asset.private_key_shard,
+ content_outer_encrypted: asset.content_inner_encrypted,
+ });
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.ASSETS.CREATE);
+ logApiDebug('Create Asset URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify(asset),
+ });
+
+ logApiDebug('Create Asset Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to create asset');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Create asset error:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Claim an inherited asset
+ * @param claim - Asset claim data
+ * @param token - JWT token for authentication
+ * @returns Claimed asset with decrypted content
+ */
+ async claimAsset(claim: AssetClaim, token: string): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Claim Asset', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ asset_id: claim.asset_id,
+ title: 'Mock Claimed Asset',
+ decrypted_content: 'This is the decrypted content of the claimed asset.',
+ server_shard_key: 'mock_server_shard',
+ });
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.ASSETS.CLAIM);
+ logApiDebug('Claim Asset URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify(claim),
+ });
+
+ logApiDebug('Claim Asset Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to claim asset');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Claim asset error:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Assign an asset to an heir
+ * @param assignment - Asset assignment data
+ * @param token - JWT token for authentication
+ * @returns Success message
+ */
+ async assignAsset(assignment: AssetAssign, token: string): Promise<{ message: string }> {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Assign Asset', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({ message: `Asset assigned to ${assignment.heir_name}` });
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.ASSETS.ASSIGN);
+ logApiDebug('Assign Asset URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(token),
+ body: JSON.stringify(assignment),
+ });
+
+ logApiDebug('Assign Asset Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Failed to assign asset');
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Assign asset error:', error);
+ throw error;
+ }
+ },
+};
diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts
index a1f221d..3621dc6 100644
--- a/src/services/auth.service.ts
+++ b/src/services/auth.service.ts
@@ -1,80 +1,87 @@
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
-
-const PLATFORM_URL = 'http://192.168.56.103:8000';
-const no_backend_mode = true;
-
-const MOCK_USER: User = {
- id: 999,
- username: 'MockCaptain',
- public_key: 'mock_public_key',
- is_admin: true,
- guale: false,
- tier: 'premium',
- tier_expires_at: '2026-12-31T23:59:59Z',
- last_active_at: new Date().toISOString(),
-};
+import {
+ NO_BACKEND_MODE,
+ API_ENDPOINTS,
+ MOCK_CONFIG,
+ buildApiUrl,
+ getApiHeaders,
+ logApiDebug,
+} from '../config';
export const authService = {
- async login(credentials: LoginRequest): Promise {
- if (no_backend_mode) {
- console.log('No-Backend Mode: Simulating login...');
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({
- access_token: 'mock_access_token',
- token_type: 'bearer',
- user: { ...MOCK_USER, username: credentials.username },
- });
- }, 200);
- });
- }
- try {
- const response = await fetch(`${PLATFORM_URL}/login`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(credentials),
- });
+ async login(credentials: LoginRequest): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Login', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({
+ access_token: MOCK_CONFIG.ACCESS_TOKEN,
+ token_type: 'bearer',
+ user: { ...MOCK_CONFIG.USER, username: credentials.username } as User,
+ });
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
- if (!response.ok) {
- throw new Error('Login failed');
- }
+ const url = buildApiUrl(API_ENDPOINTS.AUTH.LOGIN);
+ logApiDebug('Login URL', url);
- return await response.json();
- } catch (error) {
- console.error('Login error:', error);
- throw error;
- }
- },
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(),
+ body: JSON.stringify(credentials),
+ });
- async register(data: RegisterRequest): Promise {
- if (no_backend_mode) {
- console.log('No-Backend Mode: Simulating registration...');
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({ ...MOCK_USER, username: data.username });
- }, 200);
- });
- }
- try {
- const response = await fetch(`${PLATFORM_URL}/register`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(data),
- });
+ logApiDebug('Login Response Status', response.status);
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- throw new Error(errorData.detail?.[0]?.msg || 'Registration failed');
- }
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Login failed');
+ }
- return await response.json();
- } catch (error) {
- console.error('Registration error:', error);
- throw error;
- }
- },
+ const data = await response.json();
+ logApiDebug('Login Success', { username: data.user?.username });
+ return data;
+ } catch (error) {
+ console.error('Login error:', error);
+ throw error;
+ }
+ },
+
+ async register(data: RegisterRequest): Promise {
+ if (NO_BACKEND_MODE) {
+ logApiDebug('Register', 'Using mock mode');
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve({ ...MOCK_CONFIG.USER, username: data.username } as User);
+ }, MOCK_CONFIG.RESPONSE_DELAY);
+ });
+ }
+
+ const url = buildApiUrl(API_ENDPOINTS.AUTH.REGISTER);
+ logApiDebug('Register URL', url);
+
+ try {
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: getApiHeaders(),
+ body: JSON.stringify(data),
+ });
+
+ logApiDebug('Register Response Status', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail?.[0]?.msg || errorData.detail || 'Registration failed');
+ }
+
+ const result = await response.json();
+ logApiDebug('Register Success', { username: result.username });
+ return result;
+ } catch (error) {
+ console.error('Registration error:', error);
+ throw error;
+ }
+ },
};
diff --git a/src/services/index.ts b/src/services/index.ts
new file mode 100644
index 0000000..1eb48a7
--- /dev/null
+++ b/src/services/index.ts
@@ -0,0 +1,25 @@
+/**
+ * Services Index
+ *
+ * Central export for all API services.
+ * Import services from here for cleaner imports.
+ *
+ * Usage:
+ * import { authService, aiService, assetsService, adminService } from '../services';
+ */
+
+export { authService } from './auth.service';
+export { aiService, type AIMessage, type AIRequest, type AIResponse } from './ai.service';
+export {
+ assetsService,
+ type Asset,
+ type AssetCreate,
+ type AssetClaim,
+ type AssetClaimResponse,
+ type AssetAssign
+} from './assets.service';
+export {
+ adminService,
+ type DeclareGualeRequest,
+ type DeclareGualeResponse
+} from './admin.service';