From fdd99257633b20518b096a20b24b82d80273fd71 Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 24 Oct 2024 22:24:06 +0100 Subject: [PATCH 01/58] Implement backend static functionality and cookies. Implement the ability to return index and error HTML pages. Implement the ability to 'sign up' by receiving a unique user ID, provide and revoke cookie consents. Create index.html and error.html (both contain placeholders / 'lorem ipsum' at the moment. Create a random string generator. --- backend/pom.xml | 4 ++ .../AiLearningToolApplication.java | 4 +- .../AILearningTool/DatabaseController.java | 29 +++++++++++++ .../com/UoB/AILearningTool/RandomString.java | 13 ++++++ .../UoB/AILearningTool/SpringController.java | 43 +++++++++++++++++++ .../java/com/UoB/AILearningTool/User.java | 23 ++++++++++ .../src/main/resources/application.properties | 3 ++ backend/src/main/resources/static/error.html | 12 ++++++ backend/src/main/resources/static/index.html | 13 ++++++ 9 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/RandomString.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/SpringController.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/User.java create mode 100644 backend/src/main/resources/static/error.html create mode 100644 backend/src/main/resources/static/index.html diff --git a/backend/pom.xml b/backend/pom.xml index ccfe7193..fce94172 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -42,6 +42,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-logging + org.mariadb.jdbc diff --git a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java index 98d4a429..92b9777b 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java +++ b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -@SpringBootApplication +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) public class AiLearningToolApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java new file mode 100644 index 00000000..19234a73 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java @@ -0,0 +1,29 @@ +package com.UoB.AILearningTool; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +// Communication with SQL database. +public class DatabaseController { + private Map users = new HashMap<>(); + + public DatabaseController() { + // TODO: Connect to a MariaDB database. + } + + // Create a new user and return their ID for cookie assignment + public String addUser() { + String id = RandomString.make(20); + // TODO: Add a user profile record to the database. + users.put(id, new User()); + return id; + } + + // Remove all data stored about the user (profile, chat, etc.) + public void removeUser(String id) { + // TODO: Remove a user profile record from the database. + users.remove(id); + } + +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/RandomString.java b/backend/src/main/java/com/UoB/AILearningTool/RandomString.java new file mode 100644 index 00000000..876cb713 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/RandomString.java @@ -0,0 +1,13 @@ +package com.UoB.AILearningTool; + +public class RandomString { + public static String make(int n) { + String newString = ""; + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + for (int i = 0; i < n; i++) { + int index = (int)(characters.length() * Math.random()); + newString = newString + characters.charAt(index); + } + return newString; + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java new file mode 100644 index 00000000..da0e59a6 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java @@ -0,0 +1,43 @@ +package com.UoB.AILearningTool; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RestController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RestController +public class SpringController { + private final Logger log = LoggerFactory.getLogger(SpringController.class); + private final DatabaseController DBC = new DatabaseController(); + + // If user consents to optional cookies, assign a unique user ID for them. + @GetMapping("/signup") + public void signup(@CookieValue(value = "optionalConsent", defaultValue = "false") boolean optionalConsent, + HttpServletResponse response) { + if (optionalConsent) { + Cookie userIDCookie = new Cookie("userID", DBC.addUser()); + userIDCookie.setMaxAge(30 * 24 * 60 * 60); // Cookie will expire in 30 days + userIDCookie.setSecure(true); + response.addCookie(userIDCookie); + log.info("Assigned a new userID."); + } + } + + // If user revokes their consent for data storage / optional cookies, + // remove all data stored about them. + @GetMapping("/revokeConsent") + public void revokeConsent(@CookieValue(value = "userID", defaultValue = "") String userID, + HttpServletResponse response) { + if (userID.isEmpty()) { + log.info("Cannot withdraw consent of userID {}", userID); + } + Cookie cookie = new Cookie("userID", ""); + cookie.setMaxAge(0); + response.addCookie(cookie); + DBC.removeUser(userID); + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/User.java b/backend/src/main/java/com/UoB/AILearningTool/User.java new file mode 100644 index 00000000..59a3d492 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/User.java @@ -0,0 +1,23 @@ +package com.UoB.AILearningTool; + +import java.time.LocalDateTime; + +// Class representing a user profile (someone who consented to optional cookies). +public class User { + private final String id; + private LocalDateTime lastActivityTime; + + public String getID() { + return this.id; + } + + public void updateLastActivityTime() { + this.lastActivityTime = LocalDateTime.now(); + } + + // Create a user + public User() { + updateLastActivityTime(); + this.id = RandomString.make(25); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8d0ccbb8..4300420a 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1 +1,4 @@ spring.application.name=AILearningTool +spring.servlet.multipart.max-file-size=50MB +spring.servlet.multipart.max-request-size=50MB +spring.web.resources.static-locations=classpath:/static/ \ No newline at end of file diff --git a/backend/src/main/resources/static/error.html b/backend/src/main/resources/static/error.html new file mode 100644 index 00000000..62361444 --- /dev/null +++ b/backend/src/main/resources/static/error.html @@ -0,0 +1,12 @@ + + + + + Not found + + +

AI Learning Tool

+

Error - not found.

+

Redirecting you to the homepage...

+ + \ No newline at end of file diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html new file mode 100644 index 00000000..a5212ce4 --- /dev/null +++ b/backend/src/main/resources/static/index.html @@ -0,0 +1,13 @@ + + + + + AI Learning Tool + + +

AI Learning Tool

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ut turpis nibh. Fusce vulputate euismod leo, nec consequat diam maximus id. Curabitur nunc magna, ornare nec auctor in, sollicitudin at tortor. Aliquam egestas augue neque, quis dictum nulla congue vel. Suspendisse ante nunc, porta sed semper eget, venenatis sit amet nisi. Proin sollicitudin mi eget porta dignissim. Curabitur eu vestibulum lectus. In metus eros, varius quis volutpat eu, bibendum ac massa. Nullam molestie sapien finibus massa varius, ac euismod ante scelerisque.

+

Phasellus dignissim risus ut orci rhoncus varius. Mauris efficitur leo at magna imperdiet congue. Donec imperdiet, libero nec bibendum ultricies, lorem tellus semper metus, sed dapibus quam elit eget ipsum. Duis quis quam vulputate, semper leo non, maximus eros. Suspendisse potenti. Fusce quis finibus urna. Etiam velit justo, feugiat sit amet venenatis convallis, ultricies ut neque. Aenean tempor orci eu consectetur tincidunt. Quisque elementum feugiat orci, nec tempus ipsum pharetra id. Maecenas semper vestibulum lectus pellentesque ullamcorper.

+

Vivamus vitae efficitur sem. Cras ante odio, varius ac porttitor eu, aliquet eget diam. Mauris finibus sagittis urna non sodales. Duis at venenatis turpis. Sed in tincidunt ante. Aenean nec orci sagittis, mollis est et, rutrum enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed convallis, mauris ut mollis pulvinar, massa magna molestie neque, sit amet iaculis turpis ligula eget dolor. Sed urna enim, porta quis cursus vel, cursus nec est. Sed eu aliquet nibh, venenatis efficitur est. Sed congue metus urna, non tempor diam pharetra et. Ut ac viverra risus. Praesent eget dictum ligula. Phasellus ac quam eget sapien consequat venenatis. Aliquam erat volutpat. Nulla facilisi.

+ + \ No newline at end of file From d489c5149921fc62d5058a6cdcf7ea87f87d9da4 Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Tue, 29 Oct 2024 13:57:24 +0000 Subject: [PATCH 02/58] Update maven.yml Fix Github runner for debugging Java code. --- .github/workflows/maven.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 24364992..b79b6276 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,9 +10,9 @@ name: Java CI with Maven on: push: - branches: [ "dev" ] + branches: [ "dev", "backend", "frontend" ] pull_request: - branches: [ "dev" ] + branches: [ "dev", "backend", "frontend" ] jobs: build: @@ -28,7 +28,7 @@ jobs: distribution: 'temurin' cache: maven - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B package --file backend/pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph From cadb01ada5630d79d6061c8ab8d239e935a61b4d Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Tue, 29 Oct 2024 22:56:56 +0000 Subject: [PATCH 03/58] Revert "Supply static pages, assign / delete users." --- backend/pom.xml | 4 -- .../AiLearningToolApplication.java | 4 +- .../AILearningTool/DatabaseController.java | 29 ------------- .../com/UoB/AILearningTool/RandomString.java | 13 ------ .../UoB/AILearningTool/SpringController.java | 43 ------------------- .../java/com/UoB/AILearningTool/User.java | 23 ---------- .../src/main/resources/application.properties | 3 -- backend/src/main/resources/static/error.html | 12 ------ backend/src/main/resources/static/index.html | 13 ------ 9 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java delete mode 100644 backend/src/main/java/com/UoB/AILearningTool/RandomString.java delete mode 100644 backend/src/main/java/com/UoB/AILearningTool/SpringController.java delete mode 100644 backend/src/main/java/com/UoB/AILearningTool/User.java delete mode 100644 backend/src/main/resources/static/error.html delete mode 100644 backend/src/main/resources/static/index.html diff --git a/backend/pom.xml b/backend/pom.xml index fce94172..ccfe7193 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -42,10 +42,6 @@ org.springframework.boot spring-boot-starter-web
- - org.springframework.boot - spring-boot-starter-logging - org.mariadb.jdbc diff --git a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java index 92b9777b..98d4a429 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java +++ b/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java @@ -2,10 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) +@SpringBootApplication public class AiLearningToolApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java deleted file mode 100644 index 19234a73..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.UoB.AILearningTool; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -// Communication with SQL database. -public class DatabaseController { - private Map users = new HashMap<>(); - - public DatabaseController() { - // TODO: Connect to a MariaDB database. - } - - // Create a new user and return their ID for cookie assignment - public String addUser() { - String id = RandomString.make(20); - // TODO: Add a user profile record to the database. - users.put(id, new User()); - return id; - } - - // Remove all data stored about the user (profile, chat, etc.) - public void removeUser(String id) { - // TODO: Remove a user profile record from the database. - users.remove(id); - } - -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/RandomString.java b/backend/src/main/java/com/UoB/AILearningTool/RandomString.java deleted file mode 100644 index 876cb713..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/RandomString.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.UoB.AILearningTool; - -public class RandomString { - public static String make(int n) { - String newString = ""; - String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - for (int i = 0; i < n; i++) { - int index = (int)(characters.length() * Math.random()); - newString = newString + characters.charAt(index); - } - return newString; - } -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java deleted file mode 100644 index da0e59a6..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.UoB.AILearningTool; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestAttribute; -import org.springframework.web.bind.annotation.RestController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@RestController -public class SpringController { - private final Logger log = LoggerFactory.getLogger(SpringController.class); - private final DatabaseController DBC = new DatabaseController(); - - // If user consents to optional cookies, assign a unique user ID for them. - @GetMapping("/signup") - public void signup(@CookieValue(value = "optionalConsent", defaultValue = "false") boolean optionalConsent, - HttpServletResponse response) { - if (optionalConsent) { - Cookie userIDCookie = new Cookie("userID", DBC.addUser()); - userIDCookie.setMaxAge(30 * 24 * 60 * 60); // Cookie will expire in 30 days - userIDCookie.setSecure(true); - response.addCookie(userIDCookie); - log.info("Assigned a new userID."); - } - } - - // If user revokes their consent for data storage / optional cookies, - // remove all data stored about them. - @GetMapping("/revokeConsent") - public void revokeConsent(@CookieValue(value = "userID", defaultValue = "") String userID, - HttpServletResponse response) { - if (userID.isEmpty()) { - log.info("Cannot withdraw consent of userID {}", userID); - } - Cookie cookie = new Cookie("userID", ""); - cookie.setMaxAge(0); - response.addCookie(cookie); - DBC.removeUser(userID); - } -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/User.java b/backend/src/main/java/com/UoB/AILearningTool/User.java deleted file mode 100644 index 59a3d492..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/User.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.UoB.AILearningTool; - -import java.time.LocalDateTime; - -// Class representing a user profile (someone who consented to optional cookies). -public class User { - private final String id; - private LocalDateTime lastActivityTime; - - public String getID() { - return this.id; - } - - public void updateLastActivityTime() { - this.lastActivityTime = LocalDateTime.now(); - } - - // Create a user - public User() { - updateLastActivityTime(); - this.id = RandomString.make(25); - } -} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 4300420a..8d0ccbb8 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,4 +1 @@ spring.application.name=AILearningTool -spring.servlet.multipart.max-file-size=50MB -spring.servlet.multipart.max-request-size=50MB -spring.web.resources.static-locations=classpath:/static/ \ No newline at end of file diff --git a/backend/src/main/resources/static/error.html b/backend/src/main/resources/static/error.html deleted file mode 100644 index 62361444..00000000 --- a/backend/src/main/resources/static/error.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Not found - - -

AI Learning Tool

-

Error - not found.

-

Redirecting you to the homepage...

- - \ No newline at end of file diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html deleted file mode 100644 index a5212ce4..00000000 --- a/backend/src/main/resources/static/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - AI Learning Tool - - -

AI Learning Tool

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ut turpis nibh. Fusce vulputate euismod leo, nec consequat diam maximus id. Curabitur nunc magna, ornare nec auctor in, sollicitudin at tortor. Aliquam egestas augue neque, quis dictum nulla congue vel. Suspendisse ante nunc, porta sed semper eget, venenatis sit amet nisi. Proin sollicitudin mi eget porta dignissim. Curabitur eu vestibulum lectus. In metus eros, varius quis volutpat eu, bibendum ac massa. Nullam molestie sapien finibus massa varius, ac euismod ante scelerisque.

-

Phasellus dignissim risus ut orci rhoncus varius. Mauris efficitur leo at magna imperdiet congue. Donec imperdiet, libero nec bibendum ultricies, lorem tellus semper metus, sed dapibus quam elit eget ipsum. Duis quis quam vulputate, semper leo non, maximus eros. Suspendisse potenti. Fusce quis finibus urna. Etiam velit justo, feugiat sit amet venenatis convallis, ultricies ut neque. Aenean tempor orci eu consectetur tincidunt. Quisque elementum feugiat orci, nec tempus ipsum pharetra id. Maecenas semper vestibulum lectus pellentesque ullamcorper.

-

Vivamus vitae efficitur sem. Cras ante odio, varius ac porttitor eu, aliquet eget diam. Mauris finibus sagittis urna non sodales. Duis at venenatis turpis. Sed in tincidunt ante. Aenean nec orci sagittis, mollis est et, rutrum enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed convallis, mauris ut mollis pulvinar, massa magna molestie neque, sit amet iaculis turpis ligula eget dolor. Sed urna enim, porta quis cursus vel, cursus nec est. Sed eu aliquet nibh, venenatis efficitur est. Sed congue metus urna, non tempor diam pharetra et. Ut ac viverra risus. Praesent eget dictum ligula. Phasellus ac quam eget sapien consequat venenatis. Aliquam erat volutpat. Nulla facilisi.

