#
tokens: 61208/50000 1/772 files (page 61/62)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 61 of 62. Use http://codebase.md/doobidoo/mcp-memory-service?lines=true&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
│   ├── commands
│   │   ├── README.md
│   │   ├── refactor-function
│   │   ├── refactor-function-prod
│   │   └── refactor-function.md
│   ├── consolidation-fix-handoff.md
│   ├── consolidation-hang-fix-summary.md
│   ├── directives
│   │   ├── agents.md
│   │   ├── code-quality-workflow.md
│   │   ├── consolidation-details.md
│   │   ├── development-setup.md
│   │   ├── hooks-configuration.md
│   │   ├── memory-first.md
│   │   ├── memory-tagging.md
│   │   ├── pr-workflow.md
│   │   ├── quality-system-details.md
│   │   ├── README.md
│   │   ├── refactoring-checklist.md
│   │   ├── storage-backends.md
│   │   └── version-management.md
│   ├── prompts
│   │   └── hybrid-cleanup-integration.md
│   ├── settings.local.json.backup
│   └── settings.local.json.local
├── .commit-message
├── .coveragerc
├── .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-branch-automation.yml
│       ├── claude-code-review.yml
│       ├── claude.yml
│       ├── cleanup-images.yml.disabled
│       ├── dev-setup-validation.yml
│       ├── docker-publish.yml
│       ├── dockerfile-lint.yml
│       ├── LATEST_FIXES.md
│       ├── main-optimized.yml.disabled
│       ├── main.yml
│       ├── publish-and-test.yml
│       ├── publish-dual.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
├── .metrics
│   ├── baseline_cc_install_hooks.txt
│   ├── baseline_mi_install_hooks.txt
│   ├── baseline_nesting_install_hooks.txt
│   ├── BASELINE_REPORT.md
│   ├── COMPLEXITY_COMPARISON.txt
│   ├── QUICK_REFERENCE.txt
│   ├── README.md
│   ├── REFACTORED_BASELINE.md
│   ├── REFACTORING_COMPLETION_REPORT.md
│   └── TRACKING_TABLE.md
├── .pyscn
│   ├── .gitignore
│   └── reports
│       └── analyze_20251123_214224.html
├── AGENTS.md
├── ai-optimized-tool-descriptions.py
├── 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
│   │   ├── auto-capture-hook.js
│   │   ├── auto-capture-hook.ps1
│   │   ├── memory-retrieval.js
│   │   ├── mid-conversation.js
│   │   ├── permission-request.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-AUTO-CAPTURE.md
│   ├── README-NATURAL-TRIGGERS.md
│   ├── README-PERMISSION-REQUEST.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-permission-request.js
│   │   ├── test-session-tracking.json
│   │   └── test-threading.json
│   ├── utilities
│   │   ├── adaptive-pattern-detector.js
│   │   ├── auto-capture-patterns.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-cache.json
│   │   ├── session-tracker.js
│   │   ├── tiered-conversation-monitor.js
│   │   ├── user-override-detector.js
│   │   └── version-checker.js
│   └── WINDOWS-SESSIONSTART-BUG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── COMMIT_MESSAGE.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
│   │   ├── graph-database-design.md
│   │   ├── 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
│   ├── demo-recording-script.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-280-post-mortem.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
│   │   ├── quality-system-configs.md
│   │   └── tag-schema.json
│   ├── features
│   │   └── association-quality-boost.md
│   ├── 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
│   │   ├── memory-quality-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
│   │   └── update-restart-demo.png
│   ├── 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
│   ├── LIGHTWEIGHT_ONNX_SETUP.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
│   │   └── graph-migration-guide.md
│   ├── natural-memory-triggers
│   │   ├── cli-reference.md
│   │   ├── installation-guide.md
│   │   └── performance-optimization.md
│   ├── oauth-setup.md
│   ├── pr-graphql-integration.md
│   ├── quality-system-ui-implementation.md
│   ├── quick-setup-cloudflare-dual-environment.md
│   ├── README.md
│   ├── refactoring
│   │   └── phase-3-3-analysis.md
│   ├── releases
│   │   └── v8.72.0-testing.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
│   │   ├── database-transfer-migration.md
│   │   ├── general.md
│   │   ├── hooks-quick-reference.md
│   │   ├── memory-management.md
│   │   ├── pr162-schema-caching-issue.md
│   │   ├── session-end-hooks.md
│   │   └── sync-issues.md
│   ├── tutorials
│   │   ├── advanced-techniques.md
│   │   ├── data-analysis.md
│   │   └── demo-session-walkthrough.md
│   ├── wiki-documentation-plan.md
│   └── wiki-Graph-Database-Architecture.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
├── IMPLEMENTATION_SUMMARY.md
├── install_service.py
├── install.py
├── LICENSE
├── NOTICE
├── PR_DESCRIPTION.md
├── pyproject-lite.toml
├── pyproject.toml
├── pytest.ini
├── README.md
├── release-notes-v8.61.0.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
│   ├── ci
│   │   ├── check_dockerfile_args.sh
│   │   └── validate_imports.sh
│   ├── 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
│   │   ├── add_project_tags.py
│   │   ├── apply_quality_boost_retroactively.py
│   │   ├── assign_memory_types.py
│   │   ├── auto_retag_memory_merge.py
│   │   ├── auto_retag_memory.py
│   │   ├── backfill_graph_table.py
│   │   ├── check_memory_types.py
│   │   ├── cleanup_association_memories_hybrid.py
│   │   ├── cleanup_association_memories.py
│   │   ├── cleanup_corrupted_encoding.py
│   │   ├── cleanup_low_quality.py
│   │   ├── cleanup_memories.py
│   │   ├── cleanup_organize.py
│   │   ├── consolidate_memory_types.py
│   │   ├── consolidation_mappings.json
│   │   ├── delete_orphaned_vectors_fixed.py
│   │   ├── delete_test_memories.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
│   │   ├── retag_valuable_memories.py
│   │   ├── scan_todos.sh
│   │   ├── soft_delete_test_memories.py
│   │   └── sync_status.py
│   ├── 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
│   │   ├── pre_pr_check.sh
│   │   ├── quality_gate.sh
│   │   ├── resolve_threads.sh
│   │   ├── run_pyscn_analysis.sh
│   │   ├── run_quality_checks_on_files.sh
│   │   ├── run_quality_checks.sh
│   │   ├── thread_status.sh
│   │   └── watch_reviews.sh
│   ├── quality
│   │   ├── bulk_evaluate_onnx.py
│   │   ├── check_test_scores.py
│   │   ├── debug_deberta_scoring.py
│   │   ├── export_deberta_onnx.py
│   │   ├── fix_dead_code_install.sh
│   │   ├── migrate_to_deberta.py
│   │   ├── phase1_dead_code_analysis.md
│   │   ├── phase2_complexity_analysis.md
│   │   ├── README_PHASE1.md
│   │   ├── README_PHASE2.md
│   │   ├── rescore_deberta.py
│   │   ├── rescore_fallback.py
│   │   ├── reset_onnx_scores.py
│   │   ├── track_pyscn_metrics.sh
│   │   └── weekly_quality_review.sh
│   ├── README.md
│   ├── run
│   │   ├── memory_wrapper_cleanup.ps1
│   │   ├── memory_wrapper_cleanup.py
│   │   ├── memory_wrapper_cleanup.sh
│   │   ├── README_CLEANUP_WRAPPER.md
│   │   ├── 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
│   │   ├── http_server_manager.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
│   │   └── windows
│   │       ├── add_watchdog_trigger.ps1
│   │       ├── install_scheduled_task.ps1
│   │       ├── manage_service.ps1
│   │       ├── run_http_server_background.ps1
│   │       ├── uninstall_scheduled_task.ps1
│   │       └── update_and_restart.ps1
│   ├── setup-lightweight.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
│   ├── update_and_restart.sh
│   ├── utils
│   │   ├── claude_commands_utils.py
│   │   ├── detect_platform.py
│   │   ├── generate_personalized_claude_md.sh
│   │   ├── groq
│   │   ├── groq_agent_bridge.py
│   │   ├── list-collections.py
│   │   ├── memory_wrapper_uv.py
│   │   ├── query_memories.py
│   │   ├── README_detect_platform.md
│   │   ├── smithery_wrapper.py
│   │   ├── test_groq_bridge.sh
│   │   └── uv_wrapper.py
│   └── validation
│       ├── check_dev_setup.py
│       ├── check_documentation_links.py
│       ├── check_handler_coverage.py
│       ├── diagnose_backend_config.py
│       ├── validate_configuration_complete.py
│       ├── validate_graph_tools.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
│       ├── _version.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
│       ├── quality
│       │   ├── __init__.py
│       │   ├── ai_evaluator.py
│       │   ├── async_scorer.py
│       │   ├── config.py
│       │   ├── implicit_signals.py
│       │   ├── metadata_codec.py
│       │   ├── onnx_ranker.py
│       │   └── scorer.py
│       ├── server
│       │   ├── __init__.py
│       │   ├── __main__.py
│       │   ├── cache_manager.py
│       │   ├── client_detection.py
│       │   ├── environment.py
│       │   ├── handlers
│       │   │   ├── __init__.py
│       │   │   ├── consolidation.py
│       │   │   ├── documents.py
│       │   │   ├── graph.py
│       │   │   ├── memory.py
│       │   │   ├── quality.py
│       │   │   └── utility.py
│       │   └── logging_config.py
│       ├── server_impl.py
│       ├── services
│       │   ├── __init__.py
│       │   └── memory_service.py
│       ├── storage
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── cloudflare.py
│       │   ├── factory.py
│       │   ├── graph.py
│       │   ├── http_client.py
│       │   ├── hybrid.py
│       │   ├── migrations
│       │   │   └── 008_add_graph_table.sql
│       │   └── 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
│       │   ├── directory_ingestion.py
│       │   ├── document_processing.py
│       │   ├── gpu_detection.py
│       │   ├── hashing.py
│       │   ├── health_check.py
│       │   ├── http_server_manager.py
│       │   ├── port_detection.py
│       │   ├── quality_analytics.py
│       │   ├── startup_orchestrator.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
│           │   ├── quality.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
│               ├── i18n
│               │   ├── de.json
│               │   ├── en.json
│               │   ├── es.json
│               │   ├── fr.json
│               │   ├── ja.json
│               │   ├── ko.json
│               │   └── zh.json
│               ├── index.html
│               ├── README.md
│               ├── sse_test.html
│               └── style.css
├── start_http_debug.bat
├── start_http_server.sh
├── test_document.txt
├── test_version_checker.js
├── TESTING_NOTES.md
├── 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
│   │   └── test_graph_modes.py
│   ├── contracts
│   │   └── api-specification.yml
│   ├── integration
│   │   ├── conftest.py
│   │   ├── HANDLER_COVERAGE_REPORT.md
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── test_all_memory_handlers.py
│   │   ├── 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
│   ├── storage
│   │   ├── conftest.py
│   │   └── test_graph_storage.py
│   ├── test_client.py
│   ├── test_content_splitting.py
│   ├── test_database.py
│   ├── test_deberta_quality.py
│   ├── test_fallback_quality.py
│   ├── test_graph_traversal.py
│   ├── test_hybrid_cloudflare_limits.py
│   ├── test_hybrid_storage.py
│   ├── test_lightweight_onnx.py
│   ├── test_memory_ops.py
│   ├── test_memory_wrapper_cleanup.py
│   ├── test_quality_integration.py
│   ├── test_quality_system.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_imports.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
│       └── test_uv_no_pip_installer_fallback.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
└── verify_compression.sh
```

# Files

--------------------------------------------------------------------------------
/src/mcp_memory_service/web/static/app.js:
--------------------------------------------------------------------------------

```javascript
   1 | /**
   2 |  * MCP Memory Service Dashboard - Main Application
   3 |  * Interactive frontend for memory management with real-time updates
   4 |  */
   5 | 
   6 | console.log('⚡ app.js loading - TOP OF FILE');
   7 | 
   8 | class MemoryDashboard {
   9 |     // Delay between individual file uploads to avoid overwhelming the server (ms)
  10 |     static INDIVIDUAL_UPLOAD_DELAY = 500;
  11 | 
  12 |     // Static configuration for settings modal system information
  13 |     static SYSTEM_INFO_CONFIG = {
  14 |         settingsVersion: {
  15 |             sources: [{ path: 'version', api: 'health' }],
  16 |             formatter: (value) => value || 'N/A'
  17 |         },
  18 |         settingsBackend: {
  19 |             sources: [
  20 |                 { path: 'storage.storage_backend', api: 'detailedHealth' },
  21 |                 { path: 'storage.backend', api: 'detailedHealth' }
  22 |             ],
  23 |             formatter: (value) => value || 'N/A'
  24 |         },
  25 |         settingsPrimaryBackend: {
  26 |             sources: [
  27 |                 { path: 'storage.primary_backend', api: 'detailedHealth' },
  28 |                 { path: 'storage.backend', api: 'detailedHealth' }
  29 |             ],
  30 |             formatter: (value) => value || 'N/A'
  31 |         },
  32 |         settingsEmbeddingModel: {
  33 |             sources: [
  34 |                 { path: 'storage.primary_stats.embedding_model', api: 'detailedHealth' },
  35 |                 { path: 'storage.embedding_model', api: 'detailedHealth' }
  36 |             ],
  37 |             formatter: (value) => value || 'N/A'
  38 |         },
  39 |         settingsEmbeddingDim: {
  40 |             sources: [
  41 |                 { path: 'storage.primary_stats.embedding_dimension', api: 'detailedHealth' },
  42 |                 { path: 'storage.embedding_dimension', api: 'detailedHealth' }
  43 |             ],
  44 |             formatter: (value) => value || 'N/A'
  45 |         },
  46 |         settingsDbSize: {
  47 |             sources: [
  48 |                 { path: 'storage.primary_stats.database_size_mb', api: 'detailedHealth' },
  49 |                 { path: 'storage.database_size_mb', api: 'detailedHealth' }
  50 |             ],
  51 |             formatter: (value) => (value != null) ? `${value.toFixed(2)} MB` : 'N/A'
  52 |         },
  53 |         settingsTotalMemories: {
  54 |             sources: [{ path: 'storage.total_memories', api: 'detailedHealth' }],
  55 |             formatter: (value) => (value != null) ? value.toLocaleString() : 'N/A'
  56 |         },
  57 |         settingsUptime: {
  58 |             sources: [{ path: 'uptime_seconds', api: 'detailedHealth' }],
  59 |             formatter: (value) => (value != null) ? MemoryDashboard.formatUptime(value) : 'N/A'
  60 |         }
  61 |     };
  62 | 
  63 |     constructor() {
  64 |         this.apiBase = '/api';
  65 |         this.eventSource = null;
  66 |         this.memories = [];
  67 |         this.currentView = 'dashboard';
  68 |         // 国际化状态
  69 |         this.currentLang = 'en';
  70 |         this.translations = {};
  71 |         this.fallbackTranslations = {};
  72 |         this.supportedLanguages = ['en', 'zh'];
  73 |         this.languageMetadata = {}; // Stores language metadata (flags, names, etc.)
  74 |         this.searchResults = [];
  75 |         this.isLoading = false;
  76 |         this.liveSearchEnabled = true;
  77 |         this.debounceTimer = null;
  78 | 
  79 |         // Settings with defaults
  80 |         this.settings = {
  81 |             theme: 'light',
  82 |             viewDensity: 'comfortable',
  83 |             previewLines: 3
  84 |         };
  85 | 
  86 |         // Documents upload state
  87 |         this.selectedFiles = [];
  88 |         this.documentsListenersSetup = false;
  89 |         this.processingMode = 'batch'; // 'batch' or 'individual'
  90 | 
  91 |         // Bind methods
  92 |         this.handleSearch = this.handleSearch.bind(this);
  93 |         this.handleQuickSearch = this.handleQuickSearch.bind(this);
  94 |         this.handleNavigation = this.handleNavigation.bind(this);
  95 |         this.handleAddMemory = this.handleAddMemory.bind(this);
  96 |         this.handleMemoryClick = this.handleMemoryClick.bind(this);
  97 | 
  98 |         this.init();
  99 |     }
 100 | 
 101 |     /**
 102 |      * Initialize the application
 103 |      */
 104 |     async init() {
 105 |         await this.initI18n();
 106 |         this.loadSettings();
 107 |         this.applyTheme();
 108 |         this.setupEventListeners();
 109 |         this.setupSSE();
 110 |         await this.loadVersion();
 111 |         await this.loadDashboardData();
 112 |         this.updateConnectionStatus('connected');
 113 | 
 114 |         // Initialize sync status monitoring for hybrid mode
 115 |         await this.checkSyncStatus();
 116 |         this.startSyncStatusMonitoring();
 117 |     }
 118 | 
 119 |     /**
 120 |      * 初始化国际化:检测语言,加载词典并应用
 121 |      */
 122 |     async initI18n() {
 123 |         await this.detectAvailableLanguages(); // Dynamically detect available languages
 124 |         await this.loadLanguageMetadata(); // Load metadata for all languages
 125 |         this.currentLang = this.detectLanguage();
 126 |         await this.ensureFallbackTranslations();
 127 |         await this.loadTranslations(this.currentLang);
 128 |         await this.buildLanguageSelector(); // Build dropdown dynamically
 129 |         await this.applyTranslations();
 130 |     }
 131 | 
 132 |     /**
 133 |      * 语言检测:localStorage > 浏览器语言前缀 > en
 134 |      */
 135 |     detectLanguage() {
 136 |         const stored = localStorage.getItem('memoryDashboardLang');
 137 |         if (stored && this.supportedLanguages.includes(stored)) {
 138 |             return stored;
 139 |         }
 140 |         const browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
 141 |         if (browserLang.startsWith('zh')) return 'zh';
 142 |         return 'en';
 143 |     }
 144 | 
 145 |     /**
 146 |      * 确保英文词典作为回退存在
 147 |      */
 148 |     async ensureFallbackTranslations() {
 149 |         if (Object.keys(this.fallbackTranslations).length > 0) return;
 150 |         try {
 151 |             const response = await fetch('/static/i18n/en.json');
 152 |             if (response.ok) {
 153 |                 this.fallbackTranslations = await response.json();
 154 |             }
 155 |         } catch (error) {
 156 |             console.warn('Failed to load fallback translations', error);
 157 |             this.fallbackTranslations = {};
 158 |         }
 159 |     }
 160 | 
 161 |     /**
 162 |      * Dynamically detect available languages from API
 163 |      */
 164 |     async detectAvailableLanguages() {
 165 |         try {
 166 |             const response = await fetch('/api/languages');
 167 |             if (response.ok) {
 168 |                 const data = await response.json();
 169 |                 this.supportedLanguages = data.languages || ['en'];
 170 |             }
 171 |         } catch (error) {
 172 |             console.warn('Failed to fetch language list, using defaults:', error);
 173 |             this.supportedLanguages = ['en', 'zh'];
 174 |         }
 175 |     }
 176 | 
 177 |     /**
 178 |      * Load language metadata (flags, native names) from translation files
 179 |      */
 180 |     async loadLanguageMetadata() {
 181 |         for (const lang of this.supportedLanguages) {
 182 |             try {
 183 |                 const response = await fetch(`/static/i18n/${lang}.json`);
 184 |                 if (response.ok) {
 185 |                     const data = await response.json();
 186 |                     this.languageMetadata[lang] = {
 187 |                         code: data['meta.language.code'] || lang,
 188 |                         nativeName: data['meta.language.nativeName'] || lang.toUpperCase(),
 189 |                         englishName: data['meta.language.englishName'] || lang.toUpperCase(),
 190 |                         flag: data['meta.language.flag'] || '🌐'
 191 |                     };
 192 |                 }
 193 |             } catch (error) {
 194 |                 console.warn(`Failed to load metadata for ${lang}:`, error);
 195 |                 // Fallback metadata
 196 |                 this.languageMetadata[lang] = {
 197 |                     code: lang,
 198 |                     nativeName: lang.toUpperCase(),
 199 |                     englishName: lang.toUpperCase(),
 200 |                     flag: '🌐'
 201 |                 };
 202 |             }
 203 |         }
 204 |     }
 205 | 
 206 |     /**
 207 |      * Build language selector dropdown dynamically
 208 |      */
 209 |     async buildLanguageSelector() {
 210 |         const dropdown = document.querySelector('.lang-dropdown-menu');
 211 |         if (!dropdown) return;
 212 | 
 213 |         // Clear existing options
 214 |         dropdown.innerHTML = '';
 215 | 
 216 |         // Create option for each language
 217 |         this.supportedLanguages.forEach(lang => {
 218 |             const meta = this.languageMetadata[lang];
 219 |             if (!meta) return;
 220 | 
 221 |             const option = document.createElement('button');
 222 |             option.className = 'lang-option';
 223 |             option.dataset.lang = lang;
 224 |             option.setAttribute('role', 'menuitem');
 225 |             option.setAttribute('type', 'button');
 226 | 
 227 |             // Build option HTML
 228 |             option.innerHTML = `
 229 |                 <span class="lang-flag">${meta.flag}</span>
 230 |                 <span class="lang-name">${meta.nativeName}</span>
 231 |                 <svg class="lang-check" width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 232 |                     <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
 233 |                 </svg>
 234 |             `;
 235 | 
 236 |             // Add click handler
 237 |             option.addEventListener('click', async (e) => {
 238 |                 e.preventDefault();
 239 |                 e.stopPropagation();
 240 |                 await this.setLanguage(lang);
 241 |                 this.toggleLanguageDropdown(false);
 242 |             });
 243 | 
 244 |             dropdown.appendChild(option);
 245 |         });
 246 |     }
 247 | 
 248 |     /**
 249 |      * Toggle language dropdown visibility
 250 |      */
 251 |     toggleLanguageDropdown(show) {
 252 |         const trigger = document.querySelector('.lang-dropdown-trigger');
 253 |         const dropdown = document.querySelector('.lang-dropdown-menu');
 254 | 
 255 |         if (!trigger || !dropdown) return;
 256 | 
 257 |         if (show === undefined) {
 258 |             // Toggle
 259 |             show = dropdown.hasAttribute('hidden');
 260 |         }
 261 | 
 262 |         if (show) {
 263 |             dropdown.removeAttribute('hidden');
 264 |             trigger.setAttribute('aria-expanded', 'true');
 265 |         } else {
 266 |             dropdown.setAttribute('hidden', '');
 267 |             trigger.setAttribute('aria-expanded', 'false');
 268 |         }
 269 |     }
 270 | 
 271 |     /**
 272 |      * 加载指定语言词典
 273 |      */
 274 |     async loadTranslations(lang) {
 275 |         try {
 276 |             const response = await fetch(`/static/i18n/${lang}.json`);
 277 |             if (response.ok) {
 278 |                 this.translations = await response.json();
 279 |                 this.currentLang = lang;
 280 |                 localStorage.setItem('memoryDashboardLang', lang);
 281 |                 return;
 282 |             }
 283 |         } catch (error) {
 284 |             console.warn('Failed to load translations for', lang, error);
 285 |         }
 286 |         // 回退到英文
 287 |         this.translations = this.fallbackTranslations;
 288 |         this.currentLang = 'en';
 289 |         localStorage.setItem('memoryDashboardLang', 'en');
 290 |     }
 291 | 
 292 |     /**
 293 |      * 获取翻译,带占位符替换
 294 |      */
 295 |     t(key, fallback = '', vars = {}) {
 296 |         const fromCurrent = this.translations?.[key];
 297 |         const fromFallback = this.fallbackTranslations?.[key];
 298 |         const template = fromCurrent || fromFallback || fallback || key;
 299 |         return this.replacePlaceholders(template, vars);
 300 |     }
 301 | 
 302 |     replacePlaceholders(str, vars) {
 303 |         return str.replace(/\{(.*?)\}/g, (_, k) => vars[k] ?? `{${k}}`);
 304 |     }
 305 | 
 306 |     /**
 307 |      * 应用翻译到 DOM
 308 |      */
 309 |     async applyTranslations() {
 310 |         // 统一遍历带 i18n 属性的元素,减少多次 DOM 遍历
 311 |         document.querySelectorAll('[data-i18n], [data-i18n-html], [data-i18n-placeholder], [data-i18n-aria]').forEach(el => {
 312 |             const { i18n, i18nHtml, i18nPlaceholder, i18nAria } = el.dataset;
 313 | 
 314 |             if (i18n) {
 315 |                 const text = this.t(i18n, el.textContent?.trim() || '');
 316 |                 if (text) el.textContent = text;
 317 |             }
 318 | 
 319 |             if (i18nHtml) {
 320 |                 const html = this.t(i18nHtml, el.innerHTML.trim() || '');
 321 |                 if (html) el.innerHTML = html;
 322 |             }
 323 | 
 324 |             if (i18nPlaceholder) {
 325 |                 const text = this.t(i18nPlaceholder, el.getAttribute('placeholder') || '');
 326 |                 el.setAttribute('placeholder', text);
 327 |             }
 328 | 
 329 |             if (i18nAria) {
 330 |                 const text = this.t(i18nAria, el.getAttribute('aria-label') || '');
 331 |                 el.setAttribute('aria-label', text);
 332 |             }
 333 |         });
 334 | 
 335 |         // 文档标题
 336 |         document.title = this.t('meta.title', document.title);
 337 | 
 338 |         // 搜索模式文案
 339 |         const modeText = document.getElementById('searchModeText');
 340 |         if (modeText) {
 341 |             modeText.textContent = this.liveSearchEnabled
 342 |                 ? this.t('search.modeLive', modeText.textContent)
 343 |                 : this.t('search.modeManual', modeText.textContent);
 344 |         }
 345 | 
 346 |         // 更新语言按钮状态
 347 |         this.updateLanguageSwitcher();
 348 |     }
 349 | 
 350 |     /**
 351 |      * 切换语言
 352 |      */
 353 |     async setLanguage(lang) {
 354 |         if (!this.supportedLanguages.includes(lang) || lang === this.currentLang) return;
 355 |         await this.loadTranslations(lang);
 356 |         await this.applyTranslations();
 357 |         this.showToast(this.t('toast.languageSwitched', 'Language switched'), 'success', 2000);
 358 |     }
 359 | 
 360 |     /**
 361 |      * 更新语言选择器状态
 362 |      * Update language selector (dropdown trigger text and active option)
 363 |      */
 364 |     updateLanguageSwitcher() {
 365 |         const trigger = document.querySelector('.lang-dropdown-trigger');
 366 |         const currentLangSpan = trigger?.querySelector('.lang-current');
 367 |         const options = document.querySelectorAll('.lang-option');
 368 | 
 369 |         // Update trigger text with current language native name
 370 |         if (currentLangSpan && this.languageMetadata[this.currentLang]) {
 371 |             const meta = this.languageMetadata[this.currentLang];
 372 |             currentLangSpan.textContent = meta.nativeName;
 373 |         }
 374 | 
 375 |         // Update active state for dropdown options
 376 |         options.forEach(option => {
 377 |             const isActive = option.dataset.lang === this.currentLang;
 378 |             option.classList.toggle('active', isActive);
 379 |         });
 380 |     }
 381 | 
 382 |     /**
 383 |      * Set up event listeners for UI interactions
 384 |      */
 385 |     setupEventListeners() {
 386 |         console.log('⚡ setupEventListeners() called');
 387 |         // Navigation
 388 |         document.querySelectorAll('.nav-item').forEach(item => {
 389 |             item.addEventListener('click', this.handleNavigation);
 390 |         });
 391 | 
 392 |         // Language dropdown
 393 |         const langTrigger = document.querySelector('.lang-dropdown-trigger');
 394 |         const langDropdown = document.querySelector('.lang-dropdown-menu');
 395 | 
 396 |         if (langTrigger) {
 397 |             // Toggle dropdown on trigger click
 398 |             langTrigger.addEventListener('click', (e) => {
 399 |                 e.stopPropagation();
 400 |                 this.toggleLanguageDropdown();
 401 |             });
 402 | 
 403 |             // Keyboard navigation
 404 |             langTrigger.addEventListener('keydown', (e) => {
 405 |                 if (e.key === 'Enter' || e.key === ' ') {
 406 |                     e.preventDefault();
 407 |                     this.toggleLanguageDropdown();
 408 |                 } else if (e.key === 'Escape') {
 409 |                     this.toggleLanguageDropdown(false);
 410 |                 }
 411 |             });
 412 |         }
 413 | 
 414 |         // Close dropdown when clicking outside
 415 |         document.addEventListener('click', (e) => {
 416 |             if (langDropdown && !langDropdown.hasAttribute('hidden')) {
 417 |                 const isClickInside = langTrigger?.contains(e.target) || langDropdown.contains(e.target);
 418 |                 if (!isClickInside) {
 419 |                     this.toggleLanguageDropdown(false);
 420 |                 }
 421 |             }
 422 |         });
 423 | 
 424 |         // Search functionality
 425 |         const quickSearch = document.getElementById('quickSearch');
 426 |         const searchBtn = document.querySelector('.search-btn');
 427 | 
 428 |         if (quickSearch) {
 429 |             quickSearch.addEventListener('input', this.debounce(this.handleQuickSearch, 300));
 430 |             quickSearch.addEventListener('keypress', (e) => {
 431 |                 if (e.key === 'Enter') {
 432 |                     this.handleSearch(e.target.value);
 433 |                 }
 434 |             });
 435 |         }
 436 | 
 437 |         if (searchBtn && quickSearch) {
 438 |             searchBtn.addEventListener('click', () => {
 439 |                 this.handleSearch(quickSearch.value);
 440 |             });
 441 |         }
 442 | 
 443 |         // Add memory functionality
 444 |         const addMemoryBtn = document.getElementById('addMemoryBtn');
 445 |         if (addMemoryBtn) {
 446 |             addMemoryBtn.addEventListener('click', this.handleAddMemory);
 447 |         }
 448 |         document.querySelectorAll('[data-action="add-memory"]').forEach(btn => {
 449 |             btn.addEventListener('click', this.handleAddMemory);
 450 |         });
 451 | 
 452 |         // Modal close handlers
 453 |         document.querySelectorAll('.modal-close').forEach(btn => {
 454 |             btn.addEventListener('click', (e) => {
 455 |                 this.closeModal(e.target.closest('.modal-overlay'));
 456 |             });
 457 |         });
 458 | 
 459 |         // Modal overlay click to close
 460 |         document.querySelectorAll('.modal-overlay').forEach(overlay => {
 461 |             overlay.addEventListener('click', (e) => {
 462 |                 if (e.target === overlay) {
 463 |                     this.closeModal(overlay);
 464 |                 }
 465 |             });
 466 |         });
 467 | 
 468 |         // Add memory form submission
 469 |         const saveMemoryBtn = document.getElementById('saveMemoryBtn');
 470 |         if (saveMemoryBtn) {
 471 |             saveMemoryBtn.addEventListener('click', this.handleSaveMemory.bind(this));
 472 |         }
 473 | 
 474 |         const cancelAddBtn = document.getElementById('cancelAddBtn');
 475 |         if (cancelAddBtn) {
 476 |             cancelAddBtn.addEventListener('click', () => {
 477 |                 this.closeModal(document.getElementById('addMemoryModal'));
 478 |             });
 479 |         }
 480 | 
 481 |         // Quick action handlers
 482 |         document.querySelectorAll('.action-card').forEach(card => {
 483 |             card.addEventListener('click', (e) => {
 484 |                 const action = e.currentTarget.dataset.action;
 485 |                 this.handleQuickAction(action);
 486 |             });
 487 |         });
 488 | 
 489 |         // Live search toggle handler
 490 |         const liveSearchToggle = document.getElementById('liveSearchToggle');
 491 |         liveSearchToggle?.addEventListener('change', this.handleLiveSearchToggle.bind(this));
 492 | 
 493 |         // Filter handlers for search view
 494 |         const tagFilterInput = document.getElementById('tagFilter');
 495 |         tagFilterInput?.addEventListener('input', this.handleDebouncedFilterChange.bind(this));
 496 |         tagFilterInput?.addEventListener('keypress', (e) => {
 497 |             if (e.key === 'Enter') {
 498 |                 this.handleFilterChange();
 499 |             }
 500 |         });
 501 |         document.getElementById('dateFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
 502 |         document.getElementById('typeFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
 503 | 
 504 |         // View option handlers
 505 |         document.querySelectorAll('.view-btn').forEach(btn => {
 506 |             btn.addEventListener('click', (e) => {
 507 |                 this.handleViewModeChange(e.target.dataset.view);
 508 |             });
 509 |         });
 510 | 
 511 |         // New filter action handlers
 512 |         document.getElementById('applyFiltersBtn')?.addEventListener('click', this.handleFilterChange.bind(this));
 513 |         document.getElementById('clearFiltersBtn')?.addEventListener('click', this.clearAllFilters.bind(this));
 514 | 
 515 |         // Theme toggle button
 516 |         document.getElementById('themeToggleBtn')?.addEventListener('click', () => {
 517 |             this.toggleTheme();
 518 |         });
 519 | 
 520 |         // Settings button
 521 |         document.getElementById('settingsBtn')?.addEventListener('click', () => {
 522 |             this.openSettingsModal();
 523 |         });
 524 | 
 525 |         // Settings modal handlers
 526 |         document.getElementById('saveSettingsBtn')?.addEventListener('click', () => {
 527 |             this.saveSettings();
 528 |         });
 529 | 
 530 |         document.getElementById('cancelSettingsBtn')?.addEventListener('click', () => {
 531 |             this.closeModal(document.getElementById('settingsModal'));
 532 |         });
 533 | 
 534 |         // Tag cloud event delegation
 535 |         document.getElementById('tagsCloudContainer')?.addEventListener('click', (e) => {
 536 |             if (e.target.classList.contains('tag-bubble') || e.target.closest('.tag-bubble')) {
 537 |                 const tagButton = e.target.classList.contains('tag-bubble') ? e.target : e.target.closest('.tag-bubble');
 538 |                 const tag = tagButton.dataset.tag;
 539 |                 if (tag) {
 540 |                     this.filterByTag(tag);
 541 |                 }
 542 |             }
 543 |         });
 544 | 
 545 |         // Manage tab event listeners
 546 |         document.getElementById('deleteByTagBtn')?.addEventListener('click', this.handleBulkDeleteByTag.bind(this));
 547 |         document.getElementById('cleanupDuplicatesBtn')?.addEventListener('click', this.handleCleanupDuplicates.bind(this));
 548 |         document.getElementById('deleteByDateBtn')?.addEventListener('click', this.handleBulkDeleteByDate.bind(this));
 549 |         document.getElementById('deleteUntaggedBtn')?.addEventListener('click', this.handleDeleteUntagged.bind(this));
 550 |         document.getElementById('optimizeDbBtn')?.addEventListener('click', this.handleOptimizeDatabase.bind(this));
 551 |         document.getElementById('rebuildIndexBtn')?.addEventListener('click', this.handleRebuildIndex.bind(this));
 552 | 
 553 |         // Analytics tab event listeners
 554 |         document.getElementById('growthPeriodSelect')?.addEventListener('change', this.handleGrowthPeriodChange.bind(this));
 555 |         document.getElementById('heatmapPeriodSelect')?.addEventListener('change', this.handleHeatmapPeriodChange.bind(this));
 556 |         document.getElementById('topTagsPeriodSelect')?.addEventListener('change', this.handleTopTagsPeriodChange.bind(this));
 557 |         document.getElementById('activityGranularitySelect')?.addEventListener('change', this.handleActivityGranularityChange.bind(this));
 558 | 
 559 |         // Keyboard shortcuts
 560 |         document.addEventListener('keydown', (e) => {
 561 |             if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
 562 |                 e.preventDefault();
 563 |                 document.getElementById('searchInput').focus();
 564 |             }
 565 |             if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
 566 |                 e.preventDefault();
 567 |                 this.handleAddMemory();
 568 |             }
 569 |         });
 570 |     }
 571 | 
 572 |     /**
 573 |      * Set up Server-Sent Events for real-time updates
 574 |      */
 575 |     setupSSE() {
 576 |         try {
 577 |             this.eventSource = new EventSource(`${this.apiBase}/events`);
 578 | 
 579 |             this.eventSource.onopen = () => {
 580 |                 this.updateConnectionStatus('connected');
 581 |             };
 582 | 
 583 |             this.eventSource.onmessage = (event) => {
 584 |                 try {
 585 |                     const data = JSON.parse(event.data);
 586 |                     this.handleRealtimeUpdate(data);
 587 |                 } catch (error) {
 588 |                     console.error('Error parsing SSE data:', error);
 589 |                 }
 590 |             };
 591 | 
 592 |             // Add specific event listeners for sync progress
 593 |             this.eventSource.addEventListener('sync_progress', (event) => {
 594 |                 try {
 595 |                     const data = JSON.parse(event.data);
 596 |                     this.handleSyncProgress(data);
 597 |                 } catch (error) {
 598 |                     console.error('Error parsing sync_progress event:', error);
 599 |                 }
 600 |             });
 601 | 
 602 |             this.eventSource.addEventListener('sync_completed', (event) => {
 603 |                 try {
 604 |                     const data = JSON.parse(event.data);
 605 |                     this.handleSyncCompleted(data);
 606 |                 } catch (error) {
 607 |                     console.error('Error parsing sync_completed event:', error);
 608 |                 }
 609 |             });
 610 | 
 611 |             this.eventSource.onerror = (error) => {
 612 |                 console.error('SSE connection error:', error);
 613 |                 this.updateConnectionStatus('disconnected');
 614 | 
 615 |                 // Attempt to reconnect after 5 seconds
 616 |                 setTimeout(() => {
 617 |                     if (this.eventSource.readyState === EventSource.CLOSED) {
 618 |                         this.setupSSE();
 619 |                     }
 620 |                 }, 5000);
 621 |             };
 622 | 
 623 |         } catch (error) {
 624 |             console.error('Failed to establish SSE connection:', error);
 625 |             this.updateConnectionStatus('disconnected');
 626 |         }
 627 |     }
 628 | 
 629 |     /**
 630 |      * Handle real-time updates from SSE
 631 |      */
 632 |     handleRealtimeUpdate(data) {
 633 |         switch (data.type) {
 634 |             case 'memory_added':
 635 |                 this.handleMemoryAdded(data.memory);
 636 |                 this.showToast(this.t('toast.memoryAdded', 'Memory added successfully'), 'success');
 637 |                 break;
 638 |             case 'memory_deleted':
 639 |                 this.handleMemoryDeleted(data.memory_id);
 640 |                 this.showToast(this.t('toast.memoryDeleted', 'Memory deleted'), 'success');
 641 |                 break;
 642 |             case 'memory_updated':
 643 |                 this.handleMemoryUpdated(data.memory);
 644 |                 this.showToast(this.t('toast.memoryUpdated', 'Memory updated'), 'success');
 645 |                 break;
 646 |             case 'stats_updated':
 647 |                 this.updateDashboardStats(data.stats);
 648 |                 break;
 649 |             default:
 650 |                 // Unknown event type - ignore silently
 651 |         }
 652 |     }
 653 | 
 654 |     /**
 655 |      * Handle sync progress updates from SSE
 656 |      */
 657 |     handleSyncProgress(data) {
 658 |         console.log('Sync progress:', data);
 659 | 
 660 |         // Update sync status display if visible
 661 |         const syncStatus = document.getElementById('syncStatus');
 662 |         if (syncStatus) {
 663 |             const progressText = `Syncing: ${data.synced_count}/${data.total_count} (${data.progress_percentage}%)`;
 664 |             syncStatus.textContent = progressText;
 665 |             syncStatus.className = 'sync-status syncing';
 666 |         }
 667 | 
 668 |         // Update memory count in real-time if on dashboard
 669 |         if (this.currentView === 'dashboard') {
 670 |             const memoryCountElement = document.getElementById('totalMemories');
 671 |             if (memoryCountElement && data.synced_count) {
 672 |                 // Refresh the detailed health to get accurate count
 673 |                 this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
 674 |             }
 675 |         }
 676 | 
 677 |         // Show toast notification for manual sync
 678 |         if (data.sync_type === 'manual') {
 679 |             this.showToast(
 680 |                 data.message || this.t('toast.syncing', 'Syncing: {synced}/{total}', {
 681 |                     synced: data.synced_count,
 682 |                     total: data.total_count
 683 |                 }),
 684 |                 'info'
 685 |             );
 686 |         }
 687 |     }
 688 | 
 689 |     /**
 690 |      * Handle sync completion from SSE
 691 |      */
 692 |     handleSyncCompleted(data) {
 693 |         console.log('Sync completed:', data);
 694 | 
 695 |         // Update sync status display
 696 |         const syncStatus = document.getElementById('syncStatus');
 697 |         if (syncStatus) {
 698 |             syncStatus.textContent = 'Synced';
 699 |             syncStatus.className = 'sync-status synced';
 700 |         }
 701 | 
 702 |         // Refresh dashboard data to show updated counts
 703 |         if (this.currentView === 'dashboard') {
 704 |             this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
 705 |         }
 706 | 
 707 |         // Also refresh sync status for hybrid mode
 708 |         this.checkSyncStatus().catch(err => console.error('Error checking sync status:', err));
 709 | 
 710 |         // Show completion notification
 711 |         const message = data.message || `Sync completed: ${data.synced_count} memories synced`;
 712 |         this.showToast(message, 'success');
 713 |     }
 714 | 
 715 |     /**
 716 |      * Load application version from health endpoint
 717 |      */
 718 |     async loadVersion() {
 719 |         try {
 720 |             const healthResponse = await this.apiCall('/health');
 721 |             const versionBadge = document.getElementById('versionBadge');
 722 |             if (versionBadge && healthResponse.version) {
 723 |                 versionBadge.textContent = `v${healthResponse.version}`;
 724 |             }
 725 |         } catch (error) {
 726 |             console.error('Error loading version:', error);
 727 |             const versionBadge = document.getElementById('versionBadge');
 728 |             if (versionBadge) {
 729 |                 versionBadge.textContent = 'v?.?.?';
 730 |             }
 731 |         }
 732 |     }
 733 | 
 734 |     /**
 735 |      * Load initial dashboard data
 736 |      */
 737 |     async loadDashboardData() {
 738 |         this.setLoading(true);
 739 | 
 740 |         try {
 741 |             // Load recent memories for dashboard display
 742 |             const memoriesResponse = await this.apiCall('/memories?page=1&page_size=100');
 743 |             if (memoriesResponse.memories) {
 744 |                 this.memories = memoriesResponse.memories;
 745 |                 this.renderRecentMemories(memoriesResponse.memories);
 746 |             }
 747 | 
 748 |             // Load basic statistics
 749 |             const statsResponse = await this.apiCall('/health/detailed');
 750 |             if (statsResponse.storage) {
 751 |                 this.updateDashboardStats(statsResponse.storage);
 752 |             }
 753 | 
 754 | 
 755 |         } catch (error) {
 756 |             console.error('Error loading dashboard data:', error);
 757 |             this.showToast(this.t('toast.loadDashboardFail', 'Failed to load dashboard data'), 'error');
 758 |         } finally {
 759 |             this.setLoading(false);
 760 |         }
 761 |     }
 762 | 
 763 |     /**
 764 |      * Load browse view data (tags)
 765 |      */
 766 |     async loadBrowseData() {
 767 |         this.setLoading(true);
 768 |         try {
 769 |             // Load tags with counts from the dedicated endpoint
 770 |             const tagsResponse = await this.apiCall('/tags');
 771 |             if (tagsResponse.tags) {
 772 |                 this.tags = tagsResponse.tags;
 773 |                 this.renderTagsCloud();
 774 |             }
 775 |         } catch (error) {
 776 |             console.error('Error loading browse data:', error);
 777 |             this.showToast(this.t('toast.loadBrowseFail', 'Failed to load browse data'), 'error');
 778 |         } finally {
 779 |             this.setLoading(false);
 780 |         }
 781 |     }
 782 | 
 783 |     /**
 784 |      * Load documents view data
 785 |      */
 786 |     async loadDocumentsData() {
 787 |         this.setLoading(true);
 788 |         try {
 789 |             // Load upload history
 790 |             await this.loadUploadHistory();
 791 |             // Setup document upload event listeners
 792 |             this.setupDocumentsEventListeners();
 793 |         } catch (error) {
 794 |             console.error('Error loading documents data:', error);
 795 |             this.showToast(this.t('toast.loadDocumentsFail', 'Failed to load documents data'), 'error');
 796 |         } finally {
 797 |             this.setLoading(false);
 798 |         }
 799 |     }
 800 | 
 801 |     /**
 802 |      * Load upload history from API
 803 |      */
 804 |     async loadUploadHistory() {
 805 |         console.log('Loading upload history...');
 806 |         try {
 807 |             const historyResponse = await this.apiCall('/documents/history');
 808 |             console.log('Upload history response:', historyResponse);
 809 |             if (historyResponse.uploads) {
 810 |                 this.renderUploadHistory(historyResponse.uploads);
 811 |             } else {
 812 |                 console.warn('No uploads property in response');
 813 |                 this.renderUploadHistory([]);
 814 |             }
 815 |         } catch (error) {
 816 |             console.error('Error loading upload history:', error);
 817 |             // Show a message in the history container instead of just logging
 818 |             const historyContainer = document.getElementById('uploadHistory');
 819 |             if (historyContainer) {
 820 |                 historyContainer.innerHTML = '<p style="text-align: center; color: var(--error);">Failed to load upload history. Please check the console for details.</p>';
 821 |             }
 822 |         }
 823 |     }
 824 | 
 825 |     /**
 826 |      * Render upload history
 827 |      */
 828 |     renderUploadHistory(uploads) {
 829 |         const historyContainer = document.getElementById('uploadHistory');
 830 |         if (!historyContainer) return;
 831 | 
 832 |         if (uploads.length === 0) {
 833 |             historyContainer.innerHTML = '<p style="text-align: center; color: var(--neutral-500);">No uploads yet. Start by uploading some documents!</p>';
 834 |             return;
 835 |         }
 836 | 
 837 |         const historyHtml = uploads.map(upload => {
 838 |         const statusClass = upload.status.toLowerCase();
 839 |         const statusText = upload.status.charAt(0).toUpperCase() + upload.status.slice(1);
 840 |         const progressPercent = upload.progress || 0;
 841 |         const hasMemories = upload.chunks_stored > 0;
 842 | 
 843 |         return `
 844 |         <div class="upload-item ${statusClass}" data-upload-id="${upload.upload_id}" data-filename="${this.escapeHtml(upload.filename)}">
 845 |         <div class="upload-info">
 846 |         <div class="upload-filename">${this.escapeHtml(upload.filename)}</div>
 847 |                         <div class="upload-meta">
 848 |                             ${upload.chunks_stored || 0} chunks stored •
 849 |                             ${(upload.file_size / 1024).toFixed(1)} KB •
 850 |                             ${new Date(upload.created_at).toLocaleString()}
 851 |                         </div>
 852 |                         ${upload.status === 'processing' ? `
 853 |                             <div class="progress-bar">
 854 |                                 <div class="progress-fill" style="width: ${progressPercent}%"></div>
 855 |                             </div>
 856 |                         ` : ''}
 857 |                     </div>
 858 |                     <div class="upload-actions-container">
 859 |                         <div class="upload-status ${statusClass}">
 860 |                             <span>${statusText}</span>
 861 |                             ${upload.errors && upload.errors.length > 0 ? `
 862 |                                 <span title="${this.escapeHtml(upload.errors.join('; '))}">⚠️</span>
 863 |                             ` : ''}
 864 |                         </div>
 865 |                         ${upload.status === 'completed' && hasMemories ? `
 866 |                             <div class="upload-actions">
 867 |                                 <button class="btn-icon btn-view-memory"
 868 |                                 title="View memory chunks">
 869 |                                     <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
 870 |                                         <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"/>
 871 |                                     </svg>
 872 |                                     <span>View</span>
 873 |                                 </button>
 874 |                                 <button class="btn-icon btn-remove"
 875 |                                 title="Remove document and memories">
 876 |                                     <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
 877 |                                         <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
 878 |                                     </svg>
 879 |                                     <span>Remove</span>
 880 |                                 </button>
 881 |                             </div>
 882 |                         ` : ''}
 883 |                     </div>
 884 |                 </div>
 885 |             `;
 886 |         }).join('');
 887 | 
 888 |         historyContainer.innerHTML = historyHtml;
 889 |     }
 890 | 
 891 |     /**
 892 |      * Setup event listeners for documents view
 893 |      */
 894 |     setupDocumentsEventListeners() {
 895 |         // Prevent duplicate event listener setup
 896 |         if (this.documentsListenersSetup) {
 897 |             console.log('Document listeners already set up, skipping...');
 898 |             return;
 899 |         }
 900 | 
 901 |         // File selection buttons
 902 |         const fileSelectBtn = document.getElementById('fileSelectBtn');
 903 |         const fileInput = document.getElementById('fileInput');
 904 | 
 905 |         if (fileSelectBtn && fileInput) {
 906 |             fileSelectBtn.addEventListener('click', () => {
 907 |                 fileInput.click();
 908 |             });
 909 | 
 910 |             fileInput.addEventListener('change', (e) => {
 911 |                 this.handleFileSelection(e.target.files);
 912 |             });
 913 |         }
 914 | 
 915 |         // Drag and drop
 916 |         const dropZone = document.getElementById('dropZone');
 917 |         if (dropZone) {
 918 |             dropZone.addEventListener('dragover', (e) => {
 919 |                 e.preventDefault();
 920 |                 dropZone.classList.add('drag-over');
 921 |             });
 922 | 
 923 |             dropZone.addEventListener('dragleave', (e) => {
 924 |                 e.preventDefault();
 925 |                 dropZone.classList.remove('drag-over');
 926 |             });
 927 | 
 928 |             dropZone.addEventListener('drop', (e) => {
 929 |                 e.preventDefault();
 930 |                 dropZone.classList.remove('drag-over');
 931 |                 const files = e.dataTransfer.files;
 932 |                 this.handleFileSelection(files);
 933 |             });
 934 |         }
 935 | 
 936 |         // Configuration controls - cache for performance
 937 |         this.chunkSizeInput = document.getElementById('chunkSize');
 938 |         this.chunkOverlapInput = document.getElementById('chunkOverlap');
 939 |         this.memoryTypeInput = document.getElementById('memoryType');
 940 |         const chunkSizeValue = document.getElementById('chunkSizeValue');
 941 |         const chunkOverlapValue = document.getElementById('chunkOverlapValue');
 942 | 
 943 |         if (this.chunkSizeInput && chunkSizeValue) {
 944 |             this.chunkSizeInput.addEventListener('input', (e) => {
 945 |                 chunkSizeValue.textContent = e.target.value;
 946 |                 this.updateUploadButton();
 947 |             });
 948 |         }
 949 | 
 950 |         // Chunking help info icon
 951 |         const infoIcon = document.querySelector('.info-icon');
 952 |         if (infoIcon) {
 953 |             infoIcon.addEventListener('click', () => {
 954 |                 this.toggleChunkingHelp();
 955 |             });
 956 |         }
 957 | 
 958 |         // Overlap help info icon
 959 |         const overlapInfoIcon = document.querySelector('.info-icon-overlap');
 960 |         if (overlapInfoIcon) {
 961 |             overlapInfoIcon.addEventListener('click', () => {
 962 |                 this.toggleOverlapHelp();
 963 |             });
 964 |         }
 965 | 
 966 |         // Processing mode help info icon
 967 |         const processingModeInfoIcon = document.querySelector('.info-icon-processing');
 968 |         if (processingModeInfoIcon) {
 969 |             processingModeInfoIcon.addEventListener('click', () => {
 970 |                 this.toggleProcessingModeHelp();
 971 |             });
 972 |         }
 973 | 
 974 |         if (this.chunkOverlapInput && chunkOverlapValue) {
 975 |             this.chunkOverlapInput.addEventListener('input', (e) => {
 976 |                 chunkOverlapValue.textContent = e.target.value;
 977 |                 this.updateUploadButton();
 978 |             });
 979 |         }
 980 | 
 981 |         // Processing mode toggle buttons - cache for performance
 982 |         this.batchModeBtn = document.getElementById('batchModeBtn');
 983 |         this.individualModeBtn = document.getElementById('individualModeBtn');
 984 |         this.modeDescription = document.getElementById('modeDescription');
 985 | 
 986 |         if (this.batchModeBtn) {
 987 |             this.batchModeBtn.addEventListener('click', () => {
 988 |                 this.setProcessingMode('batch');
 989 |             });
 990 |         }
 991 | 
 992 |         if (this.individualModeBtn) {
 993 |             this.individualModeBtn.addEventListener('click', () => {
 994 |                 this.setProcessingMode('individual');
 995 |             });
 996 |         }
 997 | 
 998 |         // Upload button
 999 |         const uploadBtn = document.getElementById('uploadBtn');
1000 |         if (uploadBtn) {
1001 |             uploadBtn.addEventListener('click', () => {
1002 |                 this.handleDocumentUpload();
1003 |             });
1004 |         }
1005 | 
1006 |         // Add event listeners for buttons with data-action attribute
1007 |         document.querySelectorAll('[data-action]').forEach(button => {
1008 |             button.addEventListener('click', (e) => {
1009 |                 const action = e.currentTarget.dataset.action;
1010 |                 if (this[action] && typeof this[action] === 'function') {
1011 |                     this[action]();
1012 |                 }
1013 |             });
1014 |         });
1015 | 
1016 |         // Sync buttons event listeners are attached in checkSyncStatus()
1017 |         // after buttons are confirmed to be accessible in the DOM
1018 | 
1019 |         // Backup now button
1020 |         const backupNowButton = document.getElementById('backupNowButton');
1021 |         if (backupNowButton) {
1022 |             backupNowButton.addEventListener('click', () => {
1023 |                 this.createBackup();
1024 |             });
1025 |         }
1026 | 
1027 |         // Document search button
1028 |         const docSearchBtn = document.getElementById('docSearchBtn');
1029 |         const docSearchInput = document.getElementById('docSearchInput');
1030 |         if (docSearchBtn && docSearchInput) {
1031 |             docSearchBtn.addEventListener('click', () => {
1032 |                 const query = docSearchInput.value.trim();
1033 |                 if (query) {
1034 |                     this.searchDocumentContent(query);
1035 |                 } else {
1036 |                     this.showToast(this.t('toast.enterSearch', 'Please enter a search query'), 'warning');
1037 |                 }
1038 |             });
1039 | 
1040 |             // Enter key to search
1041 |             docSearchInput.addEventListener('keypress', (e) => {
1042 |                 if (e.key === 'Enter') {
1043 |                     const query = docSearchInput.value.trim();
1044 |                     if (query) {
1045 |                         this.searchDocumentContent(query);
1046 |                     }
1047 |                 }
1048 |             });
1049 |         }
1050 | 
1051 |         // Upload history action buttons (event delegation)
1052 |         const uploadHistory = document.getElementById('uploadHistory');
1053 |         if (uploadHistory) {
1054 |             uploadHistory.addEventListener('click', (e) => {
1055 |                 const button = e.target.closest('.btn-view-memory, .btn-remove');
1056 |                 if (!button) return;
1057 | 
1058 |                 const uploadItem = button.closest('.upload-item');
1059 |                 const uploadId = uploadItem?.dataset.uploadId;
1060 |                 const filename = uploadItem?.dataset.filename;
1061 | 
1062 |                 if (!uploadId) return;
1063 | 
1064 |                 if (button.classList.contains('btn-view-memory')) {
1065 |                     this.viewDocumentMemory(uploadId);
1066 |                 } else if (button.classList.contains('btn-remove')) {
1067 |                     this.removeDocument(uploadId, filename);
1068 |                 }
1069 |             });
1070 |         }
1071 | 
1072 |         // Close modal when clicking outside
1073 |         const memoryViewerModal = document.getElementById('memoryViewerModal');
1074 |         if (memoryViewerModal) {
1075 |             memoryViewerModal.addEventListener('click', (e) => {
1076 |                 if (e.target === memoryViewerModal) {
1077 |                     this.closeMemoryViewer();
1078 |                 }
1079 |             });
1080 |         }
1081 | 
1082 |         // Mark listeners as set up to prevent duplicates
1083 |         this.documentsListenersSetup = true;
1084 |         console.log('Document listeners setup complete');
1085 |     }
1086 | 
1087 |     /**
1088 |      * Handle file selection from input or drag-drop
1089 |      */
1090 |     handleFileSelection(files) {
1091 |         if (!files || files.length === 0) return;
1092 | 
1093 |         this.selectedFiles = Array.from(files);
1094 |         this.updateUploadButton();
1095 | 
1096 |         // Show file preview in drop zone
1097 |         const dropZone = document.getElementById('dropZone');
1098 |         if (dropZone) {
1099 |             const fileNames = this.selectedFiles.map(f => this.escapeHtml(f.name)).join(', ');
1100 |             const content = dropZone.querySelector('.drop-zone-content');
1101 |             if (content) {
1102 |                 content.innerHTML = `
1103 |                     <svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
1104 |                         <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
1105 |                     </svg>
1106 |                     <h3>${files.length} file${files.length > 1 ? 's' : ''} selected</h3>
1107 |                     <p>${fileNames}</p>
1108 |                     <input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
1109 |                 `;
1110 |             }
1111 |         }
1112 |     }
1113 | 
1114 |     /**
1115 |      * Update upload button state based on selections
1116 |      */
1117 |     updateUploadButton() {
1118 |         const uploadBtn = document.getElementById('uploadBtn');
1119 |         const hasFiles = this.selectedFiles && this.selectedFiles.length > 0;
1120 | 
1121 |         if (uploadBtn) {
1122 |             uploadBtn.disabled = !hasFiles;
1123 |             uploadBtn.textContent = hasFiles ?
1124 |                 `Upload & Ingest ${this.selectedFiles.length} file${this.selectedFiles.length > 1 ? 's' : ''}` :
1125 |                 'Upload & Ingest';
1126 |         }
1127 | 
1128 |         // Show/hide processing mode section based on file count
1129 |         const processingModeSection = document.getElementById('processingModeSection');
1130 |         if (processingModeSection) {
1131 |             processingModeSection.style.display = (hasFiles && this.selectedFiles.length > 1) ? 'block' : 'none';
1132 |         }
1133 |     }
1134 | 
1135 |     /**
1136 |      * Set processing mode (batch or individual)
1137 |      */
1138 |     setProcessingMode(mode) {
1139 |         this.processingMode = mode;
1140 | 
1141 |         // Update button states (using cached DOM elements)
1142 |         if (this.batchModeBtn) {
1143 |             this.batchModeBtn.classList.toggle('active', mode === 'batch');
1144 |         }
1145 |         if (this.individualModeBtn) {
1146 |             this.individualModeBtn.classList.toggle('active', mode === 'individual');
1147 |         }
1148 |         if (this.modeDescription) {
1149 |             this.modeDescription.innerHTML = mode === 'batch'
1150 |                 ? '<small>All selected files will be processed together with the same tags.</small>'
1151 |                 : '<small>Each file will be processed individually with the same tags.</small>';
1152 |         }
1153 | 
1154 |         console.log(`Processing mode set to: ${mode}`);
1155 |     }
1156 | 
1157 |     /**
1158 |      * Handle document upload
1159 |      */
1160 |     async handleDocumentUpload() {
1161 |         if (!this.selectedFiles || this.selectedFiles.length === 0) {
1162 |             this.showToast(this.t('toast.noFiles', 'No files selected'), 'error');
1163 |             return;
1164 |         }
1165 | 
1166 |         const tags = document.getElementById('docTags')?.value || '';
1167 |         const chunkSize = this.chunkSizeInput?.value || 1000;
1168 |         const chunkOverlap = this.chunkOverlapInput?.value || 200;
1169 |         const memoryType = this.memoryTypeInput?.value || 'document';
1170 | 
1171 |         try {
1172 |             this.setLoading(true);
1173 | 
1174 |             if (this.selectedFiles.length === 1 || this.processingMode === 'individual') {
1175 |                 // Individual file processing (single file or individual mode for multiple files)
1176 |                 for (let i = 0; i < this.selectedFiles.length; i++) {
1177 |                     const file = this.selectedFiles[i];
1178 |                     try {
1179 |                         await this.uploadSingleDocument(file, {
1180 |                             tags,
1181 |                             chunk_size: parseInt(chunkSize),
1182 |                             chunk_overlap: parseInt(chunkOverlap),
1183 |                             memory_type: memoryType
1184 |                         });
1185 | 
1186 |                         // Small delay between individual uploads to avoid overwhelming the server
1187 |                         if (i < this.selectedFiles.length - 1) {
1188 |                             await new Promise(resolve => setTimeout(resolve, this.constructor.INDIVIDUAL_UPLOAD_DELAY));
1189 |                         }
1190 |                     } catch (error) {
1191 |                         console.error(`Failed to upload ${file.name}:`, error);
1192 |                         this.showToast(
1193 |                             this.t('toast.uploadFileFail', 'Failed to upload {name}: {message}', {
1194 |                                 name: file.name,
1195 |                                 message: error.message
1196 |                             }),
1197 |                             'error'
1198 |                         );
1199 |                         // Continue with remaining files
1200 |                     }
1201 |                 }
1202 |             } else {
1203 |                 // Batch upload
1204 |                 await this.uploadBatchDocuments(this.selectedFiles, {
1205 |                     tags,
1206 |                     chunk_size: parseInt(chunkSize),
1207 |                     chunk_overlap: parseInt(chunkOverlap),
1208 |                     memory_type: memoryType
1209 |                 });
1210 |             }
1211 | 
1212 |             // Clear selection and reload history
1213 |             this.selectedFiles = [];
1214 |             this.updateUploadButton();
1215 |             await this.loadUploadHistory();
1216 | 
1217 |             // Reset drop zone
1218 |             const dropZone = document.getElementById('dropZone');
1219 |             if (dropZone) {
1220 |                 const content = dropZone.querySelector('.drop-zone-content');
1221 |                 if (content) {
1222 |                     content.innerHTML = `
1223 |                         <svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
1224 |                             <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
1225 |                         </svg>
1226 |                         <h3>Drag & drop files here</h3>
1227 |                         <p>or <button id="fileSelectBtn" class="link-button">browse to select files</button></p>
1228 |                         <p class="supported-formats">Supported formats: PDF, DOCX, PPTX, TXT, MD, JSON</p>
1229 |                         <input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
1230 |                     `;
1231 |                 }
1232 |                 // Re-setup event listeners for the new elements
1233 |                 this.setupDocumentsEventListeners();
1234 |             }
1235 | 
1236 |         } catch (error) {
1237 |             console.error('Upload error:', error);
1238 |             this.showToast(this.t('toast.uploadFailed', 'Upload failed: {message}', { message: error.message }), 'error');
1239 |         } finally {
1240 |             this.setLoading(false);
1241 |         }
1242 |     }
1243 | 
1244 |     /**
1245 |      * Upload single document
1246 |      */
1247 |     async uploadSingleDocument(file, config) {
1248 |         console.log(`Uploading file: ${file.name}, size: ${file.size} bytes`);
1249 |         const formData = new FormData();
1250 |         formData.append('file', file);
1251 |         formData.append('tags', config.tags);
1252 |         formData.append('chunk_size', config.chunk_size.toString());
1253 |         formData.append('chunk_overlap', config.chunk_overlap.toString());
1254 |         formData.append('memory_type', config.memory_type);
1255 | 
1256 |         const response = await fetch(`${this.apiBase}/documents/upload`, {
1257 |             method: 'POST',
1258 |             body: formData
1259 |         });
1260 | 
1261 |         console.log(`Upload response status: ${response.status}`);
1262 | 
1263 |         if (!response.ok) {
1264 |             let errorMessage = `Upload failed with status ${response.status}`;
1265 |             try {
1266 |                 const error = await response.json();
1267 |                 console.error('Upload error details:', error);
1268 |                 errorMessage = error.detail || error.message || errorMessage;
1269 |             } catch (e) {
1270 |                 console.error('Could not parse error response:', e);
1271 |                 try {
1272 |                     const errorText = await response.text();
1273 |                     console.error('Error response text:', errorText);
1274 |                     errorMessage = errorText || errorMessage;
1275 |                 } catch (e2) {
1276 |                     console.error('Could not read error response:', e2);
1277 |                 }
1278 |             }
1279 |             throw new Error(errorMessage);
1280 |         }
1281 | 
1282 |         const result = await response.json();
1283 |         console.log('Upload result:', result);
1284 |         this.showToast(this.t('toast.uploadStartedSingle', 'Upload started for {name}', { name: file.name }), 'success');
1285 | 
1286 |         // Monitor progress if we have an upload ID
1287 |         if (result.upload_id) {
1288 |             this.monitorUploadProgress(result.upload_id);
1289 |         }
1290 | 
1291 |         return result;
1292 |     }
1293 | 
1294 |     /**
1295 |      * Upload batch documents
1296 |      */
1297 |     async uploadBatchDocuments(files, config) {
1298 |         const formData = new FormData();
1299 |         files.forEach(file => {
1300 |             formData.append('files', file);
1301 |         });
1302 |         formData.append('tags', config.tags);
1303 |         formData.append('chunk_size', config.chunk_size.toString());
1304 |         formData.append('chunk_overlap', config.chunk_overlap.toString());
1305 |         formData.append('memory_type', config.memory_type);
1306 | 
1307 |         const response = await fetch(`${this.apiBase}/documents/batch-upload`, {
1308 |             method: 'POST',
1309 |             body: formData
1310 |         });
1311 | 
1312 |         if (!response.ok) {
1313 |             const error = await response.json();
1314 |             throw new Error(error.detail || 'Batch upload failed');
1315 |         }
1316 | 
1317 |         const result = await response.json();
1318 |         this.showToast(
1319 |             this.t('toast.uploadStartedBatch', 'Batch upload started for {count} files', { count: files.length }),
1320 |             'success'
1321 |         );
1322 | 
1323 |         // Monitor progress if we have an upload ID
1324 |         if (result.upload_id) {
1325 |             this.monitorUploadProgress(result.upload_id);
1326 |         }
1327 | 
1328 |         return result;
1329 |     }
1330 | 
1331 |     /**
1332 |      * Monitor upload progress by polling status endpoint
1333 |      */
1334 |     monitorUploadProgress(uploadId) {
1335 |         const pollStatus = async () => {
1336 |             try {
1337 |                 const statusResponse = await this.apiCall(`/documents/status/${uploadId}`);
1338 |                 this.updateUploadProgress(uploadId, statusResponse);
1339 | 
1340 |                 if (statusResponse.progress >= 100 || statusResponse.status === 'completed' || statusResponse.status === 'failed') {
1341 |                     // Upload completed, refresh history
1342 |                     this.loadUploadHistory();
1343 |                 } else {
1344 |                     // Continue polling
1345 |                     setTimeout(pollStatus, 2000); // Poll every 2 seconds
1346 |                 }
1347 |             } catch (error) {
1348 |                 // If polling fails, try again with longer interval
1349 |                 setTimeout(pollStatus, 5000);
1350 |             }
1351 |         };
1352 | 
1353 |         // Start polling after a short delay
1354 |         setTimeout(pollStatus, 1000);
1355 |     }
1356 | 
1357 |     /**
1358 |      * Update upload progress display
1359 |      */
1360 |     updateUploadProgress(uploadId, statusData) {
1361 |         // Find the upload item in history and update it
1362 |         const historyContainer = document.getElementById('uploadHistory');
1363 |         if (!historyContainer) return;
1364 | 
1365 |         const uploadItems = historyContainer.querySelectorAll('.upload-item');
1366 |         uploadItems.forEach(item => {
1367 |             const filename = item.querySelector('.upload-filename');
1368 |             if (filename && filename.textContent.includes(uploadId)) {
1369 |                 // This is a simplified update - in practice you'd match by upload ID
1370 |                 this.loadUploadHistory(); // For now, just refresh the entire history
1371 |             }
1372 |         });
1373 |     }
1374 | 
1375 |     /**
1376 |      * Check hybrid backend sync status
1377 |      */
1378 |     async checkSyncStatus() {
1379 |         // Skip UI updates during force sync to prevent periodic polling from overwriting the syncing state
1380 |         if (this._isForceSyncing) {
1381 |             return;
1382 |         }
1383 | 
1384 |         try {
1385 |             const syncStatus = await this.apiCall('/sync/status');
1386 | 
1387 |             // Get compact sync control element
1388 |             const syncControl = document.getElementById('syncControl');
1389 |             if (!syncControl) {
1390 |                 console.warn('Sync control element not found');
1391 |                 return;
1392 |             }
1393 | 
1394 |             if (!syncStatus.is_hybrid) {
1395 |                 syncControl.style.display = 'none';
1396 |                 return;
1397 |             }
1398 | 
1399 |             // Show sync control for hybrid mode
1400 |             syncControl.style.display = 'block';
1401 | 
1402 |             // Update sync status UI elements
1403 |             const statusText = document.getElementById('syncStatusText');
1404 |             const syncProgress = document.getElementById('syncProgress');
1405 |             const pauseButton = document.getElementById('pauseSyncButton');
1406 |             const resumeButton = document.getElementById('resumeSyncButton');
1407 |             const syncButton = document.getElementById('forceSyncButton');
1408 | 
1409 |             // Update pause/resume button visibility based on running state
1410 |             const isPaused = syncStatus.is_paused || !syncStatus.is_running;
1411 | 
1412 |             // Attach event listeners if not already attached
1413 |             if (pauseButton && !pauseButton._listenerAttached) {
1414 |                 pauseButton.addEventListener('click', () => {
1415 |                     this.pauseSync();
1416 |                 });
1417 |                 pauseButton._listenerAttached = true;
1418 |             }
1419 |             if (resumeButton && !resumeButton._listenerAttached) {
1420 |                 resumeButton.addEventListener('click', () => {
1421 |                     this.resumeSync();
1422 |                 });
1423 |                 resumeButton._listenerAttached = true;
1424 |             }
1425 |             if (syncButton && !syncButton._listenerAttached) {
1426 |                 syncButton.addEventListener('click', () => {
1427 |                     this.forceSync();
1428 |                 });
1429 |                 syncButton._listenerAttached = true;
1430 |             }
1431 | 
1432 |             if (pauseButton) {
1433 |                 pauseButton.style.display = isPaused ? 'none' : 'flex';
1434 |             }
1435 |             if (resumeButton) {
1436 |                 resumeButton.style.display = isPaused ? 'flex' : 'none';
1437 |             }
1438 | 
1439 |             // Determine status and update UI (dot color is handled by CSS classes)
1440 |             if (isPaused) {
1441 |                 statusText.textContent = 'Paused';
1442 |                 syncProgress.textContent = '';
1443 |                 syncControl.className = 'sync-control-compact paused';
1444 |                 if (syncButton) syncButton.disabled = true;
1445 |             } else if (syncStatus.status === 'syncing') {
1446 |                 statusText.textContent = 'Syncing';
1447 |                 syncProgress.textContent = syncStatus.operations_pending > 0 ? `${syncStatus.operations_pending} pending` : '';
1448 |                 syncControl.className = 'sync-control-compact syncing';
1449 |                 if (syncButton) syncButton.disabled = true;
1450 |             } else if (syncStatus.status === 'pending') {
1451 |                 statusText.textContent = 'Pending';
1452 |                 syncProgress.textContent = `${syncStatus.operations_pending} ops`;
1453 |                 syncControl.className = 'sync-control-compact pending';
1454 |                 if (syncButton) syncButton.disabled = false;
1455 |             } else if (syncStatus.status === 'error') {
1456 |                 statusText.textContent = 'Error';
1457 |                 syncProgress.textContent = `${syncStatus.operations_failed} failed`;
1458 |                 syncControl.className = 'sync-control-compact error';
1459 |                 if (syncButton) syncButton.disabled = false;
1460 |             } else {
1461 |                 // synced status
1462 |                 statusText.textContent = 'Synced';
1463 |                 syncProgress.textContent = '';
1464 |                 syncControl.className = 'sync-control-compact synced';
1465 |                 if (syncButton) syncButton.disabled = false;
1466 |             }
1467 | 
1468 |         } catch (error) {
1469 |             console.error('Error checking sync status:', error);
1470 |             // Hide sync control on error (likely not hybrid mode)
1471 |             const syncControl = document.getElementById('syncControl');
1472 |             if (syncControl) syncControl.style.display = 'none';
1473 |         }
1474 |     }
1475 | 
1476 |     /**
1477 |      * Format time delta in human readable format
1478 |      */
1479 |     formatTimeDelta(seconds) {
1480 |         if (seconds < 60) return `${seconds}s ago`;
1481 |         if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
1482 |         if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
1483 |         return `${Math.floor(seconds / 86400)}d ago`;
1484 |     }
1485 | 
1486 |     /**
1487 |      * Pause background sync
1488 |      */
1489 |     async pauseSync() {
1490 |         try {
1491 |             const result = await this.apiCall('/sync/pause', 'POST');
1492 |             if (result.success) {
1493 |                 this.showToast(this.t('toast.syncPaused', 'Sync paused'), 'success');
1494 | 
1495 |                 // Update UI immediately using API response data
1496 |                 const pauseButton = document.getElementById('pauseSyncButton');
1497 |                 const resumeButton = document.getElementById('resumeSyncButton');
1498 |                 const statusText = document.getElementById('syncStatusText');
1499 |                 const syncControl = document.getElementById('syncControl');
1500 | 
1501 |                 if (pauseButton) pauseButton.style.display = 'none';
1502 |                 if (resumeButton) resumeButton.style.display = 'flex';
1503 |                 if (statusText) statusText.textContent = 'Paused';
1504 |                 if (syncControl) syncControl.className = 'sync-control-compact paused';
1505 | 
1506 |                 // Small delay to allow backend state to propagate before checking status
1507 |                 await new Promise(resolve => setTimeout(resolve, 200));
1508 |             } else {
1509 |                 this.showToast(
1510 |                     this.t('toast.syncPauseFailWithReason', 'Failed to pause sync: {reason}', { reason: result.message }),
1511 |                     'error'
1512 |                 );
1513 |             }
1514 |             await this.checkSyncStatus();
1515 |         } catch (error) {
1516 |             console.error('Error pausing sync:', error);
1517 |             this.showToast(this.t('toast.syncPauseFail', 'Failed to pause sync'), 'error');
1518 |         }
1519 |     }
1520 | 
1521 |     /**
1522 |      * Resume background sync
1523 |      */
1524 |     async resumeSync() {
1525 |         try {
1526 |             const result = await this.apiCall('/sync/resume', 'POST');
1527 |             if (result.success) {
1528 |                 this.showToast(this.t('toast.syncResumed', 'Sync resumed'), 'success');
1529 | 
1530 |                 // Update UI immediately using API response data
1531 |                 const pauseButton = document.getElementById('pauseSyncButton');
1532 |                 const resumeButton = document.getElementById('resumeSyncButton');
1533 |                 const statusText = document.getElementById('syncStatusText');
1534 |                 const syncControl = document.getElementById('syncControl');
1535 | 
1536 |                 if (pauseButton) pauseButton.style.display = 'flex';
1537 |                 if (resumeButton) resumeButton.style.display = 'none';
1538 |                 if (statusText) statusText.textContent = 'Synced';
1539 |                 if (syncControl) syncControl.className = 'sync-control-compact synced';
1540 | 
1541 |                 // Small delay to allow backend state to propagate before checking status
1542 |                 await new Promise(resolve => setTimeout(resolve, 200));
1543 |             } else {
1544 |                 this.showToast(
1545 |                     this.t('toast.syncResumeFailWithReason', 'Failed to resume sync: {reason}', { reason: result.message }),
1546 |                     'error'
1547 |                 );
1548 |             }
1549 |             await this.checkSyncStatus();
1550 |         } catch (error) {
1551 |             console.error('Error resuming sync:', error);
1552 |             this.showToast(this.t('toast.syncResumeFail', 'Failed to resume sync'), 'error');
1553 |         }
1554 |     }
1555 | 
1556 |     /**
1557 |      * Check backup status and update Settings modal
1558 |      */
1559 |     async checkBackupStatus() {
1560 |         try {
1561 |             const backupStatus = await this.apiCall('/backup/status');
1562 | 
1563 |             // Update backup elements in Settings modal
1564 |             const lastBackup = document.getElementById('settingsLastBackup');
1565 |             const backupCount = document.getElementById('settingsBackupCount');
1566 |             const nextBackup = document.getElementById('settingsNextBackup');
1567 | 
1568 |             if (!backupStatus.enabled) {
1569 |                 if (lastBackup) lastBackup.textContent = 'Backups disabled';
1570 |                 if (backupCount) backupCount.textContent = '-';
1571 |                 if (nextBackup) nextBackup.textContent = '-';
1572 |                 return;
1573 |             }
1574 | 
1575 |             // Update last backup time
1576 |             if (lastBackup) {
1577 |                 if (backupStatus.time_since_last_seconds) {
1578 |                     lastBackup.textContent = this.formatTimeDelta(Math.floor(backupStatus.time_since_last_seconds)) + ' ago';
1579 |                 } else {
1580 |                     lastBackup.textContent = 'Never';
1581 |                 }
1582 |             }
1583 | 
1584 |             // Update backup count with size
1585 |             if (backupCount) {
1586 |                 const sizeMB = (backupStatus.total_size_bytes / 1024 / 1024).toFixed(1);
1587 |                 backupCount.textContent = `${backupStatus.backup_count} (${sizeMB} MB)`;
1588 |             }
1589 | 
1590 |             // Update next scheduled backup
1591 |             if (nextBackup && backupStatus.next_backup_at) {
1592 |                 const nextDate = new Date(backupStatus.next_backup_at);
1593 |                 nextBackup.textContent = nextDate.toLocaleString();
1594 |             } else if (nextBackup) {
1595 |                 nextBackup.textContent = backupStatus.scheduler_running ? 'Scheduled' : 'Not scheduled';
1596 |             }
1597 | 
1598 |         } catch (error) {
1599 |             console.error('Error checking backup status:', error);
1600 |         }
1601 |     }
1602 | 
1603 |     /**
1604 |      * Create a backup manually
1605 |      */
1606 |     async createBackup() {
1607 |         const backupButton = document.getElementById('backupNowButton');
1608 |         if (backupButton) backupButton.disabled = true;
1609 | 
1610 |         try {
1611 |             this.showToast(this.t('toast.backupCreating', 'Creating backup...'), 'info');
1612 |             const result = await this.apiCall('/backup/now', 'POST');
1613 | 
1614 |             if (result.success) {
1615 |                 const sizeMB = (result.size_bytes / 1024 / 1024).toFixed(2);
1616 |                 this.showToast(
1617 |                     this.t('toast.backupCreated', 'Backup created: {name} ({size} MB)', {
1618 |                         name: result.filename,
1619 |                         size: sizeMB
1620 |                     }),
1621 |                     'success'
1622 |                 );
1623 |             } else {
1624 |                 this.showToast(
1625 |                     this.t('toast.backupFailedWithReason', 'Backup failed: {reason}', { reason: result.error }),
1626 |                     'error'
1627 |                 );
1628 |             }
1629 | 
1630 |             await this.checkBackupStatus();
1631 | 
1632 |         } catch (error) {
1633 |             console.error('Error creating backup:', error);
1634 |             this.showToast(this.t('toast.backupFailed', 'Failed to create backup'), 'error');
1635 |         } finally {
1636 |             if (backupButton) backupButton.disabled = false;
1637 |         }
1638 |     }
1639 | 
1640 |     /**
1641 |      * Start periodic sync status monitoring
1642 |      */
1643 |     startSyncStatusMonitoring() {
1644 |         // Check sync status every 10 seconds
1645 |         setInterval(() => {
1646 |             this.checkSyncStatus();
1647 |         }, 10000);
1648 |     }
1649 | 
1650 |     /**
1651 |      * Manually force sync to Cloudflare
1652 |      */
1653 |     async forceSync() {
1654 |         const syncButton = document.getElementById('forceSyncButton');
1655 |         const originalText = syncButton.innerHTML;
1656 | 
1657 |         try {
1658 |             // Check if sync was paused before force sync
1659 |             const statusBefore = await this.apiCall('/sync/status');
1660 |             const wasPaused = statusBefore.is_paused;
1661 | 
1662 |             // Set flag to prevent periodic polling from overwriting UI during force sync
1663 |             this._isForceSyncing = true;
1664 | 
1665 |             // Disable button and show loading state (just egg timer, no text - widget shows "Syncing")
1666 |             syncButton.disabled = true;
1667 |             syncButton.innerHTML = '<span class="sync-button-icon">⏳</span>';
1668 | 
1669 |             // IMMEDIATELY update sync control widget to show syncing state
1670 |             const statusText = document.getElementById('syncStatusText');
1671 |             const syncProgress = document.getElementById('syncProgress');
1672 |             const syncControl = document.getElementById('syncControl');
1673 | 
1674 |             if (statusText) statusText.textContent = 'Syncing';
1675 |             if (syncProgress) syncProgress.textContent = statusBefore.operations_pending > 0 ? `${statusBefore.operations_pending} pending` : '';
1676 |             if (syncControl) syncControl.className = 'sync-control-compact syncing';
1677 | 
1678 |             // Show toast when sync starts
1679 |             this.showToast(this.t('toast.syncStarting', 'Starting sync...'), 'info');
1680 | 
1681 |             const result = await this.apiCall('/sync/force', 'POST');
1682 | 
1683 |             if (result.success) {
1684 |                 this.showToast(
1685 |                     this.t('toast.syncCompleted', 'Synced {count} operations in {seconds}s', {
1686 |                         count: result.operations_synced,
1687 |                         seconds: result.time_taken_seconds
1688 |                     }),
1689 |                     'success'
1690 |                 );
1691 | 
1692 |                 // Refresh dashboard data to show newly synced memories
1693 |                 if (this.currentView === 'dashboard') {
1694 |                     await this.loadDashboardData();
1695 |                 }
1696 | 
1697 |                 // If sync was paused before, pause it again after force sync
1698 |                 if (wasPaused) {
1699 |                     await this.apiCall('/sync/pause', 'POST');
1700 |                 }
1701 |             } else {
1702 |                 this.showToast(
1703 |                     this.t('toast.syncFailedWithReason', 'Sync failed: {reason}', { reason: result.message }),
1704 |                     'error'
1705 |                 );
1706 |             }
1707 | 
1708 |         } catch (error) {
1709 |             console.error('Error forcing sync:', error);
1710 |             this.showToast(
1711 |                 this.t('toast.syncForceFailed', 'Failed to force sync: {reason}', { reason: error.message }),
1712 |                 'error'
1713 |             );
1714 |         } finally {
1715 |             // Clear flag to allow periodic polling to resume
1716 |             this._isForceSyncing = false;
1717 | 
1718 |             // Re-enable button
1719 |             syncButton.disabled = false;
1720 |             syncButton.innerHTML = originalText;
1721 | 
1722 |             // Refresh sync status immediately
1723 |             await this.checkSyncStatus();
1724 |         }
1725 |     }
1726 | 
1727 |     /**
1728 |      * Render tags cloud from API data
1729 |      */
1730 |     renderTagsCloud() {
1731 |         const container = document.getElementById('tagsCloudContainer');
1732 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1733 | 
1734 |         // Hide the tagged memories view initially
1735 |         taggedContainer.style.display = 'none';
1736 | 
1737 |         if (!this.tags || this.tags.length === 0) {
1738 |             container.innerHTML = '<p class="text-neutral-600">No tags found. Start adding tags to your memories to see them here.</p>';
1739 |             return;
1740 |         }
1741 | 
1742 |         // Render tag bubbles (tags are already sorted by count from backend)
1743 |         container.innerHTML = this.tags.map(tagData => `
1744 |             <button class="tag-bubble" data-tag="${this.escapeHtml(tagData.tag)}">
1745 |                 ${this.escapeHtml(tagData.tag)}
1746 |                 <span class="count">${tagData.count}</span>
1747 |             </button>
1748 |         `).join('');
1749 |     }
1750 | 
1751 |     /**
1752 |      * Filter memories by selected tag
1753 |      */
1754 |     async filterByTag(tag) {
1755 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1756 |         const tagNameSpan = document.getElementById('selectedTagName');
1757 |         const memoriesList = document.getElementById('taggedMemoriesList');
1758 | 
1759 |         try {
1760 |             // Fetch memories for this specific tag
1761 |             const memoriesResponse = await this.apiCall(`/memories?tag=${encodeURIComponent(tag)}&limit=100`);
1762 |             const filteredMemories = memoriesResponse.memories || [];
1763 | 
1764 |             // Show the tagged memories section
1765 |             tagNameSpan.textContent = tag;
1766 |             taggedContainer.style.display = 'block';
1767 | 
1768 |             // Smooth scroll to results section for better UX
1769 |             taggedContainer.scrollIntoView({
1770 |                 behavior: 'smooth',
1771 |                 block: 'start'
1772 |             });
1773 | 
1774 |             // Render filtered memories
1775 |             this.renderMemoriesInContainer(filteredMemories, memoriesList);
1776 | 
1777 |             // Add event listener for clear filter button
1778 |             const clearBtn = document.getElementById('clearTagFilter');
1779 |             clearBtn.onclick = () => this.clearTagFilter();
1780 |         } catch (error) {
1781 |             console.error('Error filtering by tag:', error);
1782 |             this.showToast(this.t('toast.loadTagFail', 'Failed to load memories for tag'), 'error');
1783 |         }
1784 |     }
1785 | 
1786 |     /**
1787 |      * Clear tag filter and show all tags
1788 |      */
1789 |     clearTagFilter() {
1790 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1791 |         taggedContainer.style.display = 'none';
1792 |     }
1793 | 
1794 |     /**
1795 |      * Escape HTML to prevent XSS
1796 |      */
1797 |     escapeHtml(text) {
1798 |         const div = document.createElement('div');
1799 |         div.textContent = text;
1800 |         return div.innerHTML;
1801 |     }
1802 | 
1803 |     /**
1804 |      * Get time-of-day emoji icon based on timestamp
1805 |      * @param {number} timestamp - Unix timestamp in seconds
1806 |      * @returns {string} HTML with emoji + tooltip
1807 |      */
1808 |     renderTimeIcon(timestamp) {
1809 |         if (!timestamp) return '';
1810 | 
1811 |         const date = new Date(timestamp * 1000);
1812 |         const hour = date.getHours(); // Local timezone
1813 |         const segment = Math.floor(hour / 3); // 0-7
1814 | 
1815 |         const icons = ['🌙', '🌅', '☕', '💻', '🍽️', '⛅', '🍷', '🛏️'];
1816 |         const labels = [
1817 |             'Late Night',    // 00-03
1818 |             'Early Morning', // 03-06
1819 |             'Morning',       // 06-09
1820 |             'Late Morning',  // 09-12
1821 |             'Afternoon',     // 12-15
1822 |             'Late Afternoon',// 15-18
1823 |             'Evening',       // 18-21
1824 |             'Night'          // 21-24
1825 |         ];
1826 | 
1827 |         return `<span class="time-icon" title="${labels[segment]}">${icons[segment]}</span>`;
1828 |     }
1829 | 
1830 |     /**
1831 |      * Toggle chunking help section visibility
1832 |      */
1833 |     toggleChunkingHelp() {
1834 |         const helpSection = document.getElementById('chunkingHelpSection');
1835 |         if (helpSection) {
1836 |             if (helpSection.style.display === 'none') {
1837 |                 helpSection.style.display = 'block';
1838 |             } else {
1839 |                 helpSection.style.display = 'none';
1840 |             }
1841 |         }
1842 |     }
1843 | 
1844 |     /**
1845 |      * Hide chunking help section
1846 |      */
1847 |     hideChunkingHelp() {
1848 |         const helpSection = document.getElementById('chunkingHelpSection');
1849 |         if (helpSection) {
1850 |             helpSection.style.display = 'none';
1851 |         }
1852 |     }
1853 | 
1854 |     /**
1855 |      * Toggle overlap help section visibility
1856 |      */
1857 |     toggleOverlapHelp() {
1858 |         const helpSection = document.getElementById('overlapHelpSection');
1859 |         if (helpSection) {
1860 |             if (helpSection.style.display === 'none') {
1861 |                 helpSection.style.display = 'block';
1862 |             } else {
1863 |                 helpSection.style.display = 'none';
1864 |             }
1865 |         }
1866 |     }
1867 | 
1868 |     /**
1869 |      * Hide overlap help section
1870 |      */
1871 |     hideOverlapHelp() {
1872 |         const helpSection = document.getElementById('overlapHelpSection');
1873 |         if (helpSection) {
1874 |             helpSection.style.display = 'none';
1875 |         }
1876 |     }
1877 | 
1878 |     /**
1879 |      * Toggle processing mode help section visibility
1880 |      */
1881 |     toggleProcessingModeHelp() {
1882 |         const helpSection = document.getElementById('processingModeHelpSection');
1883 |         if (helpSection) {
1884 |             helpSection.style.display = helpSection.style.display === 'block' ? 'none' : 'block';
1885 |         }
1886 |     }
1887 | 
1888 |     /**
1889 |      * Hide processing mode help section
1890 |      */
1891 |     hideProcessingModeHelp() {
1892 |         const helpSection = document.getElementById('processingModeHelpSection');
1893 |         if (helpSection) {
1894 |             helpSection.style.display = 'none';
1895 |         }
1896 |     }
1897 | 
1898 |     /**
1899 |      * Render memories in a specific container
1900 |      */
1901 |     renderMemoriesInContainer(memories, container) {
1902 |         if (!memories || memories.length === 0) {
1903 |             container.innerHTML = '<p class="empty-state">No memories found with this tag.</p>';
1904 |             return;
1905 |         }
1906 | 
1907 |         container.innerHTML = memories.map(memory => this.renderMemoryCard(memory)).join('');
1908 | 
1909 |         // Add click handlers
1910 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
1911 |             card.addEventListener('click', () => this.handleMemoryClick(memories[index]));
1912 |         });
1913 |     }
1914 | 
1915 |     /**
1916 |      * Handle navigation between views
1917 |      */
1918 |     handleNavigation(e) {
1919 |         const viewName = e.currentTarget.dataset.view;
1920 |         this.switchView(viewName);
1921 |     }
1922 | 
1923 |     /**
1924 |      * Switch between different views
1925 |      */
1926 |     switchView(viewName) {
1927 |         // Update navigation active state (if navigation exists)
1928 |         document.querySelectorAll('.nav-item').forEach(item => {
1929 |             item.classList.remove('active');
1930 |         });
1931 |         const navItem = document.querySelector(`[data-view="${viewName}"]`);
1932 |         if (navItem) {
1933 |             navItem.classList.add('active');
1934 |         }
1935 | 
1936 |         // Hide all views (if view containers exist)
1937 |         document.querySelectorAll('.view-container').forEach(view => {
1938 |             view.classList.remove('active');
1939 |         });
1940 | 
1941 |         // Show target view (if it exists)
1942 |         const targetView = document.getElementById(`${viewName}View`);
1943 |         if (targetView) {
1944 |             targetView.classList.add('active');
1945 |             this.currentView = viewName;
1946 | 
1947 |             // Load view-specific data
1948 |             this.loadViewData(viewName);
1949 |         }
1950 |     }
1951 | 
1952 |     /**
1953 |      * Load data specific to the current view
1954 |      */
1955 |     async loadViewData(viewName) {
1956 |         switch (viewName) {
1957 |             case 'search':
1958 |                 // Initialize search view with recent search or empty state
1959 |                 break;
1960 |             case 'browse':
1961 |                 await this.loadBrowseData();
1962 |                 break;
1963 |             case 'documents':
1964 |                 await this.loadDocumentsData();
1965 |                 break;
1966 |             case 'manage':
1967 |                 await this.loadManageData();
1968 |                 break;
1969 |             case 'analytics':
1970 |                 await this.loadAnalyticsData();
1971 |                 break;
1972 |             case 'qualityAnalytics':
1973 |                 await this.loadQualityAnalytics();
1974 |                 break;
1975 |             case 'apiDocs':
1976 |                 // API docs view - static content, no additional loading needed
1977 |                 break;
1978 |             default:
1979 |                 // Dashboard view is loaded in loadDashboardData
1980 |                 break;
1981 |         }
1982 |     }
1983 | 
1984 |     /**
1985 |      * Handle quick search input
1986 |      */
1987 |     async handleQuickSearch(e) {
1988 |         const query = e.target.value.trim();
1989 |         if (query.length >= 2) {
1990 |             try {
1991 |                 const results = await this.searchMemories(query);
1992 |                 // Could show dropdown suggestions here
1993 |             } catch (error) {
1994 |                 console.error('Quick search error:', error);
1995 |             }
1996 |         }
1997 |     }
1998 | 
1999 |     /**
2000 |      * Handle full search
2001 |      */
2002 |     async handleSearch(query) {
2003 |         if (!query.trim()) return;
2004 | 
2005 |         this.switchView('search');
2006 |         this.setLoading(true);
2007 | 
2008 |         try {
2009 |             const results = await this.searchMemories(query);
2010 |             this.searchResults = results;
2011 |             this.renderSearchResults(results);
2012 |             this.updateResultsCount(results.length);
2013 |         } catch (error) {
2014 |             console.error('Search error:', error);
2015 |             this.showToast(this.t('toast.searchFailed', 'Search failed'), 'error');
2016 |         } finally {
2017 |             this.setLoading(false);
2018 |         }
2019 |     }
2020 | 
2021 |     /**
2022 |      * Search memories using the API
2023 |      */
2024 |     async searchMemories(query, filters = {}) {
2025 |         // Detect tag search patterns: #tag, tag:value, or "tag:value"
2026 |         const tagPattern = /^(#|tag:)(.+)$/i;
2027 |         const tagMatch = query.match(tagPattern);
2028 | 
2029 |         if (tagMatch) {
2030 |             // Use tag search endpoint
2031 |             const tagValue = tagMatch[2].trim();
2032 |             const payload = {
2033 |                 tags: [tagValue],
2034 |                 match_all: false // ANY match by default
2035 |             };
2036 | 
2037 |             const response = await this.apiCall('/search/by-tag', 'POST', payload);
2038 |             return response.results || [];
2039 |         } else {
2040 |             // Use semantic search endpoint
2041 |             const payload = {
2042 |                 query: query,
2043 |                 n_results: filters.limit || 20,
2044 |                 ...filters
2045 |             };
2046 | 
2047 |             // Only add similarity_threshold if explicitly set in filters
2048 |             if (filters.threshold !== undefined) {
2049 |                 payload.similarity_threshold = filters.threshold;
2050 |             }
2051 | 
2052 |             const response = await this.apiCall('/search', 'POST', payload);
2053 |             return response.results || [];
2054 |         }
2055 |     }
2056 | 
2057 |     /**
2058 |      * Handle filter changes in search view
2059 |      */
2060 |     async handleFilterChange() {
2061 |         const tagFilter = document.getElementById('tagFilter')?.value;
2062 |         const dateFilter = document.getElementById('dateFilter')?.value;
2063 |         const typeFilter = document.getElementById('typeFilter')?.value;
2064 |         const query = document.getElementById('quickSearch')?.value?.trim() || '';
2065 | 
2066 |         // Add loading state
2067 |         const applyBtn = document.getElementById('applyFiltersBtn');
2068 |         if (applyBtn) {
2069 |             applyBtn.classList.add('loading');
2070 |             applyBtn.disabled = true;
2071 |         }
2072 | 
2073 |         try {
2074 |             let results = [];
2075 | 
2076 |             // Priority 1: If we have a semantic query, start with semantic search
2077 |             if (query) {
2078 |                 const filters = {};
2079 |                 if (typeFilter) filters.type = typeFilter;
2080 |                 results = await this.searchMemories(query, filters);
2081 | 
2082 |                 // Apply tag filtering to semantic search results if tags are specified
2083 |                 if (tagFilter && tagFilter.trim()) {
2084 |                     const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
2085 |                     if (tags.length > 0) {
2086 |                         results = results.filter(result => {
2087 |                             const memoryTags = result.memory.tags || [];
2088 |                             // Check if any of the specified tags match memory tags (case-insensitive)
2089 |                             return tags.some(filterTag =>
2090 |                                 memoryTags.some(memoryTag =>
2091 |                                     memoryTag.toLowerCase().includes(filterTag.toLowerCase())
2092 |                                 )
2093 |                             );
2094 |                         });
2095 |                     }
2096 |                 }
2097 |             }
2098 |             // Priority 2: Tag-only search (when no semantic query)
2099 |             else if (tagFilter && tagFilter.trim()) {
2100 |                 const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
2101 | 
2102 |                 if (tags.length > 0) {
2103 |                     const payload = {
2104 |                         tags: tags,
2105 |                         match_all: false // ANY match by default
2106 |                     };
2107 | 
2108 |                     const response = await this.apiCall('/search/by-tag', 'POST', payload);
2109 |                     results = response.results || [];
2110 | 
2111 |                     // Apply type filter if present
2112 |                     if (typeFilter && typeFilter.trim()) {
2113 |                         results = results.filter(result => {
2114 |                             const memoryType = result.memory.memory_type || 'note';
2115 |                             return memoryType === typeFilter;
2116 |                         });
2117 |                     }
2118 |                 }
2119 |             }
2120 |             // Priority 3: Date-based search
2121 |             else if (dateFilter && dateFilter.trim()) {
2122 |                 const payload = {
2123 |                     query: dateFilter,
2124 |                     n_results: 100
2125 |                 };
2126 |                 const response = await this.apiCall('/search/by-time', 'POST', payload);
2127 |                 results = response.results || [];
2128 | 
2129 |                 // Apply type filter if present
2130 |                 if (typeFilter && typeFilter.trim()) {
2131 |                     results = results.filter(result => {
2132 |                         const memoryType = result.memory.memory_type || 'note';
2133 |                         return memoryType === typeFilter;
2134 |                     });
2135 |                 }
2136 |             }
2137 |             // Priority 4: Type-only filter
2138 |             else if (typeFilter && typeFilter.trim()) {
2139 |                 const allMemoriesResponse = await this.apiCall('/memories?page=1&page_size=1000');
2140 |                 if (allMemoriesResponse.memories) {
2141 |                     results = allMemoriesResponse.memories
2142 |                         .filter(memory => (memory.memory_type || 'note') === typeFilter)
2143 |                         .map(memory => ({ memory, similarity: 1.0 }));
2144 |                 }
2145 |             } else {
2146 |                 // No filters, clear results
2147 |                 results = [];
2148 |             }
2149 | 
2150 |             this.searchResults = results;
2151 |             this.renderSearchResults(results);
2152 |             this.updateResultsCount(results.length);
2153 |             this.updateActiveFilters();
2154 | 
2155 |         } catch (error) {
2156 |             console.error('Filter search error:', error);
2157 |             this.showToast(this.t('toast.filterSearchFailed', 'Filter search failed'), 'error');
2158 |         } finally {
2159 |             // Remove loading state
2160 |             const applyBtn = document.getElementById('applyFiltersBtn');
2161 |             if (applyBtn) {
2162 |                 applyBtn.classList.remove('loading');
2163 |                 applyBtn.disabled = false;
2164 |             }
2165 |         }
2166 |     }
2167 | 
2168 |     /**
2169 |      * Handle view mode changes (grid/list)
2170 |      */
2171 |     handleViewModeChange(mode) {
2172 |         document.querySelectorAll('.view-btn').forEach(btn => {
2173 |             btn.classList.remove('active');
2174 |         });
2175 |         document.querySelector(`[data-view="${mode}"]`).classList.add('active');
2176 | 
2177 |         const resultsContainer = document.getElementById('searchResultsList');
2178 |         resultsContainer.className = mode === 'grid' ? 'memory-grid' : 'memory-list';
2179 |     }
2180 | 
2181 |     /**
2182 |      * Handle quick actions
2183 |      */
2184 |     handleQuickAction(action) {
2185 |         switch (action) {
2186 |             case 'quick-search':
2187 |                 this.switchView('search');
2188 |                 const searchInput = document.getElementById('quickSearch');
2189 |                 if (searchInput) {
2190 |                     searchInput.focus();
2191 |                 }
2192 |                 break;
2193 |             case 'add-memory':
2194 |                 this.handleAddMemory();
2195 |                 break;
2196 |             case 'browse-tags':
2197 |                 this.switchView('browse');
2198 |                 break;
2199 |             case 'export-data':
2200 |                 this.handleExportData();
2201 |                 break;
2202 |         }
2203 |     }
2204 | 
2205 |     /**
2206 |      * Handle add memory action
2207 |      */
2208 |     handleAddMemory() {
2209 |         const modal = document.getElementById('addMemoryModal');
2210 | 
2211 |         // Reset modal for adding new memory
2212 |         this.resetAddMemoryModal();
2213 | 
2214 |         this.openModal(modal);
2215 |         document.getElementById('memoryContent').focus();
2216 |     }
2217 | 
2218 |     /**
2219 |      * Reset add memory modal to default state
2220 |      */
2221 |     resetAddMemoryModal() {
2222 |         const modal = document.getElementById('addMemoryModal');
2223 |         const title = modal.querySelector('.modal-header h3');
2224 |         const saveBtn = document.getElementById('saveMemoryBtn');
2225 | 
2226 |         // Reset modal title and button text
2227 |         title.textContent = 'Add New Memory';
2228 |         saveBtn.textContent = 'Save Memory';
2229 | 
2230 |         // Clear form
2231 |         document.getElementById('addMemoryForm').reset();
2232 | 
2233 |         // Clear editing state
2234 |         this.editingMemory = null;
2235 |     }
2236 | 
2237 |     /**
2238 |      * Handle save memory
2239 |      */
2240 |     async handleSaveMemory() {
2241 |         const content = document.getElementById('memoryContent').value.trim();
2242 |         const tags = document.getElementById('memoryTags').value.trim();
2243 |         const type = document.getElementById('memoryType').value;
2244 | 
2245 |         if (!content) {
2246 |             this.showToast(this.t('toast.enterMemoryContent', 'Please enter memory content'), 'warning');
2247 |             return;
2248 |         }
2249 | 
2250 |         const payload = {
2251 |             content: content,
2252 |             tags: tags ? tags.split(',').map(t => t.trim()) : [],
2253 |             memory_type: type,
2254 |             metadata: {
2255 |                 created_via: 'dashboard',
2256 |                 user_agent: navigator.userAgent,
2257 |                 updated_via: this.editingMemory ? 'dashboard_edit' : 'dashboard_create'
2258 |             }
2259 |         };
2260 | 
2261 | 
2262 |         try {
2263 |             let response;
2264 |             let successMessage;
2265 | 
2266 |             if (this.editingMemory) {
2267 |                 // Smart update: check if only metadata changed vs content changes
2268 |                 const originalContentHash = this.editingMemory.content_hash;
2269 |                 const contentChanged = this.editingMemory.content !== payload.content;
2270 | 
2271 | 
2272 |                 if (!contentChanged) {
2273 |                     // Only metadata (tags, type, metadata) changed - use PUT endpoint
2274 |                     const updatePayload = {
2275 |                         tags: payload.tags,
2276 |                         memory_type: payload.memory_type,
2277 |                         metadata: payload.metadata
2278 |                     };
2279 | 
2280 |                     response = await this.apiCall(`/memories/${originalContentHash}`, 'PUT', updatePayload);
2281 |                     successMessage = 'Memory updated successfully';
2282 |                 } else {
2283 |                     // Content changed - use create-delete approach (but with proper error handling)
2284 | 
2285 |                     try {
2286 |                         // Step 1: Create updated memory first
2287 |                         response = await this.apiCall('/memories', 'POST', payload);
2288 | 
2289 |                         // CRITICAL: Only proceed with deletion if creation actually succeeded
2290 |                         if (response.success) {
2291 |                             successMessage = 'Memory updated successfully';
2292 | 
2293 |                             try {
2294 |                                 // Step 2: Delete original memory (only after successful creation)
2295 |                                 const deleteResponse = await this.apiCall(`/memories/${originalContentHash}`, 'DELETE');
2296 |                             } catch (deleteError) {
2297 |                                 console.error('Failed to delete original memory after creating new version:', deleteError);
2298 |                                 this.showToast(
2299 |                                     this.t('toast.duplicateMemoryWarning', 'Memory updated, but original version still exists. You may need to manually delete the duplicate.'),
2300 |                                     'warning'
2301 |                                 );
2302 |                             }
2303 |                         } else {
2304 |                             // Creation failed - do NOT delete original memory
2305 |                             console.error('Creation failed:', response.message);
2306 |                             throw new Error(`Failed to create updated memory: ${response.message}`);
2307 |                         }
2308 |                     } catch (createError) {
2309 |                         // CREATE failed - original memory intact, no cleanup needed
2310 |                         console.error('Failed to create updated memory:', createError);
2311 |                         throw new Error(`Failed to update memory: ${createError.message}`);
2312 |                     }
2313 |                 }
2314 |             } else {
2315 |                 // Create new memory
2316 |                 response = await this.apiCall('/memories', 'POST', payload);
2317 |                 successMessage = 'Memory saved successfully';
2318 |             }
2319 | 
2320 |             this.closeModal(document.getElementById('addMemoryModal'));
2321 |             this.showToast(successMessage, 'success');
2322 | 
2323 |             // Reset editing state
2324 |             this.editingMemory = null;
2325 |             this.resetAddMemoryModal();
2326 | 
2327 |             // Refresh current view if needed
2328 |             if (this.currentView === 'dashboard') {
2329 |                 this.loadDashboardData();
2330 |             } else if (this.currentView === 'search') {
2331 |                 // Refresh search results
2332 |                 const query = document.getElementById('searchInput').value.trim();
2333 |                 if (query) {
2334 |                     this.handleSearch(query);
2335 |                 }
2336 |             } else if (this.currentView === 'browse') {
2337 |                 // Refresh browse view (tags cloud)
2338 |                 this.loadBrowseData();
2339 |             }
2340 |         } catch (error) {
2341 |             console.error('Error saving memory:', error);
2342 |             this.showToast(
2343 |                 this.t('toast.saveMemoryFailed', 'Failed to save memory: {reason}', { reason: error.message }),
2344 |                 'error'
2345 |             );
2346 |         }
2347 |     }
2348 | 
2349 |     /**
2350 |      * Handle memory click to show details
2351 |      */
2352 |     handleMemoryClick(memory) {
2353 |         this.showMemoryDetails(memory);
2354 |     }
2355 | 
2356 |     /**
2357 |      * Show memory details in modal
2358 |      */
2359 |     showMemoryDetails(memory) {
2360 |         const modal = document.getElementById('memoryModal');
2361 |         const title = document.getElementById('modalTitle');
2362 |         const content = document.getElementById('modalContent');
2363 | 
2364 |         title.textContent = 'Memory Details';
2365 |         content.innerHTML = this.renderMemoryDetails(memory);
2366 | 
2367 |         // Set up action buttons
2368 |         document.getElementById('editMemoryBtn').onclick = () => this.editMemory(memory);
2369 |         document.getElementById('deleteMemoryBtn').onclick = () => this.deleteMemory(memory);
2370 |         document.getElementById('shareMemoryBtn').onclick = () => this.shareMemory(memory);
2371 | 
2372 |         this.openModal(modal);
2373 |     }
2374 | 
2375 |     /**
2376 |      * Render memory details HTML
2377 |      */
2378 |     renderMemoryDetails(memory) {
2379 |         const createdDate = new Date(memory.created_at * 1000).toLocaleString();
2380 |         const updatedDate = memory.updated_at ? new Date(memory.updated_at * 1000).toLocaleString() : null;
2381 | 
2382 |         return `
2383 |             <div class="memory-detail">
2384 |                 <div class="memory-meta">
2385 |                     <p><strong>Created:</strong> ${createdDate} ${this.renderTimeIcon(memory.created_at)}</p>
2386 |                     ${updatedDate ? `<p><strong>Updated:</strong> ${updatedDate}</p>` : ''}
2387 |                     <p><strong>Type:</strong> ${memory.memory_type || 'note'}</p>
2388 |                     <p><strong>ID:</strong> ${memory.content_hash}</p>
2389 |                 </div>
2390 | 
2391 |                 <div class="memory-content">
2392 |                     <h4>Content</h4>
2393 |                     <div class="content-text">${this.escapeHtml(memory.content)}</div>
2394 |                 </div>
2395 | 
2396 |                 ${memory.tags && memory.tags.length > 0 ? `
2397 |                     <div class="memory-tags-section">
2398 |                         <h4>Tags</h4>
2399 |                         <div class="memory-tags">
2400 |                             ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2401 |                         </div>
2402 |                     </div>
2403 |                 ` : ''}
2404 | 
2405 |                 ${memory.metadata ? `
2406 |                     <div class="memory-metadata">
2407 |                         <h4 class="metadata-toggle" onclick="this.parentElement.classList.toggle('expanded')" style="cursor: pointer; user-select: none;">
2408 |                             <span class="toggle-icon">▶</span> Metadata
2409 |                         </h4>
2410 |                         <div class="metadata-content">
2411 |                             ${this.renderMetadata(memory.metadata)}
2412 |                         </div>
2413 |                     </div>
2414 |                 ` : ''}
2415 |             </div>
2416 |         `;
2417 |     }
2418 | 
2419 |     /**
2420 |      * Render metadata in a prettier format
2421 |      */
2422 |     renderMetadata(metadata) {
2423 |         if (!metadata || typeof metadata !== 'object') {
2424 |             return '<p class="metadata-empty">No metadata available</p>';
2425 |         }
2426 | 
2427 |         let html = '<div class="metadata-items">';
2428 | 
2429 |         for (const [key, value] of Object.entries(metadata)) {
2430 |             let displayValue;
2431 | 
2432 |             if (typeof value === 'string') {
2433 |                 displayValue = `<span class="metadata-string">"${this.escapeHtml(value)}"</span>`;
2434 |             } else if (typeof value === 'number') {
2435 |                 displayValue = `<span class="metadata-number">${value}</span>`;
2436 |             } else if (typeof value === 'boolean') {
2437 |                 displayValue = `<span class="metadata-boolean">${value}</span>`;
2438 |             } else if (Array.isArray(value)) {
2439 |                 displayValue = `<span class="metadata-array">[${value.map(v =>
2440 |                     typeof v === 'string' ? `"${this.escapeHtml(v)}"` : v
2441 |                 ).join(', ')}]</span>`;
2442 |             } else {
2443 |                 displayValue = `<span class="metadata-object">${JSON.stringify(value)}</span>`;
2444 |             }
2445 | 
2446 |             html += `
2447 |                 <div class="metadata-item">
2448 |                     <span class="metadata-key">${this.escapeHtml(key)}:</span>
2449 |                     <span class="metadata-value">${displayValue}</span>
2450 |                 </div>
2451 |             `;
2452 |         }
2453 | 
2454 |         html += '</div>';
2455 |         return html;
2456 |     }
2457 | 
2458 |     /**
2459 |      * Delete memory
2460 |      */
2461 |     async deleteMemory(memory) {
2462 |         if (!confirm('Are you sure you want to delete this memory? This action cannot be undone.')) {
2463 |             return;
2464 |         }
2465 | 
2466 |         try {
2467 |             await this.apiCall(`/memories/${memory.content_hash}`, 'DELETE');
2468 |             this.closeModal(document.getElementById('memoryModal'));
2469 |             this.showToast(this.t('toast.memoryDeletedSuccess', 'Memory deleted successfully'), 'success');
2470 | 
2471 |             // Refresh current view
2472 |             if (this.currentView === 'dashboard') {
2473 |                 this.loadDashboardData();
2474 |             } else if (this.currentView === 'search') {
2475 |                 this.searchResults = this.searchResults.filter(m => m.memory.content_hash !== memory.content_hash);
2476 |                 this.renderSearchResults(this.searchResults);
2477 |             } else if (this.currentView === 'browse') {
2478 |                 // Refresh browse view (tags cloud)
2479 |                 this.loadBrowseData();
2480 |             }
2481 |         } catch (error) {
2482 |             console.error('Error deleting memory:', error);
2483 |             this.showToast(this.t('toast.memoryDeleteFailed', 'Failed to delete memory'), 'error');
2484 |         }
2485 |     }
2486 | 
2487 |     /**
2488 |      * Edit memory
2489 |      */
2490 |     editMemory(memory) {
2491 |         // Close the memory details modal first
2492 |         this.closeModal(document.getElementById('memoryModal'));
2493 | 
2494 |         // Open the add memory modal with pre-filled data
2495 |         const modal = document.getElementById('addMemoryModal');
2496 |         const title = modal.querySelector('.modal-header h3');
2497 |         const saveBtn = document.getElementById('saveMemoryBtn');
2498 | 
2499 |         // Update modal for editing
2500 |         title.textContent = 'Edit Memory';
2501 |         saveBtn.textContent = 'Update Memory';
2502 | 
2503 |         // Pre-fill the form with existing data
2504 |         document.getElementById('memoryContent').value = memory.content || '';
2505 | 
2506 |         // Handle tags - ensure they're displayed correctly
2507 |         const tagsValue = memory.tags && Array.isArray(memory.tags) ? memory.tags.join(', ') : '';
2508 |         document.getElementById('memoryTags').value = tagsValue;
2509 | 
2510 |         document.getElementById('memoryType').value = memory.memory_type || 'note';
2511 | 
2512 | 
2513 |         // Store the memory being edited
2514 |         this.editingMemory = memory;
2515 | 
2516 |         this.openModal(modal);
2517 | 
2518 |         // Use setTimeout to ensure modal is fully rendered before setting values
2519 |         setTimeout(() => {
2520 |             document.getElementById('memoryContent').focus();
2521 |         }, 100);
2522 |     }
2523 | 
2524 |     /**
2525 |      * Share memory
2526 |      */
2527 |     shareMemory(memory) {
2528 |         // Create shareable data
2529 |         const shareData = {
2530 |             content: memory.content,
2531 |             tags: memory.tags || [],
2532 |             type: memory.memory_type || 'note',
2533 |             created: new Date(memory.created_at * 1000).toISOString(),
2534 |             id: memory.content_hash
2535 |         };
2536 | 
2537 |         // Try to use Web Share API if available
2538 |         if (navigator.share) {
2539 |             navigator.share({
2540 |                 title: 'Memory from MCP Memory Service',
2541 |                 text: memory.content,
2542 |                 url: window.location.href
2543 |             }).catch(err => {
2544 |                 // Share API failed, fall back to clipboard
2545 |                 this.fallbackShare(shareData);
2546 |             });
2547 |         } else {
2548 |             this.fallbackShare(shareData);
2549 |         }
2550 |     }
2551 | 
2552 |     /**
2553 |      * Fallback share method (copy to clipboard)
2554 |      */
2555 |     fallbackShare(shareData) {
2556 |         const shareText = `Memory Content:\n${shareData.content}\n\nTags: ${shareData.tags.join(', ')}\nType: ${shareData.type}\nCreated: ${shareData.created}`;
2557 | 
2558 |         navigator.clipboard.writeText(shareText).then(() => {
2559 |             this.showToast(this.t('toast.copySuccess', 'Memory copied to clipboard'), 'success');
2560 |         }).catch(err => {
2561 |             console.error('Could not copy text: ', err);
2562 |             this.showToast(this.t('toast.copyFailed', 'Failed to copy to clipboard'), 'error');
2563 |         });
2564 |     }
2565 | 
2566 |     /**
2567 |      * Handle data export
2568 |      */
2569 |     async handleExportData() {
2570 |         try {
2571 |         this.showToast(this.t('toast.exportPreparing', 'Preparing export...'), 'info');
2572 | 
2573 |             // Fetch all memories using pagination
2574 |             const allMemories = [];
2575 |             const pageSize = 100; // Reasonable batch size
2576 |             let page = 1;
2577 |             let hasMore = true;
2578 |             let totalMemories = 0;
2579 | 
2580 |             while (hasMore) {
2581 |                 const response = await this.apiCall(`/memories?page=${page}&page_size=${pageSize}`);
2582 | 
2583 |                 if (page === 1) {
2584 |                     totalMemories = response.total;
2585 |                 }
2586 | 
2587 |                 if (response.memories && response.memories.length > 0) {
2588 |                     allMemories.push(...response.memories);
2589 |                     hasMore = response.has_more;
2590 |                     page++;
2591 | 
2592 |                     // Update progress
2593 |                     this.showToast(
2594 |                         this.t('toast.exportFetching', 'Fetching memories... ({current}/{total})', {
2595 |                             current: allMemories.length,
2596 |                             total: totalMemories
2597 |                         }),
2598 |                         'info'
2599 |                     );
2600 |                 } else {
2601 |                     hasMore = false;
2602 |                 }
2603 |             }
2604 | 
2605 |             const data = {
2606 |                 export_date: new Date().toISOString(),
2607 |                 total_memories: totalMemories,
2608 |                 exported_memories: allMemories.length,
2609 |                 memories: allMemories
2610 |             };
2611 | 
2612 |             const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
2613 |             const url = URL.createObjectURL(blob);
2614 |             const a = document.createElement('a');
2615 |             a.href = url;
2616 |             a.download = `mcp-memories-export-${new Date().toISOString().split('T')[0]}.json`;
2617 |             document.body.appendChild(a);
2618 |             a.click();
2619 |             document.body.removeChild(a);
2620 |             URL.revokeObjectURL(url);
2621 | 
2622 |             this.showToast(
2623 |                 this.t('toast.exportSuccess', 'Successfully exported {count} memories', { count: allMemories.length }),
2624 |                 'success'
2625 |             );
2626 |         } catch (error) {
2627 |             console.error('Export error:', error);
2628 |             this.showToast(this.t('toast.exportFailed', 'Failed to export data'), 'error');
2629 |         }
2630 |     }
2631 | 
2632 |     /**
2633 |     * Render recent memories
2634 |     */
2635 |     renderRecentMemories(memories) {
2636 |     const container = document.getElementById('recentMemoriesList');
2637 | 
2638 |     if (!container) {
2639 |     console.error('recentMemoriesList container not found');
2640 |     return;
2641 |     }
2642 | 
2643 |     if (!memories || memories.length === 0) {
2644 |     container.innerHTML = '<p class="empty-state">No memories found. <a href="#" onclick="app.handleAddMemory()">Add your first memory</a></p>';
2645 |     return;
2646 |     }
2647 | 
2648 |     // Group document chunks by upload_id
2649 |         const groupedMemories = this.groupMemoriesByUpload(memories);
2650 | 
2651 |     container.innerHTML = groupedMemories.map(group => {
2652 |     if (group.type === 'document') {
2653 |             return this.renderDocumentGroup(group);
2654 |             } else {
2655 |                 return this.renderMemoryCard(group.memory);
2656 |             }
2657 |         }).join('');
2658 | 
2659 |         // Add click handlers for individual memories
2660 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
2661 |             const group = groupedMemories[index];
2662 |             if (group.type === 'single') {
2663 |                 card.addEventListener('click', () => this.handleMemoryClick(group.memory));
2664 |             }
2665 |         });
2666 | 
2667 |         // Add click handlers for document groups
2668 |         container.querySelectorAll('.document-group').forEach((groupEl, index) => {
2669 |             const group = groupedMemories.filter(g => g.type === 'document')[index];
2670 |             if (group) {
2671 |                 groupEl.addEventListener('click', (e) => {
2672 |                     // Don't trigger if clicking on action buttons
2673 |                     if (e.target.closest('.document-actions')) return;
2674 |                     this.showDocumentChunks(group);
2675 |                 });
2676 |             }
2677 |         });
2678 | 
2679 |         // Add click handlers for document action buttons
2680 |         container.querySelectorAll('.document-group').forEach((groupEl, index) => {
2681 |             const group = groupedMemories.filter(g => g.type === 'document')[index];
2682 |             if (group) {
2683 |                 // View chunks button
2684 |                 const viewBtn = groupEl.querySelector('.btn-view-chunks');
2685 |                 if (viewBtn) {
2686 |                     viewBtn.addEventListener('click', (e) => {
2687 |                         e.stopPropagation();
2688 |                         this.showDocumentChunks(group);
2689 |                     });
2690 |                 }
2691 | 
2692 |                 // Remove button
2693 |                 const removeBtn = groupEl.querySelector('.btn-remove');
2694 |                 if (removeBtn) {
2695 |                     removeBtn.addEventListener('click', async (e) => {
2696 |                         e.stopPropagation();
2697 |                         await this.removeDocument(group.upload_id, group.source_file);
2698 |                         // removeDocument() already handles view refresh
2699 |                     });
2700 |                 }
2701 |             }
2702 |         });
2703 |     }
2704 | 
2705 |     /**
2706 |      * Group memories by upload_id for document chunks
2707 |      */
2708 |     groupMemoriesByUpload(memories) {
2709 |         const groups = [];
2710 |         const documentGroups = new Map();
2711 |         const processedHashes = new Set();
2712 | 
2713 |         for (const memory of memories) {
2714 |             // Check if this is a document chunk
2715 |             const isDocumentChunk = memory.metadata && memory.metadata.upload_id;
2716 | 
2717 |             if (isDocumentChunk && !processedHashes.has(memory.content_hash)) {
2718 |                 const uploadId = memory.metadata.upload_id;
2719 |                 const sourceFile = memory.metadata.source_file || 'Unknown file';
2720 | 
2721 |                 if (!documentGroups.has(uploadId)) {
2722 |                     documentGroups.set(uploadId, {
2723 |                         upload_id: uploadId,
2724 |                         source_file: sourceFile,
2725 |                         memories: [],
2726 |                         created_at: memory.created_at,
2727 |                         tags: new Set()
2728 |                     });
2729 |                 }
2730 | 
2731 |                 const group = documentGroups.get(uploadId);
2732 |                 group.memories.push(memory);
2733 |                 group.tags.add(...(memory.tags || []));
2734 |                 processedHashes.add(memory.content_hash);
2735 |             } else if (!processedHashes.has(memory.content_hash)) {
2736 |                 // Regular memory
2737 |                 groups.push({
2738 |                     type: 'single',
2739 |                     memory: memory
2740 |                 });
2741 |                 processedHashes.add(memory.content_hash);
2742 |             }
2743 |         }
2744 | 
2745 |         // Convert document groups to array format
2746 |         for (const group of documentGroups.values()) {
2747 |             groups.push({
2748 |                 type: 'document',
2749 |                 upload_id: group.upload_id,
2750 |                 source_file: group.source_file,
2751 |                 memories: group.memories,
2752 |                 created_at: group.created_at,
2753 |                 tags: Array.from(group.tags)
2754 |             });
2755 |         }
2756 | 
2757 |         // Sort by creation time (most recent first)
2758 |         groups.sort((a, b) => {
2759 |             const timeA = a.type === 'document' ? a.created_at : a.memory.created_at;
2760 |             const timeB = b.type === 'document' ? b.created_at : b.memory.created_at;
2761 |             return timeB - timeA;
2762 |         });
2763 | 
2764 |         return groups;
2765 |     }
2766 | 
2767 |     /**
2768 |      * Render a document group card
2769 |      */
2770 |     renderDocumentGroup(group) {
2771 |         const createdDate = new Date(group.created_at * 1000).toLocaleDateString();
2772 |         const fileName = this.escapeHtml(group.source_file);
2773 |         const chunkCount = group.memories.length;
2774 |         // Filter out metadata tags AND tags that are too long (likely corrupted/malformed)
2775 |         const uniqueTags = [...new Set(group.tags.filter(tag =>
2776 |             !tag.startsWith('upload_id:') &&
2777 |             !tag.startsWith('source_file:') &&
2778 |             !tag.startsWith('file_type:') &&
2779 |             tag.length < 100  // Reject tags longer than 100 chars (likely corrupted metadata)
2780 |         ))];
2781 | 
2782 |         return `
2783 |             <div class="document-group" data-upload-id="${this.escapeHtml(group.upload_id)}">
2784 |                 <div class="document-header">
2785 |                     <div class="document-icon">📄</div>
2786 |                     <div class="document-info">
2787 |                         <div class="document-title">${fileName}</div>
2788 |                         <div class="document-meta">
2789 |                             ${chunkCount} chunks • ${createdDate} ${this.renderTimeIcon(group.created_at)}
2790 |                         </div>
2791 |                     </div>
2792 |                 </div>
2793 |                 <div class="document-preview">
2794 |                     ${group.memories[0] ? this.escapeHtml(group.memories[0].content.substring(0, 150)) + (group.memories[0].content.length > 150 ? '...' : '') : 'No content preview available'}
2795 |                 </div>
2796 |                 ${uniqueTags.length > 0 ? `
2797 |                     <div class="document-tags">
2798 |                         ${uniqueTags.slice(0, 3).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2799 |                         ${uniqueTags.length > 3 ? `<span class="tag more">+${uniqueTags.length - 3} more</span>` : ''}
2800 |                     </div>
2801 |                 ` : ''}
2802 |                 <div class="document-actions">
2803 |                     <button class="btn-icon btn-view-chunks" title="View all chunks">
2804 |                         <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
2805 |                             <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"/>
2806 |                         </svg>
2807 |                         <span>View Chunks</span>
2808 |                     </button>
2809 |                     <button class="btn-icon btn-remove" title="Remove document">
2810 |                         <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
2811 |                             <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
2812 |                         </svg>
2813 |                         <span>Remove</span>
2814 |                     </button>
2815 |                 </div>
2816 |             </div>
2817 |         `;
2818 |     }
2819 | 
2820 |     /**
2821 |      * Show document chunks in a modal
2822 |      */
2823 |     showDocumentChunks(group) {
2824 |         const modal = document.createElement('div');
2825 |         modal.className = 'modal-overlay';
2826 |         modal.innerHTML = `
2827 |             <div class="modal-content large-modal">
2828 |                 <div class="modal-header">
2829 |                     <h3>📄 ${this.escapeHtml(group.source_file)}</h3>
2830 |                     <button class="modal-close">&times;</button>
2831 |                 </div>
2832 |                 <div class="modal-body">
2833 |                     <div class="document-chunks">
2834 |                         ${group.memories.map((memory, index) => `
2835 |                             <div class="chunk-item">
2836 |                                 <div class="chunk-header">
2837 |                                     <span class="chunk-number">Chunk ${index + 1}</span>
2838 |                                     <div class="chunk-meta">
2839 |                                         ${memory.metadata && memory.metadata.page ? `Page ${memory.metadata.page} • ` : ''}
2840 |                                         ${memory.content.length} chars
2841 |                                     </div>
2842 |                                 </div>
2843 |                                 <div class="chunk-content">
2844 |                                     ${this.escapeHtml(memory.content)}
2845 |                                 </div>
2846 |                                 ${memory.tags && memory.tags.length > 0 ? `
2847 |                                     <div class="chunk-tags">
2848 |                                         ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2849 |                                     </div>
2850 |                                 ` : ''}
2851 |                             </div>
2852 |                         `).join('')}
2853 |                     </div>
2854 |                 </div>
2855 |             </div>
2856 |         `;
2857 | 
2858 |         document.body.appendChild(modal);
2859 | 
2860 |         // Add active class to show modal (required for display: flex)
2861 |         setTimeout(() => modal.classList.add('active'), 10);
2862 | 
2863 |         // Add close handlers
2864 |         const closeModal = () => {
2865 |             modal.classList.remove('active');
2866 |             setTimeout(() => document.body.removeChild(modal), 300);
2867 |         };
2868 |         modal.querySelector('.modal-close').addEventListener('click', closeModal);
2869 |         modal.addEventListener('click', (e) => {
2870 |             if (e.target === modal) closeModal();
2871 |         });
2872 |     }
2873 | 
2874 |     /**
2875 |      * Render tags in sidebar
2876 |      */
2877 |     renderTagsSidebar(tags) {
2878 |         const container = document.getElementById('tagsCloudContainer');
2879 | 
2880 |         if (!container) {
2881 |             console.warn('tagsCloudContainer element not found - skipping tags sidebar rendering');
2882 |             return;
2883 |         }
2884 | 
2885 |         if (!tags || tags.length === 0) {
2886 |             container.innerHTML = '<div class="no-tags">No tags found.</div>';
2887 |             return;
2888 |         }
2889 | 
2890 |         // Take top tags for sidebar display
2891 |         const topTags = tags.slice(0, 10);
2892 |         container.innerHTML = topTags.map(tagData => `
2893 |             <div class="tag-item" data-tag="${this.escapeHtml(tagData.tag)}">
2894 |                 <span class="tag-name">${this.escapeHtml(tagData.tag)}</span>
2895 |                 <span class="tag-count">${tagData.count}</span>
2896 |             </div>
2897 |         `).join('');
2898 | 
2899 |         // Add click handlers
2900 |         container.querySelectorAll('.tag-item').forEach(item => {
2901 |             item.addEventListener('click', () => {
2902 |                 const tagName = item.dataset.tag;
2903 |                 const searchInput = document.getElementById('searchInput');
2904 |                 searchInput.value = `#${tagName}`;
2905 |                 this.handleSearch(`#${tagName}`);
2906 |             });
2907 |         });
2908 |     }
2909 | 
2910 |     /**
2911 |      * Render search results
2912 |      */
2913 |     renderSearchResults(results) {
2914 |         const container = document.getElementById('searchResultsList');
2915 | 
2916 |         if (!results || results.length === 0) {
2917 |             container.innerHTML = '<p class="empty-state">No results found. Try a different search term.</p>';
2918 |             return;
2919 |         }
2920 | 
2921 |         container.innerHTML = results.map(result => this.renderMemoryCard(result.memory, result)).join('');
2922 | 
2923 |         // Add click handlers
2924 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
2925 |             card.addEventListener('click', () => this.handleMemoryClick(results[index].memory));
2926 |         });
2927 |     }
2928 | 
2929 |     /**
2930 |     * Render a memory card
2931 |     */
2932 |     renderMemoryCard(memory, searchResult = null) {
2933 |     const createdDate = new Date(memory.created_at * 1000).toLocaleDateString();
2934 |     const relevanceScore = searchResult &&
2935 |     searchResult.similarity_score !== null &&
2936 |     searchResult.similarity_score !== undefined &&
2937 |     !isNaN(searchResult.similarity_score) &&
2938 |     searchResult.similarity_score > 0
2939 |     ? (searchResult.similarity_score * 100).toFixed(1)
2940 |     : null;
2941 | 
2942 |     // Truncate content to 150 characters for preview
2943 |     const truncatedContent = memory.content.length > 150
2944 |     ? memory.content.substring(0, 150) + '...'
2945 |     : memory.content;
2946 | 
2947 |     return `
2948 |     <div class="memory-card" data-memory-id="${memory.content_hash}">
2949 |         ${this.renderQualityBadge(memory)}
2950 |     <div class="memory-header">
2951 |         <div class="memory-meta">
2952 |                         <span>${createdDate} ${this.renderTimeIcon(memory.created_at)}</span>
2953 |             ${memory.memory_type ? `<span> • ${memory.memory_type}</span>` : ''}
2954 |         ${relevanceScore ? `<span> • ${relevanceScore}% match</span>` : ''}
2955 |         </div>
2956 |                 </div>
2957 | 
2958 |     <div class="memory-content">
2959 |     ${this.escapeHtml(truncatedContent)}
2960 |     </div>
2961 | 
2962 |         ${memory.tags && memory.tags.length > 0 ? `
2963 |                 <div class="memory-tags">
2964 |                         ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2965 |                     </div>
2966 |                 ` : ''}
2967 |             </div>
2968 |         `;
2969 |     }
2970 | 
2971 |     /**
2972 |      * Update dashboard statistics
2973 |      */
2974 |     updateDashboardStats(stats) {
2975 |         const totalMemoriesEl = document.getElementById('totalMemories');
2976 |         if (totalMemoriesEl) {
2977 |             totalMemoriesEl.textContent = stats.total_memories || '0';
2978 |         }
2979 | 
2980 |         const recentMemoriesEl = document.getElementById('recentMemories');
2981 |         if (recentMemoriesEl) {
2982 |             recentMemoriesEl.textContent = stats.memories_this_week || '0';
2983 |         }
2984 | 
2985 |         const uniqueTagsEl = document.getElementById('uniqueTags');
2986 |         if (uniqueTagsEl) {
2987 |             uniqueTagsEl.textContent = stats.unique_tags || '0';
2988 |         }
2989 | 
2990 |         const storageBackendEl = document.getElementById('storageBackend');
2991 |         if (storageBackendEl) {
2992 |             storageBackendEl.textContent = stats.backend || 'unknown';
2993 |         }
2994 |     }
2995 | 
2996 |     /**
2997 |      * Update search results count
2998 |      */
2999 |     updateResultsCount(count) {
3000 |         const element = document.getElementById('resultsCount');
3001 |         if (element) {
3002 |             element.textContent = `${count} result${count !== 1 ? 's' : ''}`;
3003 |         }
3004 |     }
3005 | 
3006 |     /**
3007 |      * Update active filters display
3008 |      */
3009 |     updateActiveFilters() {
3010 |         const activeFiltersContainer = document.getElementById('activeFilters');
3011 |         const filtersList = document.getElementById('activeFiltersList');
3012 | 
3013 |         if (!activeFiltersContainer || !filtersList) return;
3014 | 
3015 |         const tagFilter = document.getElementById('tagFilter')?.value?.trim();
3016 |         const dateFilter = document.getElementById('dateFilter')?.value;
3017 |         const typeFilter = document.getElementById('typeFilter')?.value;
3018 | 
3019 |         const filters = [];
3020 | 
3021 |         if (tagFilter) {
3022 |             const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
3023 |             tags.forEach(tag => {
3024 |                 filters.push({
3025 |                     type: 'tag',
3026 |                     value: tag,
3027 |                     label: `Tag: ${tag}`
3028 |                 });
3029 |             });
3030 |         }
3031 | 
3032 |         if (dateFilter) {
3033 |             const dateLabels = {
3034 |                 'today': 'Today',
3035 |                 'week': 'This week',
3036 |                 'month': 'This month',
3037 |                 'year': 'This year'
3038 |             };
3039 |             filters.push({
3040 |                 type: 'date',
3041 |                 value: dateFilter,
3042 |                 label: `Date: ${dateLabels[dateFilter] || dateFilter}`
3043 |             });
3044 |         }
3045 | 
3046 |         if (typeFilter) {
3047 |             const typeLabels = {
3048 |                 'note': 'Notes',
3049 |                 'code': 'Code',
3050 |                 'reference': 'References',
3051 |                 'idea': 'Ideas'
3052 |             };
3053 |             filters.push({
3054 |                 type: 'type',
3055 |                 value: typeFilter,
3056 |                 label: `Type: ${typeLabels[typeFilter] || typeFilter}`
3057 |             });
3058 |         }
3059 | 
3060 |         if (filters.length === 0) {
3061 |             activeFiltersContainer.style.display = 'none';
3062 |             return;
3063 |         }
3064 | 
3065 |         activeFiltersContainer.style.display = 'block';
3066 |         filtersList.innerHTML = filters.map(filter => `
3067 |             <div class="filter-pill">
3068 |                 ${this.escapeHtml(filter.label)}
3069 |                 <button class="remove-filter" data-filter-type="${this.escapeHtml(filter.type)}" data-filter-value="${this.escapeHtml(filter.value)}">
3070 |                     ×
3071 |                 </button>
3072 |             </div>
3073 |         `).join('');
3074 | 
3075 |         // Add event listeners for filter removal
3076 |         filtersList.addEventListener('click', (e) => {
3077 |             const button = e.target.closest('.remove-filter');
3078 |             if (!button) return;
3079 | 
3080 |             const type = button.dataset.filterType;
3081 |             const value = button.dataset.filterValue;
3082 |             this.removeFilter(type, value);
3083 |         });
3084 |     }
3085 | 
3086 |     /**
3087 |      * Remove a specific filter
3088 |      */
3089 |     removeFilter(type, value) {
3090 |         switch (type) {
3091 |             case 'tag':
3092 |                 const tagInput = document.getElementById('tagFilter');
3093 |                 if (tagInput) {
3094 |                     const tags = tagInput.value.split(',').map(t => t.trim()).filter(t => t && t !== value);
3095 |                     tagInput.value = tags.join(', ');
3096 |                 }
3097 |                 break;
3098 |             case 'date':
3099 |                 const dateSelect = document.getElementById('dateFilter');
3100 |                 if (dateSelect) {
3101 |                     dateSelect.value = '';
3102 |                 }
3103 |                 break;
3104 |             case 'type':
3105 |                 const typeSelect = document.getElementById('typeFilter');
3106 |                 if (typeSelect) {
3107 |                     typeSelect.value = '';
3108 |                 }
3109 |                 break;
3110 |         }
3111 |         this.handleFilterChange();
3112 |     }
3113 | 
3114 |     /**
3115 |      * Clear all filters
3116 |      */
3117 |     clearAllFilters() {
3118 |         const tagFilter = document.getElementById('tagFilter');
3119 |         const dateFilter = document.getElementById('dateFilter');
3120 |         const typeFilter = document.getElementById('typeFilter');
3121 | 
3122 |         if (tagFilter) tagFilter.value = '';
3123 |         if (dateFilter) dateFilter.value = '';
3124 |         if (typeFilter) typeFilter.value = '';
3125 | 
3126 |         this.searchResults = [];
3127 |         this.renderSearchResults([]);
3128 |         this.updateResultsCount(0);
3129 |         this.updateActiveFilters();
3130 | 
3131 |         this.showToast(this.t('toast.filtersCleared', 'All filters cleared'), 'info');
3132 |     }
3133 | 
3134 |     /**
3135 |      * Handle live search toggle
3136 |      */
3137 |     handleLiveSearchToggle(event) {
3138 |         this.liveSearchEnabled = event.target.checked;
3139 |         const modeText = document.getElementById('searchModeText');
3140 |         if (modeText) {
3141 |             modeText.textContent = this.liveSearchEnabled
3142 |                 ? this.t('search.modeLive', 'Live Search')
3143 |                 : this.t('search.modeManual', 'Manual Search');
3144 |         }
3145 | 
3146 |         // Show a toast to indicate the mode change
3147 |         this.showToast(
3148 |             this.liveSearchEnabled
3149 |                 ? this.t('toast.searchModeLive', 'Search mode: Live (searches as you type)')
3150 |                 : this.t('toast.searchModeManual', 'Search mode: Manual (click Search button)'),
3151 |             'info'
3152 |         );
3153 |     }
3154 | 
3155 |     /**
3156 |      * Handle debounced filter changes for live search
3157 |      */
3158 |     handleDebouncedFilterChange() {
3159 |         // Clear any existing timer
3160 |         if (this.debounceTimer) {
3161 |             clearTimeout(this.debounceTimer);
3162 |         }
3163 | 
3164 |         // Only trigger search if live search is enabled
3165 |         if (this.liveSearchEnabled) {
3166 |             this.debounceTimer = setTimeout(() => {
3167 |                 this.handleFilterChange();
3168 |             }, 300); // 300ms debounce
3169 |         }
3170 |     }
3171 | 
3172 |     /**
3173 |      * Handle memory added via SSE
3174 |      */
3175 |     handleMemoryAdded(memory) {
3176 |         if (this.currentView === 'dashboard') {
3177 |             this.loadDashboardData();
3178 |         }
3179 |     }
3180 | 
3181 |     /**
3182 |      * Handle memory deleted via SSE
3183 |      */
3184 |     handleMemoryDeleted(memoryId) {
3185 |         // Remove from current view
3186 |         const cards = document.querySelectorAll(`[data-memory-id="${memoryId}"]`);
3187 |         cards.forEach(card => card.remove());
3188 | 
3189 |         // Update search results if in search view
3190 |         if (this.currentView === 'search') {
3191 |             this.searchResults = this.searchResults.filter(r => r.memory.content_hash !== memoryId);
3192 |             this.updateResultsCount(this.searchResults.length);
3193 |         }
3194 |     }
3195 | 
3196 |     /**
3197 |      * Handle memory updated via SSE
3198 |      */
3199 |     handleMemoryUpdated(memory) {
3200 |         // Refresh relevant views
3201 |         if (this.currentView === 'dashboard') {
3202 |             this.loadDashboardData();
3203 |         }
3204 |     }
3205 | 
3206 |     /**
3207 |      * Update connection status indicator
3208 |      */
3209 |     updateConnectionStatus(status) {
3210 |         const statusElement = document.getElementById('connectionStatus');
3211 |         if (statusElement) {
3212 |             const indicator = statusElement.querySelector('.status-indicator');
3213 |             const text = statusElement.querySelector('.status-text');
3214 |             if (!indicator || !text) return;
3215 | 
3216 |             // Reset indicator classes
3217 |             indicator.className = 'status-indicator';
3218 | 
3219 |             switch (status) {
3220 |                 case 'connected':
3221 |                     text.textContent = 'Connected';
3222 |                     // Connected uses default green color (no additional class needed)
3223 |                     break;
3224 |                 case 'connecting':
3225 |                     text.textContent = 'Connecting...';
3226 |                     indicator.classList.add('connecting');
3227 |                     break;
3228 |                 case 'disconnected':
3229 |                     text.textContent = 'Disconnected';
3230 |                     indicator.classList.add('disconnected');
3231 |                     break;
3232 |                 default:
3233 |                     text.textContent = 'Unknown';
3234 |                     indicator.classList.add('disconnected');
3235 |             }
3236 |         }
3237 |     }
3238 | 
3239 |     /**
3240 |      * Generic API call wrapper
3241 |      */
3242 |     async apiCall(endpoint, method = 'GET', data = null) {
3243 |         const options = {
3244 |             method: method,
3245 |             headers: {
3246 |                 'Content-Type': 'application/json',
3247 |             }
3248 |         };
3249 | 
3250 |         if (data) {
3251 |             options.body = JSON.stringify(data);
3252 |         }
3253 | 
3254 |         const response = await fetch(`${this.apiBase}${endpoint}`, options);
3255 | 
3256 |         if (!response.ok) {
3257 |             const errorData = await response.json().catch(() => ({}));
3258 |             throw new Error(errorData.detail || `HTTP ${response.status}`);
3259 |         }
3260 | 
3261 |         return await response.json();
3262 |     }
3263 | 
3264 |     /**
3265 |      * Modal management
3266 |      */
3267 |     openModal(modal) {
3268 |         modal.classList.add('active');
3269 |         document.body.style.overflow = 'hidden';
3270 | 
3271 |         // Focus first input
3272 |         const firstInput = modal.querySelector('input, textarea');
3273 |         if (firstInput) {
3274 |             firstInput.focus();
3275 |         }
3276 |     }
3277 | 
3278 |     closeModal(modal) {
3279 |         modal.classList.remove('active');
3280 |         document.body.style.overflow = '';
3281 |     }
3282 | 
3283 |     /**
3284 |      * Loading state management
3285 |      */
3286 |     setLoading(loading) {
3287 |         this.isLoading = loading;
3288 |         const indicator = document.getElementById('loadingOverlay');
3289 |         if (indicator) {
3290 |             if (loading) {
3291 |                 indicator.classList.remove('hidden');
3292 |             } else {
3293 |                 indicator.classList.add('hidden');
3294 |             }
3295 |         }
3296 |     }
3297 | 
3298 |     /**
3299 |      * View document memory chunks
3300 |      */
3301 |     async viewDocumentMemory(uploadId) {
3302 |         try {
3303 |             this.setLoading(true);
3304 |             const response = await this.apiCall(`/documents/search-content/${uploadId}`);
3305 | 
3306 |             if (response.status === 'success' || response.status === 'partial') {
3307 |                 // Show modal
3308 |                 const modal = document.getElementById('memoryViewerModal');
3309 |                 const filename = document.getElementById('memoryViewerFilename');
3310 |                 const stats = document.getElementById('memoryViewerStats');
3311 |                 const chunksList = document.getElementById('memoryChunksList');
3312 | 
3313 |                 filename.textContent = response.filename || 'Document';
3314 |                 stats.textContent = `${response.total_found} chunk${response.total_found !== 1 ? 's' : ''} found`;
3315 | 
3316 |                 // Render chunks
3317 |                 if (response.memories && response.memories.length > 0) {
3318 |                     const chunksHtml = response.memories.map((memory, index) => {
3319 |                         const chunkIndex = memory.chunk_index !== undefined ? memory.chunk_index : index;
3320 |                         const page = memory.page ? ` • Page ${memory.page}` : '';
3321 |                         const contentPreview = memory.content.length > 300
3322 |                             ? memory.content.substring(0, 300) + '...'
3323 |                             : memory.content;
3324 | 
3325 |                         return `
3326 |                             <div class="memory-chunk-item">
3327 |                                 <div class="chunk-header">
3328 |                                     <span class="chunk-number">Chunk ${chunkIndex + 1}${page}</span>
3329 |                                     <span class="chunk-hash" title="${memory.content_hash}">${memory.content_hash.substring(0, 12)}...</span>
3330 |                                 </div>
3331 |                                 <div class="chunk-content">${this.escapeHtml(contentPreview)}</div>
3332 |                                 <div class="chunk-tags">
3333 |                                     ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
3334 |                                 </div>
3335 |                             </div>
3336 |                         `;
3337 |                     }).join('');
3338 | 
3339 |                     chunksList.innerHTML = chunksHtml;
3340 |                 } else {
3341 |                     chunksList.innerHTML = '<p class="text-muted">No memory chunks found for this document.</p>';
3342 |                 }
3343 | 
3344 |                 modal.style.display = 'flex';
3345 | 
3346 |                 if (response.status === 'partial') {
3347 |                     this.showToast(
3348 |                         this.t('toast.documentChunksPartial', 'Found {count} chunks (partial results)', {
3349 |                             count: response.total_found
3350 |                         }),
3351 |                         'warning'
3352 |                     );
3353 |                 }
3354 |             } else {
3355 |                 this.showToast(this.t('toast.loadDocumentMemoriesFail', 'Failed to load document memories'), 'error');
3356 |             }
3357 |         } catch (error) {
3358 |             console.error('Error viewing document memory:', error);
3359 |             this.showToast(this.t('toast.errorLoadingDocumentMemories', 'Error loading document memories'), 'error');
3360 |         } finally {
3361 |             this.setLoading(false);
3362 |         }
3363 |     }
3364 | 
3365 |     /**
3366 |      * Close memory viewer modal
3367 |      */
3368 |     closeMemoryViewer() {
3369 |         const modal = document.getElementById('memoryViewerModal');
3370 |         if (modal) {
3371 |             modal.style.display = 'none';
3372 |         }
3373 |     }
3374 | 
3375 |     /**
3376 |      * Remove document and its memories
3377 |      */
3378 |     async removeDocument(uploadId, filename) {
3379 |         console.log('removeDocument called with:', { uploadId, filename, currentView: this.currentView });
3380 | 
3381 |         if (!confirm(`Remove "${filename}" and all its memory chunks?\n\nThis action cannot be undone.`)) {
3382 |             console.log('User cancelled removal');
3383 |             return;
3384 |         }
3385 | 
3386 |         try {
3387 |             this.setLoading(true);
3388 |             console.log('Making DELETE request to:', `/documents/remove/${uploadId}`);
3389 | 
3390 |             const response = await this.apiCall(`/documents/remove/${uploadId}`, 'DELETE');
3391 | 
3392 |             console.log('Delete response:', response);
3393 | 
3394 |             if (response.status === 'success') {
3395 |                 this.showToast(
3396 |                     this.t('toast.documentRemoved', 'Removed "{name}" ({count} memories deleted)', {
3397 |                         name: filename,
3398 |                         count: response.memories_deleted
3399 |                     }),
3400 |                     'success'
3401 |                 );
3402 |                 // Refresh the current view (Dashboard or Documents tab)
3403 |                 console.log('Refreshing view:', this.currentView);
3404 |                 if (this.currentView === 'dashboard') {
3405 |                     await this.loadDashboardData();
3406 |                 } else if (this.currentView === 'documents') {
3407 |                     await this.loadUploadHistory();
3408 |                 }
3409 |             } else {
3410 |                 console.error('Removal failed with response:', response);
3411 |                 this.showToast(this.t('toast.removeDocumentFailed', 'Failed to remove document'), 'error');
3412 |             }
3413 |         } catch (error) {
3414 |             console.error('Error removing document:', error);
3415 |             console.error('Error stack:', error.stack);
3416 |             this.showToast(this.t('toast.errorRemovingDocument', 'Error removing document'), 'error');
3417 |         } finally {
3418 |             this.setLoading(false);
3419 |         }
3420 |     }
3421 | 
3422 |     /**
3423 |      * Search document content
3424 |      */
3425 |     async searchDocumentContent(query) {
3426 |         try {
3427 |             this.setLoading(true);
3428 |             const resultsContainer = document.getElementById('docSearchResults');
3429 |             const resultsList = document.getElementById('docSearchResultsList');
3430 |             const resultsCount = document.getElementById('docSearchCount');
3431 | 
3432 |             // Use the regular search endpoint but filter for document memories
3433 |             // Higher n_results to ensure we get enough document results after filtering
3434 |             const response = await this.apiCall('/search', 'POST', {
3435 |                 query: query,
3436 |                 n_results: 100
3437 |             });
3438 | 
3439 |             if (response.results) {
3440 |                 // Filter results to only show document-type memories
3441 |                 const documentResults = response.results.filter(r =>
3442 |                     r.memory?.memory_type === 'document' || (r.memory?.tags && r.memory.tags.some(tag => tag.startsWith('upload_id:')))
3443 |                 );
3444 | 
3445 |                 // Limit display to top 20 most relevant document results
3446 |                 const displayResults = documentResults.slice(0, 20);
3447 | 
3448 |                 const count = documentResults.length;
3449 |                 const extra = count > 20 ? ' (showing top 20)' : '';
3450 |                 const key = count === 1 ? 'documents.search.count.one' : 'documents.search.count.other';
3451 |                 const fallback = count === 1 ? '{count} result{extra}' : '{count} results{extra}';
3452 |                 const baseCount = this.t(key, fallback, { count, extra });
3453 |                 resultsCount.textContent = baseCount;
3454 | 
3455 |                 if (displayResults.length > 0) {
3456 |                     const resultsHtml = displayResults.map(result => {
3457 |                         const mem = result.memory;
3458 |                         const uploadIdTag = mem.tags?.find(tag => tag.startsWith('upload_id:'));
3459 |                         const sourceFile = mem.metadata?.source_file || 'Unknown file';
3460 |                         const contentPreview = mem.content.length > 200
3461 |                             ? mem.content.substring(0, 200) + '...'
3462 |                             : mem.content;
3463 | 
3464 |                         return `
3465 |                             <div class="search-result-item">
3466 |                                 <div class="result-header">
3467 |                                     <strong>${this.escapeHtml(sourceFile)}</strong>
3468 |                                     <span class="similarity-score">${Math.round((result.similarity_score || 0) * 100)}% match</span>
3469 |                                 </div>
3470 |                                 <div class="result-content">${this.escapeHtml(contentPreview)}</div>
3471 |                                 <div class="result-tags">
3472 |                                     ${(mem.tags || []).slice(0, 5).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
3473 |                                 </div>
3474 |                             </div>
3475 |                         `;
3476 |                     }).join('');
3477 | 
3478 |                     resultsList.innerHTML = resultsHtml;
3479 |                 } else {
3480 |                     resultsList.innerHTML = `<p class="text-muted">${this.t('documents.search.noMatch', 'No matching document content found. Try different search terms.')}</p>`;
3481 |                 }
3482 | 
3483 |                 resultsContainer.style.display = 'block';
3484 |             } else {
3485 |                 this.showToast(this.t('toast.searchFailed', 'Search failed'), 'error');
3486 |             }
3487 |         } catch (error) {
3488 |             console.error('Error searching documents:', error);
3489 |             this.showToast(this.t('toast.errorPerformingSearch', 'Error performing search'), 'error');
3490 |         } finally {
3491 |             this.setLoading(false);
3492 |         }
3493 |     }
3494 | 
3495 |     /**
3496 |      * Toast notification system
3497 |      */
3498 |     showToast(message, type = 'info', duration = 5000) {
3499 |         const container = document.getElementById('toastContainer');
3500 |         const toast = document.createElement('div');
3501 |         toast.className = `toast ${type}`;
3502 |         toast.textContent = message;
3503 | 
3504 |         container.appendChild(toast);
3505 | 
3506 |         // Auto-remove after duration
3507 |         setTimeout(() => {
3508 |             toast.remove();
3509 |         }, duration);
3510 | 
3511 |         // Click to remove
3512 |         toast.addEventListener('click', () => {
3513 |             toast.remove();
3514 |         });
3515 |     }
3516 | 
3517 |     /**
3518 |      * Utility: Debounce function
3519 |      */
3520 |     debounce(func, wait) {
3521 |         let timeout;
3522 |         return function executedFunction(...args) {
3523 |             const later = () => {
3524 |                 clearTimeout(timeout);
3525 |                 func(...args);
3526 |             };
3527 |             clearTimeout(timeout);
3528 |             timeout = setTimeout(later, wait);
3529 |         };
3530 |     }
3531 | 
3532 |     /**
3533 |      * Load settings from localStorage
3534 |      */
3535 |     loadSettings() {
3536 |         try {
3537 |             const saved = localStorage.getItem('memoryDashboardSettings');
3538 |             if (saved) {
3539 |                 this.settings = { ...this.settings, ...JSON.parse(saved) };
3540 |             }
3541 |         } catch (error) {
3542 |             console.warn('Failed to load settings:', error);
3543 |         }
3544 |     }
3545 | 
3546 |     /**
3547 |      * Save settings to localStorage
3548 |      */
3549 |     saveSettingsToStorage() {
3550 |         try {
3551 |             localStorage.setItem('memoryDashboardSettings', JSON.stringify(this.settings));
3552 |         } catch (error) {
3553 |             console.error('Failed to save settings:', error);
3554 |             this.showToast(
3555 |                 this.t('toast.saveSettingsFailed', 'Failed to save settings. Your preferences will not be persisted.'),
3556 |                 'error'
3557 |             );
3558 |         }
3559 |     }
3560 | 
3561 |     /**
3562 |      * Apply theme to the page
3563 |      */
3564 |     applyTheme(theme = this.settings.theme) {
3565 |         const isDark = theme === 'dark';
3566 |         document.body.classList.toggle('dark-mode', isDark);
3567 | 
3568 |         // Configure Chart.js colors for dark mode
3569 |         if (typeof Chart !== 'undefined' && Chart.defaults) {
3570 |             // Set global defaults for text and borders
3571 |             if (Chart.defaults.color !== undefined) {
3572 |                 Chart.defaults.color = isDark ? '#f9fafb' : '#374151';
3573 |             }
3574 |             if (Chart.defaults.borderColor !== undefined) {
3575 |                 Chart.defaults.borderColor = isDark ? '#4b5563' : '#e5e7eb';
3576 |             }
3577 | 
3578 |             // Redraw existing charts if Quality tab is visible
3579 |             if (this.qualityDistributionChart) {
3580 |                 this.qualityDistributionChart.options.scales.y.ticks = {
3581 |                     ...this.qualityDistributionChart.options.scales.y.ticks,
3582 |                     color: isDark ? '#f9fafb' : '#374151'
3583 |                 };
3584 |                 this.qualityDistributionChart.options.scales.y.grid = {
3585 |                     ...this.qualityDistributionChart.options.scales.y.grid,
3586 |                     color: isDark ? '#374151' : '#e5e7eb'
3587 |                 };
3588 |                 this.qualityDistributionChart.options.scales.x = {
3589 |                     ...this.qualityDistributionChart.options.scales.x,
3590 |                     ticks: { color: isDark ? '#f9fafb' : '#374151' },
3591 |                     grid: { color: isDark ? '#374151' : '#e5e7eb' }
3592 |                 };
3593 |                 this.qualityDistributionChart.update();
3594 |             }
3595 | 
3596 |             if (this.qualityProviderChart) {
3597 |                 this.qualityProviderChart.options.plugins.legend.labels = {
3598 |                     ...this.qualityProviderChart.options.plugins.legend.labels,
3599 |                     color: isDark ? '#f9fafb' : '#374151'
3600 |                 };
3601 |                 this.qualityProviderChart.update();
3602 |             }
3603 |         }
3604 | 
3605 |         // Toggle icon visibility using CSS classes
3606 |         const sunIcon = document.getElementById('sunIcon');
3607 |         const moonIcon = document.getElementById('moonIcon');
3608 |         if (sunIcon && moonIcon) {
3609 |             sunIcon.classList.toggle('hidden', isDark);
3610 |             moonIcon.classList.toggle('hidden', !isDark);
3611 |         }
3612 |     }
3613 | 
3614 |     /**
3615 |      * Toggle between light and dark theme
3616 |      */
3617 |     toggleTheme() {
3618 |         const newTheme = this.settings.theme === 'dark' ? 'light' : 'dark';
3619 |         this.settings.theme = newTheme;
3620 |         this.applyTheme(newTheme);
3621 |         this.saveSettingsToStorage();
3622 |         this.showToast(
3623 |             this.t('toast.themeSwitched', 'Switched to {theme} mode', { theme: newTheme }),
3624 |             'success'
3625 |         );
3626 |     }
3627 | 
3628 |     /**
3629 |      * Open settings modal
3630 |      */
3631 |     async openSettingsModal() {
3632 |         const modal = document.getElementById('settingsModal');
3633 | 
3634 |         // Populate form with current settings
3635 |         document.getElementById('themeSelect').value = this.settings.theme;
3636 |         document.getElementById('viewDensity').value = this.settings.viewDensity;
3637 |         document.getElementById('previewLines').value = this.settings.previewLines;
3638 | 
3639 |         // Reset system info to loading state
3640 |         this.resetSystemInfoLoadingState();
3641 | 
3642 |         // Load system information and backup status
3643 |         await Promise.all([
3644 |             this.loadSystemInfo(),
3645 |             this.checkBackupStatus()
3646 |         ]);
3647 | 
3648 |         this.openModal(modal);
3649 |     }
3650 | 
3651 |     /**
3652 |      * Reset system info fields to loading state
3653 |      */
3654 |     resetSystemInfoLoadingState() {
3655 |         Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
3656 |             const element = document.getElementById(id);
3657 |             if (element) {
3658 |                 element.textContent = 'Loading...';
3659 |             }
3660 |         });
3661 |     }
3662 | 
3663 |     /**
3664 |      * Load system information for settings modal
3665 |      */
3666 |     async loadSystemInfo() {
3667 |         try {
3668 |             // Use Promise.allSettled for robust error handling
3669 |             const [healthResult, detailedHealthResult] = await Promise.allSettled([
3670 |                 this.apiCall('/health'),
3671 |                 this.apiCall('/health/detailed')
3672 |             ]);
3673 | 
3674 |             const apiData = {
3675 |                 health: healthResult.status === 'fulfilled' ? healthResult.value : null,
3676 |                 detailedHealth: detailedHealthResult.status === 'fulfilled' ? detailedHealthResult.value : null
3677 |             };
3678 | 
3679 |             // Update fields using configuration
3680 |             Object.entries(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(([fieldId, config]) => {
3681 |                 const element = document.getElementById(fieldId);
3682 |                 if (!element) return;
3683 | 
3684 |                 let value = null;
3685 |                 for (const source of config.sources) {
3686 |                     const apiResponse = apiData[source.api];
3687 |                     if (apiResponse) {
3688 |                         value = this.getNestedValue(apiResponse, source.path);
3689 |                         if (value !== undefined && value !== null) break;
3690 |                     }
3691 |                 }
3692 | 
3693 |                 element.textContent = config.formatter(value);
3694 |             });
3695 | 
3696 |             // Log warnings for failed API calls
3697 |             if (healthResult.status === 'rejected') {
3698 |                 console.warn('Failed to load health endpoint:', healthResult.reason);
3699 |             }
3700 |             if (detailedHealthResult.status === 'rejected') {
3701 |                 console.warn('Failed to load detailed health endpoint:', detailedHealthResult.reason);
3702 |             }
3703 |         } catch (error) {
3704 |             console.error('Unexpected error loading system info:', error);
3705 |             // Set all system info fields that are still in loading state to error
3706 |             Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
3707 |                 const element = document.getElementById(id);
3708 |                 if (element && element.textContent === 'Loading...') {
3709 |                     element.textContent = 'Error';
3710 |                 }
3711 |             });
3712 |         }
3713 |     }
3714 | 
3715 |     /**
3716 |      * Get nested object value by path string
3717 |      * @param {Object} obj - Object to traverse
3718 |      * @param {string} path - Dot-separated path (e.g., 'storage.primary_stats.embedding_model')
3719 |      * @returns {*} Value at path or undefined
3720 |      */
3721 |     getNestedValue(obj, path) {
3722 |         return path.split('.').reduce((current, key) => current?.[key], obj);
3723 |     }
3724 | 
3725 |     /**
3726 |      * Format uptime seconds into human readable string
3727 |      * @param {number} seconds - Uptime in seconds
3728 |      * @returns {string} Formatted uptime string
3729 |      */
3730 |     static formatUptime(seconds) {
3731 |         const days = Math.floor(seconds / 86400);
3732 |         const hours = Math.floor((seconds % 86400) / 3600);
3733 |         const minutes = Math.floor((seconds % 3600) / 60);
3734 | 
3735 |         const parts = [];
3736 |         if (days > 0) parts.push(`${days}d`);
3737 |         if (hours > 0) parts.push(`${hours}h`);
3738 |         if (minutes > 0) parts.push(`${minutes}m`);
3739 | 
3740 |         return parts.length > 0 ? parts.join(' ') : '< 1m';
3741 |     }
3742 | 
3743 |     /**
3744 |      * Save settings from modal
3745 |      */
3746 |     saveSettings() {
3747 |         // Get values from form
3748 |         const theme = document.getElementById('themeSelect').value;
3749 |         const viewDensity = document.getElementById('viewDensity').value;
3750 |         const previewLines = parseInt(document.getElementById('previewLines').value, 10);
3751 | 
3752 |         // Update settings
3753 |         this.settings.theme = theme;
3754 |         this.settings.viewDensity = viewDensity;
3755 |         this.settings.previewLines = previewLines;
3756 | 
3757 |         // Apply changes
3758 |         this.applyTheme(theme);
3759 |         this.saveSettingsToStorage();
3760 | 
3761 |         // Close modal and show confirmation
3762 |         this.closeModal(document.getElementById('settingsModal'));
3763 |         this.showToast(this.t('toast.settingsSaved', 'Settings saved successfully'), 'success');
3764 |     }
3765 | 
3766 |     // ===== MANAGE TAB METHODS =====
3767 | 
3768 |     /**
3769 |      * Load manage tab data
3770 |     */
3771 |     async loadManageData() {
3772 |     try {
3773 |             // Load tag statistics for bulk operations
3774 |             await this.loadTagSelectOptions();
3775 |             await this.loadTagManagementStats();
3776 |             await this.loadUntaggedCount();
3777 |         } catch (error) {
3778 |             console.error('Failed to load manage data:', error);
3779 |             this.showToast(this.t('toast.loadManageFail', 'Failed to load management data'), 'error');
3780 |         }
3781 |     }
3782 | 
3783 |     /**
3784 |      * Load count of untagged memories
3785 |      */
3786 |     async loadUntaggedCount() {
3787 |         try {
3788 |             const response = await fetch(`${this.apiBase}/manage/untagged/count`);
3789 |             if (!response.ok) return;
3790 | 
3791 |             const data = await response.json();
3792 |             const count = data.count || 0;
3793 |             const countSpan = document.getElementById('untaggedCount');
3794 |             const card = document.getElementById('deleteUntaggedCard');
3795 | 
3796 |             if (countSpan) {
3797 |                 countSpan.textContent = count;
3798 |             }
3799 |             if (card) {
3800 |                 card.style.display = count > 0 ? '' : 'none';
3801 |             }
3802 |         } catch (error) {
3803 |             console.error('Failed to load untagged count:', error);
3804 |         }
3805 |     }
3806 | 
3807 |     /**
3808 |      * Load tag options for bulk delete select
3809 |      */
3810 |     async loadTagSelectOptions() {
3811 |         try {
3812 |             const response = await fetch(`${this.apiBase}/manage/tags/stats`);
3813 |             if (!response.ok) throw new Error('Failed to load tags');
3814 | 
3815 |             const data = await response.json();
3816 |             const select = document.getElementById('deleteTagSelect');
3817 |             if (!select) return;
3818 | 
3819 |             // Clear existing options except the first
3820 |             while (select.children.length > 1) {
3821 |                 select.removeChild(select.lastChild);
3822 |             }
3823 | 
3824 |             // Add tag options
3825 |             data.tags.forEach(tagStat => {
3826 |                 const option = document.createElement('option');
3827 |                 option.value = tagStat.tag;
3828 |                 option.textContent = `${tagStat.tag} (${tagStat.count} memories)`;
3829 |                 option.dataset.count = tagStat.count;  // Store count in data attribute
3830 |                 select.appendChild(option);
3831 |             });
3832 |         } catch (error) {
3833 |             console.error('Failed to load tag options:', error);
3834 |         }
3835 |     }
3836 | 
3837 |     /**
3838 |      * Load tag management statistics
3839 |      */
3840 |     async loadTagManagementStats() {
3841 |         const container = document.getElementById('tagManagementContainer');
3842 |         if (!container) return;
3843 | 
3844 |         try {
3845 |             const response = await fetch(`${this.apiBase}/manage/tags/stats`);
3846 |             if (!response.ok) throw new Error('Failed to load tag stats');
3847 | 
3848 |             const data = await response.json();
3849 |             this.renderTagManagementTable(data);
3850 |         } catch (error) {
3851 |             console.error('Failed to load tag management stats:', error);
3852 |             container.innerHTML = '<p class="error">Failed to load tag statistics</p>';
3853 |         }
3854 |     }
3855 | 
3856 |     /**
3857 |      * Render tag management table
3858 |      */
3859 |     renderTagManagementTable(data) {
3860 |         const container = document.getElementById('tagManagementContainer');
3861 |         if (!container) return;
3862 | 
3863 |         let html = '<table class="tag-stats-table">';
3864 |         html += '<thead><tr>';
3865 |         html += '<th>Tag</th>';
3866 |         html += '<th>Count</th>';
3867 |         html += '<th>Actions</th>';
3868 |         html += '</tr></thead><tbody>';
3869 | 
3870 |         data.tags.forEach(tagStat => {
3871 |         html += '<tr>';
3872 |         html += `<td class="tag-name">${tagStat.tag}</td>`;
3873 |         html += `<td class="tag-count">${tagStat.count}</td>`;
3874 |         html += '<td class="tag-actions">';
3875 |         html += `<button class="tag-action-btn" data-action="rename-tag" data-tag="${this.escapeHtml(tagStat.tag)}">Rename</button>`;
3876 |         html += `<button class="tag-action-btn danger" data-action="delete-tag" data-tag="${this.escapeHtml(tagStat.tag)}" data-count="${tagStat.count}">Delete</button>`;
3877 |         html += '</td></tr>';
3878 |         });
3879 | 
3880 |         html += '</tbody></table>';
3881 |         container.innerHTML = html;
3882 | 
3883 |         // Add event listeners for tag actions
3884 |         container.addEventListener('click', (e) => {
3885 |             const button = e.target.closest('[data-action]');
3886 |             if (!button) return;
3887 | 
3888 |             const action = button.dataset.action;
3889 |             const tag = button.dataset.tag;
3890 | 
3891 |             if (action === 'rename-tag') {
3892 |                 this.renameTag(tag);
3893 |             } else if (action === 'delete-tag') {
3894 |                 const count = parseInt(button.dataset.count, 10);
3895 |                 this.deleteTag(tag, count);
3896 |             }
3897 |         });
3898 |     }
3899 | 
3900 |     /**
3901 |      * Handle bulk delete by tag
3902 |      */
3903 |     async handleBulkDeleteByTag() {
3904 |         const select = document.getElementById('deleteTagSelect');
3905 |         const tag = select.value;
3906 | 
3907 |         if (!tag) {
3908 |             this.showToast(this.t('toast.selectTagToDelete', 'Please select a tag to delete'), 'warning');
3909 |             return;
3910 |         }
3911 | 
3912 |         // Extract count from data attribute
3913 |         const option = select.querySelector(`option[value="${tag}"]`);
3914 |         const count = parseInt(option.dataset.count, 10) || 0;
3915 | 
3916 |         if (!await this.confirmBulkOperation(`Delete ${count} memories with tag "${tag}"?`)) {
3917 |             return;
3918 |         }
3919 | 
3920 |         this.setLoading(true);
3921 |         try {
3922 |             const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
3923 |                 method: 'POST',
3924 |                 headers: { 'Content-Type': 'application/json' },
3925 |                 body: JSON.stringify({
3926 |                     tag: tag,
3927 |                     confirm_count: count
3928 |                 })
3929 |             });
3930 | 
3931 |             const result = await response.json();
3932 | 
3933 |             // Check HTTP status first (handles HTTPException responses with 'detail')
3934 |             if (!response.ok) {
3935 |                 const errorMsg = result.detail || result.message || 'Unknown error';
3936 |                 this.showToast(errorMsg, 'error');
3937 |                 return;
3938 |             }
3939 | 
3940 |             if (result.success) {
3941 |                 this.showToast(result.message, 'success');
3942 |                 await this.loadManageData(); // Refresh data
3943 |                 await this.loadDashboardData(); // Refresh dashboard stats
3944 |             } else {
3945 |                 this.showToast(result.message || 'Operation failed', 'error');
3946 |             }
3947 |         } catch (error) {
3948 |             console.error('Bulk delete failed:', error);
3949 |             this.showToast(this.t('toast.bulkDeleteFailed', 'Bulk delete operation failed'), 'error');
3950 |         } finally {
3951 |             this.setLoading(false);
3952 |         }
3953 |     }
3954 | 
3955 |     /**
3956 |      * Handle cleanup duplicates
3957 |      */
3958 |     async handleCleanupDuplicates() {
3959 |         if (!await this.confirmBulkOperation('Remove all duplicate memories?')) {
3960 |             return;
3961 |         }
3962 | 
3963 |         this.setLoading(true);
3964 |         try {
3965 |             const response = await fetch(`${this.apiBase}/manage/cleanup-duplicates`, {
3966 |                 method: 'POST'
3967 |             });
3968 | 
3969 |             const result = await response.json();
3970 | 
3971 |             if (!response.ok) {
3972 |                 const errorMsg = result.detail || result.message || 'Unknown error';
3973 |                 this.showToast(errorMsg, 'error');
3974 |                 return;
3975 |             }
3976 | 
3977 |             if (result.success) {
3978 |                 this.showToast(result.message, 'success');
3979 |                 await this.loadManageData();
3980 |                 await this.loadDashboardData();
3981 |             } else {
3982 |                 this.showToast(result.message || 'Operation failed', 'error');
3983 |             }
3984 |         } catch (error) {
3985 |             console.error('Cleanup duplicates failed:', error);
3986 |             this.showToast(this.t('toast.cleanupFailed', 'Cleanup operation failed'), 'error');
3987 |         } finally {
3988 |             this.setLoading(false);
3989 |         }
3990 |     }
3991 | 
3992 |     /**
3993 |      * Handle bulk delete by date
3994 |      */
3995 |     async handleBulkDeleteByDate() {
3996 |         const dateInput = document.getElementById('deleteDateInput');
3997 |         const date = dateInput.value;
3998 | 
3999 |         if (!date) {
4000 |             this.showToast(this.t('toast.selectDate', 'Please select a date'), 'warning');
4001 |             return;
4002 |         }
4003 | 
4004 |         if (!await this.confirmBulkOperation(`Delete all memories before ${date}?`)) {
4005 |             return;
4006 |         }
4007 | 
4008 |         this.setLoading(true);
4009 |         try {
4010 |             const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
4011 |                 method: 'POST',
4012 |                 headers: { 'Content-Type': 'application/json' },
4013 |                 body: JSON.stringify({
4014 |                     before_date: date
4015 |                 })
4016 |             });
4017 | 
4018 |             const result = await response.json();
4019 | 
4020 |             if (!response.ok) {
4021 |                 const errorMsg = result.detail || result.message || 'Unknown error';
4022 |                 this.showToast(errorMsg, 'error');
4023 |                 return;
4024 |             }
4025 | 
4026 |             if (result.success) {
4027 |                 this.showToast(result.message, 'success');
4028 |                 await this.loadManageData();
4029 |                 await this.loadDashboardData();
4030 |             } else {
4031 |                 this.showToast(result.message || 'Operation failed', 'error');
4032 |             }
4033 |         } catch (error) {
4034 |             console.error('Bulk delete by date failed:', error);
4035 |             this.showToast(this.t('toast.bulkDeleteFailed', 'Bulk delete operation failed'), 'error');
4036 |         } finally {
4037 |             this.setLoading(false);
4038 |         }
4039 |     }
4040 | 
4041 |     /**
4042 |      * Handle delete untagged memories
4043 |      */
4044 |     async handleDeleteUntagged() {
4045 |         // Get current count
4046 |         const countSpan = document.getElementById('untaggedCount');
4047 |         const count = parseInt(countSpan?.textContent || '0', 10);
4048 | 
4049 |         if (count === 0) {
4050 |             this.showToast('No untagged memories to delete', 'info');
4051 |             return;
4052 |         }
4053 | 
4054 |         if (!await this.confirmBulkOperation(`Delete ${count} memories without tags?`)) {
4055 |             return;
4056 |         }
4057 | 
4058 |         this.setLoading(true);
4059 |         try {
4060 |             const response = await fetch(`${this.apiBase}/manage/delete-untagged?confirm_count=${count}`, {
4061 |                 method: 'POST'
4062 |             });
4063 | 
4064 |             const result = await response.json();
4065 | 
4066 |             if (!response.ok) {
4067 |                 const errorMsg = result.detail || result.message || 'Unknown error';
4068 |                 this.showToast(errorMsg, 'error');
4069 |                 return;
4070 |             }
4071 | 
4072 |             if (result.success) {
4073 |                 this.showToast(result.message, 'success');
4074 |                 await this.loadManageData();
4075 |                 await this.loadDashboardData();
4076 |             } else {
4077 |                 this.showToast(result.message || 'Operation failed', 'error');
4078 |             }
4079 |         } catch (error) {
4080 |             console.error('Delete untagged failed:', error);
4081 |             this.showToast('Delete untagged operation failed', 'error');
4082 |         } finally {
4083 |             this.setLoading(false);
4084 |         }
4085 |     }
4086 | 
4087 |     /**
4088 |      * Handle database optimization
4089 |      */
4090 |     async handleOptimizeDatabase() {
4091 |         this.showToast(this.t('toast.dbOptimizeTodo', 'Database optimization not yet implemented'), 'warning');
4092 |     }
4093 | 
4094 |     /**
4095 |      * Handle index rebuild
4096 |      */
4097 |     async handleRebuildIndex() {
4098 |         this.showToast(this.t('toast.indexRebuildTodo', 'Index rebuild not yet implemented'), 'warning');
4099 |     }
4100 | 
4101 |     /**
4102 |      * Rename a tag
4103 |      */
4104 |     async renameTag(oldTag) {
4105 |         const newTag = prompt(`Rename tag "${oldTag}" to:`, oldTag);
4106 |         if (!newTag || newTag === oldTag) return;
4107 | 
4108 |         this.showToast(this.t('toast.tagRenameTodo', 'Tag renaming not yet implemented'), 'warning');
4109 |     }
4110 | 
4111 |     /**
4112 |      * Delete a tag
4113 |      */
4114 |     async deleteTag(tag, count) {
4115 |         if (!await this.confirmBulkOperation(`Delete tag "${tag}" from ${count} memories?`)) {
4116 |             return;
4117 |         }
4118 | 
4119 |         this.showToast(this.t('toast.tagDeleteTodo', 'Tag deletion not yet implemented'), 'warning');
4120 |     }
4121 | 
4122 |     // ===== ANALYTICS TAB METHODS =====
4123 | 
4124 |     /**
4125 |      * Load analytics tab data
4126 |      */
4127 |     async loadAnalyticsData() {
4128 |         try {
4129 |             await Promise.all([
4130 |                 this.loadAnalyticsOverview(),
4131 |                 this.loadMemoryGrowthChart(),
4132 |                 this.loadTagUsageChart(),
4133 |                 this.loadMemoryTypesChart(),
4134 |                 this.loadActivityHeatmapChart(),
4135 |                 this.loadTopTagsReport(),
4136 |                 this.loadRecentActivityReport(),
4137 |                 this.loadStorageReport()
4138 |             ]);
4139 |         } catch (error) {
4140 |             console.error('Failed to load analytics data:', error);
4141 |             this.showToast(this.t('toast.loadAnalyticsFail', 'Failed to load analytics data'), 'error');
4142 |         }
4143 |     }
4144 | 
4145 |     /**
4146 |      * Load analytics overview metrics
4147 |      */
4148 |     async loadAnalyticsOverview() {
4149 |         try {
4150 |             const response = await fetch(`${this.apiBase}/analytics/overview`);
4151 |             if (!response.ok) throw new Error('Failed to load overview');
4152 | 
4153 |             const data = await response.json();
4154 | 
4155 |             // Update metric cards
4156 |             this.updateElementText('analyticsTotalMemories', data.total_memories || 0);
4157 |             this.updateElementText('analyticsThisWeek', data.memories_this_week || 0);
4158 |             this.updateElementText('analyticsUniqueTags', data.unique_tags || 0);
4159 |             this.updateElementText('analyticsDbSize', data.database_size_mb ?
4160 |                 `${data.database_size_mb.toFixed(1)} MB` : 'N/A');
4161 |         } catch (error) {
4162 |             console.error('Failed to load analytics overview:', error);
4163 |         }
4164 |     }
4165 | 
4166 |     /**
4167 |      * Load memory growth chart
4168 |      */
4169 |     async loadMemoryGrowthChart() {
4170 |         const container = document.getElementById('memoryGrowthChart');
4171 |         const period = document.getElementById('growthPeriodSelect').value;
4172 | 
4173 |         if (!container) return;
4174 | 
4175 |         try {
4176 |             const response = await fetch(`${this.apiBase}/analytics/memory-growth?period=${period}`);
4177 |             if (!response.ok) throw new Error('Failed to load growth data');
4178 | 
4179 |             const data = await response.json();
4180 |             this.renderMemoryGrowthChart(container, data);
4181 |         } catch (error) {
4182 |             console.error('Failed to load memory growth:', error);
4183 |             container.innerHTML = '<p class="error">Failed to load growth chart</p>';
4184 |         }
4185 |     }
4186 | 
4187 |     /**
4188 |      * Render memory growth chart
4189 |      */
4190 |     renderMemoryGrowthChart(container, data) {
4191 |         if (!data.data_points || data.data_points.length === 0) {
4192 |             container.innerHTML = '<p>No growth data available</p>';
4193 |             return;
4194 |         }
4195 | 
4196 |         // Find max count for scaling
4197 |         const recentPoints = data.data_points.slice(-10);
4198 |         const maxCount = Math.max(...recentPoints.map(p => p.count), 1);
4199 | 
4200 |         let html = '<div class="simple-chart">';
4201 | 
4202 |         recentPoints.forEach(point => {
4203 |             // Normalize bar width relative to max, then convert to pixels (200px scale)
4204 |             const barWidthPx = (point.count / maxCount) * 200;
4205 |             const displayCount = point.count || 0;
4206 |             const displayCumulative = point.cumulative || 0;
4207 |             // Use label if available, otherwise fall back to date for backward compatibility
4208 |             const displayLabel = point.label || point.date;
4209 | 
4210 |             html += `<div class="chart-row">
4211 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
4212 |                 <span class="chart-value">+${displayCount} <small>(${displayCumulative} total)</small></span>
4213 |                 <span class="chart-label">${displayLabel}</span>
4214 |             </div>`;
4215 |         });
4216 | 
4217 |         html += '</div>';
4218 |         container.innerHTML = html;
4219 |     }
4220 | 
4221 |     /**
4222 |      * Load tag usage chart
4223 |      */
4224 |     async loadTagUsageChart() {
4225 |         const container = document.getElementById('tagUsageChart');
4226 |         if (!container) return;
4227 | 
4228 |         try {
4229 |             const response = await fetch(`${this.apiBase}/analytics/tag-usage`);
4230 |             if (!response.ok) throw new Error('Failed to load tag usage');
4231 | 
4232 |             const data = await response.json();
4233 |             this.renderTagUsageChart(container, data);
4234 |         } catch (error) {
4235 |             console.error('Failed to load tag usage:', error);
4236 |             container.innerHTML = '<p class="error">Failed to load tag usage chart</p>';
4237 |         }
4238 |     }
4239 | 
4240 |     /**
4241 |      * Render tag usage chart
4242 |      */
4243 |     renderTagUsageChart(container, data) {
4244 |         if (!data.tags || data.tags.length === 0) {
4245 |             container.innerHTML = '<p>No tags found</p>';
4246 |             return;
4247 |         }
4248 | 
4249 |         // Filter tags with >10 memories, aggregate the rest
4250 |         const significantTags = data.tags.filter(t => t.count > 10);
4251 |         const minorTags = data.tags.filter(t => t.count <= 10);
4252 | 
4253 |         let html = '<div class="simple-chart">';
4254 | 
4255 |         // Render significant tags
4256 |         significantTags.forEach(tag => {
4257 |             const barWidthPx = (tag.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
4258 |             html += `<div class="chart-row">
4259 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
4260 |                 <span class="chart-value">${tag.count} (${tag.percentage}%)</span>
4261 |                 <span class="chart-label">${tag.tag}</span>
4262 |             </div>`;
4263 |         });
4264 | 
4265 |         // Add "diverse" category if there are minor tags
4266 |         if (minorTags.length > 0) {
4267 |             const diverseCount = minorTags.reduce((sum, t) => sum + t.count, 0);
4268 |             const diversePercentage = minorTags.reduce((sum, t) => sum + t.percentage, 0);
4269 |             const barWidthPx = (diversePercentage / 100) * 200;
4270 |             html += `<div class="chart-row">
4271 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
4272 |                 <span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
4273 |                 <span class="chart-label" title="${minorTags.length} tags with ≤10 memories each">diverse (${minorTags.length} tags)</span>
4274 |             </div>`;
4275 |         }
4276 | 
4277 |         html += '</div>';
4278 |         container.innerHTML = html;
4279 |     }
4280 | 
4281 |     /**
4282 |      * Load memory types chart
4283 |      */
4284 |     async loadMemoryTypesChart() {
4285 |         const container = document.getElementById('memoryTypesChart');
4286 |         if (!container) return;
4287 | 
4288 |         try {
4289 |             const response = await fetch(`${this.apiBase}/analytics/memory-types`);
4290 |             if (!response.ok) throw new Error('Failed to load memory types');
4291 | 
4292 |             const data = await response.json();
4293 |             this.renderMemoryTypesChart(container, data);
4294 |         } catch (error) {
4295 |             console.error('Failed to load memory types:', error);
4296 |             container.innerHTML = '<p class="error">Failed to load memory types chart</p>';
4297 |         }
4298 |     }
4299 | 
4300 |     /**
4301 |      * Render memory types chart
4302 |      */
4303 |     renderMemoryTypesChart(container, data) {
4304 |         if (!data.types || data.types.length === 0) {
4305 |             container.innerHTML = '<p>No memory types found</p>';
4306 |             return;
4307 |         }
4308 | 
4309 |         // Filter types with >10 memories, aggregate the rest
4310 |         const significantTypes = data.types.filter(t => t.count > 10);
4311 |         const minorTypes = data.types.filter(t => t.count <= 10);
4312 | 
4313 |         let html = '<div class="simple-chart">';
4314 | 
4315 |         // Render significant types
4316 |         significantTypes.forEach(type => {
4317 |             const barWidthPx = (type.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
4318 |             const typeName = type.memory_type || 'untyped';
4319 |             html += `<div class="chart-row">
4320 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
4321 |                 <span class="chart-value">${type.count} (${type.percentage.toFixed(1)}%)</span>
4322 |                 <span class="chart-label" title="${typeName}">${typeName}</span>
4323 |             </div>`;
4324 |         });
4325 | 
4326 |         // Add "diverse" category if there are minor types
4327 |         if (minorTypes.length > 0) {
4328 |             const diverseCount = minorTypes.reduce((sum, t) => sum + t.count, 0);
4329 |             const diversePercentage = minorTypes.reduce((sum, t) => sum + t.percentage, 0);
4330 |             const barWidthPx = (diversePercentage / 100) * 200;
4331 |             html += `<div class="chart-row">
4332 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
4333 |                 <span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
4334 |                 <span class="chart-label" title="${minorTypes.length} types with ≤10 memories each">diverse (${minorTypes.length} types)</span>
4335 |             </div>`;
4336 |         }
4337 | 
4338 |         html += '</div>';
4339 |         container.innerHTML = html;
4340 |     }
4341 | 
4342 |     /**
4343 |     * Load top tags report
4344 |     */
4345 |     async loadTopTagsReport() {
4346 |     const container = document.getElementById('topTagsList');
4347 |     const period = document.getElementById('topTagsPeriodSelect')?.value || '30d';
4348 |         if (!container) return;
4349 | 
4350 |     try {
4351 |     const response = await fetch(`${this.apiBase}/analytics/top-tags?period=${period}`);
4352 |             if (!response.ok) throw new Error('Failed to load top tags');
4353 | 
4354 |     const data = await response.json();
4355 |         this.renderTopTagsReport(container, data);
4356 |     } catch (error) {
4357 |     console.error('Failed to load top tags:', error);
4358 |         container.innerHTML = '<p class="error">Failed to load top tags</p>';
4359 |         }
4360 |     }
4361 | 
4362 |     /**
4363 |     * Render top tags report
4364 |     */
4365 |     renderTopTagsReport(container, data) {
4366 |     if (!data.tags || data.tags.length === 0) {
4367 |     container.innerHTML = '<p>No tags found</p>';
4368 |     return;
4369 |     }
4370 | 
4371 |     let html = '<div class="enhanced-tags-report">';
4372 |     html += `<div class="report-period">Period: ${data.period}</div>`;
4373 |     html += '<ul class="tags-list">';
4374 |     data.tags.slice(0, 10).forEach(tag => {
4375 |         const trendIcon = tag.trending ? '📈' : '';
4376 |             const growthText = tag.growth_rate !== null ? ` (${tag.growth_rate > 0 ? '+' : ''}${tag.growth_rate}%)` : '';
4377 |         html += `<li>
4378 |                 <div class="tag-header">
4379 |                     <strong>${tag.tag}</strong>${trendIcon}
4380 |                     <span class="tag-count">${tag.count} memories (${tag.percentage}%)${growthText}</span>
4381 |                 </div>`;
4382 |             if (tag.co_occurring_tags && tag.co_occurring_tags.length > 0) {
4383 |                 html += '<div class="tag-cooccurrence">Often with: ';
4384 |                 html += tag.co_occurring_tags.slice(0, 3).map(co => `${co.tag} (${co.strength.toFixed(2)})`).join(', ');
4385 |                 html += '</div>';
4386 |             }
4387 |             html += '</li>';
4388 |         });
4389 |         html += '</ul></div>';
4390 | 
4391 |         container.innerHTML = html;
4392 |     }
4393 | 
4394 |     /**
4395 |     * Load recent activity report
4396 |     */
4397 |     async loadRecentActivityReport() {
4398 |     const container = document.getElementById('recentActivityList');
4399 |     const granularity = document.getElementById('activityGranularitySelect')?.value || 'daily';
4400 |         if (!container) return;
4401 | 
4402 |     try {
4403 |     const response = await fetch(`${this.apiBase}/analytics/activity-breakdown?granularity=${granularity}`);
4404 |     if (!response.ok) throw new Error('Failed to load activity breakdown');
4405 | 
4406 |     const data = await response.json();
4407 |     this.renderRecentActivityReport(container, data);
4408 |     } catch (error) {
4409 |     console.error('Failed to load recent activity:', error);
4410 |     container.innerHTML = '<p class="error">Failed to load recent activity</p>';
4411 |     }
4412 |     }
4413 | 
4414 |     /**
4415 |     * Render recent activity report
4416 |     */
4417 |     renderRecentActivityReport(container, data) {
4418 |     let html = '<div class="activity-breakdown">';
4419 | 
4420 |     // Summary stats
4421 |     html += '<div class="activity-summary">';
4422 |         html += `<div class="activity-stat"><strong>Active Days:</strong> ${data.active_days}/${data.total_days}</div>`;
4423 |     html += `<div class="activity-stat"><strong>Current Streak:</strong> ${data.current_streak} days</div>`;
4424 |     html += `<div class="activity-stat"><strong>Longest Streak:</strong> ${data.longest_streak} days</div>`;
4425 |     html += '</div>';
4426 | 
4427 |     if (data.peak_times && data.peak_times.length > 0) {
4428 |     html += '<div class="peak-times">';
4429 |         html += '<strong>Peak Times:</strong> ';
4430 |         html += data.peak_times.join(', ');
4431 |             html += '</div>';
4432 |     }
4433 | 
4434 |         // Activity breakdown chart
4435 |         if (data.breakdown && data.breakdown.length > 0) {
4436 |             html += '<div class="activity-chart">';
4437 |             // Calculate total count for percentage-based distribution
4438 |             const totalCount = data.breakdown.reduce((sum, d) => sum + d.count, 0);
4439 |             const maxCount = Math.max(...data.breakdown.map(d => d.count));
4440 | 
4441 |             data.breakdown.forEach(item => {
4442 |                 // Show percentage of total activity, with minimum width for visibility
4443 |                 const percentage = totalCount > 0 ? (item.count / totalCount * 100) : 0;
4444 |                 // Use percentage for bar width, but scale up for better visualization
4445 |                 // Use max count for scaling to ensure largest bar reaches reasonable width
4446 |                 const barWidth = totalCount > 0 ? (item.count / maxCount * 100) : 0;
4447 | 
4448 |                 html += `<div class="activity-bar-row">
4449 |                     <span class="activity-label">${item.label}</span>
4450 |                     <div class="activity-bar" style="width: ${barWidth}%" title="${item.count} memories (${percentage.toFixed(1)}%)"></div>
4451 |                     <span class="activity-count">${item.count} (${percentage.toFixed(1)}%)</span>
4452 |                 </div>`;
4453 |             });
4454 | 
4455 |             html += '</div>';
4456 |         }
4457 | 
4458 |         html += '</div>';
4459 |         container.innerHTML = html;
4460 |     }
4461 | 
4462 |     /**
4463 |     * Load activity heatmap chart
4464 |     */
4465 |     async loadActivityHeatmapChart() {
4466 |     const container = document.getElementById('activityHeatmapChart');
4467 |         const period = document.getElementById('heatmapPeriodSelect').value;
4468 | 
4469 |         if (!container) return;
4470 | 
4471 |         try {
4472 |             const response = await fetch(`${this.apiBase}/analytics/activity-heatmap?days=${period}`);
4473 |             if (!response.ok) throw new Error('Failed to load heatmap data');
4474 | 
4475 |             const data = await response.json();
4476 |             this.renderActivityHeatmapChart(container, data);
4477 |         } catch (error) {
4478 |             console.error('Failed to load activity heatmap:', error);
4479 |             container.innerHTML = '<p class="error">Failed to load activity heatmap</p>';
4480 |         }
4481 |     }
4482 | 
4483 |     /**
4484 |      * Render activity heatmap chart
4485 |      */
4486 |     renderActivityHeatmapChart(container, data) {
4487 |         if (!data.data || data.data.length === 0) {
4488 |             container.innerHTML = '<p>No activity data available</p>';
4489 |             return;
4490 |         }
4491 | 
4492 |         // Create calendar grid
4493 |         let html = '<div class="activity-heatmap">';
4494 |         html += '<div class="heatmap-stats">';
4495 |         html += `<span>${data.total_days} active days</span>`;
4496 |         html += `<span>Max: ${data.max_count} memories/day</span>`;
4497 |         html += '</div>';
4498 | 
4499 |         // Group by months
4500 |         const months = {};
4501 |         data.data.forEach(day => {
4502 |             const date = new Date(day.date);
4503 |             const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
4504 |             if (!months[monthKey]) {
4505 |                 months[monthKey] = [];
4506 |             }
4507 |             months[monthKey].push(day);
4508 |         });
4509 | 
4510 |         // Render each month
4511 |         Object.keys(months).sort().reverse().forEach(monthKey => {
4512 |             const [year, month] = monthKey.split('-');
4513 |             const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'short' });
4514 | 
4515 |             html += `<div class="heatmap-month">`;
4516 |             html += `<div class="month-label">${monthName} ${year}</div>`;
4517 |             html += '<div class="month-grid">';
4518 | 
4519 |             // Create 7x6 grid (weeks x days)
4520 |             const monthData = months[monthKey];
4521 |             // Parse date in local timezone to avoid day-of-week shifting near timezone boundaries
4522 |             const [fYear, fMonth, fDay] = monthData[0].date.split('-').map(Number);
4523 |             const firstDay = new Date(fYear, fMonth - 1, fDay).getDay();
4524 | 
4525 |             // Add empty cells for days before month starts
4526 |             for (let i = 0; i < firstDay; i++) {
4527 |                 html += '<div class="heatmap-cell empty"></div>';
4528 |             }
4529 | 
4530 |             // Add cells for each day
4531 |             monthData.forEach(day => {
4532 |                 const level = day.level;
4533 |                 const tooltip = `${day.date}: ${day.count} memories`;
4534 |                 html += `<div class="heatmap-cell level-${level}" title="${tooltip}"></div>`;
4535 |             });
4536 | 
4537 |             html += '</div></div>';
4538 |         });
4539 | 
4540 |         // Legend
4541 |         html += '<div class="heatmap-legend">';
4542 |         html += '<span>Less</span>';
4543 |         for (let i = 0; i <= 4; i++) {
4544 |             html += `<div class="legend-cell level-${i}"></div>`;
4545 |         }
4546 |         html += '<span>More</span>';
4547 |         html += '</div>';
4548 | 
4549 |         html += '</div>';
4550 |         container.innerHTML = html;
4551 |     }
4552 | 
4553 |     /**
4554 |      * Handle heatmap period change
4555 |      */
4556 |     async handleHeatmapPeriodChange() {
4557 |         await this.loadActivityHeatmapChart();
4558 |     }
4559 | 
4560 |     /**
4561 |      * Handle top tags period change
4562 |      */
4563 |     async handleTopTagsPeriodChange() {
4564 |         await this.loadTopTagsReport();
4565 |     }
4566 | 
4567 |     /**
4568 |      * Handle activity granularity change
4569 |      */
4570 |     async handleActivityGranularityChange() {
4571 |         await this.loadRecentActivityReport();
4572 |     }
4573 | 
4574 |     /**
4575 |      * Load storage report
4576 |      */
4577 |     async loadStorageReport() {
4578 |         const container = document.getElementById('storageReport');
4579 |         if (!container) return;
4580 | 
4581 |         try {
4582 |             const response = await fetch(`${this.apiBase}/analytics/storage-stats`);
4583 |             if (!response.ok) throw new Error('Failed to load storage stats');
4584 | 
4585 |             const data = await response.json();
4586 |             this.renderStorageReport(container, data);
4587 |         } catch (error) {
4588 |             console.error('Failed to load storage report:', error);
4589 |             container.innerHTML = '<p class="error">Failed to load storage report</p>';
4590 |         }
4591 |     }
4592 | 
4593 |     /**
4594 |      * Render storage report
4595 |      */
4596 |     renderStorageReport(container, data) {
4597 |         let html = '<div class="storage-report">';
4598 | 
4599 |         // Summary stats
4600 |         html += '<div class="storage-summary">';
4601 |         html += `<div class="storage-stat"><strong>Total Size:</strong> ${data.total_size_mb} MB</div>`;
4602 |         html += `<div class="storage-stat"><strong>Average Memory:</strong> ${data.average_memory_size} chars</div>`;
4603 |         html += `<div class="storage-stat"><strong>Efficiency:</strong> ${data.storage_efficiency}%</div>`;
4604 |         html += '</div>';
4605 | 
4606 |         // Largest memories
4607 |         if (data.largest_memories && data.largest_memories.length > 0) {
4608 |             html += '<h4>Largest Memories</h4>';
4609 |             html += '<ul class="largest-memories">';
4610 |             data.largest_memories.slice(0, 5).forEach(memory => {
4611 |                 // Backend provides created_at as ISO string, not timestamp
4612 |                 const date = memory.created_at ? new Date(memory.created_at).toLocaleDateString() : 'Unknown';
4613 |                 // Backend provides size_kb and preview (not size and content_preview)
4614 |                 const sizeDisplay = memory.size_kb ? `${memory.size_kb} KB` : `${memory.size_bytes || 0} bytes`;
4615 |                 html += `<li>
4616 |                     <div class="memory-size">${sizeDisplay}</div>
4617 |                     <div class="memory-preview">${this.escapeHtml(memory.preview || '')}</div>
4618 |                     <div class="memory-meta">${date} • Tags: ${memory.tags.join(', ') || 'none'}</div>
4619 |                 </li>`;
4620 |             });
4621 |             html += '</ul>';
4622 |         }
4623 | 
4624 |         html += '</div>';
4625 |         container.innerHTML = html;
4626 |     }
4627 | 
4628 |     /**
4629 |      * Handle growth period change
4630 |      */
4631 |     async handleGrowthPeriodChange() {
4632 |         await this.loadMemoryGrowthChart();
4633 |     }
4634 | 
4635 |     // ===== QUALITY ANALYTICS METHODS =====
4636 | 
4637 |     /**
4638 |      * Load quality analytics dashboard
4639 |      */
4640 |     async loadQualityAnalytics() {
4641 |         try {
4642 |             const response = await fetch(`${this.apiBase}/quality/distribution`);
4643 |             if (!response.ok) throw new Error('Failed to load quality analytics');
4644 | 
4645 |             const data = await response.json();
4646 | 
4647 |             // Update summary stats
4648 |             this.updateElementText('quality-total-memories', data.total_memories.toLocaleString());
4649 |             this.updateElementText('quality-high-count', data.high_quality_count.toLocaleString());
4650 |             this.updateElementText('quality-medium-count', data.medium_quality_count.toLocaleString());
4651 |             this.updateElementText('quality-low-count', data.low_quality_count.toLocaleString());
4652 |             this.updateElementText('quality-average-score', data.average_score.toFixed(2));
4653 | 
4654 |             // Render charts
4655 |             this.renderQualityDistributionChart(data);
4656 |             this.renderQualityProviderChart(data.provider_breakdown);
4657 | 
4658 |             // Render top/bottom memories
4659 |             this.renderTopQualityMemories(data.top_memories);
4660 |             this.renderBottomQualityMemories(data.bottom_memories);
4661 | 
4662 |         } catch (error) {
4663 |             console.error('Failed to load quality analytics:', error);
4664 |             this.showToast('Failed to load quality analytics', 'error');
4665 |         }
4666 |     }
4667 | 
4668 |     /**
4669 |      * Render quality distribution bar chart
4670 |      */
4671 |     renderQualityDistributionChart(data) {
4672 |         const canvas = document.getElementById('quality-distribution-chart');
4673 |         if (!canvas) return;
4674 | 
4675 |         // Destroy existing chart if present
4676 |         if (this.qualityDistributionChart) {
4677 |             this.qualityDistributionChart.destroy();
4678 |         }
4679 | 
4680 |         const ctx = canvas.getContext('2d');
4681 |         const isDark = document.body.classList.contains('dark-mode');
4682 |         const textColor = isDark ? '#f9fafb' : '#374151';
4683 |         const gridColor = isDark ? '#374151' : '#e5e7eb';
4684 | 
4685 |         this.qualityDistributionChart = new Chart(ctx, {
4686 |             type: 'bar',
4687 |             data: {
4688 |                 labels: ['Low (<0.5)', 'Medium (0.5-0.7)', 'High (≥0.7)'],
4689 |                 datasets: [{
4690 |                     label: 'Number of Memories',
4691 |                     data: [
4692 |                         data.low_quality_count,
4693 |                         data.medium_quality_count,
4694 |                         data.high_quality_count
4695 |                     ],
4696 |                     backgroundColor: [
4697 |                         '#F8D7DA',  // Low
4698 |                         '#FFF3CD',  // Medium
4699 |                         '#D4EDDA'   // High
4700 |                     ],
4701 |                     borderColor: [
4702 |                         '#F5C6CB',
4703 |                         '#FFEAA7',
4704 |                         '#C3E6CB'
4705 |                     ],
4706 |                     borderWidth: 1
4707 |                 }]
4708 |             },
4709 |             options: {
4710 |                 responsive: true,
4711 |                 maintainAspectRatio: true,
4712 |                 scales: {
4713 |                     y: {
4714 |                         beginAtZero: true,
4715 |                         title: {
4716 |                             display: true,
4717 |                             text: 'Count',
4718 |                             color: textColor
4719 |                         },
4720 |                         ticks: {
4721 |                             color: textColor
4722 |                         },
4723 |                         grid: {
4724 |                             color: gridColor
4725 |                         }
4726 |                     },
4727 |                     x: {
4728 |                         ticks: {
4729 |                             color: textColor
4730 |                         },
4731 |                         grid: {
4732 |                             color: gridColor
4733 |                         }
4734 |                     }
4735 |                 },
4736 |                 plugins: {
4737 |                     legend: {
4738 |                         display: false
4739 |                     }
4740 |                 }
4741 |             }
4742 |         });
4743 |     }
4744 | 
4745 |     /**
4746 |      * Render quality provider pie chart
4747 |      */
4748 |     renderQualityProviderChart(providerData) {
4749 |         const canvas = document.getElementById('quality-provider-chart');
4750 |         if (!canvas) return;
4751 | 
4752 |         // Destroy existing chart if present
4753 |         if (this.qualityProviderChart) {
4754 |             this.qualityProviderChart.destroy();
4755 |         }
4756 | 
4757 |         const labels = Object.keys(providerData);
4758 |         const values = Object.values(providerData);
4759 | 
4760 |         // Friendly provider names
4761 |         const friendlyLabels = labels.map(label => {
4762 |             switch(label) {
4763 |                 case 'ONNXRankerModel': return 'Local SLM';
4764 |                 case 'GroqEvaluator': return 'Groq API';
4765 |                 case 'GeminiEvaluator': return 'Gemini API';
4766 |                 case 'ImplicitSignalsEvaluator': return 'Implicit Only';
4767 |                 case 'local': return 'Local SLM';
4768 |                 case 'groq': return 'Groq API';
4769 |                 case 'gemini': return 'Gemini API';
4770 |                 case 'implicit': return 'Implicit Only';
4771 |                 default: return label;
4772 |             }
4773 |         });
4774 | 
4775 |         const ctx = canvas.getContext('2d');
4776 |         const isDark = document.body.classList.contains('dark-mode');
4777 |         const textColor = isDark ? '#f9fafb' : '#374151';
4778 | 
4779 |         this.qualityProviderChart = new Chart(ctx, {
4780 |             type: 'pie',
4781 |             data: {
4782 |                 labels: friendlyLabels,
4783 |                 datasets: [{
4784 |                     data: values,
4785 |                     backgroundColor: [
4786 |                         '#4CAF50',  // Local (primary)
4787 |                         '#2196F3',  // Groq
4788 |                         '#FF9800',  // Gemini
4789 |                         '#9E9E9E'   // Implicit
4790 |                     ]
4791 |                 }]
4792 |             },
4793 |             options: {
4794 |                 responsive: true,
4795 |                 maintainAspectRatio: true,
4796 |                 plugins: {
4797 |                     legend: {
4798 |                         position: 'bottom',
4799 |                         labels: {
4800 |                             color: textColor
4801 |                         }
4802 |                     }
4803 |                 }
4804 |             }
4805 |         });
4806 |     }
4807 | 
4808 |     /**
4809 |      * Attach click handlers to memory preview elements
4810 |      * @param {HTMLElement} container - Container with .memory-preview elements
4811 |      * @param {Array} memories - Array of memory objects corresponding to preview elements
4812 |      */
4813 |     _attachMemoryClickHandlers(container, memories) {
4814 |         container.querySelectorAll('.memory-preview').forEach((el, index) => {
4815 |             el.addEventListener('click', () => this.handleMemoryClick(memories[index]));
4816 |         });
4817 |     }
4818 | 
4819 |     /**
4820 |      * Render top quality memories list
4821 |      */
4822 |     renderTopQualityMemories(memories) {
4823 |         const container = document.getElementById('quality-top-memories-list');
4824 |         if (!container) return;
4825 | 
4826 |         if (!memories || memories.length === 0) {
4827 |             container.innerHTML = '<p class="text-muted">No memories available</p>';
4828 |             return;
4829 |         }
4830 | 
4831 |         const html = memories.map(memory => `
4832 |             <div class="memory-preview" data-content-hash="${memory.content_hash}">
4833 |                 <div class="quality-badge quality-tier-high">
4834 |                     <span class="quality-star">★</span>
4835 |                     <span class="quality-score">${memory.quality_score.toFixed(2)}</span>
4836 |                 </div>
4837 |                 <div class="memory-content">${this.escapeHtml(memory.content)}</div>
4838 |             </div>
4839 |         `).join('');
4840 | 
4841 |         container.innerHTML = html;
4842 |         this._attachMemoryClickHandlers(container, memories);
4843 |     }
4844 | 
4845 |     /**
4846 |      * Render bottom quality memories list (for improvement)
4847 |      */
4848 |     renderBottomQualityMemories(memories) {
4849 |         const container = document.getElementById('quality-bottom-memories-list');
4850 |         if (!container) return;
4851 | 
4852 |         if (!memories || memories.length === 0) {
4853 |             container.innerHTML = '<p class="text-muted">No memories available</p>';
4854 |             return;
4855 |         }
4856 | 
4857 |         const html = memories.map(memory => `
4858 |             <div class="memory-preview" data-content-hash="${memory.content_hash}">
4859 |                 <div class="quality-badge quality-tier-low">
4860 |                     <span class="quality-star">★</span>
4861 |                     <span class="quality-score">${memory.quality_score.toFixed(2)}</span>
4862 |                 </div>
4863 |                 <div class="memory-content">${this.escapeHtml(memory.content)}</div>
4864 |             </div>
4865 |         `).join('');
4866 | 
4867 |         container.innerHTML = html;
4868 |         this._attachMemoryClickHandlers(container, memories);
4869 |     }
4870 | 
4871 |     /**
4872 |      * Render quality badge for a memory
4873 |      */
4874 |     renderQualityBadge(memory) {
4875 |         const score = memory.quality_score || memory.metadata?.quality_score || 0.5;
4876 |         const provider = memory.quality_provider || memory.metadata?.quality_provider || 'none';
4877 | 
4878 |         // Determine tier
4879 |         let tier = 'low';
4880 |         if (score >= 0.7) tier = 'high';
4881 |         else if (score >= 0.5) tier = 'medium';
4882 | 
4883 |         return `
4884 |             <div class="quality-badge quality-tier-${tier}"
4885 |                  data-quality-score="${score.toFixed(2)}"
4886 |                  title="Quality Score: ${score.toFixed(2)} (${provider})">
4887 |                 <span class="quality-star">★</span>
4888 |                 <span class="quality-score">${score.toFixed(2)}</span>
4889 |             </div>
4890 |         `;
4891 |     }
4892 | 
4893 |     /**
4894 |      * Rate a memory manually
4895 |      */
4896 |     async rateMemory(contentHash, rating) {
4897 |         try {
4898 |             const response = await fetch(`${this.apiBase}/quality/memories/${contentHash}/rate`, {
4899 |                 method: 'POST',
4900 |                 headers: { 'Content-Type': 'application/json' },
4901 |                 body: JSON.stringify({
4902 |                     rating: rating,
4903 |                     feedback: ''
4904 |                 })
4905 |             });
4906 | 
4907 |             if (!response.ok) throw new Error('Failed to rate memory');
4908 | 
4909 |             const result = await response.json();
4910 |             this.showToast(`Rating saved! Quality score updated to ${result.new_quality_score.toFixed(2)}`, 'success');
4911 | 
4912 |             // Refresh memory display if viewing details
4913 |             // This would refresh the modal or current view
4914 | 
4915 |         } catch (error) {
4916 |             console.error('Failed to rate memory:', error);
4917 |             this.showToast('Failed to save rating', 'error');
4918 |         }
4919 |     }
4920 | 
4921 |     // ===== UTILITY METHODS =====
4922 | 
4923 |     /**
4924 |      * Show confirmation dialog for bulk operations
4925 |      */
4926 |     async confirmBulkOperation(message) {
4927 |         return confirm(`⚠️ WARNING: ${message}
4928 | 
4929 | This action cannot be undone. Are you sure?`);
4930 |     }
4931 | 
4932 |     /**
4933 |      * Update element text content
4934 |      */
4935 |     updateElementText(elementId, text) {
4936 |         const element = document.getElementById(elementId);
4937 |         if (element) {
4938 |             element.textContent = text;
4939 |         }
4940 |     }
4941 | 
4942 |     /**
4943 |      * Cleanup when page unloads
4944 |      */
4945 |     destroy() {
4946 |         if (this.eventSource) {
4947 |             this.eventSource.close();
4948 |         }
4949 |     }
4950 | }
4951 | 
4952 | // Initialize the application when DOM is ready
4953 | console.log('⚡ Registering DOMContentLoaded listener');
4954 | document.addEventListener('DOMContentLoaded', () => {
4955 |     console.log('⚡ DOMContentLoaded fired - Creating MemoryDashboard instance');
4956 |     window.app = new MemoryDashboard();
4957 |     console.log('⚡ MemoryDashboard created, window.app =', window.app);
4958 | });
4959 | 
4960 | // Cleanup on page unload
4961 | window.addEventListener('beforeunload', () => {
4962 |     if (window.app) {
4963 |         window.app.destroy();
4964 |     }
4965 | });
4966 | 
```
Page 61/62FirstPrevNextLast