ai_role_update

This commit is contained in:
lusixing
2026-02-02 22:20:24 -08:00
parent 6822638d47
commit 0aab9a838b
5 changed files with 154 additions and 30 deletions

View File

@@ -57,6 +57,7 @@ export const API_ENDPOINTS = {
// AI Services
AI: {
PROXY: '/ai/proxy',
GET_ROLES: '/get_ai_roles',
},
// Admin Operations

View File

@@ -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}

View File

@@ -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}
>
{selectedRole && (
<Ionicons
name={selectedRole.icon as any}
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>

View File

@@ -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];
}
},
};

View File

@@ -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;
}