mona / pages /notes.py
mrradix's picture
Upload 48 files
8e4018d verified
import datetime
from typing import Dict, List, Any, Union, Optional
import gradio as gr
# Import utilities
from utils.storage import load_data, save_data, export_to_markdown, safe_get
from utils.state import generate_id, get_timestamp, record_activity
from utils.ai_models import analyze_sentiment, summarize_text, generate_text
from utils.ui_components import create_stat_card, create_activity_item
# Import new modules
from utils.config import FILE_PATHS
from utils.logging import setup_logger
from utils.error_handling import handle_exceptions
# Initialize logger
logger = setup_logger(__name__)
@handle_exceptions
def create_notes_page(state: Dict[str, Any]) -> None:
"""
Create the notes page with note-taking and organization features
Args:
state: Application state
"""
logger.info("Creating notes page")
# Create the notes page layout
with gr.Column(elem_id="notes-page"):
gr.Markdown("# πŸ“ Notes")
# Notes views and actions
with gr.Row():
# View selector
view_selector = gr.Radio(
choices=["All Notes", "Recent", "Favorites", "Tags"],
value="All Notes",
label="View",
elem_id="notes-view-selector"
)
# Search box
search_box = gr.Textbox(
placeholder="Search notes...",
label="Search",
elem_id="notes-search"
)
# Add note button
add_note_btn = gr.Button("βž• Add Note", elem_classes=["action-button"])
# Notes container
with gr.Row(elem_id="notes-container"):
# Notes list sidebar
with gr.Column(scale=1, elem_id="notes-sidebar"):
# Notes list
notes_list = gr.Dataframe(
headers=["Title", "Updated"],
datatype=["str", "str"],
col_count=(2, "fixed"),
elem_id="notes-list"
)
# Tags filter (only visible in Tags view)
tags_filter = gr.Dropdown(
multiselect=True,
label="Filter by Tags",
elem_id="tags-filter",
visible=False
)
# Note editor
with gr.Column(scale=3, elem_id="note-editor"):
# Note title
note_title = gr.Textbox(
placeholder="Note title",
label="Title",
elem_id="note-title"
)
# Note tags
note_tags = gr.Textbox(
placeholder="tag1, tag2, tag3",
label="Tags (comma separated)",
elem_id="note-tags"
)
# Note content
note_content = gr.Textbox(
placeholder="Write your note here...",
label="Content",
lines=15,
elem_id="note-content"
)
# Note metadata
note_metadata = gr.Markdown(
"*No note selected*",
elem_id="note-metadata"
)
# Note actions
with gr.Row():
favorite_btn = gr.Button("⭐ Favorite", elem_classes=["action-button"])
export_btn = gr.Button("πŸ“€ Export", elem_classes=["action-button"])
delete_btn = gr.Button("πŸ—‘οΈ Delete", elem_classes=["action-button"])
save_note_btn = gr.Button("πŸ’Ύ Save Note", elem_classes=["primary-button"])
# AI features
with gr.Accordion("πŸ€– AI Features", open=False):
with gr.Row():
# AI Sentiment Analysis
analyze_sentiment_btn = gr.Button("Analyze Sentiment")
# AI Summarization
summarize_btn = gr.Button("Summarize Note")
# AI Suggestions
suggest_btn = gr.Button("Get Suggestions")
# AI Output
ai_output = gr.Markdown(
"*AI features will appear here*",
elem_id="ai-output"
)
# Function to update notes list
@handle_exceptions
def update_notes_list(view, search_query="", tags=None):
"""Update the notes list based on view, search, and tags"""
logger.debug(f"Updating notes list with view: {view}, search: {search_query}, tags: {tags}")
notes = safe_get(state, "notes", [])
filtered_notes = []
# Apply view filter
if view == "Recent":
# Sort by last updated and take the 10 most recent
notes.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
filtered_notes = notes[:10]
elif view == "Favorites":
filtered_notes = [note for note in notes if note.get("favorite", False)]
elif view == "Tags" and tags:
# Filter by selected tags
filtered_notes = [note for note in notes if
any(tag in safe_get(note, "tags", []) for tag in tags)]
else: # All Notes
filtered_notes = notes
# Apply search filter if provided
if search_query:
search_query = search_query.lower()
filtered_notes = [note for note in filtered_notes if
search_query in safe_get(note, "title", "").lower() or
search_query in safe_get(note, "content", "").lower()]
# Format data for the table
table_data = []
for note in filtered_notes:
# Format updated date
updated = "Unknown"
if "updated_at" in note:
try:
updated_at = datetime.datetime.fromisoformat(note["updated_at"])
updated = updated_at.strftime("%b %d, %Y")
except:
logger.warning(f"Failed to parse updated_at date for note: {note.get('id', 'unknown')}")
# Add favorite indicator to title if favorited
title = safe_get(note, "title", "Untitled Note")
if note.get("favorite", False):
title = f"⭐ {title}"
table_data.append([title, updated])
# Update tags dropdown if in Tags view
all_tags = set()
for note in notes:
all_tags.update(safe_get(note, "tags", []))
return table_data, list(all_tags), gr.update(visible=(view == "Tags"))
# Set up view switching and search
view_selector.change(
update_notes_list,
inputs=[view_selector, search_box, tags_filter],
outputs=[notes_list, tags_filter, tags_filter]
)
search_box.change(
update_notes_list,
inputs=[view_selector, search_box, tags_filter],
outputs=[notes_list, tags_filter, tags_filter]
)
tags_filter.change(
update_notes_list,
inputs=[view_selector, search_box, tags_filter],
outputs=[notes_list, tags_filter, tags_filter]
)
# Current note ID (hidden state)
current_note_id = gr.State(None)
# Function to load a note
@handle_exceptions
def load_note(evt: gr.SelectData, notes_table, current_id):
"""Load a note when selected from the list"""
logger.debug(f"Loading note at index {evt.index[0]}")
if evt.index[0] >= len(safe_get(state, "notes", [])):
logger.warning("Note index out of range")
return None, "", "", "", "*No note selected*", current_id
# Get the selected note
notes = safe_get(state, "notes", [])
notes.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
note = notes[evt.index[0]]
# Format metadata
created_at = "Unknown"
updated_at = "Unknown"
if "created_at" in note:
try:
created_dt = datetime.datetime.fromisoformat(note["created_at"])
created_at = created_dt.strftime("%b %d, %Y at %H:%M")
except:
logger.warning(f"Failed to parse created_at date for note: {note.get('id', 'unknown')}")
if "updated_at" in note:
try:
updated_dt = datetime.datetime.fromisoformat(note["updated_at"])
updated_at = updated_dt.strftime("%b %d, %Y at %H:%M")
except:
logger.warning(f"Failed to parse updated_at date for note: {note.get('id', 'unknown')}")
metadata = f"*Created: {created_at} | Last updated: {updated_at}*"
# Format tags
tags_str = ", ".join(safe_get(note, "tags", []))
return safe_get(note, "title", ""), tags_str, safe_get(note, "content", ""), metadata, note["id"]
# Set up note selection
notes_list.select(
load_note,
inputs=[notes_list, current_note_id],
outputs=[note_title, note_tags, note_content, note_metadata, current_note_id]
)
# Function to save a note
@handle_exceptions
def save_note(title, tags_str, content, note_id):
"""Save a note (create new or update existing)"""
logger.debug(f"Saving note with ID: {note_id if note_id else 'new'}")
if not title.strip():
logger.warning("Attempted to save note without title")
return "Please enter a note title", note_id
# Parse tags
tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
# Get current timestamp
timestamp = get_timestamp()
if note_id: # Update existing note
# Find the note
for note in safe_get(state, "notes", []):
if note["id"] == note_id:
# Update note
note["title"] = title.strip()
note["content"] = content
note["tags"] = tags
note["updated_at"] = timestamp
# Record activity
record_activity(state, {
"type": "note_updated",
"title": title,
"timestamp": timestamp
})
break
else: # Create new note
# Create new note
new_note = {
"id": generate_id(),
"title": title.strip(),
"content": content,
"tags": tags,
"favorite": False,
"created_at": timestamp,
"updated_at": timestamp
}
# Add to state
if "notes" not in state:
state["notes"] = []
state["notes"].append(new_note)
# Update stats
if "stats" not in state:
state["stats"] = {}
if "notes_total" not in state["stats"]:
state["stats"]["notes_total"] = 0
state["stats"]["notes_total"] += 1
# Record activity
record_activity(state, {
"type": "note_created",
"title": title,
"timestamp": timestamp
})
# Set as current note
note_id = new_note["id"]
# Save to file
save_data(FILE_PATHS["notes"], safe_get(state, "notes", []))
# Update notes list
update_notes_list(view_selector.value, search_box.value, tags_filter.value)
return "Note saved successfully!", note_id
# Set up save note action
save_note_btn.click(
save_note,
inputs=[note_title, note_tags, note_content, current_note_id],
outputs=[gr.Markdown(visible=False), current_note_id]
)
# Function to toggle favorite
@handle_exceptions
def toggle_favorite(note_id):
"""Toggle favorite status of a note"""
logger.debug(f"Toggling favorite status for note: {note_id}")
if not note_id:
logger.warning("Attempted to toggle favorite without a selected note")
return "No note selected"
# Find the note
for note in safe_get(state, "notes", []):
if note["id"] == note_id:
# Toggle favorite
note["favorite"] = not note.get("favorite", False)
# Save to file
save_data(FILE_PATHS["notes"], safe_get(state, "notes", []))
# Update notes list
update_notes_list(view_selector.value, search_box.value, tags_filter.value)
return f"Note {'added to' if note['favorite'] else 'removed from'} favorites"
logger.warning(f"Note not found with ID: {note_id}")
return "Note not found"
# Set up favorite button
favorite_btn.click(
toggle_favorite,
inputs=[current_note_id],
outputs=[gr.Markdown(visible=False)]
)
# Function to delete a note
@handle_exceptions
def delete_note(note_id):
"""Delete a note"""
logger.debug(f"Deleting note: {note_id}")
if not note_id:
logger.warning("Attempted to delete without a selected note")
return "No note selected", note_id, "", "", "", "*No note selected*"
# Find the note
for i, note in enumerate(safe_get(state, "notes", [])):
if note["id"] == note_id:
# Record activity
record_activity(state, {
"type": "note_deleted",
"title": safe_get(note, "title", "Untitled Note"),
"timestamp": get_timestamp()
})
# Remove note
state["notes"].pop(i)
# Update stats
state["stats"]["notes_total"] -= 1
# Save to file
save_data(FILE_PATHS["notes"], safe_get(state, "notes", []))
# Update notes list
update_notes_list(view_selector.value, search_box.value, tags_filter.value)
return "Note deleted", None, "", "", "", "*No note selected*"
logger.warning(f"Note not found with ID: {note_id}")
return "Note not found", note_id, note_title.value, note_tags.value, note_content.value, note_metadata.value
# Set up delete button
delete_btn.click(
delete_note,
inputs=[current_note_id],
outputs=[gr.Markdown(visible=False), current_note_id, note_title, note_tags, note_content, note_metadata]
)
# Function to export a note
@handle_exceptions
def export_note(note_id):
"""Export a note to Markdown"""
logger.debug(f"Exporting note: {note_id}")
if not note_id:
logger.warning("Attempted to export without a selected note")
return "No note selected"
# Find the note
for note in safe_get(state, "notes", []):
if note["id"] == note_id:
# Export to Markdown
filename = f"note_{note_id}.md"
content = f"# {safe_get(note, 'title', 'Untitled Note')}\n\n"
# Add tags if present
if note.get("tags"):
content += "Tags: " + ", ".join(note["tags"]) + "\n\n"
# Add content
content += safe_get(note, "content", "")
# Add metadata
content += "\n\n---\n"
if "created_at" in note:
try:
created_dt = datetime.datetime.fromisoformat(note["created_at"])
content += f"Created: {created_dt.strftime('%Y-%m-%d %H:%M')}\n"
except:
logger.warning(f"Failed to parse created_at date for note: {note_id}")
if "updated_at" in note:
try:
updated_dt = datetime.datetime.fromisoformat(note["updated_at"])
content += f"Last updated: {updated_dt.strftime('%Y-%m-%d %H:%M')}"
except:
logger.warning(f"Failed to parse updated_at date for note: {note_id}")
# Export
export_to_markdown(filename, content)
# Record activity
record_activity(state, {
"type": "note_exported",
"title": safe_get(note, "title", "Untitled Note"),
"timestamp": get_timestamp()
})
return f"Note exported as {filename}"
logger.warning(f"Note not found with ID: {note_id}")
return "Note not found"
# Set up export button
export_btn.click(
export_note,
inputs=[current_note_id],
outputs=[gr.Markdown(visible=False)]
)
# Function to analyze sentiment
@handle_exceptions
def analyze_note_sentiment(content):
"""Analyze the sentiment of note content"""
logger.debug("Analyzing note sentiment")
if not content.strip():
logger.warning("Attempted to analyze sentiment with empty content")
return "Please enter some content to analyze"
sentiment = analyze_sentiment(content)
# Format sentiment result
if sentiment == "positive":
return "**Sentiment Analysis:** 😊 Positive - Your note has an optimistic and upbeat tone."
elif sentiment == "negative":
return "**Sentiment Analysis:** πŸ˜” Negative - Your note has a pessimistic or critical tone."
else: # neutral
return "**Sentiment Analysis:** 😐 Neutral - Your note has a balanced or objective tone."
# Set up sentiment analysis button
analyze_sentiment_btn.click(
analyze_note_sentiment,
inputs=[note_content],
outputs=[ai_output]
)
# Function to summarize note
@handle_exceptions
def summarize_note_content(content):
"""Summarize the content of a note"""
logger.debug("Summarizing note content")
if not content.strip():
logger.warning("Attempted to summarize empty content")
return "Please enter some content to summarize"
if len(content.split()) < 30:
logger.info("Note too short to summarize")
return "Note is too short to summarize. Add more content."
summary = summarize_text(content)
return f"**Summary:**\n\n{summary}"
# Set up summarize button
summarize_btn.click(
summarize_note_content,
inputs=[note_content],
outputs=[ai_output]
)
# Function to get suggestions
@handle_exceptions
def get_note_suggestions(title, content):
"""Get AI suggestions for the note"""
logger.debug("Getting note suggestions")
if not content.strip():
logger.warning("Attempted to get suggestions with empty content")
return "Please enter some content to get suggestions"
# Generate suggestions based on note content
prompt = f"Based on this note titled '{title}', suggest some improvements or related ideas:\n\n{content[:500]}"
suggestions = generate_text(prompt)
return f"**Suggestions:**\n\n{suggestions}"
# Set up suggestions button
suggest_btn.click(
get_note_suggestions,
inputs=[note_title, note_content],
outputs=[ai_output]
)
# Function to show add note modal
@handle_exceptions
def show_add_note():
"""Clear the editor and prepare for a new note"""
logger.debug("Showing add note form")
return "", "", "", "*New note*", None
# Set up add note button
add_note_btn.click(
show_add_note,
inputs=[],
outputs=[note_title, note_tags, note_content, note_metadata, current_note_id]
)
# Initialize notes list
notes_list.value, tags_options, _ = update_notes_list("All Notes")
tags_filter.choices = tags_options