mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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': '发现会话已删除',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user