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 Services
AI: { AI: {
PROXY: '/ai/proxy', PROXY: '/ai/proxy',
GET_ROLES: '/get_ai_roles',
}, },
// Admin Operations // Admin Operations

View File

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

View File

@@ -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}
> >
{selectedRole && (
<Ionicons <Ionicons
name={selectedRole.icon as any} name={(selectedRole?.icon || 'help-outline') as any}
size={16} size={16}
color={colors.nautical.teal} 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>

View File

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

View File

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