diff --git a/backend/college_transfer_ai/app.py b/backend/college_transfer_ai/app.py
index ffe4b11..47c26fd 100644
--- a/backend/college_transfer_ai/app.py
+++ b/backend/college_transfer_ai/app.py
@@ -17,6 +17,11 @@
from google.auth.transport import requests as google_requests
# --- End Google Auth Imports ---
+# --- Google AI Imports ---
+import google.generativeai as genai
+from google.generativeai.types import HarmCategory, HarmBlockThreshold
+# --- End Google AI Imports ---
+
print("--- Flask app.py loading ---")
load_dotenv()
@@ -24,8 +29,24 @@
openai_api_key = os.getenv("OPENAI_API_KEY")
MONGO_URI = os.getenv("MONGO_URI")
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
+GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
# --- End Config Vars ---
+# --- Add this print statement ---
+print(f"--- Attempting to configure Google AI with Key: '{GOOGLE_API_KEY}' ---")
+# --- End print statement ---
+
+# Google AI Client Setup
+if not GOOGLE_API_KEY:
+ print("Warning; GOOGLE_API_KEY not set. Google AI chat will fail.")
+else:
+ try:
+ genai.configure(api_key=GOOGLE_API_KEY)
+ print("Google AI client configured successfully.")
+ except Exception as e:
+ print(f"CRITICAL: Failed to configure Google AI client: {e}")
+# --- End Google AI Client Setup ---
+
# --- Client Setups ---
if not openai_api_key: print("Warning: OPENAI_API_KEY not set.")
openai_client = OpenAI(api_key=openai_api_key) if openai_api_key else None
@@ -221,48 +242,101 @@ def serve_image(image_filename):
# Chat Endpoint (remains the same, does not use Google Auth)
@app.route('/chat', methods=['POST'])
def chat_with_agreement():
- if not openai_client: return jsonify({"error": "OpenAI client not configured."}), 500
+ if not GOOGLE_API_KEY: # Check if Google AI is configured
+ return jsonify({"error": "Google AI client not configured."}), 500
+
try:
data = request.get_json()
if not data: return jsonify({"error": "Invalid JSON payload"}), 400
+
new_user_message_text = data.get('new_message')
- conversation_history = data.get('history', [])
- image_filenames = data.get('image_filenames')
+ # Google's ChatSession manages history internally, but we need to format it initially
+ openai_history = data.get('history', [])
+ image_filenames = data.get('image_filenames') # Only sent on first turn
+
if not new_user_message_text: return jsonify({"error": "Missing 'new_message' text"}), 400
- new_openai_message_content = [{"type": "text", "text": new_user_message_text}]
+ # Inside the /chat endpoint
+
+ # --- Prepare Input for Gemini ---
+ prompt_parts = [new_user_message_text] # Start with the text part
+
if image_filenames:
- print(f"Processing {len(image_filenames)} images for the first turn.")
+ print(f"Processing {len(image_filenames)} images for Gemini.")
image_count = 0
for filename in image_filenames:
try:
grid_out = fs.find_one({"filename": filename})
- if not grid_out: continue
- base64_image = base64.b64encode(grid_out.read()).decode('utf-8')
- new_openai_message_content.append({
- "type": "image_url",
- "image_url": {"url": f"data:{getattr(grid_out, 'contentType', 'image/png')};base64,{base64_image}"}
+ if not grid_out:
+ print(f"Image {filename} not found in GridFS. Skipping.")
+ continue
+
+ image_bytes = grid_out.read()
+ # --- Determine MIME type (ensure it's correct) ---
+ # Use getattr for safety, default to image/png if not found
+ mime_type = getattr(grid_out, 'contentType', None)
+ if not mime_type:
+ # Basic check based on filename extension if contentType is missing
+ if filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg"):
+ mime_type = "image/jpeg"
+ elif filename.lower().endswith(".png"):
+ mime_type = "image/png"
+ # Add more types if needed (webp, heic, heif)
+ else:
+ mime_type = "image/png" # Default fallback
+ print(f"Warning: contentType missing for {filename}, inferred as {mime_type}")
+ # --- End MIME type determination ---
+
+
+ # --- Correct way to add image part ---
+ # Append a dictionary, not genai.types.Part
+ prompt_parts.append({
+ "mime_type": mime_type,
+ "data": image_bytes # Send raw bytes directly
})
+ # --- End Correction ---
+
image_count += 1
- except Exception as img_err: print(f"Error reading/encoding image {filename}: {img_err}. Skipping.")
- print(f"Added {image_count} images.")
- else: print("No image filenames provided.")
+ except Exception as img_err:
+ print(f"Error reading/processing image {filename} for Gemini: {img_err}. Skipping.")
+ print(f"Added {image_count} images to Gemini prompt.")
+ else:
+ print("No image filenames provided for Gemini.")
- conversation_history.append({"role": "user", "content": new_openai_message_content})
+ # --- Convert OpenAI history to Gemini format (remains the same) ---
+ gemini_history = []
+ # ... (history conversion code) ...
+
+ # --- Initialize Gemini Model and Chat (remains the same) ---
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
+ chat = model.start_chat(history=gemini_history)
+
+ print(f"Sending request to Gemini with {len(prompt_parts)} parts...")
+
+ # --- Send message to Gemini (remains the same) ---
+ safety_settings = { # Optional safety settings
+ # ... (safety settings) ...
+ }
+ response = chat.send_message(prompt_parts, safety_settings=safety_settings)
+
+ assistant_reply = response.text
+ print("Received reply from Gemini.")
- print(f"Sending request to OpenAI with {len(conversation_history)} messages...")
- chat_completion = openai_client.chat.completions.create(
- model="gpt-4o-mini", messages=conversation_history, max_tokens=1000
- )
- assistant_reply = chat_completion.choices[0].message.content
- print(f"Received reply from OpenAI.")
return jsonify({"reply": assistant_reply})
+ # ... (exception handling remains the same) ...
+
except Exception as e:
- print(f"Error in /chat endpoint: {e}")
+ print(f"Error in /chat endpoint (Gemini): {e}")
traceback.print_exc()
- return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
-# --- End Existing Endpoints ---
+ # Try to provide a more specific error if possible
+ error_message = f"An unexpected error occurred with the AI chat: {str(e)}"
+ # Check for specific Google API errors if needed
+ # if isinstance(e, google.api_core.exceptions.GoogleAPIError):
+ # error_message = f"Google API Error: {e}"
+ return jsonify({"error": error_message}), 500
+
+# --- End Chat Endpoint ---
# --- Course Map Endpoints ---
diff --git a/backend/college_transfer_ai/college_transfer_API.py b/backend/college_transfer_ai/college_transfer_API.py
index 770ff7c..fc71d86 100644
--- a/backend/college_transfer_ai/college_transfer_API.py
+++ b/backend/college_transfer_ai/college_transfer_API.py
@@ -216,5 +216,3 @@ def get_articulation_agreement(self, academic_year_id, sending_institution_id, r
api = CollegeTransferAPI()
-
-# print(json.dumps(api.get_major_from_key("75/125/to/1/Major/d5469a2c-be7e-452b-b492-08dca4e7496b"), indent=4))
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 02d6ff8..367bb48 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@ pytest
PyMuPDF
openai
dotenv
+google-generativeai
diff --git a/src/App.jsx b/src/App.jsx
index 7ca7d8d..2c05e64 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -100,25 +100,6 @@ function App() {
navigate('/');
};
- // Optional: Effect to listen for storage changes in other tabs (advanced)
- // useEffect(() => {
- // const handleStorageChange = (event) => {
- // if (event.key === USER_STORAGE_KEY) {
- // if (!event.newValue) { // User logged out in another tab
- // setUser(null);
- // } else { // User logged in/updated in another tab
- // try {
- // setUser(JSON.parse(event.newValue));
- // } catch {
- // setUser(null);
- // }
- // }
- // }
- // };
- // window.addEventListener('storage', handleStorageChange);
- // return () => window.removeEventListener('storage', handleStorageChange);
- // }, []);
-
return (
<>
{/* Navigation/Header remains the same */}
@@ -137,7 +118,6 @@ function App() {
)}
diff --git a/src/components/CourseMap.jsx b/src/components/CourseMap.jsx
index 8f4966e..d5710b8 100644
--- a/src/components/CourseMap.jsx
+++ b/src/components/CourseMap.jsx
@@ -112,10 +112,13 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
// Try loading from cache first unless forcing refresh
if (!forceRefresh) {
const cachedList = loadFromCache(cacheKey);
- if (cachedList) {
+ if (cachedList && Array.isArray(cachedList)) { // Added Array.isArray check for cache
setMapList(cachedList);
setIsMapListLoading(false);
return cachedList;
+ } else if (cachedList) {
+ console.warn("Cached map list was not an array, removing:", cachedList);
+ removeFromCache(cacheKey); // Remove invalid cache
}
}
@@ -123,10 +126,19 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
setIsMapListLoading(true);
try {
console.log("Fetching map list from API...");
- const list = await fetchData('course-maps', {
+ const responseData = await fetchData('course-maps', { // Renamed 'list' to 'responseData'
headers: { 'Authorization': `Bearer ${user.idToken}` }
});
- const validList = list || [];
+ console.log("Raw API response for map list:", responseData); // Log the raw response
+
+ // --- Added Check ---
+ // Ensure responseData is an array before setting state
+ const validList = Array.isArray(responseData) ? responseData : [];
+ if (!Array.isArray(responseData)) {
+ console.warn("API response for map list was not an array:", responseData);
+ }
+ // --- End Check ---
+
setMapList(validList);
saveToCache(cacheKey, validList); // Save fetched list to cache
console.log("Map list fetched and cached:", validList);
@@ -165,18 +177,24 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
'Authorization': `Bearer ${user.idToken}`,
'Content-Type': 'application/json' // Good practice
},
- // Send the default name in the body
- body: JSON.stringify({ map_name: mapName })
+ // --- Send map_name AND empty nodes/edges ---
+ body: JSON.stringify({
+ map_name: mapName,
+ nodes: [], // Always send empty nodes array for new map
+ edges: [] // Always send empty edges array for new map
+ })
+ // --- End Change ---
});
+ // --- Response handling remains the same ---
if (newMapData && newMapData.map_id) {
console.log("New map record created:", newMapData);
// 1. Update Map List State & Cache
const newMapEntry = {
map_id: newMapData.map_id,
- map_name: newMapData.map_name,
- last_updated: newMapData.last_updated
+ map_name: newMapData.map_name || mapName, // Use returned name or prompted name
+ last_updated: newMapData.last_updated || new Date().toISOString() // Use returned date or now
};
setMapList(prevList => {
const newList = [newMapEntry, ...prevList];
@@ -190,7 +208,7 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
setNodes(defaultNodes); // Reset nodes/edges for the new map
setEdges(defaultEdges);
setCurrentMapId(newMapData.map_id); // Set the new ID
- setCurrentMapName(newMapData.map_name); // Set the name
+ setCurrentMapName(newMapEntry.map_name); // Set the name from newMapEntry
idCounter = 0; // Reset node counter
// 3. Update Specific Map Cache (optional but good practice)
@@ -199,8 +217,8 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
nodes: defaultNodes,
edges: defaultEdges,
map_id: newMapData.map_id,
- map_name: newMapData.map_name,
- last_updated: newMapData.last_updated
+ map_name: newMapEntry.map_name,
+ last_updated: newMapEntry.last_updated
});
setSaveStatus("New map created.");
@@ -209,6 +227,7 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
} else {
throw new Error("Failed to create map record: Invalid response from server.");
}
+ // --- End Response handling ---
} catch (error) {
console.error("Failed to create new map:", error);
@@ -230,6 +249,7 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
setCurrentMapName('Untitled Map');
idCounter = 0;
setIsLoading(false);
+ setSaveStatus(''); // Clear any previous status
return;
}
@@ -406,24 +426,22 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
setSaveStatus(`Error saving map: ${error.message}`);
}
}, [userId, user?.idToken, nodes, edges, currentMapId, currentMapName, loadMapList, setMapList]); // Added setMapList dependency
-
- // --- Handle New Map ---
// --- Handle Map Selection Change ---
const handleMapSelectChange = (event) => {
const selectedId = event.target.value;
if (selectedId === "__NEW__") {
- // Call handleNewMap which now includes the prompt
- handleNewMap();
+ console.log("Selected [Untitled Map], resetting view.");
+ loadSpecificMap(null);
} else {
// Load existing map (check cache first)
loadSpecificMap(selectedId);
}
};
- // --- Handle Delete Map ---
- const handleDeleteMap = useCallback(async () => {
+ // --- Handle Delete Map ---
+ const handleDeleteMap = useCallback(async () => {
if (!currentMapId || !userId || !user?.idToken) {
setSaveStatus("No map selected to delete or not logged in.");
return;
@@ -435,40 +453,65 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.
const mapToDeleteId = currentMapId; // Capture ID before state changes
const mapCacheKey = getCacheKey('courseMap', userId, mapToDeleteId);
+ const listCacheKey = getCacheKey('courseMapList', userId); // Cache key for the list
- // --- Add Logging ---
console.log(`[Delete Attempt] User ID: ${userId}, Map ID: ${mapToDeleteId}`);
- // --- End Logging ---
-
setSaveStatus("Deleting...");
- console.log(`Attempting to delete map: ${currentMapId}`);
try {
- await fetchData(`course-map/${mapToDeleteId}`, { // Ensure mapToDeleteId is correct here
+ // --- Call Backend DELETE ---
+ await fetchData(`course-map/${mapToDeleteId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${user.idToken}`
}
});
+ // --- Backend Delete Successful ---
- console.log("Map deleted successfully.");
+ console.log("Map deleted successfully from backend.");
setSaveStatus("Map deleted.");
- removeFromCache(mapCacheKey); // <-- Used here
- // Load the list again and load the next available map (or new)
- loadMapList().then((list) => {
- if (list && list.length > 0) {
- loadSpecificMap(list[0].map_id); // Load most recent
- } else {
- handleNewMap(); // No maps left, create new
+
+ // --- Frontend Cleanup ---
+ // 1. Remove from specific map cache
+ removeFromCache(mapCacheKey);
+
+ // 2. Update mapList state and list cache
+ let nextMapIdToLoad = null;
+ setMapList(prevList => {
+ const newList = prevList.filter(map => map.map_id !== mapToDeleteId);
+ // Determine which map to load next (e.g., the first one in the updated list)
+ if (newList.length > 0) {
+ newList.sort((a, b) => new Date(b.last_updated) - new Date(a.last_updated)); // Re-sort just in case
+ nextMapIdToLoad = newList[0].map_id;
}
+ saveToCache(listCacheKey, newList); // Update list cache
+ return newList;
});
- setTimeout(() => setSaveStatus(''), 2000);
+
+ // 3. Load the next map (or reset if list is empty)
+ if (nextMapIdToLoad) {
+ loadSpecificMap(nextMapIdToLoad);
+ } else {
+ // No maps left, reset to a new, unsaved state
+ setNodes(defaultNodes);
+ setEdges(defaultEdges);
+ setCurrentMapId(null);
+ setCurrentMapName('Untitled Map');
+ idCounter = 0;
+ // Optionally, call handleNewMap() if you want to immediately prompt for a name
+ // handleNewMap();
+ }
+ // --- End Frontend Cleanup ---
+
+ setTimeout(() => setSaveStatus(''), 2000);
} catch (error) {
console.error(`[Delete Failed] User ID: ${userId}, Map ID: ${mapToDeleteId}`, error);
setSaveStatus(`Error deleting map: ${error.message}`);
+ // Optionally, force refresh the list from API on error to ensure consistency
+ loadMapList(true);
}
- }, [userId, user?.idToken, currentMapId, currentMapName, loadMapList, loadSpecificMap, handleNewMap]);
+ }, [userId, user?.idToken, currentMapId, currentMapName, loadMapList, loadSpecificMap, setMapList, setNodes, setEdges]); // Added setMapList, setNodes, setEdges
// --- Other Callbacks (onConnect, addNode, startEditing, handleEditChange, saveEdit, onPaneClick) remain the same ---
@@ -531,7 +574,7 @@ function CourseMapFlow({ user }) { // user object now contains id, idToken, etc.