- - \ No newline at end of file From b81879177c7478eae146050dc9d7bd5650b7ae91 Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Wed, 30 Oct 2024 09:07:56 +0000 Subject: [PATCH 04/58] Update README.md Add new team member's details. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e24518ec..e0b54980 100644 --- a/README.md +++ b/README.md @@ -160,3 +160,4 @@ Gerard Chaba (tl23383) \ Mohammed Elzobair (yi23484) \ Weifan Liu (au22116) \ Zixuan Zhu (kh23199) +Siyuan Zhang (gr23994) From ea02b816c310ab4d684ae1769eb056a913b9ddb3 Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Wed, 30 Oct 2024 11:04:33 +0000 Subject: [PATCH 05/58] Added Chatbot Interaction Flow diagram - Added the Chatbot Interaction Flow Diagram - A bit of formatting fixes --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e24518ec..e332458b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - [Project Structure](#project-structure) - [Tech Stack](#tech-stack) - [User Instructions](#user-instructions) +- [Chatbot Interaction Flow](#chatbot-interaction-flow) - [Developer Instructions](#developer-instructions) - [Team Members](#team-members) @@ -93,7 +94,8 @@ The frontend is a JavaScript Vue 3-based web application. It makes requests to t ### Backend The backend is based on Spring Boot (open-source Java framework). Data will be stored in a MariaDB database. -User prompts for the chatbot will be sent using API requests from the Spring Boot backend to the IBM Watsonx language model.\ +User prompts for the chatbot will be sent using API requests from the Spring Boot backend to the IBM Watsonx language model. + ![Architecture diagram, showing the technologies used in the project.](/docs/architecture_diagram.png) ## User Instructions: @@ -134,6 +136,12 @@ User prompts for the chatbot will be sent using API requests from the Spring Boo If you accepted the optional cookies, your conversation history will be saved for 30 days. You can return to the web app at any time within that period to continue where you left off or ask follow-up questions based on previous conversations. +## Chatbot Interaction Flow: + +This flowchart outlines the interaction pathways within the chatbot, guiding users through key topics such as SkillsBuild courses, university life questions, and IBM SkillsBuild platform information. Each pathway details the chatbot's prompts, and user responses, providing an overview of the chatbot’s functionality. + +![watson_flow](https://github.com/user-attachments/assets/036f89eb-2ce5-4ed8-aa71-2e0d4e5e4021) + ## Developer Instructions: To get started with developing or contributing to this project, follow the steps below: From 80633d854ff047a7104538d3973236bfc2ccdd7f Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Wed, 30 Oct 2024 17:59:07 +0400 Subject: [PATCH 06/58] Added Chatbot Interaction Flow diagram to docs Uploaded 3 versions of the Chatbot Interaction Flow diagram to docs. --- docs/watson_flow.drawio | 155 ++++++++++++++++++++++++++++++++++++++++ docs/watson_flow.png | Bin 0 -> 64755 bytes docs/watson_flow.svg | 4 ++ 3 files changed, 159 insertions(+) create mode 100644 docs/watson_flow.drawio create mode 100644 docs/watson_flow.png create mode 100644 docs/watson_flow.svg diff --git a/docs/watson_flow.drawio b/docs/watson_flow.drawio new file mode 100644 index 00000000..6ca82d30 --- /dev/null +++ b/docs/watson_flow.drawio @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/watson_flow.png b/docs/watson_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..edb3a76f5d7a61a601bd1535e500e3d24a40e174 GIT binary patch literal 64755 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 + + +
Cookies consent form
Cookies consent form
Greeting
Greeting
University Life
University Life
SkillsBuild Courses
SkillsBuild Courses
Beginner (no prior knowledge)
Beginner (no prior k...
Intermediate (some prior knowledge)
Intermediate (some p...
Asks for duration preferences
Asks for duration pr...
Asks for areas of interest
Asks for areas of in...
IBM SkillsBuild Plaform
IBM SkillsBuild Plaf...
Advanced (lots of prior knowledge)
Advanced (lots of pr...
Short (1-6 hours)
Short (1-6 hours)
Medium (7-15 hours)
Medium (7-15 hou...
Long (16-20+ hours)
Long (16-20+ hours)
Provides a brief description of each course, including prerequisites, duration, and course link
Provides a brief des...
Academic Challenges (time management, studying advice...)
Academic Challenges...
Personal Well-being (social life, mental well-being...)
Personal Well-being...
Offers tips, suggestions, or university resources
Offers tips, suggest...
Provides guidance, links to support articles, SkillsBuild help pages, or a contact form for users needing direct assistance with platform navigation or account issues
Provides guidance, l...
Text is not SVG - cannot display
\ No newline at end of file From 74b046aa1737f29b0ab956756c0fab3cba21167c Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Wed, 30 Oct 2024 14:02:13 +0000 Subject: [PATCH 07/58] Updated link to Chatbot Interaction Flowchart in README Changed the link of watson_flow.png from https://github.com... to /docs/watson_flow.png --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e332458b..b058b197 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ User prompts for the chatbot will be sent using API requests from the Spring Boo This flowchart outlines the interaction pathways within the chatbot, guiding users through key topics such as SkillsBuild courses, university life questions, and IBM SkillsBuild platform information. Each pathway details the chatbot's prompts, and user responses, providing an overview of the chatbot’s functionality. -![watson_flow](https://github.com/user-attachments/assets/036f89eb-2ce5-4ed8-aa71-2e0d4e5e4021) +![watson_flow](/docs/watson_flow.png) ## Developer Instructions: To get started with developing or contributing to this project, follow the steps below: From e43bb2e273e0f0526330bc342a6c01824426546c Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Wed, 30 Oct 2024 23:02:38 +0000 Subject: [PATCH 08/58] Remove "frontend" branch Remove "frontend" from the branch list. --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index b79b6276..007a7866 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,9 +10,9 @@ name: Java CI with Maven on: push: - branches: [ "dev", "backend", "frontend" ] + branches: [ "dev", "backend" ] pull_request: - branches: [ "dev", "backend", "frontend" ] + branches: [ "dev", "backend" ] jobs: build: From f3fbbf7c8a6cb0aa2c1087bdbe6613a00809d006 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sun, 3 Nov 2024 18:35:08 +0000 Subject: [PATCH 09/58] Add Bearer token requestor. Add IBMAuthenticator.java and IBMAuthenticatorTest.java . IBMAuthenticator uses IBM IAM API key to receive temporary bearer tokens. Bearer tokens are used to make requests to Watsonx API. --- backend/pom.xml | 5 ++ .../UoB/AILearningTool/IBMAuthenticator.java | 80 +++++++++++++++++++ .../AILearningTool/IBMAuthenticatorTest.java | 38 +++++++++ 3 files changed, 123 insertions(+) create mode 100644 backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java create mode 100644 backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java diff --git a/backend/pom.xml b/backend/pom.xml index fce94172..65792ca6 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -46,6 +46,11 @@ org.springframework.boot spring-boot-starter-logging
+ + org.json + json + 20240303 + org.mariadb.jdbc diff --git a/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java b/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java new file mode 100644 index 00000000..482f2090 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java @@ -0,0 +1,80 @@ +package com.UoB.AILearningTool; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.json.JSONObject; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IBMAuthenticator extends Thread { + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final Logger log = LoggerFactory.getLogger(SpringController.class); + + private String bearerToken; + private Integer statusCode; + + // Returns a bearer token + public String getBearerToken() { + if (this.statusCode == null) { + System.out.println("Token is not available yet. Requesting a new token."); + requestNewToken(); + } + return this.bearerToken; + } + + // Return status code of the latest request + public int getStatusCode() { + if (this.statusCode == null) { + requestNewToken(); + } + return this.statusCode; + } + + // Uses IBM IAM API key to receive a temporary Bearer token. + public void requestNewToken() { + final String API_KEY = "wXU_-wyEW1tG1S8n4T3-6eiZ70Pfc2WxqXwqExsorjDH"; + + // Prepare a request with an API key to receive a Bearer token. + HttpRequest request = HttpRequest.newBuilder(URI.create("https://iam.cloud.ibm.com/identity/token")) + .headers("Content-Type", "application/x-www-form-urlencoded") + .POST( + HttpRequest.BodyPublishers.ofString( + "grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=" + API_KEY + ) + ).build(); + + try { + HttpResponse response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofString()); + this.statusCode = response.statusCode(); + if (this.statusCode == 200) { + this.bearerToken = new JSONObject(response.body()).getString("access_token"); + log.info("New Bearer token obtained."); + } + } catch (Exception e) { + this.bearerToken = null; + this.statusCode = 400; + log.error("Exception {}\nHTTP status code: {}", e, this.statusCode.toString()); + } + } + + // Finishes the current request and stops the scheduler. + public void stopTimer() { + this.scheduler.shutdown(); + log.info("Scheduler stopped."); + } + + // Runs a scheduler that executes requestNewToken() in specified frequency. + public void run() { + this.scheduler.scheduleAtFixedRate(this::requestNewToken, 0, 58, TimeUnit.MINUTES); + } +} diff --git a/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java b/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java new file mode 100644 index 00000000..f5f39475 --- /dev/null +++ b/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java @@ -0,0 +1,38 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Receiving IBM Bearer tokens.") +public class IBMAuthenticatorTest { + @Test + // Checking whether a single request works. + public void singleRequestTest() { + IBMAuthenticator testAuthenticator = new IBMAuthenticator(); + testAuthenticator.start(); + + String token1 = testAuthenticator.getBearerToken(); + + Assertions.assertEquals(200, testAuthenticator.getStatusCode()); + Assertions.assertNotEquals(null, token1); + + testAuthenticator.stopTimer(); + } + + @Test + // Checking whether multiple requests return unique tokens. + public void multiRequestTest() { + IBMAuthenticator testAuthenticator = new IBMAuthenticator(); + testAuthenticator.start(); + + String token1 = testAuthenticator.getBearerToken(); + testAuthenticator.requestNewToken(); + String token2 = testAuthenticator.getBearerToken(); + + Assertions.assertEquals(200, testAuthenticator.getStatusCode()); + Assertions.assertNotEquals(token1, token2); + + testAuthenticator.stopTimer(); + } +} From 76a972ee88b3c50b443dcc65fa73cbaf1e2f0b03 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 5 Nov 2024 14:48:33 +0000 Subject: [PATCH 10/58] Add Watsonx API Controller. Add WatsonxAPIController.java, WatsonxResponse.java. Rename RandomString.java to SpringTools.java, add method for preparing input strings. Refactor IBMAuthenticator.java . The code can now make requests to Watsonx API. --- .../UoB/AILearningTool/IBMAuthenticator.java | 2 +- .../com/UoB/AILearningTool/RandomString.java | 13 --- .../com/UoB/AILearningTool/StringTools.java | 26 ++++++ .../AILearningTool/WatsonxAPIController.java | 81 +++++++++++++++++++ .../UoB/AILearningTool/WatsonxResponse.java | 10 +++ .../AILearningTool/IBMAuthenticatorTest.java | 2 +- .../WatsonxAPIControllerTest.java | 56 +++++++++++++ 7 files changed, 175 insertions(+), 15 deletions(-) delete mode 100644 backend/src/main/java/com/UoB/AILearningTool/RandomString.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/StringTools.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java create mode 100644 backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java create mode 100644 backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java b/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java index 482f2090..e674bda1 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java +++ b/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java @@ -62,7 +62,7 @@ public void requestNewToken() { } } catch (Exception e) { this.bearerToken = null; - this.statusCode = 400; + this.statusCode = 500; log.error("Exception {}\nHTTP status code: {}", e, this.statusCode.toString()); } } diff --git a/backend/src/main/java/com/UoB/AILearningTool/RandomString.java b/backend/src/main/java/com/UoB/AILearningTool/RandomString.java deleted file mode 100644 index 876cb713..00000000 --- a/backend/src/main/java/com/UoB/AILearningTool/RandomString.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.UoB.AILearningTool; - -public class RandomString { - public static String make(int n) { - String newString = ""; - String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - for (int i = 0; i < n; i++) { - int index = (int)(characters.length() * Math.random()); - newString = newString + characters.charAt(index); - } - return newString; - } -} diff --git a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java new file mode 100644 index 00000000..2472010b --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java @@ -0,0 +1,26 @@ +package com.UoB.AILearningTool; + +// StringTools create / transform strings to the required formats. +public class StringTools { + + // Generates a random string of specific size (e.g. for userID / chatID) + public static String RandomString(int n) { + String newString = ""; + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + for (int i = 0; i < n; i++) { + int index = (int)(characters.length() * Math.random()); + newString = newString + characters.charAt(index); + } + return newString; + } + + // Prepares "input" JSON key value string for Watsonx API request + public static String messageHistoryPrepare(String input) { + String x = "{\"input\": \"" + input.replace("\n", "\\n") + .replace("\'", "'\''") + .replace("\"", "\\\"") + "\\n<|assistant|>\\n\","; + + System.out.println(x); + return x; + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java b/backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java new file mode 100644 index 00000000..ddd7df5c --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java @@ -0,0 +1,81 @@ +package com.UoB.AILearningTool; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.header.Header; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; + +public class WatsonxAPIController { + private final Logger log = LoggerFactory.getLogger(SpringController.class); + private final IBMAuthenticator authenticator; + + final static String technicalDataPayload = """ + "parameters": { + "decoding_method": "greedy", + "max_new_tokens": 900, + "min_new_tokens": 0, + "stop_sequences": [], + "repetition_penalty": 1.05 + }, + "model_id": "ibm/granite-13b-chat-v2", + "project_id": "30726e85-a528-491c-83a3-eee53797f0da" + } + """; + + public WatsonxAPIController() { + this.authenticator = new IBMAuthenticator(); + this.authenticator.start(); + this.authenticator.requestNewToken(); + } + + public void stopAuthenticator(){ + this.authenticator.stopTimer(); + } + + // Sends a message to Watsonx API. + public WatsonxResponse sendUserMessage(String preparedMessageHistory) { + String dataPayload = preparedMessageHistory + technicalDataPayload; + + WatsonxResponse WXResponse; + HttpRequest request = HttpRequest + .newBuilder(URI.create("https://eu-gb.ml.cloud.ibm.com/ml/v1/text/generation?version=2023-05-29")) + .headers("Content-Type", "application/json", + "Accept", "application/json", + "Authorization", ("Bearer " + authenticator.getBearerToken()) ) + .POST(HttpRequest.BodyPublishers.ofString(dataPayload)).build(); + + try { + HttpResponse response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofString()); + Integer statusCode = response.statusCode(); + if (statusCode == 200) { + String message = new JSONObject(response.body()) + .getJSONArray("results") + .getJSONObject(0) + .getString("generated_text"); + log.info("200 Watsonx response received."); + + WXResponse = new WatsonxResponse(statusCode, message); + } else { + log.warn("Non-200 Watsonx response received."); + String message = new JSONObject(response.body()) + .getJSONArray("errors") + .getJSONObject(0) + .getString("message"); + WXResponse = new WatsonxResponse(500, message); + } + } catch (Exception e) { + WXResponse = new WatsonxResponse(500, null); + log.error("Exception {}\nHTTP status code: {}", e, 500); + } + return WXResponse; + } +} diff --git a/backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java b/backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java new file mode 100644 index 00000000..01abc9ac --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java @@ -0,0 +1,10 @@ +package com.UoB.AILearningTool; + +public class WatsonxResponse { + public String responseText; + public Integer statusCode; + public WatsonxResponse(Integer statusCode, String responseText) { + this.statusCode = statusCode; + this.responseText = responseText; + } +} diff --git a/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java b/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java index f5f39475..09417224 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java @@ -7,7 +7,7 @@ @DisplayName("Receiving IBM Bearer tokens.") public class IBMAuthenticatorTest { @Test - // Checking whether a single request works. + @DisplayName("Checking whether a single request works") public void singleRequestTest() { IBMAuthenticator testAuthenticator = new IBMAuthenticator(); testAuthenticator.start(); diff --git a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java new file mode 100644 index 00000000..4b11bb9e --- /dev/null +++ b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java @@ -0,0 +1,56 @@ +package com.UoB.AILearningTool; + +import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@DisplayName("Checking the functionality of WatsonxAPIController.") +public class WatsonxAPIControllerTest { + + @Test + @DisplayName("Response from Watsonx API with status code 200 and size > 0 can be received") + public void watsonxSingleValidResponseTest() { + final Logger log = LoggerFactory.getLogger(SpringController.class); + + final String unformattedTestInput = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, g'\'day, morning, afternoon, evening, night, what\'s up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nhello, please introduce yourself."; + final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); + + WatsonxAPIController testWXController = new WatsonxAPIController(); + + WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); + + testWXController.stopAuthenticator(); + + Assertions.assertEquals(200, testMessage.statusCode); + Assertions.assertTrue(testMessage.responseText.length() > 1); + } + + @Test + @DisplayName("Multiple valid responses from Watsonx API with status code 200 and size > 0 can be received") + public void watsonxMultiValidResponsesTest() { + final Logger log = LoggerFactory.getLogger(SpringController.class); + + final String unformattedTestInput = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, g'\'day, morning, afternoon, evening, night, what\'s up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nhello, please introduce yourself."; + final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); + + WatsonxAPIController testWXController = new WatsonxAPIController(); + + for (int i = 1; i < 11; i++) { + log.info("Valid response N{}/10 received", i); + WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); + Assertions.assertEquals(200, testMessage.statusCode); + Assertions.assertTrue(testMessage.responseText.length() > 1); + } + + testWXController.stopAuthenticator(); + } + +} From fe705ab5c39b7bbea3b4d4fd5fde18c9486103c7 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 5 Nov 2024 23:15:42 +0000 Subject: [PATCH 11/58] Improve User.java User.java now stores the value of consent to optional cookies. Add UserTest.java - unit tests for 'User' class. --- .../java/com/UoB/AILearningTool/User.java | 14 ++++++++++-- .../java/com/UoB/AILearningTool/UserTest.java | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 backend/src/test/java/com/UoB/AILearningTool/UserTest.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/User.java b/backend/src/main/java/com/UoB/AILearningTool/User.java index 59a3d492..bda2cbbf 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/User.java +++ b/backend/src/main/java/com/UoB/AILearningTool/User.java @@ -1,23 +1,33 @@ package com.UoB.AILearningTool; import java.time.LocalDateTime; +import java.util.ArrayList; // Class representing a user profile (someone who consented to optional cookies). public class User { private final String id; + private boolean optionalConsent = false; private LocalDateTime lastActivityTime; public String getID() { + updateLastActivityTime(); return this.id; } + public boolean getOptionalConsent() { + updateLastActivityTime(); + return this.optionalConsent; + } + public void updateLastActivityTime() { this.lastActivityTime = LocalDateTime.now(); } + // Create a user - public User() { + public User(boolean optionalConsent) { updateLastActivityTime(); - this.id = RandomString.make(25); + this.id = StringTools.RandomString(25); + this.optionalConsent = optionalConsent; } } diff --git a/backend/src/test/java/com/UoB/AILearningTool/UserTest.java b/backend/src/test/java/com/UoB/AILearningTool/UserTest.java new file mode 100644 index 00000000..c3cff525 --- /dev/null +++ b/backend/src/test/java/com/UoB/AILearningTool/UserTest.java @@ -0,0 +1,22 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class UserTest { + @Test + @DisplayName("Checking whether 'User' constructor creates an object with optional consent correctly") + public void correctFullConsentUserCreationTest() { + User user = new User(true); + Assertions.assertEquals(25, user.getID().length()); + Assertions.assertTrue(user.getOptionalConsent()); + } + @Test + @DisplayName("Checking whether 'User' constructor creates an object without optional consent correctly") + public void correctRequiredConsentUserCreationTest() { + User user = new User(false); + Assertions.assertEquals(25, user.getID().length()); + Assertions.assertFalse(user.getOptionalConsent()); + } +} From 354c15baf5ecebb17bd8a92d4493426182f9da60 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 6 Nov 2024 00:49:30 +0000 Subject: [PATCH 12/58] Add Chat.java Chat.java stores information about the chat - its owner and message history. It allows to add the user's and the AI's messages to its history. ChatTest.java contains unit tests for the Chat class. --- .../java/com/UoB/AILearningTool/Chat.java | 41 ++++++++ .../java/com/UoB/AILearningTool/ChatTest.java | 98 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 backend/src/main/java/com/UoB/AILearningTool/Chat.java create mode 100644 backend/src/test/java/com/UoB/AILearningTool/ChatTest.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/Chat.java b/backend/src/main/java/com/UoB/AILearningTool/Chat.java new file mode 100644 index 00000000..54a14232 --- /dev/null +++ b/backend/src/main/java/com/UoB/AILearningTool/Chat.java @@ -0,0 +1,41 @@ +package com.UoB.AILearningTool; + +import java.util.ArrayList; + +public class Chat { + final private String owner; + private String messageHistory; + + public Chat(User user, String initialMessage) { + this.owner = user.getID(); + this.messageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\n" + initialMessage; + } + + public Boolean checkOwner(User user) { + return this.owner.equals(user.getID()); + } + + public boolean addUserMessage(String userID, String message) { + if (! this.owner.equals(userID)) { + return false; + } else { + this.messageHistory += "\n<|user|>\n" + message; + return true; + } + } + + public boolean addAIMessage(String userID, String message) { + if (this.owner.equals(userID)) { + this.messageHistory += "\n<|assistant|>\n" + message; + return true; + } else { + return false; + } + } + + public String getMessageHistory(User user) { + if (checkOwner(user)) { + return this.messageHistory; + } else {return null;} + } +} diff --git a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java new file mode 100644 index 00000000..bed42ff2 --- /dev/null +++ b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java @@ -0,0 +1,98 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +public class ChatTest { + @Test + @DisplayName("Check if checkOwner method compares owners correctly.") + public void checkOwnerTest() { + User user = new User(true); + Chat chat = new Chat(user, "This is a first message."); + + Assertions.assertTrue(chat.checkOwner(user)); + } + + @Test + @DisplayName("Check if initial message history is saved correctly.") + public void initialMessageHistoryTest() { + String initialMessage = "This is a first message."; + String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message."; + User user = new User(true); + Chat chat = new Chat(user, initialMessage); + + String actualMessageHistory = chat.getMessageHistory(user); + Assertions.assertEquals(expectedMessageHistory, actualMessageHistory); + } + + @Test + @DisplayName("Check if user messages are added to message history correctly.") + public void addUserMessageTest() { + String initialMessage = "This is a first message."; + String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message.\n<|user|>\nTell me a joke."; + String extraUserMessage = "Tell me a joke."; + + // Create a chat + User user = new User(true); + Chat chat = new Chat(user, initialMessage); + + // Add a new user message to the chat + chat.addUserMessage(user.getID(), extraUserMessage); + + String actualMessageHistory = chat.getMessageHistory(user); + Assertions.assertEquals(expectedMessageHistory, actualMessageHistory); + } + + @Test + @DisplayName("Check if AI messages are added to message history correctly.") + public void addAIMessageTest() { + String initialMessage = "This is a first message."; + String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message.\n<|assistant|>\nTell me a joke."; + String extraUserMessage = "Tell me a joke."; + + // Create a chat + User user = new User(true); + Chat chat = new Chat(user, initialMessage); + + // Add a new user message to the chat + chat.addAIMessage(user.getID(), extraUserMessage); + + String actualMessageHistory = chat.getMessageHistory(user); + Assertions.assertEquals(expectedMessageHistory, actualMessageHistory); + } + + @Test + @DisplayName("Check if message history in a chat can only be accessed by its owner.") + public void chatMessageHistoryPermissionTest() { + final String initialMessage = "This is a first message."; + String actualMessageHistory; + User currentUser; + Chat currentChat; + ArrayList users = new ArrayList<>(); + ArrayList chats = new ArrayList<>(); + + // Creating chats and checking whether their owners can access their message histories. + for (int i = 0; i < 20; i++) { + currentUser = new User(true); + users.add(currentUser); + currentChat = new Chat(currentUser, initialMessage); + chats.add(currentChat); + // Message history has to be returned to its owner + Assertions.assertNotNull(currentChat.getMessageHistory(currentUser)); + } + + for (int i = 0; i < 20; i++) { + currentChat = chats.get(i); + // Users that aren't owners of the chat will receive null instead of message history. + for (int j = 0; j < 20; j++) { + if (i == j) {continue;} + currentUser = users.get(j); + actualMessageHistory = currentChat.getMessageHistory(currentUser); + Assertions.assertNull(actualMessageHistory); + } + } + } +} From 54a939e738d6acb10b3479fa2ee217f6f5fb951f Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 6 Nov 2024 08:54:06 +0000 Subject: [PATCH 13/58] Refactor DatabaseController and StringTools DatabaseController now has getUser method, improved addUser and removeUser methods. DatabaseController now has new createChat, deleteChat and getChat methods. Add DatabaseController unit tests. SpringTools.messageHistoryPrepare now appends 'assistant' keyword before replacements. --- .../AILearningTool/DatabaseController.java | 54 +++++++-- .../com/UoB/AILearningTool/StringTools.java | 3 +- .../DatabaseControllerTest.java | 103 ++++++++++++++++++ 3 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java index 19234a73..3a7f0baa 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java +++ b/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java @@ -7,23 +7,63 @@ // Communication with SQL database. public class DatabaseController { private Map users = new HashMap<>(); + private Map chats = new HashMap<>(); + + public User getUser(String userID) { + return users.get(userID); + } public DatabaseController() { // TODO: Connect to a MariaDB database. } // Create a new user and return their ID for cookie assignment - public String addUser() { - String id = RandomString.make(20); - // TODO: Add a user profile record to the database. - users.put(id, new User()); + public String addUser(boolean optionalConsent) { + User user = new User(optionalConsent); + String id = user.getID(); + // TODO: Add a user profile record to the MariaDB database. + users.put(id, user); return id; } // Remove all data stored about the user (profile, chat, etc.) - public void removeUser(String id) { - // TODO: Remove a user profile record from the database. - users.remove(id); + public boolean removeUser(String id) { + // TODO: Remove a user profile record from the MariaDB database. + if (users.containsKey(id)) { + users.remove(id); + return true; + } else {return false;} + } + + // Creates a new chat + public String createChat(User user, String initialMessage) { + String id = StringTools.RandomString(20); + chats.put(id, new Chat(user, initialMessage)); + return id; + } + + // Deletes an existing chat + public Boolean deleteChat(User user, String chatID) { + Boolean success; + Chat chat = chats.get(chatID); + if (chat != null) { + if (chat.checkOwner(user)) { + chats.remove(chatID); + return true; + } + } + return false; + } + + public Chat getChat(User user, String chatID) { + Chat chat = chats.get(chatID); + if (chat != null) { + if (chat.checkOwner(user)) { + return chat; + } else {return null;} + } else { + return null; + } } } diff --git a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java index 2472010b..9f3e757e 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java +++ b/backend/src/main/java/com/UoB/AILearningTool/StringTools.java @@ -16,9 +16,10 @@ public static String RandomString(int n) { // Prepares "input" JSON key value string for Watsonx API request public static String messageHistoryPrepare(String input) { + input = input + "\n<|assistant|>\n"; String x = "{\"input\": \"" + input.replace("\n", "\\n") .replace("\'", "'\''") - .replace("\"", "\\\"") + "\\n<|assistant|>\\n\","; + .replace("\"", "\\\"") + "\","; System.out.println(x); return x; diff --git a/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java new file mode 100644 index 00000000..6ee3db56 --- /dev/null +++ b/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java @@ -0,0 +1,103 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +public class DatabaseControllerTest { + @Test + @DisplayName("Check whether users can be created.") + public void createUsers() { + DatabaseController DBC = new DatabaseController(); + ArrayList usernames = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + usernames.add(DBC.addUser(true)); + } + + for (String username : usernames) { + User user = DBC.getUser(username); + Assertions.assertEquals(username, user.getID()); + } + } + + @Test + @DisplayName("Check whether users can be deleted.") + public void deleteUsers() { + DatabaseController DBC = new DatabaseController(); + ArrayList usernames = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + usernames.add(DBC.addUser(true)); + } + + // Deleting all users + for (String username : usernames) { + DBC.removeUser(username); + } + + // Search for non-existing user must return null + for (String username : usernames) { + Assertions.assertNull(DBC.getUser(username)); + } + } + + @Test + @DisplayName("Check whether chats can be created and accessed.") + public void createChats() { + DatabaseController DBC = new DatabaseController(); + ArrayList users = new ArrayList<>(); + ArrayList chatIDs = new ArrayList<>(); + + // Create users. + for (int i = 0; i < 20; i++) { + users.add(DBC.getUser(DBC.addUser(true))); + } + + // Create chats. + for (User user : users) { + chatIDs.add(DBC.createChat(user, "This is a first message.")); + } + + // DBC.getChat() must return a non-null element. + for (int i = 0; i < 20; i++) { + Chat actualChat = DBC.getChat(users.get(i), chatIDs.get(i)); + Assertions.assertNotNull(actualChat); + } + } + + @Test + @DisplayName("Check whether chats can only be accessed by their owners.") + public void accessChatPermissionTest() { + Chat currentChat; + User currentUser; + DatabaseController DBC = new DatabaseController(); + ArrayList users = new ArrayList<>(); + ArrayList chatIDs = new ArrayList<>(); + + // Create users. + for (int i = 0; i < 20; i++) { + users.add(DBC.getUser(DBC.addUser(true))); + } + + // Create chats. + for (User user : users) { + chatIDs.add(DBC.createChat(user, "This is a first message.")); + } + + for (int i = 0; i < 20; i++) { + // If currentUser is the chat owner, Chat object is returned. + currentUser = users.get(i); + currentChat = DBC.getChat(currentUser, chatIDs.get(i)); + Assertions.assertNotNull(currentChat); + for (int j = 0; j < 20; j++) { + if (i == j) {continue;} + // If currentUser isn't the chat owner, null object is returned. + currentUser = users.get(j); + currentChat = DBC.getChat(currentUser, chatIDs.get(i)); + Assertions.assertNull(currentChat); + } + } + } + +} From 45ff1c63cf80207f69b9a2c1b81b517f6aa84e04 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 6 Nov 2024 10:50:10 +0000 Subject: [PATCH 14/58] Add the right AI system prompt. Hardcode the correct system prompt to Chat.java . Update ChatTest and WatsonxAPIControllerTest . --- backend/src/main/java/com/UoB/AILearningTool/Chat.java | 3 ++- .../src/test/java/com/UoB/AILearningTool/ChatTest.java | 6 +++--- .../com/UoB/AILearningTool/WatsonxAPIControllerTest.java | 9 +++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/UoB/AILearningTool/Chat.java b/backend/src/main/java/com/UoB/AILearningTool/Chat.java index 54a14232..7711e1a6 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/Chat.java +++ b/backend/src/main/java/com/UoB/AILearningTool/Chat.java @@ -8,7 +8,8 @@ public class Chat { public Chat(User user, String initialMessage) { this.owner = user.getID(); - this.messageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\n" + initialMessage; + this.messageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\n" + initialMessage; + } public Boolean checkOwner(User user) { diff --git a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java index bed42ff2..a5baadd1 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java @@ -20,7 +20,7 @@ public void checkOwnerTest() { @DisplayName("Check if initial message history is saved correctly.") public void initialMessageHistoryTest() { String initialMessage = "This is a first message."; - String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message."; + String expectedMessageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; User user = new User(true); Chat chat = new Chat(user, initialMessage); @@ -32,7 +32,7 @@ public void initialMessageHistoryTest() { @DisplayName("Check if user messages are added to message history correctly.") public void addUserMessageTest() { String initialMessage = "This is a first message."; - String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message.\n<|user|>\nTell me a joke."; + String expectedMessageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message.\n<|user|>\nTell me a joke."; String extraUserMessage = "Tell me a joke."; // Create a chat @@ -50,7 +50,7 @@ public void addUserMessageTest() { @DisplayName("Check if AI messages are added to message history correctly.") public void addAIMessageTest() { String initialMessage = "This is a first message."; - String expectedMessageHistory = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, good morning, good afternoon, good evening, , what's up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nThis is a first message.\n<|assistant|>\nTell me a joke."; + String expectedMessageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message.\n<|assistant|>\nTell me a joke."; String extraUserMessage = "Tell me a joke."; // Create a chat diff --git a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java index 4b11bb9e..bf81ad08 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java @@ -20,7 +20,7 @@ public class WatsonxAPIControllerTest { public void watsonxSingleValidResponseTest() { final Logger log = LoggerFactory.getLogger(SpringController.class); - final String unformattedTestInput = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, g'\'day, morning, afternoon, evening, night, what\'s up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nhello, please introduce yourself."; + final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); WatsonxAPIController testWXController = new WatsonxAPIController(); @@ -38,16 +38,17 @@ public void watsonxSingleValidResponseTest() { public void watsonxMultiValidResponsesTest() { final Logger log = LoggerFactory.getLogger(SpringController.class); - final String unformattedTestInput = "<|system|>\nYou are Granite Chat, an AI language model developed by IBM. You are a cautious assistant. You carefully follow instructions. You are helpful and harmless and you follow ethical guidelines and promote positive behavior. You always respond to greetings (for example, hi, hello, g'\'day, morning, afternoon, evening, night, what\'s up, nice to meet you, sup, etc) with \"Hello! I am Granite Chat, created by IBM. How can I help you today?\". Please do not say anything else and do not start a conversation.\n<|user|>\nhello, please introduce yourself."; + final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); WatsonxAPIController testWXController = new WatsonxAPIController(); - for (int i = 1; i < 11; i++) { - log.info("Valid response N{}/10 received", i); + for (int i = 1; i < 4; i++) { WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); Assertions.assertEquals(200, testMessage.statusCode); Assertions.assertTrue(testMessage.responseText.length() > 1); + log.info("Valid response N{}/10 received", i); + log.info("response: {}", testMessage.responseText); } testWXController.stopAuthenticator(); From 56f5df31696870df23bf6e85e36909bedef1a43d Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 6 Nov 2024 11:34:43 +0000 Subject: [PATCH 15/58] Improve SpringContoller.java Improve signup and revokeConsent methods. Add createChat, sendMessage, sendIncognitoMessage, getChatHistory methods. --- .../UoB/AILearningTool/SpringController.java | 161 ++++++++++++++++-- .../AiLearningToolApplicationTests.java | 31 ++++ 2 files changed, 174 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java index da0e59a6..d7d1be67 100644 --- a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java +++ b/backend/src/main/java/com/UoB/AILearningTool/SpringController.java @@ -2,29 +2,29 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.bind.annotation.CookieValue; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestAttribute; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + @RestController public class SpringController { private final Logger log = LoggerFactory.getLogger(SpringController.class); private final DatabaseController DBC = new DatabaseController(); + private final WatsonxAPIController WXC = new WatsonxAPIController(); - // If user consents to optional cookies, assign a unique user ID for them. + // Assign a unique user ID for the user. @GetMapping("/signup") public void signup(@CookieValue(value = "optionalConsent", defaultValue = "false") boolean optionalConsent, HttpServletResponse response) { - if (optionalConsent) { - Cookie userIDCookie = new Cookie("userID", DBC.addUser()); - userIDCookie.setMaxAge(30 * 24 * 60 * 60); // Cookie will expire in 30 days - userIDCookie.setSecure(true); - response.addCookie(userIDCookie); - log.info("Assigned a new userID."); - } + Cookie userIDCookie = new Cookie("userID", DBC.addUser(optionalConsent)); + userIDCookie.setMaxAge(30 * 24 * 60 * 60); // Cookie will expire in 30 days + userIDCookie.setSecure(false); +// userIDCookie.setAttribute("SameSite", "None"); + userIDCookie.setPath("/"); + response.addCookie(userIDCookie); + log.info("Assigned a new userID."); } // If user revokes their consent for data storage / optional cookies, @@ -32,12 +32,137 @@ public void signup(@CookieValue(value = "optionalConsent", defaultValue = "false @GetMapping("/revokeConsent") public void revokeConsent(@CookieValue(value = "userID", defaultValue = "") String userID, HttpServletResponse response) { - if (userID.isEmpty()) { - log.info("Cannot withdraw consent of userID {}", userID); + if (DBC.removeUser(userID)) { + response.setStatus(200); + Cookie cookie = new Cookie("userID", ""); + cookie.setMaxAge(0); + response.addCookie(cookie); + log.info("Revoked consent for user, ID: {}", userID); + } else { + response.setStatus(400); + } + + } + + // Create a new chat session with Watsonx + @GetMapping("/createChat") + public void createChat(@CookieValue(value = "userID", defaultValue = "") String userID, + @RequestParam(name = "initialMessage") String initialMessage, + HttpServletResponse response) { + response.setContentType("text/plain"); // Set the content type to text + + // Create a chat + User user = DBC.getUser(userID); + if (user != null) { + WatsonxResponse wresponse; + String chatID = DBC.createChat(user, initialMessage); + + // Send the message history (system prompt and initial message) to Watsonx API, + // add the AI response to the message history of the chat. + try { + wresponse = WXC.sendUserMessage(DBC.getChat(DBC.getUser(userID), chatID).getMessageHistory(user)); + response.getWriter().write(chatID); + response.setStatus(wresponse.statusCode); + if (wresponse.statusCode == 200) { + DBC.getChat(DBC.getUser(userID), chatID).addAIMessage(userID, wresponse.responseText); + } + } catch (IOException e) { + log.warn(String.valueOf(e)); + } + } else { + try { + response.getWriter().write("null"); + response.setStatus(401); + } catch (IOException e) { + log.warn(String.valueOf(e)); + } + } + } + + // Send a message to the chat and receive a response + @GetMapping("/sendMessage") + public void sendMessage(@CookieValue(value = "userID", defaultValue = "") String userID, + @RequestParam(name = "newMessage") String newMessage, + @RequestParam(name = "chatID") String chatID, + HttpServletResponse response) { + Chat chat = DBC.getChat(DBC.getUser(userID), chatID); + String inputString = chat.getMessageHistory(DBC.getUser(userID)); + + // Default response - Unauthorised + WatsonxResponse wresponse = new WatsonxResponse(401, ""); + response.setContentType("text/plain"); + response.setStatus(401); + + if (chat != null && inputString != null) { + // If a message can be added to the message history of a chat, then send the message history + // to Watsonx API. + boolean success = chat.addUserMessage(userID, newMessage); + if (success) { + wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); + response.setStatus(wresponse.statusCode); + } + try { + if (wresponse.statusCode == 200) { + chat.addAIMessage(userID, wresponse.responseText); + } + response.getWriter().write(wresponse.responseText); + } catch (IOException e) { + log.warn(String.valueOf(e)); + response.setStatus(500); + } + } + } + + // Send a message history and receive a response from Watsonx. + // Used for users who declined optional cookies. + @GetMapping("/sendIncognitoMessage") + public void sendIncognitoMessage(@CookieValue(value = "userID") String userID, + @RequestParam(name = "inputString") String inputString, + HttpServletResponse response) { + log.warn("ID {}, newMessage: {}", userID, inputString); + User user = DBC.getUser(userID); + WatsonxResponse wresponse; + response.setContentType("text/plain"); + + if (user != null) { + wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); + response.setStatus(wresponse.statusCode); + try { + response.getWriter().write(wresponse.responseText); + log.warn(wresponse.responseText); + } catch (IOException e) { + log.warn(String.valueOf(e)); + response.setStatus(500); + } + } else { + log.warn("else {}", DBC.getUser(userID) == null); + response.setStatus(401); + } + } + + // Get message history from a chat + @GetMapping("/getChatHistory") + public void getChatHistory(@CookieValue(value = "userID", defaultValue = "") String userID, + @RequestParam(name = "chatID") String chatID, + HttpServletResponse response) { + Chat chat = DBC.getChat(DBC.getUser(userID), chatID); + String messageHistory = chat.getMessageHistory(DBC.getUser(userID)); + if (chat != null && messageHistory != null) { + response.setContentType("text/plain"); + response.setStatus(200); + try { + response.getWriter().write(messageHistory); + } catch (IOException e) { + log.warn(String.valueOf(e)); + } + } else { + response.setContentType("text/plain"); + response.setStatus(401); + try { + response.getWriter().write(""); + } catch (IOException e) { + log.warn(String.valueOf(e)); + } } - Cookie cookie = new Cookie("userID", ""); - cookie.setMaxAge(0); - response.addCookie(cookie); - DBC.removeUser(userID); } } diff --git a/backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java b/backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java index d6dcb73d..cb04fb58 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java +++ b/backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java @@ -1,13 +1,44 @@ package com.UoB.AILearningTool; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.ResponseEntity; + @SpringBootTest class AiLearningToolApplicationTests { + @LocalServerPort + private int port; + + /*@Autowired + private AiLearningToolApplication controller; + + private final RestTemplate restTemplate = new RestTemplate(); + @Test + @DisplayName("Verify that the application context loads successfully.") void contextLoads() { + Assertions.assertNotNull(controller); } + @Test + @DisplayName("Verify that the '/signup' endpoint creates a user ID cookie.") + void signupEndpointTest() { + // Act + ResponseEntity response = restTemplate.getForEntity("http://localhost:" + port + "/signup", String.class); + + // Assert + Assertions.assertNotNull(response.getHeaders().get("Set-Cookie"), "Response should contain a 'Set-Cookie' header."); + Assertions.assertTrue(response.getHeaders().get("Set-Cookie").toString().contains("userID"), "Cookie should contain 'userID'."); + } + + */ } + + From cf842651640b3013e1e75d4022bd2352b1a4967c Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 7 Nov 2024 20:30:55 +0000 Subject: [PATCH 16/58] refactor: Temporary disable AI API tests. Due to issues with IBM APIs, temporary disable WatsonxAPIControllerTest tests. --- .../WatsonxAPIControllerTest.java | 77 ++++++++++--------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java index bf81ad08..8cc8a12c 100644 --- a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java +++ b/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java @@ -15,43 +15,44 @@ @DisplayName("Checking the functionality of WatsonxAPIController.") public class WatsonxAPIControllerTest { - @Test - @DisplayName("Response from Watsonx API with status code 200 and size > 0 can be received") - public void watsonxSingleValidResponseTest() { - final Logger log = LoggerFactory.getLogger(SpringController.class); - - final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; - final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); - - WatsonxAPIController testWXController = new WatsonxAPIController(); - - WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); - - testWXController.stopAuthenticator(); - - Assertions.assertEquals(200, testMessage.statusCode); - Assertions.assertTrue(testMessage.responseText.length() > 1); - } - - @Test - @DisplayName("Multiple valid responses from Watsonx API with status code 200 and size > 0 can be received") - public void watsonxMultiValidResponsesTest() { - final Logger log = LoggerFactory.getLogger(SpringController.class); - - final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; - final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); - - WatsonxAPIController testWXController = new WatsonxAPIController(); - - for (int i = 1; i < 4; i++) { - WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); - Assertions.assertEquals(200, testMessage.statusCode); - Assertions.assertTrue(testMessage.responseText.length() > 1); - log.info("Valid response N{}/10 received", i); - log.info("response: {}", testMessage.responseText); - } - - testWXController.stopAuthenticator(); - } +// @Test +// @DisplayName("Response from Watsonx API with status code 200 and size > 0 can be received") +// public void watsonxSingleValidResponseTest() { +// final Logger log = LoggerFactory.getLogger(SpringController.class); +// +// final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; +// final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); +// +// WatsonxAPIController testWXController = new WatsonxAPIController(); +// +// WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); +// +// testWXController.stopAuthenticator(); +// log.info(testMessage.responseText); +// +// Assertions.assertEquals(200, testMessage.statusCode); +// Assertions.assertTrue(testMessage.responseText.length() > 1); +// } +// +// @Test +// @DisplayName("Multiple valid responses from Watsonx API with status code 200 and size > 0 can be received") +// public void watsonxMultiValidResponsesTest() { +// final Logger log = LoggerFactory.getLogger(SpringController.class); +// +// final String unformattedTestInput = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; +// final String formattedTestInput = StringTools.messageHistoryPrepare(unformattedTestInput); +// +// WatsonxAPIController testWXController = new WatsonxAPIController(); +// +// for (int i = 1; i < 4; i++) { +// WatsonxResponse testMessage = testWXController.sendUserMessage(formattedTestInput); +// Assertions.assertEquals(200, testMessage.statusCode); +// Assertions.assertTrue(testMessage.responseText.length() > 1); +// log.info("Valid response N{}/10 received", i); +// log.info("response: {}", testMessage.responseText); +// } +// +// testWXController.stopAuthenticator(); +// } } From 45d95130fec5cff89ed3cf4db767e69f78f50da1 Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 7 Nov 2024 21:00:02 +0000 Subject: [PATCH 17/58] chore: add backend documentation. Add backendHTTPMethods flowchart; Add userRegistrationFlowchart flowchart; Both flowcharts are saved to 'docs' directory. --- docs/backendHTTPMethods.drawio | 67 ++++++++++++++++++++ docs/backendHTTPMethods.png | Bin 0 -> 64683 bytes docs/backendHTTPMethods.svg | 4 ++ docs/userRegistrationFlowchart.drawio | 86 ++++++++++++++++++++++++++ docs/userRegistrationFlowchart.png | Bin 0 -> 71071 bytes docs/userRegistrationFlowchart.svg | 4 ++ 6 files changed, 161 insertions(+) create mode 100644 docs/backendHTTPMethods.drawio create mode 100644 docs/backendHTTPMethods.png create mode 100644 docs/backendHTTPMethods.svg create mode 100644 docs/userRegistrationFlowchart.drawio create mode 100644 docs/userRegistrationFlowchart.png create mode 100644 docs/userRegistrationFlowchart.svg diff --git a/docs/backendHTTPMethods.drawio b/docs/backendHTTPMethods.drawio new file mode 100644 index 00000000..3a169580 --- /dev/null +++ b/docs/backendHTTPMethods.drawio @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/backendHTTPMethods.png b/docs/backendHTTPMethods.png new file mode 100644 index 0000000000000000000000000000000000000000..3993d5c46df07738ba453063e7dadbf47f8eb283 GIT binary patch literal 64683 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

o1tFrZWDVO{5ft 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 TwDN}n{u!P$)qSMx9P_^b_(V2~ literal 0 HcmV?d00001 diff --git a/docs/backendHTTPMethods.svg b/docs/backendHTTPMethods.svg new file mode 100644 index 00000000..76343940 --- /dev/null +++ b/docs/backendHTTPMethods.svg @@ -0,0 +1,4 @@ + + + +
GET /signup
GET /signup
Receives:
optionalConsent cookie
Receives:...
Returns:
userID cookie
Returns:...
GET /revokeConsent
GET /revokeConsent
Receives:
optionalConsent cookie
Receives:...
Returns:
userID cookie
Returns:...
GET /createChat
GET /createChat
Receives:
userID cookie
initialMessage string
Receives:...
Returns:
chatID text/plain
Returns:...
GET /sendMessage
GET /sendMessage
Receives:
userID cookie
newMessage string
chatID string
Receives:...
Returns:
responseText text/plain
Returns:...
GET /sendIncognitoMessage
GET /sendIncognit...
Receives:
userID cookie
inputString string
Receives:...
Returns:
responseText text/plain
Returns:...
GET /getChatHistory
GET /getChatHisto...
Receives:
userID cookie
chatID string
Receives:...
Returns:
responseText text/plain
Returns:...
Backend HTTP methods
Backend HTTP methods
\ No newline at end of file diff --git a/docs/userRegistrationFlowchart.drawio b/docs/userRegistrationFlowchart.drawio new file mode 100644 index 00000000..a69aeb8f --- /dev/null +++ b/docs/userRegistrationFlowchart.drawio @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/userRegistrationFlowchart.png b/docs/userRegistrationFlowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ada594454fd4271e59bb24a4581469ff7c54ea GIT binary patch literal 71071 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

o>{?gm4Uah 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%0ACn8mbcWvBr{xN&l{D48# zcA@B^h6Pepw%HZYpoC14*;)&_V|j%;%Ea*mH2syh|K|elKNHLfo+f-BIj1El*XiuS zgRxEeFDX(l`4K39=+e;Nzv|6re)VJSmu&M9Ia~Jf*CD1}R}cRHb6SM-roFiBW#GkA7-}I!ea{izC0MN8J!dnV|6;9AqF_SQXs`G7Kb?-RgcY<-ek5uqn3swt z&wQSKf~LqFWZ(aK()wxUXDzAQoX_^1AEotQMAK`A;{+0eyiccyYL&&$S>#^Wgy`I1 zjm68fu-+?^mji-@LR=)i^7c!0nj6$G_Woh3+3PONr0k-Ax=#KV9SGlr^SWzO=soK` z36j_dobY zeOWeOR>BiNF75v<8BRP20jLP{1VHuaN|v&xWQbKySF1OX+XlYGZL44QfQ%ysT$?m{22cE3+$<+H0;fz+G$_IY!z5qns?G~v z6Z26OEJ_T;D5old7F)e5U;c^+ zHpnmZRFz!;hP1M+n4^-3Mr{53OLF}-6OB}>iWlOA50hr7l=aO!Te}u({)K>J;JBwp5cMeM>!i>&ir z1m0g6SlT`x*hXrA38SdzsPHvr#Ri&rv|3%yYefo+xL0X)yt-fEi}wzfqz;uJriBgT zVWY!egJKT5d1++f6Jqh!BP1oC_6+~_N1wK7IN}H9HuKEfY2izsyHKZFn3_;dXWSB+ zRT6u^`?#CE4zOSU@$g`NJwi&WUu2%LEIk2H702nD{)V+WmLuIGyHYiC?oi|>2w_Jb z_zgU*z9dBBJXvF%^K}u;&95539z|FxhxZMZS?+9Rx>OboNi2hG;&y|v)brkf0)@c} z+WEt|8O$`&Ei_8stxu{7&;qAwZbw`ZnvPx$QfMRLljp8f^rt*tr42t3N1CVKd>ocQ z_z#=j^5>%b(WQXS=S1^z_{wy@)TVkd8fL8ndT#5VkRQ|y>4XYns*ld^2L6cIr}#nMTzI&WK{|#F<=f zc-Xw*o~I!rO)`D(&P6==!KSSLbHFiOw&$g<^PrL6yFwsXLzTJenHJhea8Ah7`yAq; z>Nj5DfP}p$s44xQm;Qg@fM~KzK(H;q3nL9)g_WsTNsE6QuInfNWw^dJAzPtZ4nL^L z4;Shv1Vd3p?>sZXNCQ%x7j)F}3W!GJZZ%(LqJ3Mp%eamXDf{+Hf0e}srM1|$)&DD4 z^LCuV%bO*7HN3vVH6?zqovXQGF?*>3&IcPUTPh&%;N~=r^(AcT%6DQHPEW=dBJiJy MfjP1206Fge0GR_C=Kufz literal 0 HcmV?d00001 diff --git a/docs/userRegistrationFlowchart.svg b/docs/userRegistrationFlowchart.svg new file mode 100644 index 00000000..64e759e7 --- /dev/null +++ b/docs/userRegistrationFlowchart.svg @@ -0,0 +1,4 @@ + + + +
Yes
Yes
No
No
Set optionalConsent cookie to true
Set optionalConsent cookie...
Set optionalConsent cookie to false
Set optionalConsent cookie...
Send GET /signup request to backend
Send GET /signup request to...
Save userID cookie received from response
Save userID cookie received...
User opens the webapp for
the first time
User opens the webapp for...
Does the user consent to optional cookies?
Does the user consent to optional cookies?
Backend generates userID and returns it with a cookie in the response
Backend generates userID an...
User registration process
User registration process
\ No newline at end of file From 735aef0f43b069791c07e06f0a936637850b7e79 Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 7 Nov 2024 23:28:18 +0000 Subject: [PATCH 18/58] refactor: Change folder structure (for merge into 'dev'). Move backend files out of 'backend' directory into /; Remove 'backend' directory; Edit the Maven Github workflow runner (runs only on 'dev' branch now); Adjust .gitignore. --- .github/workflows/maven.yml | 35 +++++++++++++++++++ .gitignore | 4 ++- backend/HELP.md => HELP.md | 0 backend/mvnw => mvnw | 0 backend/mvnw.cmd => mvnw.cmd | 0 backend/pom.xml => pom.xml | 0 .../AiLearningToolApplication.java | 0 .../java/com/UoB/AILearningTool/Chat.java | 0 .../AILearningTool/DatabaseController.java | 0 .../UoB/AILearningTool/IBMAuthenticator.java | 0 .../UoB/AILearningTool/SpringController.java | 0 .../com/UoB/AILearningTool/StringTools.java | 0 .../java/com/UoB/AILearningTool/User.java | 0 .../AILearningTool/WatsonxAPIController.java | 0 .../UoB/AILearningTool/WatsonxResponse.java | 0 .../main/resources/application.properties | 0 .../main/resources/static/error.html | 0 .../main/resources/static/index.html | 0 .../AiLearningToolApplicationTests.java | 0 .../java/com/UoB/AILearningTool/ChatTest.java | 0 .../DatabaseControllerTest.java | 0 .../AILearningTool/IBMAuthenticatorTest.java | 0 .../java/com/UoB/AILearningTool/UserTest.java | 0 .../WatsonxAPIControllerTest.java | 0 24 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/maven.yml rename backend/HELP.md => HELP.md (100%) rename backend/mvnw => mvnw (100%) rename backend/mvnw.cmd => mvnw.cmd (100%) rename backend/pom.xml => pom.xml (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/Chat.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/DatabaseController.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/IBMAuthenticator.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/SpringController.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/StringTools.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/User.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/WatsonxAPIController.java (100%) rename {backend/src => src}/main/java/com/UoB/AILearningTool/WatsonxResponse.java (100%) rename {backend/src => src}/main/resources/application.properties (100%) rename {backend/src => src}/main/resources/static/error.html (100%) rename {backend/src => src}/main/resources/static/index.html (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/ChatTest.java (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/UserTest.java (100%) rename {backend/src => src}/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java (100%) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 00000000..24364992 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,35 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI with Maven + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B package --file pom.xml + + # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/.gitignore b/.gitignore index bbfef583..547f7b99 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ -backend/target +target backend/.idea +.idea + diff --git a/backend/HELP.md b/HELP.md similarity index 100% rename from backend/HELP.md rename to HELP.md diff --git a/backend/mvnw b/mvnw similarity index 100% rename from backend/mvnw rename to mvnw diff --git a/backend/mvnw.cmd b/mvnw.cmd similarity index 100% rename from backend/mvnw.cmd rename to mvnw.cmd diff --git a/backend/pom.xml b/pom.xml similarity index 100% rename from backend/pom.xml rename to pom.xml diff --git a/backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java rename to src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/Chat.java b/src/main/java/com/UoB/AILearningTool/Chat.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/Chat.java rename to src/main/java/com/UoB/AILearningTool/Chat.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java b/src/main/java/com/UoB/AILearningTool/DatabaseController.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/DatabaseController.java rename to src/main/java/com/UoB/AILearningTool/DatabaseController.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java b/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java rename to src/main/java/com/UoB/AILearningTool/IBMAuthenticator.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/SpringController.java b/src/main/java/com/UoB/AILearningTool/SpringController.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/SpringController.java rename to src/main/java/com/UoB/AILearningTool/SpringController.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/StringTools.java b/src/main/java/com/UoB/AILearningTool/StringTools.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/StringTools.java rename to src/main/java/com/UoB/AILearningTool/StringTools.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/User.java b/src/main/java/com/UoB/AILearningTool/User.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/User.java rename to src/main/java/com/UoB/AILearningTool/User.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java b/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java rename to src/main/java/com/UoB/AILearningTool/WatsonxAPIController.java diff --git a/backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java b/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java similarity index 100% rename from backend/src/main/java/com/UoB/AILearningTool/WatsonxResponse.java rename to src/main/java/com/UoB/AILearningTool/WatsonxResponse.java diff --git a/backend/src/main/resources/application.properties b/src/main/resources/application.properties similarity index 100% rename from backend/src/main/resources/application.properties rename to src/main/resources/application.properties diff --git a/backend/src/main/resources/static/error.html b/src/main/resources/static/error.html similarity index 100% rename from backend/src/main/resources/static/error.html rename to src/main/resources/static/error.html diff --git a/backend/src/main/resources/static/index.html b/src/main/resources/static/index.html similarity index 100% rename from backend/src/main/resources/static/index.html rename to src/main/resources/static/index.html diff --git a/backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java b/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java rename to src/test/java/com/UoB/AILearningTool/AiLearningToolApplicationTests.java diff --git a/backend/src/test/java/com/UoB/AILearningTool/ChatTest.java b/src/test/java/com/UoB/AILearningTool/ChatTest.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/ChatTest.java rename to src/test/java/com/UoB/AILearningTool/ChatTest.java diff --git a/backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java b/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java rename to src/test/java/com/UoB/AILearningTool/DatabaseControllerTest.java diff --git a/backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java b/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java rename to src/test/java/com/UoB/AILearningTool/IBMAuthenticatorTest.java diff --git a/backend/src/test/java/com/UoB/AILearningTool/UserTest.java b/src/test/java/com/UoB/AILearningTool/UserTest.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/UserTest.java rename to src/test/java/com/UoB/AILearningTool/UserTest.java diff --git a/backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java b/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java similarity index 100% rename from backend/src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java rename to src/test/java/com/UoB/AILearningTool/WatsonxAPIControllerTest.java From ce076f7731ed170c3e5f820dac80dd2e1293395a Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Fri, 8 Nov 2024 00:07:44 +0000 Subject: [PATCH 19/58] fix: Disable 'dependency graph' Remove 'Dependency graph' from maven.yml runner; Dependency graphs are not supported in our repository. --- .github/workflows/maven.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 24364992..37861089 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,7 +29,3 @@ jobs: cache: maven - name: Build with Maven run: mvn -B package --file pom.xml - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 From 84af9ec051e423a1929591272f5b6a65c985ebbb Mon Sep 17 00:00:00 2001 From: RainBOY-ZZX Date: Sat, 9 Nov 2024 11:58:54 +0000 Subject: [PATCH 20/58] Update AiLearningToolApplication.java --- .../java/com/UoB/AILearningTool/AiLearningToolApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java index 98d4a429..26a32b6b 100644 --- a/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java +++ b/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java @@ -3,7 +3,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) public class AiLearningToolApplication { public static void main(String[] args) { From 0ac2f1ea663968831ca0e6ab1bb8e083741ca998 Mon Sep 17 00:00:00 2001 From: RainBOY-ZZX Date: Sat, 9 Nov 2024 12:00:53 +0000 Subject: [PATCH 21/58] Update application.properties --- src/main/resources/application.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8d0ccbb8..4ebf8f3d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,5 @@ spring.application.name=AILearningTool +spring.servlet.multipart.max-file-size=50MB +spring.servlet.multipart.max-request-size=50MB +spring.web.resources.static-locations=classpath:/static/ +server.port=8080 From da4a0a0141a5f0474e86f8ee2d875907fa41420d Mon Sep 17 00:00:00 2001 From: Vlad Date: Sun, 10 Nov 2024 00:34:51 +0000 Subject: [PATCH 22/58] bugfix: Import missing classes in AiLearningToolApplication class. --- .../java/com/UoB/AILearningTool/AiLearningToolApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java b/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java index 26a32b6b..92b9777b 100644 --- a/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java +++ b/src/main/java/com/UoB/AILearningTool/AiLearningToolApplication.java @@ -2,6 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) public class AiLearningToolApplication { From 8698b12f1846fceeb68a7a9d1158fcfaf3c9efea Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Mon, 11 Nov 2024 22:25:39 +0000 Subject: [PATCH 23/58] Create OpenAIAPIController.java Created OpenAIAPIController.java based on WatsonxAPIController.java --- .../AILearningTool/OpenAIAPIController.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java diff --git a/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java new file mode 100644 index 00000000..f655aecf --- /dev/null +++ b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java @@ -0,0 +1,72 @@ +package com.UoB.AILearningTool; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class OpenAIAPIController { + private final Logger log = LoggerFactory.getLogger(OpenAIAPIController.class); + private final OpenAIAuthenticator authenticator; + + final static String technicalDataPayload = """ + { + "model": "gpt-4", + "messages": [ + {"role": "system", "content": "You are an assistant."} + ], + "temperature": 0.7, + "max_tokens": 900 + } + """; + + public OpenAIAPIController() { + this.authenticator = new OpenAIAuthenticator(); + this.authenticator.requestNewToken(); + } + + // Sends a message to OpenAI's ChatGPT API. + public OpenAIResponse sendUserMessage(String preparedMessageHistory) { + String dataPayload = preparedMessageHistory + technicalDataPayload; + + OpenAIResponse AIResponse; + HttpRequest request = HttpRequest + .newBuilder(URI.create("https://api.openai.com/v1/chat/completions")) + .headers("Content-Type", "application/json", + "Authorization", "Bearer " + authenticator.getBearerToken()) + .POST(HttpRequest.BodyPublishers.ofString(dataPayload)) + .build(); + + try { + HttpResponse response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofString()); + Integer statusCode = response.statusCode(); + if (statusCode == 200) { + String message = new JSONObject(response.body()) + .getJSONArray("choices") + .getJSONObject(0) + .getJSONObject("message") + .getString("content"); + log.info("200 OpenAI response received."); + + AIResponse = new OpenAIResponse(statusCode, message); + } else { + log.warn("Non-200 OpenAI response received."); + String message = new JSONObject(response.body()) + .getJSONArray("error") + .getJSONObject(0) + .getString("message"); + AIResponse = new OpenAIResponse(500, message); + } + } catch (Exception e) { + AIResponse = new OpenAIResponse(500, null); + log.error("Exception {}\nHTTP status code: {}", e, 500); + } + return AIResponse; + } +} From 99f8b93d7c411a9343fd62b62d2c3bb1163b68c0 Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Mon, 11 Nov 2024 22:32:17 +0000 Subject: [PATCH 24/58] Create OpenAIAuthenticator.java Created OpenAIAuthenticator.java based off of IBMAuthenticator.java (still need to replace "your-api-key" with an actual API key). --- .../AILearningTool/OpenAIAuthenticator.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java diff --git a/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java b/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java new file mode 100644 index 00000000..0c311bbb --- /dev/null +++ b/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java @@ -0,0 +1,30 @@ +package com.UoB.AILearningTool; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenAIAuthenticator { + private final Logger log = LoggerFactory.getLogger(OpenAIAuthenticator.class); + + private String apiKey; + + // Constructor that initializes the API key + public OpenAIAuthenticator() { + this.apiKey = "your-openai-api-key-here"; // Replace with actual OpenAI API key + log.info("OpenAI API key set."); + } + + // Returns the API key as a "Bearer token" (used in authorization headers) + public String getBearerToken() { + if (this.apiKey == null || this.apiKey.isEmpty()) { + log.error("API Key not set or is empty."); + throw new IllegalStateException("API Key for OpenAI is not set."); + } + return this.apiKey; + } +} From 8146f33896f261ea409ac63123d59a48fddcc5bb Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Tue, 12 Nov 2024 13:45:17 +0000 Subject: [PATCH 25/58] refactor: added the OpenAI API Key Added the OpenAI API Key to the code. --- src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java b/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java index 0c311bbb..90284d9e 100644 --- a/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java +++ b/src/main/java/com/UoB/AILearningTool/OpenAIAuthenticator.java @@ -15,7 +15,7 @@ public class OpenAIAuthenticator { // Constructor that initializes the API key public OpenAIAuthenticator() { - this.apiKey = "your-openai-api-key-here"; // Replace with actual OpenAI API key + this.apiKey = "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"; log.info("OpenAI API key set."); } From 04c7f92976f366c0a590dca031ed679c27acb470 Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Tue, 12 Nov 2024 15:45:57 +0000 Subject: [PATCH 26/58] Update CookieAPI.js --- frontend/api/CookieAPI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/api/CookieAPI.js b/frontend/api/CookieAPI.js index 8c6ce2b0..7a2b5c7c 100644 --- a/frontend/api/CookieAPI.js +++ b/frontend/api/CookieAPI.js @@ -1,7 +1,7 @@ // src/api/api.js import axios from 'axios'; -const API_BASE_URL = 'http://localhost:8080'; +const API_BASE_URL = 'https://ailearningtool.ddns.net:8080'; const api = axios.create({ From 8b76a632dcd6fd2da2d1dc3854207c3de0e34b10 Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Tue, 12 Nov 2024 15:46:38 +0000 Subject: [PATCH 27/58] Update Login.vue --- frontend/src/components/Login.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index 8fc50862..dd118ff7 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -16,7 +16,7 @@ export default { methods: { async handleConsent(isAccepted) { try { - const response = await axios.post('api', { + const response = await axios.get('https://ailearningtool.ddns.net:8080', { consent: isAccepted }); From cee19b9a4d05e9d394d4bd563a95fb634b1832c5 Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Tue, 12 Nov 2024 16:03:45 +0000 Subject: [PATCH 28/58] feature: update the dev instructions and project structure Updated the developer instructions and project structure (they haven't been updated since the beginning). Also reformated the team members section. --- README.md | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 63cea16d..d3512ac8 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,23 @@ As a university student, I want the AI chatbot to explain course concepts and fo ## Project Structure: Below is an overview of the key components of the system: -- docs: Contains all project-related documentation. Notable files include: - - ETHICS.md -- LICENSE: Includes the project's MIT license file. +- [workflows](/.github/workflows): Contains Maven Continuous Integration. +- [docs](/docs): Contains all project-related documentation. Notable files include: + - [ETHICS.md](/docs/ETHICS.md): Includes the date of the ethics pre-approval request. + - All diagrams/flowcharts. +- [frontend](/frontend): Contains all of the front-end code (in Vue 3 and Yarn) and documents: + - [api](/frontend/api): Includes the cookies API. + - [public](/frontend/public): Includes some front-end documents. + - [src](/frontend/src): Includes the front-end code. +- [src](/src): Contains all of the back-end code (in Java) and documents: + - [main](/src/main): Includes the back-end code. + - [test/java/com/UoB/AILearningTool](/src/test/java/com/UoB/AILearningTool): Includes all of the unit tests. +- [LICENSE](/LICENSE): Includes the project's MIT license file. +- [mvnw](/mvnw) and [pom.xml](/pom.xml): Documents for Maven. ## Tech Stack: ### Frontend -The frontend is a JavaScript Vue 3-based web application. It makes requests to the backend using HTTP requests. +The front end is a JavaScript Vue 3-based web application. It makes requests to the backend using HTTP requests. ### Backend The backend is based on Spring Boot (open-source Java framework). Data will be stored in a MariaDB database. @@ -159,13 +169,21 @@ To get started with developing or contributing to this project, follow the steps 4. **Install Maven**: This project uses Maven as the build automation tool. If you don't have Maven installed, download the latest stable release [here](https://maven.apache.org/download.cgi). -6. **Open the Project in Your IDE**: +5. **Install Vue and Yarn**: + The front end of this project is built using Vue 3 and Yarn, so make sure you have them installed: + - Vue 3 installation guide [here](https://v3.ru.vuejs.org/guide/installation.html) + - Yarn installation guide [here](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable) + +7. **Open the Project in Your IDE**: Open the cloned repository in your preferred Integrated Development Environment (IDE) (we recommend IntelliJ) for further development. ## Team Members: -Vlad Kirlovics (fi23561) \ -Gerard Chaba (tl23383) \ -Mohammed Elzobair (yi23484) \ -Weifan Liu (au22116) \ -Zixuan Zhu (kh23199) -Siyuan Zhang (gr23994) + +| Name | GitHub | Email | +|-------------------|-------------------------------------------------------------|-----------------------| +| Vlad Kirilovics | [vladislav-k1](https://github.com/vladislav-k1) | fi23561@bristol.ac.uk | +| Gerard Chaba | [GerardChabaBristol](https://github.com/GerardChabaBristol) | tl23383@bristol.ac.uk | +| Mohammed Elzobair | [yi23484](https://github.com/yi23484) | yi23484@bristol.ac.uk | +| Weifan Liu | [Liuwf4319](https://github.com/Liuwf4319) | au22116@bristol.ac.uk | +| Zixuan Zhu | [RainBOY-ZZX](https://github.com/RainBOY-ZZX) | kh23199@bristol.ac.uk | +| Siyuan Zhang | [Siyuan106](https://github.com/Siyuan106) | gr23994@bristol.ac.uk | From 195a0710a32a4087673f79cd629f9ad608715aae Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 12 Nov 2024 17:54:07 +0000 Subject: [PATCH 29/58] feature: add watsonxToOpenAI method. watsonxToOpenAI method of StringTools class converts a message history of Watsonx format to OpenAI request payload; Add unit tests for watsonxToOpenAI. --- .../com/UoB/AILearningTool/StringTools.java | 61 +++++++++++++++++++ .../UoB/AILearningTool/StringToolsTest.java | 60 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/test/java/com/UoB/AILearningTool/StringToolsTest.java diff --git a/src/main/java/com/UoB/AILearningTool/StringTools.java b/src/main/java/com/UoB/AILearningTool/StringTools.java index 9f3e757e..5e3b34f7 100644 --- a/src/main/java/com/UoB/AILearningTool/StringTools.java +++ b/src/main/java/com/UoB/AILearningTool/StringTools.java @@ -1,4 +1,8 @@ package com.UoB.AILearningTool; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; // StringTools create / transform strings to the required formats. public class StringTools { @@ -24,4 +28,61 @@ public static String messageHistoryPrepare(String input) { System.out.println(x); return x; } + + // Convert Watsonx message history to OpenAI request payload + public static String watsonxToOpenAI(String messageHistory) { + String currentRole, nextRole, openAIRole; + String currentMessage; + JSONObject currentMessageJSON; + JSONObject dataPayload = new JSONObject(); + dataPayload.put("model", "gpt-4o-mini"); + + JSONArray messageArray = new JSONArray(); + + // Parse the message history string. + for (int i = 0; ;i++) { + if (i == 0) { + // Parameters for parsing first message in chat history (system prompt) + currentRole = "<|system|>"; + openAIRole = "system"; + nextRole = "<|user|>"; + } else if (i % 2 != 0) { + // Parameters for parsing a user message + currentRole = "<|user|>"; + openAIRole = "user"; + nextRole = "<|assistant|>"; + } else { + // Parameters for parsing an AI message + currentRole = "<|assistant|>"; + openAIRole = "assistant"; + nextRole = "<|user|>"; + } + if (messageHistory.contains(currentRole)) { + // Removing previous messages + messageHistory = messageHistory.substring(messageHistory.indexOf(currentRole) + currentRole.length()); + // Parsing a message + int nextRoleIndex = messageHistory.indexOf(nextRole); + if (nextRoleIndex != -1) { + currentMessage = messageHistory.substring(0, messageHistory.indexOf(nextRole)); + } else { + currentMessage = messageHistory; + } + currentMessageJSON = new JSONObject(); + currentMessageJSON.put("role", openAIRole); + currentMessageJSON.put("content", currentMessage); + messageArray.put(currentMessageJSON); + } else { + break; + } + } + + dataPayload.put("messages", messageArray); + return dataPayload.toString(); + } + +// TODO: Conversion from OpenAI JSON response to Watsonx message history format. +// public static String openAIToWatsonx(String messageHistory) { +// +// } + } diff --git a/src/test/java/com/UoB/AILearningTool/StringToolsTest.java b/src/test/java/com/UoB/AILearningTool/StringToolsTest.java new file mode 100644 index 00000000..f4a6886a --- /dev/null +++ b/src/test/java/com/UoB/AILearningTool/StringToolsTest.java @@ -0,0 +1,60 @@ +package com.UoB.AILearningTool; + +import org.json.JSONObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +public class StringToolsTest { + @Test + @DisplayName("Check if watsonxToOpenAI method converts initial message history correctly.") + public void watsonxToOpenAIInitialHistoryTest() { + String watsonxFormat = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message."; + ArrayList messages = new ArrayList<>(); + messages.add("\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease."); + messages.add("\nThis is a first message."); + + String openAIFormat = StringTools.watsonxToOpenAI(watsonxFormat); + org.json.JSONArray messageArray = new JSONObject(openAIFormat).getJSONArray("messages"); + + // Check message contents + for (int i = 0; i < messages.size(); i++) { + Assertions.assertEquals(messages.get(i), + messageArray.getJSONObject(i).getString("content")); + } + + // Check message roles + Assertions.assertEquals("system", messageArray.getJSONObject(0).getString("role")); + Assertions.assertEquals("user", messageArray.getJSONObject(1).getString("role")); + } + + @Test + @DisplayName("Check if watsonxToOpenAI method converts chat history with more than 1 message correctly.") + public void watsonxToOpenAIDevelopedHistoryTest() { + String watsonxFormat = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message.\n<|assistant|>\nHow are you doing?\n<|user|>\nI'm fine, thanks. What about you?\n<|assistant|>\nI feel the same, human."; + ArrayList messages = new ArrayList<>(); + messages.add("\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease."); + messages.add("\nThis is a first message.\n"); + messages.add("\nHow are you doing?\n"); + messages.add("\nI'm fine, thanks. What about you?\n"); + messages.add("\nI feel the same, human."); + + String openAIFormat = StringTools.watsonxToOpenAI(watsonxFormat); + org.json.JSONArray messageArray = new JSONObject(openAIFormat).getJSONArray("messages"); + + // Check message contents + for (int i = 0; i < messages.size(); i++) { + Assertions.assertEquals(messages.get(i), + messageArray.getJSONObject(i).getString("content")); + } + + // Check message roles + Assertions.assertEquals("system", messageArray.getJSONObject(0).getString("role")); + Assertions.assertEquals("user", messageArray.getJSONObject(1).getString("role")); + Assertions.assertEquals("assistant", messageArray.getJSONObject(2).getString("role")); + Assertions.assertEquals("user", messageArray.getJSONObject(3).getString("role")); + Assertions.assertEquals("assistant", messageArray.getJSONObject(4).getString("role")); + } +} From 79a331e84c46a4d0f8169b786fe7eb3808e7a04e Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Tue, 12 Nov 2024 23:20:04 +0000 Subject: [PATCH 30/58] feature: created OpenAIAPIController.java Made a test class for OpenAIAPIController.java --- .../AILearningTool/OpenAIAPIController.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java diff --git a/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java b/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java new file mode 100644 index 00000000..6672a266 --- /dev/null +++ b/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java @@ -0,0 +1,41 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Testing OpenAI API Controller for response handling") +public class OpenAIAPIControllerTest { + + @Test + @DisplayName("Single request test") + public void singleRequestTest() { + OpenAIAPIController openAIAPIController = new OpenAIAPIController(); + openAIAPIController.authenticator.requestNewToken(); // Ensure token is available + + String preparedMessageHistory = "{\"input\": \"Hello, how can you help me?\"}"; + OpenAIResponse response = openAIAPIController.sendUserMessage(preparedMessageHistory); + + Assertions.assertEquals(200, response.getStatusCode(), "The status code should be 200 for a successful response."); + Assertions.assertNotNull(response.getMessage(), "The message content should not be null."); + } + + @Test + @DisplayName("Multiple requests test") + public void multiRequestTest() { + OpenAIAPIController openAIAPIController = new OpenAIAPIController(); + openAIAPIController.authenticator.requestNewToken(); // Ensure token is available + + String preparedMessageHistory1 = "{\"input\": \"Tell me a joke.\"}"; + OpenAIResponse response1 = openAIAPIController.sendUserMessage(preparedMessageHistory1); + + String preparedMessageHistory2 = "{\"input\": \"What's the weather like today?\"}"; + OpenAIResponse response2 = openAIAPIController.sendUserMessage(preparedMessageHistory2); + + Assertions.assertEquals(200, response1.getStatusCode(), "First request should have a status code of 200."); + Assertions.assertEquals(200, response2.getStatusCode(), "Second request should also have a status code of 200."); + Assertions.assertNotNull(response1.getMessage(), "The first response message should not be null."); + Assertions.assertNotNull(response2.getMessage(), "The second response message should not be null."); + Assertions.assertNotEquals(response1.getMessage(), response2.getMessage(), "The responses for different inputs should differ."); + } +} From 6c8c5f1b0e74a0ef0e323666ef9f7c7459313ba4 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 01:12:01 +0000 Subject: [PATCH 31/58] fix: Debug OpenAIAPIController; debug unit tests for OpenAIAPIController. --- .../AILearningTool/OpenAIAPIController.java | 24 +++-------- .../AILearningTool/OpenAIAPIController.java | 41 ------------------- .../OpenAIAPIControllerTest.java | 35 ++++++++++++++++ 3 files changed, 41 insertions(+), 59 deletions(-) delete mode 100644 src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java create mode 100644 src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java diff --git a/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java index f655aecf..a9dbef8b 100644 --- a/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java +++ b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java @@ -13,27 +13,15 @@ public class OpenAIAPIController { private final Logger log = LoggerFactory.getLogger(OpenAIAPIController.class); private final OpenAIAuthenticator authenticator; - final static String technicalDataPayload = """ - { - "model": "gpt-4", - "messages": [ - {"role": "system", "content": "You are an assistant."} - ], - "temperature": 0.7, - "max_tokens": 900 - } - """; - public OpenAIAPIController() { this.authenticator = new OpenAIAuthenticator(); - this.authenticator.requestNewToken(); } // Sends a message to OpenAI's ChatGPT API. - public OpenAIResponse sendUserMessage(String preparedMessageHistory) { - String dataPayload = preparedMessageHistory + technicalDataPayload; + public WatsonxResponse sendUserMessage(String messageHistory) { + String dataPayload = StringTools.watsonxToOpenAI(messageHistory); - OpenAIResponse AIResponse; + WatsonxResponse AIResponse; HttpRequest request = HttpRequest .newBuilder(URI.create("https://api.openai.com/v1/chat/completions")) .headers("Content-Type", "application/json", @@ -54,17 +42,17 @@ public OpenAIResponse sendUserMessage(String preparedMessageHistory) { .getString("content"); log.info("200 OpenAI response received."); - AIResponse = new OpenAIResponse(statusCode, message); + AIResponse = new WatsonxResponse(statusCode, message); } else { log.warn("Non-200 OpenAI response received."); String message = new JSONObject(response.body()) .getJSONArray("error") .getJSONObject(0) .getString("message"); - AIResponse = new OpenAIResponse(500, message); + AIResponse = new WatsonxResponse(500, message); } } catch (Exception e) { - AIResponse = new OpenAIResponse(500, null); + AIResponse = new WatsonxResponse(500, null); log.error("Exception {}\nHTTP status code: {}", e, 500); } return AIResponse; diff --git a/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java b/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java deleted file mode 100644 index 6672a266..00000000 --- a/src/test/java/com/UoB/AILearningTool/OpenAIAPIController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.UoB.AILearningTool; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("Testing OpenAI API Controller for response handling") -public class OpenAIAPIControllerTest { - - @Test - @DisplayName("Single request test") - public void singleRequestTest() { - OpenAIAPIController openAIAPIController = new OpenAIAPIController(); - openAIAPIController.authenticator.requestNewToken(); // Ensure token is available - - String preparedMessageHistory = "{\"input\": \"Hello, how can you help me?\"}"; - OpenAIResponse response = openAIAPIController.sendUserMessage(preparedMessageHistory); - - Assertions.assertEquals(200, response.getStatusCode(), "The status code should be 200 for a successful response."); - Assertions.assertNotNull(response.getMessage(), "The message content should not be null."); - } - - @Test - @DisplayName("Multiple requests test") - public void multiRequestTest() { - OpenAIAPIController openAIAPIController = new OpenAIAPIController(); - openAIAPIController.authenticator.requestNewToken(); // Ensure token is available - - String preparedMessageHistory1 = "{\"input\": \"Tell me a joke.\"}"; - OpenAIResponse response1 = openAIAPIController.sendUserMessage(preparedMessageHistory1); - - String preparedMessageHistory2 = "{\"input\": \"What's the weather like today?\"}"; - OpenAIResponse response2 = openAIAPIController.sendUserMessage(preparedMessageHistory2); - - Assertions.assertEquals(200, response1.getStatusCode(), "First request should have a status code of 200."); - Assertions.assertEquals(200, response2.getStatusCode(), "Second request should also have a status code of 200."); - Assertions.assertNotNull(response1.getMessage(), "The first response message should not be null."); - Assertions.assertNotNull(response2.getMessage(), "The second response message should not be null."); - Assertions.assertNotEquals(response1.getMessage(), response2.getMessage(), "The responses for different inputs should differ."); - } -} diff --git a/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java b/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java new file mode 100644 index 00000000..96ed92f3 --- /dev/null +++ b/src/test/java/com/UoB/AILearningTool/OpenAIAPIControllerTest.java @@ -0,0 +1,35 @@ +package com.UoB.AILearningTool; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +@DisplayName("Testing OpenAI API Controller for response handling") +public class OpenAIAPIControllerTest { + + @Test + @DisplayName("OpenAI API should be able to generate some response for initial chat.") + public void initialMessageHistoryRequestTest() { + OpenAIAPIController openAIAPIController = new OpenAIAPIController(); + + String messageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message.\n<|assistant|>\nTell me a joke."; + WatsonxResponse response = openAIAPIController.sendUserMessage((messageHistory)); + + Assertions.assertNotNull(response.responseText, "The message content should not be null."); + Assertions.assertEquals(200, response.statusCode, "The status code should be 200 for a successful response."); + } + + @Test + @DisplayName("OpenAI API should be able to generate response for chat history with multiple messages.") + public void developedMessageHistoryRequestTest() { + OpenAIAPIController openAIAPIController = new OpenAIAPIController(); + + String messageHistory = "<|system|>\nYour name is AI Learning Tool, and you are an assistant for IBM SkillsBuild, a platform dedicated to providing skills and training in technology and professional development. Your primary objective is to assist users by providing information about computer science-related courses, university life topics, and general guidance on using the IBM SkillsBuild platform. You help users find computer science courses that suit their knowledge level and time availability by tailoring recommendations based on their input, such as current experience level (beginner, intermediate, or advanced) and preferred course duration (short, medium, or long). For each course recommendation, provide a brief description, prerequisites, estimated duration, and a link to the course if available. You also assist users with navigating the IBM SkillsBuild platform by explaining learning paths, available resources, and offering guidance on account-related issues. If users need help with platform navigation or account matters, direct them to appropriate resources or help articles. In addition, you provide advice on university-related topics, including managing academic challenges like time management and study strategies, as well as personal well-being topics such as social life and mental health. Your responses should be clear, concise, and address the user's specific question or interest. Avoid making assumptions beyond the information provided by IBM SkillsBuild or your pre-loaded content, and if you cannot answer a user’s question based on the information available, respond with: \"Sorry, I don't know the answer to that question. Can you provide more information to help me understand it better?\" Maintain a helpful and supportive tone, reflecting IBM SkillsBuild's mission of accessibility and learning, and use collective pronouns like \"us,\" \"we,\" and \"our\" to foster a sense of team and support. Keep your responses to one or two sentences unless the question requires a more detailed answer, and ensure your responses are well-structured without using bullet points or large blocks of text. Do not provide any links, contact information, or resources that have not been explicitly included in your setup. Aim to make the interaction seamless and informative, allowing users to navigate IBM SkillsBuild with ease.<|user|>\nThis is a first message.\n<|assistant|>\nHow are you doing?\n<|user|>\nI'm fine, thanks. What about you?\n<|assistant|>\nI feel the same, human."; + WatsonxResponse response = openAIAPIController.sendUserMessage(messageHistory); + + Assertions.assertNotNull(response.responseText, "The message content should not be null."); + Assertions.assertEquals(200, response.statusCode, "The status code should be 200 for a successful response."); + } +} From 2d0789ff60172cda203405ef911f2ce058be2a28 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 01:40:00 +0000 Subject: [PATCH 32/58] refactor: Adapt SpringController to work with OpenAIAPIController. --- .../com/UoB/AILearningTool/SpringController.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/UoB/AILearningTool/SpringController.java b/src/main/java/com/UoB/AILearningTool/SpringController.java index d7d1be67..51bb9737 100644 --- a/src/main/java/com/UoB/AILearningTool/SpringController.java +++ b/src/main/java/com/UoB/AILearningTool/SpringController.java @@ -12,7 +12,9 @@ public class SpringController { private final Logger log = LoggerFactory.getLogger(SpringController.class); private final DatabaseController DBC = new DatabaseController(); - private final WatsonxAPIController WXC = new WatsonxAPIController(); + // TODO: Replace with OpenAIAPIController with WatsonxAPIController when API quota issue will be resolved. + // private final WatsonxAPIController WXC = new WatsonxAPIController(); + private final OpenAIAPIController WXC = new OpenAIAPIController(); // Assign a unique user ID for the user. @GetMapping("/signup") @@ -98,7 +100,10 @@ public void sendMessage(@CookieValue(value = "userID", defaultValue = "") String // to Watsonx API. boolean success = chat.addUserMessage(userID, newMessage); if (success) { - wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); + inputString = chat.getMessageHistory(DBC.getUser(userID)); +// TODO: Revert wresponse when issue with Watsonx API quota will be resolved. +// wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); + wresponse = WXC.sendUserMessage(inputString); response.setStatus(wresponse.statusCode); } try { @@ -125,7 +130,9 @@ public void sendIncognitoMessage(@CookieValue(value = "userID") String userID, response.setContentType("text/plain"); if (user != null) { - wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); +// TODO: Revert wresponse when issue with Watsonx API quota will be resolved. + wresponse = WXC.sendUserMessage(inputString); +// wresponse = WXC.sendUserMessage(StringTools.messageHistoryPrepare(inputString)); response.setStatus(wresponse.statusCode); try { response.getWriter().write(wresponse.responseText); From 7bb58e96d4df4f888198418a2d6e30fc86b54e98 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 01:41:59 +0000 Subject: [PATCH 33/58] chore: Remove unnecessary comments from StringTools class. --- src/main/java/com/UoB/AILearningTool/StringTools.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/UoB/AILearningTool/StringTools.java b/src/main/java/com/UoB/AILearningTool/StringTools.java index 5e3b34f7..3441a011 100644 --- a/src/main/java/com/UoB/AILearningTool/StringTools.java +++ b/src/main/java/com/UoB/AILearningTool/StringTools.java @@ -79,10 +79,4 @@ public static String watsonxToOpenAI(String messageHistory) { dataPayload.put("messages", messageArray); return dataPayload.toString(); } - -// TODO: Conversion from OpenAI JSON response to Watsonx message history format. -// public static String openAIToWatsonx(String messageHistory) { -// -// } - } From 5909b49fed56eef30c82de4d8da6ed6195aa0558 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 01:47:44 +0000 Subject: [PATCH 34/58] chore: Add more comments to OpenAIAPIController. --- src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java index a9dbef8b..549073dd 100644 --- a/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java +++ b/src/main/java/com/UoB/AILearningTool/OpenAIAPIController.java @@ -22,6 +22,7 @@ public WatsonxResponse sendUserMessage(String messageHistory) { String dataPayload = StringTools.watsonxToOpenAI(messageHistory); WatsonxResponse AIResponse; + // Create HTTP request for OpenAI with API key in header and message history in request body. HttpRequest request = HttpRequest .newBuilder(URI.create("https://api.openai.com/v1/chat/completions")) .headers("Content-Type", "application/json", @@ -29,11 +30,13 @@ public WatsonxResponse sendUserMessage(String messageHistory) { .POST(HttpRequest.BodyPublishers.ofString(dataPayload)) .build(); + // Try sending an HTTPS request to OpenAI API. try { HttpResponse response = HttpClient.newBuilder() .build() .send(request, HttpResponse.BodyHandlers.ofString()); Integer statusCode = response.statusCode(); + // If OpenAI API returned status 200, return the status code and the message. if (statusCode == 200) { String message = new JSONObject(response.body()) .getJSONArray("choices") @@ -44,6 +47,7 @@ public WatsonxResponse sendUserMessage(String messageHistory) { AIResponse = new WatsonxResponse(statusCode, message); } else { + // If OpenAI API request returned a status code other than 200, return error 500 and error message. log.warn("Non-200 OpenAI response received."); String message = new JSONObject(response.body()) .getJSONArray("error") From a9a9afe4698966719d72d4f019279f7481a63952 Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Wed, 13 Nov 2024 10:13:17 +0000 Subject: [PATCH 35/58] feature: updated Team Members and Developer Instructions Updated Team Members section to include Vlad's second GitHub account and the Developer Instructions to explain src/main/resources/static. --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d3512ac8..8228dbf1 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ Below is an overview of the key components of the system: - [src](/frontend/src): Includes the front-end code. - [src](/src): Contains all of the back-end code (in Java) and documents: - [main](/src/main): Includes the back-end code. + - [static](/src/main/resources/static): Includes all static documents, including index.html of the front-end. - [test/java/com/UoB/AILearningTool](/src/test/java/com/UoB/AILearningTool): Includes all of the unit tests. - [LICENSE](/LICENSE): Includes the project's MIT license file. - [mvnw](/mvnw) and [pom.xml](/pom.xml): Documents for Maven. @@ -179,11 +180,11 @@ To get started with developing or contributing to this project, follow the steps ## Team Members: -| Name | GitHub | Email | -|-------------------|-------------------------------------------------------------|-----------------------| -| Vlad Kirilovics | [vladislav-k1](https://github.com/vladislav-k1) | fi23561@bristol.ac.uk | -| Gerard Chaba | [GerardChabaBristol](https://github.com/GerardChabaBristol) | tl23383@bristol.ac.uk | -| Mohammed Elzobair | [yi23484](https://github.com/yi23484) | yi23484@bristol.ac.uk | -| Weifan Liu | [Liuwf4319](https://github.com/Liuwf4319) | au22116@bristol.ac.uk | -| Zixuan Zhu | [RainBOY-ZZX](https://github.com/RainBOY-ZZX) | kh23199@bristol.ac.uk | -| Siyuan Zhang | [Siyuan106](https://github.com/Siyuan106) | gr23994@bristol.ac.uk | +| Name | GitHub | Email | +|-------------------|-----------------------------------------------------------------------------------------------------------|-----------------------| +| Vlad Kirilovics | [vladislav-k1](https://github.com/vladislav-k1) and [kirilovich-vlad](https://github.com/kirilovich-vlad) | fi23561@bristol.ac.uk | +| Gerard Chaba | [GerardChabaBristol](https://github.com/GerardChabaBristol) | tl23383@bristol.ac.uk | +| Mohammed Elzobair | [yi23484](https://github.com/yi23484) | yi23484@bristol.ac.uk | +| Weifan Liu | [Liuwf4319](https://github.com/Liuwf4319) | au22116@bristol.ac.uk | +| Zixuan Zhu | [RainBOY-ZZX](https://github.com/RainBOY-ZZX) | kh23199@bristol.ac.uk | +| Siyuan Zhang | [Siyuan106](https://github.com/Siyuan106) | gr23994@bristol.ac.uk | From b6b8606678b26835e540321a347992c1d023e5ef Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Wed, 13 Nov 2024 10:56:13 +0000 Subject: [PATCH 36/58] feature: create enable_https.sh Created enable_https.sh which overwrites application.properties and verifies that keystore.p12 exists in the directory. --- enable_https.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 enable_https.sh diff --git a/enable_https.sh b/enable_https.sh new file mode 100644 index 00000000..18c54ad8 --- /dev/null +++ b/enable_https.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Define the target file and keystore file +PROPERTIES_FILE="application.properties" +KEYSTORE_FILE="keystore.p12" + +# Verify if keystore.p12 exists in the current directory +if [[ ! -f "$KEYSTORE_FILE" ]]; then + echo "Error: $KEYSTORE_FILE not found in the current directory." + exit 1 +fi + +# Overwrite the application.properties file with the new content +cat > "$PROPERTIES_FILE" < Date: Wed, 13 Nov 2024 11:13:52 +0000 Subject: [PATCH 37/58] feature: add Siyuan to MIT License Added Siyuan's name to the MIT License --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 8e18f380..35893834 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Gerard Chaba, Vladislavs Kirilovics, Zixuan Zhu, Mohammed Elzobair Eltayeb, Weifan Liu +Copyright (c) 2024 Gerard Chaba, Vladislavs Kirilovics, Zixuan Zhu, Mohammed Elzobair Eltayeb, Weifan Liu, Siyuan Zhang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From dbc243aca295515a92e2302bc17f948c1efb53af Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 11:53:47 +0000 Subject: [PATCH 38/58] fix: Debug enable_https.sh --- enable_https.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/enable_https.sh b/enable_https.sh index 18c54ad8..765f5b93 100644 --- a/enable_https.sh +++ b/enable_https.sh @@ -1,13 +1,19 @@ #!/bin/bash -# Define the target file and keystore file -PROPERTIES_FILE="application.properties" +# The script will change Spring Boot application parameters to deploy with HTTPS encryption. + +# Define the application properties file and keystore file +PROPERTIES_FILE="src/main/resources/application.properties" KEYSTORE_FILE="keystore.p12" # Verify if keystore.p12 exists in the current directory if [[ ! -f "$KEYSTORE_FILE" ]]; then echo "Error: $KEYSTORE_FILE not found in the current directory." exit 1 +else + sudo cp "$KEYSTORE_FILE" "src/main/resources/$KEYSTORE_FILE" + sudo chmod 777 "src/main/resources/$KEYSTORE_FILE" + echo "keystore file has been successfully copied." fi # Overwrite the application.properties file with the new content From 59fc8dc4d3d5ea19150572ffbde95157700f8da6 Mon Sep 17 00:00:00 2001 From: Vlad Date: Wed, 13 Nov 2024 12:04:50 +0000 Subject: [PATCH 39/58] feat: Add startserver.sh --- startServer.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 startServer.sh diff --git a/startServer.sh b/startServer.sh new file mode 100644 index 00000000..8e04a09a --- /dev/null +++ b/startServer.sh @@ -0,0 +1,4 @@ +# Recompiles target classes and starts the server. + +mvn clean +mvn spring-boot:run \ No newline at end of file From c7ffa088a14ed5b2b2b784045b408a3e483f5650 Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 14 Nov 2024 02:43:55 +0000 Subject: [PATCH 40/58] feat: Add emergency frontend files. --- src/main/resources/static/css/main.css | 16 +++ src/main/resources/static/index.html | 37 ++++-- src/main/resources/static/js/script.js | 164 +++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/static/css/main.css create mode 100644 src/main/resources/static/js/script.js diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 00000000..6a4a0afe --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,16 @@ +#cookiePopUp { + border: 5px solid grey; + display: block; +} + +.userMessage { + background-color: #8dff6e; +} + +.assistantMessage { + background-color: #a496ff; +} + +#chatContainer { + margin: 3rem 1rem 3rem 1rem; +} \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index a5212ce4..f41bb3ee 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -1,13 +1,34 @@ - + - - AI Learning Tool + AI-Learning-Tool + -

AI Learning Tool

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis ut turpis nibh. Fusce vulputate euismod leo, nec consequat diam maximus id. Curabitur nunc magna, ornare nec auctor in, sollicitudin at tortor. Aliquam egestas augue neque, quis dictum nulla congue vel. Suspendisse ante nunc, porta sed semper eget, venenatis sit amet nisi. Proin sollicitudin mi eget porta dignissim. Curabitur eu vestibulum lectus. In metus eros, varius quis volutpat eu, bibendum ac massa. Nullam molestie sapien finibus massa varius, ac euismod ante scelerisque.

-

Phasellus dignissim risus ut orci rhoncus varius. Mauris efficitur leo at magna imperdiet congue. Donec imperdiet, libero nec bibendum ultricies, lorem tellus semper metus, sed dapibus quam elit eget ipsum. Duis quis quam vulputate, semper leo non, maximus eros. Suspendisse potenti. Fusce quis finibus urna. Etiam velit justo, feugiat sit amet venenatis convallis, ultricies ut neque. Aenean tempor orci eu consectetur tincidunt. Quisque elementum feugiat orci, nec tempus ipsum pharetra id. Maecenas semper vestibulum lectus pellentesque ullamcorper.

-

Vivamus vitae efficitur sem. Cras ante odio, varius ac porttitor eu, aliquet eget diam. Mauris finibus sagittis urna non sodales. Duis at venenatis turpis. Sed in tincidunt ante. Aenean nec orci sagittis, mollis est et, rutrum enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed convallis, mauris ut mollis pulvinar, massa magna molestie neque, sit amet iaculis turpis ligula eget dolor. Sed urna enim, porta quis cursus vel, cursus nec est. Sed eu aliquet nibh, venenatis efficitur est. Sed congue metus urna, non tempor diam pharetra et. Ut ac viverra risus. Praesent eget dictum ligula. Phasellus ac quam eget sapien consequat venenatis. Aliquam erat volutpat. Nulla facilisi.

+

Watsonx assistant

+
+
Would you like to accept optional cookies?
+ + +
+ +
+
+ + + +
+
+
+ +
+ + +
+
+ + + - \ No newline at end of file + diff --git a/src/main/resources/static/js/script.js b/src/main/resources/static/js/script.js new file mode 100644 index 00000000..ce46dc34 --- /dev/null +++ b/src/main/resources/static/js/script.js @@ -0,0 +1,164 @@ +let optionalConsent = false; +let currentTurn = "user" +const messageContainer = document.getElementById("messageContainer") + +// Creates "optionalConsent" cookie +function setConsentCookie() { + const d = new Date(1000*60*60*24*30); + d.setTime(d.getTime() + (24*30)); + let expires = "expires="+ d.toUTCString(); + document.cookie = "optionalConsent=" + optionalConsent + ";" + expires + ";path=/"; +} + +// Sends signup GET request +function signUp() { + fetch("https://ailearningtool.ddns.net:8080/signup", + { + method: "GET", + credentials: "include", + } + ).then(response => { + if (!response.ok) { + throw new Error("Non-200 backend API response"); + } else { + document.getElementById("cookiePopUp").style.display = "none" + } + }) +} + +// Adds a message to the UI. +function addMessageToUI(message) { + messageContainer.innerHTML += ("
\n" + + "
\n" + + "
" + currentTurn + "
\n" + + "
" + message + "
\n" + + "
") + if (currentTurn === "user") { + currentTurn = "assistant" + } else { + currentTurn = "user" + } +} + +// Receives cookie preference from user and signs up in the system. +function acceptOpt(x) { + optionalConsent = x + setConsentCookie() + signUp() +} + +// Parses chat history from HTTP response to good UI. +function processChatHistory(messageHistory) { + messageHistory = String(messageHistory) + let currentRole; + let openAIRole; + let nextRole; + let nextRoleIndex; + let currentMessage; + for (let i = 0; ; i++) { + if (i === 0) { + // Parameters for parsing first message in chat history (system prompt) + currentRole = "<|system|>"; + openAIRole = "system"; + nextRole = "<|user|>"; + } else if (i % 2 !== 0) { + // Parameters for parsing a user message + currentRole = "<|user|>"; + openAIRole = "user"; + nextRole = "<|assistant|>"; + } else { + // Parameters for parsing an AI message + currentRole = "<|assistant|>"; + openAIRole = "assistant"; + nextRole = "<|user|>"; + } + if (messageHistory.includes(currentRole)) { + // Removing previous messages + messageHistory = messageHistory.substring(messageHistory.indexOf(currentRole) + currentRole.length); + // Parsing a message + nextRoleIndex = messageHistory.indexOf(nextRole); + if (nextRoleIndex !== -1) { + currentMessage = messageHistory.substring(0, messageHistory.indexOf(nextRole)); + } else { + currentMessage = messageHistory; + } + if (currentRole === "<|system|>") {continue} + addMessageToUI(currentMessage) + } else { + break; + } + } +} + +// Create a new chat and requests its history +function createChat(firstChoice) { + fetch("https://ailearningtool.ddns.net:8080/createChat?" + new URLSearchParams({ + "initialMessage": firstChoice + }), + { + method: "GET", + credentials: "include", + } + ).then(async response => { + if (!response.ok) { + throw new Error("Non-200 backend API response"); + } + localStorage.setItem("chatID", await response.text()) + getChatHistory() + }) + document.getElementById("chatInitialiser").setAttribute("display", "none") +} + +// Makes a request for chat history +function getChatHistory() { + fetch("https://ailearningtool.ddns.net:8080/getChatHistory?" + new URLSearchParams({ + "chatID": localStorage.getItem("chatID") + }), + { + method: "GET", + credentials: "include", + } + ).then(async response => { + if (!response.ok) { + throw new Error("Non-200 backend API response"); + } else { + processChatHistory(await response.text()) + } + + }) + document.getElementById("chatInitialiser").style.display = "none" +} + +// Sends a message to existing chat +function sendMessage() { + addMessageToUI(document.getElementById("promptInput").value); + fetch("https://ailearningtool.ddns.net:8080/sendMessage?" + new URLSearchParams({ + "chatID": localStorage.getItem("chatID"), + "newMessage": document.getElementById("promptInput").value + }), + { + method: "GET", + credentials: "include", + } + ).then(async response => { + if (!response.ok) { + throw new Error("Non-200 backend API response"); + } else { + addMessageToUI(await response.text()) + } + }) +} + +function revokeConsent() { + fetch("https://ailearningtool.ddns.net:8080/revokeConsent", + { + method: "GET", + credentials: "include", + } + ).then(response => { + if (!response.ok) { + throw new Error("Non-200 backend API response"); + } + }) +} + From 6426f05f750fb715ddcaf625a2199b1e8b7c5464 Mon Sep 17 00:00:00 2001 From: Vlad Date: Thu, 14 Nov 2024 02:47:55 +0000 Subject: [PATCH 41/58] refactor: Change URLs to localhost. --- src/main/resources/static/js/script.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/static/js/script.js b/src/main/resources/static/js/script.js index ce46dc34..1e475656 100644 --- a/src/main/resources/static/js/script.js +++ b/src/main/resources/static/js/script.js @@ -12,7 +12,7 @@ function setConsentCookie() { // Sends signup GET request function signUp() { - fetch("https://ailearningtool.ddns.net:8080/signup", + fetch("http://localhost:8080/signup", { method: "GET", credentials: "include", @@ -92,7 +92,7 @@ function processChatHistory(messageHistory) { // Create a new chat and requests its history function createChat(firstChoice) { - fetch("https://ailearningtool.ddns.net:8080/createChat?" + new URLSearchParams({ + fetch("http://localhost:8080/createChat?" + new URLSearchParams({ "initialMessage": firstChoice }), { @@ -111,7 +111,7 @@ function createChat(firstChoice) { // Makes a request for chat history function getChatHistory() { - fetch("https://ailearningtool.ddns.net:8080/getChatHistory?" + new URLSearchParams({ + fetch("http://localhost:8080/getChatHistory?" + new URLSearchParams({ "chatID": localStorage.getItem("chatID") }), { @@ -132,7 +132,7 @@ function getChatHistory() { // Sends a message to existing chat function sendMessage() { addMessageToUI(document.getElementById("promptInput").value); - fetch("https://ailearningtool.ddns.net:8080/sendMessage?" + new URLSearchParams({ + fetch("http://localhost:8080/sendMessage?" + new URLSearchParams({ "chatID": localStorage.getItem("chatID"), "newMessage": document.getElementById("promptInput").value }), @@ -150,7 +150,7 @@ function sendMessage() { } function revokeConsent() { - fetch("https://ailearningtool.ddns.net:8080/revokeConsent", + fetch("http://localhost:8080/revokeConsent", { method: "GET", credentials: "include", From 2c6c5d433f2f267bf8307eca253691bc790e2819 Mon Sep 17 00:00:00 2001 From: Gerard Chaba Date: Tue, 19 Nov 2024 14:55:58 +0000 Subject: [PATCH 42/58] feature: update-readme Updated the project structure and developer instructions in the README. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8228dbf1..9c96590d 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Below is an overview of the key components of the system: - [docs](/docs): Contains all project-related documentation. Notable files include: - [ETHICS.md](/docs/ETHICS.md): Includes the date of the ethics pre-approval request. - All diagrams/flowcharts. -- [frontend](/frontend): Contains all of the front-end code (in Vue 3 and Yarn) and documents: +- [frontend](/frontend): Contains all of the front-end code (in Vue 3 and Yarn) and documents (not used in the MVP stage): - [api](/frontend/api): Includes the cookies API. - [public](/frontend/public): Includes some front-end documents. - [src](/frontend/src): Includes the front-end code. @@ -175,9 +175,13 @@ To get started with developing or contributing to this project, follow the steps - Vue 3 installation guide [here](https://v3.ru.vuejs.org/guide/installation.html) - Yarn installation guide [here](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable) -7. **Open the Project in Your IDE**: +6. **Open the Project in Your IDE**: Open the cloned repository in your preferred Integrated Development Environment (IDE) (we recommend IntelliJ) for further development. +7. **Test and Run the Server**: + - To run the unit tests, use the command ```mvn test``` + - To start the server, use the command ```mvn spring-boot:run``` + ## Team Members: | Name | GitHub | Email | From e82a901d1df81f5540972f593add8257362d8b67 Mon Sep 17 00:00:00 2001 From: Vlad Kirilovics Date: Wed, 27 Nov 2024 10:26:14 +0000 Subject: [PATCH 43/58] Create pull_request_template.md feat: Create pull_request_template.md . --- docs/pull_request_template.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/pull_request_template.md diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md new file mode 100644 index 00000000..1479a932 --- /dev/null +++ b/docs/pull_request_template.md @@ -0,0 +1,18 @@ +### Issue(s): +#issueNumber + +### Type of change: (choose required ones) +- Bug fix +- New feature +- Breaking change +- Documentation update +- Refactor/Optimization + +### Description: +One or more sentences of description. + +### Additional context: +Some additional, important things about the code (eg. the code contains a temporary solution that will soon be refactored). + +### Testing instructions: +Commands and other guidance on how to test your new code. From 579188f17d01f78ae5b0542c3e2a45e131ea27db Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 10:38:49 +0000 Subject: [PATCH 44/58] Update maven.yml separate the building and testing --- .github/workflows/maven.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 37861089..36597e4e 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -27,5 +27,9 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven + # Build the project - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B clean compile + # Run tests + - name: Test with Maven + run: mvn -B test From e6d1cab2c695cec713ca1085e31ffd840ebddb11 Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 10:41:54 +0000 Subject: [PATCH 45/58] Update maven.yml --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 36597e4e..db054cf1 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,7 +29,7 @@ jobs: cache: maven # Build the project - name: Build with Maven - run: mvn -B clean compile + run: mvn -B clean compile --file pom.xml # Run tests - - name: Test with Maven + - name: Test with Maven --file pom.xml run: mvn -B test From 0652e79bf2c896edf31e3afb3a3e34babd446ace Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 10:54:49 +0000 Subject: [PATCH 46/58] Update pom.xml plugin maven-checkstyle-plugin components --- pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pom.xml b/pom.xml index 65792ca6..b7931aef 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,19 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + verify + + check + + + + From 161707ee6d5b9a30f6387e43a42417858ebe97fe Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 10:57:11 +0000 Subject: [PATCH 47/58] Update maven.yml --- .github/workflows/maven.yml | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index db054cf1..4b8d71e3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,8 +16,23 @@ on: jobs: build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + - name: Build Project + run: mvn -B package --file pom.xml + + test: runs-on: ubuntu-latest + needs: build steps: - uses: actions/checkout@v4 @@ -27,9 +42,16 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven - # Build the project - - name: Build with Maven - run: mvn -B clean compile --file pom.xml - # Run tests - - name: Test with Maven --file pom.xml - run: mvn -B test + + - name: Run Unit Tests + run: mvn test --file pom.xml + + - name: Run Integration Tests + run: mvn verify --file pom.xml + + - name: Run Static Code Analysis (Checkstyle) + run: mvn checkstyle:check --file pom.xml + + - name: Generate Test Coverage Report (JaCoCo) + run: mvn jacoco:report --file pom.xml + From 1e2a4ace6f673e40a17210579f9d24787727157e Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 10:59:27 +0000 Subject: [PATCH 48/58] Update pom.xml --- pom.xml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b7931aef..2578f9ee 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.2 + 3.1.2 verify @@ -88,6 +88,19 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.8 + + + + prepare-agent + report + + + + From eba37454fbf581341232774f5dc536cce53f329f Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 11:02:33 +0000 Subject: [PATCH 49/58] Update maven.yml --- .github/workflows/maven.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 4b8d71e3..2bed48a3 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -42,16 +42,6 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven - - - name: Run Unit Tests - run: mvn test --file pom.xml - - - name: Run Integration Tests - run: mvn verify --file pom.xml - - - name: Run Static Code Analysis (Checkstyle) - run: mvn checkstyle:check --file pom.xml - - - name: Generate Test Coverage Report (JaCoCo) + - name: Test project run: mvn jacoco:report --file pom.xml From 6577e46b1df8230504a1a76d809cf3074846a4a4 Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 11:06:53 +0000 Subject: [PATCH 50/58] Update maven.yml --- .github/workflows/maven.yml | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 2bed48a3..648b3f12 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,23 +16,8 @@ on: jobs: build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - cache: maven - - - name: Build Project - run: mvn -B package --file pom.xml - test: runs-on: ubuntu-latest - needs: build steps: - uses: actions/checkout@v4 @@ -42,6 +27,8 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven - - name: Test project - run: mvn jacoco:report --file pom.xml + - name: Build with Maven + run: mvn -B clean compile --file pom.xml + - name: Test with Maven + run: mvn -B test --file pom.xml From 09b9d510deeee8df737886828059327ec20a326e Mon Sep 17 00:00:00 2001 From: SiyuanZhang Date: Wed, 27 Nov 2024 11:07:16 +0000 Subject: [PATCH 51/58] Update pom.xml --- pom.xml | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/pom.xml b/pom.xml index 2578f9ee..65792ca6 100644 --- a/pom.xml +++ b/pom.xml @@ -75,32 +75,6 @@ org.springframework.boot spring-boot-maven-plugin - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.1.2 - - - verify - - check - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.8 - - - - prepare-agent - report - - - - From 6341d130c44e4bd4c361d57e4689156ae299ba51 Mon Sep 17 00:00:00 2001 From: WeifanLiu2270269 <123551283+Liuwf4319@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:13:03 +0000 Subject: [PATCH 52/58] Add files via upload --- src/App.vue | 24 ++++ src/Display interface/Cookie.vue | 113 ++++++++++++++++ src/Display interface/MainView.vue | 50 +++++++ src/assets/logo.png | Bin 0 -> 6849 bytes src/components/HistorySidebar.vue | 108 ++++++++++++++++ src/components/ImportantSidebar.vue | 79 ++++++++++++ src/components/MainContent.vue | 193 ++++++++++++++++++++++++++++ src/components/SettingSidebar.vue | 135 +++++++++++++++++++ src/main.js | 25 ++++ src/router/index.js | 15 +++ 10 files changed, 742 insertions(+) create mode 100644 src/App.vue create mode 100644 src/Display interface/Cookie.vue create mode 100644 src/Display interface/MainView.vue create mode 100644 src/assets/logo.png create mode 100644 src/components/HistorySidebar.vue create mode 100644 src/components/ImportantSidebar.vue create mode 100644 src/components/MainContent.vue create mode 100644 src/components/SettingSidebar.vue create mode 100644 src/main.js create mode 100644 src/router/index.js diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 00000000..50e28e54 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/Display interface/Cookie.vue b/src/Display interface/Cookie.vue new file mode 100644 index 00000000..f7408f81 --- /dev/null +++ b/src/Display interface/Cookie.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/Display interface/MainView.vue b/src/Display interface/MainView.vue new file mode 100644 index 00000000..c8e25072 --- /dev/null +++ b/src/Display interface/MainView.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 + + + + + + diff --git a/src/components/ImportantSidebar.vue b/src/components/ImportantSidebar.vue new file mode 100644 index 00000000..f88cd709 --- /dev/null +++ b/src/components/ImportantSidebar.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/components/MainContent.vue b/src/components/MainContent.vue new file mode 100644 index 00000000..d6145761 --- /dev/null +++ b/src/components/MainContent.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/src/components/SettingSidebar.vue b/src/components/SettingSidebar.vue new file mode 100644 index 00000000..bd8a7ef7 --- /dev/null +++ b/src/components/SettingSidebar.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 00000000..33f844e4 --- /dev/null +++ b/src/main.js @@ -0,0 +1,25 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import { createRouter, createWebHistory } from 'vue-router'; + +const Cookie = () => import('./Display interface/Cookie.vue'); +const MainView = () => import('./Display interface/MainView.vue'); + +const routes = [ + { path: '/', component: Cookie, meta: { title: 'Cookie' } }, + { path: '/main', component: MainView, meta: { title: 'Chatbot' } }, + // 移除了 404 页面配置 +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +router.afterEach((to) => { + document.title = to.meta.title || 'Default title'; +}); + +const app = createApp(App); +app.use(router); +app.mount('#app'); diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 00000000..161fcc56 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,15 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import MainView from '../components/MainView.vue'; +import Cookie from '../Display interface/Cookie.vue'; + +const routes = [ + { path: '/', component: Cookie }, + { path: '/main', component: MainView }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +export default router; From 6220d8ef4e5f4c08c3b9fc480ecbaa4b59557401 Mon Sep 17 00:00:00 2001 From: WeifanLiu2270269 <123551283+Liuwf4319@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:25:22 +0000 Subject: [PATCH 53/58] Update main.js --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 33f844e4..e967f253 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,7 @@ const MainView = () => import('./Display interface/MainView.vue'); const routes = [ { path: '/', component: Cookie, meta: { title: 'Cookie' } }, { path: '/main', component: MainView, meta: { title: 'Chatbot' } }, - // 移除了 404 页面配置 + ]; const router = createRouter({ From 6dd118ba1adfded642d507e5c1db14e57f58721b Mon Sep 17 00:00:00 2001 From: WeifanLiu2270269 <123551283+Liuwf4319@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:26:30 +0000 Subject: [PATCH 54/58] Update SettingSidebar.vue --- src/components/SettingSidebar.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SettingSidebar.vue b/src/components/SettingSidebar.vue index bd8a7ef7..e7abba00 100644 --- a/src/components/SettingSidebar.vue +++ b/src/components/SettingSidebar.vue @@ -56,7 +56,7 @@ export default { } }, goToCookiePage() { - // 跳转到 /cookie 页面以查看或修改 Cookie 设置 + //Go to the /cookie page to view or modify Cookie Settings this.$router.push("/"); }, }, From 269ef6e47c492c457146f4b6827c3f1ae7af152d Mon Sep 17 00:00:00 2001 From: RainBOY-ZZX Date: Wed, 15 Jan 2025 10:29:32 +0000 Subject: [PATCH 55/58] Update App.vue --- src/App.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.vue b/src/App.vue index 50e28e54..36d3ace3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@ @@ -12,7 +12,7 @@ export default {