cesjavi commited on
Commit
a6159a5
·
1 Parent(s): 2257631

UI: Integrated Evidence & Entity Intelligence view (Phase 6)

Browse files
frontend/src/components/EvidenceView.tsx ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Database, Search, ExternalLink, ShieldCheck, Filter } from 'lucide-react';
3
+ import { getApiUrl } from '../services/runtimeConfig';
4
+
5
+ interface Claim {
6
+ id: string;
7
+ claim_text: string;
8
+ entity_name: string | null;
9
+ source_url: string | null;
10
+ confidence: string;
11
+ merged_count?: number;
12
+ }
13
+
14
+ interface EvidenceSummary {
15
+ claim_count: number;
16
+ sourced_claim_count: number;
17
+ unsourced_claim_count: number;
18
+ source_coverage: number;
19
+ by_entity: Record<string, number>;
20
+ }
21
+
22
+ interface EvidenceData {
23
+ project_id: string;
24
+ merged: boolean;
25
+ summary: EvidenceSummary;
26
+ claims: Claim[];
27
+ }
28
+
29
+ interface EvidenceViewProps {
30
+ projectId: string;
31
+ }
32
+
33
+ const EvidenceView: React.FC<EvidenceViewProps> = ({ projectId }) => {
34
+ const [data, setData] = useState<EvidenceData | null>(null);
35
+ const [loading, setLoading] = useState(true);
36
+ const [error, setError] = useState<string | null>(null);
37
+ const [mergeEnabled, setMergeEnabled] = useState(true);
38
+
39
+ const loadEvidence = async () => {
40
+ setLoading(true);
41
+ setError(null);
42
+ try {
43
+ const apiUrl = getApiUrl();
44
+ const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/evidence?merge=${mergeEnabled}`);
45
+ if (!response.ok) throw new Error('Failed to fetch evidence data');
46
+ const result = await response.json();
47
+ setData(result);
48
+ } catch (err) {
49
+ setError(err instanceof Error ? err.message : 'Unknown error');
50
+ } finally {
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ useEffect(() => {
56
+ loadEvidence();
57
+ }, [projectId, mergeEnabled]);
58
+
59
+ if (loading && !data) {
60
+ return (
61
+ <div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center' }}>
62
+ <div className="loading-spinner" style={{ margin: '0 auto var(--space-md)' }} />
63
+ <p style={{ color: 'var(--text-dim)' }}>Analyzing project evidence...</p>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (error) {
69
+ return (
70
+ <div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center', borderColor: 'var(--danger)' }}>
71
+ <p style={{ color: 'var(--danger)' }}>Error: {error}</p>
72
+ <button className="btn btn-glass" onClick={loadEvidence} style={{ marginTop: 'var(--space-md)' }}>
73
+ Retry
74
+ </button>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ const summary = data?.summary;
80
+ const claims = data?.claims || [];
81
+
82
+ return (
83
+ <div className="evidence-view">
84
+ {/* Stats Summary */}
85
+ <div className="evidence-stats-grid">
86
+ <div className="glass-panel stat-card">
87
+ <Database size={20} color="var(--accent)" />
88
+ <div className="stat-content">
89
+ <span className="stat-label">Total Claims</span>
90
+ <span className="stat-value">{summary?.claim_count || 0}</span>
91
+ </div>
92
+ </div>
93
+ <div className="glass-panel stat-card">
94
+ <ShieldCheck size={20} color="var(--success)" />
95
+ <div className="stat-content">
96
+ <span className="stat-label">Source Coverage</span>
97
+ <span className="stat-value">{(summary?.source_coverage || 0 * 100).toFixed(0)}%</span>
98
+ </div>
99
+ </div>
100
+ <div className="glass-panel stat-card">
101
+ <Search size={20} color="var(--info)" />
102
+ <div className="stat-content">
103
+ <span className="stat-label">Entities</span>
104
+ <span className="stat-value">{Object.keys(summary?.by_entity || {}).length}</span>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ {/* Controls */}
110
+ <div className="evidence-controls">
111
+ <button
112
+ className={`btn ${mergeEnabled ? 'btn-primary' : 'btn-glass'}`}
113
+ onClick={() => setMergeEnabled(!mergeEnabled)}
114
+ style={{ gap: 'var(--space-xs)' }}
115
+ >
116
+ <Filter size={16} />
117
+ {mergeEnabled ? 'Semantic Merging Active' : 'Show All Raw Claims'}
118
+ </button>
119
+ </div>
120
+
121
+ {/* Claims List */}
122
+ <div className="claims-container">
123
+ {claims.length === 0 ? (
124
+ <div className="glass-panel" style={{ padding: 'var(--space-xl)', textAlign: 'center' }}>
125
+ <p style={{ color: 'var(--text-dim)' }}>No evidence claims have been extracted for this project yet.</p>
126
+ </div>
127
+ ) : (
128
+ <div className="claims-grid">
129
+ {claims.map((claim) => (
130
+ <div key={claim.id} className="glass-panel claim-card">
131
+ <div className="claim-header">
132
+ {claim.entity_name && (
133
+ <span className="entity-badge">{claim.entity_name}</span>
134
+ )}
135
+ <span className={`confidence-badge confidence-${claim.confidence.toLowerCase()}`}>
136
+ {claim.confidence} confidence
137
+ </span>
138
+ {claim.merged_count && claim.merged_count > 1 && (
139
+ <span className="merged-badge">x{claim.merged_count} verified</span>
140
+ )}
141
+ </div>
142
+ <p className="claim-text">{claim.claim_text}</p>
143
+ {claim.source_url && (
144
+ <div className="claim-source">
145
+ <ExternalLink size={14} />
146
+ <a href={claim.source_url} target="_blank" rel="noopener noreferrer">
147
+ {new URL(claim.source_url).hostname}
148
+ </a>
149
+ </div>
150
+ )}
151
+ </div>
152
+ ))}
153
+ </div>
154
+ )}
155
+ </div>
156
+
157
+ <style>{`
158
+ .evidence-view {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: var(--space-lg);
162
+ padding: var(--space-md) 0;
163
+ }
164
+ .evidence-stats-grid {
165
+ display: grid;
166
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
167
+ gap: var(--space-md);
168
+ }
169
+ .stat-card {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: var(--space-md);
173
+ padding: var(--space-md);
174
+ }
175
+ .stat-content {
176
+ display: flex;
177
+ flex-direction: column;
178
+ }
179
+ .stat-label {
180
+ font-size: 0.75rem;
181
+ color: var(--text-dim);
182
+ text-transform: uppercase;
183
+ letter-spacing: 0.5px;
184
+ }
185
+ .stat-value {
186
+ font-size: 1.25rem;
187
+ font-weight: 700;
188
+ color: var(--text-main);
189
+ }
190
+ .evidence-controls {
191
+ display: flex;
192
+ justify-content: flex-end;
193
+ }
194
+ .claims-grid {
195
+ display: grid;
196
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
197
+ gap: var(--space-md);
198
+ }
199
+ .claim-card {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: var(--space-sm);
203
+ padding: var(--space-md);
204
+ transition: transform 0.2s, border-color 0.2s;
205
+ }
206
+ .claim-card:hover {
207
+ transform: translateY(-2px);
208
+ border-color: var(--accent);
209
+ }
210
+ .claim-header {
211
+ display: flex;
212
+ flex-wrap: wrap;
213
+ gap: var(--space-xs);
214
+ align-items: center;
215
+ }
216
+ .entity-badge {
217
+ background: rgba(110, 89, 255, 0.15);
218
+ color: var(--accent);
219
+ padding: 2px 8px;
220
+ border-radius: 4px;
221
+ font-size: 0.7rem;
222
+ font-weight: 700;
223
+ text-transform: uppercase;
224
+ }
225
+ .confidence-badge {
226
+ font-size: 0.65rem;
227
+ padding: 2px 6px;
228
+ border-radius: 4px;
229
+ text-transform: capitalize;
230
+ }
231
+ .confidence-high { background: rgba(39, 174, 96, 0.15); color: #2ecc71; }
232
+ .confidence-medium { background: rgba(241, 196, 15, 0.15); color: #f1c40f; }
233
+ .confidence-low { background: rgba(231, 76, 60, 0.15); color: #e74c3c; }
234
+ .merged-badge {
235
+ background: rgba(52, 152, 219, 0.15);
236
+ color: #3498db;
237
+ padding: 2px 6px;
238
+ border-radius: 4px;
239
+ font-size: 0.65rem;
240
+ font-weight: 700;
241
+ }
242
+ .claim-text {
243
+ font-size: 0.95rem;
244
+ line-height: 1.5;
245
+ color: var(--text-main);
246
+ margin: 0;
247
+ }
248
+ .claim-source {
249
+ margin-top: auto;
250
+ display: flex;
251
+ align-items: center;
252
+ gap: var(--space-xs);
253
+ font-size: 0.8rem;
254
+ color: var(--text-dim);
255
+ }
256
+ .claim-source a {
257
+ color: var(--accent);
258
+ text-decoration: none;
259
+ }
260
+ .claim-source a:hover {
261
+ text-decoration: underline;
262
+ }
263
+ `}</style>
264
+ </div>
265
+ );
266
+ };
267
+
268
+ export default EvidenceView;
frontend/src/components/ProjectDetail.tsx CHANGED
@@ -6,6 +6,7 @@ import { useAuth } from '../context/useAuth';
6
  import { getDefaultModel, getDefaultProvider } from '../services/llmConfig';
7
  import { getApiUrl } from '../services/runtimeConfig';
8
  import type { UiMode } from '../services/uiMode';
 
9
 
10
  interface Project {
11
  id: string;
@@ -92,6 +93,7 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
92
  const [showRoadmap, setShowRoadmap] = useState(false);
93
  const [reportLoading, setReportLoading] = useState(false);
94
  const [pdfLoading, setPdfLoading] = useState(false);
 
95
  const defaultProvider = getDefaultProvider();
96
  const defaultModel = getDefaultModel(defaultProvider);
97
 
@@ -813,8 +815,29 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
813
  </section>
814
  )}
815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
816
  <div className="project-detail-grid">
817
- <section className="glass-panel project-form">
 
 
818
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
819
  <div className="default-agent-panel">
820
  <div>
@@ -1044,6 +1067,16 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, uiMode, initia
1044
  ))}
1045
  </div>
1046
  </section>
 
 
 
 
 
 
 
 
 
 
1047
  </div>
1048
 
1049
  {selectedTask && (
 
6
  import { getDefaultModel, getDefaultProvider } from '../services/llmConfig';
7
  import { getApiUrl } from '../services/runtimeConfig';
8
  import type { UiMode } from '../services/uiMode';
9
+ import EvidenceView from './EvidenceView';
10
 
11
  interface Project {
12
  id: string;
 
93
  const [showRoadmap, setShowRoadmap] = useState(false);
94
  const [reportLoading, setReportLoading] = useState(false);
95
  const [pdfLoading, setPdfLoading] = useState(false);
96
+ const [activeTab, setActiveTab] = useState<'tasks' | 'evidence'>('tasks');
97
  const defaultProvider = getDefaultProvider();
98
  const defaultModel = getDefaultModel(defaultProvider);
99
 
 
815
  </section>
816
  )}
817
 
818
+ <div className="tab-navigation glass-panel" style={{ marginBottom: 'var(--space-md)', padding: '4px', display: 'flex', gap: '4px' }}>
819
+ <button
820
+ className={`btn ${activeTab === 'tasks' ? 'btn-primary' : 'btn-glass'}`}
821
+ onClick={() => setActiveTab('tasks')}
822
+ style={{ flex: 1, justifyContent: 'center' }}
823
+ >
824
+ <ListTodo size={18} />
825
+ Tasks
826
+ </button>
827
+ <button
828
+ className={`btn ${activeTab === 'evidence' ? 'btn-primary' : 'btn-glass'}`}
829
+ onClick={() => setActiveTab('evidence')}
830
+ style={{ flex: 1, justifyContent: 'center' }}
831
+ >
832
+ <Database size={18} />
833
+ Evidence & Entities
834
+ </button>
835
+ </div>
836
+
837
  <div className="project-detail-grid">
838
+ {activeTab === 'tasks' ? (
839
+ <>
840
+ <section className="glass-panel project-form">
841
  {(uiMode === 'expert' || showAdvancedTaskControls) && (
842
  <div className="default-agent-panel">
843
  <div>
 
1067
  ))}
1068
  </div>
1069
  </section>
1070
+ </>
1071
+ ) : (
1072
+ <section className="glass-panel evidence-panel" style={{ gridColumn: '1 / -1' }}>
1073
+ <div className="settings-section-title">
1074
+ <Database size={22} color="var(--accent)" />
1075
+ <h3>Project Evidence & Entity Intelligence</h3>
1076
+ </div>
1077
+ <EvidenceView projectId={projectId} />
1078
+ </section>
1079
+ )}
1080
  </div>
1081
 
1082
  {selectedTask && (