local-deep-research 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. local_deep_research/__init__.py +7 -0
  2. local_deep_research/__version__.py +1 -1
  3. local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
  4. local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
  5. local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
  6. local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
  7. local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
  8. local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
  9. local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
  10. local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
  11. local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
  12. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
  13. local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
  14. local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
  15. local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
  16. local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
  17. local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
  18. local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
  19. local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
  20. local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
  21. local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
  22. local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
  23. local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
  24. local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
  25. local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
  26. local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
  27. local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
  28. local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
  29. local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
  30. local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
  31. local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
  32. local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
  33. local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
  34. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
  35. local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
  36. local_deep_research/advanced_search_system/findings/repository.py +54 -17
  37. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
  38. local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
  39. local_deep_research/advanced_search_system/questions/__init__.py +16 -0
  40. local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
  41. local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
  42. local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
  43. local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
  44. local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
  45. local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
  46. local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
  47. local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
  48. local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
  49. local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
  50. local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
  51. local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
  52. local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
  53. local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
  54. local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
  55. local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
  56. local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
  57. local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
  58. local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
  59. local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
  60. local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
  61. local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
  62. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
  63. local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
  64. local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
  65. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
  66. local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
  67. local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
  68. local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
  69. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
  70. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
  71. local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
  72. local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
  73. local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
  74. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
  75. local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
  76. local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
  77. local_deep_research/api/benchmark_functions.py +6 -2
  78. local_deep_research/api/research_functions.py +10 -4
  79. local_deep_research/benchmarks/__init__.py +9 -7
  80. local_deep_research/benchmarks/benchmark_functions.py +6 -2
  81. local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
  82. local_deep_research/benchmarks/cli.py +38 -13
  83. local_deep_research/benchmarks/comparison/__init__.py +4 -2
  84. local_deep_research/benchmarks/comparison/evaluator.py +316 -239
  85. local_deep_research/benchmarks/datasets/__init__.py +1 -1
  86. local_deep_research/benchmarks/datasets/base.py +91 -72
  87. local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
  88. local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
  89. local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
  90. local_deep_research/benchmarks/datasets/utils.py +48 -29
  91. local_deep_research/benchmarks/datasets.py +4 -11
  92. local_deep_research/benchmarks/efficiency/__init__.py +8 -4
  93. local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
  94. local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
  95. local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
  96. local_deep_research/benchmarks/evaluators/composite.py +6 -2
  97. local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
  98. local_deep_research/benchmarks/graders.py +32 -10
  99. local_deep_research/benchmarks/metrics/README.md +1 -1
  100. local_deep_research/benchmarks/metrics/calculation.py +25 -10
  101. local_deep_research/benchmarks/metrics/reporting.py +7 -3
  102. local_deep_research/benchmarks/metrics/visualization.py +42 -23
  103. local_deep_research/benchmarks/metrics.py +1 -1
  104. local_deep_research/benchmarks/optimization/__init__.py +3 -1
  105. local_deep_research/benchmarks/optimization/api.py +7 -1
  106. local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
  107. local_deep_research/benchmarks/runners.py +48 -15
  108. local_deep_research/citation_handler.py +65 -92
  109. local_deep_research/citation_handlers/__init__.py +15 -0
  110. local_deep_research/citation_handlers/base_citation_handler.py +70 -0
  111. local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
  112. local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
  113. local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
  114. local_deep_research/config/llm_config.py +271 -169
  115. local_deep_research/config/search_config.py +14 -5
  116. local_deep_research/defaults/__init__.py +0 -1
  117. local_deep_research/defaults/default_settings.json +35 -35
  118. local_deep_research/metrics/__init__.py +13 -0
  119. local_deep_research/metrics/database.py +58 -0
  120. local_deep_research/metrics/db_models.py +115 -0
  121. local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
  122. local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
  123. local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
  124. local_deep_research/metrics/migrate_research_ratings.py +31 -0
  125. local_deep_research/metrics/models.py +61 -0
  126. local_deep_research/metrics/pricing/__init__.py +12 -0
  127. local_deep_research/metrics/pricing/cost_calculator.py +237 -0
  128. local_deep_research/metrics/pricing/pricing_cache.py +143 -0
  129. local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
  130. local_deep_research/metrics/query_utils.py +51 -0
  131. local_deep_research/metrics/search_tracker.py +380 -0
  132. local_deep_research/metrics/token_counter.py +1078 -0
  133. local_deep_research/migrate_db.py +3 -1
  134. local_deep_research/report_generator.py +22 -8
  135. local_deep_research/search_system.py +390 -9
  136. local_deep_research/test_migration.py +15 -5
  137. local_deep_research/utilities/db_utils.py +7 -4
  138. local_deep_research/utilities/es_utils.py +115 -104
  139. local_deep_research/utilities/llm_utils.py +15 -5
  140. local_deep_research/utilities/log_utils.py +151 -0
  141. local_deep_research/utilities/search_cache.py +387 -0
  142. local_deep_research/utilities/search_utilities.py +14 -6
  143. local_deep_research/utilities/threading_utils.py +92 -0
  144. local_deep_research/utilities/url_utils.py +6 -0
  145. local_deep_research/web/api.py +347 -0
  146. local_deep_research/web/app.py +13 -17
  147. local_deep_research/web/app_factory.py +71 -66
  148. local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
  149. local_deep_research/web/database/migrations.py +5 -3
  150. local_deep_research/web/database/models.py +51 -2
  151. local_deep_research/web/database/schema_upgrade.py +49 -29
  152. local_deep_research/web/models/database.py +51 -61
  153. local_deep_research/web/routes/api_routes.py +56 -22
  154. local_deep_research/web/routes/benchmark_routes.py +4 -1
  155. local_deep_research/web/routes/globals.py +22 -0
  156. local_deep_research/web/routes/history_routes.py +71 -46
  157. local_deep_research/web/routes/metrics_routes.py +1155 -0
  158. local_deep_research/web/routes/research_routes.py +227 -41
  159. local_deep_research/web/routes/settings_routes.py +156 -55
  160. local_deep_research/web/services/research_service.py +310 -103
  161. local_deep_research/web/services/resource_service.py +36 -11
  162. local_deep_research/web/services/settings_manager.py +58 -18
  163. local_deep_research/web/services/settings_service.py +12 -4
  164. local_deep_research/web/services/socket_service.py +295 -188
  165. local_deep_research/web/static/css/custom_dropdown.css +180 -0
  166. local_deep_research/web/static/css/styles.css +39 -1
  167. local_deep_research/web/static/js/components/detail.js +633 -267
  168. local_deep_research/web/static/js/components/details.js +751 -0
  169. local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
  170. local_deep_research/web/static/js/components/fallback/ui.js +23 -23
  171. local_deep_research/web/static/js/components/history.js +76 -76
  172. local_deep_research/web/static/js/components/logpanel.js +61 -13
  173. local_deep_research/web/static/js/components/progress.js +13 -2
  174. local_deep_research/web/static/js/components/research.js +99 -12
  175. local_deep_research/web/static/js/components/results.js +239 -106
  176. local_deep_research/web/static/js/components/settings.js +70 -47
  177. local_deep_research/web/static/js/main.js +40 -40
  178. local_deep_research/web/static/js/services/audio.js +1 -1
  179. local_deep_research/web/static/js/services/formatting.js +11 -11
  180. local_deep_research/web/static/js/services/keyboard.js +157 -0
  181. local_deep_research/web/static/js/services/pdf.js +80 -80
  182. local_deep_research/web/static/sounds/README.md +1 -1
  183. local_deep_research/web/templates/base.html +1 -0
  184. local_deep_research/web/templates/components/log_panel.html +7 -1
  185. local_deep_research/web/templates/components/mobile_nav.html +1 -1
  186. local_deep_research/web/templates/components/sidebar.html +3 -0
  187. local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
  188. local_deep_research/web/templates/pages/details.html +325 -24
  189. local_deep_research/web/templates/pages/history.html +1 -1
  190. local_deep_research/web/templates/pages/metrics.html +1929 -0
  191. local_deep_research/web/templates/pages/progress.html +2 -2
  192. local_deep_research/web/templates/pages/research.html +53 -17
  193. local_deep_research/web/templates/pages/results.html +12 -1
  194. local_deep_research/web/templates/pages/star_reviews.html +803 -0
  195. local_deep_research/web/utils/formatters.py +9 -3
  196. local_deep_research/web_search_engines/default_search_engines.py +5 -3
  197. local_deep_research/web_search_engines/engines/full_search.py +8 -2
  198. local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
  199. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
  200. local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
  201. local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
  202. local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
  203. local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
  204. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
  205. local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
  206. local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
  207. local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
  208. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
  209. local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
  210. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
  211. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
  212. local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
  213. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
  214. local_deep_research/web_search_engines/search_engine_base.py +83 -35
  215. local_deep_research/web_search_engines/search_engine_factory.py +25 -8
  216. local_deep_research/web_search_engines/search_engines_config.py +9 -3
  217. {local_deep_research-0.4.3.dist-info → local_deep_research-0.5.0.dist-info}/METADATA +8 -2
  218. local_deep_research-0.5.0.dist-info/RECORD +265 -0
  219. local_deep_research-0.4.3.dist-info/RECORD +0 -177
  220. {local_deep_research-0.4.3.dist-info → local_deep_research-0.5.0.dist-info}/WHEEL +0 -0
  221. {local_deep_research-0.4.3.dist-info → local_deep_research-0.5.0.dist-info}/entry_points.txt +0 -0
  222. {local_deep_research-0.4.3.dist-info → local_deep_research-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -395,16 +395,28 @@
395
395
  shouldSaveImmediately = true;
396
396
  }
