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.
- demo/api/index.py +14 -0
- demo/gitradar/__init__.py +22 -0
- demo/gitradar/cli.py +214 -0
- demo/gitradar/config.py +117 -0
- demo/gitradar/models.py +67 -0
- demo/gitradar/prompts/__init__.py +18 -0
- demo/gitradar/services/github.py +179 -0
- demo/gitradar/services/llm.py +342 -0
- demo/gitradar/utils/ui.py +196 -0
- demo/gitradar/web/__init__.py +7 -0
- demo/gitradar/web/app.py +173 -0
- examples/python_sdk_usage.py +48 -0
- gitradar/__init__.py +22 -0
- gitradar/cli.py +214 -0
- gitradar/config.py +117 -0
- gitradar/models.py +67 -0
- gitradar/prompts/__init__.py +18 -0
- gitradar/prompts/gap_analysis_system.j2 +36 -0
- gitradar/prompts/gap_analysis_user.j2 +21 -0
- gitradar/prompts/query_expansion_system.j2 +19 -0
- gitradar/prompts/query_expansion_user.j2 +1 -0
- gitradar/prompts/relevance_evaluation_system.j2 +22 -0
- gitradar/prompts/relevance_evaluation_user.j2 +14 -0
- gitradar/services/github.py +179 -0
- gitradar/services/llm.py +342 -0
- gitradar/utils/ui.py +196 -0
- gitradar/web/__init__.py +7 -0
- gitradar/web/app.py +173 -0
- gitradar/web/static/apple-touch-icon.png +0 -0
- gitradar/web/static/css/style.css +1430 -0
- gitradar/web/static/favicon-96x96.png +0 -0
- gitradar/web/static/favicon.ico +0 -0
- gitradar/web/static/favicon.svg +1 -0
- gitradar/web/static/js/app.js +1206 -0
- gitradar/web/static/site.webmanifest +21 -0
- gitradar/web/static/web-app-manifest-192x192.png +0 -0
- gitradar/web/static/web-app-manifest-512x512.png +0 -0
- gitradar/web/templates/index.html +521 -0
- gitradar-0.1.5.dist-info/METADATA +251 -0
- gitradar-0.1.5.dist-info/RECORD +51 -0
- gitradar-0.1.5.dist-info/WHEEL +5 -0
- gitradar-0.1.5.dist-info/entry_points.txt +2 -0
- gitradar-0.1.5.dist-info/licenses/LICENSE +21 -0
- gitradar-0.1.5.dist-info/top_level.txt +4 -0
- tests/conftest.py +27 -0
- tests/test_cli.py +29 -0
- tests/test_llm.py +163 -0
- tests/test_models.py +90 -0
- tests/test_prompts.py +58 -0
- tests/test_relevance.py +75 -0
- 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, '&')
|
|
1183
|
+
.replace(/</g, '<')
|
|
1184
|
+
.replace(/>/g, '>')
|
|
1185
|
+
.replace(/"/g, '"')
|
|
1186
|
+
.replace(/'/g, ''');
|
|
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
|
+
});
|