gitradar 0.1.5__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 (51) hide show
  1. demo/api/index.py +14 -0
  2. demo/gitradar/__init__.py +22 -0
  3. demo/gitradar/cli.py +214 -0
  4. demo/gitradar/config.py +117 -0
  5. demo/gitradar/models.py +67 -0
  6. demo/gitradar/prompts/__init__.py +18 -0
  7. demo/gitradar/services/github.py +179 -0
  8. demo/gitradar/services/llm.py +342 -0
  9. demo/gitradar/utils/ui.py +196 -0
  10. demo/gitradar/web/__init__.py +7 -0
  11. demo/gitradar/web/app.py +173 -0
  12. examples/python_sdk_usage.py +48 -0
  13. gitradar/__init__.py +22 -0
  14. gitradar/cli.py +214 -0
  15. gitradar/config.py +117 -0
  16. gitradar/models.py +67 -0
  17. gitradar/prompts/__init__.py +18 -0
  18. gitradar/prompts/gap_analysis_system.j2 +36 -0
  19. gitradar/prompts/gap_analysis_user.j2 +21 -0
  20. gitradar/prompts/query_expansion_system.j2 +19 -0
  21. gitradar/prompts/query_expansion_user.j2 +1 -0
  22. gitradar/prompts/relevance_evaluation_system.j2 +22 -0
  23. gitradar/prompts/relevance_evaluation_user.j2 +14 -0
  24. gitradar/services/github.py +179 -0
  25. gitradar/services/llm.py +342 -0
  26. gitradar/utils/ui.py +196 -0
  27. gitradar/web/__init__.py +7 -0
  28. gitradar/web/app.py +173 -0
  29. gitradar/web/static/apple-touch-icon.png +0 -0
  30. gitradar/web/static/css/style.css +1430 -0
  31. gitradar/web/static/favicon-96x96.png +0 -0
  32. gitradar/web/static/favicon.ico +0 -0
  33. gitradar/web/static/favicon.svg +1 -0
  34. gitradar/web/static/js/app.js +1206 -0
  35. gitradar/web/static/site.webmanifest +21 -0
  36. gitradar/web/static/web-app-manifest-192x192.png +0 -0
  37. gitradar/web/static/web-app-manifest-512x512.png +0 -0
  38. gitradar/web/templates/index.html +521 -0
  39. gitradar-0.1.5.dist-info/METADATA +251 -0
  40. gitradar-0.1.5.dist-info/RECORD +51 -0
  41. gitradar-0.1.5.dist-info/WHEEL +5 -0
  42. gitradar-0.1.5.dist-info/entry_points.txt +2 -0
  43. gitradar-0.1.5.dist-info/licenses/LICENSE +21 -0
  44. gitradar-0.1.5.dist-info/top_level.txt +4 -0
  45. tests/conftest.py +27 -0
  46. tests/test_cli.py +29 -0
  47. tests/test_llm.py +163 -0
  48. tests/test_models.py +90 -0
  49. tests/test_prompts.py +58 -0
  50. tests/test_relevance.py +75 -0
  51. tests/test_web.py +81 -0