397
397
  }
398
- // Handle range/slider (save on change/input or blur)
399
- else if (input.type === 'range') {
398
+ // Handle range/slider (save on change/input or blur)
399
+ else if (input.type === 'range') {
400
400
  value = input.value;
401
401
  if (eventType === 'change' || eventType === 'input' || eventType === 'blur') {
402
402
  shouldSaveImmediately = true;
403
403
  }
404
- }
404
+ }
405
405
  // Handle other inputs (text, number, textarea) - Save on Enter or Blur
406
406
  else {
407
407
  value = input.value;
408
+
409
+ // Handle JSON.
410
+ if (input.classList.contains('json-content')) {
411
+ try {
412
+ // Validate
413
+ value = JSON.parse(input.value);
414
+ } catch (e) {
415
+ markInvalidInput(input, 'Invalid JSON');
416
+ return;
417
+ }
418
+ }
419
+
408
420
  // Basic validation for number
409
421
  if (input.type === 'number') {
410
422
  try {
@@ -1429,31 +1441,32 @@
1429
1441
  // Generate the appropriate input element based on UI element type
1430
1442
  switch(setting.ui_element) {
1431
1443
  case 'textarea':
1432
- // Check if it's JSON
1433
- let isJson = false;
1434
- let jsonClass = '';
1444
+ inputElement = `
1445
+ <textarea id="${settingId}" name="${setting.key}"
1446
+ class="settings-textarea"
1447
+ ${!setting.editable ? 'disabled' : ''}
1448
+ >${setting.value !== null ? setting.value : ''}</textarea>
1449
+ `;
1450
+ break;
1451
+
1452
+ case 'json':
1453
+ let jsonClass = ' json-content';
1435
1454
 
1436
- if (typeof setting.value === 'string' &&
1437
- (setting.value.startsWith('{') || setting.value.startsWith('['))) {
1438
- isJson = true;
1439
- jsonClass = ' json-content';
1455
+ // Try to format the JSON for better display
1456
+ try {
1457
+ setting.value = JSON.stringify(JSON.parse(setting.value), null, 2);
1458
+ } catch (e) {
1459
+ // If parsing fails, keep the original value
1460
+ console.log('Error formatting JSON:', e);
1461
+ }
1440
1462
 
1441
- // Try to format the JSON for better display
1463
+ // If it's an object (not an array), render individual controls
1464
+ if (setting.value.startsWith('{')) {
1442
1465
  try {
1443
- setting.value = JSON.stringify(JSON.parse(setting.value), null, 2);
1466
+ const jsonObj = JSON.parse(setting.value);
1467
+ return renderExpandedJsonControls(setting, settingId, jsonObj);
1444
1468
  } catch (e) {
1445
- // If parsing fails, keep the original value
1446
- console.log('Error formatting JSON:', e);
1447
- }
1448
-
1449
- // If it's an object (not an array), render individual controls
1450
- if (setting.value.startsWith('{')) {
1451
- try {
1452
- const jsonObj = JSON.parse(setting.value);
1453
- return renderExpandedJsonControls(setting, settingId, jsonObj);
1454
- } catch (e) {
1455
- console.log('Error parsing JSON for controls:', e);
1456
- }
1469
+ console.log('Error parsing JSON for controls:', e);
1457
1470
  }
1458
1471
  }
1459
1472
 
@@ -1581,26 +1594,15 @@
1581
1594
 
1582
1595
  default:
1583
1596
  // Handle llm.model here explicitly if not handled by ui_element
1584
- if (typeof setting.value === 'string' &&
1585
- (setting.value.startsWith('{') || setting.value.startsWith('['))) {
1586
- // Handle JSON objects/arrays rendered as textareas if not expanded
1587
- inputElement = `
1588
- <textarea id="${settingId}" name="${setting.key}"
1589
- class="settings-textarea json-content"
1590
- ${!setting.editable ? 'disabled' : ''}
1591
- >${setting.value}</textarea>
1592
- `;
1593
- } else {
1594
- // Default to text input
1595
- inputElement = `
1596
- <input type="${setting.ui_element === 'password' ? 'password' : 'text'}"
1597
- id="${settingId}" name="${setting.key}"
1598
- class="settings-input form-control"
1599
- value="${setting.value !== null ? setting.value : ''}"
1600
- ${!setting.editable ? 'disabled' : ''}
1601
- >
1602
- `;
1603
- }
1597
+ // Default to text input
1598
+ inputElement = `
1599
+ <input type="${setting.ui_element === 'password' ? 'password' : 'text'}"
1600
+ id="${settingId}" name="${setting.key}"
1601
+ class="settings-input form-control"
1602
+ value="${setting.value !== null ? setting.value : ''}"
1603
+ ${!setting.editable ? 'disabled' : ''}
1604
+ >
1605
+ `;
1604
1606
  break;
1605
1607
  }
1606
1608
 
@@ -1945,6 +1947,22 @@
1945
1947
  }, 1500);
1946
1948
  }
1947
1949
 
1950
+ /**
1951
+ * Validates user-specified JSON data and shows and error if it is not
1952
+ * valid JSON.
1953
+ * @param content The content to validate.
1954
+ * @return True if the content is valid.
1955
+ */
1956
+ function validateJsonContent(content) {
1957
+ try {
1958
+ JSON.parse(content);
1959
+ return true;
1960
+ } catch (e) {
1961
+ showMessage('Setting value must be valid JSON.', 'error', 5000);
1962
+ return false;
1963
+ }
1964
+ }
1965
+
1948
1966
  /**
1949
1967
  * Submit settings data to the API
1950
1968
  * @param {Object} formData - The settings to save
@@ -1962,6 +1980,11 @@
1962
1980
  } else if (sourceElement.classList.contains('json-property-control')) {
1963
1981
  // For JSON property controls, use the property item
1964
1982
  loadingContainer = sourceElement.closest('.json-property-item') || sourceElement;
1983
+ } else if (sourceElement.classList.contains('json-content')) {
1984
+ // For JSON content, validate it before saving.
1985
+ if (!validateJsonContent(sourceElement.value)) {
1986
+ return;
1987
+ }
1965
1988
  } else {
1966
1989
  // For other inputs, use the form-group or settings-item
1967
1990
  loadingContainer = sourceElement.closest('.form-group') ||
@@ -3688,16 +3711,16 @@
3688
3711
 
3689
3712
  return false;
3690
3713
  }
3691
-
3714
+
3692
3715
  if (providerUpper === 'OPENAI_ENDPOINT') {
3693
3716
  if (model.provider && model.provider.toUpperCase() === 'OPENAI_ENDPOINT') {
3694
3717
  return true;
3695
3718
  }
3696
-
3719
+
3697
3720
  if (model.label && model.label.toLowerCase().includes('custom')) {
3698
3721
  return true;
3699
3722
  }
3700
-
3723
+
3701
3724
  return false;
3702
3725
  }
3703
3726
 
@@ -12,7 +12,7 @@
12
12
  'page-history': ['history.js'],
13
13
  'page-settings': ['settings.js']
14
14
  };
15
-
15
+
16
16
  // Core services to always load
17
17
  const coreServices = [
18
18
  'formatting.js',
@@ -21,53 +21,53 @@
21
21
  'socket.js'
22
22
  // 'audio.js' - Removed from here, loaded separately
23
23
  ];
24
-
24
+
25
25
  // Optional services to load when needed
26
26
  const optionalServices = {
27
27
  'page-results': ['pdf.js'],
28
28
  'page-detail': ['pdf.js']
29
29
  };
30
-
30
+
31
31
  /**
32
32
  * Initialize the application
33
33
  */
34
34
  function initializeApp() {
35
35
  // Detect current page
36
36
  const currentPage = detectCurrentPage();
37
-
37
+
38
38
  if (!currentPage) {
39
39
  console.error('Cannot detect current page type');
40
40
  return;
41
41
  }
42
-
42
+
43
43
  console.log('Current page detected:', currentPage);
44
-
44
+
45
45
  // IMPORTANT: Load audio.js first, before ANY other scripts
46
46
  loadAudioServiceFirst(() => {
47
47
  // Continue loading other scripts after audio service is loaded
48
48
  console.log('Audio service script loaded, continuing with other scripts');
49
-
49
+
50
50
  // Load UI and formatting utils
51
51
  loadScripts('utils', coreServices.filter(s => s.includes('formatting') || s.includes('ui')));
52
-
52
+
53
53
  // Then load the rest
54
54
  loadScripts('services', coreServices.filter(s => s.includes('api') || s.includes('socket')));
55
-
55
+
56
56
  // Load optional services for this page
57
57
  if (optionalServices[currentPage]) {
58
58
  loadScripts('services', optionalServices[currentPage]);
59
59
  }
60
-
60
+
61
61
  // Load components for this page AFTER all services
62
62
  if (pageComponents[currentPage]) {
63
63
  loadScripts('components', pageComponents[currentPage]);
64
64
  }
65
-
65
+
66
66
  // Initialize tooltips and other global UI elements
67
67
  initializeGlobalUI();
68
68
  });
69
69
  }
70
-
70
+
71
71
  /**
72
72
  * Load audio service first and separately to ensure it's fully loaded before other scripts
73
73
  * @param {Function} callback - Function to call after audio service is loaded
@@ -77,11 +77,11 @@
77
77
  const audioScript = document.createElement('script');
78
78
  audioScript.src = `/research/static/js/services/audio.js?t=${new Date().getTime()}`; // Add timestamp to avoid cache
79
79
  audioScript.async = false;
80
-
80
+
81
81
  // Set up callback for when script loads
82
82
  audioScript.onload = function() {
83
83
  console.log('Audio service script loaded successfully');
84
-
84
+
85
85
  // Check if audio service is available in window object
86
86
  setTimeout(() => {
87
87
  if (window.audio) {
@@ -89,39 +89,39 @@
89
89
  } else {
90
90
  console.warn('Audio service not available in window object after script load');
91
91
  }
92
-
92
+
93
93
  // Continue regardless
94
94
  callback();
95
95
  }, 100); // Small delay to ensure script executes
96
96
  };
97
-
97
+
98
98
  // Error handling
99
99
  audioScript.onerror = function() {
100
100
  console.error('Failed to load audio service script');
101
101
  // Continue with other scripts even if audio fails
102
102
  callback();
103
103
  };
104
-
104
+
105
105
  // Add to document
106
106
  document.body.appendChild(audioScript);
107
107
  }
108
-
108
+
109
109
  /**
110
110
  * Detect the current page based on body class
111
111
  * @returns {string|null} The page identifier or null if not found
112
112
  */
113
113
  function detectCurrentPage() {
114
114
  const bodyClasses = document.body.classList;
115
-
115
+
116
116
  for (const pageId in pageComponents) {
117
117
  if (bodyClasses.contains(pageId)) {
118
118
  return pageId;
119
119
  }
120
120
  }
121
-
121
+
122
122
  // Check URL patterns as fallback
123
123
  const path = window.location.pathname;
124
-
124
+
125
125
  if (path === '/' || path === '/index' || path === '/home' || path === '/research/') {
126
126
  return 'page-home';
127
127
  } else if (path.includes('/research/progress')) {
@@ -135,10 +135,10 @@
135
135
  } else if (path.includes('/research/settings')) {
136
136
  return 'page-settings';
137
137
  }
138
-
138
+
139
139
  return null;
140
140
  }
141
-
141
+
142
142
  /**
143
143
  * Load scripts dynamically
144
144
  * @param {string} folder - The folder containing the scripts
@@ -146,7 +146,7 @@
146
146
  */
147
147
  function loadScripts(folder, scripts) {
148
148
  if (!scripts || !scripts.length) return;
149
-
149
+
150
150
  scripts.forEach(script => {
151
151
  const scriptElement = document.createElement('script');
152
152
  scriptElement.src = `/research/static/js/${folder}/${script}`;
@@ -154,7 +154,7 @@
154
154
  document.body.appendChild(scriptElement);
155
155
  });
156
156
  }
157
-
157
+
158
158
  /**
159
159
  * Initialize global UI elements
160
160
  */
@@ -164,7 +164,7 @@
164
164
  // Only ask for permission when user interacts with the page
165
165
  document.addEventListener('click', requestNotificationPermission, { once: true });
166
166
  }
