ai_role_update
This commit is contained in:
@@ -57,6 +57,7 @@ export const API_ENDPOINTS = {
|
||||
// AI Services
|
||||
AI: {
|
||||
PROXY: '/ai/proxy',
|
||||
GET_ROLES: '/get_ai_roles',
|
||||
},
|
||||
|
||||
// Admin Operations
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
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 { User, LoginRequest, RegisterRequest, AIRole } from '../types';
|
||||
import { authService } from '../services/auth.service';
|
||||
import { aiService } from '../services/ai.service';
|
||||
import { storageService } from '../services/storage.service';
|
||||
|
||||
// =============================================================================
|
||||
@@ -18,11 +19,13 @@ import { storageService } from '../services/storage.service';
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
aiRoles: AIRole[];
|
||||
isLoading: boolean;
|
||||
isInitializing: boolean;
|
||||
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||
signUp: (data: RegisterRequest) => Promise<void>;
|
||||
signOut: () => void;
|
||||
refreshAIRoles: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Storage keys
|
||||
@@ -44,6 +47,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [aiRoles, setAIRoles] = useState<AIRole[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
|
||||
@@ -66,6 +70,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setToken(storedToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
||||
// Fetch AI roles after restoring session
|
||||
fetchAIRoles(storedToken);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Auth] Failed to load stored auth:', error);
|
||||
@@ -74,6 +80,29 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch AI roles from API
|
||||
*/
|
||||
const fetchAIRoles = async (authToken: string) => {
|
||||
console.log('[Auth] Fetching AI roles with token:', authToken ? `${authToken.substring(0, 10)}...` : 'MISSING');
|
||||
try {
|
||||
const roles = await aiService.getAIRoles(authToken);
|
||||
setAIRoles(roles);
|
||||
console.log('[Auth] AI roles fetched successfully:', roles.length);
|
||||
} catch (error) {
|
||||
console.error('[Auth] Failed to fetch AI roles:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manual refresh of AI roles
|
||||
*/
|
||||
const refreshAIRoles = async () => {
|
||||
if (token) {
|
||||
await fetchAIRoles(token);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save authentication to AsyncStorage
|
||||
*/
|
||||
@@ -114,6 +143,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
setToken(response.access_token);
|
||||
setUser(response.user);
|
||||
await saveAuth(response.access_token, response.user);
|
||||
// Fetch AI roles immediately after login
|
||||
await fetchAIRoles(response.access_token);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
@@ -143,7 +174,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const signOut = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
setAIRoles([]);
|
||||
clearAuth();
|
||||
|
||||
|
||||
//storageService.clearAllData();
|
||||
};
|
||||
|
||||
@@ -152,11 +186,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
value={{
|
||||
user,
|
||||
token,
|
||||
aiRoles,
|
||||
isLoading,
|
||||
isInitializing,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut
|
||||
signOut,
|
||||
refreshAIRoles
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { AIRole } from '../types';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { aiService, AIMessage } from '../services/ai.service';
|
||||
import { assetsService } from '../services/assets.service';
|
||||
@@ -63,7 +64,7 @@ interface ChatSession {
|
||||
// =============================================================================
|
||||
|
||||
export default function FlowScreen() {
|
||||
const { token, user, signOut } = useAuth();
|
||||
const { token, user, signOut, aiRoles, refreshAIRoles } = useAuth();
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
// Current conversation state
|
||||
@@ -73,8 +74,8 @@ export default function FlowScreen() {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||
|
||||
// AI Role state
|
||||
const [selectedRole, setSelectedRole] = useState(AI_CONFIG.ROLES[0]);
|
||||
// AI Role state - start with null to detect first load
|
||||
const [selectedRole, setSelectedRole] = useState<AIRole | null>(aiRoles[0] || null);
|
||||
const [showRoleModal, setShowRoleModal] = useState(false);
|
||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||
|
||||
@@ -159,9 +160,9 @@ export default function FlowScreen() {
|
||||
// Load messages whenever role changes
|
||||
useEffect(() => {
|
||||
const loadRoleMessages = async () => {
|
||||
if (!user) return;
|
||||
if (!user || !selectedRole) return;
|
||||
try {
|
||||
const savedMessages = await storageService.getCurrentChat(selectedRole.id, user.id);
|
||||
const savedMessages = await storageService.getCurrentChat(selectedRole?.id || '', user.id);
|
||||
if (savedMessages) {
|
||||
const formattedMessages = savedMessages.map((msg: any) => ({
|
||||
...msg,
|
||||
@@ -172,18 +173,42 @@ export default function FlowScreen() {
|
||||
setMessages([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load messages for role ${selectedRole.id}:`, error);
|
||||
if (selectedRole) {
|
||||
console.error(`Failed to load messages for role ${selectedRole?.id}:`, error);
|
||||
}
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadRoleMessages();
|
||||
}, [selectedRole.id, user]);
|
||||
}, [selectedRole?.id, user]);
|
||||
|
||||
// Ensure we have a valid selected role from the dynamic list
|
||||
useEffect(() => {
|
||||
if (aiRoles.length > 0) {
|
||||
if (!selectedRole) {
|
||||
// Initial load or first time roles become available
|
||||
setSelectedRole(aiRoles[0]);
|
||||
} else {
|
||||
// If roles refreshed, make sure current selectedRole is still valid or updated
|
||||
const updatedRole = aiRoles.find(r => r.id === selectedRole?.id);
|
||||
if (updatedRole) {
|
||||
setSelectedRole(updatedRole);
|
||||
} else {
|
||||
// Current role no longer exists in dynamic list, fallback to first
|
||||
setSelectedRole(aiRoles[0]);
|
||||
}
|
||||
}
|
||||
} else if (!selectedRole) {
|
||||
// Fallback if no dynamic roles yet
|
||||
setSelectedRole(AI_CONFIG.ROLES[0]);
|
||||
}
|
||||
}, [aiRoles]);
|
||||
|
||||
// Save current messages for the active role when they change
|
||||
useEffect(() => {
|
||||
if (user && messages.length >= 0) { // Save even if empty to allow clearing
|
||||
storageService.saveCurrentChat(selectedRole.id, messages, user.id);
|
||||
if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing
|
||||
storageService.saveCurrentChat(selectedRole?.id || '', messages, user.id);
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
@@ -191,7 +216,7 @@ export default function FlowScreen() {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
}
|
||||
}, [messages, selectedRole.id, user]);
|
||||
}, [messages, selectedRole?.id, user]);
|
||||
|
||||
// Save history when it changes
|
||||
useEffect(() => {
|
||||
@@ -229,7 +254,7 @@ export default function FlowScreen() {
|
||||
* Handle sending a message to AI
|
||||
*/
|
||||
const handleSendMessage = async () => {
|
||||
if (!newContent.trim() || isSending) return;
|
||||
if (!newContent.trim() || isSending || !selectedRole) return;
|
||||
|
||||
// Check authentication
|
||||
if (!token) {
|
||||
@@ -256,7 +281,7 @@ export default function FlowScreen() {
|
||||
|
||||
try {
|
||||
// Call AI proxy with selected role's system prompt
|
||||
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole.systemPrompt);
|
||||
const aiResponse = await aiService.sendMessage(userMessage, token, selectedRole?.systemPrompt || '');
|
||||
|
||||
// Add AI response
|
||||
const aiMsg: ChatMessage = {
|
||||
@@ -424,8 +449,8 @@ export default function FlowScreen() {
|
||||
|
||||
// Clear current messages and storage for this role
|
||||
setMessages([]);
|
||||
if (user) {
|
||||
storageService.saveCurrentChat(selectedRole.id, [], user.id);
|
||||
if (user && selectedRole) {
|
||||
storageService.saveCurrentChat(selectedRole?.id || '', [], user.id);
|
||||
}
|
||||
closeHistoryModal();
|
||||
};
|
||||
@@ -647,9 +672,9 @@ export default function FlowScreen() {
|
||||
<View style={styles.emptyIcon}>
|
||||
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
|
||||
<Text style={styles.emptyTitle}>Chatting with {selectedRole?.name || 'AI'}</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
{selectedRole.description}
|
||||
{selectedRole?.description || 'Loading AI Assistant...'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -707,13 +732,15 @@ export default function FlowScreen() {
|
||||
onPress={() => setShowRoleModal(true)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons
|
||||
name={selectedRole.icon as any}
|
||||
size={16}
|
||||
color={colors.nautical.teal}
|
||||
/>
|
||||
{selectedRole && (
|
||||
<Ionicons
|
||||
name={(selectedRole?.icon || 'help-outline') as any}
|
||||
size={16}
|
||||
color={colors.nautical.teal}
|
||||
/>
|
||||
)}
|
||||
<Text style={styles.headerRoleText} numberOfLines={1}>
|
||||
{selectedRole.name}
|
||||
{selectedRole?.name || 'Loading...'}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
@@ -911,34 +938,34 @@ export default function FlowScreen() {
|
||||
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
||||
|
||||
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
||||
{AI_CONFIG.ROLES.map((role) => (
|
||||
{aiRoles.map((role) => (
|
||||
<View key={role.id} style={styles.roleItemContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.roleItem,
|
||||
selectedRole.id === role.id && styles.roleItemActive
|
||||
selectedRole?.id === role.id && styles.roleItemActive
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.roleSelectionArea}
|
||||
onPress={() => {
|
||||
setSelectedRole(role as any);
|
||||
setSelectedRole(role);
|
||||
setShowRoleModal(false);
|
||||
}}
|
||||
>
|
||||
<View style={[
|
||||
styles.roleItemIcon,
|
||||
selectedRole.id === role.id && styles.roleItemIconActive
|
||||
selectedRole?.id === role.id && styles.roleItemIconActive
|
||||
]}>
|
||||
<Ionicons
|
||||
name={role.icon as any}
|
||||
size={20}
|
||||
color={selectedRole.id === role.id ? '#fff' : colors.nautical.teal}
|
||||
color={selectedRole?.id === role.id ? '#fff' : colors.nautical.teal}
|
||||
/>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.roleItemName,
|
||||
selectedRole.id === role.id && styles.roleItemNameActive
|
||||
selectedRole?.id === role.id && styles.roleItemNameActive
|
||||
]}>
|
||||
{role.name}
|
||||
</Text>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getApiHeaders,
|
||||
logApiDebug,
|
||||
} from '../config';
|
||||
import { AIRole } from '../types';
|
||||
|
||||
// =============================================================================
|
||||
// Type Definitions
|
||||
@@ -271,4 +272,53 @@ export const aiService = {
|
||||
const response = await this.chat([...historicalMessages, summaryPrompt], token);
|
||||
return response.choices[0]?.message?.content || 'No summary generated';
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch available AI roles from backend
|
||||
* @param token - Optional JWT token for authentication
|
||||
* @returns Array of AI roles
|
||||
*/
|
||||
async getAIRoles(token?: string): Promise<AIRole[]> {
|
||||
if (NO_BACKEND_MODE) {
|
||||
logApiDebug('AI Roles', 'Using mock roles');
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.warn('[AI Service] getAIRoles called without token, falling back to static roles');
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
const url = buildApiUrl(API_ENDPOINTS.AI.GET_ROLES);
|
||||
const headers = getApiHeaders(token);
|
||||
|
||||
logApiDebug('AI Roles Request', {
|
||||
url,
|
||||
hasToken: !!token,
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: headers.Authorization ? `${headers.Authorization.substring(0, 15)}...` : 'MISSING'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[AI Service] Failed to fetch AI roles: ${response.status}. Falling back to static roles.`);
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
logApiDebug('AI Roles Success', { count: data.length });
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[AI Service] Fetch AI roles error:', error);
|
||||
// Fallback to config roles if API fails for better UX
|
||||
return [...AI_CONFIG.ROLES];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -104,3 +104,13 @@ export interface LoginResponse {
|
||||
token_type: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
// AI Types
|
||||
export interface AIRole {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
systemPrompt: string;
|
||||
icon: string;
|
||||
iconFamily: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user