mirror of
https://github.com/mblanke/ThreatHunt.git
synced 2026-03-01 05:50:21 -05:00
Add ThreatHunt agent backend/frontend scaffolding
This commit is contained in:
17164
frontend/package-lock.json
generated
Normal file
17164
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
frontend/package.json
Normal file
39
frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "threathunt-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
}
|
||||
17
frontend/public/index.html
Normal file
17
frontend/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1976d2" />
|
||||
<meta
|
||||
name="description"
|
||||
content="ThreatHunt - Analyst-assist threat hunting platform with agent guidance"
|
||||
/>
|
||||
<title>ThreatHunt - Threat Hunting with Agent Assistance</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
224
frontend/src/App.css
Normal file
224
frontend/src/App.css
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Main application styles.
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.app-header {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
|
||||
color: white;
|
||||
padding: 24px 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
.app-main {
|
||||
flex: 1;
|
||||
padding: 24px 32px;
|
||||
max-width: 1400px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 420px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.main-panel h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.data-view {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.sample-data {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sample-data thead {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.sample-data th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.sample-data td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.sample-data tbody tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* Agent sidebar */
|
||||
.agent-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.app-footer {
|
||||
background: #f9f9f9;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 32px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto 32px;
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.footer-section p {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.footer-section ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer-section li {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.footer-section li:before {
|
||||
content: "• ";
|
||||
color: #1976d2;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.app-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sample-data {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sample-data th,
|
||||
.sample-data td {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
124
frontend/src/App.tsx
Normal file
124
frontend/src/App.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Main ThreatHunt application entry point.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import "./App.css";
|
||||
import AgentPanel from "./components/AgentPanel";
|
||||
|
||||
function App() {
|
||||
// Sample state for demonstration
|
||||
const [currentDataset] = useState("FileList-2025-12-26");
|
||||
const [currentHost] = useState("DESKTOP-ABC123");
|
||||
const [currentArtifact] = useState("FileList");
|
||||
const [dataDescription] = useState(
|
||||
"File listing from system scan showing recent modifications"
|
||||
);
|
||||
|
||||
const handleAnalysisAction = (action: string) => {
|
||||
console.log("Analysis action triggered:", action);
|
||||
// In a real app, this would update the analysis view or apply filters
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>ThreatHunt - Analyst-Assist Platform</h1>
|
||||
<p className="subtitle">
|
||||
Powered by agent guidance for faster threat hunting
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main className="app-main">
|
||||
<div className="app-content">
|
||||
<section className="main-panel">
|
||||
<h2>Analysis Dashboard</h2>
|
||||
<p className="placeholder-text">
|
||||
[Main analysis interface would display here]
|
||||
</p>
|
||||
<div className="data-view">
|
||||
<table className="sample-data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File</th>
|
||||
<th>Modified</th>
|
||||
<th>Size</th>
|
||||
<th>Hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>System32\drivers\etc\hosts</td>
|
||||
<td>2025-12-20 14:32</td>
|
||||
<td>456 B</td>
|
||||
<td>d41d8cd98f00b204...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows\Temp\cache.bin</td>
|
||||
<td>2025-12-26 09:15</td>
|
||||
<td>2.3 MB</td>
|
||||
<td>5d41402abc4b2a76...</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Users\Admin\AppData\Roaming\config.xml</td>
|
||||
<td>2025-12-25 16:45</td>
|
||||
<td>12.4 KB</td>
|
||||
<td>e99a18c428cb38d5...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="agent-sidebar">
|
||||
<AgentPanel
|
||||
dataset_name={currentDataset}
|
||||
artifact_type={currentArtifact}
|
||||
host_identifier={currentHost}
|
||||
data_summary={dataDescription}
|
||||
onAnalysisAction={handleAnalysisAction}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="app-footer">
|
||||
<div className="footer-content">
|
||||
<div className="footer-section">
|
||||
<h4>About Analyst-Assist Agent</h4>
|
||||
<p>
|
||||
The agent provides advisory guidance on artifact data, analytical
|
||||
pivots, and hypotheses. All decisions remain with the analyst.
|
||||
</p>
|
||||
</div>
|
||||
<div className="footer-section">
|
||||
<h4>Capabilities</h4>
|
||||
<ul>
|
||||
<li>Interpret CSV artifact data</li>
|
||||
<li>Suggest analytical directions</li>
|
||||
<li>Highlight anomalies</li>
|
||||
<li>Propose investigative steps</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="footer-section">
|
||||
<h4>Governance</h4>
|
||||
<ul>
|
||||
<li>Read-only guidance</li>
|
||||
<li>No tool execution</li>
|
||||
<li>No autonomous actions</li>
|
||||
<li>Analyst controls decisions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<p>
|
||||
© 2025 ThreatHunt. Agent guidance is advisory only. All
|
||||
analytical decisions remain with the analyst.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
373
frontend/src/components/AgentPanel.css
Normal file
373
frontend/src/components/AgentPanel.css
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Styles for analyst-assist agent chat panel.
|
||||
*/
|
||||
|
||||
.agent-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.agent-panel-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.agent-panel-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.agent-context {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.context-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #1976d2;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.agent-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-welcome {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-welcome ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 12px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.agent-welcome li {
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.agent-welcome li:before {
|
||||
content: "✓ ";
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.welcome-note {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 16px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.user {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #1976d2;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.message.agent {
|
||||
background: #f5f5f5;
|
||||
border-left: 4px solid #757575;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.message.loading {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #ffebee;
|
||||
border-left: 4px solid #d32f2f;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-role {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-details {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-section h5 {
|
||||
margin: 0 0 6px 0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-section li {
|
||||
padding: 4px 0;
|
||||
margin-left: 8px;
|
||||
border-left: 2px solid #ccc;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.pivot-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #1976d2;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.pivot-button:hover {
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.detail-section code {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.detail-section.caveats {
|
||||
background: #fffde7;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #fbc02d;
|
||||
}
|
||||
|
||||
.detail-section.caveats p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.confidence {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #d32f2f;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #999;
|
||||
animation: bounce 1.4s infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Input form */
|
||||
.agent-input-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.agent-input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.agent-input:focus {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.agent-input:disabled {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.agent-input-form button {
|
||||
padding: 10px 20px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.agent-input-form button:hover:not(:disabled) {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
.agent-input-form button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.agent-footer {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background: #f9f9f9;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.message {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.message.agent {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.agent-panel-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.agent-messages {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.agent-input-form {
|
||||
padding: 10px;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
264
frontend/src/components/AgentPanel.tsx
Normal file
264
frontend/src/components/AgentPanel.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Analyst-assist agent chat panel component.
|
||||
* Provides context-aware guidance on artifact data and analysis.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import "./AgentPanel.css";
|
||||
import {
|
||||
requestAgentAssistance,
|
||||
AssistResponse,
|
||||
AssistRequest,
|
||||
} from "../utils/agentApi";
|
||||
|
||||
export interface AgentPanelProps {
|
||||
/** Name of the current dataset */
|
||||
dataset_name?: string;
|
||||
/** Type of artifact (e.g., FileList, ProcessList) */
|
||||
artifact_type?: string;
|
||||
/** Host name, IP, or identifier */
|
||||
host_identifier?: string;
|
||||
/** Summary of the uploaded data */
|
||||
data_summary?: string;
|
||||
/** Callback when user needs to execute analysis based on suggestions */
|
||||
onAnalysisAction?: (action: string) => void;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: "user" | "agent";
|
||||
content: string;
|
||||
response?: AssistResponse;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const AgentPanel: React.FC<AgentPanelProps> = ({
|
||||
dataset_name,
|
||||
artifact_type,
|
||||
host_identifier,
|
||||
data_summary,
|
||||
onAnalysisAction,
|
||||
}) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [query, setQuery] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!query.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message
|
||||
const userMessage: Message = {
|
||||
role: "user",
|
||||
content: query,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setQuery("");
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Build conversation history for context
|
||||
const conversation_history = messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
// Request guidance from agent
|
||||
const response = await requestAgentAssistance({
|
||||
query: query,
|
||||
dataset_name,
|
||||
artifact_type,
|
||||
host_identifier,
|
||||
data_summary,
|
||||
conversation_history,
|
||||
});
|
||||
|
||||
// Add agent response
|
||||
const agentMessage: Message = {
|
||||
role: "agent",
|
||||
content: response.guidance,
|
||||
response,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, agentMessage]);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to get guidance";
|
||||
setError(errorMessage);
|
||||
|
||||
// Add error message
|
||||
const errorMsg: Message = {
|
||||
role: "agent",
|
||||
content: `Error: ${errorMessage}. The agent service may be unavailable.`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, errorMsg]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="agent-panel">
|
||||
<div className="agent-panel-header">
|
||||
<h3>Analyst Assist Agent</h3>
|
||||
<div className="agent-context">
|
||||
{host_identifier && (
|
||||
<span className="context-badge">Host: {host_identifier}</span>
|
||||
)}
|
||||
{artifact_type && (
|
||||
<span className="context-badge">Artifact: {artifact_type}</span>
|
||||
)}
|
||||
{dataset_name && (
|
||||
<span className="context-badge">Dataset: {dataset_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-messages">
|
||||
{messages.length === 0 ? (
|
||||
<div className="agent-welcome">
|
||||
<p className="welcome-title">Welcome to Analyst Assist</p>
|
||||
<p className="welcome-text">
|
||||
Ask questions about your artifact data. I can help you:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Interpret and explain data patterns</li>
|
||||
<li>Suggest analytical pivots and filters</li>
|
||||
<li>Help form and test hypotheses</li>
|
||||
<li>Highlight anomalies and points of interest</li>
|
||||
</ul>
|
||||
<p className="welcome-note">
|
||||
💡 This agent provides guidance only. All analytical decisions
|
||||
remain with you.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg, idx) => (
|
||||
<div key={idx} className={`message ${msg.role}`}>
|
||||
<div className="message-header">
|
||||
<span className="message-role">
|
||||
{msg.role === "user" ? "You" : "Agent"}
|
||||
</span>
|
||||
<span className="message-time">
|
||||
{msg.timestamp.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="message-content">{msg.content}</div>
|
||||
|
||||
{msg.response && (
|
||||
<div className="message-details">
|
||||
{msg.response.suggested_pivots.length > 0 && (
|
||||
<div className="detail-section">
|
||||
<h5>Suggested Pivots:</h5>
|
||||
<ul>
|
||||
{msg.response.suggested_pivots.map((pivot, i) => (
|
||||
<li key={i}>
|
||||
<button
|
||||
className="pivot-button"
|
||||
onClick={() =>
|
||||
onAnalysisAction && onAnalysisAction(pivot)
|
||||
}
|
||||
>
|
||||
{pivot}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.response.suggested_filters.length > 0 && (
|
||||
<div className="detail-section">
|
||||
<h5>Suggested Filters:</h5>
|
||||
<ul>
|
||||
{msg.response.suggested_filters.map((filter, i) => (
|
||||
<li key={i}>
|
||||
<code>{filter}</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.response.caveats && (
|
||||
<div className="detail-section caveats">
|
||||
<h5>⚠️ Caveats:</h5>
|
||||
<p>{msg.response.caveats}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg.response.confidence && (
|
||||
<div className="detail-section">
|
||||
<span className="confidence">
|
||||
Confidence: {(msg.response.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="message agent loading">
|
||||
<div className="loading-indicator">
|
||||
<span className="dot"></span>
|
||||
<span className="dot"></span>
|
||||
<span className="dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="message agent error">
|
||||
<p className="error-text">⚠️ {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="agent-input-form">
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Ask about your data, patterns, or next steps..."
|
||||
disabled={loading}
|
||||
className="agent-input"
|
||||
/>
|
||||
<button type="submit" disabled={loading || !query.trim()}>
|
||||
{loading ? "Thinking..." : "Ask"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="agent-footer">
|
||||
<p className="footer-note">
|
||||
ℹ️ Agent provides guidance only. All decisions remain with the analyst.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentPanel;
|
||||
29
frontend/src/index.css
Normal file
29
frontend/src/index.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* CSS Modules and style definitions for components.
|
||||
*/
|
||||
|
||||
/* Ensure consistent styling across all components */
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
13
frontend/src/index.tsx
Normal file
13
frontend/src/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
65
frontend/src/utils/agentApi.ts
Normal file
65
frontend/src/utils/agentApi.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* API utility functions for agent communication.
|
||||
*/
|
||||
|
||||
export interface AssistRequest {
|
||||
query: string;
|
||||
dataset_name?: string;
|
||||
artifact_type?: string;
|
||||
host_identifier?: string;
|
||||
data_summary?: string;
|
||||
conversation_history?: Array<{ role: string; content: string }>;
|
||||
}
|
||||
|
||||
export interface AssistResponse {
|
||||
guidance: string;
|
||||
confidence: number;
|
||||
suggested_pivots: string[];
|
||||
suggested_filters: string[];
|
||||
caveats?: string;
|
||||
reasoning?: string;
|
||||
}
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:8000";
|
||||
|
||||
/**
|
||||
* Request guidance from the analyst-assist agent.
|
||||
*/
|
||||
export async function requestAgentAssistance(
|
||||
request: AssistRequest
|
||||
): Promise<AssistResponse> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agent/assist`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Agent request failed: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if agent is available.
|
||||
*/
|
||||
export async function checkAgentHealth(): Promise<{
|
||||
status: string;
|
||||
provider?: string;
|
||||
configured_providers?: Record<string, boolean>;
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agent/health`);
|
||||
if (!response.ok) {
|
||||
return { status: "error" };
|
||||
}
|
||||
return response.json();
|
||||
} catch {
|
||||
return { status: "offline" };
|
||||
}
|
||||
}
|
||||
22
frontend/tsconfig.json
Normal file
22
frontend/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* TypeScript configuration.
|
||||
*/
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user