167
-
167
+
168
168
  // Initialize theme toggle
169
169
  const themeToggle = document.getElementById('theme-toggle');
170
170
  if (themeToggle) {
@@ -172,42 +172,42 @@
172
172
  const savedTheme = localStorage.getItem('theme');
173
173
  const systemDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
174
174
  const isDarkMode = savedTheme === 'dark' || (savedTheme === null && systemDarkMode);
175
-
175
+
176
176
  // Set initial theme
177
177
  document.documentElement.setAttribute('data-theme', isDarkMode ? 'dark' : 'light');
178
- themeToggle.innerHTML = isDarkMode ?
179
- '<i class="fas fa-sun"></i>' :
178
+ themeToggle.innerHTML = isDarkMode ?
179
+ '<i class="fas fa-sun"></i>' :
180
180
  '<i class="fas fa-moon"></i>';
181
-
181
+
182
182
  // Listen for theme toggle click
183
183
  themeToggle.addEventListener('click', function() {
184
184
  const currentTheme = document.documentElement.getAttribute('data-theme');
185
185
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
186
-
186
+
187
187
  document.documentElement.setAttribute('data-theme', newTheme);
188
188
  localStorage.setItem('theme', newTheme);
189
-
190
- themeToggle.innerHTML = newTheme === 'dark' ?
191
- '<i class="fas fa-sun"></i>' :
189
+
190
+ themeToggle.innerHTML = newTheme === 'dark' ?
191
+ '<i class="fas fa-sun"></i>' :
192
192
  '<i class="fas fa-moon"></i>';
193
193
  });
194
194
  }
