Skip to content

Commit

Permalink
Merge pull request #7 from yale-swe/buwei-michal-cas
Browse files Browse the repository at this point in the history
Implement CAS
  • Loading branch information
BuweiChen authored Apr 1, 2024
2 parents 86daa2d + 76369ad commit 44109b9
Show file tree
Hide file tree
Showing 5 changed files with 460 additions and 77 deletions.
57 changes: 56 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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():

Expand Down
4 changes: 3 additions & 1 deletion frontend/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
reactStrictMode: false,
};

export default nextConfig;
228 changes: 153 additions & 75 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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<string>; }; }) => {
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<string> };
}) => {
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
Expand All @@ -72,70 +105,115 @@ export default function Chat() {
setTimeout(typeCharacter, typingSpeedMs);
}
};

typeCharacter();
};

const toggleChatVisibility = () => {
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 <strong key={index}>{part}</strong>;
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 (
<>
<button
onClick={toggleChatVisibility}
className={styles.floatingChatButton}
aria-label="Toggle Chat"
>
{}
</button>

{chatVisible && (
<div className={`${styles.chatContainer} ${chatVisible ? styles.chatVisible : ''}`}>
<div className={styles.chatHeader}>
BluebookAI Assistant
</div>
<div className={styles.messages}>
{messages.map((m) => (
<div key={m.id} className={`${styles.message} ${m.role === 'user' ? styles.user : styles.ai}`}>
{formatMessage(m.content)}
</div>
))}
{isTyping && (
<div className={styles['typing-indicator']}>
<span></span>
<span></span>
<span></span>
<button
onClick={handleButtonClick}
className={styles.floatingChatButton}
aria-label="Toggle Chat"
>
{}
</button>

{chatVisible && (
<div
className={`${styles.chatContainer} ${
chatVisible ? styles.chatVisible : ""
}`}
>
<div className={styles.chatHeader}>BluebookAI Assistant</div>
<div className={styles.messages}>
{messages.map((m) => (
<div
key={m.id}
className={`${styles.message} ${
m.role === "user" ? styles.user : styles.ai
}`}
>
{m.content}
</div>
))}
{isTyping && (
<div className={styles.typingIndicator}>
<span></span>
<span></span>
<span></span>
</div>
)}
</div>
)}
<form onSubmit={handleSubmit} className={styles.inputForm}>
<input
type="text"
className={styles.inputField}
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
<button type="submit" className={styles.sendButton}>
Send
</button>
</form>
</div>
<form onSubmit={handleSubmit} className={styles.inputForm}>
<input
type="text"
className={styles.inputField}
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
<button type="submit" className={styles.sendButton}>Send</button>
</form>
</div>
)}
)}
</>
);
}
Loading

0 comments on commit 44109b9

Please sign in to comment.