diff --git a/backend/app.py b/backend/app.py index 901604d91..b11c51e7f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,10 +1,14 @@ -from flask import Flask, request, jsonify +from urllib.parse import urljoin +from flask import Flask, request, jsonify, session, redirect, url_for from flask_cors import CORS +from flask_cas import CAS, login_required import os from dotenv import load_dotenv from lib import chat_completion_request, create_embedding import json from pymongo.mongo_client import MongoClient +import requests +import xml.etree.ElementTree as ET COURSE_QUERY_LIMIT = 5 SAFETY_CHECK_ENABLED = False @@ -27,8 +31,59 @@ # flask app = Flask(__name__) +app.secret_key = os.environ.get('FLASK_SECRET_KEY', '3d6f45a5fc12445dbac2f59c3b6c7cb1') CORS(app) +CAS(app) +app.config['CAS_SERVER'] = 'https://secure.its.yale.edu/cas' + +@app.route('/login', methods=['GET']) +def login(): + return redirect(url_for('cas.login')) + +@app.route('/logout', methods=['GET']) +def logout(): + session.clear() + return redirect(url_for('cas.logout')) + +@app.route('/route_after_login', methods=['GET']) +@login_required +def route_after_login(): + # Handle what happens after successful login + return 'Logged in as ' + session['CAS_USERNAME'] + +@app.route('/validate_ticket', methods=['POST']) +def validate_cas_ticket(): + data = request.get_json() + ticket = data.get("ticket") + service_url = data.get("service_url") + print(f"Received ticket: {ticket}, service URL: {service_url}") # Log details + + if not ticket or not service_url: + return jsonify({"error": "Ticket or service URL not provided"}), 400 + + cas_validate_url = 'https://secure.its.yale.edu/cas/serviceValidate' + params = {'ticket': ticket, 'service': service_url} + response = requests.get(cas_validate_url, params=params) + + if response.status_code == 200: + # Parse the XML response + root = ET.fromstring(response.content) + + # Namespace in the XML response + ns = {'cas': 'http://www.yale.edu/tp/cas'} + + # Check for authentication success + if root.find('.//cas:authenticationSuccess', ns) is not None: + user = root.find('.//cas:user', ns).text + # Optionally handle proxyGrantingTicket if you need it + return jsonify({"isAuthenticated": True, "user": user}) + else: + return jsonify({"isAuthenticated": False}), 401 + else: + print("response status is not 200") + return jsonify({"isAuthenticated": False}), 401 + @app.route('/api/chat', methods=['POST']) def chat(): diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 4678774e6..61cd5eddd 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,4 +1,6 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + reactStrictMode: false, +}; export default nextConfig; diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 36dc2f879..ceafe26f0 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,63 +1,96 @@ -'use client'; +"use client"; -import React, { useState } from 'react'; -import styles from './page.module.css'; -import { format } from 'path'; +import React, { useState } from "react"; +import { useEffect } from "react"; +import styles from "./page.module.css"; // change to ur own directory export default function Chat() { const [isTyping, setIsTyping] = useState(false); - const [input, setInput] = useState(''); - const [messages, setMessages] = useState([{ id: 'welcome-msg', content: 'How may I help you?', role: 'ai' }]); + const [input, setInput] = useState(""); + const [messages, setMessages] = useState([ + { id: "welcome-msg", content: "How may I help you?", role: "ai" }, + ]); const [chatVisible, setChatVisible] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); - const handleInputChange = (e: { target: { value: React.SetStateAction; }; }) => { + useEffect(() => { + // Check for CAS ticket in URL parameters + const urlParams = new URLSearchParams(window.location.search); + const ticket = urlParams.get("ticket"); + + if (ticket) { + validateTicket(ticket); + } + }, []); + + const handleInputChange = (e: { + target: { value: React.SetStateAction }; + }) => { setInput(e.target.value); }; - const handleSubmit = async (e: { preventDefault: () => void; }) => { + // Call this function after your authentication logic or on page load + useEffect(() => { + clearTicketFromUrl(); + }, []); + + const handleSubmit = async (e: { preventDefault: () => void }) => { e.preventDefault(); - setInput(''); - // add the user's message to the chat. - const newUserMessage = { id: `user-${Date.now()}`, content: input, role: 'user' }; + const newUserMessage = { + id: `user-${Date.now()}`, + content: input, + role: "user", + }; - setMessages(messages => [...messages, newUserMessage]); + setMessages((messages) => [...messages, newUserMessage]); setIsTyping(true); - const response = await fetch('http://127.0.0.1:8000/api/chat', { - method: 'POST', + const response = await fetch("http://127.0.0.1:8000/api/chat", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, // body: JSON.stringify({ message: [{ content: input, role: 'user' }] }), body: JSON.stringify({ message: [...messages, newUserMessage], }), }); - - setIsTyping(false); + + setIsTyping(false); if (response.ok) { const data = await response.json(); // simulateTypingEffect(data.message[0].content, 'ai', `ai-${Date.now()}`); - simulateTypingEffect(data.response, 'ai', `ai-${Date.now()}`); + simulateTypingEffect(data.response, "ai", `ai-${Date.now()}`); } else { - console.error('Failed to send message'); + console.error("Failed to send message"); } - + + setInput(""); }; - - const simulateTypingEffect = (message: string, role: string, messageId: string) => { + + const simulateTypingEffect = ( + message: string, + role: string, + messageId: string + ) => { let index = 0; const typingSpeedMs = 20; - + const typeCharacter = () => { if (index < message.length) { - const updatedMessage = { id: messageId, content: message.substring(0, index + 1), role: role }; - setMessages(currentMessages => { + const updatedMessage = { + id: messageId, + content: message.substring(0, index + 1), + role: role, + }; + setMessages((currentMessages) => { // is message being typed already in array - const existingIndex = currentMessages.findIndex(msg => msg.id === messageId); + const existingIndex = currentMessages.findIndex( + (msg) => msg.id === messageId + ); let newMessages = [...currentMessages]; if (existingIndex >= 0) { // update existing message @@ -72,7 +105,7 @@ export default function Chat() { setTimeout(typeCharacter, typingSpeedMs); } }; - + typeCharacter(); }; @@ -80,62 +113,107 @@ export default function Chat() { console.log("Toggling chat visibility. Current state:", chatVisible); setChatVisible(!chatVisible); }; - - const formatMessage = (content: string) => { - const boldRegex = /\*\*(.*?)\*\*/g; - return content.split(boldRegex).map((part, index) => { - // Every even index is not bold, odd indices are the bold text between **. - if (index % 2 === 0) { - // Normal text - return part; + + // Redirect to CAS login page + const redirectToCasLogin = () => { + const casLoginUrl = `https://secure.its.yale.edu/cas/login?service=${encodeURIComponent( + window.location.href + )}`; + window.location.href = casLoginUrl; + }; + + const validateTicket = async (ticket: string) => { + const serviceUrl = window.location.origin + "/"; + + try { + const response = await fetch("http://127.0.0.1:8000/validate_ticket", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ticket, service_url: serviceUrl }), + }); + + if (response.ok) { + const data = await response.json(); + setIsAuthenticated(data.isAuthenticated); // Update the state based on the response + clearTicketFromUrl(); } else { - // Bold text - return {part}; + console.error( + "Failed to validate ticket - server responded with an error" + ); } - }); + } catch (error) { + console.error("Failed to validate ticket:", error); + } + }; + + // Function to clear the ticket from the URL + const clearTicketFromUrl = () => { + const url = new URL(window.location.href); + url.searchParams.delete("ticket"); // Remove the ticket parameter + + window.history.replaceState({}, document.title, url.pathname + url.search); + }; + + const handleButtonClick = () => { + if (isAuthenticated) { + toggleChatVisibility(); + } else { + redirectToCasLogin(); + } }; return ( <> - - - {chatVisible && ( -
-
- BluebookAI Assistant -
-
- {messages.map((m) => ( -
- {formatMessage(m.content)} -
- ))} - {isTyping && ( -
- - - + + + {chatVisible && ( +
+
BluebookAI Assistant
+
+ {messages.map((m) => ( +
+ {m.content} +
+ ))} + {isTyping && ( +
+ + + +
+ )}
- )} +
+ + +
-
- - -
-
- )} + )} ); } diff --git a/frontend/src/app/page.tsx.orig b/frontend/src/app/page.tsx.orig new file mode 100644 index 000000000..ceafe26f0 --- /dev/null +++ b/frontend/src/app/page.tsx.orig @@ -0,0 +1,219 @@ +"use client"; + +import React, { useState } from "react"; +import { useEffect } from "react"; +import styles from "./page.module.css"; // change to ur own directory + +export default function Chat() { + const [isTyping, setIsTyping] = useState(false); + const [input, setInput] = useState(""); + const [messages, setMessages] = useState([ + { id: "welcome-msg", content: "How may I help you?", role: "ai" }, + ]); + const [chatVisible, setChatVisible] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + // Check for CAS ticket in URL parameters + const urlParams = new URLSearchParams(window.location.search); + const ticket = urlParams.get("ticket"); + + if (ticket) { + validateTicket(ticket); + } + }, []); + + const handleInputChange = (e: { + target: { value: React.SetStateAction }; + }) => { + setInput(e.target.value); + }; + + // Call this function after your authentication logic or on page load + useEffect(() => { + clearTicketFromUrl(); + }, []); + + const handleSubmit = async (e: { preventDefault: () => void }) => { + e.preventDefault(); + + // add the user's message to the chat. + const newUserMessage = { + id: `user-${Date.now()}`, + content: input, + role: "user", + }; + + setMessages((messages) => [...messages, newUserMessage]); + setIsTyping(true); + + const response = await fetch("http://127.0.0.1:8000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // body: JSON.stringify({ message: [{ content: input, role: 'user' }] }), + body: JSON.stringify({ + message: [...messages, newUserMessage], + }), + }); + + setIsTyping(false); + + if (response.ok) { + const data = await response.json(); + // simulateTypingEffect(data.message[0].content, 'ai', `ai-${Date.now()}`); + simulateTypingEffect(data.response, "ai", `ai-${Date.now()}`); + } else { + console.error("Failed to send message"); + } + + setInput(""); + }; + + const simulateTypingEffect = ( + message: string, + role: string, + messageId: string + ) => { + let index = 0; + const typingSpeedMs = 20; + + const typeCharacter = () => { + if (index < message.length) { + const updatedMessage = { + id: messageId, + content: message.substring(0, index + 1), + role: role, + }; + setMessages((currentMessages) => { + // is message being typed already in array + const existingIndex = currentMessages.findIndex( + (msg) => msg.id === messageId + ); + let newMessages = [...currentMessages]; + if (existingIndex >= 0) { + // update existing message + newMessages[existingIndex] = updatedMessage; + } else { + // add new message if it doesn't exist + newMessages.push(updatedMessage); + } + return newMessages; + }); + index++; + setTimeout(typeCharacter, typingSpeedMs); + } + }; + + typeCharacter(); + }; + + const toggleChatVisibility = () => { + console.log("Toggling chat visibility. Current state:", chatVisible); + setChatVisible(!chatVisible); + }; + + // Redirect to CAS login page + const redirectToCasLogin = () => { + const casLoginUrl = `https://secure.its.yale.edu/cas/login?service=${encodeURIComponent( + window.location.href + )}`; + window.location.href = casLoginUrl; + }; + + const validateTicket = async (ticket: string) => { + const serviceUrl = window.location.origin + "/"; + + try { + const response = await fetch("http://127.0.0.1:8000/validate_ticket", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ ticket, service_url: serviceUrl }), + }); + + if (response.ok) { + const data = await response.json(); + setIsAuthenticated(data.isAuthenticated); // Update the state based on the response + clearTicketFromUrl(); + } else { + console.error( + "Failed to validate ticket - server responded with an error" + ); + } + } catch (error) { + console.error("Failed to validate ticket:", error); + } + }; + + // Function to clear the ticket from the URL + const clearTicketFromUrl = () => { + const url = new URL(window.location.href); + url.searchParams.delete("ticket"); // Remove the ticket parameter + + window.history.replaceState({}, document.title, url.pathname + url.search); + }; + + const handleButtonClick = () => { + if (isAuthenticated) { + toggleChatVisibility(); + } else { + redirectToCasLogin(); + } + }; + + return ( + <> + + + {chatVisible && ( +
+
BluebookAI Assistant
+
+ {messages.map((m) => ( +
+ {m.content} +
+ ))} + {isTyping && ( +
+ + + +
+ )} +
+
+ + +
+
+ )} + + ); +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..644fbdfe4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +annotated-types==0.6.0 +anyio==4.3.0 +blinker==1.7.0 +certifi==2024.2.2 +click==8.1.7 +distro==1.9.0 +dnspython==2.6.1 +exceptiongroup==1.2.0 +Flask==2.1.3 +Flask-Cors==4.0.0 +h11==0.14.0 +httpcore==1.0.3 +httpx==0.26.0 +idna==3.6 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +openai==1.14.3 +pydantic==2.6.1 +pydantic_core==2.16.2 +pymongo==4.6.2 +python-dotenv==1.0.1 +sniffio==1.3.0 +tenacity==8.2.3 +tqdm==4.66.2 +typing_extensions==4.9.0 +Werkzeug==2.2.2 +flask_cas==1.0.2 +requests==2.31.0 \ No newline at end of file