195
-
195
+
196
196
  // Initialize mobile menu toggle
197
197
  const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
198
198
  const navMenu = document.getElementById('main-nav');
199
-
199
+
200
200
  if (mobileMenuToggle && navMenu) {
201
201
  mobileMenuToggle.addEventListener('click', function() {
202
202
  navMenu.classList.toggle('open');
203
203
  mobileMenuToggle.setAttribute(
204
- 'aria-expanded',
204
+ 'aria-expanded',
205
205
  navMenu.classList.contains('open') ? 'true' : 'false'
206
206
  );
207
207
  });
208
208
  }
209
209
  }
210
-
210
+
211
211
  /**
212
212
  * Request notification permission
213
213
  */
@@ -216,11 +216,11 @@
216
216
  Notification.requestPermission();
217
217
  }
218
218
  }
219
-
219
+
220
220
  // Initialize when DOM is ready
221
221
  if (document.readyState === 'loading') {
222
222
  document.addEventListener('DOMContentLoaded', initializeApp);
223
223
  } else {
224
224
  initializeApp();
225
225
  }
226
- })();
226
+ })();
@@ -28,4 +28,4 @@ window.audio = {
28
28
  };
29
29
 
30
30
  // Log that audio is disabled
31
- console.log('Audio service is currently disabled - notifications will be implemented later');
31
+ console.log('Audio service is currently disabled - notifications will be implemented later');
@@ -38,31 +38,31 @@ function formatMode(mode) {
38
38
  */
39
39
  function formatDate(date, duration = null) {
40
40
  if (!date) return 'Unknown';
41
-
41
+
42
42
  try {
43
43
  const dateObj = new Date(date);
44
- const options = {
45
- year: 'numeric',
46
- month: 'short',
44
+ const options = {
45
+ year: 'numeric',
46
+ month: 'short',
47
47
  day: 'numeric',
48
48
  hour: '2-digit',
49
49
  minute: '2-digit'
50
50
  };
51
-
51
+
52
52
  let formattedDate = dateObj.toLocaleDateString('en-US', options);
53
-
53
+
54
54
  if (duration) {
55
55
  // Format the duration
56
56
  const minutes = Math.floor(duration / 60);
57
57
  const seconds = duration % 60;
58
-
58
+
59
59
  if (minutes > 0) {
60
60
  formattedDate += ` (${minutes}m ${seconds}s)`;
61
61
  } else {
62
62
  formattedDate += ` (${seconds}s)`;
63
63
  }
64
64
  }
65
-
65
+
66
66
  return formattedDate;
67
67
  } catch (e) {
68
68
  console.error('Error formatting date:', e);
@@ -77,10 +77,10 @@ function formatDate(date, duration = null) {
77
77
  */
78
78
  function formatDuration(seconds) {
79
79
  if (!seconds || isNaN(seconds)) return 'Unknown';
80
-
80
+
81
81
  const minutes = Math.floor(seconds / 60);
82
82
  const remainingSeconds = Math.floor(seconds % 60);
83
-
83
+
84
84
  if (minutes === 0) {
85
85
  return `${remainingSeconds}s`;
86
86
  } else {
@@ -116,4 +116,4 @@ window.formatting = {
116
116
  formatDuration,
117
117
  formatNumber,
118
118
  capitalizeFirstLetter
119
- };
119
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Global Keyboard Shortcuts Service
3
+ * Provides consistent keyboard shortcuts across the application
4
+ */
5
+ (function() {
6
+ 'use strict';
7
+
8
+ // Keyboard shortcut registry - simplified to just the essential ones
9
+ const shortcuts = {
10
+ 'newSearch': {
11
+ keys: ['ctrl+enter', 'cmd+enter'],
12
+ description: 'Return to main search',
13
+ handler: () => {
14
+ // Always navigate to main research page
15
+ window.location.href = '/';
16
+ }
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Check if a keyboard event matches a shortcut pattern
22
+ */
23
+ function matchesShortcut(event, pattern) {
24
+ const parts = pattern.toLowerCase().split('+');
25
+ const key = parts[parts.length - 1];
26
+
27
+ // Check modifiers
28
+ const requiresCtrl = parts.includes('ctrl');
29
+ const requiresCmd = parts.includes('cmd');
30
+ const requiresShift = parts.includes('shift');
31
+ const requiresAlt = parts.includes('alt');
32
+
33
+ // For single key shortcuts, ensure NO modifiers are pressed
34
+ if (parts.length === 1) {
35
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ // Match key
41
+ const eventKey = event.key.toLowerCase();
42
+ if (eventKey !== key && event.code.toLowerCase() !== `key${key}`) {
43
+ // Special handling for special keys
44
+ if (key === '/' && eventKey !== '/') return false;
45
+ else if (key === ',' && eventKey !== ',') return false;
46
+ else if (key !== '/' && key !== ',' && eventKey !== key) return false;
47
+ }
48
+
49
+ // Match modifiers (only if required)
50
+ if (requiresCtrl && !event.ctrlKey) return false;
51
+ if (requiresCmd && !event.metaKey) return false;
52
+ if (requiresShift && !event.shiftKey) return false;
53
+ if (requiresAlt && !event.altKey) return false;
54
+
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Initialize keyboard shortcuts
60
+ */
61
+ function initializeKeyboardShortcuts() {
62
+ console.log('Keyboard shortcuts initialized');
63
+
64
+ document.addEventListener('keydown', function(event) {
65
+ // Skip if user is typing in an input field
66
+ const activeElement = document.activeElement;
67
+ const isTyping = activeElement && (
68
+ activeElement.tagName === 'INPUT' ||
69
+ activeElement.tagName === 'TEXTAREA' ||
70
+ activeElement.contentEditable === 'true'
71
+ );
72
+
73
+ // Allow Escape even when typing
74
+ if (isTyping && event.key !== 'Escape') {
75
+ return;
76
+ }
77
+
78
+ // Debug log
79
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
80
+ console.log('Key pressed:', event.key, 'Code:', event.code);
81
+ }
82
+
83
+ // Check each shortcut
84
+ for (const [name, shortcut] of Object.entries(shortcuts)) {
85
+ for (const pattern of shortcut.keys) {
86
+ if (matchesShortcut(event, pattern)) {
87
+ console.log('Shortcut matched:', name, pattern);
88
+ event.preventDefault();
89
+ shortcut.handler(event);
90
+ return;
91
+ }
92
+ }
93
+ }
94
+ });
95
+
96
+ // Add help text to footer if on main pages
97
+ addKeyboardHints();
98
+ }
99
+
100
+ /**
101
+ * Add subtle keyboard hints to the UI
102
+ */
103
+ function addKeyboardHints() {
104
+ // Keyboard hints disabled
105
+ }
106
+
107
+ /**
108
+ * Get list of available shortcuts for current page
109
+ */
110
+ function getAvailableShortcuts() {
111
+ const currentPath = window.location.pathname;
112
+ const allShortcuts = { ...shortcuts };
113
+
114
+ // Add page-specific shortcuts
115
+ if (currentPath.includes('/research/progress/')) {
116
+ allShortcuts.viewResults = {
117
+ keys: ['enter'],
118
+ description: 'View results (when complete)',
119
+ handler: () => {
120
+ const viewBtn = document.getElementById('view-results-btn');
121
+ if (viewBtn && viewBtn.style.display !== 'none') {
122
+ window.location.href = viewBtn.href;
123
+ }
124
+ }
125
+ };
126
+ }
127
+
128
+ if (currentPath.includes('/research/results/')) {
129
+ allShortcuts.escape = {
130
+ keys: ['escape'],
131
+ description: 'Back to new search',
132
+ handler: () => window.location.href = '/'
133
+ };
134
+ }
135
+
136
+ return allShortcuts;
137
+ }
138
+
139
+ // Initialize keyboard shortcuts on all pages
140
+ if (document.readyState === 'loading') {
141
+ document.addEventListener('DOMContentLoaded', initializeKeyboardShortcuts);
142
+ } else {
143
+ initializeKeyboardShortcuts();
144
+ }
145
+
146
+ // Expose API for other components
147
+ window.KeyboardService = {
148
+ shortcuts: getAvailableShortcuts,
149
+ addShortcut: (name, config) => {
150
+ shortcuts[name] = config;
151
+ },
152
+ removeShortcut: (name) => {
153
+ delete shortcuts[name];
154
+ }
155
+ };
156
+
157
+ })();