From 0aab9a838bed47339a06747586eddd8f860eb3cf Mon Sep 17 00:00:00 2001 From: lusixing <32328454+lusixing@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:20:24 -0800 Subject: [PATCH] ai_role_update --- src/config/index.ts | 1 + src/context/AuthContext.tsx | 40 +++++++++++++++++- src/screens/FlowScreen.tsx | 83 ++++++++++++++++++++++++------------- src/services/ai.service.ts | 50 ++++++++++++++++++++++ src/types/index.ts | 10 +++++ 5 files changed, 154 insertions(+), 30 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 9d93b62..854d1a9 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -57,6 +57,7 @@ export const API_ENDPOINTS = { // AI Services AI: { PROXY: '/ai/proxy', + GET_ROLES: '/get_ai_roles', }, // Admin Operations diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 025d324..dcb32e4 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -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; signUp: (data: RegisterRequest) => Promise; signOut: () => void; + refreshAIRoles: () => Promise; } // Storage keys @@ -44,6 +47,7 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); + const [aiRoles, setAIRoles] = useState([]); 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} diff --git a/src/screens/FlowScreen.tsx b/src/screens/FlowScreen.tsx index 3332abb..a9d2271 100644 --- a/src/screens/FlowScreen.tsx +++ b/src/screens/FlowScreen.tsx @@ -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(null); // Current conversation state @@ -73,8 +74,8 @@ export default function FlowScreen() { const [isRecording, setIsRecording] = useState(false); const [selectedImage, setSelectedImage] = useState(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(aiRoles[0] || null); const [showRoleModal, setShowRoleModal] = useState(false); const [expandedRoleId, setExpandedRoleId] = useState(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() { - Chatting with {selectedRole.name} + Chatting with {selectedRole?.name || 'AI'} - {selectedRole.description} + {selectedRole?.description || 'Loading AI Assistant...'} ); @@ -707,13 +732,15 @@ export default function FlowScreen() { onPress={() => setShowRoleModal(true)} activeOpacity={0.7} > - + {selectedRole && ( + + )} - {selectedRole.name} + {selectedRole?.name || 'Loading...'} @@ -911,34 +938,34 @@ export default function FlowScreen() { Choose AI Assistant - {AI_CONFIG.ROLES.map((role) => ( + {aiRoles.map((role) => ( { - setSelectedRole(role as any); + setSelectedRole(role); setShowRoleModal(false); }} > {role.name} diff --git a/src/services/ai.service.ts b/src/services/ai.service.ts index 743c403..b6c0149 100644 --- a/src/services/ai.service.ts +++ b/src/services/ai.service.ts @@ -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 { + 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]; + } + }, }; diff --git a/src/types/index.ts b/src/types/index.ts index f334ca2..f187e2d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; +}