@@ -0,0 +1,1206 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const tabAnalyzeBtn = document.getElementById('tab-analyze-btn');
3
+ const tabSearchBtn = document.getElementById('tab-search-btn');
4
+ const analyzeSection = document.getElementById('analyze-section');
5
+ const searchSection = document.getElementById('search-section');
6
+
7
+ const ideaInput = document.getElementById('idea-input');
8
+ const analyzeBtn = document.getElementById('analyze-btn');
9
+ const searchInput = document.getElementById('search-input');
10
+ const quickSearchBtn = document.getElementById('quick-search-btn');
11
+ const searchSortSelect = document.getElementById('search-sort-select');
12
+ const searchLimitSelect = document.getElementById('search-limit-select');
13
+
14
+ const loading = document.getElementById('loading');
15
+ const errorBox = document.getElementById('error-box');
16
+ const errorMessage = document.getElementById('error-message');
17
+ const results = document.getElementById('results');
18
+
19
+ // Export Buttons
20
+ const exportMDBtn = document.getElementById('export-md-btn');
21
+ const exportJSONBtn = document.getElementById('export-json-btn');
22
+ const copyReportBtn = document.getElementById('copy-report-btn');
23
+ const copyBtnText = document.getElementById('copy-btn-text');
24
+
25
+ // Settings Modal Elements
26
+ const settingsModal = document.getElementById('settings-modal');
27
+ const openSettingsBtn = document.getElementById('open-settings-btn');
28
+ const closeSettingsBtn = document.getElementById('close-settings-btn');
29
+ const cancelSettingsBtn = document.getElementById('cancel-settings-btn');
30
+ const saveSettingsBtn = document.getElementById('save-settings-btn');
31
+
32
+ const settingReportLangSelect = document.getElementById('setting-report-lang-select');
33
+ const settingModelSelect = document.getElementById('setting-model-select');
34
+ const settingCustomModelInput = document.getElementById('setting-custom-model-input');
35
+ const settingLimitInput = document.getElementById('setting-limit-input');
36
+ const settingMinRelevanceInput = document.getElementById('setting-min-relevance-input');
37
+ const settingApiKey = document.getElementById('setting-api-key') || document.getElementById('setting-groq-key');
38
+ const settingBaseUrl = document.getElementById('setting-base-url');
39
+ const settingGithubToken = document.getElementById('setting-github-token');
40
+
41
+ // Load Settings and Preferences from LocalStorage
42
+ let customSettings = JSON.parse(localStorage.getItem('gitradar_settings') || '{}');
43
+ let currentTheme = localStorage.getItem('gitradar_theme') || 'dark';
44
+ let currentLang = localStorage.getItem('gitradar_lang') || 'en';
45
+
46
+ // Theme Controls
47
+ function applyTheme(theme) {
48
+ currentTheme = theme;
49
+ document.documentElement.setAttribute('data-theme', theme);
50
+
51
+ // Synchronize modal segmented theme buttons
52
+ document.querySelectorAll('[data-theme-choice]').forEach(btn => {
53
+ if (btn.getAttribute('data-theme-choice') === theme) {
54
+ btn.classList.add('active');
55
+ } else {
56
+ btn.classList.remove('active');
57
+ }
58
+ });
59
+
60
+ const themeMoonIcon = document.getElementById('theme-moon-icon');
61
+ const themeSunIcon = document.getElementById('theme-sun-icon');
62
+ if (themeMoonIcon && themeSunIcon) {
63
+ if (theme === 'light') {
64
+ themeMoonIcon.classList.add('hidden');
65
+ themeSunIcon.classList.remove('hidden');
66
+ } else {
67
+ themeSunIcon.classList.add('hidden');
68
+ themeMoonIcon.classList.remove('hidden');
69
+ }
70
+ }
71
+ }
72
+
73
+ applyTheme(currentTheme);
74
+
75
+ document.querySelectorAll('[data-theme-choice]').forEach(btn => {
76
+ btn.addEventListener('click', () => {
77
+ const chosenTheme = btn.getAttribute('data-theme-choice');
78
+ if (chosenTheme) {
79
+ applyTheme(chosenTheme);
80
+ localStorage.setItem('gitradar_theme', chosenTheme);
81
+ }
82
+ });
83
+ });
84
+
85
+ const themeToggleBtn = document.getElementById('theme-toggle-btn');
86
+ if (themeToggleBtn) {
87
+ themeToggleBtn.addEventListener('click', () => {
88
+ const nextTheme = currentTheme === 'dark' ? 'light' : 'dark';
89
+ applyTheme(nextTheme);
90
+ localStorage.setItem('gitradar_theme', nextTheme);
91
+ });
92
+ }
93
+
94
+ // ==========================================================================
95
+ // Hallmark Custom Tactile Dropdown Component
96
+ // ==========================================================================
97
+ function initCustomDropdowns() {
98
+ document.querySelectorAll('select.form-select').forEach(select => {
99
+ if (select.dataset.customEnhanced) return;
100
+ select.dataset.customEnhanced = 'true';
101
+
102
+ const wrapper = document.createElement('div');
103
+ wrapper.className = 'custom-select-wrapper' + (select.classList.contains('form-select-sm') ? ' custom-select-sm' : '');
104
+
105
+ select.parentNode.insertBefore(wrapper, select);
106
+ wrapper.appendChild(select);
107
+ select.classList.add('visually-hidden-select');
108
+ select.setAttribute('tabindex', '-1');
109
+ select.setAttribute('aria-hidden', 'true');
110
+
111
+ const trigger = document.createElement('button');
112
+ trigger.type = 'button';
113
+ trigger.className = 'custom-select-trigger';
114
+ trigger.setAttribute('aria-haspopup', 'listbox');
115
+ trigger.setAttribute('aria-expanded', 'false');
116
+
117
+ const labelSpan = document.createElement('span');
118
+ labelSpan.className = 'custom-select-label';
119
+
120
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
121
+ arrow.setAttribute('class', 'custom-select-arrow');
122
+ arrow.setAttribute('width', '14');
123
+ arrow.setAttribute('height', '14');
124
+ arrow.setAttribute('viewBox', '0 0 24 24');
125
+ arrow.setAttribute('fill', 'none');
126
+ arrow.setAttribute('stroke', 'currentColor');
127
+ arrow.setAttribute('stroke-width', '2');
128
+ arrow.setAttribute('stroke-linecap', 'round');
129
+ arrow.setAttribute('stroke-linejoin', 'round');
130
+ arrow.innerHTML = '<polyline points="6 9 12 15 18 9"></polyline>';
131
+
132
+ trigger.appendChild(labelSpan);
133
+ trigger.appendChild(arrow);
134
+ wrapper.appendChild(trigger);
135
+
136
+ const dropdown = document.createElement('div');
137
+ dropdown.className = 'custom-select-dropdown';
138
+ dropdown.setAttribute('role', 'listbox');
139
+ wrapper.appendChild(dropdown);
140
+
141
+ function updateTriggerAndOptions() {
142
+ const curIdx = select.selectedIndex >= 0 ? select.selectedIndex : 0;
143
+ const curOpt = select.options[curIdx];
144
+ labelSpan.textContent = curOpt ? curOpt.text : '';
145
+
146
+ dropdown.querySelectorAll('.custom-select-option').forEach((el, idx) => {
147
+ if (idx === curIdx) {
148
+ el.classList.add('is-selected');
149
+ } else {
150
+ el.classList.remove('is-selected');
151
+ }
152
+ });
153
+ }
154
+
155
+ function buildOptionElements() {
156
+ dropdown.innerHTML = '';
157
+ const curIdx = select.selectedIndex >= 0 ? select.selectedIndex : 0;
158
+ const curOpt = select.options[curIdx];
159
+ labelSpan.textContent = curOpt ? curOpt.text : '';
160
+
161
+ Array.from(select.options).forEach((opt, idx) => {
162
+ const optEl = document.createElement('div');
163
+ optEl.className = 'custom-select-option' + (idx === curIdx ? ' is-selected' : '');
164
+ optEl.setAttribute('role', 'option');
165
+ optEl.setAttribute('data-value', opt.value);
166
+
167
+ const textSpan = document.createElement('span');
168
+ textSpan.textContent = opt.text;
169
+ optEl.appendChild(textSpan);
170
+
171
+ const check = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
172
+ check.setAttribute('class', 'check-icon');
173
+ check.setAttribute('width', '14');
174
+ check.setAttribute('height', '14');
175
+ check.setAttribute('viewBox', '0 0 24 24');
176
+ check.setAttribute('fill', 'none');
177
+ check.setAttribute('stroke', 'currentColor');
178
+ check.setAttribute('stroke-width', '2.5');
179
+ check.setAttribute('stroke-linecap', 'round');
180
+ check.setAttribute('stroke-linejoin', 'round');
181
+ check.innerHTML = '<polyline points="20 6 9 17 4 12"></polyline>';
182
+ optEl.appendChild(check);
183
+
184
+ optEl.addEventListener('click', (e) => {
185
+ e.stopPropagation();
186
+ select.selectedIndex = idx;
187
+ select.value = opt.value;
188
+ updateTriggerAndOptions();
189
+ closeDropdown();
190
+ select.dispatchEvent(new Event('change', { bubbles: true }));
191
+ });
192
+
193
+ dropdown.appendChild(optEl);
194
+ });
195
+ }
196
+
197
+ function closeDropdown() {
198
+ wrapper.classList.remove('is-open');
199
+ wrapper.classList.remove('drop-up');
200
+ trigger.setAttribute('aria-expanded', 'false');
201
+ }
202
+
203
+ function toggleDropdown() {
204
+ const isOpen = wrapper.classList.contains('is-open');
205
+ document.querySelectorAll('.custom-select-wrapper.is-open').forEach(w => {
206
+ if (w !== wrapper) {
207
+ w.classList.remove('is-open');
208
+ w.classList.remove('drop-up');
209
+ const tr = w.querySelector('.custom-select-trigger');
210
+ if (tr) tr.setAttribute('aria-expanded', 'false');
211
+ }
212
+ });
213
+ if (isOpen) {
214
+ closeDropdown();
215
+ } else {
216
+ const rect = trigger.getBoundingClientRect();
217
+ const spaceBelow = window.innerHeight - rect.bottom;
218
+ const spaceAbove = rect.top;
219
+ if (spaceBelow < 230 && spaceAbove > spaceBelow) {
220
+ wrapper.classList.add('drop-up');
221
+ } else {
222
+ wrapper.classList.remove('drop-up');
223
+ }
224
+
225
+ wrapper.classList.add('is-open');
226
+ trigger.setAttribute('aria-expanded', 'true');
227
+ const sel = dropdown.querySelector('.custom-select-option.is-selected');
228
+ if (sel) sel.scrollIntoView({ block: 'nearest' });
229
+ }
230
+ }
231
+
232
+ trigger.addEventListener('click', (e) => {
233
+ e.preventDefault();
234
+ e.stopPropagation();
235
+ toggleDropdown();
236
+ });
237
+
238
+ trigger.addEventListener('keydown', (e) => {
239
+ const isOpen = wrapper.classList.contains('is-open');
240
+ if (e.key === 'ArrowDown' || e.key === 'Down') {
241
+ e.preventDefault();
242
+ if (!isOpen) {
243
+ toggleDropdown();
244
+ } else if (select.selectedIndex < select.options.length - 1) {
245
+ select.selectedIndex++;
246
+ updateTriggerAndOptions();
247
+ select.dispatchEvent(new Event('change', { bubbles: true }));
248
+ }
249
+ } else if (e.key === 'ArrowUp' || e.key === 'Up') {
250
+ e.preventDefault();
251
+ if (!isOpen) {
252
+ toggleDropdown();
253
+ } else if (select.selectedIndex > 0) {
254
+ select.selectedIndex--;
255
+ updateTriggerAndOptions();
256
+ select.dispatchEvent(new Event('change', { bubbles: true }));
257
+ }
258
+ } else if (e.key === 'Escape') {
259
+ if (isOpen) {
260
+ e.preventDefault();
261
+ closeDropdown();
262
+ }
263
+ } else if (e.key === 'Enter' || e.key === ' ') {
264
+ e.preventDefault();
265
+ toggleDropdown();
266
+ }
267
+ });
268
+
269
+ select._syncDropdown = updateTriggerAndOptions;
270
+ select._rebuildDropdown = buildOptionElements;
271
+
272
+ buildOptionElements();
273
+ });
274
+ }
275
+
276
+ function syncCustomDropdown(selectEl) {
277
+ if (selectEl && typeof selectEl._syncDropdown === 'function') {
278
+ selectEl._syncDropdown();
279
+ }
280
+ }
281
+
282
+ // Dismiss dropdowns when clicking outside
283
+ document.addEventListener('click', (e) => {
284
+ if (!e.target.closest('.custom-select-wrapper')) {
285
+ document.querySelectorAll('.custom-select-wrapper.is-open').forEach(w => {
286
+ w.classList.remove('is-open');
287
+ w.classList.remove('drop-up');
288
+ const tr = w.querySelector('.custom-select-trigger');
289
+ if (tr) tr.setAttribute('aria-expanded', 'false');
290
+ });
291
+ }
292
+ });
293
+
294
+ if (settingModelSelect) {
295
+ settingModelSelect.addEventListener('change', () => {
296
+ if (settingModelSelect.value === 'custom') {
297
+ if (settingCustomModelInput) {
298
+ settingCustomModelInput.classList.remove('hidden');
299
+ settingCustomModelInput.focus();
300
+ }
301
+ } else {
302
+ if (settingCustomModelInput) settingCustomModelInput.classList.add('hidden');
303
+ }
304
+ });
305
+ }
306
+
307
+ function populateSettingsModal() {
308
+ if (customSettings.language) settingReportLangSelect.value = customSettings.language;
309
+ if (customSettings.model && settingModelSelect) {
310
+ let found = false;
311
+ for (let i = 0; i < settingModelSelect.options.length; i++) {
312
+ if (settingModelSelect.options[i].value === customSettings.model) {
313
+ settingModelSelect.value = customSettings.model;
314
+ found = true;
315
+ break;
316
+ }
317
+ }
318
+ if (!found && customSettings.model !== 'custom') {
319
+ settingModelSelect.value = 'custom';
320
+ if (settingCustomModelInput) {
321
+ settingCustomModelInput.value = customSettings.model;
322
+ settingCustomModelInput.classList.remove('hidden');
323
+ }
324
+ } else if (settingCustomModelInput) {
325
+ settingCustomModelInput.classList.add('hidden');
326
+ }
327
+ }
328
+ if (customSettings.limit) settingLimitInput.value = customSettings.limit;
329
+ if (customSettings.minRelevance) settingMinRelevanceInput.value = customSettings.minRelevance;
330
+ const existingKey = customSettings.apiKey || customSettings.openaiKey || customSettings.groqKey;
331
+ if (existingKey && settingApiKey) settingApiKey.value = existingKey;
332
+ if (customSettings.baseUrl && settingBaseUrl) settingBaseUrl.value = customSettings.baseUrl;
333
+ if (customSettings.githubToken && settingGithubToken) settingGithubToken.value = customSettings.githubToken;
334
+
335
+ // Synchronize custom dropdown UIs
336
+ syncCustomDropdown(settingReportLangSelect);
337
+ syncCustomDropdown(settingModelSelect);
338
+
339
+ // Synchronize theme & language buttons
340
+ applyTheme(currentTheme);
341
+ document.querySelectorAll('.lang-btn').forEach(btn => {
342
+ if (btn.getAttribute('data-lang') === currentLang) btn.classList.add('active');
343
+ else btn.classList.remove('active');
344
+ });
345
+ }
346
+
347
+ // Initialize custom dropdowns and populate modal inputs on load
348
+ initCustomDropdowns();
349
+ populateSettingsModal();
350
+
351
+ openSettingsBtn.addEventListener('click', () => {
352
+ populateSettingsModal();
353
+ settingsModal.classList.remove('hidden');
354
+ });
355
+
356
+ function closeSettings() {
357
+ settingsModal.classList.add('hidden');
358
+ }
359
+
360
+ closeSettingsBtn.addEventListener('click', closeSettings);
361
+ cancelSettingsBtn.addEventListener('click', closeSettings);
362
+
363
+ settingsModal.addEventListener('click', (e) => {
364
+ if (e.target === settingsModal) closeSettings();
365
+ });
366
+
367
+ document.addEventListener('keydown', (e) => {
368
+ if (e.key === 'Escape' && !settingsModal.classList.contains('hidden')) {
369
+ closeSettings();
370
+ }
371
+ });
372
+
373
+ saveSettingsBtn.addEventListener('click', () => {
374
+ let chosenModel = settingModelSelect.value;
375
+ if (chosenModel === 'custom' && settingCustomModelInput) {
376
+ chosenModel = settingCustomModelInput.value.trim() || 'gpt-4o-mini';
377
+ }
378
+
379
+ const enteredKey = settingApiKey ? settingApiKey.value.trim() : '';
380
+ const enteredBaseUrl = settingBaseUrl ? settingBaseUrl.value.trim() : '';
381
+
382
+ customSettings = {
383
+ language: settingReportLangSelect.value,
384
+ model: chosenModel,
385
+ limit: parseInt(settingLimitInput.value, 10) || 10,
386
+ minRelevance: parseInt(settingMinRelevanceInput.value, 10) || 50,
387
+ apiKey: enteredKey,
388
+ openaiKey: enteredKey,
389
+ baseUrl: enteredBaseUrl,
390
+ githubToken: settingGithubToken.value.trim(),
391
+ };
392
+ localStorage.setItem('gitradar_settings', JSON.stringify(customSettings));
393
+
394
+ // Also update UI language if setting language matches TR or English
395
+ if (customSettings.language === 'Turkish') {
396
+ currentLang = 'tr';
397
+ } else if (customSettings.language === 'English') {
398
+ currentLang = 'en';
399
+ }
400
+ localStorage.setItem('gitradar_lang', currentLang);
401
+ setLanguage(currentLang);
402
+
403
+ closeSettings();
404
+ });
405
+
406
+ // i18n Translations
407
+ const TRANSLATIONS = {
408
+ en: {
409
+ engine_active: "Engine Active",
410
+ tab_market: "Market Analysis",
411
+ tab_search: "Repository Search",
412
+ analyze_title: "Developer Project Idea Analysis",
413
+ analyze_desc: "Enter your developer tool concept. GitRadar expands query terms, scans live GitHub repositories via REST API, and synthesizes competitive market gaps.",
414
+ idea_placeholder: "e.g. AI-powered terminal code review and git diff analyzer...",
415
+ quick_prompts: "Quick Prompts:",
416
+ chip1: "Terminal AI Reviewer",
417
+ chip2: "K8s Log Explainer",
418
+ chip3: "Rust Git Credential Scanner",
419
+ run_analysis: "Run Market Analysis",
420
+ search_title: "Direct GitHub Search",
421
+ search_desc: "Execute direct GitHub REST API queries to retrieve repository star counts, forks, and details.",
422
+ search_placeholder: "e.g. terminal devtools, git hooks, rust cli...",
423
+ search_repos: "Search Repositories",
424
+ loading_status: "Synthesizing Search Strategy...",
425
+ loading_subtext: "Fetching active GitHub repositories & evaluating market saturation",
426
+ error_title: "Analysis Failed",
427
+ strategy_title: "Synthesized Query Strategy",
428
+ keywords_label: "Keywords:",
429
+ topics_label: "GitHub Topics:",
430
+ strategy_label: "Strategy:",
431
+ opp_score: "Opportunity Score",
432
+ opp_hint: "Market viability rating",
433
+ sat_score: "Market Saturation",
434
+ sat_index: "Saturation index:",
435
+ repos_analyzed: "Repositories Analyzed",
436
+ repos_hint: "GitHub API candidates verified",
437
+ market_synthesis: "Market Synthesis",
438
+ discovered_repos: "Discovered Candidate Repositories",
439
+ th_repo: "Repository",
440
+ th_relevance: "Relevance",
441
+ th_stars: "Stars",
442
+ th_forks: "Forks",
443
+ th_lang: "Language",
444
+ th_updated: "Updated",
445
+ th_desc: "Description",
446
+ competitors_title: "Top Competitor Profile Breakdown",
447
+ strengths_label: "Strengths",
448
+ weaknesses_label: "Gaps & Weaknesses",
449
+ unmet_title: "Unmet Market Needs",
450
+ diff_title: "Key Differentiators",
451
+ rec_title: "Strategic Action Items",
452
+ settings_title: "Configuration Settings",
453
+ setting_report_lang: "AI Response Language",
454
+ setting_model: "LLM Model",
455
+ setting_limit: "Max Repositories to Analyze",
456
+ setting_min_relevance: "Min Relevance Threshold (%)",
457
+ setting_api_key: "OpenAI / Compatible API Key",
458
+ setting_base_url: "Custom Base URL (Optional)",
459
+ setting_groq_key: "Groq API Key",
460
+ setting_github_token: "GitHub Access Token (Optional)",
461
+ btn_cancel: "Cancel",
462
+ btn_save: "Save Settings",
463
+ tech_guide_title: "Technical Implementation & Open-Source Roadmap",
464
+ recommended_stack_label: "Recommended Tech Stack",
465
+ arch_overview_label: "Architecture Overview",
466
+ open_source_blocks_label: "Recommended Open-Source Building Blocks",
467
+ export_report_label: "Export Report",
468
+ btn_export_md: "Export Markdown (.md)",
469
+ btn_export_json: "Export JSON (.json)",
470
+ btn_copy_report: "Copy Report",
471
+ copied_text: "Copied!",
472
+ sort_label: "Sort By",
473
+ limit_label: "Results",
474
+ sort_stars: "★ Most Stars",
475
+ sort_forks: "⑂ Most Forks",
476
+ sort_updated: "⏱ Recently Updated",
477
+ limit_10: "10 Repositories",
478
+ limit_20: "20 Repositories",
479
+ limit_30: "30 Repositories",
480
+ settings_btn: "Settings",
481
+ modal_section_appearance: "Appearance & Language",
482
+ setting_theme: "Interface Theme",
483
+ theme_dark: "Dark Mode",
484
+ theme_light: "Light Mode",
485
+ setting_ui_lang: "Interface Language",
486
+ modal_section_ai: "AI Engine & Model",
487
+ modal_section_analysis: "Analysis Parameters",
488
+ },
489
+ tr: {
490
+ engine_active: "Servis Aktif",
491
+ tab_market: "Pazar Analizi",
492
+ tab_search: "Depo Arama",
493
+ analyze_title: "Geliştirici Proje Fikir Analizi",
494
+ analyze_desc: "Proje fikrinizi girin. GitRadar sorgu terimlerini genişletir, GitHub canlı depolarını tarar ve pazar fırsatlarını sentezler.",
495
+ idea_placeholder: "örn. Yapay zeka destekli terminal kod inceleme ve git diff analiz aracı...",
496
+ quick_prompts: "Örnek Komutlar:",
497
+ chip1: "Terminal Yapay Zeka İnceleyici",
498
+ chip2: "K8s Log Açıklayıcı",
499
+ chip3: "Rust Git Güvenlik Tarayıcı",
500
+ run_analysis: "Pazar Analizini Başlat",
501
+ search_title: "Doğrudan GitHub Araması",
502
+ search_desc: "GitHub REST API üzerinden doğrudan depo yıldız, çatallanma ve ayrıntı araması gerçekleştirin.",
503
+ search_placeholder: "örn. terminal devtools, git hooks, rust cli...",
504
+ search_repos: "Depoları Ara",
505
+ loading_status: "Arama Stratejisi Oluşturuluyor...",
506
+ loading_subtext: "Aktif GitHub depoları getiriliyor & pazar doygunluğu değerlendiriliyor",
507
+ error_title: "Analiz Başarısız Oldu",
508
+ strategy_title: "Oluşturulan Sorgu Stratejisi",
509
+ keywords_label: "Anahtar Kelimeler:",
510
+ topics_label: "GitHub Konuları:",
511
+ strategy_label: "Strateji:",
512
+ opp_score: "Fırsat Skoru",
513
+ opp_hint: "Pazar uygulanabilirlik derecesi",
514
+ sat_score: "Pazar Doygunluğu",
515
+ sat_index: "Doygunluk indeksi:",
516
+ repos_analyzed: "Analiz Edilen Depolar",
517
+ repos_hint: "Doğrulanan GitHub API adayları",
518
+ market_synthesis: "Pazar Özeti",
519
+ discovered_repos: "Keşfedilen Aday Depolar",
520
+ th_repo: "Depo",
521
+ th_relevance: "Uygunluk",
522
+ th_stars: "Yıldız",
523
+ th_forks: "Çatallanma",
524
+ th_lang: "Dil",
525
+ th_updated: "Güncellenme",
526
+ th_desc: "Açıklama",
527
+ competitors_title: "Rakip Profil Analizi",
528
+ strengths_label: "Güçlü Yönler",
529
+ weaknesses_label: "Eksik & Zayıf Yönler",
530
+ unmet_title: "Karşılanmayan Pazar İhtiyaçları",
531
+ diff_title: "Öne Çıkan Farklılaştırıcılar",
532
+ rec_title: "Stratejik Öneriler",
533
+ settings_title: "Yapılandırma Ayarları",
534
+ setting_report_lang: "Yapay Zeka Yanıt Dili",
535
+ setting_model: "LLM Modeli",
536
+ setting_limit: "Maks. Depo Sayısı",
537
+ setting_min_relevance: "Min Uygunluk Eşiği (%)",
538
+ setting_api_key: "OpenAI / Uyumlu API Anahtarı",
539
+ setting_base_url: "Özel API Taban Adresi (İsteğe Bağlı)",
540
+ setting_groq_key: "Groq API Anahtarı",
541
+ setting_github_token: "GitHub Erişim Jetonu (İsteğe Bağlı)",
542
+ btn_cancel: "İptal",
543
+ btn_save: "Ayarları Kaydet",
544
+ tech_guide_title: "Teknik Mimari & Açık Kaynak Yol Haritası",
545
+ recommended_stack_label: "Önerilen Teknoloji Yığını",
546
+ arch_overview_label: "Mimari Genel Bakış",
547
+ open_source_blocks_label: "Önerilen Açık Kaynak Yapı Taşları",
548
+ export_report_label: "Raporu Dışa Aktar",
549
+ btn_export_md: "Markdown İndir (.md)",
550
+ btn_export_json: "JSON İndir (.json)",
551
+ btn_copy_report: "Raporu Kopyala",
552
+ copied_text: "Kopyalandı!",
553
+ sort_label: "Sıralama",
554
+ limit_label: "Sonuç",
555
+ sort_stars: "★ En Çok Yıldız",
556
+ sort_forks: "⑂ En Çok Çatallanma",
557
+ sort_updated: "⏱ Son Güncellenen",
558
+ limit_10: "10 Depo",
559
+ limit_20: "20 Depo",
560
+ limit_30: "30 Depo",
561
+ settings_btn: "Ayarlar",
562
+ modal_section_appearance: "Görünüm ve Dil",
563
+ setting_theme: "Arayüz Teması",
564
+ theme_dark: "Koyu Mod",
565
+ theme_light: "Açık Mod",
566
+ setting_ui_lang: "Arayüz Dili",
567
+ modal_section_ai: "Yapay Zeka Motoru & Model",
568
+ modal_section_analysis: "Analiz Parametreleri",
569
+ }
570
+ };
571
+
572
+ currentLang = localStorage.getItem('gitradar_lang') || currentLang || 'en';
573
+ setLanguage(currentLang);
574
+
575
+ document.querySelectorAll('.lang-btn').forEach(btn => {
576
+ btn.addEventListener('click', () => {
577
+ const selectedLang = btn.getAttribute('data-lang');
578
+ if (selectedLang && selectedLang !== currentLang) {
579
+ currentLang = selectedLang;
580
+ localStorage.setItem('gitradar_lang', currentLang);
581
+
582
+ // Update customSettings language as well
583
+ customSettings.language = currentLang === 'tr' ? 'Turkish' : 'English';
584
+ localStorage.setItem('gitradar_settings', JSON.stringify(customSettings));
585
+
586
+ setLanguage(currentLang);
587
+ }
588
+ });
589
+ });
590
+
591
+ function setLanguage(lang) {
592
+ const t = TRANSLATIONS[lang] || TRANSLATIONS.en;
593
+
594
+ // Toggle active lang button
595
+ document.querySelectorAll('.lang-btn').forEach(btn => {
596
+ if (btn.getAttribute('data-lang') === lang) btn.classList.add('active');
597
+ else btn.classList.remove('active');
598
+ });
599
+
600
+ // Replace text content for data-i18n
601
+ document.querySelectorAll('[data-i18n]').forEach(el => {
602
+ const key = el.getAttribute('data-i18n');
603
+ if (t[key]) el.textContent = t[key];
604
+ });
605
+
606
+ // Replace chip texts
607
+ document.querySelectorAll('[data-i18n-chip]').forEach(el => {
608
+ const key = el.getAttribute('data-i18n-chip');
609
+ if (t[key]) el.textContent = t[key];
610
+ });
611
+
612
+ // Placeholders
613
+ ideaInput.setAttribute('placeholder', t.idea_placeholder);
614
+ searchInput.setAttribute('placeholder', t.search_placeholder);
615
+
616
+ // Sync AI response language dropdown with active UI language
617
+ const targetLangName = lang === 'tr' ? 'Turkish' : 'English';
618
+ if (settingReportLangSelect) {
619
+ settingReportLangSelect.value = targetLangName;
620
+ syncCustomDropdown(settingReportLangSelect);
621
+ }
622
+ customSettings.language = targetLangName;
623
+ localStorage.setItem('gitradar_settings', JSON.stringify(customSettings));
624
+
625
+ // Update and rebuild search dropdown options
626
+ if (searchSortSelect && searchSortSelect.options.length >= 3) {
627
+ searchSortSelect.options[0].text = t.sort_stars;
628
+ searchSortSelect.options[1].text = t.sort_forks;
629
+ searchSortSelect.options[2].text = t.sort_updated;
630
+ if (typeof searchSortSelect._rebuildDropdown === 'function') {
631
+ searchSortSelect._rebuildDropdown();
632
+ }
633
+ }
634
+ if (searchLimitSelect && searchLimitSelect.options.length >= 3) {
635
+ searchLimitSelect.options[0].text = t.limit_10;
636
+ searchLimitSelect.options[1].text = t.limit_20;
637
+ searchLimitSelect.options[2].text = t.limit_30;
638
+ if (typeof searchLimitSelect._rebuildDropdown === 'function') {
639
+ searchLimitSelect._rebuildDropdown();
640
+ }
641
+ }
642
+
643
+ // If report is already rendered, re-render texts where applicable
644
+ if (window.lastReportData) {
645
+ renderResults(window.lastReportData);
646
+ }
647
+ }
648
+
649
+ // Quick Prompt Chips
650
+ document.querySelectorAll('.chip').forEach(chip => {
651
+ chip.addEventListener('click', () => {
652
+ const promptText = chip.getAttribute('data-prompt');
653
+ if (promptText) {
654
+ ideaInput.value = promptText;
655
+ ideaInput.focus();
656
+ }
657
+ });
658
+ });
659
+
660
+ // Tab switching
661
+ tabAnalyzeBtn.addEventListener('click', () => {
662
+ tabAnalyzeBtn.classList.add('active');
663
+ tabSearchBtn.classList.remove('active');
664
+ analyzeSection.classList.remove('hidden');
665
+ searchSection.classList.add('hidden');
666
+ results.classList.add('hidden');
667
+ });
668
+
669
+ tabSearchBtn.addEventListener('click', () => {
670
+ tabSearchBtn.classList.add('active');
671
+ tabAnalyzeBtn.classList.remove('active');
672
+ searchSection.classList.remove('hidden');
673
+ analyzeSection.classList.add('hidden');
674
+ results.classList.add('hidden');
675
+ });
676
+
677
+ // Enter key trigger for search boxes (Shift+Enter inserts newline)
678
+ ideaInput.addEventListener('keydown', (e) => {
679
+ if (e.key === 'Enter' && !e.shiftKey) {
680
+ e.preventDefault();
681
+ analyzeBtn.click();
682
+ }
683
+ });
684
+
685
+ searchInput.addEventListener('keydown', (e) => {
686
+ if (e.key === 'Enter' && !e.shiftKey) {
687
+ e.preventDefault();
688
+ quickSearchBtn.click();
689
+ }
690
+ });
691
+
692
+ // Export Button Event Handlers
693
+ exportMDBtn.addEventListener('click', () => {
694
+ if (!window.lastReportData) return;
695
+ const mdContent = generateMarkdownReport(window.lastReportData);
696
+ const dateStr = new Date().toISOString().slice(0, 10);
697
+ downloadFile(`gitradar_report_${dateStr}.md`, mdContent, 'text/markdown');
698
+ });
699
+
700
+ exportJSONBtn.addEventListener('click', () => {
701
+ if (!window.lastReportData) return;
702
+ const jsonContent = JSON.stringify(window.lastReportData, null, 2);
703
+ const dateStr = new Date().toISOString().slice(0, 10);
704
+ downloadFile(`gitradar_report_${dateStr}.json`, jsonContent, 'application/json');
705
+ });
706
+
707
+ copyReportBtn.addEventListener('click', () => {
708
+ if (!window.lastReportData) return;
709
+ const mdContent = generateMarkdownReport(window.lastReportData);
710
+ const t = TRANSLATIONS[currentLang] || TRANSLATIONS.en;
711
+
712
+ navigator.clipboard.writeText(mdContent).then(() => {
713
+ const origText = copyBtnText.textContent;
714
+ copyBtnText.textContent = t.copied_text || "Copied!";
715
+ setTimeout(() => {
716
+ copyBtnText.textContent = origText;
717
+ }, 2000);
718
+ }).catch(err => {
719
+ console.error('Failed to copy to clipboard:', err);
720
+ });
721
+ });
722
+
723
+ function downloadFile(filename, text, mimeType) {
724
+ const blob = new Blob([text], { type: mimeType });
725
+ const url = URL.createObjectURL(blob);
726
+ const a = document.createElement('a');
727
+ a.href = url;
728
+ a.download = filename;
729
+ document.body.appendChild(a);
730
+ a.click();
731
+ document.body.removeChild(a);
732
+ URL.revokeObjectURL(url);
733
+ }
734
+
735
+ function generateMarkdownReport(data) {
736
+ const { queries, repositories, report } = data;
737
+ const dateStr = new Date().toLocaleString();
738
+
739
+ let md = `# 📡 GitRadar Market & Gap Analysis Report\n`;
740
+ md += `*Generated on: ${dateStr}*\n\n`;
741
+
742
+ md += `## 📌 Executive Overview\n`;
743
+ md += `- **Project Idea:** ${report.idea_summary || 'N/A'}\n`;
744
+ md += `- **Opportunity Score:** ${report.opportunity_score}/100 🚀\n`;
745
+ md += `- **Market Saturation:** ${report.market_saturation} (Score: ${report.saturation_score || 50}/100)\n`;
746
+ md += `- **Repositories Analyzed:** ${repositories.length}\n\n`;
747
+
748
+ md += `### 💡 Market Summary\n${report.market_summary}\n\n`;
749
+
750
+ if (queries) {
751
+ md += `## 🧠 Synthesized Search Strategy\n`;
752
+ md += `- **Keywords:** ${queries.search_keywords ? queries.search_keywords.join(', ') : ''}\n`;
753
+ md += `- **GitHub Topics:** ${queries.github_topics ? queries.github_topics.map(t => '#' + t).join(', ') : ''}\n`;
754
+ md += `- **Strategy Note:** ${queries.search_explanation || ''}\n\n`;
755
+ }
756
+
757
+ if (repositories && repositories.length > 0) {
758
+ md += `## 📊 Discovered Candidate Repositories\n`;
759
+ md += `| # | Repository | Relevance | Stars | Forks | Language | Updated | Description |\n`;
760
+ md += `|---|------------|-----------|-------|-------|----------|---------|-------------|\n`;
761
+ repositories.forEach((r, idx) => {
762
+ const relStr = (r.relevance_score !== undefined && r.relevance_score !== null) ? `${r.relevance_score}%` : 'N/A';
763
+ const descStr = (r.description || '').replace(/\|/g, '-');
764
+ const fitStr = r.relevance_reason ? ` *(Fit: ${r.relevance_reason.replace(/\|/g, '-')})*` : '';
765
+ md += `| ${idx + 1} | [${r.full_name}](${r.html_url}) | ${relStr} | ${r.stars} | ${r.forks} | ${r.language || 'N/A'} | ${r.updated_at || '-'} | ${descStr}${fitStr} |\n`;
766
+ });
767
+ md += `\n`;
768
+ }
769
+
770
+ if (report.top_competitors && report.top_competitors.length > 0) {
771
+ md += `## 🏆 Top Competitor Profiles\n`;
772
+ report.top_competitors.forEach(c => {
773
+ md += `### ${c.repo_name}\n`;
774
+ md += `**Strengths:**\n`;
775
+ (c.key_strengths || []).forEach(s => { md += `- ✅ ${s}\n`; });
776
+ md += `**Gaps & Weaknesses:**\n`;
777
+ (c.weaknesses_or_gaps || []).forEach(w => { md += `- ❌ ${w}\n`; });
778
+ md += `\n`;
779
+ });
780
+ }
781
+
782
+ if (report.unmet_needs && report.unmet_needs.length > 0) {
783
+ md += `## 🚨 Unmet Needs & Ecosystem Gaps\n`;
784
+ report.unmet_needs.forEach(g => { md += `- ❌ ${g}\n`; });
785
+ md += `\n`;
786
+ }
787
+
788
+ if (report.differentiators && report.differentiators.length > 0) {
789
+ md += `## 💎 Key Differentiators\n`;
790
+ report.differentiators.forEach(d => { md += `- ✨ ${d}\n`; });
791
+ md += `\n`;
792
+ }
793
+
794
+ if (report.actionable_recommendations && report.actionable_recommendations.length > 0) {
795
+ md += `## 🛠️ Strategic Action Items\n`;
796
+ report.actionable_recommendations.forEach((r, idx) => { md += `${idx + 1}. ${r}\n`; });
797
+ md += `\n`;
798
+ }
799
+
800
+ if (report.implementation_guide) {
801
+ const g = report.implementation_guide;
802
+ md += `## ⚙️ Technical Implementation & Open-Source Roadmap\n`;
803
+ if (g.recommended_tech_stack && g.recommended_tech_stack.length > 0) {
804
+ md += `**Recommended Tech Stack:** ${g.recommended_tech_stack.join(', ')}\n\n`;
805
+ }
806
+ if (g.architecture_overview) {
807
+ md += `### Architecture Overview\n${g.architecture_overview}\n\n`;
808
+ }
809
+ if (g.open_source_building_blocks && g.open_source_building_blocks.length > 0) {
810
+ md += `### Recommended Open-Source Building Blocks\n`;
811
+ g.open_source_building_blocks.forEach(tool => {
812
+ const urlStr = tool.repo_url ? ` ([GitHub](${tool.repo_url}))` : '';
813
+ md += `- **${tool.name}** \`[${tool.category}]\`${urlStr}: ${tool.description_and_usage}\n`;
814
+ });
815
+ md += `\n`;
816
+ }
817
+ }
818
+
819
+ return md;
820
+ }
821
+
822
+ function sanitizeHeaderValue(val) {
823
+ if (!val) return '';
824
+ return String(val).replace(/[\r\n\t]/g, '').trim().replace(/^['"]|['"]$/g, '');
825
+ }
826
+
827
+ // Handle Full Analysis
828
+ analyzeBtn.addEventListener('click', async () => {
829
+ const idea = ideaInput.value.trim();
830
+ if (!idea) {
831
+ ideaInput.focus();
832
+ return;
833
+ }
834
+
835
+ const t = TRANSLATIONS[currentLang] || TRANSLATIONS.en;
836
+ showLoading(t.loading_status);
837
+ hideError();
838
+ results.classList.add('hidden');
839
+
840
+ const activeApiKey = sanitizeHeaderValue(
841
+ (settingApiKey ? settingApiKey.value : '') || customSettings.apiKey || customSettings.openaiKey || customSettings.groqKey
842
+ );
843
+ const activeBaseUrl = sanitizeHeaderValue(
844
+ (settingBaseUrl ? settingBaseUrl.value : '') || customSettings.baseUrl
845
+ );
846
+ const activeGithubToken = sanitizeHeaderValue(settingGithubToken.value || customSettings.githubToken);
847
+ const limit = parseInt(settingLimitInput.value, 10) || customSettings.limit || 10;
848
+ const min_relevance = parseInt(settingMinRelevanceInput.value, 10) || customSettings.minRelevance || 50;
849
+
850
+ let model = settingModelSelect.value;
851
+ if (model === 'custom' && settingCustomModelInput) {
852
+ model = settingCustomModelInput.value.trim() || 'gpt-4o-mini';
853
+ }
854
+ model = model || customSettings.model || 'gpt-4o-mini';
855
+
856
+ const language = currentLang === 'tr' ? 'Turkish' : (settingReportLangSelect.value || customSettings.language || 'English');
857
+
858
+ // Keep customSettings synced
859
+ customSettings = {
860
+ language,
861
+ model,
862
+ limit,
863
+ minRelevance: min_relevance,
864
+ apiKey: activeApiKey,
865
+ openaiKey: activeApiKey,
866
+ baseUrl: activeBaseUrl,
867
+ githubToken: activeGithubToken,
868
+ };
869
+ localStorage.setItem('gitradar_settings', JSON.stringify(customSettings));
870
+
871
+ const headers = { 'Content-Type': 'application/json' };
872
+ if (activeApiKey) {
873
+ headers['X-OpenAI-Api-Key'] = activeApiKey;
874
+ headers['X-Groq-Api-Key'] = activeApiKey;
875
+ }
876
+ if (activeBaseUrl) {
877
+ headers['X-OpenAI-Base-Url'] = activeBaseUrl;
878
+ }
879
+ if (activeGithubToken) headers['X-Github-Token'] = activeGithubToken;
880
+
881
+ try {
882
+ const response = await fetch('/api/analyze', {
883
+ method: 'POST',
884
+ headers,
885
+ body: JSON.stringify({
886
+ idea,
887
+ limit,
888
+ min_relevance,
889
+ model,
890
+ language,
891
+ openai_key: activeApiKey || undefined,
892
+ base_url: activeBaseUrl || undefined,
893
+ groq_key: activeApiKey || undefined,
894
+ github_token: activeGithubToken || undefined,
895
+ })
896
+ });
897
+
898
+ const data = await response.json();
899
+ if (!response.ok) throw new Error(data.error || 'Failed to analyze project idea.');
900
+
901
+ window.lastReportData = data;
902
+ renderResults(data);
903
+ } catch (err) {
904
+ showError(err.message);
905
+ } finally {
906
+ hideLoading();
907
+ }
908
+ });
909
+
910
+ // Handle Standalone Search
911
+ quickSearchBtn.addEventListener('click', async () => {
912
+ const query = searchInput.value.trim();
913
+ if (!query) {
914
+ searchInput.focus();
915
+ return;
916
+ }
917
+
918
+ showLoading(`Searching GitHub for '${query}'...`);
919
+ hideError();
920
+ results.classList.add('hidden');
921
+
922
+ const activeGithubToken = sanitizeHeaderValue(settingGithubToken.value || customSettings.githubToken);
923
+ const limit = (searchLimitSelect ? parseInt(searchLimitSelect.value, 10) : null) || parseInt(settingLimitInput.value, 10) || customSettings.limit || 10;
924
+ const sort = (searchSortSelect ? searchSortSelect.value : 'stars') || 'stars';
925
+
926
+ const headers = { 'Content-Type': 'application/json' };
927
+ if (activeGithubToken) headers['X-Github-Token'] = activeGithubToken;
928
+
929
+ try {
930
+ const response = await fetch('/api/search', {
931
+ method: 'POST',
932
+ headers,
933
+ body: JSON.stringify({
934
+ query,
935
+ limit,
936
+ sort,
937
+ github_token: activeGithubToken || undefined,
938
+ })
939
+ });
940
+
941
+ const data = await response.json();
942
+ if (!response.ok) throw new Error(data.error || 'Failed to search repositories.');
943
+
944
+ renderSearchOnlyResults(data.repositories);
945
+ } catch (err) {
946
+ showError(err.message);
947
+ } finally {
948
+ hideLoading();
949
+ }
950
+ });
951
+
952
+ function renderResults(data) {
953
+ const { queries, repositories, report } = data;
954
+ const t = TRANSLATIONS[currentLang] || TRANSLATIONS.en;
955
+
956
+ // Strategy Panel
957
+ const keywordsHtml = queries.search_keywords
958
+ .map(k => `<span class="badge-code">${escapeHtml(k)}</span>`)
959
+ .join(' ');
960
+
961
+ const topicsHtml = queries.github_topics
962
+ .map(t => `<span class="badge-topic">#${escapeHtml(t)}</span>`)
963
+ .join(' ');
964
+
965
+ document.getElementById('strategy-content').innerHTML = `
966
+ <div style="display: flex; flex-direction: column; gap: 0.6rem;">
967
+ <div><strong style="font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase;">${t.keywords_label}</strong> <div style="display: inline-flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.2rem;">${keywordsHtml}</div></div>
968
+ <div><strong style="font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase;">${t.topics_label}</strong> <div style="display: inline-flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.2rem;">${topicsHtml}</div></div>
969
+ <p style="color: var(--text-secondary); font-size: 0.875rem; margin-top: 0.2rem;">${escapeHtml(queries.search_explanation)}</p>
970
+ </div>
971
+ `;
972
+
973
+ // Metrics Overview
974
+ document.getElementById('metric-opportunity').textContent = report.opportunity_score;
975
+ document.getElementById('opp-bar-fill').style.width = `${Math.min(100, Math.max(0, report.opportunity_score))}%`;
976
+
977
+ const satEl = document.getElementById('metric-saturation');
978
+ satEl.textContent = report.market_saturation;
979
+ const satScore = report.saturation_score || 50;
980
+ document.getElementById('sat-bar-fill').style.width = `${satScore}%`;
981
+ document.getElementById('metric-sat-score').textContent = `${t.sat_index} ${satScore}/100`;
982
+
983
+ if (['Low', 'Düşük'].includes(report.market_saturation)) {
984
+ satEl.style.color = 'var(--accent-emerald)';
985
+ document.getElementById('sat-bar-fill').style.background = 'var(--accent-emerald)';
986
+ } else if (['Moderate', 'Orta'].includes(report.market_saturation)) {
987
+ satEl.style.color = 'var(--accent-amber)';
988
+ document.getElementById('sat-bar-fill').style.background = 'var(--accent-amber)';
989
+ } else {
990
+ satEl.style.color = 'var(--accent-rose)';
991
+ document.getElementById('sat-bar-fill').style.background = 'var(--accent-rose)';
992
+ }
993
+
994
+ document.getElementById('metric-repos-count').textContent = repositories.length;
995
+
996
+ // Market Overview
997
+ document.getElementById('market-summary-text').textContent = report.market_summary;
998
+
999
+ // Repositories Table
1000
+ renderTable(repositories);
1001
+
1002
+ // Competitors Analysis Grid
1003
+ const compGrid = document.getElementById('competitor-grid');
1004
+ compGrid.innerHTML = '';
1005
+ report.top_competitors.forEach(c => {
1006
+ const card = document.createElement('div');
1007
+ card.className = 'comp-card';
1008
+
1009
+ const strengthsHtml = c.key_strengths.map(s => `
1010
+ <div class="list-item-clean">
1011
+ <svg class="list-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--accent-emerald)" stroke-width="2.5"><polyline points="20 6 9 17 4 12"></polyline></svg>
1012
+ <span>${escapeHtml(s)}</span>
1013
+ </div>
1014
+ `).join('');
1015
+
1016
+ const weaknessesHtml = c.weaknesses_or_gaps.map(w => `
1017
+ <div class="list-item-clean">
1018
+ <svg class="list-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--accent-rose)" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
1019
+ <span>${escapeHtml(w)}</span>
1020
+ </div>
1021
+ `).join('');
1022
+
1023
+ card.innerHTML = `
1024
+ <div class="comp-repo-name">${escapeHtml(c.repo_name)}</div>
1025
+ <div class="comp-section-title strengths">${t.strengths_label}</div>
1026
+ ${strengthsHtml}
1027
+ <div class="comp-section-title weaknesses">${t.weaknesses_label}</div>
1028
+ ${weaknessesHtml}
1029
+ `;
1030
+ compGrid.appendChild(card);
1031
+ });
1032
+
1033
+ // Unmet Market Needs
1034
+ const unmetContainer = document.getElementById('unmet-list');
1035
+ unmetContainer.innerHTML = report.unmet_needs.map(g => `
1036
+ <div class="list-item-clean" style="background: rgba(244, 63, 94, 0.06); padding: 0.6rem 0.8rem; border-radius: 8px; border: 1px solid rgba(244, 63, 94, 0.15);">
1037
+ <svg class="list-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-rose)" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
1038
+ <span style="color: var(--text-primary); font-size: 0.875rem;">${escapeHtml(g)}</span>
1039
+ </div>
1040
+ `).join('');
1041
+
1042
+ // Key Differentiators
1043
+ const diffContainer = document.getElementById('diff-list');
1044
+ diffContainer.innerHTML = report.differentiators.map(d => `
1045
+ <div class="list-item-clean" style="background: rgba(16, 185, 129, 0.06); padding: 0.6rem 0.8rem; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.15);">
1046
+ <svg class="list-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--accent-emerald)" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
1047
+ <span style="color: var(--text-primary); font-size: 0.875rem;">${escapeHtml(d)}</span>
1048
+ </div>
1049
+ `).join('');
1050
+
1051
+ // Recommendations
1052
+ const recContainer = document.getElementById('rec-list');
1053
+ recContainer.innerHTML = report.actionable_recommendations.map((r, idx) => `
1054
+ <div class="recommendation-card">
1055
+ <div class="rec-number">${idx + 1}</div>
1056
+ <div class="rec-text">${escapeHtml(r)}</div>
1057
+ </div>
1058
+ `).join('');
1059
+
1060
+ // Technical Implementation Guide
1061
+ const implSection = document.getElementById('implementation-section');
1062
+ if (report.implementation_guide) {
1063
+ const guide = report.implementation_guide;
1064
+
1065
+ // Tech Stack Badges
1066
+ const stackBadges = (guide.recommended_tech_stack || [])
1067
+ .map(t => `<span class="badge-code" style="border-color: rgba(99, 102, 241, 0.3); color: var(--accent-indigo);">${escapeHtml(t)}</span>`)
1068
+ .join(' ');
1069
+ document.getElementById('tech-stack-badges').innerHTML = stackBadges || '<span style="color: var(--text-muted); font-size: 0.85rem;">N/A</span>';
1070
+
1071
+ // Architecture Overview
1072
+ document.getElementById('arch-overview-text').textContent = guide.architecture_overview || '';
1073
+
1074
+ // Open Source Building Blocks
1075
+ const toolsList = document.getElementById('open-source-tools-list');
1076
+ toolsList.innerHTML = '';
1077
+ (guide.open_source_building_blocks || []).forEach(tool => {
1078
+ const card = document.createElement('div');
1079
+ card.className = 'comp-card';
1080
+ card.style.borderColor = 'rgba(99, 102, 241, 0.2)';
1081
+
1082
+ const linkHtml = tool.repo_url
1083
+ ? `<a href="${escapeHtml(tool.repo_url)}" target="_blank" rel="noopener noreferrer" style="color: var(--accent-indigo); font-size: 0.8rem; font-family: var(--font-mono); text-decoration: none;"> 🔗 GitHub</a>`
1084
+ : '';
1085
+
1086
+ card.innerHTML = `
1087
+ <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.4rem;">
1088
+ <div style="font-weight: 700; color: var(--text-primary); font-size: 0.95rem;">${escapeHtml(tool.name)} ${linkHtml}</div>
1089
+ <span class="badge-topic" style="font-size: 0.7rem; padding: 0.15rem 0.4rem;">${escapeHtml(tool.category)}</span>
1090
+ </div>
1091
+ <p style="color: var(--text-secondary); font-size: 0.85rem; line-height: 1.4;">${escapeHtml(tool.description_and_usage)}</p>
1092
+ `;
1093
+ toolsList.appendChild(card);
1094
+ });
1095
+
1096
+ implSection.classList.remove('hidden');
1097
+ } else {
1098
+ implSection.classList.add('hidden');
1099
+ }
1100
+
1101
+ document.getElementById('strategy-panel').classList.remove('hidden');
1102
+ document.querySelectorAll('.metrics-grid').forEach(el => el.classList.remove('hidden'));
1103
+ results.classList.remove('hidden');
1104
+ }
1105
+
1106
+ function renderSearchOnlyResults(repos) {
1107
+ document.getElementById('strategy-panel').classList.add('hidden');
1108
+ document.getElementById('implementation-section').classList.add('hidden');
1109
+ document.querySelectorAll('.metrics-grid').forEach(el => el.classList.add('hidden'));
1110
+ renderTable(repos);
1111
+ results.classList.remove('hidden');
1112
+ }
1113
+
1114
+ function renderTable(repos) {
1115
+ const tbody = document.getElementById('repos-table-body');
1116
+ tbody.innerHTML = '';
1117
+ repos.forEach((r, idx) => {
1118
+ const tr = document.createElement('tr');
1119
+ const langColor = getLanguageColor(r.language);
1120
+
1121
+ let relBadge = '<span style="color: var(--text-muted); font-size: 0.8rem;">N/A</span>';
1122
+ if (r.relevance_score !== null && r.relevance_score !== undefined) {
1123
+ let cls = 'badge-relevance-low';
1124
+ if (r.relevance_score >= 80) cls = 'badge-relevance-high';
1125
+ else if (r.relevance_score >= 50) cls = 'badge-relevance-mid';
1126
+ relBadge = `<span class="badge-relevance ${cls}" title="${escapeHtml(r.relevance_reason || '')}">🎯 ${r.relevance_score}%</span>`;
1127
+ }
1128
+
1129
+ let descText = escapeHtml(r.description || '');
1130
+ if (r.relevance_reason) {
1131
+ descText += `<div style="font-size: 0.78rem; color: var(--text-muted); font-style: italic; margin-top: 0.2rem;">Fit: ${escapeHtml(r.relevance_reason)}</div>`;
1132
+ }
1133
+
1134
+ tr.innerHTML = `
1135
+ <td style="font-family: var(--font-mono); color: var(--text-muted); width: 40px;">${idx + 1}</td>
1136
+ <td>
1137
+ <a class="repo-link" href="${escapeHtml(r.html_url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(r.full_name)}</a>
1138
+ </td>
1139
+ <td>${relBadge}</td>
1140
+ <td class="stat-cell">
1141
+ <span style="color: var(--accent-amber); display: inline-flex; align-items: center; gap: 0.3rem;">
1142
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
1143
+ ${(r.stars || 0).toLocaleString()}
1144
+ </span>
1145
+ </td>
1146
+ <td class="stat-cell">
1147
+ <span style="color: var(--text-secondary); display: inline-flex; align-items: center; gap: 0.3rem;">
1148
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><circle cx="18" cy="6" r="3"></circle><path d="M18 9v2a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9"></path><path d="M12 12v3"></path></svg>
1149
+ ${(r.forks || 0).toLocaleString()}
1150
+ </span>
1151
+ </td>
1152
+ <td>
1153
+ <span class="language-dot" style="background-color: ${langColor};"></span>
1154
+ <span>${escapeHtml(r.language || 'Unknown')}</span>
1155
+ </td>
1156
+ <td style="font-family: var(--font-mono); font-size: 0.8rem;">${escapeHtml(r.updated_at || '-')}</td>
1157
+ <td style="color: var(--text-secondary); font-size: 0.85rem; max-width: 320px;">${descText}</td>
1158
+ `;
1159
+ tbody.appendChild(tr);
1160
+ });
1161
+ }
1162
+
1163
+ function getLanguageColor(lang) {
1164
+ const colors = {
1165
+ 'Python': '#3572A5',
1166
+ 'Rust': '#dea584',
1167
+ 'Go': '#00ADD8',
1168
+ 'TypeScript': '#3178c6',
1169
+ 'JavaScript': '#f1e05a',
1170
+ 'C++': '#f34b7d',
1171
+ 'C': '#555555',
1172
+ 'Shell': '#89e051',
1173
+ 'HTML': '#e34c26',
1174
+ 'CSS': '#563d7c',
1175
+ };
1176
+ return colors[lang] || '#94a3b8';
1177
+ }
1178
+
1179
+ function escapeHtml(str) {
1180
+ if (!str) return '';
1181
+ return String(str)
1182
+ .replace(/&/g, '&amp;')
1183
+ .replace(/</g, '&lt;')
1184
+ .replace(/>/g, '&gt;')
1185
+ .replace(/"/g, '&quot;')
1186
+ .replace(/'/g, '&#039;');
1187
+ }
1188
+
1189
+ function showLoading(msg) {
1190
+ document.getElementById('loading-status').textContent = msg;
1191
+ loading.style.display = 'block';
1192
+ }
1193
+
1194
+ function hideLoading() {
1195
+ loading.style.display = 'none';
1196
+ }
1197
+
1198
+ function showError(msg) {
1199
+ errorMessage.textContent = msg;
1200
+ errorBox.classList.remove('hidden');
1201
+ }
1202
+
1203
+ function hideError() {
1204
+ errorBox.classList.add('hidden');
1205
+ }
1206
+ });