feat(discovery): enhance discovery index reading and issue exporting

- Improved the reading of the discovery index by adding a fallback mechanism to scan directories for discovery folders if the index.json is invalid or missing.
- Added sorting of discoveries by creation time in descending order.
- Enhanced the `appendToIssuesJsonl` function to include deduplication logic for issues based on ID and source finding ID.
- Updated the discovery route handler to reflect the number of issues added and skipped during export.
- Introduced UI elements for selecting and deselecting findings in the dashboard.
- Added CSS styles for exported findings and action buttons.
- Implemented search functionality for filtering findings based on title, file, and description.
- Added internationalization support for new UI elements.
- Created scripts for automated API extraction from various project types, including FastAPI and TypeScript.
- Documented the API extraction process and library bundling instructions.
This commit is contained in:
catlog22
2025-12-28 19:27:34 +08:00
parent 3ef1e54412
commit 169f218f7a
18 changed files with 1602 additions and 612 deletions

View File

@@ -358,17 +358,52 @@
}
.findings-count {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
border-bottom: 1px solid hsl(var(--border));
}
.findings-count-left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.findings-count .selected-count {
color: hsl(var(--primary));
font-weight: 500;
}
.findings-count-actions {
display: flex;
gap: 0.5rem;
}
.select-action-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
background: transparent;
border: 1px solid hsl(var(--border));
cursor: pointer;
transition: all 0.15s ease;
}
.select-action-btn:hover {
color: hsl(var(--foreground));
background: hsl(var(--muted));
border-color: hsl(var(--primary) / 0.3);
}
/* Findings List */
.findings-list {
flex: 1;
@@ -413,6 +448,35 @@
opacity: 0.5;
}
.finding-item.exported {
opacity: 0.6;
background: hsl(var(--success) / 0.05);
border: 1px solid hsl(var(--success) / 0.2);
}
.finding-item.exported:hover {
background: hsl(var(--success) / 0.08);
}
/* Exported Badge */
.exported-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
background: hsl(var(--success) / 0.1);
color: hsl(var(--success));
}
.finding-checkbox input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.finding-checkbox {
display: flex;
align-items: flex-start;

View File

@@ -28,6 +28,8 @@ const i18n = {
'common.deleteFailed': 'Delete failed',
'common.retry': 'Retry',
'common.refresh': 'Refresh',
'common.back': 'Back',
'common.search': 'Search...',
'common.minutes': 'minutes',
'common.enabled': 'Enabled',
'common.disabled': 'Disabled',
@@ -1783,6 +1785,8 @@ const i18n = {
'discovery.impact': 'Impact',
'discovery.recommendation': 'Recommendation',
'discovery.exportAsIssues': 'Export as Issues',
'discovery.selectAll': 'Select All',
'discovery.deselectAll': 'Deselect All',
'discovery.deleteSession': 'Delete Session',
'discovery.confirmDelete': 'Are you sure you want to delete this discovery session?',
'discovery.deleted': 'Discovery session deleted',
@@ -1944,6 +1948,8 @@ const i18n = {
'common.deleteFailed': '删除失败',
'common.retry': '重试',
'common.refresh': '刷新',
'common.back': '返回',
'common.search': '搜索...',
'common.minutes': '分钟',
'common.enabled': '已启用',
'common.disabled': '已禁用',
@@ -3527,6 +3533,8 @@ const i18n = {
'common.edit': '编辑',
'common.close': '关闭',
'common.refresh': '刷新',
'common.back': '返回',
'common.search': '搜索...',
'common.refreshed': '已刷新',
'common.refreshing': '刷新中...',
'common.loading': '加载中...',
@@ -3708,6 +3716,8 @@ const i18n = {
'discovery.impact': '影响',
'discovery.recommendation': '建议',
'discovery.exportAsIssues': '导出为议题',
'discovery.selectAll': '全选',
'discovery.deselectAll': '取消全选',
'discovery.deleteSession': '删除会话',
'discovery.confirmDelete': '确定要删除此发现会话吗?',
'discovery.deleted': '发现会话已删除',

View File

@@ -18,6 +18,28 @@ var discoveryData = {
var discoveryLoading = false;
var discoveryPollingInterval = null;
// ========== Helper Functions ==========
function getFilteredFindings() {
const findings = discoveryData.findings || [];
let filtered = findings;
if (discoveryData.perspectiveFilter !== 'all') {
filtered = filtered.filter(f => f.perspective === discoveryData.perspectiveFilter);
}
if (discoveryData.priorityFilter !== 'all') {
filtered = filtered.filter(f => f.priority === discoveryData.priorityFilter);
}
if (discoveryData.searchQuery) {
const q = discoveryData.searchQuery.toLowerCase();
filtered = filtered.filter(f =>
(f.title && f.title.toLowerCase().includes(q)) ||
(f.file && f.file.toLowerCase().includes(q)) ||
(f.description && f.description.toLowerCase().includes(q))
);
}
return filtered;
}
// ========== Main Render Function ==========
async function renderIssueDiscovery() {
const container = document.getElementById('mainContent');
@@ -258,23 +280,7 @@ function renderDiscoveryDetailSection() {
const findings = discoveryData.findings || [];
const perspectives = [...new Set(findings.map(f => f.perspective))];
// Filter findings
let filteredFindings = findings;
if (discoveryData.perspectiveFilter !== 'all') {
filteredFindings = filteredFindings.filter(f => f.perspective === discoveryData.perspectiveFilter);
}
if (discoveryData.priorityFilter !== 'all') {
filteredFindings = filteredFindings.filter(f => f.priority === discoveryData.priorityFilter);
}
if (discoveryData.searchQuery) {
const q = discoveryData.searchQuery.toLowerCase();
filteredFindings = filteredFindings.filter(f =>
(f.title && f.title.toLowerCase().includes(q)) ||
(f.file && f.file.toLowerCase().includes(q)) ||
(f.description && f.description.toLowerCase().includes(q))
);
}
const filteredFindings = getFilteredFindings();
return `
<div class="discovery-detail-container">
@@ -305,10 +311,22 @@ function renderDiscoveryDetailSection() {
<!-- Findings Count -->
<div class="findings-count">
<span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
${discoveryData.selectedFindings.size > 0 ? `
<span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
` : ''}
<div class="findings-count-left">
<span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
${discoveryData.selectedFindings.size > 0 ? `
<span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
` : ''}
</div>
<div class="findings-count-actions">
<button class="select-action-btn" onclick="selectAllFindings()">
<i data-lucide="check-square" class="w-3 h-3"></i>
<span>${t('discovery.selectAll') || 'Select All'}</span>
</button>
<button class="select-action-btn" onclick="deselectAllFindings()">
<i data-lucide="square" class="w-3 h-3"></i>
<span>${t('discovery.deselectAll') || 'Deselect All'}</span>
</button>
</div>
</div>
<!-- Findings List -->
@@ -353,17 +371,19 @@ function renderDiscoveryDetailSection() {
function renderFindingItem(finding) {
const isSelected = discoveryData.selectedFindings.has(finding.id);
const isActive = discoveryData.selectedFinding?.id === finding.id;
const isExported = finding.exported === true;
return `
<div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''}"
<div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''} ${isExported ? 'exported' : ''}"
onclick="selectFinding('${finding.id}')">
<div class="finding-checkbox" onclick="event.stopPropagation(); toggleFindingSelection('${finding.id}')">
<input type="checkbox" ${isSelected ? 'checked' : ''}>
<input type="checkbox" ${isSelected ? 'checked' : ''} ${isExported ? 'disabled' : ''}>
</div>
<div class="finding-content">
<div class="finding-header">
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
${isExported ? '<span class="exported-badge">' + (t('discovery.exported') || 'Exported') + '</span>' : ''}
</div>
<div class="finding-title">${finding.title || 'Untitled'}</div>
<div class="finding-location">
@@ -509,6 +529,23 @@ function toggleFindingSelection(findingId) {
renderDiscoveryView();
}
function selectAllFindings() {
// Get filtered findings (respecting current filters)
const filteredFindings = getFilteredFindings();
// Select only non-exported findings
for (const finding of filteredFindings) {
if (!finding.exported) {
discoveryData.selectedFindings.add(finding.id);
}
}
renderDiscoveryView();
}
function deselectAllFindings() {
discoveryData.selectedFindings.clear();
renderDiscoveryView();
}
function filterDiscoveryByPerspective(perspective) {
discoveryData.perspectiveFilter = perspective;
renderDiscoveryView();
@@ -539,11 +576,19 @@ async function exportSelectedFindings() {
const result = await response.json();
if (result.success) {
showNotification('success', `Exported ${result.exported_count} issues`);
// Show detailed message if duplicates were skipped
const msg = result.skipped_count > 0
? `Exported ${result.exported_count} issues, skipped ${result.skipped_count} duplicates`
: `Exported ${result.exported_count} issues`;
showNotification('success', msg);
discoveryData.selectedFindings.clear();
// Reload discovery data
// Reload discovery data to reflect exported status
await loadDiscoveryData();
renderDiscoveryView();
if (discoveryData.selectedDiscovery) {
await viewDiscoveryDetail(discoveryData.selectedDiscovery.discovery_id);
} else {
renderDiscoveryView();
}
} else {
showNotification('error', result.error || 'Export failed');
}