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