local-deep-research 0.4.4__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 (220) 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/metrics/__init__.py +13 -0
  118. local_deep_research/metrics/database.py +58 -0
  119. local_deep_research/metrics/db_models.py +115 -0
  120. local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
  121. local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
  122. local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
  123. local_deep_research/metrics/migrate_research_ratings.py +31 -0
  124. local_deep_research/metrics/models.py +61 -0
  125. local_deep_research/metrics/pricing/__init__.py +12 -0
  126. local_deep_research/metrics/pricing/cost_calculator.py +237 -0
  127. local_deep_research/metrics/pricing/pricing_cache.py +143 -0
  128. local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
  129. local_deep_research/metrics/query_utils.py +51 -0
  130. local_deep_research/metrics/search_tracker.py +380 -0
  131. local_deep_research/metrics/token_counter.py +1078 -0
  132. local_deep_research/migrate_db.py +3 -1
  133. local_deep_research/report_generator.py +22 -8
  134. local_deep_research/search_system.py +390 -9
  135. local_deep_research/test_migration.py +15 -5
  136. local_deep_research/utilities/db_utils.py +7 -4
  137. local_deep_research/utilities/es_utils.py +115 -104
  138. local_deep_research/utilities/llm_utils.py +15 -5
  139. local_deep_research/utilities/log_utils.py +151 -0
  140. local_deep_research/utilities/search_cache.py +387 -0
  141. local_deep_research/utilities/search_utilities.py +14 -6
  142. local_deep_research/utilities/threading_utils.py +92 -0
  143. local_deep_research/utilities/url_utils.py +6 -0
  144. local_deep_research/web/api.py +347 -0
  145. local_deep_research/web/app.py +13 -17
  146. local_deep_research/web/app_factory.py +71 -66
  147. local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
  148. local_deep_research/web/database/migrations.py +5 -3
  149. local_deep_research/web/database/models.py +51 -2
  150. local_deep_research/web/database/schema_upgrade.py +49 -29
  151. local_deep_research/web/models/database.py +51 -61
  152. local_deep_research/web/routes/api_routes.py +56 -22
  153. local_deep_research/web/routes/benchmark_routes.py +4 -1
  154. local_deep_research/web/routes/globals.py +22 -0
  155. local_deep_research/web/routes/history_routes.py +71 -46
  156. local_deep_research/web/routes/metrics_routes.py +1155 -0
  157. local_deep_research/web/routes/research_routes.py +227 -41
  158. local_deep_research/web/routes/settings_routes.py +156 -55
  159. local_deep_research/web/services/research_service.py +310 -103
  160. local_deep_research/web/services/resource_service.py +36 -11
  161. local_deep_research/web/services/settings_manager.py +55 -17
  162. local_deep_research/web/services/settings_service.py +12 -4
  163. local_deep_research/web/services/socket_service.py +295 -188
  164. local_deep_research/web/static/css/custom_dropdown.css +180 -0
  165. local_deep_research/web/static/css/styles.css +39 -1
  166. local_deep_research/web/static/js/components/detail.js +633 -267
  167. local_deep_research/web/static/js/components/details.js +751 -0
  168. local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
  169. local_deep_research/web/static/js/components/fallback/ui.js +23 -23
  170. local_deep_research/web/static/js/components/history.js +76 -76
  171. local_deep_research/web/static/js/components/logpanel.js +61 -13
  172. local_deep_research/web/static/js/components/progress.js +13 -2
  173. local_deep_research/web/static/js/components/research.js +99 -12
  174. local_deep_research/web/static/js/components/results.js +239 -106
  175. local_deep_research/web/static/js/main.js +40 -40
  176. local_deep_research/web/static/js/services/audio.js +1 -1
  177. local_deep_research/web/static/js/services/formatting.js +11 -11
  178. local_deep_research/web/static/js/services/keyboard.js +157 -0
  179. local_deep_research/web/static/js/services/pdf.js +80 -80
  180. local_deep_research/web/static/sounds/README.md +1 -1
  181. local_deep_research/web/templates/base.html +1 -0
  182. local_deep_research/web/templates/components/log_panel.html +7 -1
  183. local_deep_research/web/templates/components/mobile_nav.html +1 -1
  184. local_deep_research/web/templates/components/sidebar.html +3 -0
  185. local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
  186. local_deep_research/web/templates/pages/details.html +325 -24
  187. local_deep_research/web/templates/pages/history.html +1 -1
  188. local_deep_research/web/templates/pages/metrics.html +1929 -0
  189. local_deep_research/web/templates/pages/progress.html +2 -2
  190. local_deep_research/web/templates/pages/research.html +53 -17
  191. local_deep_research/web/templates/pages/results.html +12 -1
  192. local_deep_research/web/templates/pages/star_reviews.html +803 -0
  193. local_deep_research/web/utils/formatters.py +9 -3
  194. local_deep_research/web_search_engines/default_search_engines.py +5 -3
  195. local_deep_research/web_search_engines/engines/full_search.py +8 -2
  196. local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
  197. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
  198. local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
  199. local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
  200. local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
  201. local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
  202. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
  203. local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
  204. local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
  205. local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
  206. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
  207. local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
  208. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
  209. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
  210. local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
  211. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
  212. local_deep_research/web_search_engines/search_engine_base.py +83 -35
  213. local_deep_research/web_search_engines/search_engine_factory.py +25 -8
  214. local_deep_research/web_search_engines/search_engines_config.py +9 -3
  215. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/METADATA +7 -1
  216. local_deep_research-0.5.0.dist-info/RECORD +265 -0
  217. local_deep_research-0.4.4.dist-info/RECORD +0 -177
  218. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/WHEEL +0 -0
  219. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/entry_points.txt +0 -0
  220. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,348 +1,714 @@
