ai_role_update
This commit is contained in:
@@ -57,6 +57,7 @@ export const API_ENDPOINTS = {
|
|||||||
// AI Services
|
// AI Services
|
||||||
AI: {
|
AI: {
|
||||||
PROXY: '/ai/proxy',
|
PROXY: '/ai/proxy',
|
||||||
|
GET_ROLES: '/get_ai_roles',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Admin Operations
|
// Admin Operations
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
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 { authService } from '../services/auth.service';
|
||||||
|
import { aiService } from '../services/ai.service';
|
||||||
import { storageService } from '../services/storage.service';
|
import { storageService } from '../services/storage.service';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -18,11 +19,13 @@ import { storageService } from '../services/storage.service';
|
|||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
aiRoles: AIRole[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
signIn: (credentials: LoginRequest) => Promise<void>;
|
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||||
signUp: (data: RegisterRequest) => Promise<void>;
|
signUp: (data: RegisterRequest) => Promise<void>;
|
||||||
signOut: () => void;
|
signOut: () => void;
|
||||||
|
refreshAIRoles: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage keys
|
// Storage keys
|
||||||
@@ -44,6 +47,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(null);
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [aiRoles, setAIRoles] = useState<AIRole[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isInitializing, setIsInitializing] = useState(true);
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
|
||||||
@@ -66,6 +70,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
setUser(JSON.parse(storedUser));
|
setUser(JSON.parse(storedUser));
|
||||||
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
||||||
|
// Fetch AI roles after restoring session
|
||||||
|
fetchAIRoles(storedToken);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Auth] Failed to load stored auth:', 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
|
* Save authentication to AsyncStorage
|
||||||
*/
|
*/
|
||||||
@@ -114,6 +143,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setToken(response.access_token);
|
setToken(response.access_token);
|
||||||
setUser(response.user);
|
setUser(response.user);
|
||||||
await saveAuth(response.access_token, response.user);
|
await saveAuth(response.access_token, response.user);
|
||||||
|
// Fetch AI roles immediately after login
|
||||||
|
await fetchAIRoles(response.access_token);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -143,7 +174,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const signOut = () => {
|
const signOut = () => {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
setAIRoles([]);
|
||||||
clearAuth();
|
clearAuth();
|
||||||
|
|
||||||
|
|
||||||
//storageService.clearAllData();
|
//storageService.clearAllData();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,11 +186,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
value={{
|
value={{
|
||||||
user,
|
user,
|
||||||
token,
|
token,
|
||||||
|
aiRoles,
|
||||||
isLoading,
|
isLoading,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
signIn,
|
signIn,
|
||||||
signUp,
|
signUp,
|
||||||
signOut
|
signOut,
|
||||||
|
refreshAIRoles
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, FontAwesome5 } from '@expo/vector-icons';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
|
import { AIRole } from '../types';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||||
import { aiService, AIMessage } from '../services/ai.service';
|
import { aiService, AIMessage } from '../services/ai.service';
|
||||||
import { assetsService } from '../services/assets.service';
|
import { assetsService } from '../services/assets.service';
|
||||||
@@ -63,7 +64,7 @@ interface ChatSession {
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export default function FlowScreen() {
|
export default function FlowScreen() {
|
||||||
const { token, user, signOut } = useAuth();
|
const { token, user, signOut, aiRoles, refreshAIRoles } = useAuth();
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
|
||||||
// Current conversation state
|
// Current conversation state
|
||||||
@@ -73,8 +74,8 @@ export default function FlowScreen() {
|
|||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
// AI Role state
|
// AI Role state - start with null to detect first load
|
||||||
const [selectedRole, setSelectedRole] = useState(AI_CONFIG.ROLES[0]);
|
const [selectedRole, setSelectedRole] = useState<AIRole | null>(aiRoles[0] || null);
|
||||||
const [showRoleModal, setShowRoleModal] = useState(false);
|
const [showRoleModal, setShowRoleModal] = useState(false);
|
||||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -159,9 +160,9 @@ export default function FlowScreen() {
|
|||||||
// Load messages whenever role changes
|
// Load messages whenever role changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRoleMessages = async () => {
|
const loadRoleMessages = async () => {
|
||||||
if (!user) return;
|
if (!user || !selectedRole) return;
|
||||||
try {
|
try {
|
||||||
const savedMessages = await storageService.getCurrentChat(selectedRole.id, user.id);
|
const savedMessages = await storageService.getCurrentChat(selectedRole?.id || '', user.id);
|
||||||
if (savedMessages) {
|
if (savedMessages) {
|
||||||
const formattedMessages = savedMessages.map((msg: any) => ({
|
const formattedMessages = savedMessages.map((msg: any) => ({
|
||||||
...msg,
|
...msg,
|
||||||
@@ -172,18 +173,42 @@ export default function FlowScreen() {
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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([]);
|
setMessages([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadRoleMessages();
|
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
|
// Save current messages for the active role when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user && messages.length >= 0) { // Save even if empty to allow clearing
|
if (user && selectedRole && messages.length >= 0) { // Save even if empty to allow clearing
|
||||||
storageService.saveCurrentChat(selectedRole.id, messages, user.id);
|
storageService.saveCurrentChat(selectedRole?.id || '', messages, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
@@ -191,7 +216,7 @@ export default function FlowScreen() {
|
|||||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}, [messages, selectedRole.id, user]);
|
}, [messages, selectedRole?.id, user]);
|
||||||
|
|
||||||
// Save history when it changes
|
// Save history when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -229,7 +254,7 @@ export default function FlowScreen() {
|
|||||||
* Handle sending a message to AI
|
* Handle sending a message to AI
|
||||||
*/
|
*/
|
||||||
const handleSendMessage = async () => {
|
const handleSendMessage = async () => {
|
||||||
if (!newContent.trim() || isSending) return;
|
if (!newContent.trim() || isSending || !selectedRole) return;
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -256,7 +281,7 @@ export default function FlowScreen() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Call AI proxy with selected role's system prompt
|
// 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
|
// Add AI response
|
||||||
const aiMsg: ChatMessage = {
|
const aiMsg: ChatMessage = {
|
||||||
@@ -424,8 +449,8 @@ export default function FlowScreen() {
|
|||||||
|
|
||||||
// Clear current messages and storage for this role
|
// Clear current messages and storage for this role
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
if (user) {
|
if (user && selectedRole) {
|
||||||
storageService.saveCurrentChat(selectedRole.id, [], user.id);
|
storageService.saveCurrentChat(selectedRole?.id || '', [], user.id);
|
||||||
}
|
}
|
||||||
closeHistoryModal();
|
closeHistoryModal();
|
||||||
};
|
};
|
||||||
@@ -647,9 +672,9 @@ export default function FlowScreen() {
|
|||||||
<View style={styles.emptyIcon}>
|
<View style={styles.emptyIcon}>
|
||||||
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
<Feather name="feather" size={48} color={colors.nautical.seafoam} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.emptyTitle}>Chatting with {selectedRole.name}</Text>
|
<Text style={styles.emptyTitle}>Chatting with {selectedRole?.name || 'AI'}</Text>
|
||||||
<Text style={styles.emptySubtitle}>
|
<Text style={styles.emptySubtitle}>
|
||||||
{selectedRole.description}
|
{selectedRole?.description || 'Loading AI Assistant...'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -707,13 +732,15 @@ export default function FlowScreen() {
|
|||||||
onPress={() => setShowRoleModal(true)}
|
onPress={() => setShowRoleModal(true)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Ionicons
|
{selectedRole && (
|
||||||
name={selectedRole.icon as any}
|
<Ionicons
|
||||||
size={16}
|
name={(selectedRole?.icon || 'help-outline') as any}
|
||||||
color={colors.nautical.teal}
|
size={16}
|
||||||
/>
|
color={colors.nautical.teal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Text style={styles.headerRoleText} numberOfLines={1}>
|
<Text style={styles.headerRoleText} numberOfLines={1}>
|
||||||
{selectedRole.name}
|
{selectedRole?.name || 'Loading...'}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
<Ionicons name="chevron-down" size={14} color={colors.flow.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -911,34 +938,34 @@ export default function FlowScreen() {
|
|||||||
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
<Text style={styles.modalTitle}>Choose AI Assistant</Text>
|
||||||
|
|
||||||
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.roleList} showsVerticalScrollIndicator={false}>
|
||||||
{AI_CONFIG.ROLES.map((role) => (
|
{aiRoles.map((role) => (
|
||||||
<View key={role.id} style={styles.roleItemContainer}>
|
<View key={role.id} style={styles.roleItemContainer}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.roleItem,
|
styles.roleItem,
|
||||||
selectedRole.id === role.id && styles.roleItemActive
|
selectedRole?.id === role.id && styles.roleItemActive
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.roleSelectionArea}
|
style={styles.roleSelectionArea}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedRole(role as any);
|
setSelectedRole(role);
|
||||||
setShowRoleModal(false);
|
setShowRoleModal(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.roleItemIcon,
|
styles.roleItemIcon,
|
||||||
selectedRole.id === role.id && styles.roleItemIconActive
|
selectedRole?.id === role.id && styles.roleItemIconActive
|
||||||
]}>
|
]}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={role.icon as any}
|
name={role.icon as any}
|
||||||
size={20}
|
size={20}
|
||||||
color={selectedRole.id === role.id ? '#fff' : colors.nautical.teal}
|
color={selectedRole?.id === role.id ? '#fff' : colors.nautical.teal}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.roleItemName,
|
styles.roleItemName,
|
||||||
selectedRole.id === role.id && styles.roleItemNameActive
|
selectedRole?.id === role.id && styles.roleItemNameActive
|
||||||
]}>
|
]}>
|
||||||
{role.name}
|
{role.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getApiHeaders,
|
getApiHeaders,
|
||||||
logApiDebug,
|
logApiDebug,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import { AIRole } from '../types';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Type Definitions
|
// Type Definitions
|
||||||
@@ -271,4 +272,53 @@ export const aiService = {
|
|||||||
const response = await this.chat([...historicalMessages, summaryPrompt], token);
|
const response = await this.chat([...historicalMessages, summaryPrompt], token);
|
||||||
return response.choices[0]?.message?.content || 'No summary generated';
|
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;
|
token_type: string;
|
||||||
user: User;
|
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