diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
new file mode 100644
index 0000000000..4b3508a6a5
--- /dev/null
+++ b/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: angsoontong.AngSoonTong
+
diff --git a/README.md b/README.md
index af0309a9ef..57ab5cf361 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,20 @@
-# Duke project template
+### Ang Soon Tong Chatbot ###
+> **Ang Soon Tong is a secret society and gang based in Singapore and Malaysia which has been active since the 1950s, mainly in the Sembawang area of Singapore.**
-This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it.
+[Soon Tong](https://soundcloud.com/21ast/21-ast-pht-gss-18siaokimtian) is your friendly neighbourhood ah beng, he can help you:
-## Setting up in Intellij
+- Keep track of your daily tasks/commitments
+- Entertain you
+- Lend a listening ear
+- Talk to you ~politely~
-Prerequisites: JDK 17, update Intellij to the most recent version.
+Here's 3 fun facts about Soon Tong
-1. Open Intellij (if you are not in the welcome screen, click `File` > `Close Project` to close the existing project first)
-1. Open the project into Intellij as follows:
- 1. Click `Open`.
- 1. Select the project directory, and click `OK`.
- 1. If there are any further prompts, accept the defaults.
-1. Configure the project to use **JDK 17** (not other versions) as explained in [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk).
- In the same dialog, set the **Project language level** field to the `SDK default` option.
-1. After that, locate the `src/main/java/Duke.java` file, right-click it, and choose `Run Duke.main()` (if the code editor is showing compile errors, try restarting the IDE). If the setup is correct, you should see something like the below as the output:
- ```
- Hello from
- ____ _
- | _ \ _ _| | _____
- | | | | | | | |/ / _ \
- | |_| | |_| | < __/
- |____/ \__,_|_|\_\___|
- ```
+1. He knows 2 languages - hokkien and java
+2. **Humsup**
+3. Likes Matcha 🍵🍃 and Clairo
-**Warning:** Keep the `src\main\java` folder as the root folder for Java files (i.e., don't rename those folders or move Java files to another folder outside of this folder path), as this is the default location some tools (e.g., Gradle) expect to find Java files.
+His hobbies
+
+- [x] Legal activities
+- [ ] Listen to his [favourite song](https://soundcloud.com/nicholas-chong-19/crazy-baby-nightcore-remix?si=4a9cc4d856c940eab66aadf5184fc5b4&utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing)
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..e6cd3ae736
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,69 @@
+plugins {
+ id 'java'
+ id 'application'
+ id 'com.github.johnrengelman.shadow' version '7.1.2'
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.10.0'
+ testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.10.0'
+ implementation 'org.openjfx:javafx-controls:22.0.2'
+ implementation 'org.openjfx:javafx-fxml:22.0.2'
+ implementation 'org.openjfx:javafx-controls:22.0.2'
+ implementation 'org.openjfx:javafx-media:22.0.2'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
+
+
+ String javaFxVersion = '17.0.7'
+
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-controls', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-fxml', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux'
+ implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'win'
+ implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'mac'
+ implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'linux'
+}
+
+test {
+ useJUnitPlatform()
+
+ testLogging {
+ events "passed", "skipped", "failed"
+
+ showExceptions true
+ exceptionFormat "full"
+ showCauses true
+ showStackTraces true
+ showStandardStreams = false
+ }
+}
+
+application {
+ mainClass.set("angsoontong.gui.Launcher")
+}
+
+shadowJar {
+ archiveBaseName = "angsoontong"
+ archiveClassifier = null
+}
+
+run {
+ standardInput = System.in
+ enableAssertions = true
+}
+
+
+
diff --git a/data/tasks.txt b/data/tasks.txt
new file mode 100644
index 0000000000..32772f57f3
--- /dev/null
+++ b/data/tasks.txt
@@ -0,0 +1,4 @@
+T | 1 | listen manyao#hailoksan, #airportkia
+T | 0 | orbital
+D | 0 | test | 2025-09-15
+T | 0 | test
diff --git a/data/test-data.txt b/data/test-data.txt
new file mode 100644
index 0000000000..020017334e
--- /dev/null
+++ b/data/test-data.txt
@@ -0,0 +1 @@
+T | 0 | read book
diff --git a/docs/README.md b/docs/README.md
index 47b9f984f7..80e826a64c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,30 +1,128 @@
-# Duke User Guide
+# Ang Soon Tong User Guide
+> **Ang Soon Tong is a secret society and gang based in Singapore and Malaysia which has been active since the 1950s, mainly in the Sembawang area of Singapore.**
-// Update the title above to match the actual product name
+
-// Product screenshot goes here
+[Soon Tong](https://soundcloud.com/21ast/21-ast-pht-gss-18siaokimtian) is your friendly neighbourhood ah beng, he can help you:
-// Product intro goes here
+- Keep track of your daily tasks/commitments
+- Entertain you
+- Lend a listening ear
+- Talk to you ~politely~
-## Adding deadlines
+Here's 3 fun facts about Soon Tong
-// Describe the action and its outcome.
+1. He knows 2 languages - hokkien and java
+2. **Humsup**
+3. Likes Matcha 🍵🍃 and Clairo
-// Give examples of usage
+His hobbies
-Example: `keyword (optional arguments)`
+- [x] Legal activities
+- [x] Listen to his [favourite song](https://soundcloud.com/nicholas-chong-19/crazy-baby-nightcore-remix?si=4a9cc4d856c940eab66aadf5184fc5b4&utm_source=clipboard&utm_medium=text&utm_campaign=social_sharing)
-// A description of the expected outcome goes here
+## The 3 Types of Tasks
+Soon Tong can help you keep track of 3 different kinds of tasks.
+
+### 1. ToDo
+Just plain, simple tasks - where the name is just enough.
+
+Example: `todo go for class`
+
+### 2. Deadline
+Painful tasks you need to complete before a certain date.
+
+Example: `deadline tutorial sheet /by 2025-09-15`
+
+### 3. Event
+For the events with start and end dates.
+
+Example: `event aunt's funeral /from 2025-02-15 /to 2025-02-16`
+
+## Delete
+Remove added tasks easily, with the delete feature.
+
+Example: `delete 2`\
+Returns:
```
-expected output
+Ok la! I delete already ah:
+[T][] task A
+Now you got 1 task only.
```
-## Feature ABC
+## List
+Shows you the current task list.
+
+Example: `list`\
+Returns:
+```
+Oi! This one your list:
+1. [T][] task A
+2. [D][X] task B (by: 16 Aug 2025)
+```
+
+## Mark/Unmark
+This feature allows you to mark your tasks
+as done when you finish them...
+
+Example: `mark 2`\
+Returns:
+```
+Ok la! Do already
+[T][X] go for class
+```
+
+Or unmark marked tasks if you've made a mistake.
+
+Example: `unmark 2`\
+Returns:
+```
+Huh why haven't do?!
+[T][] go for class
+```
+
+## Tag/Untag
+Add more information to your tasks! By using tags #yay
+
+Example: `tag 2 #fun #holiday`\
+Returns:
+```
+Ok tag already! :
+[T][] go bali { #fun, #holiday }
+```
+
+On the converse, use untag to remove tags.
+
+Example: `untag 2 #fun #holiday`\
+Returns:
+```
+Removed tags liao! :
+[T][] go bali
+```
+
+## Find
+Allows you to search for tasks with keywords.
+
+Example: `find CS2100`\
+Returns:
+```
+This one your matching tasks! :
+[T][] CS2100 lecture
+[T][X] CS2100 tutorial
+```
+
+## Sing
+Arguably the best feature here - listen to Soon Tong sing a song for you.
+
+Example: `sing`
-// Feature details
+Try it for yourself to find out what it does! Soon Tong knows quite a few songs!
+## Bye
+And finally, when you are done chatting with Soon Tong. Bye lets you quit the chatbot.
-## Feature XYZ
+Example: `bye`\
+Returns: `Bye. You still here for what?!`
-// Feature details
\ No newline at end of file
+The chatbot closes automatically afterward.
diff --git a/docs/Ui.png b/docs/Ui.png
new file mode 100644
index 0000000000..4885657619
Binary files /dev/null and b/docs/Ui.png differ
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..033e24c4cd
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..66c01cfeba
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000..fcb6fca147
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000..6689b85bee
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/main/java/Duke.java b/src/main/java/Duke.java
deleted file mode 100644
index 5d313334cc..0000000000
--- a/src/main/java/Duke.java
+++ /dev/null
@@ -1,10 +0,0 @@
-public class Duke {
- public static void main(String[] args) {
- String logo = " ____ _ \n"
- + "| _ \\ _ _| | _____ \n"
- + "| | | | | | | |/ / _ \\\n"
- + "| |_| | |_| | < __/\n"
- + "|____/ \\__,_|_|\\_\\___|\n";
- System.out.println("Hello from\n" + logo);
- }
-}
diff --git a/src/main/java/angsoontong/AngSoonTong.java b/src/main/java/angsoontong/AngSoonTong.java
new file mode 100644
index 0000000000..b3dbad6628
--- /dev/null
+++ b/src/main/java/angsoontong/AngSoonTong.java
@@ -0,0 +1,64 @@
+package angsoontong;
+
+import angsoontong.task.Task;
+import angsoontong.ui.Ui;
+import angsoontong.storage.Storage;
+import angsoontong.parser.Parser;
+import angsoontong.task.TaskList;
+import java.util.ArrayList;
+
+
+public class AngSoonTong {
+ private final Storage storage;
+ private TaskList tasks;
+ private final Ui ui;
+
+ /**
+ * constructor to initialize AngSoonTong
+ * @param filePath outlines location for which tasks will be written to and saved
+ */
+ public AngSoonTong(String filePath) {
+ this.ui = new Ui();
+ this.storage = new Storage(filePath);
+ this.tasks = new TaskList(); // start empty, then fill
+
+ try {
+ ArrayList loaded = storage.load();
+ for (Task t : loaded) {
+ tasks.add(t);
+ }
+ System.out.println("[Storage] Loaded " + loaded.size() + " task(s).");
+ } catch (Exception e) {
+ System.err.println("[Storage] Could not load tasks, starting fresh: " + e.getMessage());
+ }
+ }
+
+ // commented out old main() and run() method meant for CLI
+// public void run() {
+// ui.showGreeting();
+// boolean running = true;
+// Scanner sc = new Scanner(System.in);
+//
+// while (running) {
+// String input = sc.nextLine();
+// if (input.equals("bye")) {
+// ui.showGoodbye();
+// running = false;
+// } else {
+// String response = Parser.parse(input, tasks, ui, storage);
+// ui.show(response);
+// }
+// }
+// }
+
+// public static void main(String[] args) throws IOException {
+// new AngSoonTong("data/tasks.txt").run();
+// }
+
+ /**
+ * Generates a response for the user's chat message.
+ */
+ public String getResponse(String input) {
+ return Parser.parse(input, tasks, ui, storage);
+ }
+}
diff --git a/src/main/java/angsoontong/TaskDecoder.java b/src/main/java/angsoontong/TaskDecoder.java
new file mode 100644
index 0000000000..764545392b
--- /dev/null
+++ b/src/main/java/angsoontong/TaskDecoder.java
@@ -0,0 +1,51 @@
+package angsoontong;
+
+import angsoontong.task.Task;
+import angsoontong.task.Deadline;
+import angsoontong.task.Event;
+import angsoontong.task.ToDo;
+
+
+public class TaskDecoder {
+ public static Task decode(String line) {
+ assert line != null : "decode line is null"; // assertion to make sure no empty input
+ String[] parts = line.split(" \\| ");
+ String type = parts[0];
+ Task t;
+
+ /**
+ * read the task type and return the task with proper description
+ */
+ switch (type) {
+ case "T": {
+ String desc = parts[2];
+ t = new ToDo(desc);
+ if ("1".equals(parts[1])) t.markDone();
+ if (parts.length >= 4) t.loadTagsFromCsv(parts[3]);
+ break;
+ }
+ case "D": {
+ String desc = parts[2];
+ String by = parts[3];
+ t = new Deadline(desc, by);
+ if ("1".equals(parts[1])) t.markDone();
+ if (parts.length >= 5) t.loadTagsFromCsv(parts[4]);
+ break;
+ }
+ case "E": {
+ String desc = parts[2];
+ String start = parts[3];
+ String end = parts[4];
+ t = new Event(desc, start, end);
+ if ("1".equals(parts[1])) t.markDone();
+ if (parts.length >= 6) t.loadTagsFromCsv(parts[5]);
+ break;
+ }
+ default:
+ // if task is not a T, D or E
+ throw new IllegalArgumentException("Unknown task type: " + type);
+ }
+
+ return t;
+ }
+}
diff --git a/src/main/java/angsoontong/gui/DialogBox.java b/src/main/java/angsoontong/gui/DialogBox.java
new file mode 100644
index 0000000000..49b1ef00c5
--- /dev/null
+++ b/src/main/java/angsoontong/gui/DialogBox.java
@@ -0,0 +1,73 @@
+package angsoontong.gui;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.fxml.FXML;
+import javafx.fxml.FXMLLoader;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+
+import java.io.IOException;
+
+public class DialogBox extends HBox {
+ @FXML
+ private Label dialog;
+ @FXML
+ private ImageView displayPicture;
+
+ /**
+ * Constructs a DialogBox with the specified text and image
+ * @param text String message to be displayed inside the dialog box
+ * @param img Image to display alongside the dialog text
+ */
+ private DialogBox(String text, Image img) {
+ try {
+ FXMLLoader fxmlLoader = new FXMLLoader(MainWindow.class.getResource("/view/DialogBox.fxml"));
+ fxmlLoader.setController(this);
+ fxmlLoader.setRoot(this);
+ fxmlLoader.load();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ dialog.setText(text);
+ displayPicture.setImage(img);
+ }
+
+ /**
+ * Flips the dialog box so that its elements are mirrored
+ */
+ private void flip() {
+ this.setAlignment(Pos.TOP_LEFT);
+ ObservableList tmp = FXCollections.observableArrayList(this.getChildren());
+ FXCollections.reverse(tmp);
+ this.getChildren().setAll(tmp);
+ }
+
+ /**
+ * Creates a DialogBox representing the user's message
+ * @param s The message text provided by the user
+ * @param i Image representing the profile picture
+ * @return DialogBox containing the user's message and image
+ */
+ public static DialogBox getUserDialog(String s, Image i) {
+ return new DialogBox(s, i);
+ }
+
+ /**
+ * Creates a DialogBox representing the AngSoonTong's response
+ * @param s String message reply generated by the application
+ * @param i Image representing AngSoonTong's profile picture
+ * @return A DialogBox containing the application's response and image
+ */
+ public static DialogBox getASTDialog(String s, Image i) {
+ var db = new DialogBox(s, i);
+ db.flip();
+ return db;
+ }
+}
+
diff --git a/src/main/java/angsoontong/gui/Launcher.java b/src/main/java/angsoontong/gui/Launcher.java
new file mode 100644
index 0000000000..a2560c964f
--- /dev/null
+++ b/src/main/java/angsoontong/gui/Launcher.java
@@ -0,0 +1,11 @@
+package angsoontong.gui;
+import javafx.application.Application;
+
+public class Launcher {
+ /**
+ * A launcher class to workaround classpath issues.
+ */
+ public static void main(String[] args) {
+ Application.launch(MainApp.class, args); // hands off to the JavaFX Application below
+ }
+}
diff --git a/src/main/java/angsoontong/gui/MainApp.java b/src/main/java/angsoontong/gui/MainApp.java
new file mode 100644
index 0000000000..f9a0c5ecb3
--- /dev/null
+++ b/src/main/java/angsoontong/gui/MainApp.java
@@ -0,0 +1,39 @@
+package angsoontong.gui;
+
+import java.io.IOException;
+
+import angsoontong.AngSoonTong;
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.scene.layout.AnchorPane;
+import javafx.stage.Stage;
+
+/**
+ * A GUI for Duke using FXML
+ */
+public class MainApp extends Application {
+
+ /**
+ * init AngSoonTong with filePath
+ */
+ private AngSoonTong angSoonTong = new AngSoonTong("data/tasks.txt");
+
+ /**
+ * Starts the JavaFX application by setting up the primary stage.
+ * @param stage Stage representing the main application window.
+ */
+ @Override
+ public void start(Stage stage) {
+ try {
+ FXMLLoader fxmlLoader = new FXMLLoader(MainApp.class.getResource("/view/MainWindow.fxml"));
+ AnchorPane ap = fxmlLoader.load();
+ Scene scene = new Scene(ap);
+ stage.setScene(scene);
+ fxmlLoader.getController().setAngSoonTong(angSoonTong);
+ stage.show();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/angsoontong/gui/MainWindow.java b/src/main/java/angsoontong/gui/MainWindow.java
new file mode 100644
index 0000000000..897744e0fd
--- /dev/null
+++ b/src/main/java/angsoontong/gui/MainWindow.java
@@ -0,0 +1,209 @@
+package angsoontong.gui;
+
+import angsoontong.AngSoonTong;
+import javafx.fxml.FXML;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.media.Media;
+import javafx.scene.media.MediaPlayer;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.util.Duration;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+
+/**
+ * Controller for the main GUI.
+ */
+public class MainWindow extends AnchorPane {
+ @FXML
+ private ScrollPane scrollPane;
+ @FXML
+ private VBox dialogContainer;
+ @FXML
+ private TextField userInput;
+ @FXML
+ private Button sendButton;
+
+ private static class LyricLine {
+ final double atSec; // when to show (seconds from start)
+ final String text;
+ LyricLine(double atSec, String text) { this.atSec = atSec; this.text = text; }
+ }
+
+ /**
+ * lyrics for the 1st song that could possibly be played
+ */
+ private final java.util.List LYRICS1 = java.util.List.of(
+ new LyricLine(0.3, "希望你以后不会后悔没选择我"),
+ new LyricLine(6.4, "也相信你有更好的生活"),
+ new LyricLine(13.0, "我会在心里"),
+ new LyricLine(15.5, "默默地为你而执着~")
+ );
+
+ /**
+ * lyrics for the 2nd song that could possibly be played
+ */
+ private final java.util.List LYRICS2 = java.util.List.of(
+ new LyricLine(0.2, "gang chant~"),
+ new LyricLine(13.6, "toh teng jit ki ang ji kao!"),
+ new LyricLine(23.7, "kaninabeh si kah po!"),
+ new LyricLine(28.2, "I wanna know where you belong~")
+ );
+
+ /**
+ * lyrics for the 3rd song that could possibly be played
+ */
+ private final java.util.List LYRICS3 = java.util.List.of(
+ new LyricLine(0.0, "如果让你从新来过你会不会爱我"),
+ new LyricLine(5.1, "爱情让人拥有快乐也会带来折磨"),
+ new LyricLine(11.9, "曾经和你一起走过传说中的爱河"),
+ new LyricLine(17.8, "已经被我泪水淹没变成痛苦的爱河")
+ );
+
+ private AngSoonTong angSoonTong;
+ private Timeline lyricsTimeline;
+ private MediaPlayer player;
+ private int currSong;
+
+ /**
+ * access images from resources folder for profile pictures
+ */
+ private Image userImage = new Image(this.getClass().getResourceAsStream("/images/AhBeng.png"));
+ private Image ASTImage = new Image(this.getClass().getResourceAsStream("/images/AngSoonTong.png"));
+
+ @FXML
+ public void initialize() {
+ scrollPane.vvalueProperty().bind(dialogContainer.heightProperty());
+ }
+
+ /**
+ * Injects the AST instance
+ */
+ public void setAngSoonTong(AngSoonTong ast) {
+ angSoonTong = ast;
+ dialogContainer.getChildren().add(
+ DialogBox.getASTDialog("Eh! I'm Soon Tong\nWhat you want?!", ASTImage)
+ );
+ }
+
+ /**
+ * array containing filePaths for each of the possible songs
+ */
+ private static final String[] SONGS = {
+ "/audio/song1.wav",
+ "/audio/song2.wav",
+ "/audio/song3.wav"
+ };
+
+ /**
+ * randomly picks a song number and returns its filePath, and sets currSong to that number
+ */
+ private String pickRandomSong() {
+ int i = ThreadLocalRandom.current().nextInt(SONGS.length);
+ this.currSong = i + 1;
+ return SONGS[i];
+ }
+
+ /**
+ * sing function randomly chooses and then plays a song when sing command is called
+ */
+ private void sing() {
+
+ // setup media
+ String path = getClass().getResource(pickRandomSong()).toExternalForm();
+ player = new MediaPlayer(new Media(path));
+ player.setStopTime(Duration.seconds(42));
+
+ // lyric timeline
+ if (lyricsTimeline != null) lyricsTimeline.stop();
+ lyricsTimeline = new Timeline();
+ lyricsTimeline.setCycleCount(1);
+
+ // add keyframe for each lyric line
+ if (currSong == 1) {
+ for (LyricLine line : LYRICS1) {
+ lyricsTimeline.getKeyFrames().add(
+ new KeyFrame(Duration.seconds(line.atSec), e -> showResponse(line.text))
+ );
+ }
+ } else if (currSong == 2) {
+ for (LyricLine line : LYRICS2) {
+ lyricsTimeline.getKeyFrames().add(
+ new KeyFrame(Duration.seconds(line.atSec), e -> showResponse(line.text))
+ );
+ }
+ } else if (currSong == 3) {
+ for (LyricLine line : LYRICS3) {
+ lyricsTimeline.getKeyFrames().add(
+ new KeyFrame(Duration.seconds(line.atSec), e -> showResponse(line.text))
+ );
+ }
+ } else { }
+
+ player.play();
+ lyricsTimeline.play();
+
+ // cleanup when media ends
+ player.setOnEndOfMedia(() -> {
+ if (lyricsTimeline != null) lyricsTimeline.stop();
+ });
+ }
+
+ /**
+ * Creates two dialog boxes, one echoing user input and the other containing
+ * AngSoonTong's reply and then appends them to
+ * the dialog container. Clears the user input after processing.
+ */
+ @FXML
+ private void handleUserInput() {
+ String input = userInput.getText();
+ String response = angSoonTong.getResponse(input);
+ dialogContainer.getChildren().addAll(
+ DialogBox.getUserDialog(input, userImage),
+ DialogBox.getASTDialog(response, ASTImage)
+ );
+ userInput.clear();
+
+ if (input.equalsIgnoreCase("sing")) {
+ sing();
+ userInput.clear();
+ return;
+ }
+
+ // shutdown on 'bye' input
+ if (input.trim().equalsIgnoreCase("bye")) {
+ // delay a bit so user sees the final message
+ new Thread(() -> {
+ try {
+ Thread.sleep(1000); // 1 second delay
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ javafx.application.Platform.exit();
+ }).start();
+ }
+ }
+
+ /**
+ * Adds a line of text to the chat window as the chatbot's response.
+ */
+ public void showResponse(String text) {
+ Label label = new Label(text);
+ label.setWrapText(true);
+ dialogContainer.getChildren().add(label);
+
+ // ensure we’re not setting a bound property
+ javafx.application.Platform.runLater(() -> {
+ scrollPane.vvalueProperty().unbind(); // <— important
+ scrollPane.setVvalue(1.0);
+ });
+ }
+
+}
diff --git a/src/main/java/angsoontong/parser/Parser.java b/src/main/java/angsoontong/parser/Parser.java
new file mode 100644
index 0000000000..098ce6bfa9
--- /dev/null
+++ b/src/main/java/angsoontong/parser/Parser.java
@@ -0,0 +1,154 @@
+package angsoontong.parser;
+
+import angsoontong.task.*;
+import angsoontong.ui.Ui;
+import angsoontong.storage.Storage;
+
+public class Parser {
+ /**
+ * helper that runs runnable, and then subsequently saves mutated task list
+ * @param mutation Runnable executing the command involved
+ * @param storage Storage to save the new task list to
+ * @param tasks TaskList to call the command on
+ */
+ private static void mutateAndSave(Runnable mutation, TaskList tasks, Storage storage) {
+ mutation.run();
+ tasks.save(storage);
+ }
+
+ /**
+ * main parsing command to read input
+ * @param input String input from the user
+ * @param tasks TaskList containing current tasks
+ * @param ui UI to receive String messages from to reply user
+ * @param storage To write changes in task list to
+ */
+ public static String parse(String input, TaskList tasks, Ui ui, Storage storage) {
+ String[] words = input.split(" ");
+ String command = words[0];
+
+ switch (command) {
+ case "bye":
+ return "Bye. Why you still here?!";
+
+ case "sing":
+ return "OK 来!";
+
+ case "list":
+ return ui.showList(tasks);
+
+ case "mark":
+ int markIndex = Integer.parseInt(words[1]) - 1;
+ Task marked = tasks.get(markIndex);
+
+ mutateAndSave(() -> marked.markDone(), tasks, storage);
+ return ui.showMarked(marked);
+
+ case "unmark":
+ int unmarkIndex = Integer.parseInt(words[1]) - 1;
+ Task unmarked = tasks.get(unmarkIndex);
+
+ mutateAndSave(() -> unmarked.markUndone(), tasks, storage);
+ return ui.showUnmarked(unmarked);
+
+ case "delete":
+ int deleteIndex = Integer.parseInt(words[1]) - 1;
+ Task deleted = tasks.delete(deleteIndex);
+
+ tasks.save(storage);
+ return ui.showDeleted(deleted, tasks.size());
+
+ case "find":
+ String keyword = input.length() > 4 ? input.substring(5).trim() : "";
+ if (keyword.isEmpty()) {
+ ui.show("Eh, tell me what to find leh! (e.g., find book)");
+ }
+ var indices = tasks.findIndices(keyword);
+ return ui.showFindResults(indices, tasks);
+
+ case "todo":
+ Task todo = new ToDo(input.substring(5));
+
+ mutateAndSave(() -> tasks.add(todo), tasks, storage);
+ return ui.showAdded(todo, tasks.size());
+
+ case "deadline":
+ String[] deadlineParts = input.split("/by ");
+ Task deadline = new Deadline(deadlineParts[0].substring(9), deadlineParts[1]);
+
+ mutateAndSave(() -> tasks.add(deadline), tasks, storage);
+ return ui.showAdded(deadline, tasks.size());
+
+ case "event":
+ String[] eventParts = input.split("/from |/to ");
+ Task event = new Event(eventParts[0].substring(6), eventParts[1], eventParts[2]);
+
+ mutateAndSave(() -> tasks.add(event), tasks, storage);
+ return ui.showAdded(event, tasks.size());
+
+ case "tag":
+ // Usage: tag #tag1 #tag2 ...
+ String[] w = input.trim().split("\\s+");
+ if (w.length < 3) {
+ return ui.show("Eh use properly! : tag #tag1 #tag2");
+ }
+ int index;
+
+ try {
+ index = Integer.parseInt(w[1]) - 1;
+ } catch (NumberFormatException e) {
+ return ui.show("Eh use properly! : tag #tag1 #tag2");
+ }
+
+ if (index < 0 || index >= tasks.size()) {
+ return "Index out of range.";
+ }
+ Task t = tasks.get(index);
+
+ // collect the rest as tags (normalize in Task)
+ java.util.List tags = new java.util.ArrayList<>();
+ for (int i = 2; i < w.length; i++) tags.add(w[i]);
+
+ t.addTags(tags);
+ tasks.save(storage);
+ return ui.showTagged(t);
+
+ case "untag": {
+ // Usage: untag #tag1 #tag2 ...
+ w = input.trim().split("\\s+");
+ if (w.length < 3) {
+ return ui.show("Eh use properly! : untag [#tag1 #tag2 ... | all]");
+ }
+
+ int idx;
+ try {
+ idx = Integer.parseInt(w[1]) - 1;
+ } catch (NumberFormatException e) {
+ return ui.show("Eh use properly! : untag [#tag1 #tag2 ... | all]");
+ }
+ if (idx < 0 || idx >= tasks.size()) {
+ return "Index out of range.";
+ }
+
+ t = tasks.get(idx);
+
+ if (w.length == 3 && (w[2].equalsIgnoreCase("all")
+ || w[2].equalsIgnoreCase("--all"))) {
+ t.clearTags();
+ tasks.save(storage);
+ return ui.showUntagged(t);
+ }
+
+ java.util.List tagsToRemove = new java.util.ArrayList<>();
+ for (int i = 2; i < w.length; i++) tagsToRemove.add(w[i]);
+
+ t.removeTags(tagsToRemove);
+ tasks.save(storage);
+ return ui.showUntagged(t);
+ }
+
+ default:
+ return "Eh! Say properly leh, I don't know what that means la!\n";
+ }
+ }
+}
diff --git a/src/main/java/angsoontong/storage/Storage.java b/src/main/java/angsoontong/storage/Storage.java
new file mode 100644
index 0000000000..6bdac3b88e
--- /dev/null
+++ b/src/main/java/angsoontong/storage/Storage.java
@@ -0,0 +1,61 @@
+package angsoontong.storage;
+
+import angsoontong.task.TaskList;
+import angsoontong.task.*;
+import angsoontong.TaskDecoder;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Storage {
+ private final String filePath;
+
+ /**
+ * constructor
+ * @param filePath address of the storage file to read/write tasks
+ */
+ public Storage(String filePath) {
+ assert filePath != null && !filePath.isBlank() : "Storage filePath must not be empty";
+ this.filePath = filePath;
+ }
+
+ /**
+ * Load tasks from the save file.
+ * Returns an ArrayList of tasks (decoded).
+ */
+ public ArrayList load() throws IOException {
+ ArrayList tasks = new ArrayList<>();
+
+ File file = new File(filePath);
+ if (!file.exists()) {
+ // create directories if they do not exist
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ return tasks; // return empty list if no file yet
+ }
+
+ List lines = Files.readAllLines(Paths.get(filePath));
+ for (String line : lines) {
+ Task t = TaskDecoder.decode(line);
+ if (t != null) {
+ tasks.add(t);
+ }
+ }
+ return tasks;
+ }
+
+ /**
+ * Save all tasks to the save file.
+ */
+ public void save(TaskList tasks) throws IOException {
+ FileWriter fw = new FileWriter(filePath);
+ for (Task task : tasks.getAll()) { // TaskList should expose getAll()
+ fw.write(task.toFileFormat() + System.lineSeparator());
+ }
+ fw.close();
+ }
+}
diff --git a/src/main/java/angsoontong/task/Deadline.java b/src/main/java/angsoontong/task/Deadline.java
new file mode 100644
index 0000000000..e3c3513167
--- /dev/null
+++ b/src/main/java/angsoontong/task/Deadline.java
@@ -0,0 +1,39 @@
+package angsoontong.task;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+public class Deadline extends Task {
+ protected LocalDate dueDate;
+
+ /**
+ * constructor
+ * @param name String name to describe task
+ * @param dueDate LocalDate indicating the date the task is due
+ */
+ public Deadline(String name, String dueDate) {
+ super(name);
+ this.dueDate = LocalDate.parse(dueDate);
+ }
+
+ /**
+ * custom toString representation for deadline
+ */
+ @Override
+ public String toString() {
+ return withTags(String.format("[D]" + super.toString() +
+ "(by: " + dueDate.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ")"));
+ }
+
+ /**
+ * method to format task description to write to storage file
+ */
+ @Override
+ public String toFileFormat() {
+ return "D | " +
+ (super.isDone() ? "1" : "0") +
+ " | " + super.getName() +
+ " | " + this.dueDate +
+ tagsForFile();
+ }
+}
diff --git a/src/main/java/angsoontong/task/Event.java b/src/main/java/angsoontong/task/Event.java
new file mode 100644
index 0000000000..776d74679b
--- /dev/null
+++ b/src/main/java/angsoontong/task/Event.java
@@ -0,0 +1,43 @@
+package angsoontong.task;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+public class Event extends Task{
+ private LocalDate start;
+ private LocalDate end;
+
+ /**
+ * constructor
+ * @param name String name to describe task
+ * @param start LocalDate indicating start date of event
+ * @param end LocalDate indicating end date of event
+ */
+ public Event(String name, String start, String end) {
+ super(name);
+ this.start = LocalDate.parse(start);
+ this.end = LocalDate.parse(end);
+ }
+
+ /**
+ * custom toString representation for event
+ */
+ @Override
+ public String toString() {
+ return withTags(String.format("[E]" + super.toString() +
+ "(from: " + start.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
+ + " to: " + end.format(DateTimeFormatter.ofPattern("dd MMM yyyy")) + ")"));
+ }
+
+ /**
+ * method to format task description to write to storage file
+ */
+ @Override
+ public String toFileFormat() {
+ return "D | " +
+ (super.isDone() ? "1" : "0") +
+ " | " + super.getName() +
+ " | " + this.start + " | " + this.end +
+ tagsForFile();
+ }
+}
diff --git a/src/main/java/angsoontong/task/Task.java b/src/main/java/angsoontong/task/Task.java
new file mode 100644
index 0000000000..1c1f2e32fd
--- /dev/null
+++ b/src/main/java/angsoontong/task/Task.java
@@ -0,0 +1,113 @@
+package angsoontong.task;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class Task {
+ private boolean isDone;
+ private final String NAME;
+ protected final Set tags = new LinkedHashSet<>();
+
+ public Task(String name) {
+ this.NAME = name;
+ this.isDone = false;
+ }
+
+ /**
+ * mark task as done
+ */
+ public void markDone() {
+ this.isDone = true;
+ }
+
+ /**
+ * unmarks task, task is not done
+ */
+ public void markUndone() {
+ this.isDone = false;
+ }
+
+ /**
+ * getter for boolean isDone
+ */
+ public boolean isDone() {
+ return this.isDone;
+ }
+
+ /**
+ * getter for name
+ */
+ public String getName() {
+ return this.NAME;
+ }
+
+ /**
+ * add tags to this task object
+ * @param newTags list of tags to be added
+ */
+ public void addTags(List newTags) {
+ if (newTags == null) return;
+ for (String t : newTags) {
+ String norm = normalizeTag(t);
+ System.out.println("adding " + norm);
+ if (!norm.isEmpty()) tags.add(norm);
+ }
+ }
+
+ /**
+ * add tags to this task object
+ * @param toRemove list of tags to be removed
+ */
+ public void removeTags(List toRemove) {
+ if (toRemove == null) return;
+ for (String t : toRemove) {
+ String norm = normalizeTag(t);
+ if (!norm.isEmpty()) {
+ System.out.println("removing " + norm);
+ tags.remove(norm);}
+ }
+ }
+
+ public void clearTags() {
+ tags.clear();
+ }
+
+ private String normalizeTag(String t) {
+ if (t == null) return "";
+ t = t.trim();
+ if (t.isEmpty()) return "";
+ if (!t.startsWith("#")) t = "#" + t;
+ return t;
+ }
+
+ /**
+ * Load tags from CSV saved in file.
+ */
+ public void loadTagsFromCsv(String csv) {
+ if (csv == null || csv.isBlank()) return;
+ for (String piece : csv.split(",")) addTags(List.of(piece));
+ }
+
+ protected String withTags(String base) {
+ if (tags.isEmpty()) return base;
+ // Render like: {#fun #school} appended at end
+ String suffix = " { " + String.join(" ", tags) + " }";
+ return base + suffix;
+ }
+
+ public String tagsForFile() { return String.join(", ", tags); }
+
+ /**
+ * custom toString representation for task
+ */
+ @Override
+ public String toString() {
+ return String.format("[" + (isDone ? "X" : "") + "] %s", NAME);
+ }
+
+ /**
+ * abstract method for task subclasses to write into file
+ */
+ public abstract String toFileFormat();
+}
diff --git a/src/main/java/angsoontong/task/TaskList.java b/src/main/java/angsoontong/task/TaskList.java
new file mode 100644
index 0000000000..1c92105ad2
--- /dev/null
+++ b/src/main/java/angsoontong/task/TaskList.java
@@ -0,0 +1,104 @@
+package angsoontong.task;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import angsoontong.storage.Storage;
+
+
+
+public class TaskList {
+ private ArrayList tasks;
+
+ /**
+ * default constructor
+ */
+ public TaskList() {
+ tasks = new ArrayList<>();
+ }
+
+
+ // alternative constructor commented out - not needed anymore
+ // due to storage read/write feature
+ /*
+ public TaskList(ArrayList list) {
+ this.tasks = list;
+ }
+ */
+
+ /**
+ * method to write tasks to storage file
+ * @param storage Storage object where current task list will be saved to
+ * @throws IOException if storage object does not exist/ filePath is invalid
+ */
+ public void save(Storage storage) {
+ try {
+ storage.save(this);
+ } catch (IOException e) {
+ System.out.println("Error saving file: " + e.getMessage());
+ }
+ }
+
+ /**
+ * a case-insensitive search method that returns indices of matching results
+ * @param keyword String that is the search term to match with task name
+ * @return List of indices of matching results
+ */
+ public List findIndices(String keyword) {
+ String k = keyword.toLowerCase();
+ List results = new ArrayList<>();
+ for (int i = 0; i < size(); i++) {
+ Task t = get(i);
+ if (t.toString().toLowerCase().contains(k)) {
+ results.add(i); // 0-based index; Ui will print as (i+1)
+ }
+ }
+ return results;
+ }
+
+ /**
+ * method to add Task to task list
+ */
+ public void add(Task t) {
+ tasks.add(t);
+ }
+
+ /**
+ * getter to retrieve tasks in task list
+ */
+ public Task get(int index) {
+ // ensures input index is within bounds
+ assert index >= 0 && index < tasks.size() : "get index out of bounds: " + index;
+
+ return tasks.get(index);
+ }
+
+ /**
+ * delete task from task list
+ * @param index represents the integer index of the task to be deleted in the ArrayList
+ */
+ public Task delete(int index) {
+ // ensures index to be deleted is within bounds
+ assert index >= 0 && index < tasks.size() : "delete index out of bounds: " + index;
+
+ Task deletedTask = this.get(index);
+ tasks.remove(index);
+ return deletedTask;
+ }
+
+ /**
+ * returns size of taskList
+ * @return number of tasks in the tasklist
+ */
+ public int size() {
+ return tasks.size();
+ }
+
+ /**
+ * returns the internal ArrayList
+ */
+ public List getAll() {
+ return tasks;
+ }
+}
+
diff --git a/src/main/java/angsoontong/task/ToDo.java b/src/main/java/angsoontong/task/ToDo.java
new file mode 100644
index 0000000000..1ea256cc91
--- /dev/null
+++ b/src/main/java/angsoontong/task/ToDo.java
@@ -0,0 +1,32 @@
+package angsoontong.task;
+
+public class ToDo extends angsoontong.task.Task {
+
+ /**
+ * constructor
+ * @param name String name to describe task
+ */
+ public ToDo(String name) {
+ super(name);
+ }
+
+ /**
+ * custom toString representation
+ */
+ @Override
+ public String toString() {
+ return withTags(String.format("[T]" + super.toString()));
+ }
+
+
+ /**
+ * method to format task description to write to storage file
+ */
+ @Override
+ public String toFileFormat() {
+ return "T | " +
+ (super.isDone() ? "1" : "0") +
+ " | " + super.getName() +
+ tagsForFile();
+ }
+}
diff --git a/src/main/java/angsoontong/ui/Ui.java b/src/main/java/angsoontong/ui/Ui.java
new file mode 100644
index 0000000000..8bf7f7d5d7
--- /dev/null
+++ b/src/main/java/angsoontong/ui/Ui.java
@@ -0,0 +1,113 @@
+package angsoontong.ui;
+
+import angsoontong.task.TaskList;
+import angsoontong.task.Task;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Ui class contains methods that produces Strings to be
+ * displayed on GUI, depending on the specific scenario
+*/
+public class Ui {
+ private Scanner sc;
+
+ /**
+ * constructor
+ */
+ public Ui() {
+ this.sc = new Scanner(System.in);
+ }
+
+ // method to print custom message
+ public String show(String message) {
+ return message;
+ }
+
+ // prints greeting
+ public String showGreeting() {
+ return "Eh! I'm Soon Tong\nWhat you want?!";
+ }
+
+ // prints goodbye message
+ public String showGoodbye() {
+ return "Bye. You still here for what?!";
+ }
+
+ /**
+ * method to print message for adding a task
+ * @param task Task to be added into task list
+ * @param size Size of task list after the task has been added
+ */
+ public String showAdded(Task task, int size) {
+ return String.format("Steady! I add this already:\n %s\nNow your list got %d tasks.\n",
+ task, size);
+ }
+
+ /**
+ * method to print message for deleting a task
+ * @param task Task to be removed from task list
+ * @param size Size of task list after the task has been deleted
+ */
+ public String showDeleted(Task task, int size) {
+ return String.format("Ok la! I delete already ah:\n %s\nNow you got %d task%s only.\n",
+ task,
+ size,
+ size == 1 ? "" : "s");
+ }
+
+ /**
+ * method to print out every task in the list to ui
+ * @param tasks TaskList instance which is a list of all current tasks
+ */
+ public String showList(TaskList tasks) {
+ if (tasks.size() == 0) {
+ return "Eh your list empty sia!";
+ }
+ StringBuilder sb = new StringBuilder("Oi! This one your list:\n");
+ for (int i = 0; i < tasks.size(); i++) {
+ sb.append(String.format("%d.%s\n", i + 1, tasks.get(i)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * method to print out every task found by the find command
+ */
+ public String showFindResults(List indices, TaskList tasks) {
+ if (indices.isEmpty()) {
+ return "No matching tasks found la.";
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append("This one your matching tasks! :\n");
+ int line = 1;
+ for (Integer idx : indices) {
+ sb.append(line++)
+ .append(".")
+ .append(tasks.get(idx)) // prints like [T][X] read book
+ .append("\n");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * simple methods to indicate a completed action
+ * for tag/untag/mark/unmark commands
+ */
+ public String showTagged(Task task) {
+ return "Ok tag already! :\n " + task;
+ }
+
+ public String showUntagged(angsoontong.task.Task task) {
+ return "Removed tags liao! :\n " + task;
+ }
+
+ public String showMarked(Task task) {
+ return "Ok la! Do already!\n" + task + "\n";
+ }
+
+ public String showUnmarked(Task task) {
+ return "Huh why haven't do?!\n" + task + "\n";
+ }
+}
+
diff --git a/src/main/resources/audio/song1.wav b/src/main/resources/audio/song1.wav
new file mode 100644
index 0000000000..5749faeef5
Binary files /dev/null and b/src/main/resources/audio/song1.wav differ
diff --git a/src/main/resources/audio/song2.wav b/src/main/resources/audio/song2.wav
new file mode 100644
index 0000000000..8828066a9f
Binary files /dev/null and b/src/main/resources/audio/song2.wav differ
diff --git a/src/main/resources/audio/song3.wav b/src/main/resources/audio/song3.wav
new file mode 100644
index 0000000000..5c1370bb17
Binary files /dev/null and b/src/main/resources/audio/song3.wav differ
diff --git a/src/main/resources/images/18.png b/src/main/resources/images/18.png
new file mode 100644
index 0000000000..aacf6164dc
Binary files /dev/null and b/src/main/resources/images/18.png differ
diff --git a/src/main/resources/images/AhBeng.png b/src/main/resources/images/AhBeng.png
new file mode 100644
index 0000000000..30eaee44e9
Binary files /dev/null and b/src/main/resources/images/AhBeng.png differ
diff --git a/src/main/resources/images/AngSoonTong.png b/src/main/resources/images/AngSoonTong.png
new file mode 100644
index 0000000000..96e4a680c6
Binary files /dev/null and b/src/main/resources/images/AngSoonTong.png differ
diff --git a/src/main/resources/images/Cheng.png b/src/main/resources/images/Cheng.png
new file mode 100644
index 0000000000..830bd8d165
Binary files /dev/null and b/src/main/resources/images/Cheng.png differ
diff --git a/src/main/resources/styles/chat.css b/src/main/resources/styles/chat.css
new file mode 100644
index 0000000000..8ee9c8e13e
--- /dev/null
+++ b/src/main/resources/styles/chat.css
@@ -0,0 +1,52 @@
+/* ---------- COLOR VARIABLES ---------- */
+/* JavaFX doesn't have native CSS variables, so we define classes to reuse colors */
+
+.root {
+ -fx-background-color: #F7F8FB; /* Background */
+ -fx-font-family: "Inter", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
+ -fx-font-size: 14px;
+}
+
+/* Surface card: use for scrollpane or chat container background */
+.surface {
+ -fx-background-color: #FFFFFF;
+ -fx-background-radius: 16;
+}
+
+/* User bubble */
+.user-bubble {
+ -fx-background-color: #3B82F6; /* Primary */
+ -fx-text-fill: white;
+ -fx-background-radius: 18 18 4 18;
+ -fx-padding: 10 12;
+}
+
+/* Bot bubble */
+.bot-bubble {
+ -fx-background-color: #FFFFFF;
+ -fx-text-fill: #111827;
+ -fx-background-radius: 18 18 18 4;
+ -fx-padding: 10 12;
+ -fx-border-color: rgba(17, 24, 39, 0.06);
+ -fx-border-width: 1;
+}
+
+/* Accent colors for status messages */
+.success {
+ -fx-text-fill: #10B981;
+}
+.warning {
+ -fx-text-fill: #F59E0B;
+}
+
+/* Text field + button styling */
+.text-input {
+ -fx-background-color: #F3F4F6;
+ -fx-background-radius: 10;
+ -fx-prompt-text-fill: #6B7280; /* Muted text */
+}
+.primary-btn {
+ -fx-background-color: #3B82F6;
+ -fx-text-fill: white;
+ -fx-background-radius: 10;
+}
diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml
new file mode 100644
index 0000000000..fe672c90ea
--- /dev/null
+++ b/src/main/resources/view/DialogBox.fxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml
new file mode 100644
index 0000000000..902c461d2e
--- /dev/null
+++ b/src/main/resources/view/MainWindow.fxml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/angsoontong/parser/ParserTest.java b/src/test/java/angsoontong/parser/ParserTest.java
new file mode 100644
index 0000000000..d4c10ebcc1
--- /dev/null
+++ b/src/test/java/angsoontong/parser/ParserTest.java
@@ -0,0 +1,41 @@
+package angsoontong.parser;
+
+import angsoontong.task.*;
+import angsoontong.ui.Ui;
+import angsoontong.storage.Storage;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ParserTest {
+ /**
+ * test for parsing command to add tasks to the task list
+ */
+ @Test
+ public void parse_addTodo_success() {
+ Storage storage = new Storage("data/test-data.txt");
+ TaskList tasks = new TaskList();
+ Ui ui = new Ui();
+
+ String response = Parser.parse("todo read book", tasks, ui, storage);
+
+ assertEquals(1, tasks.size());
+ assertTrue(tasks.get(0) instanceof ToDo);
+ assertTrue(response.contains("Steady! I add this already"));
+ }
+
+ /**
+ * test for parsing command to mark tasks as done
+ */
+ @Test
+ public void parse_markTodo_success() {
+ Storage storage = new Storage("data/test-data.txt");
+ TaskList tasks = new TaskList();
+ Ui ui = new Ui();
+
+ tasks.add(new ToDo("write tests"));
+ Parser.parse("mark 1", tasks, ui, storage);
+
+ assertTrue(tasks.get(0).isDone());
+ }
+}
diff --git a/src/test/java/angsoontong/task/TaskListTest.java b/src/test/java/angsoontong/task/TaskListTest.java
new file mode 100644
index 0000000000..ec5e67555a
--- /dev/null
+++ b/src/test/java/angsoontong/task/TaskListTest.java
@@ -0,0 +1,52 @@
+package angsoontong.task;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TaskListTest {
+
+ /**
+ * test involving adding of tasks to tasklist
+ */
+ @Test
+ public void addTask_sizeIncreases() {
+ TaskList tasks = new TaskList();
+ ToDo todo = new ToDo("read book");
+
+ tasks.add(todo);
+
+ assertEquals(1, tasks.size());
+ }
+
+ /**
+ * test involving deletion of tasks from tasklist
+ */
+ @Test
+ public void deleteTask_validIndex_taskRemoved() {
+ TaskList tasks = new TaskList();
+ ToDo t1 = new ToDo("task one");
+ ToDo t2 = new ToDo("task two");
+ tasks.add(t1);
+ tasks.add(t2);
+
+ Task removed = tasks.delete(0); // remove first task
+
+ assertEquals(t1, removed); // check correct task removed
+ assertEquals(1, tasks.size());
+ assertEquals(t2, tasks.get(0)); // remaining task is correct
+ }
+
+ // test trying to access task that does not exist
+ // commented out JUNIT test as case is covered by assertions in TaskList class
+ /*
+ @Test
+ public void deleteTask_invalidIndex_throwsException() {
+ TaskList tasks = new TaskList();
+ assertThrows(IndexOutOfBoundsException.class, () -> {
+ tasks.delete(5); // no tasks yet, should throw
+ });
+ }
+ */
+
+}
+
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 657e74f6e7..d2e5840764 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,7 +1,8 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
+Eh! I'm Soon Tong
+What you want?!
+Command: || [blah]
+Eh! Say properly leh, I don't know what that means la!
+Command: || [bye]
+Bye. Why you still here?!
+
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index e69de29bb2..ed4584340c 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -0,0 +1,2 @@
+blah
+bye
\ No newline at end of file
diff --git a/text-ui-test/runtest.bat b/text-ui-test/runtest.bat
index 0873744649..32c25bb101 100644
--- a/text-ui-test/runtest.bat
+++ b/text-ui-test/runtest.bat
@@ -15,7 +15,7 @@ IF ERRORLEVEL 1 (
REM no error here, errorlevel == 0
REM run the program, feed commands from input.txt file and redirect the output to the ACTUAL.TXT
-java -classpath ..\bin Duke < input.txt > ACTUAL.TXT
+java -classpath ..\bin AngSoonTong < input.txt > ACTUAL.TXT
REM compare the output to the expected output
FC ACTUAL.TXT EXPECTED.TXT
diff --git a/text-ui-test/runtest.sh b/text-ui-test/runtest.sh
old mode 100644
new mode 100755