|
import datetime |
|
from typing import Dict, List, Any, Union, Optional |
|
import gradio as gr |
|
|
|
|
|
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 |
|
|
|
|
|
from utils.config import FILE_PATHS |
|
from utils.logging import setup_logger |
|
from utils.error_handling import handle_exceptions |
|
|
|
|
|
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") |
|
|
|
with gr.Column(elem_id="notes-page"): |
|
gr.Markdown("# π Notes") |
|
|
|
|
|
with gr.Row(): |
|
|
|
view_selector = gr.Radio( |
|
choices=["All Notes", "Recent", "Favorites", "Tags"], |
|
value="All Notes", |
|
label="View", |
|
elem_id="notes-view-selector" |
|
) |
|
|
|
|
|
search_box = gr.Textbox( |
|
placeholder="Search notes...", |
|
label="Search", |
|
elem_id="notes-search" |
|
) |
|
|
|
|
|
add_note_btn = gr.Button("β Add Note", elem_classes=["action-button"]) |
|
|
|
|
|
with gr.Row(elem_id="notes-container"): |
|
|
|
with gr.Column(scale=1, elem_id="notes-sidebar"): |
|
|
|
notes_list = gr.Dataframe( |
|
headers=["Title", "Updated"], |
|
datatype=["str", "str"], |
|
col_count=(2, "fixed"), |
|
elem_id="notes-list" |
|
) |
|
|
|
|
|
tags_filter = gr.Dropdown( |
|
multiselect=True, |
|
label="Filter by Tags", |
|
elem_id="tags-filter", |
|
visible=False |
|
) |
|
|
|
|
|
with gr.Column(scale=3, elem_id="note-editor"): |
|
|
|
note_title = gr.Textbox( |
|
placeholder="Note title", |
|
label="Title", |
|
elem_id="note-title" |
|
) |
|
|
|
|
|
note_tags = gr.Textbox( |
|
placeholder="tag1, tag2, tag3", |
|
label="Tags (comma separated)", |
|
elem_id="note-tags" |
|
) |
|
|
|
|
|
note_content = gr.Textbox( |
|
placeholder="Write your note here...", |
|
label="Content", |
|
lines=15, |
|
elem_id="note-content" |
|
) |
|
|
|
|
|
note_metadata = gr.Markdown( |
|
"*No note selected*", |
|
elem_id="note-metadata" |
|
) |
|
|
|
|
|
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"]) |
|
|
|
|
|
with gr.Accordion("π€ AI Features", open=False): |
|
with gr.Row(): |
|
|
|
analyze_sentiment_btn = gr.Button("Analyze Sentiment") |
|
|
|
summarize_btn = gr.Button("Summarize Note") |
|
|
|
suggest_btn = gr.Button("Get Suggestions") |
|
|
|
|
|
ai_output = gr.Markdown( |
|
"*AI features will appear here*", |
|
elem_id="ai-output" |
|
) |
|
|
|
|
|
@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 = [] |
|
|
|
|
|
if view == "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: |
|
|
|
filtered_notes = [note for note in notes if |
|
any(tag in safe_get(note, "tags", []) for tag in tags)] |
|
else: |
|
filtered_notes = notes |
|
|
|
|
|
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()] |
|
|
|
|
|
table_data = [] |
|
for note in filtered_notes: |
|
|
|
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')}") |
|
|
|
|
|
title = safe_get(note, "title", "Untitled Note") |
|
if note.get("favorite", False): |
|
title = f"β {title}" |
|
|
|
table_data.append([title, updated]) |
|
|
|
|
|
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")) |
|
|
|
|
|
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 = gr.State(None) |
|
|
|
|
|
@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 |
|
|
|
|
|
notes = safe_get(state, "notes", []) |
|
notes.sort(key=lambda x: x.get("updated_at", ""), reverse=True) |
|
note = notes[evt.index[0]] |
|
|
|
|
|
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}*" |
|
|
|
|
|
tags_str = ", ".join(safe_get(note, "tags", [])) |
|
|
|
return safe_get(note, "title", ""), tags_str, safe_get(note, "content", ""), metadata, note["id"] |
|
|
|
|
|
notes_list.select( |
|
load_note, |
|
inputs=[notes_list, current_note_id], |
|
outputs=[note_title, note_tags, note_content, note_metadata, current_note_id] |
|
) |
|
|
|
|
|
@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 |
|
|
|
|
|
tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()] |
|
|
|
|
|
timestamp = get_timestamp() |
|
|
|
if note_id: |
|
|
|
for note in safe_get(state, "notes", []): |
|
if note["id"] == note_id: |
|
|
|
note["title"] = title.strip() |
|
note["content"] = content |
|
note["tags"] = tags |
|
note["updated_at"] = timestamp |
|
|
|
|
|
record_activity(state, { |
|
"type": "note_updated", |
|
"title": title, |
|
"timestamp": timestamp |
|
}) |
|
break |
|
else: |
|
|
|
new_note = { |
|
"id": generate_id(), |
|
"title": title.strip(), |
|
"content": content, |
|
"tags": tags, |
|
"favorite": False, |
|
"created_at": timestamp, |
|
"updated_at": timestamp |
|
} |
|
|
|
|
|
if "notes" not in state: |
|
state["notes"] = [] |
|
state["notes"].append(new_note) |
|
|
|
|
|
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(state, { |
|
"type": "note_created", |
|
"title": title, |
|
"timestamp": timestamp |
|
}) |
|
|
|
|
|
note_id = new_note["id"] |
|
|
|
|
|
save_data(FILE_PATHS["notes"], safe_get(state, "notes", [])) |
|
|
|
|
|
update_notes_list(view_selector.value, search_box.value, tags_filter.value) |
|
|
|
return "Note saved successfully!", note_id |
|
|
|
|
|
save_note_btn.click( |
|
save_note, |
|
inputs=[note_title, note_tags, note_content, current_note_id], |
|
outputs=[gr.Markdown(visible=False), current_note_id] |
|
) |
|
|
|
|
|
@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" |
|
|
|
|
|
for note in safe_get(state, "notes", []): |
|
if note["id"] == note_id: |
|
|
|
note["favorite"] = not note.get("favorite", False) |
|
|
|
|
|
save_data(FILE_PATHS["notes"], safe_get(state, "notes", [])) |
|
|
|
|
|
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" |
|
|
|
|
|
favorite_btn.click( |
|
toggle_favorite, |
|
inputs=[current_note_id], |
|
outputs=[gr.Markdown(visible=False)] |
|
) |
|
|
|
|
|
@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*" |
|
|
|
|
|
for i, note in enumerate(safe_get(state, "notes", [])): |
|
if note["id"] == note_id: |
|
|
|
record_activity(state, { |
|
"type": "note_deleted", |
|
"title": safe_get(note, "title", "Untitled Note"), |
|
"timestamp": get_timestamp() |
|
}) |
|
|
|
|
|
state["notes"].pop(i) |
|
|
|
|
|
state["stats"]["notes_total"] -= 1 |
|
|
|
|
|
save_data(FILE_PATHS["notes"], safe_get(state, "notes", [])) |
|
|
|
|
|
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 |
|
|
|
|
|
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] |
|
) |
|
|
|
|
|
@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" |
|
|
|
|
|
for note in safe_get(state, "notes", []): |
|
if note["id"] == note_id: |
|
|
|
filename = f"note_{note_id}.md" |
|
content = f"# {safe_get(note, 'title', 'Untitled Note')}\n\n" |
|
|
|
|
|
if note.get("tags"): |
|
content += "Tags: " + ", ".join(note["tags"]) + "\n\n" |
|
|
|
|
|
content += safe_get(note, "content", "") |
|
|
|
|
|
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_to_markdown(filename, content) |
|
|
|
|
|
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" |
|
|
|
|
|
export_btn.click( |
|
export_note, |
|
inputs=[current_note_id], |
|
outputs=[gr.Markdown(visible=False)] |
|
) |
|
|
|
|
|
@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) |
|
|
|
|
|
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: |
|
return "**Sentiment Analysis:** π Neutral - Your note has a balanced or objective tone." |
|
|
|
|
|
analyze_sentiment_btn.click( |
|
analyze_note_sentiment, |
|
inputs=[note_content], |
|
outputs=[ai_output] |
|
) |
|
|
|
|
|
@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}" |
|
|
|
|
|
summarize_btn.click( |
|
summarize_note_content, |
|
inputs=[note_content], |
|
outputs=[ai_output] |
|
) |
|
|
|
|
|
@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" |
|
|
|
|
|
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}" |
|
|
|
|
|
suggest_btn.click( |
|
get_note_suggestions, |
|
inputs=[note_title, note_content], |
|
outputs=[ai_output] |
|
) |
|
|
|
|
|
@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 |
|
|
|
|
|
add_note_btn.click( |
|
show_add_note, |
|
inputs=[], |
|
outputs=[note_title, note_tags, note_content, note_metadata, current_note_id] |
|
) |
|
|
|
|
|
notes_list.value, tags_options, _ = update_notes_list("All Notes") |
|
tags_filter.choices = tags_options |