1
1
  /**
2
- * Detail Component
3
- * Manages the display of detailed information for a specific research topic
2
+ * Research Details Component
3
+ * Manages the display of research details and token metrics for a specific research
4
4
  */
5
5
  (function() {
6
6
  // DOM Elements
7
- let detailContainer = null;
8
- let sourcesList = null;
9
- let tabsContainer = null;
10
-
7
+ let queryElement = null;
8
+ let statusElement = null;
9
+ let modeElement = null;
10
+ let progressBar = null;
11
+ let progressPercentage = null;
12
+ let pollInterval = null;
13
+
14
+ // Token metrics elements
15
+ let timelineChart = null;
16
+
11
17
  // Component state
12
18
  let currentResearchId = null;
13
- let currentTopicId = null;
14
- let detailData = null;
15
-
19
+ let isCompleted = false;
20
+
16
21
  /**
17
- * Initialize the detail component
22
+ * Initialize the research details component
18
23
  */
19
- function initializeDetail() {
20
- // Get IDs from URL
21
- const ids = getIdsFromUrl();
22
- currentResearchId = ids.researchId;
23
- currentTopicId = ids.topicId;
24
-
25
- if (!currentResearchId || !currentTopicId) {
26
- console.error('No research or topic ID found');
24
+ async function initializeResearchDetails() {
25
+ console.log('Initializing research details component...');
26
+
27
+ // Get research ID from URL
28
+ currentResearchId = getResearchIdFromUrl();
29
+
30
+ if (!currentResearchId) {
31
+ console.error('No research ID found in URL');
27
32
  if (window.ui && window.ui.showError) {
28
- window.ui.showError('Invalid research or topic. Please return to the results page.');
33
+ window.ui.showError('No research ID found. Please return to the history page.');
29
34
  }
30
35
  return;
31
36
  }
32
-
37
+
38
+ console.log('Research ID:', currentResearchId);
39
+
33
40
  // Get DOM elements
34
- detailContainer = document.getElementById('research-log');
35
-
36
- if (!detailContainer) {
37
- console.error('Required DOM elements not found for detail component');
38
- return;
39
- }
40
-
41
+ queryElement = document.getElementById('detail-query');
42
+ statusElement = document.getElementById('detail-status');
43
+ modeElement = document.getElementById('detail-mode');
44
+ progressBar = document.getElementById('detail-progress-fill');
45
+ progressPercentage = document.getElementById('detail-progress-percentage');
46
+
47
+ console.log('DOM elements found, setting up event listeners...');
48
+
41
49
  // Set up event listeners
42
50
  setupEventListeners();
43
-
44
- // Load topic detail
45
- loadTopicDetail();
46
-
47
- console.log('Detail component initialized for research:', currentResearchId, 'topic:', currentTopicId);
51
+
52
+ // Load initial research details
53
+ await loadResearchDetails();
54
+
55
+ // Start polling for updates (only if needed)
56
+ startPolling();
57
+
58
+ console.log('Research details component initialized');
48
59
  }
49
-
60
+
50
61
  /**
51
- * Extract research and topic IDs from URL
52
- * @returns {Object} Object with researchId and topicId
62
+ * Extract research ID from URL
63
+ * @returns {string|null} Research ID or null if not found
53
64
  */
54
- function getIdsFromUrl() {
65
+ function getResearchIdFromUrl() {
55
66
  const pathParts = window.location.pathname.split('/');
56
- const detailIndex = pathParts.indexOf('detail');
57
-
58
- if (detailIndex > 0 && detailIndex + 2 < pathParts.length) {
59
- return {
60
- researchId: pathParts[detailIndex + 1],
61
- topicId: pathParts[detailIndex + 2]
62
- };
67
+ const detailsIndex = pathParts.indexOf('details');
68
+
69
+ if (detailsIndex > 0 && detailsIndex + 1 < pathParts.length) {
70
+ return pathParts[detailsIndex + 1];
63
71
  }
64
-
65
- return { researchId: null, topicId: null };
72
+
73
+ return null;
66
74
  }
67
-
75
+
68
76
  /**
69
77
  * Set up event listeners
70
78
  */
71
79
  function setupEventListeners() {
72
- // Back button
80
+ const viewResultsButton = document.getElementById('view-results-btn');
81
+ if (viewResultsButton) {
82
+ viewResultsButton.addEventListener('click', function() {
83
+ window.location.href = `/research/results/${currentResearchId}`;
84
+ });
85
+ }
86
+
73
87
  const backButton = document.getElementById('back-to-history-from-details');
74
88
  if (backButton) {
75
89
  backButton.addEventListener('click', function() {
76
90
  window.location.href = '/research/history';
77
91
  });
78
92
  }
79
-
80
- // Progress elements
81
- const progressBar = document.getElementById('detail-progress-fill');
82
- const progressPercentage = document.getElementById('detail-progress-percentage');
83
-
84
- // Tab click events
85
- if (tabsContainer) {
86
- tabsContainer.addEventListener('click', function(e) {
87
- if (e.target && e.target.matches('.tab-item')) {
88
- const tabId = e.target.dataset.tab;
89
- switchTab(tabId);
90
- }
91
- });
92
- }
93
-
94
- // Source highlight and citation copy
95
- if (sourcesList) {
96
- sourcesList.addEventListener('click', function(e) {
97
- // Handle citation copy
98
- if (e.target && e.target.matches('.copy-citation-btn')) {
99
- const sourceId = e.target.closest('.source-item').dataset.id;
100
- handleCopyCitation(sourceId);
101
- }
102
-
103
- // Handle source highlighting
104
- if (e.target && e.target.closest('.source-item')) {
105
- const sourceItem = e.target.closest('.source-item');
106
- if (!e.target.matches('.copy-citation-btn')) {
107
- toggleSourceHighlight(sourceItem);
108
- }
93
+ }
94
+
95
+ /**
96
+ * Load basic research details (for polling)
97
+ */
98
+ async function loadBasicResearchDetails() {
99
+ console.log('Loading basic research details...');
100
+
101
+ try {
102
+ const response = await fetch(`/research/api/research/${currentResearchId}`, {
103
+ headers: {
104
+ 'Accept': 'application/json'
109
105
  }
110
106
  });
107
+
108
+ if (!response.ok) {
109
+ throw new Error(`Failed to load research details: ${response.statusText}`);
110
+ }
111
+
112
+ const data = await response.json();
113
+
114
+ // Update UI with research details
115
+ updateResearchInfo(data);
116
+
117
+ // Check if research is completed
118
+ isCompleted = data.status === 'Completed' || data.status === 'Failed';
119
+
120
+ } catch (error) {
121
+ console.error('Error loading basic research details:', error);
111
122
  }
112
123
  }
113
-
124
+
114
125
  /**
115
- * Load topic detail from API
126
+ * Load research details from API (full load including token metrics)
116
127
  */
117
- async function loadTopicDetail() {
118
- // Show loading state
119
- window.ui.showSpinner(detailContainer, 'Loading topic details...');
120
-
128
+ async function loadResearchDetails() {
129
+ console.log('Loading research details...');
130
+
121
131
  try {
122
- // Get topic detail
123
- detailData = await window.api.getTopicDetail(currentResearchId, currentTopicId);
124
-
125
- if (!detailData) {
126
- throw new Error('No topic details found');
132
+ // Fetch research details
133
+ const response = await fetch(`/research/api/research/${currentResearchId}`, {
134
+ headers: {
135
+ 'Accept': 'application/json'
136
+ }
137
+ });
138
+
139
+ if (!response.ok) {
140
+ throw new Error(`Failed to load research details: ${response.statusText}`);
141
+ }
142
+
143
+ const data = await response.json();
144
+ console.log('Research details loaded:', data);
145
+
146
+ // Update UI with research details
147
+ updateResearchInfo(data);
148
+
149
+ // Check if research is completed
150
+ isCompleted = data.status === 'Completed' || data.status === 'Failed';
151
+ if (isCompleted && pollInterval) {
152
+ clearInterval(pollInterval);
153
+ pollInterval = null;
127
154
  }
128
-
129
- // Render detail
130
- renderDetail(detailData);
155
+
156
+ // Load token metrics
157
+ await loadTokenMetrics();
158
+
159
+ // Load search metrics
160
+ await loadSearchMetrics();
161
+
131
162
  } catch (error) {
132
- console.error('Error loading topic detail:', error);
133
- window.ui.hideSpinner(detailContainer);
134
- window.ui.showError('Error loading topic detail: ' + error.message);
163
+ console.error('Error loading research details:', error);
164
+ if (window.ui && window.ui.showError) {
165
+ window.ui.showError('Error loading research details: ' + error.message);
166
+ }
135
167
  }
136
168
  }
137
-
169
+
138
170
  /**
139
- * Render topic detail
140
- * @param {Object} data - The topic detail data
171
+ * Update research info display
172
+ * @param {Object} data - Research data
141
173
  */
142
- function renderDetail(data) {
143
- // Hide spinner
144
- window.ui.hideSpinner(detailContainer);
145
-
146
- // Set page title
147
- document.title = `${data.title || 'Topic Detail'} - Local Deep Research`;
148
-
149
- // Update page header
150
- const pageTitle = document.querySelector('h1.page-title');
151
- if (pageTitle && data.title) {
152
- pageTitle.textContent = data.title;
174
+ function updateResearchInfo(data) {
175
+ if (queryElement) {
176
+ queryElement.textContent = data.query || 'N/A';
153
177
  }
154
-
155
- // Render content
156
- if (data.content) {
157
- // Render markdown
158
- const renderedHtml = window.ui.renderMarkdown(data.content);
159
- detailContainer.innerHTML = renderedHtml;
160
-
161
- // Add syntax highlighting
162
- highlightCodeBlocks();
163
- } else {
164
- detailContainer.innerHTML = '<p class="text-error">No content available for this topic.</p>';
178
+
179
+ if (statusElement) {
180
+ statusElement.textContent = data.status || 'N/A';
181
+ statusElement.className = 'metadata-value status-' + (data.status || 'unknown').toLowerCase();
165
182
  }
166
-
167
- // Render sources
168
- if (sourcesList && data.sources && data.sources.length > 0) {
169
- renderSources(data.sources);
183
+
184
+ if (modeElement) {
185
+ modeElement.textContent = data.mode || 'standard';
170
186
  }
171
-
172
- // Set sources count
173
- const sourcesCountEl = document.getElementById('sources-count');
174
- if (sourcesCountEl && data.sources) {
175
- sourcesCountEl.textContent = `${data.sources.length} source${data.sources.length !== 1 ? 's' : ''}`;
187
+
188
+ // Update progress
189
+ const progress = data.progress_percentage || 0;
190
+ if (progressBar) {
191
+ progressBar.style.width = progress + '%';
176
192
  }
177
-
178
- // Show first tab
179
- if (tabsContainer) {
180
- const firstTab = tabsContainer.querySelector('.tab-item');
181
- if (firstTab) {
182
- switchTab(firstTab.dataset.tab);
193
+ if (progressPercentage) {
194
+ progressPercentage.textContent = progress + '%';
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Load search metrics for this research
200
+ */
201
+ async function loadSearchMetrics() {
202
+ console.log('Loading search metrics...');
203
+
204
+ try {
205
+ const response = await fetch(`/metrics/api/metrics/research/${currentResearchId}/search`, {
206
+ headers: {
207
+ 'Accept': 'application/json'
208
+ }
209
+ });
210
+
211
+ if (!response.ok) {
212
+ console.log('No search metrics available for this research');
213
+ return;
214
+ }
215
+
216
+ const data = await response.json();
217
+
218
+ if (data.status === 'success' && data.metrics) {
219
+ console.log('Search metrics loaded:', data.metrics);
220
+ displaySearchMetrics(data.metrics);
221
+ displaySearchEnginePerformance(data.metrics.engine_stats || []);
222
+ displaySearchTimeline(data.metrics.search_calls || []);
183
223
  }
224
+
225
+ } catch (error) {
226
+ console.error('Error loading search metrics:', error);
184
227
  }
185
228
  }
186
-
229
+
187
230
  /**
188
- * Render sources list
189
- * @param {Array} sources - Array of source objects
231
+ * Display search metrics summary
232
+ * @param {Object} searchData - Search metrics data
190
233
  */
191
- function renderSources(sources) {
192
- sourcesList.innerHTML = '';
193
-
194
- sources.forEach((source, index) => {
195
- const sourceEl = document.createElement('div');
196
- sourceEl.className = 'source-item';
197
- sourceEl.dataset.id = index;
198
-
199
- const title = source.title || source.url || `Source ${index + 1}`;
200
- const url = source.url || '';
201
- const author = source.author || '';
202
- const date = source.date || '';
203
-
204
- sourceEl.innerHTML = `
205
- <div class="source-header">
206
- <h4 class="source-title">${title}</h4>
207
- <button class="copy-citation-btn" title="Copy citation">
208
- <i class="fas fa-clipboard"></i>
209
- </button>
210
- </div>
211
- ${url ? `<div class="source-url"><a href="${url}" target="_blank">${url}</a></div>` : ''}
212
- <div class="source-metadata">
213
- ${author ? `<span><i class="fas fa-user"></i> ${author}</span>` : ''}
214
- ${date ? `<span><i class="far fa-calendar"></i> ${date}</span>` : ''}
234
+ function displaySearchMetrics(searchData) {
235
+ const searchSummary = document.getElementById('search-summary');
236
+ if (!searchSummary || !searchData) return;
237
+
238
+ // Show the search analytics section
239
+ const searchSection = document.getElementById('search-metrics-section');
240
+ if (searchSection) {
241
+ searchSection.style.display = 'block';
242
+ }
243
+
244
+ const totalSearches = searchData.total_searches || 0;
245
+ const successRate = searchData.success_rate || 0;
246
+ const avgResponseTime = (searchData.avg_response_time || 0) / 1000; // Convert ms to seconds
247
+ const totalResults = searchData.total_results || 0;
248
+
249
+ // Update individual metric elements
250
+ const totalSearchesEl = document.getElementById('total-searches');
251
+ const totalResultsEl = document.getElementById('total-search-results');
252
+ const avgResponseTimeEl = document.getElementById('avg-search-response-time');
253
+ const successRateEl = document.getElementById('search-success-rate');
254
+
255
+ if (totalSearchesEl) totalSearchesEl.textContent = totalSearches;
256
+ if (totalResultsEl) totalResultsEl.textContent = formatNumber(totalResults);
257
+ if (avgResponseTimeEl) avgResponseTimeEl.textContent = avgResponseTime.toFixed(2) + 's';
258
+ if (successRateEl) successRateEl.textContent = successRate.toFixed(1) + '%';
259
+ }
260
+
261
+ /**
262
+ * Display search engine performance breakdown
263
+ * @param {Array} enginePerformance - Search engine performance data
264
+ */
265
+ function displaySearchEnginePerformance(enginePerformance) {
266
+ const engineContainer = document.getElementById('search-engine-performance');
267
+ if (!engineContainer) return;
268
+
269
+ if (!enginePerformance || enginePerformance.length === 0) {
270
+ engineContainer.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No search engine data available</p>';
271
+ return;
272
+ }
273
+
274
+ const engineHtml = enginePerformance.map(engine => `
275
+ <div class="search-engine-item">
276
+ <div class="engine-name">${escapeHtml(engine.engine)}</div>
277
+ <div class="engine-stats">
278
+ <span class="stat">${engine.call_count} searches</span>
279
+ <span class="stat">${engine.success_rate.toFixed(1)}% success</span>
280
+ <span class="stat">${(engine.avg_response_time / 1000).toFixed(2)}s avg</span>
281
+ <span class="stat">${formatNumber(engine.total_results)} results</span>
215
282
  </div>
216
- ${source.relevance ? `<div class="source-relevance">Relevance: ${source.relevance}</div>` : ''}
217
- `;
218
-
219
- sourcesList.appendChild(sourceEl);
220
- });
283
+ </div>
284
+ `).join('');
285
+
286
+ engineContainer.innerHTML = engineHtml;
221
287
  }
222
-
288
+
223
289
  /**
224
- * Switch between tabs
225
- * @param {string} tabId - The tab ID to switch to
290
+ * Display search timeline
291
+ * @param {Array} searchTimeline - Search timeline data
226
292
  */
227
- function switchTab(tabId) {
228
- // Update active tab
229
- const tabs = document.querySelectorAll('.tab-item');
230
- tabs.forEach(tab => {
231
- tab.classList.toggle('active', tab.dataset.tab === tabId);
232
- });
233
-
234
- // Update visible content
235
- const tabContents = document.querySelectorAll('.tab-content');
236
- tabContents.forEach(content => {
237
- content.style.display = content.dataset.tab === tabId ? 'block' : 'none';
238
- });
293
+ function displaySearchTimeline(searchTimeline) {
294
+ const timelineContainer = document.getElementById('search-timeline');
295
+ if (!timelineContainer) return;
296
+
297
+ if (!searchTimeline || searchTimeline.length === 0) {
298
+ timelineContainer.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No search timeline data available</p>';
299
+ return;
300
+ }
301
+
302
+ const timelineHtml = searchTimeline.map(search => {
303
+ const timestamp = new Date(search.timestamp).toLocaleTimeString();
304
+ const statusClass = search.success_status === 'success' ? 'success' : 'error';
305
+ const statusText = search.success_status === 'success' ? 'Success' : 'Failed';
306
+
307
+ return `
308
+ <div class="timeline-item">
309
+ <div class="timeline-time">${timestamp}</div>
310
+ <div class="timeline-engine">${escapeHtml(search.engine)}</div>
311
+ <div class="timeline-query">${escapeHtml(search.query)}</div>
312
+ <div class="timeline-results">${formatNumber(search.results_count)} results</div>
313
+ <div class="timeline-response">${(search.response_time_ms / 1000).toFixed(2)}s</div>
314
+ <div class="timeline-status ${statusClass}">${statusText}</div>
315
+ </div>
316
+ `;
317
+ }).join('');
318
+
319
+ timelineContainer.innerHTML = timelineHtml;
239
320
  }
240
-
321
+
322
+
241
323
  /**
242
- * Toggle source highlight
243
- * @param {HTMLElement} sourceEl - The source element to highlight
324
+ * Start polling for updates (only if research is not completed)
244
325
  */
245
- function toggleSourceHighlight(sourceEl) {
246
- // Remove highlight from all sources
247
- const allSources = document.querySelectorAll('.source-item');
248
- allSources.forEach(s => s.classList.remove('highlighted'));
249
-
250
- // Add highlight to clicked source
251
- sourceEl.classList.add('highlighted');
252
-
253
- // Get source index
254
- const sourceIndex = parseInt(sourceEl.dataset.id);
255
-
256
- // Highlight content with this source
257
- highlightContentFromSource(sourceIndex);
326
+ function startPolling() {
327
+ // Only poll if research is not completed
328
+ if (!isCompleted) {
329
+ console.log('Starting polling for updates...');
330
+
331
+ // Poll every 10 seconds (much less frequent)
332
+ pollInterval = setInterval(async () => {
333
+ if (!isCompleted) {
334
+ // Only reload basic research info, not token metrics
335
+ await loadBasicResearchDetails();
336
+ } else {
337
+ // Stop polling once completed
338
+ if (pollInterval) {
339
+ clearInterval(pollInterval);
340
+ pollInterval = null;
341
+ console.log('Research completed, stopped polling');
342
+ }
343
+ }
344
+ }, 10000);
345
+ } else {
346
+ console.log('Research already completed, no polling needed');
347
+ }
258
348
  }
259
-
349
+
260
350
  /**
261
- * Highlight content sections from a specific source
262
- * @param {number} sourceIndex - The source index to highlight
351
+ * Load token metrics for this research
263
352
  */
264
- function highlightContentFromSource(sourceIndex) {
265
- // Remove all existing highlights
266
- const allCitations = document.querySelectorAll('.citation-highlight');
267
- allCitations.forEach(el => {
268
- el.classList.remove('citation-highlight');
269
- });
270
-
271
- // Add highlight to citations from this source
272
- const citations = document.querySelectorAll(`.citation[data-source="${sourceIndex}"]`);
273
- citations.forEach(citation => {
274
- citation.classList.add('citation-highlight');
275
-
276
- // Scroll to first citation
277
- if (citations.length > 0) {
278
- citations[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
353
+ async function loadTokenMetrics() {
354
+ console.log('Loading token metrics...');
355
+
356
+ try {
357
+ const response = await fetch(`/metrics/api/metrics/research/${currentResearchId}/timeline`, {
358
+ headers: {
359
+ 'Accept': 'application/json'
360
+ }
361
+ });
362
+
363
+ if (!response.ok) {
364
+ console.log('No token metrics available for this research');
365
+ return;
279
366
  }
280
- });
367
+
368
+ const data = await response.json();
369
+
370
+ if (data.status === 'success') {
371
+ console.log('Token metrics loaded:', data.metrics);
372
+ displayTokenMetrics(data.metrics);
373
+ }
374
+
375
+ } catch (error) {
376
+ console.error('Error loading token metrics:', error);
377
+ }
281
378
  }
282
-
379
+
283
380
  /**
284
- * Handle copying citation
285
- * @param {number} sourceIndex - The source index to copy
381
+ * Display token metrics on the page
382
+ * @param {Object} metrics - Token metrics data
286
383
  */
287
- async function handleCopyCitation(sourceIndex) {
288
- if (!detailData || !detailData.sources || !detailData.sources[sourceIndex]) {
289
- return;
384
+ function displayTokenMetrics(metrics) {
385
+ console.log('Displaying token metrics with data:', metrics);
386
+
387
+ // Show the metrics section
388
+ const metricsSection = document.getElementById('token-metrics-section');
389
+ if (metricsSection) {
390
+ metricsSection.style.display = 'block';
290
391
  }
291
-
292
- const source = detailData.sources[sourceIndex];
293
-
294
- // Format citation
295
- let citation = '';
296
-
297
- if (source.author) {
298
- citation += source.author;
299
- if (source.date) {
300
- citation += ` (${source.date})`;
392
+
393
+ // Update summary cards with safe data access
394
+ const summary = metrics.summary || {};
395
+ const totalTokensEl = document.getElementById('total-tokens');
396
+ const totalPromptTokensEl = document.getElementById('total-prompt-tokens');
397
+ const totalCompletionTokensEl = document.getElementById('total-completion-tokens');
398
+ const totalCallsEl = document.getElementById('total-calls');
399
+ const avgResponseTimeEl = document.getElementById('avg-response-time');
400
+ const successRateEl = document.getElementById('success-rate');
401
+
402
+ if (totalTokensEl) totalTokensEl.textContent = formatNumber(summary.total_tokens || 0);
403
+ if (totalPromptTokensEl) totalPromptTokensEl.textContent = formatNumber(summary.total_prompt_tokens || 0);
404
+ if (totalCompletionTokensEl) totalCompletionTokensEl.textContent = formatNumber(summary.total_completion_tokens || 0);
405
+ if (totalCallsEl) totalCallsEl.textContent = formatNumber(summary.total_calls || 0);
406
+ if (avgResponseTimeEl) avgResponseTimeEl.textContent = (summary.avg_response_time || 0) + 'ms';
407
+ if (successRateEl) successRateEl.textContent = (summary.success_rate || 0) + '%';
408
+
409
+ // Create timeline chart
410
+ createTimelineChart(metrics.timeline);
411
+
412
+ // Display phase breakdown
413
+ displayPhaseBreakdown(metrics.phase_stats);
414
+
415
+ // Display call stack traces
416
+ displayCallStackTraces(metrics.timeline);
417
+ }
418
+
419
+ /**
420
+ * Create timeline chart
421
+ * @param {Array} timeline - Timeline data
422
+ */
423
+ function createTimelineChart(timeline) {
424
+ const ctx = document.getElementById('timeline-chart');
425
+ if (!ctx) return;
426
+
427
+ // Handle empty timeline
428
+ if (!timeline || timeline.length === 0) {
429
+ console.log('No timeline data available');
430
+ ctx.getContext('2d').clearRect(0, 0, ctx.width, ctx.height);
431
+
432
+ // Show a message in the chart area
433
+ const chartContainer = ctx.parentElement;
434
+ if (chartContainer) {
435
+ chartContainer.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-secondary);">No token usage timeline available for this research.<br>This research may have used search-only mode or meta search engine.</div>';
301
436
  }
302
- citation += '. ';
303
- }
304
-
305
- if (source.title) {
306
- citation += `"${source.title}". `;
437
+ return;
307
438
  }
308
-
309
- if (source.url) {
310
- citation += `Retrieved from ${source.url}`;
439
+
440
+ // Prepare data
441
+ const labels = timeline.map(item => {
442
+ const date = new Date(item.timestamp);
443
+ return date.toLocaleTimeString('en-US', {
444
+ hour: '2-digit',
445
+ minute: '2-digit',
446
+ second: '2-digit'
447
+ });
448
+ });
449
+
450
+ const cumulativeTokens = timeline.map(item => item.cumulative_tokens);
451
+ const cumulativePromptTokens = timeline.map(item => item.cumulative_prompt_tokens);
452
+ const cumulativeCompletionTokens = timeline.map(item => item.cumulative_completion_tokens);
453
+ const promptTokens = timeline.map(item => item.prompt_tokens);
454
+ const completionTokens = timeline.map(item => item.completion_tokens);
455
+
456
+ // Debug logging
457
+ console.log('Timeline data sample:', timeline.slice(0, 2));
458
+ console.log('Prompt tokens sample:', promptTokens.slice(0, 5));
459
+ console.log('Completion tokens sample:', completionTokens.slice(0, 5));
460
+
461
+ // Destroy existing chart
462
+ if (timelineChart) {
463
+ timelineChart.destroy();
311
464
  }
312
-
313
- // Copy to clipboard
314
- try {
315
- await navigator.clipboard.writeText(citation);
316
-
317
- // Show tooltip
318
- const btn = document.querySelector(`.source-item[data-id="${sourceIndex}"] .copy-citation-btn`);
319
- if (btn) {
320
- const originalHTML = btn.innerHTML;
321
- btn.innerHTML = '<i class="fas fa-check"></i>';
322
-
323
- setTimeout(() => {
324
- btn.innerHTML = originalHTML;
325
- }, 2000);
465
+
466
+ timelineChart = new Chart(ctx, {
467
+ type: 'line',
468
+ data: {
469
+ labels: labels,
470
+ datasets: [{
471
+ label: 'Cumulative Total Tokens',
472
+ data: cumulativeTokens,
473
+ borderColor: 'rgba(75, 192, 192, 1)',
474
+ backgroundColor: 'rgba(75, 192, 192, 0.1)',
475
+ borderWidth: 3,
476
+ fill: true,
477
+ tension: 0.1,
478
+ pointRadius: 4,
479
+ pointHoverRadius: 6,
480
+ }, {
481
+ label: 'Cumulative Input Tokens',
482
+ data: cumulativePromptTokens,
483
+ borderColor: 'rgba(54, 162, 235, 1)',
484
+ backgroundColor: 'rgba(54, 162, 235, 0.05)',
485
+ borderWidth: 2,
486
+ fill: true,
487
+ tension: 0.1,
488
+ pointRadius: 2,
489
+ pointHoverRadius: 4,
490
+ }, {
491
+ label: 'Cumulative Output Tokens',
492
+ data: cumulativeCompletionTokens,
493
+ borderColor: 'rgba(255, 99, 132, 1)',
494
+ backgroundColor: 'rgba(255, 99, 132, 0.05)',
495
+ borderWidth: 2,
496
+ fill: true,
497
+ tension: 0.1,
498
+ pointRadius: 2,
499
+ pointHoverRadius: 4,
500
+ }, {
501
+ label: 'Input Tokens per Call',
502
+ data: promptTokens,
503
+ borderColor: 'rgba(54, 162, 235, 0.8)',
504
+ backgroundColor: 'rgba(54, 162, 235, 0.1)',
505
+ borderWidth: 1,
506
+ fill: false,
507
+ tension: 0.1,
508
+ pointRadius: 2,
509
+ pointHoverRadius: 4,
510
+ yAxisID: 'y1',
511
+ borderDash: [5, 5],
512
+ }, {
513
+ label: 'Output Tokens per Call',
514
+ data: completionTokens,
515
+ borderColor: 'rgba(255, 99, 132, 0.8)',
516
+ backgroundColor: 'rgba(255, 99, 132, 0.1)',
517
+ borderWidth: 1,
518
+ fill: false,
519
+ tension: 0.1,
520
+ pointRadius: 2,
521
+ pointHoverRadius: 4,
522
+ yAxisID: 'y1',
523
+ borderDash: [5, 5],
524
+ }]
525
+ },
526
+ options: {
527
+ responsive: true,
528
+ maintainAspectRatio: false,
529
+ interaction: {
530
+ intersect: false,
531
+ mode: 'index'
532
+ },
533
+ scales: {
534
+ x: {
535
+ display: true,
536
+ title: {
537
+ display: true,
538
+ text: 'Time'
539
+ }
540
+ },
541
+ y: {
542
+ type: 'linear',
543
+ display: true,
544
+ position: 'left',
545
+ title: {
546
+ display: true,
547
+ text: 'Cumulative Tokens'
548
+ },
549
+ beginAtZero: true
550
+ },
551
+ y1: {
552
+ type: 'linear',
553
+ display: true,
554
+ position: 'right',
555
+ title: {
556
+ display: true,
557
+ text: 'Tokens per Call'
558
+ },
559
+ beginAtZero: true,
560
+ grid: {
561
+ drawOnChartArea: false,
562
+ },
563
+ }
564
+ },
565
+ plugins: {
566
+ legend: {
567
+ position: 'top'
568
+ },
569
+ tooltip: {
570
+ callbacks: {
571
+ title: function(context) {
572
+ const index = context[0].dataIndex;
573
+ const item = timeline[index];
574
+ return `${item.research_phase || 'Unknown Phase'} - ${item.model_name || 'Unknown Model'}`;
575
+ },
576
+ label: function(context) {
577
+ let label = context.dataset.label || '';
578
+ if (label) {
579
+ label += ': ';
580
+ }
581
+ label += formatNumber(context.parsed.y);
582
+ return label;
583
+ },
584
+ afterBody: function(context) {
585
+ const index = context[0].dataIndex;
586
+ const item = timeline[index];
587
+ return [
588
+ `Response Time: ${item.response_time_ms || 0}ms`,
589
+ `Status: ${item.success_status || 'unknown'}`,
590
+ `Engine: ${item.search_engine_selected || 'N/A'}`
591
+ ];
592
+ }
593
+ }
594
+ }
595
+ }
326
596
  }
327
- } catch (error) {
328
- console.error('Failed to copy citation:', error);
329
- }
597
+ });
330
598
  }
331
-
599
+
332
600
  /**
333
- * Apply syntax highlighting to code blocks
601
+ * Display phase breakdown
602
+ * @param {Object} phaseStats - Phase statistics
334
603
  */
335
- function highlightCodeBlocks() {
336
- // Check if Prism is available
337
- if (typeof Prism !== 'undefined') {
338
- Prism.highlightAllUnder(detailContainer);
604
+ function displayPhaseBreakdown(phaseStats) {
605
+ const container = document.getElementById('phase-breakdown');
606
+ if (!container) return;
607
+
608
+ container.innerHTML = '';
609
+
610
+ Object.keys(phaseStats).forEach(phase => {
611
+ const stats = phaseStats[phase];
612
+ const item = document.createElement('div');
613
+ item.className = 'phase-stat-item';
614
+ item.innerHTML = `
615
+ <div class="phase-name">${phase}</div>
616
+ <div class="phase-tokens">${formatNumber(stats.tokens)} tokens</div>
617
+ <div class="phase-calls">${stats.count} calls | ${stats.avg_response_time}ms avg</div>
618
+ `;
619
+ container.appendChild(item);
620
+ });
621
+ }
622
+
623
+ /**
624
+ * Display call stack traces for LLM calls
625
+ * @param {Array} timeline - Timeline data with call stack info
626
+ */
627
+ function displayCallStackTraces(timeline) {
628
+ const container = document.getElementById('call-stack-traces');
629
+ if (!container) return;
630
+
631
+ container.innerHTML = '';
632
+
633
+ // Filter timeline items that have call stack information
634
+ const itemsWithCallStack = timeline.filter(item => item.call_stack || item.calling_function);
635
+
636
+ if (itemsWithCallStack.length === 0) {
637
+ container.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">No call stack traces available for this research</p>';
638
+ return;
339
639
  }
640
+
641
+ itemsWithCallStack.forEach((item, index) => {
642
+ const trace = document.createElement('div');
643
+ trace.style.marginBottom = '1rem';
644
+ trace.style.padding = '0.75rem';
645
+ trace.style.backgroundColor = 'var(--card-bg)';
646
+ trace.style.borderRadius = '6px';
647
+ trace.style.border = '1px solid var(--border-color)';
648
+
649
+ // Format timestamp
650
+ const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : 'Unknown time';
651
+
652
+ trace.innerHTML = `
653
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
654
+ <div style="font-weight: 500; color: var(--text-primary);">
655
+ ${escapeHtml(item.calling_function || 'Unknown Function')}
656
+ </div>
657
+ <div style="font-size: 0.875rem; color: var(--text-secondary);">
658
+ ${timestamp} - ${item.prompt_tokens || 0} in + ${item.completion_tokens || 0} out = ${item.tokens || 0} tokens, ${item.response_time_ms || 0}ms
659
+ </div>
660
+ </div>
661
+ <div style="font-family: 'Courier New', monospace; font-size: 0.75rem; background: var(--bg-color); padding: 0.5rem; border-radius: 4px; color: var(--text-secondary); overflow-x: auto; margin-bottom: 0.5rem;">
662
+ ${escapeHtml(item.call_stack || 'No stack trace available')}
663
+ </div>
664
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.5rem; font-size: 0.875rem;">
665
+ <div><strong>File:</strong> ${escapeHtml((item.calling_file || 'Unknown').split('/').pop())}</div>
666
+ <div><strong>Phase:</strong> ${escapeHtml(item.research_phase || 'N/A')}</div>
667
+ <div><strong>Model:</strong> ${escapeHtml(item.model_name || 'N/A')}</div>
668
+ <div><strong>Status:</strong>
669
+ <span style="color: ${item.success_status === 'success' ? 'green' : 'red'}">
670
+ ${escapeHtml(item.success_status || 'Unknown')}
671
+ </span>
672
+ </div>
673
+ </div>
674
+ `;
675
+ container.appendChild(trace);
676
+ });
677
+ }
678
+
679
+ /**
680
+ * Format large numbers with commas
681
+ * @param {number} num - Number to format
682
+ * @returns {string} Formatted number
683
+ */
684
+ function formatNumber(num) {
685
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
340
686
  }
341
-
687
+
688
+ /**
689
+ * Escape HTML to prevent XSS
690
+ * @param {string} text - Text to escape
691
+ * @returns {string} Escaped text
692
+ */
693
+ function escapeHtml(text) {
694
+ const div = document.createElement('div');
695
+ div.textContent = text;
696
+ return div.innerHTML;
697
+ }
698
+
699
+ // Clean up on page unload
700
+ window.addEventListener('beforeunload', function() {
701
+ if (pollInterval) {
702
+ clearInterval(pollInterval);
703
+ }
704
+ });
705
+
342
706
  // Initialize on DOM content loaded
343
707
  if (document.readyState === 'loading') {
344
- document.addEventListener('DOMContentLoaded', initializeDetail);
708
+ document.addEventListener('DOMContentLoaded', () => {
709
+ initializeResearchDetails().catch(console.error);
710
+ });
345
711
  } else {
346
- initializeDetail();
712
+ initializeResearchDetails().catch(console.error);
347
713
  }
348
- })();
714
+ })();