diff --git a/Track_2_ToDo_App/Sprint-06 - Advanced To-Do Details/src/README.md b/Track_2_ToDo_App/Sprint-06 - Advanced To-Do Details/src/README.md new file mode 100644 index 00000000..5eac6cfd --- /dev/null +++ b/Track_2_ToDo_App/Sprint-06 - Advanced To-Do Details/src/README.md @@ -0,0 +1,8 @@ +# Sprint 6 - Source Code Directory +This directory contains the completed source code after the end of each user story. The directory is structured as `app-s06-f01-us01` where +- `s06` - represents the sprint number, in this case sprint 6 +- `f01` - represents the feature number, in this case feature 1 +- `us01` - represents the user story number, in this case user story 1 + +> [!NOTE] +> The code in the directory is the completed solution after the completion of that user story. Like many other things in life, there are many ways to solve a problem. The source code in these directories is just one solution to the problem and does not necessarily represent best practices for a given solution. In many cases we chose simplicity or readability over efficiency. \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 1 - Add Completed Checkbox.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 1 - Add Completed Checkbox.md new file mode 100644 index 00000000..526e4b54 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 1 - Add Completed Checkbox.md @@ -0,0 +1,221 @@ +# User Story: Add Completed Checkbox and due date details to main list - Step-by-Step +⏲️ _Est. time to complete: 30 min._ ⏲️ + +## User Story +*As a user, I want to be able to mark a task as complete from the main list as well see the due date of a task if one is defined* + +## 🎯Acceptance Criteria: +- A checkbox should be added to the task list to allow users to mark a task as complete. +- The task should be visually differentiated when marked as complete. i.e., by using strikethrough text on the task name. +- The due date of the task should be displayed in a friendly format underneath the name of the task. + - If a due date is past due, the due date should be displayed inside a red badge . + - If a due date is in the future, the due date should be displayed in light blue badge. + - If there is no due date set for the task, then nothing should display for the due date. +- The interface should look something like this: + + ![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US01.png) + + +## 🎓Know Before You Start +no resources at this time + +## 📋Steps + +In order to complete this user story you will need to complete the following tasks + +### Open Visual Studio Code +Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-06%20-%20Advanced%20To-Do%20Details/src/app-s06-f01-us01/) + +
+ +### Updating the Web Application Backend + +#### 1. New Completed Status Route +In order to support the ability to mark a task as complete when the user checks the checkbox, we will need to add a new route to the backend that will update the completed status of a task. Open the `app.py` file and add the following code to the bottom of the file before the `if __name__ == '__main__':` line: + +```python +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + + #update todo in the database + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + ``` + +This code will create a new route called `/completed` that will take in the id and complete status of the task. It will then get the task from the database, update the completed status of the task, and then redirect the user back to the main task list. + +
+ +#### 2. Create a Context Processor to Pass the Current Date to the User Interface +Per the acceptance criteria above there is a requirement to display the due date of the task in a friendly format underneath the name of the task as well as understand if the due date is past due or in the future. To support this requirement, we will need update the app to pass the current date to the user interface. We could continue to do this through the flask global module `g` in a similar way that we pass the other variables, but for the sake of this exercise we wanted to show you another way to pass variables to the user interface. We will do this through context processors. To enable this we first need to create a new file called `context_processors.py` in the `app` folder. Add the following code to the file: + +```python +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} +``` + +This code will create a function called `inject_current_date` that will return the current date in the format `YYYY-MM-DD`. + +
+ +#### 3. Import Context Processor module into the Application +We will then need to add this functionality to our flask application by making two changes to the `app.py` file. First, we need to import the context processor into the application. Add the following code to the top of the file: + +```python +from context_processors import inject_current_date +``` +
+ +#### 4. Register the Context Processor with the Application +Next we will need to register the context processor with the application. Add the following code to the top of the `app.py` file after the `db.init_app(app) with app.app_context(): db.create_all()` lines: + +```python +@app.context_processor +def inject_common_variables(): + return inject_current_date() +``` +This code will register the `inject_current_date` function as a context processor with the application. This will allow the `current_date` variable to be passed to the user interface on every request, which will be used in a later step to decide how we display the due date of the task (i.e., past due or in the future). + +
+ +### Updating the Web Application Frontend +There are several changes that we need to make to the user interface to support the ability to mark a task as complete and display the due date of the task. + +#### 1. Update the Task List to Include a Checkbox and Due Date Details +First, we need to add a checkbox to the task list and the due date details. Open the `index.html` file and update the code between the `{% for todo in g.todos %}...{% endfor %}` tags as seen below: + +Replace the code below: + +```html +{% for todo in g.todos %} +
  • +
    +
    {{ todo.name }}
    +
    + + + + + + +
  • +{% endfor %} +``` + +with the following code: + +```html +{% for todo in g.todos %} +
  • +
    +
    + {% if todo.completed %} + + {% else %} + + {% endif %} + +
    {{ todo.name }}
    +
    + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
    +
    +
    + + + + + + +
  • +{% endfor %} +``` + +This code will adds the following functionality: +- **add a checkbox to the task list**. Upon loading of the web page, if the task is marked as completed, the checkbox will be checked. If the task is not completed, the checkbox will be unchecked. This is handled through the `checked` attribute in the `` tag. There is also an `onclick` event that will call the `handleClick` function when the checkbox is clicked. This function will be created in the next step. +- **due date** - If there is a due date assigned, the due date will be displayed in a friendly format underneath the name of the task. If the task is marked as completed, a "Completed" badge will be displayed. If the task is past due, a "Past Due" badge will be displayed in red text. If the task is not past due, a "Due Date" badge will be displayed in blue text. + +
    + +#### 2. Add Javascript to Handle Checkbox Click +Next, we need to add the `handleClick` function to the `static/js/app.js` file. Open the `app.js` file and add the following code within the DOMContentLoaded event listener: + +```javascript +window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; +}; +``` + +This code will create a function called `handleClick` that will be called when the checkbox is clicked. The function will stop the event from propagating, get the id and checked status of the checkbox, and then redirect the user to the `/completed` route with the id and checked status as parameters. + +
    + +## 3. Update the `style.css` File +Finally, we will need to update the `style.css` file to add a strikethrough effect to the task name when it is marked as completed. Add the following code to the bottom of the file: + +```css +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} +``` + +This code will add a strikethrough effect to the task name when the task is marked as completed. It will also update the styling of the task name and due date to make it more visually appealing. + +
    + +#### 4. Run the Application +Now let's run the application to see the checkbox in action. Open a terminal window in Visual Studio Code and run the following command: + +```bash +python app.py +``` + +You should now see a checkbox next to each task in the task list. When you click on the checkbox, the task should be marked as completed and visually differentiated with a strikethrough effect. The web page should look something like this: + +![outcome-S07-F01-US01](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US01.png) + + +
    +🎉 Congratulations! You have now updated the user interface to include a checkbox to mark tasks as completed and visually differentiate completed tasks. +
    + +> [!NOTE] +> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us01/). + +
    + +[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 7 ](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/README.md) | [**◀ Previous user story** (in previous sprint)](/Track_2_ToDo_App/Sprint-06%20-%20Advanced%20To-Do%20Details/Feature%201%20-%20Add%20Additional%20To-Do%20Details/User%20Story%201%20-%20Add%20additional%20details%20to%20to-do%20item.md) | [**Next user story** ▶](User%20Story%202%20-%20Add%20Tabbed%20Interface.md) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 2 - Add Tabbed Interface.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 2 - Add Tabbed Interface.md new file mode 100644 index 00000000..516722d4 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 2 - Add Tabbed Interface.md @@ -0,0 +1,266 @@ +# User Story: Add Tabbed Interface - Step-by-Step +⏲️ _Est. time to complete: 30 min._ ⏲️ + +## User Story +*As a user, I want to have a cleaner interface for my tasks and move the details, edit and recommendation buttons over to a tabbed interace to reduce clutter* + +## 🎯Acceptance Criteria: +- A tabbed interface should be added to the main page with the following tabs: Details, Edit, Recommendations that get displayed when selecting an item from the primary list. +- The Details tab should display the task details +- The Edit tab should allow the user to edit the task +- The Recommendations tab should display the recommendations for the task +- The Details, Edit and Recommendations buttons should be removed from the main list +- The interface should look something like this: + ![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US02.png) + +## 🎓Know Before You Start +no resources at this time + +## 📋Steps + +In order to complete this user story you will need to complete the following tasks + +### Open Visual Studio Code +Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us01/) + +
    + +### Updating the Web Application Frontend +To get a tabbed-type interface working and functional in the web application, you will need to make the following changes: +- Add a tabbed user interface element to the main page +- Update the styles for the tabbed interface +- Update the application so that you can select a task by clicking on it in the list +- Remove the Details, Edit, and Recommendations buttons from the main list + +#### 1. Add a tabbed user interface element to the main page +First, we will add the tabbed user interface element to the `index.html` file. To do this, add the following code right below the `
    ` and before the `{% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %}`: + +![ReplaceCode](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/ReplaceCode-S07-F01-US02-01.png) + +with the following code: + +```html +{% if g.selectedTab != g.TabEnum.NONE %} + +{% endif %} +``` + +This code will add a tabbed interface to the right panel of the main page. The tabbed interface will have three tabs: Details, Edit, and Recommendations. The tabs will link to the appropriate routes in the application. + +
    + +#### 2. Update the styles for the tabbed interface +You will then need to update the styles for the application . Open the `styles.css` file and add the following code: + +```css +.card { + border-top: none !important; +} + +.nav-link { + background-color: white; +} + +.highlighted-item { + background-color: #f2f2f2; +} +``` + +This code will update the syles for the tabbed inferface and highlighted items in the application. The code does the following: +- The `card` class will have the top border removed from the previous Details, Edit and Recommendation panes so that it lines up directly below the tabbed interface and looks like one piece. +- The `nav-link` class is part of the tabbed interface and will have a white background color. +- The `highlighted-item` class will have a light gray background color and will be used in an upcoming step for highlighting tasks items. + +
    + +#### 3. Run the Application +If you run the application now you will see that the Details, Edit, and Recommendations have been added to the tabbed interface on the right panel of the main page. You can click on the tabs to view the different sections of the task details, edit, and recommendations. +![Tabbed Interface](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/tabbed-interface-S07-F01-US02-1.png) + +Note however that the user interface is still not complete +- the Details, Edit, and Recommendations buttons are still visible on the main list +- you can't select a task by clicking on it in the main list, +- the selected task is not highlighted when the button's are pressed +- and the tabbed interface really doesn't show which tab is active. + +We will address these in the next set of steps. + +
    + +### Selecting a task by clicking on it in the list +#### 1. Add a showDetails function to user interface +Before we remove the buttons, we need to update the application so that you can select a task by clicking on it in the list otherwise there wouldn't be a way to view the Details, Edit, and Recommendations tabs. Open the `index.html` file and replace the `
  • ` with the following code: + +```html +
  • +``` + +This code adds a showDetails function to the task item in the list. The showDetails function will be used to select a task by clicking on it. + +
    + +#### 2. Implement the showDetails function +We now need to implement the showDetails function as well as several other functions to help with highlighting the selected item and visually showing the right tab. Open the `app.js` file and replace the `handleClick` function + +```javascript + window.handleClick = function (event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + }; +``` + +with the following code: + +```javascript +const HIGHLIGHTEDITEM = 'highlighted-item'; +const currentPath = window.location.pathname; + +// Set the active tab based on the URL +const setActiveTab = (tabId) => { + const tabElement = document.getElementById(tabId); + if (tabElement) { + tabElement.classList.add('active'); + } +}; + +switch (true) { + case currentPath.includes('/edit'): + setActiveTab('edit-tab'); + break; + case currentPath.includes('/completed'): + setActiveTab('completed-tab'); + break; + case currentPath.includes('/details'): + setActiveTab('details-tab'); + break; + case currentPath.includes('/recommend'): + setActiveTab('recommendations-tab'); + break; + default: + break; +} + +const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); +console.log('highlightedItemId', highlightedItemId); +if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.add(HIGHLIGHTEDITEM); + } +} + +window.clearHighlight = function() { + localStorage.removeItem(HIGHLIGHTEDITEM); +}; + +window.showDetails = function(li) { + highlight(li); + const rootUrl = window.location.origin; + const dataId = li.getAttribute('data-id'); + window.location.href = `${rootUrl}/details/${dataId}`; +}; + +window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + clearHighlight(); +}; + + +window.highlight = function(element) { + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.remove(HIGHLIGHTEDITEM); + } + } + + const closestListItem = element.closest('li'); + closestListItem.classList.add(HIGHLIGHTEDITEM); + + localStorage.setItem(HIGHLIGHTEDITEM, closestListItem.id); +}; +``` + +This code implements the following: +- **adds a showDetails function** that will be called when a task item is clicked. The function will highlight the selected task item and then redirect the user to the Details tab for the selected task. +- The **highlight function** will add a class to the selected task item to visually differentiate it from the other task items. +- The **setActiveTab function** will set the active tab based on the URL. +- The **handleClick function** will be called when the checkbox is clicked and will redirect the user to the completed tab for the selected task. +- The **clearHighlight function** will remove the highlighted class from the selected task item. + +
    + +#### 3. Run the Application +If you run the application now you will see that you can select a task by clicking on it in the list. The selected task will be highlighted and the Details tab will be displayed in the right panel. The details tab name will also show in black font to show that it is highlight. These changes are somewhat subtle but enhance the overall user experience and help the end user see where they are at in the application. + +![Tabbed Interface-2](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/tabbed-interface-S07-F01-US02-2.png) + +Note however, that the Details, Edit, and Recommendations buttons are still visible on the main list. We will remove these in the next step. + +
    + +#### Removing extra buttons from the main list + +#### 1. Remove the Details, Edit, and Recommendations buttons from the main list +Next, we need to remove the Details, Edit, and Recommendations buttons from the main list. Open the `index.html` file and remove the following code from the `
  • `: + +```html + + + +``` + +This code will remove the Details, Edit, and Recommendations buttons from the main list. The tabs on the right panel will now be the only way to access the Details, Edit, and Recommendations sections of the task. + +#### 2. Run the Application +Run the application again and you will see that the Details, Edit, and Recommendations buttons have been removed from the main list. This will help to reduce clutter and provide a cleaner interface for users to interact with. + +![Tabbed Interface-3](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/tabbed-interface-S07-F01-US02-3.png) + +
    + +### Updating Add button to remove highlighting from previously selected task + +#### 1. Add an onclick event to the Add button +If you add a new task you may notice that the highlighting is not removed from the previously selected task. This is because the highlighted class is not being removed when a new task is created. To fix this, we need to make an update the `index.html` with the following code: + +```html + +``` +This code will add an `onclick` event to the Add button that will call the `clearHighlight` function when the button is clicked. The `clearHighlight` function will remove the highlighted class from the selected task item. + +
    + +#### 2. Run the Application +If you run the application now you will see that the Details, Edit, and Recommendations buttons have been removed from the main list. You can select a task by clicking on it in the list and the selected task will be highlighted. The Details, Edit, and Recommendations tabs will be displayed in the right panel. You can click on the tabs to view the different sections of the task details, edit, and recommendations. If you add a new task, the highlighting will be removed from the previously selected task. + +
    + +🎉 Hooray! You have successfully added a tabbed interface to the main page of your application. This will help to reduce clutter and provide a cleaner interface for users to interact with. + +
    + +> [!NOTE] +> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us02/). + +
    + +[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 7 ](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/README.md) | [**◀ Previous user story** ](User%20Story%201%20-%20Add%20Completed%20Checkbox.md) | [**Next user story** ▶](User%20Story%203%20-%20Prevent%20User%20from%20adding%20blank%20task.md) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 3 - Prevent User from adding blank task.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 3 - Prevent User from adding blank task.md new file mode 100644 index 00000000..46b5dbc8 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 3 - Prevent User from adding blank task.md @@ -0,0 +1,118 @@ +# User Story: Prevent User from adding blank task and limit characters- Step-by-Step +⏲️ _Est. time to complete: 30 min._ ⏲️ + +## User Story +*As a user, I want to prevent someone from accidentally entering a task with no name and I want to limit the name to 75 characters* + +## 🎯Acceptance Criteria: +- Disable add button if the task name is blank +- Enable add button if the task name is not blank +- Don't allow the user to add a task if the task name is blank +- Limit the task name to 75 characters +- The interface should look something like this: + + ![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US03.png) + + +## 🎓Know Before You Start +no resources at this time + + +## 📋Steps + +In order to complete this user story you will need to complete the following tasks + +### Open Visual Studio Code +Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us02/) + + +### Update the user interface to prevent adding a blank task + +#### 1. Update the user interface elements to support this functionality +The first step is to update the user interface elements to support this functionality. Open the `index.html` file and replace the `
    ... + + + + + + + Maximum 75 characters +
    +``` +We are making the following changes to the form: +- We are adding a `maxlength` attribute to the input field with a value of 75. This will limit the number of characters the user can enter in the input field to 75. +- We are adding an id of `addButton` to the button tag and setting the `disabled` attribute to `true`. This will disable the button by default. The button will be enabled when the user types in the input field. the id of `addButton` will be used by the javascript code to enable and disable the button in one of the next steps. +- We are adding a `` tag with a class of `text-muted` to display a message to the user that the maximum number of characters is 75. + +
    + +#### 2. Add Javascript listener to enable the add button when the task name is not blank +Next, we need to add a check to see if the task name is blank. Open the `app.js` file and add the following function: + +```javascript +nameInput.addEventListener("keyup", function() { + const addButton = document.querySelector("button[id='addButton']"); + addButton.disabled = this.value.trim() === ""; +}); +``` +This code will add an event listener to the input field with the id of `todo`. When the user types in the input field, the function will check if the value of the input field is blank. If the value is blank, the add button will be disabled. If the value is not blank, the add button will be enabled. + +
    + +#### 3. Update `recognition.onresult` function +We also need to update the `recognition.onresult` function in our Javascript to enable the add button when the name input field is updated with the voice functions. Open the `app.js` file and replace the `recognition.onresult` function with the following code: + +```javascript +recognition.onresult = function(event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + + const addButton = document.querySelector("button[id='addButton']"); + addButton.disabled = false; +}; +``` +This code will add an event listener to the input field with the id of `todo`. When the user types in the input field, the function will check if the value of the input field is blank. If the value is blank, the add button will be disabled. If the value is not blank, the add button will be enabled. + +
    + +#### 4. Update the `style.css` file +We need to update the `style.css` file to change the text-muted class to have a color of white so that it shows up better against the current background image: + +```css +.limit-text { + color: white; +} +``` + +
    + +#### 5. Run the application +Now let's run the application to test the Add button is initially disabled and by either typing in the window or adding a name via the voice functionality that the Add button will become enabled and allow you to add a task. Open a terminal window in Visual Studio Code and run the following command: + +```bash +python app.py +``` +The initial interface should look like this: + +![outcome](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US03.png) + +🎉 Congrats! You have successfully added logic to prevent a user from accidentally adding a blank to-do item. + +
    + +> [!NOTE] +> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us03/). + +
    + +[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 7 ](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/README.md) | [**◀ Previous user story** ](User%20Story%202%20-%20Add%20Tabbed%20Interface.md) | [**Next user story** ▶](User%20Story%204%20-%20Confirm%20Delete.md) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 4 - Confirm Delete.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 4 - Confirm Delete.md new file mode 100644 index 00000000..46aaa79a --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 4 - Confirm Delete.md @@ -0,0 +1,121 @@ +# User Story: Confirm Delete - Step-by-Step +⏲️ _Est. time to complete: 30 min._ ⏲️ + +## User Story +*As a user, I want to be able to verify that you really want to delete a task before deleting it.* + +## 🎯Acceptance Criteria: +- A modal dialog should be displayed when the delete button is clicked to verify if you really want to delete the selected item +- The modal dialog should have a cancel and confirm button +- If the confirm button is clicked, the task should be deleted +- If the cancel button is clicked, the modal dialog should be closed and the task should not be deleted +- The interface should look something like this: + + ![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US04.png) + +## 🎓Know Before You Start +no resources at this time + +## 📋Steps + +In order to complete this user story you will need to complete the following tasks + +### Open Visual Studio Code +Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us03/) + +
    + +### Delete Confirmation +If you currently run the application, you may have noticed that if you accidentally hit the "remove" button on any task, the task is immediately deleted. This is generally thought of as a bad user experience. To improve this, we will add a modal dialog to confirm that you really want to delete a task before deleting it. + +#### 1. Add additional Bootstrap script +First, we need to add an additional bootstrap script to the `index.html` file to add support for modal dialogues. Open the `index.html` file and add the following code within the `` tag: + +```html + +``` +
    + +#### 2. Update the user interface to include a modal dialog +First, we need to add a modal dialog to the application. Open the `index.html` file and add the following code to the bottom of the file before the closing `` tag: + +```html + + +``` + +This code will add a modal dialog to the application. The modal dialog will have a title of "Delete Task", a message asking the user if they are sure they want to delete the task, a cancel button, and a delete button. The modal dialog will be hidden by default. + +
    + +#### 3. Update the remove button to show the modal dialog +Next we need to update the remove button to show the modal dialog when the delete button is clicked. Open the `app.js` file replace the `` with the following code: + +```html +Remove +``` + +
    + +#### 4. Add an event listener to the delete button +Next, we need to add an event listener to the delete button to show the modal dialog when the delete button is clicked. Open the `app.js` file and add the following code within the DOMContentLoaded event listener: + +```javascript +const myModal = document.getElementById('confirmModal') +const deleteButtons = document.getElementsByClassName('delete-btn'); +Array.from(deleteButtons).forEach((deleteButton) => { + deleteButton.addEventListener('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + const url = this.getAttribute('data-url'); + document.getElementById('deleteLink').setAttribute('href', url); + const taskname_paragraph = document.querySelector("p[id='taskName']"); + const taskname = this.getAttribute('data-taskname'); + taskname_paragraph.textContent = taskname; + myModal.addEventListener('shown.bs.modal', () => { + deleteButton.focus() + }) + clearHighlight(); + }); +}); +``` + +This code will add an event listener to the delete button. When the delete button is clicked, the code will get the URL of the delete link and the name of the task to be deleted. The code will then set the href attribute of the delete link to the URL of the delete link and set the text of the task name paragraph to the name of the task to be deleted. + +
    + +#### 5. Run the application +Save the changes to the `app.js` and `index.html` files and run the application. Open the application in a browser and try to delete a task. A modal dialog should be displayed asking if you are sure you want to delete the task. If you click the delete button, the task should be deleted. If you click the cancel button, the modal dialog should be closed and the task should not be deleted. The interface should look something like this: + +![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US04.png) + +
    + +🎉 Congratulations! You have now updated the user interface to confirm that you really want to delete a task before deleting it. + +
    + +> [!NOTE] +> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us04/). + +
    + +[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 7 ](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/README.md) | [**◀ Previous user story** ](User%20Story%203%20-%20Prevent%20User%20from%20adding%20blank%20task.md) | [**Next user story** ▶](User%20Story%205%20-%20Show%20Spinner.md) + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 5 - Show Spinner.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 5 - Show Spinner.md new file mode 100644 index 00000000..a7cd48cd --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/Feature 1 - Advanced Styling/User Story 5 - Show Spinner.md @@ -0,0 +1,107 @@ +# User Story: Show spinner when loading recommendations Step-by-Step +⏲️ _Est. time to complete: 30 min._ ⏲️ + +## User Story +*Clicking on the recommendations tab can take some time so we need to let the user visually know that the system is working on loading the recommendations by showing a spinner graphic* + +## 🎯Acceptance Criteria: +- The spinner should be displayed when the recommendations tab is clicked +- The spinner should be removed when the recommendations are loaded +- The spinner should look something like this: + + ![Index](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US05.png) + +## 🎓Know Before You Start +no resources at this time + +## 📋Steps + +In order to complete this user story you will need to complete the following tasks + +### Open Visual Studio Code +Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us04/) + +
    + +### Update the user interface to show a spinner when clicking on the recommendation tab + +#### 1. Update the user interface to support the spinner +In order to add a spinner, first we need to update the user interface to support this functionality. Open the `index.html` file and replace the `
  • ` element for the recommendation tab with the following code: + +```html + +``` +This code will add a spinner to the recommendations tab. The spinner will be hidden by default. + +
    + +#### 2. Update Javascript to handle tab clicks +Next, we need to update the `app.js` file to show the spinner when the recommendations tab is clicked. To do this we are going to override the default "click" behavior so that we can enable the spinner user interface element before calling the server to load recommendations. This will visually show the spinner while we wait for the server to respond. Open the `app.js` file and add the following code within the `DOMContentLoaded` event listener: + +```javascript +//override the default behavior of the nav links so that we can +//turn on a spinner control when the user clicks on the recommendations tab +const navLinks = document.getElementsByClassName('nav-link'); +Array.from(navLinks).forEach((navLink) => { + navLink.addEventListener('click', function(e) { + e.preventDefault(); + const currentPath = this.getAttribute('href'); + const rootUrl = window.location.origin; + + //turn on the spinner control if the user clicks on the recommendations tab + if (this.getAttribute('id') === 'recommendations-tab') { + var recommend_pane = document.querySelector("span[id='recommendation-spinner']"); + if (recommend_pane) { + recommend_pane.removeAttribute('hidden'); + } + } + window.location.href = rootUrl + currentPath; + }); +}); +``` +
    + +### Update the user interface to show the spinner when hitting the refresh button + +#### 1. Update the user interface to support the spinner on the refresh button +Since the recommendation functionality can also be called through the refresh button, we will need to update the user interface to show the spinner when the refresh button is clicked. Open the `index.html` file and add an onclick event to the refresh button, the new code should look like: + +```html + Refresh +``` +
    + +#### 2. Update the `app.js` file to handle the new onclick event +We also need to update the `app.js` file handle this new onclick event. Open the `app.js` file and add the following code within the `DOMContentLoaded` event listener: + +```javascript +window.handleRefresh = function() { + var recommend_pane = document.querySelector("span[id='recommendation-spinner']"); + if (recommend_pane) { + recommend_pane.removeAttribute('hidden'); + } +} +``` + +#### 3. Run the Application +Save all the changes and run the application. Click on the recommendations tab. You should see a spinner displayed next to the Recommendations text. The spinner should be removed when the recommendations are loaded. This should be true for both the initial click on the recommendations tab and the refresh button. The change should look something like this: The partial circle before the recommendations text in the picture should be a spinning circle when you actually run the application versus a partial circle for this screenshot + +![outcome](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/images/outcome-S07-F01-US05.png) + +
    +🎉 Congratulations! You have now updated the user interface to visually show the user when recommendations are loading + +
    + +> [!NOTE] +> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/src/app-s07-f01-us05/). + +
    + +[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 7 ](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/README.md) | [**◀ Previous user story** ](User%20Story%204%20-%20Confirm%20Delete.md) | [**Next user story** (in next sprint) ▶](/Track_2_ToDo_App/Sprint-08%20-%20Deploy%20to%20the%20Cloud/Feature%201%20-%20Deploy%20to%20Azure.md/User%20Story%201%20-%20Deploy%20to%20Azure.md) + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/README.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/README.md new file mode 100644 index 00000000..dc791c11 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/README.md @@ -0,0 +1,15 @@ +# Sprint 7: Advanced Styling For Your Web App +⏲️ _Est. time to complete: 60 min._ ⏲️ + +This sprint is designed to help students add advanced styling to the To-Do application. The sprint will walk students through adding advanced styling features such as a tabbed interface, modal dialog, and advanced rules to ensure that a blank to-do is not added. + +**📕Feature: Advanced Web App Styling** +1. [**📖 Add completed checkbox and due date details to main list**](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/Feature%201%20-%20Advanced%20Styling/User%20Story%201%20-%20Add%20Completed%20Checkbox.md) +2. [**📖 Add a tabbed interface to show different views**](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/Feature%201%20-%20Advanced%20Styling/User%20Story%202%20-%20Add%20Tabbed%20Interface.md) +3. [**📖 Prevent User from adding blank task and limit characters**](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/Feature%201%20-%20Advanced%20Styling/User%20Story%203%20-%20Prevent%20User%20from%20adding%20blank%20task.md) +4. [**📖 Confirm before deleting a task**](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/Feature%201%20-%20Advanced%20Styling/User%20Story%204%20-%20Confirm%20Delete.md) +5. [**📖 Show spinner when loading recommendations**](/Track_2_ToDo_App/Sprint-07%20-%20Advanced%20Styling%20Your%20Web%20App/Feature%201%20-%20Advanced%20Styling/User%20Story%205%20-%20Show%20Spinner.md) + +
    + +[🔼 Hackathon Home Page ](/Track_2_ToDo_App/README.md) | [◀ Previous Sprint](/Track_2_ToDo_App/Sprint-06%20-%20Advanced%20To-Do%20Details/README.md) | [Next sprint ▶](/Track_2_ToDo_App/Sprint-08%20-%20Deploy%20to%20the%20Cloud/README.md) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/ReplaceCode-S07-F01-US02-01.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/ReplaceCode-S07-F01-US02-01.png new file mode 100644 index 00000000..966f5d65 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/ReplaceCode-S07-F01-US02-01.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US01.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US01.png new file mode 100644 index 00000000..61cf5e00 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US01.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US02.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US02.png new file mode 100644 index 00000000..dca8a750 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US02.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US03.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US03.png new file mode 100644 index 00000000..8bbff2f1 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US03.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US04.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US04.png new file mode 100644 index 00000000..91545fea Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US04.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US05.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US05.png new file mode 100644 index 00000000..b6b5c02b Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/outcome-S07-F01-US05.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-1.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-1.png new file mode 100644 index 00000000..ec08db4d Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-1.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-2.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-2.png new file mode 100644 index 00000000..9d837898 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-2.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-3.png b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-3.png new file mode 100644 index 00000000..dca8a750 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/images/tabbed-interface-S07-F01-US02-3.png differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/README.md b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/README.md new file mode 100644 index 00000000..8ccf2d07 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/README.md @@ -0,0 +1,8 @@ +# Sprint 7 - Source Code Directory +This directory contains the completed source code after the end of each user story. The directory is structured as `app-s06-f01-us01` where +- `s07` - represents the sprint number, in this case sprint 7 +- `f01` - represents the feature number, in this case feature 1 +- `us01` - represents the user story number, in this case user story 1 + +> [!NOTE] +> The code in the directory is the completed solution after the completion of that user story. Like many other things in life, there are many ways to solve a problem. The source code in these directories is just one solution to the problem and does not necessarily represent best practices for a given solution. In many cases we chose simplicity or readability over efficiency. \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/.env-example b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/.env-example new file mode 100644 index 00000000..7d05b090 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/.env-example @@ -0,0 +1,9 @@ +DEBUG_APP="True" +USE_AZURE_OPENAI="True" +OPENAI_API_KEY="" +OPENAI_ORG_ID="" +OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo" +AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo" +AZURE_OPENAI_ENDPOINT="" +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_VERSION="2023-05-15" diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/app.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/app.py new file mode 100644 index 00000000..ea263eac --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/app.py @@ -0,0 +1,177 @@ +############################################################################### +## Sprint 6: Advanced To-DO Details +## Feature 1: Add Additional Details to To-Do Items +## User Story 2: Add Priority and Notes Fields +############################################################################ +import os +import json +from flask import Flask, render_template, request, redirect, url_for, g +from database import db, Todo +from recommendation_engine import RecommendationEngine +from tab import Tab +from priority import Priority +from context_processors import inject_current_date + +app = Flask(__name__) +basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file +todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory +app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.context_processor +def inject_common_variables(): + return inject_current_date() + +with app.app_context(): + db.create_all() + +@app.before_request +def load_data_to_g(): + todos = Todo.query.all() + g.todos = todos + g.todo = None + g.TabEnum = Tab + g.PriorityEnum = Priority + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/add", methods=["POST"]) +def add_todo(): + + # Get the data from the form + todo = Todo( + name=request.form["todo"] + ) + + # Add the new ToDo to the list + db.session.add(todo) + db.session.commit() + + # Add the new ToDo to the list + return redirect(url_for('index')) + +# Details of ToDo Item +@app.route('/details/', methods=['GET']) +def details(id): + g.selectedTab = Tab.DETAILS + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Edit a new ToDo +@app.route('/edit/', methods=['GET']) +def edit(id): + g.selectedTab = Tab.EDIT + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Save existing To Do Item +@app.route('/update/', methods=['POST']) +def update_todo(id): + g.selectedTab = Tab.DETAILS + + if request.form.get('cancel') != None: + return redirect(url_for('index')) + + # Get the data from the form + name = request.form['name'] + due_date = request.form.get('duedate') + notes=request.form.get('notes') + priority=request.form.get('priority') + completed=request.form.get('completed') + + todo = db.session.query(Todo).filter_by(id=id).first() + if todo != None: + todo.name = name + + if due_date != "None": + todo.due_date = due_date + + if notes != None: + todo.notes = notes + + if priority != None: + todo.priority = int(priority) + + if completed == None: + todo.completed = False + elif completed == "on": + todo.completed = True + # + db.session.add(todo) + db.session.commit() + # + return redirect(url_for('index')) + + +# Delete a ToDo +@app.route('/remove/', methods=["POST"]) +def remove_todo(id): + g.selectedTab = Tab.NONE + db.session.delete(Todo.query.filter_by(id=id).first()) + db.session.commit() + return redirect(url_for('index')) + +# Show AI recommendations +@app.route('/recommend/', methods=['GET']) +@app.route('/recommend//', methods=['GET']) +async def recommend(id, refresh=False): + g.selectedTab = Tab.RECOMMENDATIONS + recommendation_engine = RecommendationEngine() + g.todo = db.session.query(Todo).filter_by(id=id).first() + + if g.todo and not refresh: + try: + #attempt to load any saved recommendation from the DB + if g.todo.recommendations_json is not None: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + return render_template('index.html') + except ValueError as e: + print("Error:", e) + + previous_links_str = None + if refresh: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + # Extract links + links = [item["link"] for item in g.todo.recommendations] + # Convert list of links to a single string + previous_links_str = ", ".join(links) + + g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str) + + # Save the recommendations to the database + try: + g.todo.recommendations_json = json.dumps(g.todo.recommendations) + db.session.add(g.todo) + db.session.commit() + except Exception as e: + print(f"Error adding and committing todo: {e}") + return + + return render_template('index.html') + +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + + #update todo in the database + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/context_processors.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/context_processors.py new file mode 100644 index 00000000..a9690944 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/context_processors.py @@ -0,0 +1,6 @@ +# context_processors.py + +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/database.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/database.py new file mode 100644 index 00000000..228b939c --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/database.py @@ -0,0 +1,50 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Boolean, JSON, func +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +class Todo(db.Model): + id = db.Column(Integer, primary_key=True) + name = db.Column(String(100), nullable=False) + notes = db.Column(String(100)) + priority = db.Column(Integer, default=0) + completed = db.Column(Boolean, default=False) + recommendations_json = db.Column(db.JSON) #json serialized version of recommendations for storing in DB + due_date = db.Column(String(50)) + + #transient variables (i.e., not stored in db) + recommendations = [] #recommendations as a collection + + def __str__(self): + return self.name + + def priority_str(self): + """ + Returns the priority as a string. + """ + str = "Not Set" + if self.priority == 1: + str = "High" + elif self.priority == 2: + str = "Medium" + elif self.priority == 3: + str = "Low" + + return str + + def completed_str(self): + """ + Returns the completed status as a string. + """ + if self.completed: + return "Yes" + else: + return "No" + + + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/priority.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/priority.py new file mode 100644 index 00000000..2afa8405 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/priority.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Priority(Enum): + NONE = 0, + HIGH = 1, + MEDIUM = 2, + LOW = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/recommendation_engine.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/recommendation_engine.py new file mode 100644 index 00000000..35bed9af --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/recommendation_engine.py @@ -0,0 +1,77 @@ +import json +import asyncio +import semantic_kernel as sk +from services import Service +from openai import AzureOpenAI +from dotenv import dotenv_values + +config = dotenv_values(".env") + +#uses the USE_AZURE_OPENAI variable from the .env file to determine which AI service to use +#False means use OpenAI, True means use Azure OpenAI +selectedService = Service.AzureOpenAI if config.get("USE_AZURE_OPENAI") == "True" else Service.OpenAI +deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + +class RecommendationEngine: + def __init__(self): + deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + + self.client = AzureOpenAI(azure_endpoint = endpoint, + api_key=api_key, + api_version="2024-02-15-preview" + ) + + + async def get_recommendations(self, keyword_phrase, previous_links_str=None): + prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE""" + system_prompt = """You are an administrative assistant bot who is good at giving + recommendations for tasks that need to be done by referencing website links that can provide + assistance to helping complete the task. + + If there are not any recommendations simply return an empty collection. + + EXPECTED OUTPUT: + Provide your response as a JSON object with the following schema: + [{"title": "...", "link": "..."}, + {"title": "...", "link": "..."}, + {"title": "...", "link": "..."}] + """ + + if previous_links_str is not None: + prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}" + + message_text = [{"role":"system","content":system_prompt}, + {"role":"user","content":prompt},] + + response = self.client.chat.completions.create( + model=deployment, + messages = message_text, + temperature=0.14, + max_tokens=800, + top_p=0.17, + frequency_penalty=0, + presence_penalty=0, + stop=None + ) + + result = response.choices[0].message.content + print(result) + + try: + recommendation = json.loads(result) + except Exception as e: + print(f"Error loading recommendations: {e}") + recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}] + + return recommendation + +async def test_recommendation_engine(): + engine = RecommendationEngine() + recommendations = await engine.get_recommendations("Buy a birthday gift for mom") + count = 1 + for recommendation in recommendations: + print(f"{count} - {recommendation['title']}: {recommendation['link']}") + count += 1 + +if __name__ == "__main__": + asyncio.run(test_recommendation_engine()) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/services.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/services.py new file mode 100644 index 00000000..657f1270 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/services.py @@ -0,0 +1,18 @@ +""" +This module defines an enumeration representing different services. +""" + +from enum import Enum + + +class Service(Enum): + """ + Attributes: + OpenAI (str): Represents the OpenAI service. + AzureOpenAI (str): Represents the Azure OpenAI service. + HuggingFace (str): Represents the HuggingFace service. + """ + + OpenAI = "openai" + AzureOpenAI = "azureopenai" + HuggingFace = "huggingface" \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/css/style.css b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/css/style.css new file mode 100644 index 00000000..2cd15af8 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/css/style.css @@ -0,0 +1,34 @@ +body { + background-color: f0f0f0; /* light grey */ + color: darkslategray; /* default font color */ + font-family: Arial, sans-serif; + background-image: url("../images/Designer02.jpeg"); + background-repeat: no-repeat; + background-position: center; + background-size:cover; +} + +h1 { + color: darkgray; /* font for the h1 header*/ +} + +.list-group-item { + color: #333; /* dark grey */ +} + +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer01.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer01.jpeg new file mode 100644 index 00000000..f59d0d61 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer01.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer02.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer02.jpeg new file mode 100644 index 00000000..0c36a33e Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/Designer02.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/favicon.ico b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/favicon.ico new file mode 100644 index 00000000..47f38580 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/images/favicon.ico differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/js/app.js b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/js/app.js new file mode 100644 index 00000000..baf10c27 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/static/js/app.js @@ -0,0 +1,41 @@ +document.addEventListener("DOMContentLoaded", function () { + const nameInput = document.getElementById("todo"); + + window.handleClick = function (event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + }; + + //add javascript to support speech recognition for the todo input field + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.lang = "en-US"; + recognition.interimResults = false; + + window.captureVoice = function () { + recognition.start(); + nameInput.value = "Your microphone is activated, speak to record voice"; + }; + + recognition.onresult = function (event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + }; + + recognition.onspeechend = function () { + recognition.stop(); + }; + + recognition.onnomatch = function (event) { + nameInput.value = "I didn't recognize that prompt."; + }; + + recognition.onerror = function (event) { + nameInput.value = "Error occurred in recognition: " + event.error; + }; +}); \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/tab.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/tab.py new file mode 100644 index 00000000..982b8536 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/tab.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Tab(Enum): + NONE = 0, + DETAILS = 1, + EDIT = 2, + RECOMMENDATIONS = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/templates/index.html b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/templates/index.html new file mode 100644 index 00000000..29ffd26d --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us01/templates/index.html @@ -0,0 +1,184 @@ + + + + My To-Do List + + + + + + +
    +
    +
    +
    +

    My To-Do List

    +
    +
    +
    +
    +
    +
      + {% for todo in g.todos %} +
    1. +
      +
      + {% if todo.completed %} + + {% else %} + + {% endif %} + +
      {{ todo.name }}
      +
      + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
      +
      +
      + + + + + + +
    2. + {% endfor %} +
    +
    +
    +
    +
    +
    + + + + + + +
    + +
    +
    + {% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %} +
    +
    +
    +

    AI Recommendations for "{{ g.todo.name }}"

    + {% for recommend in g.todo.recommendations %} + {{ recommend.title }} + {% endfor %} +
    +
    + Don't like recommendations? + Refresh +
    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.DETAILS %} +
    +
    +

    Task: {{ g.todo.name }}

    +

    Priority: {{ g.todo.priority_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.EDIT %} +
    +
    +

    Edit

    +
    + +
    + + +
    +
    +
    +

    Priority:

    + {% if g.todo != None and g.todo.priority == 1 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 2 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 3 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + {% if g.todo.completed %} + + + {% else %} + + + {% endif %} +
    +
    + + +
    +
    +
    + {% endif %} +
    +
    +
    + + \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/.env-example b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/.env-example new file mode 100644 index 00000000..7d05b090 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/.env-example @@ -0,0 +1,9 @@ +DEBUG_APP="True" +USE_AZURE_OPENAI="True" +OPENAI_API_KEY="" +OPENAI_ORG_ID="" +OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo" +AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo" +AZURE_OPENAI_ENDPOINT="" +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_VERSION="2023-05-15" diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/app.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/app.py new file mode 100644 index 00000000..fc7ed612 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/app.py @@ -0,0 +1,177 @@ +############################################################################### +## Sprint 6: Advanced To-DO Details +## Feature 1: Add Additional Details to To-Do Items +## User Story 2: Add Priority and Notes Fields +############################################################################ +import os +import json +from flask import Flask, render_template, request, redirect, url_for, g +from database import db, Todo +from recommendation_engine import RecommendationEngine +from tab import Tab +from priority import Priority +from context_processors import inject_current_date + +app = Flask(__name__) +basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file +todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory +app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.context_processor +def inject_common_variables(): + return inject_current_date() + +with app.app_context(): + db.create_all() + +@app.before_request +def load_data_to_g(): + todos = Todo.query.all() + g.todos = todos + g.todo = None + g.TabEnum = Tab + g.PriorityEnum = Priority + g.selectedTab = Tab.NONE + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/add", methods=["POST"]) +def add_todo(): + + # Get the data from the form + todo = Todo( + name=request.form["todo"] + ) + + # Add the new ToDo to the list + db.session.add(todo) + db.session.commit() + + # Add the new ToDo to the list + return redirect(url_for('index')) + +# Details of ToDo Item +@app.route('/details/', methods=['GET']) +def details(id): + g.selectedTab = Tab.DETAILS + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Edit a new ToDo +@app.route('/edit/', methods=['GET']) +def edit(id): + g.selectedTab = Tab.EDIT + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Save existing To Do Item +@app.route('/update/', methods=['POST']) +def update_todo(id): + g.selectedTab = Tab.DETAILS + + if request.form.get('cancel') != None: + return redirect(url_for('index')) + + # Get the data from the form + name = request.form['name'] + due_date = request.form.get('duedate') + notes=request.form.get('notes') + priority=request.form.get('priority') + completed=request.form.get('completed') + + todo = db.session.query(Todo).filter_by(id=id).first() + if todo != None: + todo.name = name + + if due_date != "None": + todo.due_date = due_date + + if notes != None: + todo.notes = notes + + if priority != None: + todo.priority = int(priority) + + if completed == None: + todo.completed = False + elif completed == "on": + todo.completed = True + # + db.session.add(todo) + db.session.commit() + # + return redirect(url_for('index')) + + +# Delete a ToDo +@app.route('/remove/', methods=["POST"]) +def remove_todo(id): + g.selectedTab = Tab.NONE + db.session.delete(Todo.query.filter_by(id=id).first()) + db.session.commit() + return redirect(url_for('index')) + +# Show AI recommendations +@app.route('/recommend/', methods=['GET']) +@app.route('/recommend//', methods=['GET']) +async def recommend(id, refresh=False): + g.selectedTab = Tab.RECOMMENDATIONS + recommendation_engine = RecommendationEngine() + g.todo = db.session.query(Todo).filter_by(id=id).first() + + if g.todo and not refresh: + try: + #attempt to load any saved recommendation from the DB + if g.todo.recommendations_json is not None: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + return render_template('index.html') + except ValueError as e: + print("Error:", e) + + previous_links_str = None + if refresh: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + # Extract links + links = [item["link"] for item in g.todo.recommendations] + # Convert list of links to a single string + previous_links_str = ", ".join(links) + + g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str) + + # Save the recommendations to the database + try: + g.todo.recommendations_json = json.dumps(g.todo.recommendations) + db.session.add(g.todo) + db.session.commit() + except Exception as e: + print(f"Error adding and committing todo: {e}") + return + + return render_template('index.html') + +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + # + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/context_processors.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/context_processors.py new file mode 100644 index 00000000..a9690944 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/context_processors.py @@ -0,0 +1,6 @@ +# context_processors.py + +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/database.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/database.py new file mode 100644 index 00000000..7a99d674 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/database.py @@ -0,0 +1,48 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Boolean, JSON, func +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +class Todo(db.Model): + id = db.Column(Integer, primary_key=True) + name = db.Column(String(100), nullable=False) + notes = db.Column(String(100)) + priority = db.Column(Integer, default=0) + completed = db.Column(Boolean, default=False) + recommendations_json = db.Column(db.JSON) #json serialized version of recommendations for storing in DB + due_date = db.Column(String(50)) + + #transient variables (i.e., not stored in db) + recommendations = [] #recommendations as a collection + + def __str__(self): + return self.name + + + def priority_str(self): + """ + Returns the priority as a string. + """ + str = "Not Set" + if self.priority == 1: + str = "High" + elif self.priority == 2: + str = "Medium" + elif self.priority == 3: + str = "Low" + + return str + + def completed_str(self): + """ + Returns the completed status as a string. + """ + if self.completed: + return "Yes" + else: + return "No" + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/priority.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/priority.py new file mode 100644 index 00000000..2afa8405 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/priority.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Priority(Enum): + NONE = 0, + HIGH = 1, + MEDIUM = 2, + LOW = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/recommendation_engine.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/recommendation_engine.py new file mode 100644 index 00000000..35bed9af --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/recommendation_engine.py @@ -0,0 +1,77 @@ +import json +import asyncio +import semantic_kernel as sk +from services import Service +from openai import AzureOpenAI +from dotenv import dotenv_values + +config = dotenv_values(".env") + +#uses the USE_AZURE_OPENAI variable from the .env file to determine which AI service to use +#False means use OpenAI, True means use Azure OpenAI +selectedService = Service.AzureOpenAI if config.get("USE_AZURE_OPENAI") == "True" else Service.OpenAI +deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + +class RecommendationEngine: + def __init__(self): + deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + + self.client = AzureOpenAI(azure_endpoint = endpoint, + api_key=api_key, + api_version="2024-02-15-preview" + ) + + + async def get_recommendations(self, keyword_phrase, previous_links_str=None): + prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE""" + system_prompt = """You are an administrative assistant bot who is good at giving + recommendations for tasks that need to be done by referencing website links that can provide + assistance to helping complete the task. + + If there are not any recommendations simply return an empty collection. + + EXPECTED OUTPUT: + Provide your response as a JSON object with the following schema: + [{"title": "...", "link": "..."}, + {"title": "...", "link": "..."}, + {"title": "...", "link": "..."}] + """ + + if previous_links_str is not None: + prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}" + + message_text = [{"role":"system","content":system_prompt}, + {"role":"user","content":prompt},] + + response = self.client.chat.completions.create( + model=deployment, + messages = message_text, + temperature=0.14, + max_tokens=800, + top_p=0.17, + frequency_penalty=0, + presence_penalty=0, + stop=None + ) + + result = response.choices[0].message.content + print(result) + + try: + recommendation = json.loads(result) + except Exception as e: + print(f"Error loading recommendations: {e}") + recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}] + + return recommendation + +async def test_recommendation_engine(): + engine = RecommendationEngine() + recommendations = await engine.get_recommendations("Buy a birthday gift for mom") + count = 1 + for recommendation in recommendations: + print(f"{count} - {recommendation['title']}: {recommendation['link']}") + count += 1 + +if __name__ == "__main__": + asyncio.run(test_recommendation_engine()) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/services.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/services.py new file mode 100644 index 00000000..657f1270 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/services.py @@ -0,0 +1,18 @@ +""" +This module defines an enumeration representing different services. +""" + +from enum import Enum + + +class Service(Enum): + """ + Attributes: + OpenAI (str): Represents the OpenAI service. + AzureOpenAI (str): Represents the Azure OpenAI service. + HuggingFace (str): Represents the HuggingFace service. + """ + + OpenAI = "openai" + AzureOpenAI = "azureopenai" + HuggingFace = "huggingface" \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/css/style.css b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/css/style.css new file mode 100644 index 00000000..02243a3f --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/css/style.css @@ -0,0 +1,46 @@ +body { + background-color: f0f0f0; /* light grey */ + color: darkslategray; /* default font color */ + font-family: Arial, sans-serif; + background-image: url("../images/Designer02.jpeg"); + background-repeat: no-repeat; + background-position: center; + background-size:cover; +} + +h1 { + color: darkgray; /* font for the h1 header*/ +} + +.list-group-item { + color: #333; /* dark grey */ +} + +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} + +.card { + border-top: none !important; +} + +.nav-link { + background-color: white; +} + +.highlighted-item { + background-color: #f2f2f2; +} + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer01.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer01.jpeg new file mode 100644 index 00000000..f59d0d61 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer01.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer02.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer02.jpeg new file mode 100644 index 00000000..0c36a33e Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/Designer02.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/favicon.ico b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/favicon.ico new file mode 100644 index 00000000..47f38580 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/images/favicon.ico differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/js/app.js b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/js/app.js new file mode 100644 index 00000000..f474bf30 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/static/js/app.js @@ -0,0 +1,105 @@ +document.addEventListener("DOMContentLoaded", function() { + const HIGHLIGHTEDITEM = 'highlighted-item'; + const currentPath = window.location.pathname; + + // Set the active tab based on the URL + const setActiveTab = (tabId) => { + const tabElement = document.getElementById(tabId); + if (tabElement) { + tabElement.classList.add('active'); + } + }; + + switch (true) { + case currentPath.includes('/edit'): + setActiveTab('edit-tab'); + break; + case currentPath.includes('/completed'): + setActiveTab('completed-tab'); + break; + case currentPath.includes('/details'): + setActiveTab('details-tab'); + break; + case currentPath.includes('/recommend'): + setActiveTab('recommendations-tab'); + break; + default: + break; + } + + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + console.log('highlightedItemId', highlightedItemId); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.add(HIGHLIGHTEDITEM); + } + } + + window.clearHighlight = function() { + localStorage.removeItem(HIGHLIGHTEDITEM); + }; + + window.showDetails = function(li) { + highlight(li); + const rootUrl = window.location.origin; + const dataId = li.getAttribute('data-id'); + window.location.href = `${rootUrl}/details/${dataId}`; + }; + + window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + clearHighlight(); + }; + + + window.highlight = function(element) { + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.remove(HIGHLIGHTEDITEM); + } + } + + const closestListItem = element.closest('li'); + closestListItem.classList.add(HIGHLIGHTEDITEM); + + localStorage.setItem(HIGHLIGHTEDITEM, closestListItem.id); + }; + + + //add javascript to support speech recognition for the todo input field + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.lang = "en-US"; + recognition.interimResults = false; + + window.captureVoice = function () { + recognition.start(); + nameInput.value = "Your microphone is activated, speak to record voice"; + }; + + recognition.onresult = function (event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + }; + + recognition.onspeechend = function () { + recognition.stop(); + }; + + recognition.onnomatch = function (event) { + nameInput.value = "I didn't recognize that prompt."; + }; + + recognition.onerror = function (event) { + nameInput.value = "Error occurred in recognition: " + event.error; + }; +}); \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/tab.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/tab.py new file mode 100644 index 00000000..982b8536 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/tab.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Tab(Enum): + NONE = 0, + DETAILS = 1, + EDIT = 2, + RECOMMENDATIONS = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/templates/index.html b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/templates/index.html new file mode 100644 index 00000000..bc8e0bec --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us02/templates/index.html @@ -0,0 +1,193 @@ + + + + My To-Do List + + + + + + +
    +
    +
    +
    +

    My To-Do List

    +
    +
    +
    +
    +
    +
      + {% for todo in g.todos %} +
    1. +
      +
      + {% if todo.completed %} + + {% else %} + + {% endif %} + +
      {{ todo.name }}
      +
      + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
      +
      +
      + + + +
    2. + {% endfor %} +
    +
    +
    +
    +
    +
    + + + + + + +
    + +
    +
    + {% if g.selectedTab != g.TabEnum.NONE %} + + {% endif %} + + {% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %} +
    +
    +
    + {% for recommend in g.todo.recommendations %} + {{ recommend.title }} + {% endfor %} +
    +
    + Don't like recommendations? + Refresh +
    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.DETAILS %} +
    +
    +

    Task: {{ g.todo.name }}

    +

    Priority: {{ g.todo.priority_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.EDIT %} +
    +
    +
    + +
    + + +
    +
    +
    +

    Priority:

    + {% if g.todo != None and g.todo.priority == 1 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 2 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 3 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + {% if g.todo.completed %} + + + {% else %} + + + {% endif %} +
    +
    + + +
    +
    +
    + {% endif %} +
    +
    + + + \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/.env-example b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/.env-example new file mode 100644 index 00000000..7d05b090 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/.env-example @@ -0,0 +1,9 @@ +DEBUG_APP="True" +USE_AZURE_OPENAI="True" +OPENAI_API_KEY="" +OPENAI_ORG_ID="" +OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo" +AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo" +AZURE_OPENAI_ENDPOINT="" +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_VERSION="2023-05-15" diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/app.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/app.py new file mode 100644 index 00000000..fc7ed612 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/app.py @@ -0,0 +1,177 @@ +############################################################################### +## Sprint 6: Advanced To-DO Details +## Feature 1: Add Additional Details to To-Do Items +## User Story 2: Add Priority and Notes Fields +############################################################################ +import os +import json +from flask import Flask, render_template, request, redirect, url_for, g +from database import db, Todo +from recommendation_engine import RecommendationEngine +from tab import Tab +from priority import Priority +from context_processors import inject_current_date + +app = Flask(__name__) +basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file +todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory +app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.context_processor +def inject_common_variables(): + return inject_current_date() + +with app.app_context(): + db.create_all() + +@app.before_request +def load_data_to_g(): + todos = Todo.query.all() + g.todos = todos + g.todo = None + g.TabEnum = Tab + g.PriorityEnum = Priority + g.selectedTab = Tab.NONE + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/add", methods=["POST"]) +def add_todo(): + + # Get the data from the form + todo = Todo( + name=request.form["todo"] + ) + + # Add the new ToDo to the list + db.session.add(todo) + db.session.commit() + + # Add the new ToDo to the list + return redirect(url_for('index')) + +# Details of ToDo Item +@app.route('/details/', methods=['GET']) +def details(id): + g.selectedTab = Tab.DETAILS + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Edit a new ToDo +@app.route('/edit/', methods=['GET']) +def edit(id): + g.selectedTab = Tab.EDIT + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Save existing To Do Item +@app.route('/update/', methods=['POST']) +def update_todo(id): + g.selectedTab = Tab.DETAILS + + if request.form.get('cancel') != None: + return redirect(url_for('index')) + + # Get the data from the form + name = request.form['name'] + due_date = request.form.get('duedate') + notes=request.form.get('notes') + priority=request.form.get('priority') + completed=request.form.get('completed') + + todo = db.session.query(Todo).filter_by(id=id).first() + if todo != None: + todo.name = name + + if due_date != "None": + todo.due_date = due_date + + if notes != None: + todo.notes = notes + + if priority != None: + todo.priority = int(priority) + + if completed == None: + todo.completed = False + elif completed == "on": + todo.completed = True + # + db.session.add(todo) + db.session.commit() + # + return redirect(url_for('index')) + + +# Delete a ToDo +@app.route('/remove/', methods=["POST"]) +def remove_todo(id): + g.selectedTab = Tab.NONE + db.session.delete(Todo.query.filter_by(id=id).first()) + db.session.commit() + return redirect(url_for('index')) + +# Show AI recommendations +@app.route('/recommend/', methods=['GET']) +@app.route('/recommend//', methods=['GET']) +async def recommend(id, refresh=False): + g.selectedTab = Tab.RECOMMENDATIONS + recommendation_engine = RecommendationEngine() + g.todo = db.session.query(Todo).filter_by(id=id).first() + + if g.todo and not refresh: + try: + #attempt to load any saved recommendation from the DB + if g.todo.recommendations_json is not None: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + return render_template('index.html') + except ValueError as e: + print("Error:", e) + + previous_links_str = None + if refresh: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + # Extract links + links = [item["link"] for item in g.todo.recommendations] + # Convert list of links to a single string + previous_links_str = ", ".join(links) + + g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str) + + # Save the recommendations to the database + try: + g.todo.recommendations_json = json.dumps(g.todo.recommendations) + db.session.add(g.todo) + db.session.commit() + except Exception as e: + print(f"Error adding and committing todo: {e}") + return + + return render_template('index.html') + +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + # + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/context_processors.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/context_processors.py new file mode 100644 index 00000000..a9690944 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/context_processors.py @@ -0,0 +1,6 @@ +# context_processors.py + +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/database.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/database.py new file mode 100644 index 00000000..fb8c6cba --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/database.py @@ -0,0 +1,48 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Boolean, JSON, func +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +class Todo(db.Model): + id = db.Column(Integer, primary_key=True) + name = db.Column(String(100), nullable=False) + notes = db.Column(String(100)) + priority = db.Column(Integer, default=0) + completed = db.Column(Boolean, default=False) + recommendations_json = db.Column(db.JSON) #json serialized version of recommendations for storing in DB + due_date = db.Column(String(50)) + + #transient variables (i.e., not stored in db) + recommendations = [] #recommendations as a collection + + def __str__(self): + return self.name + + def priority_str(self): + """ + Returns the priority as a string. + """ + str = "Not Set" + if self.priority == 1: + str = "High" + elif self.priority == 2: + str = "Medium" + elif self.priority == 3: + str = "Low" + + return str + + def completed_str(self): + """ + Returns the completed status as a string. + """ + if self.completed: + return "Yes" + else: + return "No" + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/priority.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/priority.py new file mode 100644 index 00000000..2afa8405 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/priority.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Priority(Enum): + NONE = 0, + HIGH = 1, + MEDIUM = 2, + LOW = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/recommendation_engine.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/recommendation_engine.py new file mode 100644 index 00000000..35bed9af --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/recommendation_engine.py @@ -0,0 +1,77 @@ +import json +import asyncio +import semantic_kernel as sk +from services import Service +from openai import AzureOpenAI +from dotenv import dotenv_values + +config = dotenv_values(".env") + +#uses the USE_AZURE_OPENAI variable from the .env file to determine which AI service to use +#False means use OpenAI, True means use Azure OpenAI +selectedService = Service.AzureOpenAI if config.get("USE_AZURE_OPENAI") == "True" else Service.OpenAI +deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + +class RecommendationEngine: + def __init__(self): + deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + + self.client = AzureOpenAI(azure_endpoint = endpoint, + api_key=api_key, + api_version="2024-02-15-preview" + ) + + + async def get_recommendations(self, keyword_phrase, previous_links_str=None): + prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE""" + system_prompt = """You are an administrative assistant bot who is good at giving + recommendations for tasks that need to be done by referencing website links that can provide + assistance to helping complete the task. + + If there are not any recommendations simply return an empty collection. + + EXPECTED OUTPUT: + Provide your response as a JSON object with the following schema: + [{"title": "...", "link": "..."}, + {"title": "...", "link": "..."}, + {"title": "...", "link": "..."}] + """ + + if previous_links_str is not None: + prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}" + + message_text = [{"role":"system","content":system_prompt}, + {"role":"user","content":prompt},] + + response = self.client.chat.completions.create( + model=deployment, + messages = message_text, + temperature=0.14, + max_tokens=800, + top_p=0.17, + frequency_penalty=0, + presence_penalty=0, + stop=None + ) + + result = response.choices[0].message.content + print(result) + + try: + recommendation = json.loads(result) + except Exception as e: + print(f"Error loading recommendations: {e}") + recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}] + + return recommendation + +async def test_recommendation_engine(): + engine = RecommendationEngine() + recommendations = await engine.get_recommendations("Buy a birthday gift for mom") + count = 1 + for recommendation in recommendations: + print(f"{count} - {recommendation['title']}: {recommendation['link']}") + count += 1 + +if __name__ == "__main__": + asyncio.run(test_recommendation_engine()) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/services.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/services.py new file mode 100644 index 00000000..657f1270 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/services.py @@ -0,0 +1,18 @@ +""" +This module defines an enumeration representing different services. +""" + +from enum import Enum + + +class Service(Enum): + """ + Attributes: + OpenAI (str): Represents the OpenAI service. + AzureOpenAI (str): Represents the Azure OpenAI service. + HuggingFace (str): Represents the HuggingFace service. + """ + + OpenAI = "openai" + AzureOpenAI = "azureopenai" + HuggingFace = "huggingface" \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/css/style.css b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/css/style.css new file mode 100644 index 00000000..9a6717e5 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/css/style.css @@ -0,0 +1,52 @@ +body { + background-color: f0f0f0; /* light grey */ + color: darkslategray; /* default font color */ + font-family: Arial, sans-serif; + background-image: url("../images/Designer02.jpeg"); + background-repeat: no-repeat; + background-position: center; + background-size:cover; +} + +h1 { + color: darkgray; /* font for the h1 header*/ +} + +.list-group-item { + color: #333; /* dark grey */ +} + +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} + +.card { + border-top: none !important; +} + +.nav-link { + background-color: white; +} + +.highlighted-item { + background-color: #f2f2f2; +} + +.limit-text { + color: white; +} + + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer01.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer01.jpeg new file mode 100644 index 00000000..f59d0d61 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer01.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer02.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer02.jpeg new file mode 100644 index 00000000..0c36a33e Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/Designer02.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/favicon.ico b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/favicon.ico new file mode 100644 index 00000000..47f38580 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/images/favicon.ico differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/js/app.js b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/js/app.js new file mode 100644 index 00000000..b4ad0c04 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/static/js/app.js @@ -0,0 +1,116 @@ +document.addEventListener("DOMContentLoaded", function() { + const HIGHLIGHTEDITEM = 'highlighted-item'; + const currentPath = window.location.pathname; + + // Set the active tab based on the URL + const setActiveTab = (tabId) => { + const tabElement = document.getElementById(tabId); + if (tabElement) { + tabElement.classList.add('active'); + } + }; + + switch (true) { + case currentPath.includes('/edit'): + setActiveTab('edit-tab'); + break; + case currentPath.includes('/completed'): + setActiveTab('completed-tab'); + break; + case currentPath.includes('/details'): + setActiveTab('details-tab'); + break; + case currentPath.includes('/recommend'): + setActiveTab('recommendations-tab'); + break; + default: + break; + } + + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + console.log('highlightedItemId', highlightedItemId); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.add(HIGHLIGHTEDITEM); + } + } + + window.clearHighlight = function() { + localStorage.removeItem(HIGHLIGHTEDITEM); + }; + + window.onAdd = function() { + clearHighlight(); + } + + window.showDetails = function(li) { + console.log(li); + highlight(li); + const rootUrl = window.location.origin; + const dataId = li.getAttribute('data-id'); + window.location.href = `${rootUrl}/details/${dataId}`; + }; + + window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + clearHighlight(); + }; + + + window.highlight = function(element) { + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.remove(HIGHLIGHTEDITEM); + } + } + + const closestListItem = element.closest('li'); + closestListItem.classList.add(HIGHLIGHTEDITEM); + + localStorage.setItem(HIGHLIGHTEDITEM, closestListItem.id); + }; + + const nameInput = document.getElementById("todo"); + nameInput.addEventListener("keyup", function() { + const addButton = document.querySelector("button[id='addButton']"); + addButton.disabled = this.value.trim() === ""; + }); + + + //add javascript to support speech recognition for the todo input field + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.lang = "en-US"; + recognition.interimResults = false; + + window.captureVoice = function () { + recognition.start(); + nameInput.value = "Your microphone is activated, speak to record voice"; + }; + + recognition.onresult = function (event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + }; + + recognition.onspeechend = function () { + recognition.stop(); + }; + + recognition.onnomatch = function (event) { + nameInput.value = "I didn't recognize that prompt."; + }; + + recognition.onerror = function (event) { + nameInput.value = "Error occurred in recognition: " + event.error; + }; +}); \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/tab.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/tab.py new file mode 100644 index 00000000..982b8536 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/tab.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Tab(Enum): + NONE = 0, + DETAILS = 1, + EDIT = 2, + RECOMMENDATIONS = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/templates/index.html b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/templates/index.html new file mode 100644 index 00000000..0f48a83a --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us03/templates/index.html @@ -0,0 +1,191 @@ + + + + My To-Do List + + + + + + +
    +
    +
    +
    +

    My To-Do List

    +
    +
    +
    +
    +
    +
      + {% for todo in g.todos %} +
    1. +
      +
      + {% if todo.completed %} + + {% else %} + + {% endif %} + +
      {{ todo.name }}
      +
      + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
      +
      +
      + + + +
    2. + {% endfor %} +
    +
    +
    + + + + + + + Maximum 75 characters +
    + +
    +
    + {% if g.selectedTab != g.TabEnum.NONE %} + + {% endif %} + + {% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %} +
    +
    +
    + {% for recommend in g.todo.recommendations %} + {{ recommend.title }} + {% endfor %} +
    +
    + Don't like recommendations? + Refresh +
    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.DETAILS %} +
    +
    +

    Task: {{ g.todo.name }}

    +

    Priority: {{ g.todo.priority_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.EDIT %} +
    +
    +
    + +
    + + +
    +
    +
    +

    Priority:

    + {% if g.todo != None and g.todo.priority == 1 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 2 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 3 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + {% if g.todo.completed %} + + + {% else %} + + + {% endif %} +
    +
    + + +
    +
    +
    + {% endif %} +
    +
    + + + \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/.env-example b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/.env-example new file mode 100644 index 00000000..7d05b090 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/.env-example @@ -0,0 +1,9 @@ +DEBUG_APP="True" +USE_AZURE_OPENAI="True" +OPENAI_API_KEY="" +OPENAI_ORG_ID="" +OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo" +AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo" +AZURE_OPENAI_ENDPOINT="" +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_VERSION="2023-05-15" diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/app.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/app.py new file mode 100644 index 00000000..618ecd5c --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/app.py @@ -0,0 +1,177 @@ +############################################################################### +## Sprint 6: Advanced To-DO Details +## Feature 1: Add Additional Details to To-Do Items +## User Story 2: Add Priority and Notes Fields +############################################################################ +import os +import json +from flask import Flask, render_template, request, redirect, url_for, g +from database import db, Todo +from recommendation_engine import RecommendationEngine +from tab import Tab +from priority import Priority +from context_processors import inject_current_date + +app = Flask(__name__) +basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file +todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory +app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.context_processor +def inject_common_variables(): + return inject_current_date() + +with app.app_context(): + db.create_all() + +@app.before_request +def load_data_to_g(): + todos = Todo.query.all() + g.todos = todos + g.todo = None + g.TabEnum = Tab + g.PriorityEnum = Priority + g.selectedTab = Tab.NONE + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/add", methods=["POST"]) +def add_todo(): + + # Get the data from the form + todo = Todo( + name=request.form["todo"] + ) + + # Add the new ToDo to the list + db.session.add(todo) + db.session.commit() + + # Add the new ToDo to the list + return redirect(url_for('index')) + +# Details of ToDo Item +@app.route('/details/', methods=['GET']) +def details(id): + g.selectedTab = Tab.DETAILS + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Edit a new ToDo +@app.route('/edit/', methods=['GET']) +def edit(id): + g.selectedTab = Tab.EDIT + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Save existing To Do Item +@app.route('/update/', methods=['POST']) +def update_todo(id): + g.selectedTab = Tab.DETAILS + + if request.form.get('cancel') != None: + return redirect(url_for('index')) + + # Get the data from the form + name = request.form['name'] + due_date = request.form.get('duedate') + notes=request.form.get('notes') + priority=request.form.get('priority') + completed=request.form.get('completed') + + todo = db.session.query(Todo).filter_by(id=id).first() + if todo != None: + todo.name = name + + if due_date != "None": + todo.due_date = due_date + + if notes != None: + todo.notes = notes + + if priority != None: + todo.priority = int(priority) + + if completed == None: + todo.completed = False + elif completed == "on": + todo.completed = True + # + db.session.add(todo) + db.session.commit() + # + return redirect(url_for('index')) + + +# Delete a ToDo +@app.route('/remove/', methods=["POST", "GET"]) +def remove_todo(id): + g.selectedTab = Tab.NONE + db.session.delete(Todo.query.filter_by(id=id).first()) + db.session.commit() + return redirect(url_for('index')) + +# Show AI recommendations +@app.route('/recommend/', methods=['GET']) +@app.route('/recommend//', methods=['GET']) +async def recommend(id, refresh=False): + g.selectedTab = Tab.RECOMMENDATIONS + recommendation_engine = RecommendationEngine() + g.todo = db.session.query(Todo).filter_by(id=id).first() + + if g.todo and not refresh: + try: + #attempt to load any saved recommendation from the DB + if g.todo.recommendations_json is not None: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + return render_template('index.html') + except ValueError as e: + print("Error:", e) + + previous_links_str = None + if refresh: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + # Extract links + links = [item["link"] for item in g.todo.recommendations] + # Convert list of links to a single string + previous_links_str = ", ".join(links) + + g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str) + + # Save the recommendations to the database + try: + g.todo.recommendations_json = json.dumps(g.todo.recommendations) + db.session.add(g.todo) + db.session.commit() + except Exception as e: + print(f"Error adding and committing todo: {e}") + return + + return render_template('index.html') + +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + # + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/context_processors.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/context_processors.py new file mode 100644 index 00000000..a9690944 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/context_processors.py @@ -0,0 +1,6 @@ +# context_processors.py + +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/database.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/database.py new file mode 100644 index 00000000..228b939c --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/database.py @@ -0,0 +1,50 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Boolean, JSON, func +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +class Todo(db.Model): + id = db.Column(Integer, primary_key=True) + name = db.Column(String(100), nullable=False) + notes = db.Column(String(100)) + priority = db.Column(Integer, default=0) + completed = db.Column(Boolean, default=False) + recommendations_json = db.Column(db.JSON) #json serialized version of recommendations for storing in DB + due_date = db.Column(String(50)) + + #transient variables (i.e., not stored in db) + recommendations = [] #recommendations as a collection + + def __str__(self): + return self.name + + def priority_str(self): + """ + Returns the priority as a string. + """ + str = "Not Set" + if self.priority == 1: + str = "High" + elif self.priority == 2: + str = "Medium" + elif self.priority == 3: + str = "Low" + + return str + + def completed_str(self): + """ + Returns the completed status as a string. + """ + if self.completed: + return "Yes" + else: + return "No" + + + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/priority.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/priority.py new file mode 100644 index 00000000..2afa8405 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/priority.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Priority(Enum): + NONE = 0, + HIGH = 1, + MEDIUM = 2, + LOW = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/recommendation_engine.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/recommendation_engine.py new file mode 100644 index 00000000..35bed9af --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/recommendation_engine.py @@ -0,0 +1,77 @@ +import json +import asyncio +import semantic_kernel as sk +from services import Service +from openai import AzureOpenAI +from dotenv import dotenv_values + +config = dotenv_values(".env") + +#uses the USE_AZURE_OPENAI variable from the .env file to determine which AI service to use +#False means use OpenAI, True means use Azure OpenAI +selectedService = Service.AzureOpenAI if config.get("USE_AZURE_OPENAI") == "True" else Service.OpenAI +deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + +class RecommendationEngine: + def __init__(self): + deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + + self.client = AzureOpenAI(azure_endpoint = endpoint, + api_key=api_key, + api_version="2024-02-15-preview" + ) + + + async def get_recommendations(self, keyword_phrase, previous_links_str=None): + prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE""" + system_prompt = """You are an administrative assistant bot who is good at giving + recommendations for tasks that need to be done by referencing website links that can provide + assistance to helping complete the task. + + If there are not any recommendations simply return an empty collection. + + EXPECTED OUTPUT: + Provide your response as a JSON object with the following schema: + [{"title": "...", "link": "..."}, + {"title": "...", "link": "..."}, + {"title": "...", "link": "..."}] + """ + + if previous_links_str is not None: + prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}" + + message_text = [{"role":"system","content":system_prompt}, + {"role":"user","content":prompt},] + + response = self.client.chat.completions.create( + model=deployment, + messages = message_text, + temperature=0.14, + max_tokens=800, + top_p=0.17, + frequency_penalty=0, + presence_penalty=0, + stop=None + ) + + result = response.choices[0].message.content + print(result) + + try: + recommendation = json.loads(result) + except Exception as e: + print(f"Error loading recommendations: {e}") + recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}] + + return recommendation + +async def test_recommendation_engine(): + engine = RecommendationEngine() + recommendations = await engine.get_recommendations("Buy a birthday gift for mom") + count = 1 + for recommendation in recommendations: + print(f"{count} - {recommendation['title']}: {recommendation['link']}") + count += 1 + +if __name__ == "__main__": + asyncio.run(test_recommendation_engine()) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/services.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/services.py new file mode 100644 index 00000000..657f1270 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/services.py @@ -0,0 +1,18 @@ +""" +This module defines an enumeration representing different services. +""" + +from enum import Enum + + +class Service(Enum): + """ + Attributes: + OpenAI (str): Represents the OpenAI service. + AzureOpenAI (str): Represents the Azure OpenAI service. + HuggingFace (str): Represents the HuggingFace service. + """ + + OpenAI = "openai" + AzureOpenAI = "azureopenai" + HuggingFace = "huggingface" \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/css/style.css b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/css/style.css new file mode 100644 index 00000000..9a6717e5 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/css/style.css @@ -0,0 +1,52 @@ +body { + background-color: f0f0f0; /* light grey */ + color: darkslategray; /* default font color */ + font-family: Arial, sans-serif; + background-image: url("../images/Designer02.jpeg"); + background-repeat: no-repeat; + background-position: center; + background-size:cover; +} + +h1 { + color: darkgray; /* font for the h1 header*/ +} + +.list-group-item { + color: #333; /* dark grey */ +} + +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} + +.card { + border-top: none !important; +} + +.nav-link { + background-color: white; +} + +.highlighted-item { + background-color: #f2f2f2; +} + +.limit-text { + color: white; +} + + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer01.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer01.jpeg new file mode 100644 index 00000000..f59d0d61 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer01.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer02.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer02.jpeg new file mode 100644 index 00000000..0c36a33e Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/Designer02.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/favicon.ico b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/favicon.ico new file mode 100644 index 00000000..47f38580 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/images/favicon.ico differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/js/app.js b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/js/app.js new file mode 100644 index 00000000..f92dcdaa --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/static/js/app.js @@ -0,0 +1,131 @@ +document.addEventListener("DOMContentLoaded", function() { + const HIGHLIGHTEDITEM = 'highlighted-item'; + const currentPath = window.location.pathname; + + // Set the active tab based on the URL + const setActiveTab = (tabId) => { + const tabElement = document.getElementById(tabId); + if (tabElement) { + tabElement.classList.add('active'); + } + }; + + switch (true) { + case currentPath.includes('/edit'): + setActiveTab('edit-tab'); + break; + case currentPath.includes('/completed'): + setActiveTab('completed-tab'); + break; + case currentPath.includes('/details'): + setActiveTab('details-tab'); + break; + case currentPath.includes('/recommend'): + setActiveTab('recommendations-tab'); + break; + default: + break; + } + + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + console.log('highlightedItemId', highlightedItemId); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.add(HIGHLIGHTEDITEM); + } + } + + const myModal = document.getElementById('confirmModal') + const deleteButtons = document.getElementsByClassName('delete-btn'); + Array.from(deleteButtons).forEach((deleteButton) => { + deleteButton.addEventListener('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + const url = this.getAttribute('data-url'); + document.getElementById('deleteLink').setAttribute('href', url); + const taskname_paragraph = document.querySelector("p[id='taskName']"); + const taskname = this.getAttribute('data-taskname'); + taskname_paragraph.textContent = taskname; + myModal.addEventListener('shown.bs.modal', () => { + deleteButton.focus() + }) + clearHighlight(); + }); + }); + + + window.clearHighlight = function() { + localStorage.removeItem(HIGHLIGHTEDITEM); + }; + + window.showDetails = function(li) { + console.log(li); + highlight(li); + const rootUrl = window.location.origin; + const dataId = li.getAttribute('data-id'); + window.location.href = `${rootUrl}/details/${dataId}`; + }; + + window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + clearHighlight(); + }; + + + window.highlight = function(element) { + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.remove(HIGHLIGHTEDITEM); + } + } + + const closestListItem = element.closest('li'); + closestListItem.classList.add(HIGHLIGHTEDITEM); + + localStorage.setItem(HIGHLIGHTEDITEM, closestListItem.id); + }; + + const nameInput = document.getElementById("todo"); + nameInput.addEventListener("keyup", function() { + const addButton = document.querySelector("button[id='addButton']"); + addButton.disabled = this.value.trim() === ""; + }); + + + //add javascript to support speech recognition for the todo input field + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.lang = "en-US"; + recognition.interimResults = false; + + window.captureVoice = function () { + recognition.start(); + nameInput.value = "Your microphone is activated, speak to record voice"; + }; + + recognition.onresult = function (event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + }; + + recognition.onspeechend = function () { + recognition.stop(); + }; + + recognition.onnomatch = function (event) { + nameInput.value = "I didn't recognize that prompt."; + }; + + recognition.onerror = function (event) { + nameInput.value = "Error occurred in recognition: " + event.error; + }; +}); \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/tab.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/tab.py new file mode 100644 index 00000000..982b8536 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/tab.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Tab(Enum): + NONE = 0, + DETAILS = 1, + EDIT = 2, + RECOMMENDATIONS = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/templates/index.html b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/templates/index.html new file mode 100644 index 00000000..409d93e8 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us04/templates/index.html @@ -0,0 +1,214 @@ + + + + My To-Do List + + + + + + + +
    +
    +
    +
    +

    My To-Do List

    +
    +
    +
    +
    +
    +
      + {% for todo in g.todos %} +
    1. +
      +
      + {% if todo.completed %} + + {% else %} + + {% endif %} + +
      {{ todo.name }}
      +
      + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
      +
      +
      + + + Remove + +
    2. + {% endfor %} +
    +
    +
    + + + + + + + Maximum 75 characters +
    + +
    +
    + {% if g.selectedTab != g.TabEnum.NONE %} + + {% endif %} + + {% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %} +
    +
    +
    + {% for recommend in g.todo.recommendations %} + {{ recommend.title }} + {% endfor %} +
    +
    + Don't like recommendations? + Refresh +
    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.DETAILS %} +
    +
    +

    Task: {{ g.todo.name }}

    +

    Priority: {{ g.todo.priority_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.EDIT %} +
    +
    +
    + +
    + + +
    +
    +
    +

    Priority:

    + {% if g.todo != None and g.todo.priority == 1 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 2 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 3 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + {% if g.todo.completed %} + + + {% else %} + + + {% endif %} +
    +
    + + +
    +
    +
    + {% endif %} +
    +
    + + + + + + + \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/.env-example b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/.env-example new file mode 100644 index 00000000..7d05b090 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/.env-example @@ -0,0 +1,9 @@ +DEBUG_APP="True" +USE_AZURE_OPENAI="True" +OPENAI_API_KEY="" +OPENAI_ORG_ID="" +OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo" +AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo" +AZURE_OPENAI_ENDPOINT="" +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_VERSION="2023-05-15" diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/app.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/app.py new file mode 100644 index 00000000..618ecd5c --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/app.py @@ -0,0 +1,177 @@ +############################################################################### +## Sprint 6: Advanced To-DO Details +## Feature 1: Add Additional Details to To-Do Items +## User Story 2: Add Priority and Notes Fields +############################################################################ +import os +import json +from flask import Flask, render_template, request, redirect, url_for, g +from database import db, Todo +from recommendation_engine import RecommendationEngine +from tab import Tab +from priority import Priority +from context_processors import inject_current_date + +app = Flask(__name__) +basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file +todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory +app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db.init_app(app) + +@app.context_processor +def inject_common_variables(): + return inject_current_date() + +with app.app_context(): + db.create_all() + +@app.before_request +def load_data_to_g(): + todos = Todo.query.all() + g.todos = todos + g.todo = None + g.TabEnum = Tab + g.PriorityEnum = Priority + g.selectedTab = Tab.NONE + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/add", methods=["POST"]) +def add_todo(): + + # Get the data from the form + todo = Todo( + name=request.form["todo"] + ) + + # Add the new ToDo to the list + db.session.add(todo) + db.session.commit() + + # Add the new ToDo to the list + return redirect(url_for('index')) + +# Details of ToDo Item +@app.route('/details/', methods=['GET']) +def details(id): + g.selectedTab = Tab.DETAILS + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Edit a new ToDo +@app.route('/edit/', methods=['GET']) +def edit(id): + g.selectedTab = Tab.EDIT + g.todos = Todo.query.all() + g.todo = Todo.query.filter_by(id=id).first() + + return render_template('index.html') + +# Save existing To Do Item +@app.route('/update/', methods=['POST']) +def update_todo(id): + g.selectedTab = Tab.DETAILS + + if request.form.get('cancel') != None: + return redirect(url_for('index')) + + # Get the data from the form + name = request.form['name'] + due_date = request.form.get('duedate') + notes=request.form.get('notes') + priority=request.form.get('priority') + completed=request.form.get('completed') + + todo = db.session.query(Todo).filter_by(id=id).first() + if todo != None: + todo.name = name + + if due_date != "None": + todo.due_date = due_date + + if notes != None: + todo.notes = notes + + if priority != None: + todo.priority = int(priority) + + if completed == None: + todo.completed = False + elif completed == "on": + todo.completed = True + # + db.session.add(todo) + db.session.commit() + # + return redirect(url_for('index')) + + +# Delete a ToDo +@app.route('/remove/', methods=["POST", "GET"]) +def remove_todo(id): + g.selectedTab = Tab.NONE + db.session.delete(Todo.query.filter_by(id=id).first()) + db.session.commit() + return redirect(url_for('index')) + +# Show AI recommendations +@app.route('/recommend/', methods=['GET']) +@app.route('/recommend//', methods=['GET']) +async def recommend(id, refresh=False): + g.selectedTab = Tab.RECOMMENDATIONS + recommendation_engine = RecommendationEngine() + g.todo = db.session.query(Todo).filter_by(id=id).first() + + if g.todo and not refresh: + try: + #attempt to load any saved recommendation from the DB + if g.todo.recommendations_json is not None: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + return render_template('index.html') + except ValueError as e: + print("Error:", e) + + previous_links_str = None + if refresh: + g.todo.recommendations = json.loads(g.todo.recommendations_json) + # Extract links + links = [item["link"] for item in g.todo.recommendations] + # Convert list of links to a single string + previous_links_str = ", ".join(links) + + g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str) + + # Save the recommendations to the database + try: + g.todo.recommendations_json = json.dumps(g.todo.recommendations) + db.session.add(g.todo) + db.session.commit() + except Exception as e: + print(f"Error adding and committing todo: {e}") + return + + return render_template('index.html') + +@app.route('/completed//', methods=['GET']) +def completed(id, complete): + g.selectedTab = Tab.NONE + g.todo = Todo.query.filter_by(id=id).first() + if (g.todo != None and complete == "true"): + g.todo.completed = True + elif (g.todo != None and complete == "false"): + g.todo.completed = False + # + db.session.add(g.todo) + db.session.commit() + # + return redirect(url_for('index')) + + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/context_processors.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/context_processors.py new file mode 100644 index 00000000..a9690944 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/context_processors.py @@ -0,0 +1,6 @@ +# context_processors.py + +from datetime import datetime + +def inject_current_date(): + return {'current_date': datetime.now().strftime('%Y-%m-%d')} \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/database.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/database.py new file mode 100644 index 00000000..fb8c6cba --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/database.py @@ -0,0 +1,48 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Integer, String, Boolean, JSON, func +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass + +db = SQLAlchemy(model_class=Base) + +class Todo(db.Model): + id = db.Column(Integer, primary_key=True) + name = db.Column(String(100), nullable=False) + notes = db.Column(String(100)) + priority = db.Column(Integer, default=0) + completed = db.Column(Boolean, default=False) + recommendations_json = db.Column(db.JSON) #json serialized version of recommendations for storing in DB + due_date = db.Column(String(50)) + + #transient variables (i.e., not stored in db) + recommendations = [] #recommendations as a collection + + def __str__(self): + return self.name + + def priority_str(self): + """ + Returns the priority as a string. + """ + str = "Not Set" + if self.priority == 1: + str = "High" + elif self.priority == 2: + str = "Medium" + elif self.priority == 3: + str = "Low" + + return str + + def completed_str(self): + """ + Returns the completed status as a string. + """ + if self.completed: + return "Yes" + else: + return "No" + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/priority.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/priority.py new file mode 100644 index 00000000..2afa8405 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/priority.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Priority(Enum): + NONE = 0, + HIGH = 1, + MEDIUM = 2, + LOW = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/recommendation_engine.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/recommendation_engine.py new file mode 100644 index 00000000..35bed9af --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/recommendation_engine.py @@ -0,0 +1,77 @@ +import json +import asyncio +import semantic_kernel as sk +from services import Service +from openai import AzureOpenAI +from dotenv import dotenv_values + +config = dotenv_values(".env") + +#uses the USE_AZURE_OPENAI variable from the .env file to determine which AI service to use +#False means use OpenAI, True means use Azure OpenAI +selectedService = Service.AzureOpenAI if config.get("USE_AZURE_OPENAI") == "True" else Service.OpenAI +deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + +class RecommendationEngine: + def __init__(self): + deployment, api_key, endpoint = sk.azure_openai_settings_from_dot_env() + + self.client = AzureOpenAI(azure_endpoint = endpoint, + api_key=api_key, + api_version="2024-02-15-preview" + ) + + + async def get_recommendations(self, keyword_phrase, previous_links_str=None): + prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE""" + system_prompt = """You are an administrative assistant bot who is good at giving + recommendations for tasks that need to be done by referencing website links that can provide + assistance to helping complete the task. + + If there are not any recommendations simply return an empty collection. + + EXPECTED OUTPUT: + Provide your response as a JSON object with the following schema: + [{"title": "...", "link": "..."}, + {"title": "...", "link": "..."}, + {"title": "...", "link": "..."}] + """ + + if previous_links_str is not None: + prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}" + + message_text = [{"role":"system","content":system_prompt}, + {"role":"user","content":prompt},] + + response = self.client.chat.completions.create( + model=deployment, + messages = message_text, + temperature=0.14, + max_tokens=800, + top_p=0.17, + frequency_penalty=0, + presence_penalty=0, + stop=None + ) + + result = response.choices[0].message.content + print(result) + + try: + recommendation = json.loads(result) + except Exception as e: + print(f"Error loading recommendations: {e}") + recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}] + + return recommendation + +async def test_recommendation_engine(): + engine = RecommendationEngine() + recommendations = await engine.get_recommendations("Buy a birthday gift for mom") + count = 1 + for recommendation in recommendations: + print(f"{count} - {recommendation['title']}: {recommendation['link']}") + count += 1 + +if __name__ == "__main__": + asyncio.run(test_recommendation_engine()) diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/services.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/services.py new file mode 100644 index 00000000..657f1270 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/services.py @@ -0,0 +1,18 @@ +""" +This module defines an enumeration representing different services. +""" + +from enum import Enum + + +class Service(Enum): + """ + Attributes: + OpenAI (str): Represents the OpenAI service. + AzureOpenAI (str): Represents the Azure OpenAI service. + HuggingFace (str): Represents the HuggingFace service. + """ + + OpenAI = "openai" + AzureOpenAI = "azureopenai" + HuggingFace = "huggingface" \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/css/style.css b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/css/style.css new file mode 100644 index 00000000..9a6717e5 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/css/style.css @@ -0,0 +1,52 @@ +body { + background-color: f0f0f0; /* light grey */ + color: darkslategray; /* default font color */ + font-family: Arial, sans-serif; + background-image: url("../images/Designer02.jpeg"); + background-repeat: no-repeat; + background-position: center; + background-size:cover; +} + +h1 { + color: darkgray; /* font for the h1 header*/ +} + +.list-group-item { + color: #333; /* dark grey */ +} + +.form-check input[type="checkbox"]:checked + .title { + text-decoration: line-through; +} + +.title { + display: inline-flex; + font-weight: bold; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #666; + padding-left: 20px; +} + +.card { + border-top: none !important; +} + +.nav-link { + background-color: white; +} + +.highlighted-item { + background-color: #f2f2f2; +} + +.limit-text { + color: white; +} + + + diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer01.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer01.jpeg new file mode 100644 index 00000000..f59d0d61 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer01.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer02.jpeg b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer02.jpeg new file mode 100644 index 00000000..0c36a33e Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/Designer02.jpeg differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/favicon.ico b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/favicon.ico new file mode 100644 index 00000000..47f38580 Binary files /dev/null and b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/images/favicon.ico differ diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/js/app.js b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/js/app.js new file mode 100644 index 00000000..459184a2 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/static/js/app.js @@ -0,0 +1,158 @@ +document.addEventListener("DOMContentLoaded", function() { + const HIGHLIGHTEDITEM = 'highlighted-item'; + const currentPath = window.location.pathname; + + // Set the active tab based on the URL + const setActiveTab = (tabId) => { + const tabElement = document.getElementById(tabId); + if (tabElement) { + tabElement.classList.add('active'); + } + }; + + switch (true) { + case currentPath.includes('/edit'): + setActiveTab('edit-tab'); + break; + case currentPath.includes('/completed'): + setActiveTab('completed-tab'); + break; + case currentPath.includes('/details'): + setActiveTab('details-tab'); + break; + case currentPath.includes('/recommend'): + setActiveTab('recommendations-tab'); + break; + default: + break; + } + + //override the default behavior of the nav links so that we can + //turn on a spinner control when the user clicks on the recommendations tab + const navLinks = document.getElementsByClassName('nav-link'); + Array.from(navLinks).forEach((navLink) => { + navLink.addEventListener('click', function(e) { + e.preventDefault(); + const currentPath = this.getAttribute('href'); + const rootUrl = window.location.origin; + + //turn on the spinner control if the user clicks on the recommendations tab + if (this.getAttribute('id') === 'recommendations-tab') { + var recommend_pane = document.querySelector("span[id='recommendation-spinner']"); + if (recommend_pane) { + recommend_pane.removeAttribute('hidden'); + } + } + window.location.href = rootUrl + currentPath; + }); + }); + + + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + console.log('highlightedItemId', highlightedItemId); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.add(HIGHLIGHTEDITEM); + } + } + + const myModal = document.getElementById('confirmModal') + const deleteButtons = document.getElementsByClassName('delete-btn'); + Array.from(deleteButtons).forEach((deleteButton) => { + deleteButton.addEventListener('click', function(e) { + e.stopPropagation(); + e.preventDefault(); + const url = this.getAttribute('data-url'); + document.getElementById('deleteLink').setAttribute('href', url); + const taskname_paragraph = document.querySelector("p[id='taskName']"); + const taskname = this.getAttribute('data-taskname'); + taskname_paragraph.textContent = taskname; + myModal.addEventListener('shown.bs.modal', () => { + deleteButton.focus() + }) + clearHighlight(); + }); + }); + + window.clearHighlight = function() { + localStorage.removeItem(HIGHLIGHTEDITEM); + }; + + window.handleRefresh = function() { + var recommend_pane = document.querySelector("span[id='recommendation-spinner']"); + if (recommend_pane) { + recommend_pane.removeAttribute('hidden'); + } + } + + window.showDetails = function(li) { + console.log(li); + highlight(li); + const rootUrl = window.location.origin; + const dataId = li.getAttribute('data-id'); + window.location.href = `${rootUrl}/details/${dataId}`; + }; + + window.handleClick = function(event, cb) { + event.stopPropagation(); + const rootUrl = window.location.origin; + const cbId = cb.id; + const cbChecked = cb.checked; + window.location.href = `${rootUrl}/completed/${cbId}/${cbChecked}`; + clearHighlight(); + }; + + + window.highlight = function(element) { + const highlightedItemId = localStorage.getItem(HIGHLIGHTEDITEM); + if (highlightedItemId) { + const highlightedItem = document.getElementById(highlightedItemId); + if (highlightedItem) { + highlightedItem.classList.remove(HIGHLIGHTEDITEM); + } + } + + const closestListItem = element.closest('li'); + closestListItem.classList.add(HIGHLIGHTEDITEM); + + localStorage.setItem(HIGHLIGHTEDITEM, closestListItem.id); + }; + + const nameInput = document.getElementById("todo"); + nameInput.addEventListener("keyup", function() { + const addButton = document.querySelector("button[id='addButton']"); + addButton.disabled = this.value.trim() === ""; + }); + + + //add javascript to support speech recognition for the todo input field + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = false; + recognition.lang = "en-US"; + recognition.interimResults = false; + + window.captureVoice = function () { + recognition.start(); + nameInput.value = "Your microphone is activated, speak to record voice"; + }; + + recognition.onresult = function (event) { + const transcript = event.results[0][0].transcript; + const recognizedText = transcript.endsWith('.') ? transcript.slice(0, -1) : transcript; + nameInput.value = recognizedText; + }; + + recognition.onspeechend = function () { + recognition.stop(); + }; + + recognition.onnomatch = function (event) { + nameInput.value = "I didn't recognize that prompt."; + }; + + recognition.onerror = function (event) { + nameInput.value = "Error occurred in recognition: " + event.error; + }; +}); \ No newline at end of file diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/tab.py b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/tab.py new file mode 100644 index 00000000..982b8536 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/tab.py @@ -0,0 +1,7 @@ +from enum import Enum + +class Tab(Enum): + NONE = 0, + DETAILS = 1, + EDIT = 2, + RECOMMENDATIONS = 3 diff --git a/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/templates/index.html b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/templates/index.html new file mode 100644 index 00000000..ef2b2cf0 --- /dev/null +++ b/Track_2_ToDo_App/Sprint-07 - Advanced Styling Your Web App/src/app-s07-f01-us05/templates/index.html @@ -0,0 +1,216 @@ + + + + My To-Do List + + + + + + + +
    +
    +
    +
    +

    My To-Do List

    +
    +
    +
    +
    +
    +
      + {% for todo in g.todos %} +
    1. +
      +
      + {% if todo.completed %} + + {% else %} + + {% endif %} + +
      {{ todo.name }}
      +
      + {% if todo.completed %} + Completed + {% elif todo.due_date %} + {% if todo.due_date < current_date %} + Past Due: {{ todo.due_date }} + {% else %} + Due Date: {{ todo.due_date }} + {% endif %} + {% endif %} +
      +
      +
      + + + Remove + +
    2. + {% endfor %} +
    +
    +
    + + + + + + + Maximum 75 characters +
    + +
    +
    + {% if g.selectedTab != g.TabEnum.NONE %} + + {% endif %} + + {% if g.todo != None and g.selectedTab == g.TabEnum.RECOMMENDATIONS %} +
    +
    +
    + {% for recommend in g.todo.recommendations %} + {{ recommend.title }} + {% endfor %} +
    +
    + Don't like recommendations? + Refresh +
    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.DETAILS %} +
    +
    +

    Task: {{ g.todo.name }}

    +

    Priority: {{ g.todo.priority_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +

    Due Date: {{ g.todo.due_date }}

    +

    Additional Notes: {{ g.todo.notes }}

    +

    Completed: {{ g.todo.completed_str() }}

    +
    +
    + {% endif %} + {% if g.todo != None and g.selectedTab == g.TabEnum.EDIT %} +
    +
    +
    + +
    + + +
    +
    +
    +

    Priority:

    + {% if g.todo != None and g.todo.priority == 1 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 2 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} + {%if g.todo != None and g.todo.priority == 3 %} +
    + + +
    + {% else %} +
    + + +
    + {% endif %} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + {% if g.todo.completed %} + + + {% else %} + + + {% endif %} +
    +
    + + +
    +
    +
    + {% endif %} +
    +
    + + + + + + + \ No newline at end of file diff --git a/Track_2_ToDo_App/Workshop-Format.md b/Track_2_ToDo_App/Workshop-Format.md index 5e7013ba..f6030c77 100644 --- a/Track_2_ToDo_App/Workshop-Format.md +++ b/Track_2_ToDo_App/Workshop-Format.md @@ -101,6 +101,8 @@ This sprint is designed to help students add additional details to the To-Do app ## Sprint 7 - Advanced Styling in your Web Application +⏲️ _Est. time to complete: 60 min._ ⏲️ + This sprint is designed to help students add advanced styling to the To-Do application. The sprint will walk students through adding advanced styling features such as a tabbed interface, modal dialog, and advanced rules to ensure that a blank to-do is not added. **📕Feature: Advanced Web App Styling**