This is page 33 of 35. Use http://codebase.md/doobidoo/mcp-memory-service?page={x} to view the full context.
# Directory Structure
```
├── .claude
│ ├── agents
│ │ ├── amp-bridge.md
│ │ ├── amp-pr-automator.md
│ │ ├── code-quality-guard.md
│ │ ├── gemini-pr-automator.md
│ │ └── github-release-manager.md
│ ├── settings.local.json.backup
│ └── settings.local.json.local
├── .commit-message
├── .dockerignore
├── .env.example
├── .env.sqlite.backup
├── .envnn#
├── .gitattributes
├── .github
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── performance_issue.yml
│ ├── pull_request_template.md
│ └── workflows
│ ├── bridge-tests.yml
│ ├── CACHE_FIX.md
│ ├── claude-code-review.yml
│ ├── claude.yml
│ ├── cleanup-images.yml.disabled
│ ├── dev-setup-validation.yml
│ ├── docker-publish.yml
│ ├── LATEST_FIXES.md
│ ├── main-optimized.yml.disabled
│ ├── main.yml
│ ├── publish-and-test.yml
│ ├── README_OPTIMIZATION.md
│ ├── release-tag.yml.disabled
│ ├── release.yml
│ ├── roadmap-review-reminder.yml
│ ├── SECRET_CONDITIONAL_FIX.md
│ └── WORKFLOW_FIXES.md
├── .gitignore
├── .mcp.json.backup
├── .mcp.json.template
├── .pyscn
│ ├── .gitignore
│ └── reports
│ └── analyze_20251123_214224.html
├── AGENTS.md
├── archive
│ ├── deployment
│ │ ├── deploy_fastmcp_fixed.sh
│ │ ├── deploy_http_with_mcp.sh
│ │ └── deploy_mcp_v4.sh
│ ├── deployment-configs
│ │ ├── empty_config.yml
│ │ └── smithery.yaml
│ ├── development
│ │ └── test_fastmcp.py
│ ├── docs-removed-2025-08-23
│ │ ├── authentication.md
│ │ ├── claude_integration.md
│ │ ├── claude-code-compatibility.md
│ │ ├── claude-code-integration.md
│ │ ├── claude-code-quickstart.md
│ │ ├── claude-desktop-setup.md
│ │ ├── complete-setup-guide.md
│ │ ├── database-synchronization.md
│ │ ├── development
│ │ │ ├── autonomous-memory-consolidation.md
│ │ │ ├── CLEANUP_PLAN.md
│ │ │ ├── CLEANUP_README.md
│ │ │ ├── CLEANUP_SUMMARY.md
│ │ │ ├── dream-inspired-memory-consolidation.md
│ │ │ ├── hybrid-slm-memory-consolidation.md
│ │ │ ├── mcp-milestone.md
│ │ │ ├── multi-client-architecture.md
│ │ │ ├── test-results.md
│ │ │ └── TIMESTAMP_FIX_SUMMARY.md
│ │ ├── distributed-sync.md
│ │ ├── invocation_guide.md
│ │ ├── macos-intel.md
│ │ ├── master-guide.md
│ │ ├── mcp-client-configuration.md
│ │ ├── multi-client-server.md
│ │ ├── service-installation.md
│ │ ├── sessions
│ │ │ └── MCP_ENHANCEMENT_SESSION_MEMORY_v4.1.0.md
│ │ ├── UBUNTU_SETUP.md
│ │ ├── ubuntu.md
│ │ ├── windows-setup.md
│ │ └── windows.md
│ ├── docs-root-cleanup-2025-08-23
│ │ ├── AWESOME_LIST_SUBMISSION.md
│ │ ├── CLOUDFLARE_IMPLEMENTATION.md
│ │ ├── DOCUMENTATION_ANALYSIS.md
│ │ ├── DOCUMENTATION_CLEANUP_PLAN.md
│ │ ├── DOCUMENTATION_CONSOLIDATION_COMPLETE.md
│ │ ├── LITESTREAM_SETUP_GUIDE.md
│ │ ├── lm_studio_system_prompt.md
│ │ ├── PYTORCH_DOWNLOAD_FIX.md
│ │ └── README-ORIGINAL-BACKUP.md
│ ├── investigations
│ │ └── MACOS_HOOKS_INVESTIGATION.md
│ ├── litestream-configs-v6.3.0
│ │ ├── install_service.sh
│ │ ├── litestream_master_config_fixed.yml
│ │ ├── litestream_master_config.yml
│ │ ├── litestream_replica_config_fixed.yml
│ │ ├── litestream_replica_config.yml
│ │ ├── litestream_replica_simple.yml
│ │ ├── litestream-http.service
│ │ ├── litestream.service
│ │ └── requirements-cloudflare.txt
│ ├── release-notes
│ │ └── release-notes-v7.1.4.md
│ └── setup-development
│ ├── README.md
│ ├── setup_consolidation_mdns.sh
│ ├── STARTUP_SETUP_GUIDE.md
│ └── test_service.sh
├── CHANGELOG-HISTORIC.md
├── CHANGELOG.md
├── claude_commands
│ ├── memory-context.md
│ ├── memory-health.md
│ ├── memory-ingest-dir.md
│ ├── memory-ingest.md
│ ├── memory-recall.md
│ ├── memory-search.md
│ ├── memory-store.md
│ ├── README.md
│ └── session-start.md
├── claude-hooks
│ ├── config.json
│ ├── config.template.json
│ ├── CONFIGURATION.md
│ ├── core
│ │ ├── memory-retrieval.js
│ │ ├── mid-conversation.js
│ │ ├── session-end.js
│ │ ├── session-start.js
│ │ └── topic-change.js
│ ├── debug-pattern-test.js
│ ├── install_claude_hooks_windows.ps1
│ ├── install_hooks.py
│ ├── memory-mode-controller.js
│ ├── MIGRATION.md
│ ├── README-NATURAL-TRIGGERS.md
│ ├── README-phase2.md
│ ├── README.md
│ ├── simple-test.js
│ ├── statusline.sh
│ ├── test-adaptive-weights.js
│ ├── test-dual-protocol-hook.js
│ ├── test-mcp-hook.js
│ ├── test-natural-triggers.js
│ ├── test-recency-scoring.js
│ ├── tests
│ │ ├── integration-test.js
│ │ ├── phase2-integration-test.js
│ │ ├── test-code-execution.js
│ │ ├── test-cross-session.json
│ │ ├── test-session-tracking.json
│ │ └── test-threading.json
│ ├── utilities
│ │ ├── adaptive-pattern-detector.js
│ │ ├── context-formatter.js
│ │ ├── context-shift-detector.js
│ │ ├── conversation-analyzer.js
│ │ ├── dynamic-context-updater.js
│ │ ├── git-analyzer.js
│ │ ├── mcp-client.js
│ │ ├── memory-client.js
│ │ ├── memory-scorer.js
│ │ ├── performance-manager.js
│ │ ├── project-detector.js
│ │ ├── session-tracker.js
│ │ ├── tiered-conversation-monitor.js
│ │ └── version-checker.js
│ └── WINDOWS-SESSIONSTART-BUG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Development-Sprint-November-2025.md
├── docs
│ ├── amp-cli-bridge.md
│ ├── api
│ │ ├── code-execution-interface.md
│ │ ├── memory-metadata-api.md
│ │ ├── PHASE1_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_IMPLEMENTATION_SUMMARY.md
│ │ ├── PHASE2_REPORT.md
│ │ └── tag-standardization.md
│ ├── architecture
│ │ ├── search-enhancement-spec.md
│ │ └── search-examples.md
│ ├── architecture.md
│ ├── archive
│ │ └── obsolete-workflows
│ │ ├── load_memory_context.md
│ │ └── README.md
│ ├── assets
│ │ └── images
│ │ ├── dashboard-v3.3.0-preview.png
│ │ ├── memory-awareness-hooks-example.png
│ │ ├── project-infographic.svg
│ │ └── README.md
│ ├── CLAUDE_CODE_QUICK_REFERENCE.md
│ ├── cloudflare-setup.md
│ ├── deployment
│ │ ├── docker.md
│ │ ├── dual-service.md
│ │ ├── production-guide.md
│ │ └── systemd-service.md
│ ├── development
│ │ ├── ai-agent-instructions.md
│ │ ├── code-quality
│ │ │ ├── phase-2a-completion.md
│ │ │ ├── phase-2a-handle-get-prompt.md
│ │ │ ├── phase-2a-index.md
│ │ │ ├── phase-2a-install-package.md
│ │ │ └── phase-2b-session-summary.md
│ │ ├── code-quality-workflow.md
│ │ ├── dashboard-workflow.md
│ │ ├── issue-management.md
│ │ ├── pr-review-guide.md
│ │ ├── refactoring-notes.md
│ │ ├── release-checklist.md
│ │ └── todo-tracker.md
│ ├── docker-optimized-build.md
│ ├── document-ingestion.md
│ ├── DOCUMENTATION_AUDIT.md
│ ├── enhancement-roadmap-issue-14.md
│ ├── examples
│ │ ├── analysis-scripts.js
│ │ ├── maintenance-session-example.md
│ │ ├── memory-distribution-chart.jsx
│ │ └── tag-schema.json
│ ├── first-time-setup.md
│ ├── glama-deployment.md
│ ├── guides
│ │ ├── advanced-command-examples.md
│ │ ├── chromadb-migration.md
│ │ ├── commands-vs-mcp-server.md
│ │ ├── mcp-enhancements.md
│ │ ├── mdns-service-discovery.md
│ │ ├── memory-consolidation-guide.md
│ │ ├── migration.md
│ │ ├── scripts.md
│ │ └── STORAGE_BACKENDS.md
│ ├── HOOK_IMPROVEMENTS.md
│ ├── hooks
│ │ └── phase2-code-execution-migration.md
│ ├── http-server-management.md
│ ├── ide-compatability.md
│ ├── IMAGE_RETENTION_POLICY.md
│ ├── images
│ │ └── dashboard-placeholder.md
│ ├── implementation
│ │ ├── health_checks.md
│ │ └── performance.md
│ ├── IMPLEMENTATION_PLAN_HTTP_SSE.md
│ ├── integration
│ │ ├── homebrew.md
│ │ └── multi-client.md
│ ├── integrations
│ │ ├── gemini.md
│ │ ├── groq-bridge.md
│ │ ├── groq-integration-summary.md
│ │ └── groq-model-comparison.md
│ ├── integrations.md
│ ├── legacy
│ │ └── dual-protocol-hooks.md
│ ├── LM_STUDIO_COMPATIBILITY.md
│ ├── maintenance
│ │ └── memory-maintenance.md
│ ├── mastery
│ │ ├── api-reference.md
│ │ ├── architecture-overview.md
│ │ ├── configuration-guide.md
│ │ ├── local-setup-and-run.md
│ │ ├── testing-guide.md
│ │ └── troubleshooting.md
│ ├── migration
│ │ └── code-execution-api-quick-start.md
│ ├── natural-memory-triggers
│ │ ├── cli-reference.md
│ │ ├── installation-guide.md
│ │ └── performance-optimization.md
│ ├── oauth-setup.md
│ ├── pr-graphql-integration.md
│ ├── quick-setup-cloudflare-dual-environment.md
│ ├── README.md
│ ├── remote-configuration-wiki-section.md
│ ├── research
│ │ ├── code-execution-interface-implementation.md
│ │ └── code-execution-interface-summary.md
│ ├── ROADMAP.md
│ ├── sqlite-vec-backend.md
│ ├── statistics
│ │ ├── charts
│ │ │ ├── activity_patterns.png
│ │ │ ├── contributors.png
│ │ │ ├── growth_trajectory.png
│ │ │ ├── monthly_activity.png
│ │ │ └── october_sprint.png
│ │ ├── data
│ │ │ ├── activity_by_day.csv
│ │ │ ├── activity_by_hour.csv
│ │ │ ├── contributors.csv
│ │ │ └── monthly_activity.csv
│ │ ├── generate_charts.py
│ │ └── REPOSITORY_STATISTICS.md
│ ├── technical
│ │ ├── development.md
│ │ ├── memory-migration.md
│ │ ├── migration-log.md
│ │ ├── sqlite-vec-embedding-fixes.md
│ │ └── tag-storage.md
│ ├── testing
│ │ └── regression-tests.md
│ ├── testing-cloudflare-backend.md
│ ├── troubleshooting
│ │ ├── cloudflare-api-token-setup.md
│ │ ├── cloudflare-authentication.md
│ │ ├── general.md
│ │ ├── hooks-quick-reference.md
│ │ ├── pr162-schema-caching-issue.md
│ │ ├── session-end-hooks.md
│ │ └── sync-issues.md
│ └── tutorials
│ ├── advanced-techniques.md
│ ├── data-analysis.md
│ └── demo-session-walkthrough.md
├── examples
│ ├── claude_desktop_config_template.json
│ ├── claude_desktop_config_windows.json
│ ├── claude-desktop-http-config.json
│ ├── config
│ │ └── claude_desktop_config.json
│ ├── http-mcp-bridge.js
│ ├── memory_export_template.json
│ ├── README.md
│ ├── setup
│ │ └── setup_multi_client_complete.py
│ └── start_https_example.sh
├── install_service.py
├── install.py
├── LICENSE
├── NOTICE
├── pyproject.toml
├── pytest.ini
├── README.md
├── run_server.py
├── scripts
│ ├── .claude
│ │ └── settings.local.json
│ ├── archive
│ │ └── check_missing_timestamps.py
│ ├── backup
│ │ ├── backup_memories.py
│ │ ├── backup_sqlite_vec.sh
│ │ ├── export_distributable_memories.sh
│ │ └── restore_memories.py
│ ├── benchmarks
│ │ ├── benchmark_code_execution_api.py
│ │ ├── benchmark_hybrid_sync.py
│ │ └── benchmark_server_caching.py
│ ├── database
│ │ ├── analyze_sqlite_vec_db.py
│ │ ├── check_sqlite_vec_status.py
│ │ ├── db_health_check.py
│ │ └── simple_timestamp_check.py
│ ├── development
│ │ ├── debug_server_initialization.py
│ │ ├── find_orphaned_files.py
│ │ ├── fix_mdns.sh
│ │ ├── fix_sitecustomize.py
│ │ ├── remote_ingest.sh
│ │ ├── setup-git-merge-drivers.sh
│ │ ├── uv-lock-merge.sh
│ │ └── verify_hybrid_sync.py
│ ├── hooks
│ │ └── pre-commit
│ ├── installation
│ │ ├── install_linux_service.py
│ │ ├── install_macos_service.py
│ │ ├── install_uv.py
│ │ ├── install_windows_service.py
│ │ ├── install.py
│ │ ├── setup_backup_cron.sh
│ │ ├── setup_claude_mcp.sh
│ │ └── setup_cloudflare_resources.py
│ ├── linux
│ │ ├── service_status.sh
│ │ ├── start_service.sh
│ │ ├── stop_service.sh
│ │ ├── uninstall_service.sh
│ │ └── view_logs.sh
│ ├── maintenance
│ │ ├── assign_memory_types.py
│ │ ├── check_memory_types.py
│ │ ├── cleanup_corrupted_encoding.py
│ │ ├── cleanup_memories.py
│ │ ├── cleanup_organize.py
│ │ ├── consolidate_memory_types.py
│ │ ├── consolidation_mappings.json
│ │ ├── delete_orphaned_vectors_fixed.py
│ │ ├── fast_cleanup_duplicates_with_tracking.sh
│ │ ├── find_all_duplicates.py
│ │ ├── find_cloudflare_duplicates.py
│ │ ├── find_duplicates.py
│ │ ├── memory-types.md
│ │ ├── README.md
│ │ ├── recover_timestamps_from_cloudflare.py
│ │ ├── regenerate_embeddings.py
│ │ ├── repair_malformed_tags.py
│ │ ├── repair_memories.py
│ │ ├── repair_sqlite_vec_embeddings.py
│ │ ├── repair_zero_embeddings.py
│ │ ├── restore_from_json_export.py
│ │ └── scan_todos.sh
│ ├── migration
│ │ ├── cleanup_mcp_timestamps.py
│ │ ├── legacy
│ │ │ └── migrate_chroma_to_sqlite.py
│ │ ├── mcp-migration.py
│ │ ├── migrate_sqlite_vec_embeddings.py
│ │ ├── migrate_storage.py
│ │ ├── migrate_tags.py
│ │ ├── migrate_timestamps.py
│ │ ├── migrate_to_cloudflare.py
│ │ ├── migrate_to_sqlite_vec.py
│ │ ├── migrate_v5_enhanced.py
│ │ ├── TIMESTAMP_CLEANUP_README.md
│ │ └── verify_mcp_timestamps.py
│ ├── pr
│ │ ├── amp_collect_results.sh
│ │ ├── amp_detect_breaking_changes.sh
│ │ ├── amp_generate_tests.sh
│ │ ├── amp_pr_review.sh
│ │ ├── amp_quality_gate.sh
│ │ ├── amp_suggest_fixes.sh
│ │ ├── auto_review.sh
│ │ ├── detect_breaking_changes.sh
│ │ ├── generate_tests.sh
│ │ ├── lib
│ │ │ └── graphql_helpers.sh
│ │ ├── quality_gate.sh
│ │ ├── resolve_threads.sh
│ │ ├── run_pyscn_analysis.sh
│ │ ├── run_quality_checks.sh
│ │ ├── thread_status.sh
│ │ └── watch_reviews.sh
│ ├── quality
│ │ ├── fix_dead_code_install.sh
│ │ ├── phase1_dead_code_analysis.md
│ │ ├── phase2_complexity_analysis.md
│ │ ├── README_PHASE1.md
│ │ ├── README_PHASE2.md
│ │ ├── track_pyscn_metrics.sh
│ │ └── weekly_quality_review.sh
│ ├── README.md
│ ├── run
│ │ ├── run_mcp_memory.sh
│ │ ├── run-with-uv.sh
│ │ └── start_sqlite_vec.sh
│ ├── run_memory_server.py
│ ├── server
│ │ ├── check_http_server.py
│ │ ├── check_server_health.py
│ │ ├── memory_offline.py
│ │ ├── preload_models.py
│ │ ├── run_http_server.py
│ │ ├── run_memory_server.py
│ │ ├── start_http_server.bat
│ │ └── start_http_server.sh
│ ├── service
│ │ ├── deploy_dual_services.sh
│ │ ├── install_http_service.sh
│ │ ├── mcp-memory-http.service
│ │ ├── mcp-memory.service
│ │ ├── memory_service_manager.sh
│ │ ├── service_control.sh
│ │ ├── service_utils.py
│ │ └── update_service.sh
│ ├── sync
│ │ ├── check_drift.py
│ │ ├── claude_sync_commands.py
│ │ ├── export_memories.py
│ │ ├── import_memories.py
│ │ ├── litestream
│ │ │ ├── apply_local_changes.sh
│ │ │ ├── enhanced_memory_store.sh
│ │ │ ├── init_staging_db.sh
│ │ │ ├── io.litestream.replication.plist
│ │ │ ├── manual_sync.sh
│ │ │ ├── memory_sync.sh
│ │ │ ├── pull_remote_changes.sh
│ │ │ ├── push_to_remote.sh
│ │ │ ├── README.md
│ │ │ ├── resolve_conflicts.sh
│ │ │ ├── setup_local_litestream.sh
│ │ │ ├── setup_remote_litestream.sh
│ │ │ ├── staging_db_init.sql
│ │ │ ├── stash_local_changes.sh
│ │ │ ├── sync_from_remote_noconfig.sh
│ │ │ └── sync_from_remote.sh
│ │ ├── README.md
│ │ ├── safe_cloudflare_update.sh
│ │ ├── sync_memory_backends.py
│ │ └── sync_now.py
│ ├── testing
│ │ ├── run_complete_test.py
│ │ ├── run_memory_test.sh
│ │ ├── simple_test.py
│ │ ├── test_cleanup_logic.py
│ │ ├── test_cloudflare_backend.py
│ │ ├── test_docker_functionality.py
│ │ ├── test_installation.py
│ │ ├── test_mdns.py
│ │ ├── test_memory_api.py
│ │ ├── test_memory_simple.py
│ │ ├── test_migration.py
│ │ ├── test_search_api.py
│ │ ├── test_sqlite_vec_embeddings.py
│ │ ├── test_sse_events.py
│ │ ├── test-connection.py
│ │ └── test-hook.js
│ ├── utils
│ │ ├── claude_commands_utils.py
│ │ ├── generate_personalized_claude_md.sh
│ │ ├── groq
│ │ ├── groq_agent_bridge.py
│ │ ├── list-collections.py
│ │ ├── memory_wrapper_uv.py
│ │ ├── query_memories.py
│ │ ├── smithery_wrapper.py
│ │ ├── test_groq_bridge.sh
│ │ └── uv_wrapper.py
│ └── validation
│ ├── check_dev_setup.py
│ ├── check_documentation_links.py
│ ├── diagnose_backend_config.py
│ ├── validate_configuration_complete.py
│ ├── validate_memories.py
│ ├── validate_migration.py
│ ├── validate_timestamp_integrity.py
│ ├── verify_environment.py
│ ├── verify_pytorch_windows.py
│ └── verify_torch.py
├── SECURITY.md
├── selective_timestamp_recovery.py
├── SPONSORS.md
├── src
│ └── mcp_memory_service
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── client.py
│ │ ├── operations.py
│ │ ├── sync_wrapper.py
│ │ └── types.py
│ ├── backup
│ │ ├── __init__.py
│ │ └── scheduler.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── ingestion.py
│ │ ├── main.py
│ │ └── utils.py
│ ├── config.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── associations.py
│ │ ├── base.py
│ │ ├── clustering.py
│ │ ├── compression.py
│ │ ├── consolidator.py
│ │ ├── decay.py
│ │ ├── forgetting.py
│ │ ├── health.py
│ │ └── scheduler.py
│ ├── dependency_check.py
│ ├── discovery
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── mdns_service.py
│ ├── embeddings
│ │ ├── __init__.py
│ │ └── onnx_embeddings.py
│ ├── ingestion
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── chunker.py
│ │ ├── csv_loader.py
│ │ ├── json_loader.py
│ │ ├── pdf_loader.py
│ │ ├── registry.py
│ │ ├── semtools_loader.py
│ │ └── text_loader.py
│ ├── lm_studio_compat.py
│ ├── mcp_server.py
│ ├── models
│ │ ├── __init__.py
│ │ └── memory.py
│ ├── server.py
│ ├── services
│ │ ├── __init__.py
│ │ └── memory_service.py
│ ├── storage
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── cloudflare.py
│ │ ├── factory.py
│ │ ├── http_client.py
│ │ ├── hybrid.py
│ │ └── sqlite_vec.py
│ ├── sync
│ │ ├── __init__.py
│ │ ├── exporter.py
│ │ ├── importer.py
│ │ └── litestream_config.py
│ ├── utils
│ │ ├── __init__.py
│ │ ├── cache_manager.py
│ │ ├── content_splitter.py
│ │ ├── db_utils.py
│ │ ├── debug.py
│ │ ├── document_processing.py
│ │ ├── gpu_detection.py
│ │ ├── hashing.py
│ │ ├── http_server_manager.py
│ │ ├── port_detection.py
│ │ ├── system_detection.py
│ │ └── time_parser.py
│ └── web
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── analytics.py
│ │ ├── backup.py
│ │ ├── consolidation.py
│ │ ├── documents.py
│ │ ├── events.py
│ │ ├── health.py
│ │ ├── manage.py
│ │ ├── mcp.py
│ │ ├── memories.py
│ │ ├── search.py
│ │ └── sync.py
│ ├── app.py
│ ├── dependencies.py
│ ├── oauth
│ │ ├── __init__.py
│ │ ├── authorization.py
│ │ ├── discovery.py
│ │ ├── middleware.py
│ │ ├── models.py
│ │ ├── registration.py
│ │ └── storage.py
│ ├── sse.py
│ └── static
│ ├── app.js
│ ├── index.html
│ ├── README.md
│ ├── sse_test.html
│ └── style.css
├── start_http_debug.bat
├── start_http_server.sh
├── test_document.txt
├── test_version_checker.js
├── tests
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── test_compact_types.py
│ │ └── test_operations.py
│ ├── bridge
│ │ ├── mock_responses.js
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── test_http_mcp_bridge.js
│ ├── conftest.py
│ ├── consolidation
│ │ ├── __init__.py
│ │ ├── conftest.py
│ │ ├── test_associations.py
│ │ ├── test_clustering.py
│ │ ├── test_compression.py
│ │ ├── test_consolidator.py
│ │ ├── test_decay.py
│ │ └── test_forgetting.py
│ ├── contracts
│ │ └── api-specification.yml
│ ├── integration
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ ├── test_api_key_fallback.py
│ │ ├── test_api_memories_chronological.py
│ │ ├── test_api_tag_time_search.py
│ │ ├── test_api_with_memory_service.py
│ │ ├── test_bridge_integration.js
│ │ ├── test_cli_interfaces.py
│ │ ├── test_cloudflare_connection.py
│ │ ├── test_concurrent_clients.py
│ │ ├── test_data_serialization_consistency.py
│ │ ├── test_http_server_startup.py
│ │ ├── test_mcp_memory.py
│ │ ├── test_mdns_integration.py
│ │ ├── test_oauth_basic_auth.py
│ │ ├── test_oauth_flow.py
│ │ ├── test_server_handlers.py
│ │ └── test_store_memory.py
│ ├── performance
│ │ ├── test_background_sync.py
│ │ └── test_hybrid_live.py
│ ├── README.md
│ ├── smithery
│ │ └── test_smithery.py
│ ├── sqlite
│ │ └── simple_sqlite_vec_test.py
│ ├── test_client.py
│ ├── test_content_splitting.py
│ ├── test_database.py
│ ├── test_hybrid_cloudflare_limits.py
│ ├── test_hybrid_storage.py
│ ├── test_memory_ops.py
│ ├── test_semantic_search.py
│ ├── test_sqlite_vec_storage.py
│ ├── test_time_parser.py
│ ├── test_timestamp_preservation.py
│ ├── timestamp
│ │ ├── test_hook_vs_manual_storage.py
│ │ ├── test_issue99_final_validation.py
│ │ ├── test_search_retrieval_inconsistency.py
│ │ ├── test_timestamp_issue.py
│ │ └── test_timestamp_simple.py
│ └── unit
│ ├── conftest.py
│ ├── test_cloudflare_storage.py
│ ├── test_csv_loader.py
│ ├── test_fastapi_dependencies.py
│ ├── test_import.py
│ ├── test_json_loader.py
│ ├── test_mdns_simple.py
│ ├── test_mdns.py
│ ├── test_memory_service.py
│ ├── test_memory.py
│ ├── test_semtools_loader.py
│ ├── test_storage_interface_compatibility.py
│ └── test_tag_time_filtering.py
├── tools
│ ├── docker
│ │ ├── DEPRECATED.md
│ │ ├── docker-compose.http.yml
│ │ ├── docker-compose.pythonpath.yml
│ │ ├── docker-compose.standalone.yml
│ │ ├── docker-compose.uv.yml
│ │ ├── docker-compose.yml
│ │ ├── docker-entrypoint-persistent.sh
│ │ ├── docker-entrypoint-unified.sh
│ │ ├── docker-entrypoint.sh
│ │ ├── Dockerfile
│ │ ├── Dockerfile.glama
│ │ ├── Dockerfile.slim
│ │ ├── README.md
│ │ └── test-docker-modes.sh
│ └── README.md
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/src/mcp_memory_service/web/static/app.js:
--------------------------------------------------------------------------------
```javascript
/**
* MCP Memory Service Dashboard - Main Application
* Interactive frontend for memory management with real-time updates
*/
console.log('⚡ app.js loading - TOP OF FILE');
class MemoryDashboard {
// Delay between individual file uploads to avoid overwhelming the server (ms)
static INDIVIDUAL_UPLOAD_DELAY = 500;
// Static configuration for settings modal system information
static SYSTEM_INFO_CONFIG = {
settingsVersion: {
sources: [{ path: 'version', api: 'health' }],
formatter: (value) => value || 'N/A'
},
settingsBackend: {
sources: [
{ path: 'storage.storage_backend', api: 'detailedHealth' },
{ path: 'storage.backend', api: 'detailedHealth' }
],
formatter: (value) => value || 'N/A'
},
settingsPrimaryBackend: {
sources: [
{ path: 'storage.primary_backend', api: 'detailedHealth' },
{ path: 'storage.backend', api: 'detailedHealth' }
],
formatter: (value) => value || 'N/A'
},
settingsEmbeddingModel: {
sources: [
{ path: 'storage.primary_stats.embedding_model', api: 'detailedHealth' },
{ path: 'storage.embedding_model', api: 'detailedHealth' }
],
formatter: (value) => value || 'N/A'
},
settingsEmbeddingDim: {
sources: [
{ path: 'storage.primary_stats.embedding_dimension', api: 'detailedHealth' },
{ path: 'storage.embedding_dimension', api: 'detailedHealth' }
],
formatter: (value) => value || 'N/A'
},
settingsDbSize: {
sources: [
{ path: 'storage.primary_stats.database_size_mb', api: 'detailedHealth' },
{ path: 'storage.database_size_mb', api: 'detailedHealth' }
],
formatter: (value) => (value != null) ? `${value.toFixed(2)} MB` : 'N/A'
},
settingsTotalMemories: {
sources: [{ path: 'storage.total_memories', api: 'detailedHealth' }],
formatter: (value) => (value != null) ? value.toLocaleString() : 'N/A'
},
settingsUptime: {
sources: [{ path: 'uptime_seconds', api: 'detailedHealth' }],
formatter: (value) => (value != null) ? MemoryDashboard.formatUptime(value) : 'N/A'
}
};
constructor() {
this.apiBase = '/api';
this.eventSource = null;
this.memories = [];
this.currentView = 'dashboard';
this.searchResults = [];
this.isLoading = false;
this.liveSearchEnabled = true;
this.debounceTimer = null;
// Settings with defaults
this.settings = {
theme: 'light',
viewDensity: 'comfortable',
previewLines: 3
};
// Documents upload state
this.selectedFiles = [];
this.documentsListenersSetup = false;
this.processingMode = 'batch'; // 'batch' or 'individual'
// Bind methods
this.handleSearch = this.handleSearch.bind(this);
this.handleQuickSearch = this.handleQuickSearch.bind(this);
this.handleNavigation = this.handleNavigation.bind(this);
this.handleAddMemory = this.handleAddMemory.bind(this);
this.handleMemoryClick = this.handleMemoryClick.bind(this);
this.init();
}
/**
* Initialize the application
*/
async init() {
this.loadSettings();
this.applyTheme();
this.setupEventListeners();
this.setupSSE();
await this.loadVersion();
await this.loadDashboardData();
this.updateConnectionStatus('connected');
// Initialize sync status monitoring for hybrid mode
await this.checkSyncStatus();
this.startSyncStatusMonitoring();
}
/**
* Set up event listeners for UI interactions
*/
setupEventListeners() {
console.log('⚡ setupEventListeners() called');
// Navigation
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', this.handleNavigation);
});
// Search functionality
const quickSearch = document.getElementById('quickSearch');
const searchBtn = document.querySelector('.search-btn');
if (quickSearch) {
quickSearch.addEventListener('input', this.debounce(this.handleQuickSearch, 300));
quickSearch.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleSearch(e.target.value);
}
});
}
if (searchBtn && quickSearch) {
searchBtn.addEventListener('click', () => {
this.handleSearch(quickSearch.value);
});
}
// Add memory functionality
const addMemoryBtn = document.getElementById('addMemoryBtn');
if (addMemoryBtn) {
addMemoryBtn.addEventListener('click', this.handleAddMemory);
}
document.querySelectorAll('[data-action="add-memory"]').forEach(btn => {
btn.addEventListener('click', this.handleAddMemory);
});
// Modal close handlers
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', (e) => {
this.closeModal(e.target.closest('.modal-overlay'));
});
});
// Modal overlay click to close
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.closeModal(overlay);
}
});
});
// Add memory form submission
const saveMemoryBtn = document.getElementById('saveMemoryBtn');
if (saveMemoryBtn) {
saveMemoryBtn.addEventListener('click', this.handleSaveMemory.bind(this));
}
const cancelAddBtn = document.getElementById('cancelAddBtn');
if (cancelAddBtn) {
cancelAddBtn.addEventListener('click', () => {
this.closeModal(document.getElementById('addMemoryModal'));
});
}
// Quick action handlers
document.querySelectorAll('.action-card').forEach(card => {
card.addEventListener('click', (e) => {
const action = e.currentTarget.dataset.action;
this.handleQuickAction(action);
});
});
// Live search toggle handler
const liveSearchToggle = document.getElementById('liveSearchToggle');
liveSearchToggle?.addEventListener('change', this.handleLiveSearchToggle.bind(this));
// Filter handlers for search view
const tagFilterInput = document.getElementById('tagFilter');
tagFilterInput?.addEventListener('input', this.handleDebouncedFilterChange.bind(this));
tagFilterInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleFilterChange();
}
});
document.getElementById('dateFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
document.getElementById('typeFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
// View option handlers
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.handleViewModeChange(e.target.dataset.view);
});
});
// New filter action handlers
document.getElementById('applyFiltersBtn')?.addEventListener('click', this.handleFilterChange.bind(this));
document.getElementById('clearFiltersBtn')?.addEventListener('click', this.clearAllFilters.bind(this));
// Theme toggle button
document.getElementById('themeToggleBtn')?.addEventListener('click', () => {
this.toggleTheme();
});
// Settings button
document.getElementById('settingsBtn')?.addEventListener('click', () => {
this.openSettingsModal();
});
// Settings modal handlers
document.getElementById('saveSettingsBtn')?.addEventListener('click', () => {
this.saveSettings();
});
document.getElementById('cancelSettingsBtn')?.addEventListener('click', () => {
this.closeModal(document.getElementById('settingsModal'));
});
// Tag cloud event delegation
document.getElementById('tagsCloudContainer')?.addEventListener('click', (e) => {
if (e.target.classList.contains('tag-bubble') || e.target.closest('.tag-bubble')) {
const tagButton = e.target.classList.contains('tag-bubble') ? e.target : e.target.closest('.tag-bubble');
const tag = tagButton.dataset.tag;
if (tag) {
this.filterByTag(tag);
}
}
});
// Manage tab event listeners
document.getElementById('deleteByTagBtn')?.addEventListener('click', this.handleBulkDeleteByTag.bind(this));
document.getElementById('cleanupDuplicatesBtn')?.addEventListener('click', this.handleCleanupDuplicates.bind(this));
document.getElementById('deleteByDateBtn')?.addEventListener('click', this.handleBulkDeleteByDate.bind(this));
document.getElementById('optimizeDbBtn')?.addEventListener('click', this.handleOptimizeDatabase.bind(this));
document.getElementById('rebuildIndexBtn')?.addEventListener('click', this.handleRebuildIndex.bind(this));
// Analytics tab event listeners
document.getElementById('growthPeriodSelect')?.addEventListener('change', this.handleGrowthPeriodChange.bind(this));
document.getElementById('heatmapPeriodSelect')?.addEventListener('change', this.handleHeatmapPeriodChange.bind(this));
document.getElementById('topTagsPeriodSelect')?.addEventListener('change', this.handleTopTagsPeriodChange.bind(this));
document.getElementById('activityGranularitySelect')?.addEventListener('change', this.handleActivityGranularityChange.bind(this));
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
document.getElementById('searchInput').focus();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
e.preventDefault();
this.handleAddMemory();
}
});
}
/**
* Set up Server-Sent Events for real-time updates
*/
setupSSE() {
try {
this.eventSource = new EventSource(`${this.apiBase}/events`);
this.eventSource.onopen = () => {
this.updateConnectionStatus('connected');
};
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleRealtimeUpdate(data);
} catch (error) {
console.error('Error parsing SSE data:', error);
}
};
// Add specific event listeners for sync progress
this.eventSource.addEventListener('sync_progress', (event) => {
try {
const data = JSON.parse(event.data);
this.handleSyncProgress(data);
} catch (error) {
console.error('Error parsing sync_progress event:', error);
}
});
this.eventSource.addEventListener('sync_completed', (event) => {
try {
const data = JSON.parse(event.data);
this.handleSyncCompleted(data);
} catch (error) {
console.error('Error parsing sync_completed event:', error);
}
});
this.eventSource.onerror = (error) => {
console.error('SSE connection error:', error);
this.updateConnectionStatus('disconnected');
// Attempt to reconnect after 5 seconds
setTimeout(() => {
if (this.eventSource.readyState === EventSource.CLOSED) {
this.setupSSE();
}
}, 5000);
};
} catch (error) {
console.error('Failed to establish SSE connection:', error);
this.updateConnectionStatus('disconnected');
}
}
/**
* Handle real-time updates from SSE
*/
handleRealtimeUpdate(data) {
switch (data.type) {
case 'memory_added':
this.handleMemoryAdded(data.memory);
this.showToast('Memory added successfully', 'success');
break;
case 'memory_deleted':
this.handleMemoryDeleted(data.memory_id);
this.showToast('Memory deleted', 'success');
break;
case 'memory_updated':
this.handleMemoryUpdated(data.memory);
this.showToast('Memory updated', 'success');
break;
case 'stats_updated':
this.updateDashboardStats(data.stats);
break;
default:
// Unknown event type - ignore silently
}
}
/**
* Handle sync progress updates from SSE
*/
handleSyncProgress(data) {
console.log('Sync progress:', data);
// Update sync status display if visible
const syncStatus = document.getElementById('syncStatus');
if (syncStatus) {
const progressText = `Syncing: ${data.synced_count}/${data.total_count} (${data.progress_percentage}%)`;
syncStatus.textContent = progressText;
syncStatus.className = 'sync-status syncing';
}
// Update memory count in real-time if on dashboard
if (this.currentView === 'dashboard') {
const memoryCountElement = document.getElementById('totalMemories');
if (memoryCountElement && data.synced_count) {
// Refresh the detailed health to get accurate count
this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
}
}
// Show toast notification for manual sync
if (data.sync_type === 'manual') {
this.showToast(data.message || `Syncing: ${data.synced_count}/${data.total_count}`, 'info');
}
}
/**
* Handle sync completion from SSE
*/
handleSyncCompleted(data) {
console.log('Sync completed:', data);
// Update sync status display
const syncStatus = document.getElementById('syncStatus');
if (syncStatus) {
syncStatus.textContent = 'Synced';
syncStatus.className = 'sync-status synced';
}
// Refresh dashboard data to show updated counts
if (this.currentView === 'dashboard') {
this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
}
// Also refresh sync status for hybrid mode
this.checkSyncStatus().catch(err => console.error('Error checking sync status:', err));
// Show completion notification
const message = data.message || `Sync completed: ${data.synced_count} memories synced`;
this.showToast(message, 'success');
}
/**
* Load application version from health endpoint
*/
async loadVersion() {
try {
const healthResponse = await this.apiCall('/health');
const versionBadge = document.getElementById('versionBadge');
if (versionBadge && healthResponse.version) {
versionBadge.textContent = `v${healthResponse.version}`;
}
} catch (error) {
console.error('Error loading version:', error);
const versionBadge = document.getElementById('versionBadge');
if (versionBadge) {
versionBadge.textContent = 'v?.?.?';
}
}
}
/**
* Load initial dashboard data
*/
async loadDashboardData() {
this.setLoading(true);
try {
// Load recent memories for dashboard display
const memoriesResponse = await this.apiCall('/memories?page=1&page_size=100');
if (memoriesResponse.memories) {
this.memories = memoriesResponse.memories;
this.renderRecentMemories(memoriesResponse.memories);
}
// Load basic statistics
const statsResponse = await this.apiCall('/health/detailed');
if (statsResponse.storage) {
this.updateDashboardStats(statsResponse.storage);
}
} catch (error) {
console.error('Error loading dashboard data:', error);
this.showToast('Failed to load dashboard data', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Load browse view data (tags)
*/
async loadBrowseData() {
this.setLoading(true);
try {
// Load tags with counts from the dedicated endpoint
const tagsResponse = await this.apiCall('/tags');
if (tagsResponse.tags) {
this.tags = tagsResponse.tags;
this.renderTagsCloud();
}
} catch (error) {
console.error('Error loading browse data:', error);
this.showToast('Failed to load browse data', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Load documents view data
*/
async loadDocumentsData() {
this.setLoading(true);
try {
// Load upload history
await this.loadUploadHistory();
// Setup document upload event listeners
this.setupDocumentsEventListeners();
} catch (error) {
console.error('Error loading documents data:', error);
this.showToast('Failed to load documents data', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Load upload history from API
*/
async loadUploadHistory() {
console.log('Loading upload history...');
try {
const historyResponse = await this.apiCall('/documents/history');
console.log('Upload history response:', historyResponse);
if (historyResponse.uploads) {
this.renderUploadHistory(historyResponse.uploads);
} else {
console.warn('No uploads property in response');
this.renderUploadHistory([]);
}
} catch (error) {
console.error('Error loading upload history:', error);
// Show a message in the history container instead of just logging
const historyContainer = document.getElementById('uploadHistory');
if (historyContainer) {
historyContainer.innerHTML = '<p style="text-align: center; color: var(--error);">Failed to load upload history. Please check the console for details.</p>';
}
}
}
/**
* Render upload history
*/
renderUploadHistory(uploads) {
const historyContainer = document.getElementById('uploadHistory');
if (!historyContainer) return;
if (uploads.length === 0) {
historyContainer.innerHTML = '<p style="text-align: center; color: var(--neutral-500);">No uploads yet. Start by uploading some documents!</p>';
return;
}
const historyHtml = uploads.map(upload => {
const statusClass = upload.status.toLowerCase();
const statusText = upload.status.charAt(0).toUpperCase() + upload.status.slice(1);
const progressPercent = upload.progress || 0;
const hasMemories = upload.chunks_stored > 0;
return `
<div class="upload-item ${statusClass}" data-upload-id="${upload.upload_id}" data-filename="${this.escapeHtml(upload.filename)}">
<div class="upload-info">
<div class="upload-filename">${this.escapeHtml(upload.filename)}</div>
<div class="upload-meta">
${upload.chunks_stored || 0} chunks stored •
${(upload.file_size / 1024).toFixed(1)} KB •
${new Date(upload.created_at).toLocaleString()}
</div>
${upload.status === 'processing' ? `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progressPercent}%"></div>
</div>
` : ''}
</div>
<div class="upload-actions-container">
<div class="upload-status ${statusClass}">
<span>${statusText}</span>
${upload.errors && upload.errors.length > 0 ? `
<span title="${this.escapeHtml(upload.errors.join('; '))}">⚠️</span>
` : ''}
</div>
${upload.status === 'completed' && hasMemories ? `
<div class="upload-actions">
<button class="btn-icon btn-view-memory"
title="View memory chunks">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>
<span>View</span>
</button>
<button class="btn-icon btn-remove"
title="Remove document and memories">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
<span>Remove</span>
</button>
</div>
` : ''}
</div>
</div>
`;
}).join('');
historyContainer.innerHTML = historyHtml;
}
/**
* Setup event listeners for documents view
*/
setupDocumentsEventListeners() {
// Prevent duplicate event listener setup
if (this.documentsListenersSetup) {
console.log('Document listeners already set up, skipping...');
return;
}
// File selection buttons
const fileSelectBtn = document.getElementById('fileSelectBtn');
const fileInput = document.getElementById('fileInput');
if (fileSelectBtn && fileInput) {
fileSelectBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
this.handleFileSelection(e.target.files);
});
}
// Drag and drop
const dropZone = document.getElementById('dropZone');
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
this.handleFileSelection(files);
});
}
// Configuration controls - cache for performance
this.chunkSizeInput = document.getElementById('chunkSize');
this.chunkOverlapInput = document.getElementById('chunkOverlap');
this.memoryTypeInput = document.getElementById('memoryType');
const chunkSizeValue = document.getElementById('chunkSizeValue');
const chunkOverlapValue = document.getElementById('chunkOverlapValue');
if (this.chunkSizeInput && chunkSizeValue) {
this.chunkSizeInput.addEventListener('input', (e) => {
chunkSizeValue.textContent = e.target.value;
this.updateUploadButton();
});
}
// Chunking help info icon
const infoIcon = document.querySelector('.info-icon');
if (infoIcon) {
infoIcon.addEventListener('click', () => {
this.toggleChunkingHelp();
});
}
// Overlap help info icon
const overlapInfoIcon = document.querySelector('.info-icon-overlap');
if (overlapInfoIcon) {
overlapInfoIcon.addEventListener('click', () => {
this.toggleOverlapHelp();
});
}
// Processing mode help info icon
const processingModeInfoIcon = document.querySelector('.info-icon-processing');
if (processingModeInfoIcon) {
processingModeInfoIcon.addEventListener('click', () => {
this.toggleProcessingModeHelp();
});
}
if (this.chunkOverlapInput && chunkOverlapValue) {
this.chunkOverlapInput.addEventListener('input', (e) => {
chunkOverlapValue.textContent = e.target.value;
this.updateUploadButton();
});
}
// Processing mode toggle buttons - cache for performance
this.batchModeBtn = document.getElementById('batchModeBtn');
this.individualModeBtn = document.getElementById('individualModeBtn');
this.modeDescription = document.getElementById('modeDescription');
if (this.batchModeBtn) {
this.batchModeBtn.addEventListener('click', () => {
this.setProcessingMode('batch');
});
}
if (this.individualModeBtn) {
this.individualModeBtn.addEventListener('click', () => {
this.setProcessingMode('individual');
});
}
// Upload button
const uploadBtn = document.getElementById('uploadBtn');
if (uploadBtn) {
uploadBtn.addEventListener('click', () => {
this.handleDocumentUpload();
});
}
// Add event listeners for buttons with data-action attribute
document.querySelectorAll('[data-action]').forEach(button => {
button.addEventListener('click', (e) => {
const action = e.currentTarget.dataset.action;
if (this[action] && typeof this[action] === 'function') {
this[action]();
}
});
});
// Sync buttons event listeners are attached in checkSyncStatus()
// after buttons are confirmed to be accessible in the DOM
// Backup now button
const backupNowButton = document.getElementById('backupNowButton');
if (backupNowButton) {
backupNowButton.addEventListener('click', () => {
this.createBackup();
});
}
// Document search button
const docSearchBtn = document.getElementById('docSearchBtn');
const docSearchInput = document.getElementById('docSearchInput');
if (docSearchBtn && docSearchInput) {
docSearchBtn.addEventListener('click', () => {
const query = docSearchInput.value.trim();
if (query) {
this.searchDocumentContent(query);
} else {
this.showToast('Please enter a search query', 'warning');
}
});
// Enter key to search
docSearchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const query = docSearchInput.value.trim();
if (query) {
this.searchDocumentContent(query);
}
}
});
}
// Upload history action buttons (event delegation)
const uploadHistory = document.getElementById('uploadHistory');
if (uploadHistory) {
uploadHistory.addEventListener('click', (e) => {
const button = e.target.closest('.btn-view-memory, .btn-remove');
if (!button) return;
const uploadItem = button.closest('.upload-item');
const uploadId = uploadItem?.dataset.uploadId;
const filename = uploadItem?.dataset.filename;
if (!uploadId) return;
if (button.classList.contains('btn-view-memory')) {
this.viewDocumentMemory(uploadId);
} else if (button.classList.contains('btn-remove')) {
this.removeDocument(uploadId, filename);
}
});
}
// Close modal when clicking outside
const memoryViewerModal = document.getElementById('memoryViewerModal');
if (memoryViewerModal) {
memoryViewerModal.addEventListener('click', (e) => {
if (e.target === memoryViewerModal) {
this.closeMemoryViewer();
}
});
}
// Mark listeners as set up to prevent duplicates
this.documentsListenersSetup = true;
console.log('Document listeners setup complete');
}
/**
* Handle file selection from input or drag-drop
*/
handleFileSelection(files) {
if (!files || files.length === 0) return;
this.selectedFiles = Array.from(files);
this.updateUploadButton();
// Show file preview in drop zone
const dropZone = document.getElementById('dropZone');
if (dropZone) {
const fileNames = this.selectedFiles.map(f => this.escapeHtml(f.name)).join(', ');
const content = dropZone.querySelector('.drop-zone-content');
if (content) {
content.innerHTML = `
<svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
</svg>
<h3>${files.length} file${files.length > 1 ? 's' : ''} selected</h3>
<p>${fileNames}</p>
<input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
`;
}
}
}
/**
* Update upload button state based on selections
*/
updateUploadButton() {
const uploadBtn = document.getElementById('uploadBtn');
const hasFiles = this.selectedFiles && this.selectedFiles.length > 0;
if (uploadBtn) {
uploadBtn.disabled = !hasFiles;
uploadBtn.textContent = hasFiles ?
`Upload & Ingest ${this.selectedFiles.length} file${this.selectedFiles.length > 1 ? 's' : ''}` :
'Upload & Ingest';
}
// Show/hide processing mode section based on file count
const processingModeSection = document.getElementById('processingModeSection');
if (processingModeSection) {
processingModeSection.style.display = (hasFiles && this.selectedFiles.length > 1) ? 'block' : 'none';
}
}
/**
* Set processing mode (batch or individual)
*/
setProcessingMode(mode) {
this.processingMode = mode;
// Update button states (using cached DOM elements)
if (this.batchModeBtn) {
this.batchModeBtn.classList.toggle('active', mode === 'batch');
}
if (this.individualModeBtn) {
this.individualModeBtn.classList.toggle('active', mode === 'individual');
}
if (this.modeDescription) {
this.modeDescription.innerHTML = mode === 'batch'
? '<small>All selected files will be processed together with the same tags.</small>'
: '<small>Each file will be processed individually with the same tags.</small>';
}
console.log(`Processing mode set to: ${mode}`);
}
/**
* Handle document upload
*/
async handleDocumentUpload() {
if (!this.selectedFiles || this.selectedFiles.length === 0) {
this.showToast('No files selected', 'error');
return;
}
const tags = document.getElementById('docTags')?.value || '';
const chunkSize = this.chunkSizeInput?.value || 1000;
const chunkOverlap = this.chunkOverlapInput?.value || 200;
const memoryType = this.memoryTypeInput?.value || 'document';
try {
this.setLoading(true);
if (this.selectedFiles.length === 1 || this.processingMode === 'individual') {
// Individual file processing (single file or individual mode for multiple files)
for (let i = 0; i < this.selectedFiles.length; i++) {
const file = this.selectedFiles[i];
try {
await this.uploadSingleDocument(file, {
tags,
chunk_size: parseInt(chunkSize),
chunk_overlap: parseInt(chunkOverlap),
memory_type: memoryType
});
// Small delay between individual uploads to avoid overwhelming the server
if (i < this.selectedFiles.length - 1) {
await new Promise(resolve => setTimeout(resolve, this.constructor.INDIVIDUAL_UPLOAD_DELAY));
}
} catch (error) {
console.error(`Failed to upload ${file.name}:`, error);
this.showToast(`Failed to upload ${file.name}: ${error.message}`, 'error');
// Continue with remaining files
}
}
} else {
// Batch upload
await this.uploadBatchDocuments(this.selectedFiles, {
tags,
chunk_size: parseInt(chunkSize),
chunk_overlap: parseInt(chunkOverlap),
memory_type: memoryType
});
}
// Clear selection and reload history
this.selectedFiles = [];
this.updateUploadButton();
await this.loadUploadHistory();
// Reset drop zone
const dropZone = document.getElementById('dropZone');
if (dropZone) {
const content = dropZone.querySelector('.drop-zone-content');
if (content) {
content.innerHTML = `
<svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
</svg>
<h3>Drag & drop files here</h3>
<p>or <button id="fileSelectBtn" class="link-button">browse to select files</button></p>
<p class="supported-formats">Supported formats: PDF, DOCX, PPTX, TXT, MD, JSON</p>
<input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
`;
}
// Re-setup event listeners for the new elements
this.setupDocumentsEventListeners();
}
} catch (error) {
console.error('Upload error:', error);
this.showToast('Upload failed: ' + error.message, 'error');
} finally {
this.setLoading(false);
}
}
/**
* Upload single document
*/
async uploadSingleDocument(file, config) {
console.log(`Uploading file: ${file.name}, size: ${file.size} bytes`);
const formData = new FormData();
formData.append('file', file);
formData.append('tags', config.tags);
formData.append('chunk_size', config.chunk_size.toString());
formData.append('chunk_overlap', config.chunk_overlap.toString());
formData.append('memory_type', config.memory_type);
const response = await fetch(`${this.apiBase}/documents/upload`, {
method: 'POST',
body: formData
});
console.log(`Upload response status: ${response.status}`);
if (!response.ok) {
let errorMessage = `Upload failed with status ${response.status}`;
try {
const error = await response.json();
console.error('Upload error details:', error);
errorMessage = error.detail || error.message || errorMessage;
} catch (e) {
console.error('Could not parse error response:', e);
try {
const errorText = await response.text();
console.error('Error response text:', errorText);
errorMessage = errorText || errorMessage;
} catch (e2) {
console.error('Could not read error response:', e2);
}
}
throw new Error(errorMessage);
}
const result = await response.json();
console.log('Upload result:', result);
this.showToast(`Upload started for ${file.name}`, 'success');
// Monitor progress if we have an upload ID
if (result.upload_id) {
this.monitorUploadProgress(result.upload_id);
}
return result;
}
/**
* Upload batch documents
*/
async uploadBatchDocuments(files, config) {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
formData.append('tags', config.tags);
formData.append('chunk_size', config.chunk_size.toString());
formData.append('chunk_overlap', config.chunk_overlap.toString());
formData.append('memory_type', config.memory_type);
const response = await fetch(`${this.apiBase}/documents/batch-upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Batch upload failed');
}
const result = await response.json();
this.showToast(`Batch upload started for ${files.length} files`, 'success');
// Monitor progress if we have an upload ID
if (result.upload_id) {
this.monitorUploadProgress(result.upload_id);
}
return result;
}
/**
* Monitor upload progress by polling status endpoint
*/
monitorUploadProgress(uploadId) {
const pollStatus = async () => {
try {
const statusResponse = await this.apiCall(`/documents/status/${uploadId}`);
this.updateUploadProgress(uploadId, statusResponse);
if (statusResponse.progress >= 100 || statusResponse.status === 'completed' || statusResponse.status === 'failed') {
// Upload completed, refresh history
this.loadUploadHistory();
} else {
// Continue polling
setTimeout(pollStatus, 2000); // Poll every 2 seconds
}
} catch (error) {
// If polling fails, try again with longer interval
setTimeout(pollStatus, 5000);
}
};
// Start polling after a short delay
setTimeout(pollStatus, 1000);
}
/**
* Update upload progress display
*/
updateUploadProgress(uploadId, statusData) {
// Find the upload item in history and update it
const historyContainer = document.getElementById('uploadHistory');
if (!historyContainer) return;
const uploadItems = historyContainer.querySelectorAll('.upload-item');
uploadItems.forEach(item => {
const filename = item.querySelector('.upload-filename');
if (filename && filename.textContent.includes(uploadId)) {
// This is a simplified update - in practice you'd match by upload ID
this.loadUploadHistory(); // For now, just refresh the entire history
}
});
}
/**
* Check hybrid backend sync status
*/
async checkSyncStatus() {
// Skip UI updates during force sync to prevent periodic polling from overwriting the syncing state
if (this._isForceSyncing) {
return;
}
try {
const syncStatus = await this.apiCall('/sync/status');
// Get compact sync control element
const syncControl = document.getElementById('syncControl');
if (!syncControl) {
console.warn('Sync control element not found');
return;
}
if (!syncStatus.is_hybrid) {
syncControl.style.display = 'none';
return;
}
// Show sync control for hybrid mode
syncControl.style.display = 'block';
// Update sync status UI elements
const statusText = document.getElementById('syncStatusText');
const syncProgress = document.getElementById('syncProgress');
const pauseButton = document.getElementById('pauseSyncButton');
const resumeButton = document.getElementById('resumeSyncButton');
const syncButton = document.getElementById('forceSyncButton');
// Update pause/resume button visibility based on running state
const isPaused = syncStatus.is_paused || !syncStatus.is_running;
// Attach event listeners if not already attached
if (pauseButton && !pauseButton._listenerAttached) {
pauseButton.addEventListener('click', () => {
this.pauseSync();
});
pauseButton._listenerAttached = true;
}
if (resumeButton && !resumeButton._listenerAttached) {
resumeButton.addEventListener('click', () => {
this.resumeSync();
});
resumeButton._listenerAttached = true;
}
if (syncButton && !syncButton._listenerAttached) {
syncButton.addEventListener('click', () => {
this.forceSync();
});
syncButton._listenerAttached = true;
}
if (pauseButton) {
pauseButton.style.display = isPaused ? 'none' : 'flex';
}
if (resumeButton) {
resumeButton.style.display = isPaused ? 'flex' : 'none';
}
// Determine status and update UI (dot color is handled by CSS classes)
if (isPaused) {
statusText.textContent = 'Paused';
syncProgress.textContent = '';
syncControl.className = 'sync-control-compact paused';
if (syncButton) syncButton.disabled = true;
} else if (syncStatus.status === 'syncing') {
statusText.textContent = 'Syncing';
syncProgress.textContent = syncStatus.operations_pending > 0 ? `${syncStatus.operations_pending} pending` : '';
syncControl.className = 'sync-control-compact syncing';
if (syncButton) syncButton.disabled = true;
} else if (syncStatus.status === 'pending') {
statusText.textContent = 'Pending';
syncProgress.textContent = `${syncStatus.operations_pending} ops`;
syncControl.className = 'sync-control-compact pending';
if (syncButton) syncButton.disabled = false;
} else if (syncStatus.status === 'error') {
statusText.textContent = 'Error';
syncProgress.textContent = `${syncStatus.operations_failed} failed`;
syncControl.className = 'sync-control-compact error';
if (syncButton) syncButton.disabled = false;
} else {
// synced status
statusText.textContent = 'Synced';
syncProgress.textContent = '';
syncControl.className = 'sync-control-compact synced';
if (syncButton) syncButton.disabled = false;
}
} catch (error) {
console.error('Error checking sync status:', error);
// Hide sync control on error (likely not hybrid mode)
const syncControl = document.getElementById('syncControl');
if (syncControl) syncControl.style.display = 'none';
}
}
/**
* Format time delta in human readable format
*/
formatTimeDelta(seconds) {
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
/**
* Pause background sync
*/
async pauseSync() {
try {
const result = await this.apiCall('/sync/pause', 'POST');
if (result.success) {
this.showToast('Sync paused', 'success');
// Update UI immediately using API response data
const pauseButton = document.getElementById('pauseSyncButton');
const resumeButton = document.getElementById('resumeSyncButton');
const statusText = document.getElementById('syncStatusText');
const syncControl = document.getElementById('syncControl');
if (pauseButton) pauseButton.style.display = 'none';
if (resumeButton) resumeButton.style.display = 'flex';
if (statusText) statusText.textContent = 'Paused';
if (syncControl) syncControl.className = 'sync-control-compact paused';
// Small delay to allow backend state to propagate before checking status
await new Promise(resolve => setTimeout(resolve, 200));
} else {
this.showToast('Failed to pause sync: ' + result.message, 'error');
}
await this.checkSyncStatus();
} catch (error) {
console.error('Error pausing sync:', error);
this.showToast('Failed to pause sync', 'error');
}
}
/**
* Resume background sync
*/
async resumeSync() {
try {
const result = await this.apiCall('/sync/resume', 'POST');
if (result.success) {
this.showToast('Sync resumed', 'success');
// Update UI immediately using API response data
const pauseButton = document.getElementById('pauseSyncButton');
const resumeButton = document.getElementById('resumeSyncButton');
const statusText = document.getElementById('syncStatusText');
const syncControl = document.getElementById('syncControl');
if (pauseButton) pauseButton.style.display = 'flex';
if (resumeButton) resumeButton.style.display = 'none';
if (statusText) statusText.textContent = 'Synced';
if (syncControl) syncControl.className = 'sync-control-compact synced';
// Small delay to allow backend state to propagate before checking status
await new Promise(resolve => setTimeout(resolve, 200));
} else {
this.showToast('Failed to resume sync: ' + result.message, 'error');
}
await this.checkSyncStatus();
} catch (error) {
console.error('Error resuming sync:', error);
this.showToast('Failed to resume sync', 'error');
}
}
/**
* Check backup status and update Settings modal
*/
async checkBackupStatus() {
try {
const backupStatus = await this.apiCall('/backup/status');
// Update backup elements in Settings modal
const lastBackup = document.getElementById('settingsLastBackup');
const backupCount = document.getElementById('settingsBackupCount');
const nextBackup = document.getElementById('settingsNextBackup');
if (!backupStatus.enabled) {
if (lastBackup) lastBackup.textContent = 'Backups disabled';
if (backupCount) backupCount.textContent = '-';
if (nextBackup) nextBackup.textContent = '-';
return;
}
// Update last backup time
if (lastBackup) {
if (backupStatus.time_since_last_seconds) {
lastBackup.textContent = this.formatTimeDelta(Math.floor(backupStatus.time_since_last_seconds)) + ' ago';
} else {
lastBackup.textContent = 'Never';
}
}
// Update backup count with size
if (backupCount) {
const sizeMB = (backupStatus.total_size_bytes / 1024 / 1024).toFixed(1);
backupCount.textContent = `${backupStatus.backup_count} (${sizeMB} MB)`;
}
// Update next scheduled backup
if (nextBackup && backupStatus.next_backup_at) {
const nextDate = new Date(backupStatus.next_backup_at);
nextBackup.textContent = nextDate.toLocaleString();
} else if (nextBackup) {
nextBackup.textContent = backupStatus.scheduler_running ? 'Scheduled' : 'Not scheduled';
}
} catch (error) {
console.error('Error checking backup status:', error);
}
}
/**
* Create a backup manually
*/
async createBackup() {
const backupButton = document.getElementById('backupNowButton');
if (backupButton) backupButton.disabled = true;
try {
this.showToast('Creating backup...', 'info');
const result = await this.apiCall('/backup/now', 'POST');
if (result.success) {
const sizeMB = (result.size_bytes / 1024 / 1024).toFixed(2);
this.showToast(`Backup created: ${result.filename} (${sizeMB} MB)`, 'success');
} else {
this.showToast('Backup failed: ' + result.error, 'error');
}
await this.checkBackupStatus();
} catch (error) {
console.error('Error creating backup:', error);
this.showToast('Failed to create backup', 'error');
} finally {
if (backupButton) backupButton.disabled = false;
}
}
/**
* Start periodic sync status monitoring
*/
startSyncStatusMonitoring() {
// Check sync status every 10 seconds
setInterval(() => {
this.checkSyncStatus();
}, 10000);
}
/**
* Manually force sync to Cloudflare
*/
async forceSync() {
const syncButton = document.getElementById('forceSyncButton');
const originalText = syncButton.innerHTML;
try {
// Check if sync was paused before force sync
const statusBefore = await this.apiCall('/sync/status');
const wasPaused = statusBefore.is_paused;
// Set flag to prevent periodic polling from overwriting UI during force sync
this._isForceSyncing = true;
// Disable button and show loading state (just egg timer, no text - widget shows "Syncing")
syncButton.disabled = true;
syncButton.innerHTML = '<span class="sync-button-icon">⏳</span>';
// IMMEDIATELY update sync control widget to show syncing state
const statusText = document.getElementById('syncStatusText');
const syncProgress = document.getElementById('syncProgress');
const syncControl = document.getElementById('syncControl');
if (statusText) statusText.textContent = 'Syncing';
if (syncProgress) syncProgress.textContent = statusBefore.operations_pending > 0 ? `${statusBefore.operations_pending} pending` : '';
if (syncControl) syncControl.className = 'sync-control-compact syncing';
// Show toast when sync starts
this.showToast('Starting sync...', 'info');
const result = await this.apiCall('/sync/force', 'POST');
if (result.success) {
this.showToast(`Synced ${result.operations_synced} operations in ${result.time_taken_seconds}s`, 'success');
// Refresh dashboard data to show newly synced memories
if (this.currentView === 'dashboard') {
await this.loadDashboardData();
}
// If sync was paused before, pause it again after force sync
if (wasPaused) {
await this.apiCall('/sync/pause', 'POST');
}
} else {
this.showToast('Sync failed: ' + result.message, 'error');
}
} catch (error) {
console.error('Error forcing sync:', error);
this.showToast('Failed to force sync: ' + error.message, 'error');
} finally {
// Clear flag to allow periodic polling to resume
this._isForceSyncing = false;
// Re-enable button
syncButton.disabled = false;
syncButton.innerHTML = originalText;
// Refresh sync status immediately
await this.checkSyncStatus();
}
}
/**
* Render tags cloud from API data
*/
renderTagsCloud() {
const container = document.getElementById('tagsCloudContainer');
const taggedContainer = document.getElementById('taggedMemoriesContainer');
// Hide the tagged memories view initially
taggedContainer.style.display = 'none';
if (!this.tags || this.tags.length === 0) {
container.innerHTML = '<p class="text-neutral-600">No tags found. Start adding tags to your memories to see them here.</p>';
return;
}
// Render tag bubbles (tags are already sorted by count from backend)
container.innerHTML = this.tags.map(tagData => `
<button class="tag-bubble" data-tag="${this.escapeHtml(tagData.tag)}">
${this.escapeHtml(tagData.tag)}
<span class="count">${tagData.count}</span>
</button>
`).join('');
}
/**
* Filter memories by selected tag
*/
async filterByTag(tag) {
const taggedContainer = document.getElementById('taggedMemoriesContainer');
const tagNameSpan = document.getElementById('selectedTagName');
const memoriesList = document.getElementById('taggedMemoriesList');
try {
// Fetch memories for this specific tag
const memoriesResponse = await this.apiCall(`/memories?tag=${encodeURIComponent(tag)}&limit=100`);
const filteredMemories = memoriesResponse.memories || [];
// Show the tagged memories section
tagNameSpan.textContent = tag;
taggedContainer.style.display = 'block';
// Smooth scroll to results section for better UX
taggedContainer.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Render filtered memories
this.renderMemoriesInContainer(filteredMemories, memoriesList);
// Add event listener for clear filter button
const clearBtn = document.getElementById('clearTagFilter');
clearBtn.onclick = () => this.clearTagFilter();
} catch (error) {
console.error('Error filtering by tag:', error);
this.showToast('Failed to load memories for tag', 'error');
}
}
/**
* Clear tag filter and show all tags
*/
clearTagFilter() {
const taggedContainer = document.getElementById('taggedMemoriesContainer');
taggedContainer.style.display = 'none';
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Toggle chunking help section visibility
*/
toggleChunkingHelp() {
const helpSection = document.getElementById('chunkingHelpSection');
if (helpSection) {
if (helpSection.style.display === 'none') {
helpSection.style.display = 'block';
} else {
helpSection.style.display = 'none';
}
}
}
/**
* Hide chunking help section
*/
hideChunkingHelp() {
const helpSection = document.getElementById('chunkingHelpSection');
if (helpSection) {
helpSection.style.display = 'none';
}
}
/**
* Toggle overlap help section visibility
*/
toggleOverlapHelp() {
const helpSection = document.getElementById('overlapHelpSection');
if (helpSection) {
if (helpSection.style.display === 'none') {
helpSection.style.display = 'block';
} else {
helpSection.style.display = 'none';
}
}
}
/**
* Hide overlap help section
*/
hideOverlapHelp() {
const helpSection = document.getElementById('overlapHelpSection');
if (helpSection) {
helpSection.style.display = 'none';
}
}
/**
* Toggle processing mode help section visibility
*/
toggleProcessingModeHelp() {
const helpSection = document.getElementById('processingModeHelpSection');
if (helpSection) {
helpSection.style.display = helpSection.style.display === 'block' ? 'none' : 'block';
}
}
/**
* Hide processing mode help section
*/
hideProcessingModeHelp() {
const helpSection = document.getElementById('processingModeHelpSection');
if (helpSection) {
helpSection.style.display = 'none';
}
}
/**
* Render memories in a specific container
*/
renderMemoriesInContainer(memories, container) {
if (!memories || memories.length === 0) {
container.innerHTML = '<p class="empty-state">No memories found with this tag.</p>';
return;
}
container.innerHTML = memories.map(memory => this.renderMemoryCard(memory)).join('');
// Add click handlers
container.querySelectorAll('.memory-card').forEach((card, index) => {
card.addEventListener('click', () => this.handleMemoryClick(memories[index]));
});
}
/**
* Handle navigation between views
*/
handleNavigation(e) {
const viewName = e.currentTarget.dataset.view;
this.switchView(viewName);
}
/**
* Switch between different views
*/
switchView(viewName) {
// Update navigation active state (if navigation exists)
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
const navItem = document.querySelector(`[data-view="${viewName}"]`);
if (navItem) {
navItem.classList.add('active');
}
// Hide all views (if view containers exist)
document.querySelectorAll('.view-container').forEach(view => {
view.classList.remove('active');
});
// Show target view (if it exists)
const targetView = document.getElementById(`${viewName}View`);
if (targetView) {
targetView.classList.add('active');
this.currentView = viewName;
// Load view-specific data
this.loadViewData(viewName);
}
}
/**
* Load data specific to the current view
*/
async loadViewData(viewName) {
switch (viewName) {
case 'search':
// Initialize search view with recent search or empty state
break;
case 'browse':
await this.loadBrowseData();
break;
case 'documents':
await this.loadDocumentsData();
break;
case 'manage':
await this.loadManageData();
break;
case 'analytics':
await this.loadAnalyticsData();
break;
case 'apiDocs':
// API docs view - static content, no additional loading needed
break;
default:
// Dashboard view is loaded in loadDashboardData
break;
}
}
/**
* Handle quick search input
*/
async handleQuickSearch(e) {
const query = e.target.value.trim();
if (query.length >= 2) {
try {
const results = await this.searchMemories(query);
// Could show dropdown suggestions here
} catch (error) {
console.error('Quick search error:', error);
}
}
}
/**
* Handle full search
*/
async handleSearch(query) {
if (!query.trim()) return;
this.switchView('search');
this.setLoading(true);
try {
const results = await this.searchMemories(query);
this.searchResults = results;
this.renderSearchResults(results);
this.updateResultsCount(results.length);
} catch (error) {
console.error('Search error:', error);
this.showToast('Search failed', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Search memories using the API
*/
async searchMemories(query, filters = {}) {
// Detect tag search patterns: #tag, tag:value, or "tag:value"
const tagPattern = /^(#|tag:)(.+)$/i;
const tagMatch = query.match(tagPattern);
if (tagMatch) {
// Use tag search endpoint
const tagValue = tagMatch[2].trim();
const payload = {
tags: [tagValue],
match_all: false // ANY match by default
};
const response = await this.apiCall('/search/by-tag', 'POST', payload);
return response.results || [];
} else {
// Use semantic search endpoint
const payload = {
query: query,
n_results: filters.limit || 20,
...filters
};
// Only add similarity_threshold if explicitly set in filters
if (filters.threshold !== undefined) {
payload.similarity_threshold = filters.threshold;
}
const response = await this.apiCall('/search', 'POST', payload);
return response.results || [];
}
}
/**
* Handle filter changes in search view
*/
async handleFilterChange() {
const tagFilter = document.getElementById('tagFilter')?.value;
const dateFilter = document.getElementById('dateFilter')?.value;
const typeFilter = document.getElementById('typeFilter')?.value;
const query = document.getElementById('quickSearch')?.value?.trim() || '';
// Add loading state
const applyBtn = document.getElementById('applyFiltersBtn');
if (applyBtn) {
applyBtn.classList.add('loading');
applyBtn.disabled = true;
}
try {
let results = [];
// Priority 1: If we have a semantic query, start with semantic search
if (query) {
const filters = {};
if (typeFilter) filters.type = typeFilter;
results = await this.searchMemories(query, filters);
// Apply tag filtering to semantic search results if tags are specified
if (tagFilter && tagFilter.trim()) {
const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
if (tags.length > 0) {
results = results.filter(result => {
const memoryTags = result.memory.tags || [];
// Check if any of the specified tags match memory tags (case-insensitive)
return tags.some(filterTag =>
memoryTags.some(memoryTag =>
memoryTag.toLowerCase().includes(filterTag.toLowerCase())
)
);
});
}
}
}
// Priority 2: Tag-only search (when no semantic query)
else if (tagFilter && tagFilter.trim()) {
const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
if (tags.length > 0) {
const payload = {
tags: tags,
match_all: false // ANY match by default
};
const response = await this.apiCall('/search/by-tag', 'POST', payload);
results = response.results || [];
// Apply type filter if present
if (typeFilter && typeFilter.trim()) {
results = results.filter(result => {
const memoryType = result.memory.memory_type || 'note';
return memoryType === typeFilter;
});
}
}
}
// Priority 3: Date-based search
else if (dateFilter && dateFilter.trim()) {
const payload = {
query: dateFilter,
n_results: 100
};
const response = await this.apiCall('/search/by-time', 'POST', payload);
results = response.results || [];
// Apply type filter if present
if (typeFilter && typeFilter.trim()) {
results = results.filter(result => {
const memoryType = result.memory.memory_type || 'note';
return memoryType === typeFilter;
});
}
}
// Priority 4: Type-only filter
else if (typeFilter && typeFilter.trim()) {
const allMemoriesResponse = await this.apiCall('/memories?page=1&page_size=1000');
if (allMemoriesResponse.memories) {
results = allMemoriesResponse.memories
.filter(memory => (memory.memory_type || 'note') === typeFilter)
.map(memory => ({ memory, similarity: 1.0 }));
}
} else {
// No filters, clear results
results = [];
}
this.searchResults = results;
this.renderSearchResults(results);
this.updateResultsCount(results.length);
this.updateActiveFilters();
} catch (error) {
console.error('Filter search error:', error);
this.showToast('Filter search failed', 'error');
} finally {
// Remove loading state
const applyBtn = document.getElementById('applyFiltersBtn');
if (applyBtn) {
applyBtn.classList.remove('loading');
applyBtn.disabled = false;
}
}
}
/**
* Handle view mode changes (grid/list)
*/
handleViewModeChange(mode) {
document.querySelectorAll('.view-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-view="${mode}"]`).classList.add('active');
const resultsContainer = document.getElementById('searchResultsList');
resultsContainer.className = mode === 'grid' ? 'memory-grid' : 'memory-list';
}
/**
* Handle quick actions
*/
handleQuickAction(action) {
switch (action) {
case 'quick-search':
this.switchView('search');
const searchInput = document.getElementById('quickSearch');
if (searchInput) {
searchInput.focus();
}
break;
case 'add-memory':
this.handleAddMemory();
break;
case 'browse-tags':
this.switchView('browse');
break;
case 'export-data':
this.handleExportData();
break;
}
}
/**
* Handle add memory action
*/
handleAddMemory() {
const modal = document.getElementById('addMemoryModal');
// Reset modal for adding new memory
this.resetAddMemoryModal();
this.openModal(modal);
document.getElementById('memoryContent').focus();
}
/**
* Reset add memory modal to default state
*/
resetAddMemoryModal() {
const modal = document.getElementById('addMemoryModal');
const title = modal.querySelector('.modal-header h3');
const saveBtn = document.getElementById('saveMemoryBtn');
// Reset modal title and button text
title.textContent = 'Add New Memory';
saveBtn.textContent = 'Save Memory';
// Clear form
document.getElementById('addMemoryForm').reset();
// Clear editing state
this.editingMemory = null;
}
/**
* Handle save memory
*/
async handleSaveMemory() {
const content = document.getElementById('memoryContent').value.trim();
const tags = document.getElementById('memoryTags').value.trim();
const type = document.getElementById('memoryType').value;
if (!content) {
this.showToast('Please enter memory content', 'warning');
return;
}
const payload = {
content: content,
tags: tags ? tags.split(',').map(t => t.trim()) : [],
memory_type: type,
metadata: {
created_via: 'dashboard',
user_agent: navigator.userAgent,
updated_via: this.editingMemory ? 'dashboard_edit' : 'dashboard_create'
}
};
try {
let response;
let successMessage;
if (this.editingMemory) {
// Smart update: check if only metadata changed vs content changes
const originalContentHash = this.editingMemory.content_hash;
const contentChanged = this.editingMemory.content !== payload.content;
if (!contentChanged) {
// Only metadata (tags, type, metadata) changed - use PUT endpoint
const updatePayload = {
tags: payload.tags,
memory_type: payload.memory_type,
metadata: payload.metadata
};
response = await this.apiCall(`/memories/${originalContentHash}`, 'PUT', updatePayload);
successMessage = 'Memory updated successfully';
} else {
// Content changed - use create-delete approach (but with proper error handling)
try {
// Step 1: Create updated memory first
response = await this.apiCall('/memories', 'POST', payload);
// CRITICAL: Only proceed with deletion if creation actually succeeded
if (response.success) {
successMessage = 'Memory updated successfully';
try {
// Step 2: Delete original memory (only after successful creation)
const deleteResponse = await this.apiCall(`/memories/${originalContentHash}`, 'DELETE');
} catch (deleteError) {
console.error('Failed to delete original memory after creating new version:', deleteError);
this.showToast('Memory updated, but original version still exists. You may need to manually delete the duplicate.', 'warning');
}
} else {
// Creation failed - do NOT delete original memory
console.error('Creation failed:', response.message);
throw new Error(`Failed to create updated memory: ${response.message}`);
}
} catch (createError) {
// CREATE failed - original memory intact, no cleanup needed
console.error('Failed to create updated memory:', createError);
throw new Error(`Failed to update memory: ${createError.message}`);
}
}
} else {
// Create new memory
response = await this.apiCall('/memories', 'POST', payload);
successMessage = 'Memory saved successfully';
}
this.closeModal(document.getElementById('addMemoryModal'));
this.showToast(successMessage, 'success');
// Reset editing state
this.editingMemory = null;
this.resetAddMemoryModal();
// Refresh current view if needed
if (this.currentView === 'dashboard') {
this.loadDashboardData();
} else if (this.currentView === 'search') {
// Refresh search results
const query = document.getElementById('searchInput').value.trim();
if (query) {
this.handleSearch(query);
}
} else if (this.currentView === 'browse') {
// Refresh browse view (tags cloud)
this.loadBrowseData();
}
} catch (error) {
console.error('Error saving memory:', error);
this.showToast(error.message || 'Failed to save memory', 'error');
}
}
/**
* Handle memory click to show details
*/
handleMemoryClick(memory) {
this.showMemoryDetails(memory);
}
/**
* Show memory details in modal
*/
showMemoryDetails(memory) {
const modal = document.getElementById('memoryModal');
const title = document.getElementById('modalTitle');
const content = document.getElementById('modalContent');
title.textContent = 'Memory Details';
content.innerHTML = this.renderMemoryDetails(memory);
// Set up action buttons
document.getElementById('editMemoryBtn').onclick = () => this.editMemory(memory);
document.getElementById('deleteMemoryBtn').onclick = () => this.deleteMemory(memory);
document.getElementById('shareMemoryBtn').onclick = () => this.shareMemory(memory);
this.openModal(modal);
}
/**
* Render memory details HTML
*/
renderMemoryDetails(memory) {
const createdDate = new Date(memory.created_at * 1000).toLocaleString();
const updatedDate = memory.updated_at ? new Date(memory.updated_at * 1000).toLocaleString() : null;
return `
<div class="memory-detail">
<div class="memory-meta">
<p><strong>Created:</strong> ${createdDate}</p>
${updatedDate ? `<p><strong>Updated:</strong> ${updatedDate}</p>` : ''}
<p><strong>Type:</strong> ${memory.memory_type || 'note'}</p>
<p><strong>ID:</strong> ${memory.content_hash}</p>
</div>
<div class="memory-content">
<h4>Content</h4>
<div class="content-text">${this.escapeHtml(memory.content)}</div>
</div>
${memory.tags && memory.tags.length > 0 ? `
<div class="memory-tags-section">
<h4>Tags</h4>
<div class="memory-tags">
${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
</div>
</div>
` : ''}
${memory.metadata ? `
<div class="memory-metadata">
<h4 class="metadata-toggle" onclick="this.parentElement.classList.toggle('expanded')" style="cursor: pointer; user-select: none;">
<span class="toggle-icon">▶</span> Metadata
</h4>
<div class="metadata-content">
${this.renderMetadata(memory.metadata)}
</div>
</div>
` : ''}
</div>
`;
}
/**
* Render metadata in a prettier format
*/
renderMetadata(metadata) {
if (!metadata || typeof metadata !== 'object') {
return '<p class="metadata-empty">No metadata available</p>';
}
let html = '<div class="metadata-items">';
for (const [key, value] of Object.entries(metadata)) {
let displayValue;
if (typeof value === 'string') {
displayValue = `<span class="metadata-string">"${this.escapeHtml(value)}"</span>`;
} else if (typeof value === 'number') {
displayValue = `<span class="metadata-number">${value}</span>`;
} else if (typeof value === 'boolean') {
displayValue = `<span class="metadata-boolean">${value}</span>`;
} else if (Array.isArray(value)) {
displayValue = `<span class="metadata-array">[${value.map(v =>
typeof v === 'string' ? `"${this.escapeHtml(v)}"` : v
).join(', ')}]</span>`;
} else {
displayValue = `<span class="metadata-object">${JSON.stringify(value)}</span>`;
}
html += `
<div class="metadata-item">
<span class="metadata-key">${this.escapeHtml(key)}:</span>
<span class="metadata-value">${displayValue}</span>
</div>
`;
}
html += '</div>';
return html;
}
/**
* Delete memory
*/
async deleteMemory(memory) {
if (!confirm('Are you sure you want to delete this memory? This action cannot be undone.')) {
return;
}
try {
await this.apiCall(`/memories/${memory.content_hash}`, 'DELETE');
this.closeModal(document.getElementById('memoryModal'));
this.showToast('Memory deleted successfully', 'success');
// Refresh current view
if (this.currentView === 'dashboard') {
this.loadDashboardData();
} else if (this.currentView === 'search') {
this.searchResults = this.searchResults.filter(m => m.memory.content_hash !== memory.content_hash);
this.renderSearchResults(this.searchResults);
} else if (this.currentView === 'browse') {
// Refresh browse view (tags cloud)
this.loadBrowseData();
}
} catch (error) {
console.error('Error deleting memory:', error);
this.showToast('Failed to delete memory', 'error');
}
}
/**
* Edit memory
*/
editMemory(memory) {
// Close the memory details modal first
this.closeModal(document.getElementById('memoryModal'));
// Open the add memory modal with pre-filled data
const modal = document.getElementById('addMemoryModal');
const title = modal.querySelector('.modal-header h3');
const saveBtn = document.getElementById('saveMemoryBtn');
// Update modal for editing
title.textContent = 'Edit Memory';
saveBtn.textContent = 'Update Memory';
// Pre-fill the form with existing data
document.getElementById('memoryContent').value = memory.content || '';
// Handle tags - ensure they're displayed correctly
const tagsValue = memory.tags && Array.isArray(memory.tags) ? memory.tags.join(', ') : '';
document.getElementById('memoryTags').value = tagsValue;
document.getElementById('memoryType').value = memory.memory_type || 'note';
// Store the memory being edited
this.editingMemory = memory;
this.openModal(modal);
// Use setTimeout to ensure modal is fully rendered before setting values
setTimeout(() => {
document.getElementById('memoryContent').focus();
}, 100);
}
/**
* Share memory
*/
shareMemory(memory) {
// Create shareable data
const shareData = {
content: memory.content,
tags: memory.tags || [],
type: memory.memory_type || 'note',
created: new Date(memory.created_at * 1000).toISOString(),
id: memory.content_hash
};
// Try to use Web Share API if available
if (navigator.share) {
navigator.share({
title: 'Memory from MCP Memory Service',
text: memory.content,
url: window.location.href
}).catch(err => {
// Share API failed, fall back to clipboard
this.fallbackShare(shareData);
});
} else {
this.fallbackShare(shareData);
}
}
/**
* Fallback share method (copy to clipboard)
*/
fallbackShare(shareData) {
const shareText = `Memory Content:\n${shareData.content}\n\nTags: ${shareData.tags.join(', ')}\nType: ${shareData.type}\nCreated: ${shareData.created}`;
navigator.clipboard.writeText(shareText).then(() => {
this.showToast('Memory copied to clipboard', 'success');
}).catch(err => {
console.error('Could not copy text: ', err);
this.showToast('Failed to copy to clipboard', 'error');
});
}
/**
* Handle data export
*/
async handleExportData() {
try {
this.showToast('Preparing export...', 'info');
// Fetch all memories using pagination
const allMemories = [];
const pageSize = 100; // Reasonable batch size
let page = 1;
let hasMore = true;
let totalMemories = 0;
while (hasMore) {
const response = await this.apiCall(`/memories?page=${page}&page_size=${pageSize}`);
if (page === 1) {
totalMemories = response.total;
}
if (response.memories && response.memories.length > 0) {
allMemories.push(...response.memories);
hasMore = response.has_more;
page++;
// Update progress
this.showToast(`Fetching memories... (${allMemories.length}/${totalMemories})`, 'info');
} else {
hasMore = false;
}
}
const data = {
export_date: new Date().toISOString(),
total_memories: totalMemories,
exported_memories: allMemories.length,
memories: allMemories
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mcp-memories-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showToast(`Successfully exported ${allMemories.length} memories`, 'success');
} catch (error) {
console.error('Export error:', error);
this.showToast('Failed to export data', 'error');
}
}
/**
* Render recent memories
*/
renderRecentMemories(memories) {
const container = document.getElementById('recentMemoriesList');
if (!container) {
console.error('recentMemoriesList container not found');
return;
}
if (!memories || memories.length === 0) {
container.innerHTML = '<p class="empty-state">No memories found. <a href="#" onclick="app.handleAddMemory()">Add your first memory</a></p>';
return;
}
// Group document chunks by upload_id
const groupedMemories = this.groupMemoriesByUpload(memories);
container.innerHTML = groupedMemories.map(group => {
if (group.type === 'document') {
return this.renderDocumentGroup(group);
} else {
return this.renderMemoryCard(group.memory);
}
}).join('');
// Add click handlers for individual memories
container.querySelectorAll('.memory-card').forEach((card, index) => {
const group = groupedMemories[index];
if (group.type === 'single') {
card.addEventListener('click', () => this.handleMemoryClick(group.memory));
}
});
// Add click handlers for document groups
container.querySelectorAll('.document-group').forEach((groupEl, index) => {
const group = groupedMemories.filter(g => g.type === 'document')[index];
if (group) {
groupEl.addEventListener('click', (e) => {
// Don't trigger if clicking on action buttons
if (e.target.closest('.document-actions')) return;
this.showDocumentChunks(group);
});
}
});
// Add click handlers for document action buttons
container.querySelectorAll('.document-group').forEach((groupEl, index) => {
const group = groupedMemories.filter(g => g.type === 'document')[index];
if (group) {
// View chunks button
const viewBtn = groupEl.querySelector('.btn-view-chunks');
if (viewBtn) {
viewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.showDocumentChunks(group);
});
}
// Remove button
const removeBtn = groupEl.querySelector('.btn-remove');
if (removeBtn) {
removeBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await this.removeDocument(group.upload_id, group.source_file);
// removeDocument() already handles view refresh
});
}
}
});
}
/**
* Group memories by upload_id for document chunks
*/
groupMemoriesByUpload(memories) {
const groups = [];
const documentGroups = new Map();
const processedHashes = new Set();
for (const memory of memories) {
// Check if this is a document chunk
const isDocumentChunk = memory.metadata && memory.metadata.upload_id;
if (isDocumentChunk && !processedHashes.has(memory.content_hash)) {
const uploadId = memory.metadata.upload_id;
const sourceFile = memory.metadata.source_file || 'Unknown file';
if (!documentGroups.has(uploadId)) {
documentGroups.set(uploadId, {
upload_id: uploadId,
source_file: sourceFile,
memories: [],
created_at: memory.created_at,
tags: new Set()
});
}
const group = documentGroups.get(uploadId);
group.memories.push(memory);
group.tags.add(...(memory.tags || []));
processedHashes.add(memory.content_hash);
} else if (!processedHashes.has(memory.content_hash)) {
// Regular memory
groups.push({
type: 'single',
memory: memory
});
processedHashes.add(memory.content_hash);
}
}
// Convert document groups to array format
for (const group of documentGroups.values()) {
groups.push({
type: 'document',
upload_id: group.upload_id,
source_file: group.source_file,
memories: group.memories,
created_at: group.created_at,
tags: Array.from(group.tags)
});
}
// Sort by creation time (most recent first)
groups.sort((a, b) => {
const timeA = a.type === 'document' ? a.created_at : a.memory.created_at;
const timeB = b.type === 'document' ? b.created_at : b.memory.created_at;
return timeB - timeA;
});
return groups;
}
/**
* Render a document group card
*/
renderDocumentGroup(group) {
const createdDate = new Date(group.created_at * 1000).toLocaleDateString();
const fileName = this.escapeHtml(group.source_file);
const chunkCount = group.memories.length;
// Filter out metadata tags AND tags that are too long (likely corrupted/malformed)
const uniqueTags = [...new Set(group.tags.filter(tag =>
!tag.startsWith('upload_id:') &&
!tag.startsWith('source_file:') &&
!tag.startsWith('file_type:') &&
tag.length < 100 // Reject tags longer than 100 chars (likely corrupted metadata)
))];
return `
<div class="document-group" data-upload-id="${this.escapeHtml(group.upload_id)}">
<div class="document-header">
<div class="document-icon">📄</div>
<div class="document-info">
<div class="document-title">${fileName}</div>
<div class="document-meta">
${chunkCount} chunks • ${createdDate}
</div>
</div>
</div>
<div class="document-preview">
${group.memories[0] ? this.escapeHtml(group.memories[0].content.substring(0, 150)) + (group.memories[0].content.length > 150 ? '...' : '') : 'No content preview available'}
</div>
${uniqueTags.length > 0 ? `
<div class="document-tags">
${uniqueTags.slice(0, 3).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
${uniqueTags.length > 3 ? `<span class="tag more">+${uniqueTags.length - 3} more</span>` : ''}
</div>
` : ''}
<div class="document-actions">
<button class="btn-icon btn-view-chunks" title="View all chunks">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
</svg>
<span>View Chunks</span>
</button>
<button class="btn-icon btn-remove" title="Remove document">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
<span>Remove</span>
</button>
</div>
</div>
`;
}
/**
* Show document chunks in a modal
*/
showDocumentChunks(group) {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content large-modal">
<div class="modal-header">
<h3>📄 ${this.escapeHtml(group.source_file)}</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<div class="document-chunks">
${group.memories.map((memory, index) => `
<div class="chunk-item">
<div class="chunk-header">
<span class="chunk-number">Chunk ${index + 1}</span>
<div class="chunk-meta">
${memory.metadata && memory.metadata.page ? `Page ${memory.metadata.page} • ` : ''}
${memory.content.length} chars
</div>
</div>
<div class="chunk-content">
${this.escapeHtml(memory.content)}
</div>
${memory.tags && memory.tags.length > 0 ? `
<div class="chunk-tags">
${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Add active class to show modal (required for display: flex)
setTimeout(() => modal.classList.add('active'), 10);
// Add close handlers
const closeModal = () => {
modal.classList.remove('active');
setTimeout(() => document.body.removeChild(modal), 300);
};
modal.querySelector('.modal-close').addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
}
/**
* Render tags in sidebar
*/
renderTagsSidebar(tags) {
const container = document.getElementById('tagsCloudContainer');
if (!container) {
console.warn('tagsCloudContainer element not found - skipping tags sidebar rendering');
return;
}
if (!tags || tags.length === 0) {
container.innerHTML = '<div class="no-tags">No tags found.</div>';
return;
}
// Take top tags for sidebar display
const topTags = tags.slice(0, 10);
container.innerHTML = topTags.map(tagData => `
<div class="tag-item" data-tag="${this.escapeHtml(tagData.tag)}">
<span class="tag-name">${this.escapeHtml(tagData.tag)}</span>
<span class="tag-count">${tagData.count}</span>
</div>
`).join('');
// Add click handlers
container.querySelectorAll('.tag-item').forEach(item => {
item.addEventListener('click', () => {
const tagName = item.dataset.tag;
const searchInput = document.getElementById('searchInput');
searchInput.value = `#${tagName}`;
this.handleSearch(`#${tagName}`);
});
});
}
/**
* Render search results
*/
renderSearchResults(results) {
const container = document.getElementById('searchResultsList');
if (!results || results.length === 0) {
container.innerHTML = '<p class="empty-state">No results found. Try a different search term.</p>';
return;
}
container.innerHTML = results.map(result => this.renderMemoryCard(result.memory, result)).join('');
// Add click handlers
container.querySelectorAll('.memory-card').forEach((card, index) => {
card.addEventListener('click', () => this.handleMemoryClick(results[index].memory));
});
}
/**
* Render a memory card
*/
renderMemoryCard(memory, searchResult = null) {
const createdDate = new Date(memory.created_at * 1000).toLocaleDateString();
const relevanceScore = searchResult &&
searchResult.similarity_score !== null &&
searchResult.similarity_score !== undefined &&
!isNaN(searchResult.similarity_score) &&
searchResult.similarity_score > 0
? (searchResult.similarity_score * 100).toFixed(1)
: null;
// Truncate content to 150 characters for preview
const truncatedContent = memory.content.length > 150
? memory.content.substring(0, 150) + '...'
: memory.content;
return `
<div class="memory-card" data-memory-id="${memory.content_hash}">
<div class="memory-header">
<div class="memory-meta">
<span>${createdDate}</span>
${memory.memory_type ? `<span> • ${memory.memory_type}</span>` : ''}
${relevanceScore ? `<span> • ${relevanceScore}% match</span>` : ''}
</div>
</div>
<div class="memory-content">
${this.escapeHtml(truncatedContent)}
</div>
${memory.tags && memory.tags.length > 0 ? `
<div class="memory-tags">
${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
</div>
`;
}
/**
* Update dashboard statistics
*/
updateDashboardStats(stats) {
const totalMemoriesEl = document.getElementById('totalMemories');
if (totalMemoriesEl) {
totalMemoriesEl.textContent = stats.total_memories || '0';
}
const recentMemoriesEl = document.getElementById('recentMemories');
if (recentMemoriesEl) {
recentMemoriesEl.textContent = stats.memories_this_week || '0';
}
const uniqueTagsEl = document.getElementById('uniqueTags');
if (uniqueTagsEl) {
uniqueTagsEl.textContent = stats.unique_tags || '0';
}
const storageBackendEl = document.getElementById('storageBackend');
if (storageBackendEl) {
storageBackendEl.textContent = stats.backend || 'unknown';
}
}
/**
* Update search results count
*/
updateResultsCount(count) {
const element = document.getElementById('resultsCount');
if (element) {
element.textContent = `${count} result${count !== 1 ? 's' : ''}`;
}
}
/**
* Update active filters display
*/
updateActiveFilters() {
const activeFiltersContainer = document.getElementById('activeFilters');
const filtersList = document.getElementById('activeFiltersList');
if (!activeFiltersContainer || !filtersList) return;
const tagFilter = document.getElementById('tagFilter')?.value?.trim();
const dateFilter = document.getElementById('dateFilter')?.value;
const typeFilter = document.getElementById('typeFilter')?.value;
const filters = [];
if (tagFilter) {
const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
tags.forEach(tag => {
filters.push({
type: 'tag',
value: tag,
label: `Tag: ${tag}`
});
});
}
if (dateFilter) {
const dateLabels = {
'today': 'Today',
'week': 'This week',
'month': 'This month',
'year': 'This year'
};
filters.push({
type: 'date',
value: dateFilter,
label: `Date: ${dateLabels[dateFilter] || dateFilter}`
});
}
if (typeFilter) {
const typeLabels = {
'note': 'Notes',
'code': 'Code',
'reference': 'References',
'idea': 'Ideas'
};
filters.push({
type: 'type',
value: typeFilter,
label: `Type: ${typeLabels[typeFilter] || typeFilter}`
});
}
if (filters.length === 0) {
activeFiltersContainer.style.display = 'none';
return;
}
activeFiltersContainer.style.display = 'block';
filtersList.innerHTML = filters.map(filter => `
<div class="filter-pill">
${this.escapeHtml(filter.label)}
<button class="remove-filter" data-filter-type="${this.escapeHtml(filter.type)}" data-filter-value="${this.escapeHtml(filter.value)}">
×
</button>
</div>
`).join('');
// Add event listeners for filter removal
filtersList.addEventListener('click', (e) => {
const button = e.target.closest('.remove-filter');
if (!button) return;
const type = button.dataset.filterType;
const value = button.dataset.filterValue;
this.removeFilter(type, value);
});
}
/**
* Remove a specific filter
*/
removeFilter(type, value) {
switch (type) {
case 'tag':
const tagInput = document.getElementById('tagFilter');
if (tagInput) {
const tags = tagInput.value.split(',').map(t => t.trim()).filter(t => t && t !== value);
tagInput.value = tags.join(', ');
}
break;
case 'date':
const dateSelect = document.getElementById('dateFilter');
if (dateSelect) {
dateSelect.value = '';
}
break;
case 'type':
const typeSelect = document.getElementById('typeFilter');
if (typeSelect) {
typeSelect.value = '';
}
break;
}
this.handleFilterChange();
}
/**
* Clear all filters
*/
clearAllFilters() {
const tagFilter = document.getElementById('tagFilter');
const dateFilter = document.getElementById('dateFilter');
const typeFilter = document.getElementById('typeFilter');
if (tagFilter) tagFilter.value = '';
if (dateFilter) dateFilter.value = '';
if (typeFilter) typeFilter.value = '';
this.searchResults = [];
this.renderSearchResults([]);
this.updateResultsCount(0);
this.updateActiveFilters();
this.showToast('All filters cleared', 'info');
}
/**
* Handle live search toggle
*/
handleLiveSearchToggle(event) {
this.liveSearchEnabled = event.target.checked;
const modeText = document.getElementById('searchModeText');
if (modeText) {
modeText.textContent = this.liveSearchEnabled ? 'Live Search' : 'Manual Search';
}
// Show a toast to indicate the mode change
this.showToast(
`Search mode: ${this.liveSearchEnabled ? 'Live (searches as you type)' : 'Manual (click Search button)'}`,
'info'
);
}
/**
* Handle debounced filter changes for live search
*/
handleDebouncedFilterChange() {
// Clear any existing timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// Only trigger search if live search is enabled
if (this.liveSearchEnabled) {
this.debounceTimer = setTimeout(() => {
this.handleFilterChange();
}, 300); // 300ms debounce
}
}
/**
* Handle memory added via SSE
*/
handleMemoryAdded(memory) {
if (this.currentView === 'dashboard') {
this.loadDashboardData();
}
}
/**
* Handle memory deleted via SSE
*/
handleMemoryDeleted(memoryId) {
// Remove from current view
const cards = document.querySelectorAll(`[data-memory-id="${memoryId}"]`);
cards.forEach(card => card.remove());
// Update search results if in search view
if (this.currentView === 'search') {
this.searchResults = this.searchResults.filter(r => r.memory.content_hash !== memoryId);
this.updateResultsCount(this.searchResults.length);
}
}
/**
* Handle memory updated via SSE
*/
handleMemoryUpdated(memory) {
// Refresh relevant views
if (this.currentView === 'dashboard') {
this.loadDashboardData();
}
}
/**
* Update connection status indicator
*/
updateConnectionStatus(status) {
const statusElement = document.getElementById('connectionStatus');
if (statusElement) {
const indicator = statusElement.querySelector('.status-indicator');
const text = statusElement.querySelector('.status-text');
if (!indicator || !text) return;
// Reset indicator classes
indicator.className = 'status-indicator';
switch (status) {
case 'connected':
text.textContent = 'Connected';
// Connected uses default green color (no additional class needed)
break;
case 'connecting':
text.textContent = 'Connecting...';
indicator.classList.add('connecting');
break;
case 'disconnected':
text.textContent = 'Disconnected';
indicator.classList.add('disconnected');
break;
default:
text.textContent = 'Unknown';
indicator.classList.add('disconnected');
}
}
}
/**
* Generic API call wrapper
*/
async apiCall(endpoint, method = 'GET', data = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(`${this.apiBase}${endpoint}`, options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
return await response.json();
}
/**
* Modal management
*/
openModal(modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// Focus first input
const firstInput = modal.querySelector('input, textarea');
if (firstInput) {
firstInput.focus();
}
}
closeModal(modal) {
modal.classList.remove('active');
document.body.style.overflow = '';
}
/**
* Loading state management
*/
setLoading(loading) {
this.isLoading = loading;
const indicator = document.getElementById('loadingOverlay');
if (indicator) {
if (loading) {
indicator.classList.remove('hidden');
} else {
indicator.classList.add('hidden');
}
}
}
/**
* View document memory chunks
*/
async viewDocumentMemory(uploadId) {
try {
this.setLoading(true);
const response = await this.apiCall(`/documents/search-content/${uploadId}`);
if (response.status === 'success' || response.status === 'partial') {
// Show modal
const modal = document.getElementById('memoryViewerModal');
const filename = document.getElementById('memoryViewerFilename');
const stats = document.getElementById('memoryViewerStats');
const chunksList = document.getElementById('memoryChunksList');
filename.textContent = response.filename || 'Document';
stats.textContent = `${response.total_found} chunk${response.total_found !== 1 ? 's' : ''} found`;
// Render chunks
if (response.memories && response.memories.length > 0) {
const chunksHtml = response.memories.map((memory, index) => {
const chunkIndex = memory.chunk_index !== undefined ? memory.chunk_index : index;
const page = memory.page ? ` • Page ${memory.page}` : '';
const contentPreview = memory.content.length > 300
? memory.content.substring(0, 300) + '...'
: memory.content;
return `
<div class="memory-chunk-item">
<div class="chunk-header">
<span class="chunk-number">Chunk ${chunkIndex + 1}${page}</span>
<span class="chunk-hash" title="${memory.content_hash}">${memory.content_hash.substring(0, 12)}...</span>
</div>
<div class="chunk-content">${this.escapeHtml(contentPreview)}</div>
<div class="chunk-tags">
${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
</div>
</div>
`;
}).join('');
chunksList.innerHTML = chunksHtml;
} else {
chunksList.innerHTML = '<p class="text-muted">No memory chunks found for this document.</p>';
}
modal.style.display = 'flex';
if (response.status === 'partial') {
this.showToast(`Found ${response.total_found} chunks (partial results)`, 'warning');
}
} else {
this.showToast('Failed to load document memories', 'error');
}
} catch (error) {
console.error('Error viewing document memory:', error);
this.showToast('Error loading document memories', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Close memory viewer modal
*/
closeMemoryViewer() {
const modal = document.getElementById('memoryViewerModal');
if (modal) {
modal.style.display = 'none';
}
}
/**
* Remove document and its memories
*/
async removeDocument(uploadId, filename) {
console.log('removeDocument called with:', { uploadId, filename, currentView: this.currentView });
if (!confirm(`Remove "${filename}" and all its memory chunks?\n\nThis action cannot be undone.`)) {
console.log('User cancelled removal');
return;
}
try {
this.setLoading(true);
console.log('Making DELETE request to:', `/documents/remove/${uploadId}`);
const response = await this.apiCall(`/documents/remove/${uploadId}`, 'DELETE');
console.log('Delete response:', response);
if (response.status === 'success') {
this.showToast(`Removed "${filename}" (${response.memories_deleted} memories deleted)`, 'success');
// Refresh the current view (Dashboard or Documents tab)
console.log('Refreshing view:', this.currentView);
if (this.currentView === 'dashboard') {
await this.loadDashboardData();
} else if (this.currentView === 'documents') {
await this.loadUploadHistory();
}
} else {
console.error('Removal failed with response:', response);
this.showToast('Failed to remove document', 'error');
}
} catch (error) {
console.error('Error removing document:', error);
console.error('Error stack:', error.stack);
this.showToast('Error removing document', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Search document content
*/
async searchDocumentContent(query) {
try {
this.setLoading(true);
const resultsContainer = document.getElementById('docSearchResults');
const resultsList = document.getElementById('docSearchResultsList');
const resultsCount = document.getElementById('docSearchCount');
// Use the regular search endpoint but filter for document memories
// Higher n_results to ensure we get enough document results after filtering
const response = await this.apiCall('/search', 'POST', {
query: query,
n_results: 100
});
if (response.results) {
// Filter results to only show document-type memories
const documentResults = response.results.filter(r =>
r.memory?.memory_type === 'document' || (r.memory?.tags && r.memory.tags.some(tag => tag.startsWith('upload_id:')))
);
// Limit display to top 20 most relevant document results
const displayResults = documentResults.slice(0, 20);
resultsCount.textContent = `${documentResults.length} result${documentResults.length !== 1 ? 's' : ''}${documentResults.length > 20 ? ' (showing top 20)' : ''}`;
if (displayResults.length > 0) {
const resultsHtml = displayResults.map(result => {
const mem = result.memory;
const uploadIdTag = mem.tags?.find(tag => tag.startsWith('upload_id:'));
const sourceFile = mem.metadata?.source_file || 'Unknown file';
const contentPreview = mem.content.length > 200
? mem.content.substring(0, 200) + '...'
: mem.content;
return `
<div class="search-result-item">
<div class="result-header">
<strong>${this.escapeHtml(sourceFile)}</strong>
<span class="similarity-score">${Math.round((result.similarity_score || 0) * 100)}% match</span>
</div>
<div class="result-content">${this.escapeHtml(contentPreview)}</div>
<div class="result-tags">
${(mem.tags || []).slice(0, 5).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
</div>
</div>
`;
}).join('');
resultsList.innerHTML = resultsHtml;
} else {
resultsList.innerHTML = '<p class="text-muted">No matching document content found. Try different search terms.</p>';
}
resultsContainer.style.display = 'block';
} else {
this.showToast('Search failed', 'error');
}
} catch (error) {
console.error('Error searching documents:', error);
this.showToast('Error performing search', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Toast notification system
*/
showToast(message, type = 'info', duration = 5000) {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
// Auto-remove after duration
setTimeout(() => {
toast.remove();
}, duration);
// Click to remove
toast.addEventListener('click', () => {
toast.remove();
});
}
/**
* Utility: Debounce function
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Load settings from localStorage
*/
loadSettings() {
try {
const saved = localStorage.getItem('memoryDashboardSettings');
if (saved) {
this.settings = { ...this.settings, ...JSON.parse(saved) };
}
} catch (error) {
console.warn('Failed to load settings:', error);
}
}
/**
* Save settings to localStorage
*/
saveSettingsToStorage() {
try {
localStorage.setItem('memoryDashboardSettings', JSON.stringify(this.settings));
} catch (error) {
console.error('Failed to save settings:', error);
this.showToast('Failed to save settings. Your preferences will not be persisted.', 'error');
}
}
/**
* Apply theme to the page
*/
applyTheme(theme = this.settings.theme) {
const isDark = theme === 'dark';
document.body.classList.toggle('dark-mode', isDark);
// Toggle icon visibility using CSS classes
const sunIcon = document.getElementById('sunIcon');
const moonIcon = document.getElementById('moonIcon');
if (sunIcon && moonIcon) {
sunIcon.classList.toggle('hidden', isDark);
moonIcon.classList.toggle('hidden', !isDark);
}
}
/**
* Toggle between light and dark theme
*/
toggleTheme() {
const newTheme = this.settings.theme === 'dark' ? 'light' : 'dark';
this.settings.theme = newTheme;
this.applyTheme(newTheme);
this.saveSettingsToStorage();
this.showToast(`Switched to ${newTheme} mode`, 'success');
}
/**
* Open settings modal
*/
async openSettingsModal() {
const modal = document.getElementById('settingsModal');
// Populate form with current settings
document.getElementById('themeSelect').value = this.settings.theme;
document.getElementById('viewDensity').value = this.settings.viewDensity;
document.getElementById('previewLines').value = this.settings.previewLines;
// Reset system info to loading state
this.resetSystemInfoLoadingState();
// Load system information and backup status
await Promise.all([
this.loadSystemInfo(),
this.checkBackupStatus()
]);
this.openModal(modal);
}
/**
* Reset system info fields to loading state
*/
resetSystemInfoLoadingState() {
Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
const element = document.getElementById(id);
if (element) {
element.textContent = 'Loading...';
}
});
}
/**
* Load system information for settings modal
*/
async loadSystemInfo() {
try {
// Use Promise.allSettled for robust error handling
const [healthResult, detailedHealthResult] = await Promise.allSettled([
this.apiCall('/health'),
this.apiCall('/health/detailed')
]);
const apiData = {
health: healthResult.status === 'fulfilled' ? healthResult.value : null,
detailedHealth: detailedHealthResult.status === 'fulfilled' ? detailedHealthResult.value : null
};
// Update fields using configuration
Object.entries(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(([fieldId, config]) => {
const element = document.getElementById(fieldId);
if (!element) return;
let value = null;
for (const source of config.sources) {
const apiResponse = apiData[source.api];
if (apiResponse) {
value = this.getNestedValue(apiResponse, source.path);
if (value !== undefined && value !== null) break;
}
}
element.textContent = config.formatter(value);
});
// Log warnings for failed API calls
if (healthResult.status === 'rejected') {
console.warn('Failed to load health endpoint:', healthResult.reason);
}
if (detailedHealthResult.status === 'rejected') {
console.warn('Failed to load detailed health endpoint:', detailedHealthResult.reason);
}
} catch (error) {
console.error('Unexpected error loading system info:', error);
// Set all system info fields that are still in loading state to error
Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
const element = document.getElementById(id);
if (element && element.textContent === 'Loading...') {
element.textContent = 'Error';
}
});
}
}
/**
* Get nested object value by path string
* @param {Object} obj - Object to traverse
* @param {string} path - Dot-separated path (e.g., 'storage.primary_stats.embedding_model')
* @returns {*} Value at path or undefined
*/
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
/**
* Format uptime seconds into human readable string
* @param {number} seconds - Uptime in seconds
* @returns {string} Formatted uptime string
*/
static formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
return parts.length > 0 ? parts.join(' ') : '< 1m';
}
/**
* Save settings from modal
*/
saveSettings() {
// Get values from form
const theme = document.getElementById('themeSelect').value;
const viewDensity = document.getElementById('viewDensity').value;
const previewLines = parseInt(document.getElementById('previewLines').value, 10);
// Update settings
this.settings.theme = theme;
this.settings.viewDensity = viewDensity;
this.settings.previewLines = previewLines;
// Apply changes
this.applyTheme(theme);
this.saveSettingsToStorage();
// Close modal and show confirmation
this.closeModal(document.getElementById('settingsModal'));
this.showToast('Settings saved successfully', 'success');
}
// ===== MANAGE TAB METHODS =====
/**
* Load manage tab data
*/
async loadManageData() {
try {
// Load tag statistics for bulk operations
await this.loadTagSelectOptions();
await this.loadTagManagementStats();
} catch (error) {
console.error('Failed to load manage data:', error);
this.showToast('Failed to load management data', 'error');
}
}
/**
* Load tag options for bulk delete select
*/
async loadTagSelectOptions() {
try {
const response = await fetch(`${this.apiBase}/manage/tags/stats`);
if (!response.ok) throw new Error('Failed to load tags');
const data = await response.json();
const select = document.getElementById('deleteTagSelect');
if (!select) return;
// Clear existing options except the first
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
// Add tag options
data.tags.forEach(tagStat => {
const option = document.createElement('option');
option.value = tagStat.tag;
option.textContent = `${tagStat.tag} (${tagStat.count} memories)`;
option.dataset.count = tagStat.count; // Store count in data attribute
select.appendChild(option);
});
} catch (error) {
console.error('Failed to load tag options:', error);
}
}
/**
* Load tag management statistics
*/
async loadTagManagementStats() {
const container = document.getElementById('tagManagementContainer');
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/manage/tags/stats`);
if (!response.ok) throw new Error('Failed to load tag stats');
const data = await response.json();
this.renderTagManagementTable(data);
} catch (error) {
console.error('Failed to load tag management stats:', error);
container.innerHTML = '<p class="error">Failed to load tag statistics</p>';
}
}
/**
* Render tag management table
*/
renderTagManagementTable(data) {
const container = document.getElementById('tagManagementContainer');
if (!container) return;
let html = '<table class="tag-stats-table">';
html += '<thead><tr>';
html += '<th>Tag</th>';
html += '<th>Count</th>';
html += '<th>Actions</th>';
html += '</tr></thead><tbody>';
data.tags.forEach(tagStat => {
html += '<tr>';
html += `<td class="tag-name">${tagStat.tag}</td>`;
html += `<td class="tag-count">${tagStat.count}</td>`;
html += '<td class="tag-actions">';
html += `<button class="tag-action-btn" data-action="rename-tag" data-tag="${this.escapeHtml(tagStat.tag)}">Rename</button>`;
html += `<button class="tag-action-btn danger" data-action="delete-tag" data-tag="${this.escapeHtml(tagStat.tag)}" data-count="${tagStat.count}">Delete</button>`;
html += '</td></tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
// Add event listeners for tag actions
container.addEventListener('click', (e) => {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.dataset.action;
const tag = button.dataset.tag;
if (action === 'rename-tag') {
this.renameTag(tag);
} else if (action === 'delete-tag') {
const count = parseInt(button.dataset.count, 10);
this.deleteTag(tag, count);
}
});
}
/**
* Handle bulk delete by tag
*/
async handleBulkDeleteByTag() {
const select = document.getElementById('deleteTagSelect');
const tag = select.value;
if (!tag) {
this.showToast('Please select a tag to delete', 'warning');
return;
}
// Extract count from data attribute
const option = select.querySelector(`option[value="${tag}"]`);
const count = parseInt(option.dataset.count, 10) || 0;
if (!await this.confirmBulkOperation(`Delete ${count} memories with tag "${tag}"?`)) {
return;
}
this.setLoading(true);
try {
const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tag: tag,
confirm_count: count
})
});
const result = await response.json();
if (result.success) {
this.showToast(result.message, 'success');
await this.loadManageData(); // Refresh data
await this.loadDashboardData(); // Refresh dashboard stats
} else {
this.showToast(result.message, 'error');
}
} catch (error) {
console.error('Bulk delete failed:', error);
this.showToast('Bulk delete operation failed', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Handle cleanup duplicates
*/
async handleCleanupDuplicates() {
if (!await this.confirmBulkOperation('Remove all duplicate memories?')) {
return;
}
this.setLoading(true);
try {
const response = await fetch(`${this.apiBase}/manage/cleanup-duplicates`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
this.showToast(result.message, 'success');
await this.loadManageData();
await this.loadDashboardData();
} else {
this.showToast(result.message, 'error');
}
} catch (error) {
console.error('Cleanup duplicates failed:', error);
this.showToast('Cleanup operation failed', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Handle bulk delete by date
*/
async handleBulkDeleteByDate() {
const dateInput = document.getElementById('deleteDateInput');
const date = dateInput.value;
if (!date) {
this.showToast('Please select a date', 'warning');
return;
}
if (!await this.confirmBulkOperation(`Delete all memories before ${date}?`)) {
return;
}
this.setLoading(true);
try {
const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
before_date: date
})
});
const result = await response.json();
if (result.success) {
this.showToast(result.message, 'success');
await this.loadManageData();
await this.loadDashboardData();
} else {
this.showToast(result.message, 'error');
}
} catch (error) {
console.error('Bulk delete by date failed:', error);
this.showToast('Bulk delete operation failed', 'error');
} finally {
this.setLoading(false);
}
}
/**
* Handle database optimization
*/
async handleOptimizeDatabase() {
this.showToast('Database optimization not yet implemented', 'warning');
}
/**
* Handle index rebuild
*/
async handleRebuildIndex() {
this.showToast('Index rebuild not yet implemented', 'warning');
}
/**
* Rename a tag
*/
async renameTag(oldTag) {
const newTag = prompt(`Rename tag "${oldTag}" to:`, oldTag);
if (!newTag || newTag === oldTag) return;
this.showToast('Tag renaming not yet implemented', 'warning');
}
/**
* Delete a tag
*/
async deleteTag(tag, count) {
if (!await this.confirmBulkOperation(`Delete tag "${tag}" from ${count} memories?`)) {
return;
}
this.showToast('Tag deletion not yet implemented', 'warning');
}
// ===== ANALYTICS TAB METHODS =====
/**
* Load analytics tab data
*/
async loadAnalyticsData() {
try {
await Promise.all([
this.loadAnalyticsOverview(),
this.loadMemoryGrowthChart(),
this.loadTagUsageChart(),
this.loadMemoryTypesChart(),
this.loadActivityHeatmapChart(),
this.loadTopTagsReport(),
this.loadRecentActivityReport(),
this.loadStorageReport()
]);
} catch (error) {
console.error('Failed to load analytics data:', error);
this.showToast('Failed to load analytics data', 'error');
}
}
/**
* Load analytics overview metrics
*/
async loadAnalyticsOverview() {
try {
const response = await fetch(`${this.apiBase}/analytics/overview`);
if (!response.ok) throw new Error('Failed to load overview');
const data = await response.json();
// Update metric cards
this.updateElementText('analyticsTotalMemories', data.total_memories || 0);
this.updateElementText('analyticsThisWeek', data.memories_this_week || 0);
this.updateElementText('analyticsUniqueTags', data.unique_tags || 0);
this.updateElementText('analyticsDbSize', data.database_size_mb ?
`${data.database_size_mb.toFixed(1)} MB` : 'N/A');
} catch (error) {
console.error('Failed to load analytics overview:', error);
}
}
/**
* Load memory growth chart
*/
async loadMemoryGrowthChart() {
const container = document.getElementById('memoryGrowthChart');
const period = document.getElementById('growthPeriodSelect').value;
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/memory-growth?period=${period}`);
if (!response.ok) throw new Error('Failed to load growth data');
const data = await response.json();
this.renderMemoryGrowthChart(container, data);
} catch (error) {
console.error('Failed to load memory growth:', error);
container.innerHTML = '<p class="error">Failed to load growth chart</p>';
}
}
/**
* Render memory growth chart
*/
renderMemoryGrowthChart(container, data) {
if (!data.data_points || data.data_points.length === 0) {
container.innerHTML = '<p>No growth data available</p>';
return;
}
// Find max count for scaling
const recentPoints = data.data_points.slice(-10);
const maxCount = Math.max(...recentPoints.map(p => p.count), 1);
let html = '<div class="simple-chart">';
recentPoints.forEach(point => {
// Normalize bar width relative to max, then convert to pixels (200px scale)
const barWidthPx = (point.count / maxCount) * 200;
const displayCount = point.count || 0;
const displayCumulative = point.cumulative || 0;
// Use label if available, otherwise fall back to date for backward compatibility
const displayLabel = point.label || point.date;
html += `<div class="chart-row">
<div class="chart-bar" style="width: ${barWidthPx}px"></div>
<span class="chart-value">+${displayCount} <small>(${displayCumulative} total)</small></span>
<span class="chart-label">${displayLabel}</span>
</div>`;
});
html += '</div>';
container.innerHTML = html;
}
/**
* Load tag usage chart
*/
async loadTagUsageChart() {
const container = document.getElementById('tagUsageChart');
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/tag-usage`);
if (!response.ok) throw new Error('Failed to load tag usage');
const data = await response.json();
this.renderTagUsageChart(container, data);
} catch (error) {
console.error('Failed to load tag usage:', error);
container.innerHTML = '<p class="error">Failed to load tag usage chart</p>';
}
}
/**
* Render tag usage chart
*/
renderTagUsageChart(container, data) {
if (!data.tags || data.tags.length === 0) {
container.innerHTML = '<p>No tags found</p>';
return;
}
// Filter tags with >10 memories, aggregate the rest
const significantTags = data.tags.filter(t => t.count > 10);
const minorTags = data.tags.filter(t => t.count <= 10);
let html = '<div class="simple-chart">';
// Render significant tags
significantTags.forEach(tag => {
const barWidthPx = (tag.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
html += `<div class="chart-row">
<div class="chart-bar" style="width: ${barWidthPx}px"></div>
<span class="chart-value">${tag.count} (${tag.percentage}%)</span>
<span class="chart-label">${tag.tag}</span>
</div>`;
});
// Add "diverse" category if there are minor tags
if (minorTags.length > 0) {
const diverseCount = minorTags.reduce((sum, t) => sum + t.count, 0);
const diversePercentage = minorTags.reduce((sum, t) => sum + t.percentage, 0);
const barWidthPx = (diversePercentage / 100) * 200;
html += `<div class="chart-row">
<div class="chart-bar" style="width: ${barWidthPx}px"></div>
<span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
<span class="chart-label" title="${minorTags.length} tags with ≤10 memories each">diverse (${minorTags.length} tags)</span>
</div>`;
}
html += '</div>';
container.innerHTML = html;
}
/**
* Load memory types chart
*/
async loadMemoryTypesChart() {
const container = document.getElementById('memoryTypesChart');
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/memory-types`);
if (!response.ok) throw new Error('Failed to load memory types');
const data = await response.json();
this.renderMemoryTypesChart(container, data);
} catch (error) {
console.error('Failed to load memory types:', error);
container.innerHTML = '<p class="error">Failed to load memory types chart</p>';
}
}
/**
* Render memory types chart
*/
renderMemoryTypesChart(container, data) {
if (!data.types || data.types.length === 0) {
container.innerHTML = '<p>No memory types found</p>';
return;
}
// Filter types with >10 memories, aggregate the rest
const significantTypes = data.types.filter(t => t.count > 10);
const minorTypes = data.types.filter(t => t.count <= 10);
let html = '<div class="simple-chart">';
// Render significant types
significantTypes.forEach(type => {
const barWidthPx = (type.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
const typeName = type.memory_type || 'untyped';
html += `<div class="chart-row">
<div class="chart-bar" style="width: ${barWidthPx}px"></div>
<span class="chart-value">${type.count} (${type.percentage.toFixed(1)}%)</span>
<span class="chart-label" title="${typeName}">${typeName}</span>
</div>`;
});
// Add "diverse" category if there are minor types
if (minorTypes.length > 0) {
const diverseCount = minorTypes.reduce((sum, t) => sum + t.count, 0);
const diversePercentage = minorTypes.reduce((sum, t) => sum + t.percentage, 0);
const barWidthPx = (diversePercentage / 100) * 200;
html += `<div class="chart-row">
<div class="chart-bar" style="width: ${barWidthPx}px"></div>
<span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
<span class="chart-label" title="${minorTypes.length} types with ≤10 memories each">diverse (${minorTypes.length} types)</span>
</div>`;
}
html += '</div>';
container.innerHTML = html;
}
/**
* Load top tags report
*/
async loadTopTagsReport() {
const container = document.getElementById('topTagsList');
const period = document.getElementById('topTagsPeriodSelect')?.value || '30d';
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/top-tags?period=${period}`);
if (!response.ok) throw new Error('Failed to load top tags');
const data = await response.json();
this.renderTopTagsReport(container, data);
} catch (error) {
console.error('Failed to load top tags:', error);
container.innerHTML = '<p class="error">Failed to load top tags</p>';
}
}
/**
* Render top tags report
*/
renderTopTagsReport(container, data) {
if (!data.tags || data.tags.length === 0) {
container.innerHTML = '<p>No tags found</p>';
return;
}
let html = '<div class="enhanced-tags-report">';
html += `<div class="report-period">Period: ${data.period}</div>`;
html += '<ul class="tags-list">';
data.tags.slice(0, 10).forEach(tag => {
const trendIcon = tag.trending ? '📈' : '';
const growthText = tag.growth_rate !== null ? ` (${tag.growth_rate > 0 ? '+' : ''}${tag.growth_rate}%)` : '';
html += `<li>
<div class="tag-header">
<strong>${tag.tag}</strong>${trendIcon}
<span class="tag-count">${tag.count} memories (${tag.percentage}%)${growthText}</span>
</div>`;
if (tag.co_occurring_tags && tag.co_occurring_tags.length > 0) {
html += '<div class="tag-cooccurrence">Often with: ';
html += tag.co_occurring_tags.slice(0, 3).map(co => `${co.tag} (${co.strength.toFixed(2)})`).join(', ');
html += '</div>';
}
html += '</li>';
});
html += '</ul></div>';
container.innerHTML = html;
}
/**
* Load recent activity report
*/
async loadRecentActivityReport() {
const container = document.getElementById('recentActivityList');
const granularity = document.getElementById('activityGranularitySelect')?.value || 'daily';
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/activity-breakdown?granularity=${granularity}`);
if (!response.ok) throw new Error('Failed to load activity breakdown');
const data = await response.json();
this.renderRecentActivityReport(container, data);
} catch (error) {
console.error('Failed to load recent activity:', error);
container.innerHTML = '<p class="error">Failed to load recent activity</p>';
}
}
/**
* Render recent activity report
*/
renderRecentActivityReport(container, data) {
let html = '<div class="activity-breakdown">';
// Summary stats
html += '<div class="activity-summary">';
html += `<div class="activity-stat"><strong>Active Days:</strong> ${data.active_days}/${data.total_days}</div>`;
html += `<div class="activity-stat"><strong>Current Streak:</strong> ${data.current_streak} days</div>`;
html += `<div class="activity-stat"><strong>Longest Streak:</strong> ${data.longest_streak} days</div>`;
html += '</div>';
if (data.peak_times && data.peak_times.length > 0) {
html += '<div class="peak-times">';
html += '<strong>Peak Times:</strong> ';
html += data.peak_times.join(', ');
html += '</div>';
}
// Activity breakdown chart
if (data.breakdown && data.breakdown.length > 0) {
html += '<div class="activity-chart">';
// Calculate total count for percentage-based distribution
const totalCount = data.breakdown.reduce((sum, d) => sum + d.count, 0);
const maxCount = Math.max(...data.breakdown.map(d => d.count));
data.breakdown.forEach(item => {
// Show percentage of total activity, with minimum width for visibility
const percentage = totalCount > 0 ? (item.count / totalCount * 100) : 0;
// Use percentage for bar width, but scale up for better visualization
// Use max count for scaling to ensure largest bar reaches reasonable width
const barWidth = totalCount > 0 ? (item.count / maxCount * 100) : 0;
html += `<div class="activity-bar-row">
<span class="activity-label">${item.label}</span>
<div class="activity-bar" style="width: ${barWidth}%" title="${item.count} memories (${percentage.toFixed(1)}%)"></div>
<span class="activity-count">${item.count} (${percentage.toFixed(1)}%)</span>
</div>`;
});
html += '</div>';
}
html += '</div>';
container.innerHTML = html;
}
/**
* Load activity heatmap chart
*/
async loadActivityHeatmapChart() {
const container = document.getElementById('activityHeatmapChart');
const period = document.getElementById('heatmapPeriodSelect').value;
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/activity-heatmap?days=${period}`);
if (!response.ok) throw new Error('Failed to load heatmap data');
const data = await response.json();
this.renderActivityHeatmapChart(container, data);
} catch (error) {
console.error('Failed to load activity heatmap:', error);
container.innerHTML = '<p class="error">Failed to load activity heatmap</p>';
}
}
/**
* Render activity heatmap chart
*/
renderActivityHeatmapChart(container, data) {
if (!data.data || data.data.length === 0) {
container.innerHTML = '<p>No activity data available</p>';
return;
}
// Create calendar grid
let html = '<div class="activity-heatmap">';
html += '<div class="heatmap-stats">';
html += `<span>${data.total_days} active days</span>`;
html += `<span>Max: ${data.max_count} memories/day</span>`;
html += '</div>';
// Group by months
const months = {};
data.data.forEach(day => {
const date = new Date(day.date);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!months[monthKey]) {
months[monthKey] = [];
}
months[monthKey].push(day);
});
// Render each month
Object.keys(months).sort().reverse().forEach(monthKey => {
const [year, month] = monthKey.split('-');
const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'short' });
html += `<div class="heatmap-month">`;
html += `<div class="month-label">${monthName} ${year}</div>`;
html += '<div class="month-grid">';
// Create 7x6 grid (weeks x days)
const monthData = months[monthKey];
// Parse date in local timezone to avoid day-of-week shifting near timezone boundaries
const [fYear, fMonth, fDay] = monthData[0].date.split('-').map(Number);
const firstDay = new Date(fYear, fMonth - 1, fDay).getDay();
// Add empty cells for days before month starts
for (let i = 0; i < firstDay; i++) {
html += '<div class="heatmap-cell empty"></div>';
}
// Add cells for each day
monthData.forEach(day => {
const level = day.level;
const tooltip = `${day.date}: ${day.count} memories`;
html += `<div class="heatmap-cell level-${level}" title="${tooltip}"></div>`;
});
html += '</div></div>';
});
// Legend
html += '<div class="heatmap-legend">';
html += '<span>Less</span>';
for (let i = 0; i <= 4; i++) {
html += `<div class="legend-cell level-${i}"></div>`;
}
html += '<span>More</span>';
html += '</div>';
html += '</div>';
container.innerHTML = html;
}
/**
* Handle heatmap period change
*/
async handleHeatmapPeriodChange() {
await this.loadActivityHeatmapChart();
}
/**
* Handle top tags period change
*/
async handleTopTagsPeriodChange() {
await this.loadTopTagsReport();
}
/**
* Handle activity granularity change
*/
async handleActivityGranularityChange() {
await this.loadRecentActivityReport();
}
/**
* Load storage report
*/
async loadStorageReport() {
const container = document.getElementById('storageReport');
if (!container) return;
try {
const response = await fetch(`${this.apiBase}/analytics/storage-stats`);
if (!response.ok) throw new Error('Failed to load storage stats');
const data = await response.json();
this.renderStorageReport(container, data);
} catch (error) {
console.error('Failed to load storage report:', error);
container.innerHTML = '<p class="error">Failed to load storage report</p>';
}
}
/**
* Render storage report
*/
renderStorageReport(container, data) {
let html = '<div class="storage-report">';
// Summary stats
html += '<div class="storage-summary">';
html += `<div class="storage-stat"><strong>Total Size:</strong> ${data.total_size_mb} MB</div>`;
html += `<div class="storage-stat"><strong>Average Memory:</strong> ${data.average_memory_size} chars</div>`;
html += `<div class="storage-stat"><strong>Efficiency:</strong> ${data.storage_efficiency}%</div>`;
html += '</div>';
// Largest memories
if (data.largest_memories && data.largest_memories.length > 0) {
html += '<h4>Largest Memories</h4>';
html += '<ul class="largest-memories">';
data.largest_memories.slice(0, 5).forEach(memory => {
// Backend provides created_at as ISO string, not timestamp
const date = memory.created_at ? new Date(memory.created_at).toLocaleDateString() : 'Unknown';
// Backend provides size_kb and preview (not size and content_preview)
const sizeDisplay = memory.size_kb ? `${memory.size_kb} KB` : `${memory.size_bytes || 0} bytes`;
html += `<li>
<div class="memory-size">${sizeDisplay}</div>
<div class="memory-preview">${this.escapeHtml(memory.preview || '')}</div>
<div class="memory-meta">${date} • Tags: ${memory.tags.join(', ') || 'none'}</div>
</li>`;
});
html += '</ul>';
}
html += '</div>';
container.innerHTML = html;
}
/**
* Handle growth period change
*/
async handleGrowthPeriodChange() {
await this.loadMemoryGrowthChart();
}
// ===== UTILITY METHODS =====
/**
* Show confirmation dialog for bulk operations
*/
async confirmBulkOperation(message) {
return confirm(`⚠️ WARNING: ${message}
This action cannot be undone. Are you sure?`);
}
/**
* Update element text content
*/
updateElementText(elementId, text) {
const element = document.getElementById(elementId);
if (element) {
element.textContent = text;
}
}
/**
* Cleanup when page unloads
*/
destroy() {
if (this.eventSource) {
this.eventSource.close();
}
}
}
// Initialize the application when DOM is ready
console.log('⚡ Registering DOMContentLoaded listener');
document.addEventListener('DOMContentLoaded', () => {
console.log('⚡ DOMContentLoaded fired - Creating MemoryDashboard instance');
window.app = new MemoryDashboard();
console.log('⚡ MemoryDashboard created, window.app =', window.app);
});
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (window.app) {
window.app.destroy();
}
});
```