blob: a9dd55039ff7fbe3d3dfc06768e7b3ce576a9261 (
plain) (
tree)
|
|
import React, { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
interface FileSummary {
id: string
name: string
description?: string
contract_phase?: string
}
interface TaskSummary {
id: string
name: string
status: string
is_supervisor?: boolean
is_red_team?: boolean
}
interface ContractRepository {
id: string
name: string
source_type: string
is_primary: boolean
}
interface Contract {
id: string
name: string
description?: string
contract_type: string
phase: string
status: string
version: number
created_at: string
red_team_enabled?: boolean
red_team_prompt?: string
}
interface RedTeamNotification {
id: string
contract_id: string
red_team_task_id: string
related_task_id?: string
message: string
severity: 'info' | 'warning' | 'critical'
file_path?: string
context?: string
delivered: boolean
delivered_at?: string
acknowledged: boolean
acknowledged_at?: string
created_at: string
}
interface ContractWithRelations {
contract: Contract
repositories: ContractRepository[]
files: FileSummary[]
tasks: TaskSummary[]
}
type Tab = 'overview' | 'files' | 'tasks' | 'repositories' | 'red-team'
export function ContractDetail() {
const { id } = useParams<{ id: string }>()
const [data, setData] = useState<ContractWithRelations | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<Tab>('overview')
const [notifications, setNotifications] = useState<RedTeamNotification[]>([])
const [notificationsLoading, setNotificationsLoading] = useState(false)
useEffect(() => {
async function fetchContract() {
if (!id) return
try {
setLoading(true)
const response = await fetch(`/api/v1/contracts/${id}`)
if (!response.ok) {
throw new Error(`Failed to fetch contract: ${response.statusText}`)
}
const contractData = await response.json()
setData(contractData)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}
fetchContract()
}, [id])
// Fetch red team notifications when viewing red-team tab
useEffect(() => {
async function fetchNotifications() {
if (!id || activeTab !== 'red-team' || !data?.contract.red_team_enabled) return
try {
setNotificationsLoading(true)
const response = await fetch(`/api/v1/contracts/${id}/red-team/notifications`)
if (response.ok) {
const notificationData = await response.json()
setNotifications(notificationData.notifications || [])
}
} catch (err) {
console.error('Failed to fetch red team notifications:', err)
} finally {
setNotificationsLoading(false)
}
}
fetchNotifications()
}, [id, activeTab, data?.contract.red_team_enabled])
if (loading) {
return (
<div className="contract-detail-container">
<div className="loading">Loading contract...</div>
</div>
)
}
if (error) {
return (
<div className="contract-detail-container">
<div className="error">Error: {error}</div>
<Link to="/contracts" className="back-link">
Back to Contracts
</Link>
</div>
)
}
if (!data) {
return (
<div className="contract-detail-container">
<div className="not-found">Contract not found</div>
<Link to="/contracts" className="back-link">
Back to Contracts
</Link>
</div>
)
}
const { contract, repositories, files, tasks } = data
return (
<div className="contract-detail-container">
<div className="contract-detail-header">
<Link to="/contracts" className="back-link">
Back to Contracts
</Link>
<h1 className="contract-title">
{contract.name}
{contract.red_team_enabled && (
<span className="red-team-badge" title="Red Team monitoring enabled">
🛡️ Red Team
</span>
)}
</h1>
{contract.description && (
<p className="contract-description">{contract.description}</p>
)}
<div className="contract-meta">
<span>Phase: {contract.phase}</span>
<span>Status: {contract.status}</span>
<span>Version: {contract.version}</span>
</div>
</div>
<div className="contract-tabs">
<button
className={`tab-button ${activeTab === 'overview' ? 'active' : ''}`}
onClick={() => setActiveTab('overview')}
>
Overview
</button>
<button
className={`tab-button ${activeTab === 'files' ? 'active' : ''}`}
onClick={() => setActiveTab('files')}
>
Files ({files.length})
</button>
<button
className={`tab-button ${activeTab === 'tasks' ? 'active' : ''}`}
onClick={() => setActiveTab('tasks')}
>
Tasks ({tasks.length})
</button>
<button
className={`tab-button ${activeTab === 'repositories' ? 'active' : ''}`}
onClick={() => setActiveTab('repositories')}
>
Repositories ({repositories.length})
</button>
{contract.red_team_enabled && (
<button
className={`tab-button red-team-tab ${activeTab === 'red-team' ? 'active' : ''}`}
onClick={() => setActiveTab('red-team')}
>
🛡️ Red Team
</button>
)}
</div>
<div className="contract-tab-content">
{activeTab === 'overview' && (
<div className="tab-panel">
<h2>Contract Overview</h2>
<dl className="overview-list">
<dt>Type</dt>
<dd>{contract.contract_type}</dd>
<dt>Phase</dt>
<dd>{contract.phase}</dd>
<dt>Status</dt>
<dd>{contract.status}</dd>
<dt>Created</dt>
<dd>{new Date(contract.created_at).toLocaleString()}</dd>
</dl>
</div>
)}
{activeTab === 'files' && (
<div className="tab-panel">
<h2>Files</h2>
{files.length === 0 ? (
<p>No files in this contract</p>
) : (
<ul className="file-list">
{files.map((file) => (
<li key={file.id} className="file-item">
<Link to={`/contracts/${contract.id}/files/${file.id}`}>
<h3>{file.name}</h3>
{file.description && <p>{file.description}</p>}
{file.contract_phase && (
<span className="file-phase">Phase: {file.contract_phase}</span>
)}
</Link>
</li>
))}
</ul>
)}
</div>
)}
{activeTab === 'tasks' && (
<div className="tab-panel">
<h2>Tasks</h2>
{tasks.length === 0 ? (
<p>No tasks in this contract</p>
) : (
<ul className="task-list">
{tasks.map((task) => (
<li key={task.id} className={`task-item ${task.is_red_team ? 'red-team-task' : ''}`}>
<h3>
{task.is_red_team && <span className="task-badge red-team">🛡️</span>}
{task.is_supervisor && <span className="task-badge supervisor">👔</span>}
{task.name}
</h3>
<span className={`task-status status-${task.status}`}>
{task.status}
</span>
</li>
))}
</ul>
)}
</div>
)}
{activeTab === 'repositories' && (
<div className="tab-panel">
<h2>Repositories</h2>
{repositories.length === 0 ? (
<p>No repositories linked to this contract</p>
) : (
<ul className="repository-list">
{repositories.map((repo) => (
<li key={repo.id} className="repository-item">
<h3>
{repo.name}
{repo.is_primary && <span className="primary-badge">Primary</span>}
</h3>
<span className="repo-type">{repo.source_type}</span>
</li>
))}
</ul>
)}
</div>
)}
{activeTab === 'red-team' && contract.red_team_enabled && (
<div className="tab-panel red-team-panel">
<h2>🛡️ Red Team Monitoring</h2>
{contract.red_team_prompt && (
<div className="red-team-prompt">
<h3>Review Criteria</h3>
<p>{contract.red_team_prompt}</p>
</div>
)}
<div className="red-team-status">
<h3>Red Team Task</h3>
{(() => {
const redTeamTask = tasks.find(t => t.is_red_team)
if (redTeamTask) {
return (
<div className="red-team-task-info">
<span className="task-name">{redTeamTask.name}</span>
<span className={`task-status status-${redTeamTask.status}`}>
{redTeamTask.status}
</span>
</div>
)
}
return <p className="no-task">Red team task not yet spawned</p>
})()}
</div>
<div className="red-team-notifications">
<h3>Alerts ({notifications.length})</h3>
{notificationsLoading ? (
<p>Loading notifications...</p>
) : notifications.length === 0 ? (
<p className="no-alerts">No alerts from red team</p>
) : (
<ul className="notification-list">
{notifications.map((notification) => (
<li
key={notification.id}
className={`notification-item severity-${notification.severity}`}
>
<div className="notification-header">
<span className={`severity-badge ${notification.severity}`}>
{notification.severity === 'critical' && '🚨'}
{notification.severity === 'warning' && '⚠️'}
{notification.severity === 'info' && 'ℹ️'}
{notification.severity.toUpperCase()}
</span>
<span className="notification-time">
{new Date(notification.created_at).toLocaleString()}
</span>
</div>
<p className="notification-message">{notification.message}</p>
{notification.file_path && (
<span className="notification-file">File: {notification.file_path}</span>
)}
{notification.context && (
<pre className="notification-context">{notification.context}</pre>
)}
</li>
))}
</ul>
)}
</div>
</div>
)}
</div>
</div>
)
}
|