feat(governance): 接入 AI 技術雷達前端讀回
This commit is contained in:
@@ -3311,6 +3311,23 @@
|
||||
"approvals": "Runtime 批准"
|
||||
}
|
||||
},
|
||||
"technologyRadar": {
|
||||
"title": "AI 技術雷達滾動監控",
|
||||
"status": "滾動狀態",
|
||||
"nearRealTime": "近即時監控",
|
||||
"reviewQueue": "審核佇列",
|
||||
"reviewQueueValue": "待審 {queue} / 高優先 {high} / 變更 {changed}",
|
||||
"domainsTitle": "市場技術領域",
|
||||
"rollingTitle": "日週月報與滾動更新控制",
|
||||
"rolesTitle": "專業 Agent 分工",
|
||||
"metrics": {
|
||||
"progress": "雷達完成度",
|
||||
"technologies": "監控技術",
|
||||
"sources": "Primary Sources",
|
||||
"failures": "來源失敗",
|
||||
"policyHolds": "審核鎖"
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"candidates": "候選數",
|
||||
"sources": "來源數",
|
||||
|
||||
@@ -3311,6 +3311,23 @@
|
||||
"approvals": "Runtime 批准"
|
||||
}
|
||||
},
|
||||
"technologyRadar": {
|
||||
"title": "AI 技術雷達滾動監控",
|
||||
"status": "滾動狀態",
|
||||
"nearRealTime": "近即時監控",
|
||||
"reviewQueue": "審核佇列",
|
||||
"reviewQueueValue": "待審 {queue} / 高優先 {high} / 變更 {changed}",
|
||||
"domainsTitle": "市場技術領域",
|
||||
"rollingTitle": "日週月報與滾動更新控制",
|
||||
"rolesTitle": "專業 Agent 分工",
|
||||
"metrics": {
|
||||
"progress": "雷達完成度",
|
||||
"technologies": "監控技術",
|
||||
"sources": "Primary Sources",
|
||||
"failures": "來源失敗",
|
||||
"policyHolds": "審核鎖"
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"candidates": "候選數",
|
||||
"sources": "來源數",
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useTranslations } from 'next-intl'
|
||||
import { GlassCard } from '@/components/ui/glass-card'
|
||||
import { StatusOrb } from '@/components/ui/status-orb'
|
||||
import { AgentActivityConstellation } from '@/components/governance/agent-activity-constellation'
|
||||
import { apiClient, type AgentMarketGovernanceSnapshot } from '@/lib/api-client'
|
||||
import { apiClient, type AgentMarketGovernanceSnapshot, type AiTechnologyRadarReadback } from '@/lib/api-client'
|
||||
|
||||
// =============================================================================
|
||||
// Helpers
|
||||
@@ -31,6 +31,23 @@ function formatDateTime(value: string): string {
|
||||
})
|
||||
}
|
||||
|
||||
function formatPercent(value: number): string {
|
||||
return `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
const TECHNOLOGY_AREA_LABELS: Record<string, string> = {
|
||||
agent_frameworks: 'Agent Frameworks',
|
||||
evaluation_and_observability: 'Eval / Observability',
|
||||
mcp_and_a2a: 'MCP / A2A',
|
||||
model_providers: 'Model Providers',
|
||||
model_serving: 'Model Serving',
|
||||
rag_and_vector: 'RAG / Vector',
|
||||
}
|
||||
|
||||
function formatTechnologyArea(value: string): string {
|
||||
return TECHNOLOGY_AREA_LABELS[value] ?? value.replace(/_/g, ' ')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Small UI
|
||||
// =============================================================================
|
||||
@@ -176,6 +193,140 @@ function DetailRow({ label, children }: { label: string; children: React.ReactNo
|
||||
)
|
||||
}
|
||||
|
||||
function RadarStat({ label, value, tone = 'neutral' }: { label: string; value: number | string; tone?: 'neutral' | 'ok' | 'warn' }) {
|
||||
const color = tone === 'ok' ? '#22C55E' : tone === 'warn' ? '#F59E0B' : '#141413'
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: 0,
|
||||
padding: 11,
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 7,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
}}>
|
||||
<span style={{
|
||||
fontFamily: "'DM Mono', monospace",
|
||||
fontSize: 10,
|
||||
color: '#87867f',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{
|
||||
fontFamily: 'Syne, sans-serif',
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
color,
|
||||
lineHeight: 1,
|
||||
}}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TechnologyDomainCard({ domain }: { domain: AiTechnologyRadarReadback['technology_domains'][number] }) {
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: 0,
|
||||
padding: 12,
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 7,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, minWidth: 0 }}>
|
||||
<span style={{
|
||||
fontFamily: 'Syne, sans-serif',
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: '#141413',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{formatTechnologyArea(domain.technology_area)}
|
||||
</span>
|
||||
<CandidatePill value={`${domain.technology_count} tech / P${domain.high_priority_count}`} muted />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
|
||||
{domain.representative_technologies.map(technology => (
|
||||
<CandidatePill key={technology} value={technology} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<CandidatePill value={`changed=${domain.changed_count}`} muted={domain.changed_count === 0} />
|
||||
<CandidatePill value={`high_priority=${domain.high_priority_count}`} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RollingUpdateCard({ control }: { control: AiTechnologyRadarReadback['rolling_update_controls'][number] }) {
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: 0,
|
||||
padding: 12,
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 7,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 9,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{control.cadence}
|
||||
</span>
|
||||
<CandidatePill value={control.gate} muted />
|
||||
</div>
|
||||
<DetailRow label="AI AUTO ACTION">
|
||||
{control.agent_auto_action}
|
||||
</DetailRow>
|
||||
<DetailRow label="OUTPUT">
|
||||
{control.output}
|
||||
</DetailRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfessionalAgentRoleCard({ role }: { role: AiTechnologyRadarReadback['professional_agent_roles'][number] }) {
|
||||
return (
|
||||
<div style={{
|
||||
minWidth: 0,
|
||||
padding: 12,
|
||||
border: '0.5px solid #e0ddd4',
|
||||
borderRadius: 7,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 9,
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 10, minWidth: 0 }}>
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{role.agent}
|
||||
</span>
|
||||
<CandidatePill value="readback" />
|
||||
</div>
|
||||
<DetailRow label="PRO ROLE">
|
||||
{role.professional_role}
|
||||
</DetailRow>
|
||||
<DetailRow label="AUTO SCOPE">
|
||||
{role.auto_scope}
|
||||
</DetailRow>
|
||||
<DetailRow label="REVIEW BOUNDARY">
|
||||
{role.review_boundary}
|
||||
</DetailRow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Component
|
||||
// =============================================================================
|
||||
@@ -183,14 +334,19 @@ function DetailRow({ label, children }: { label: string; children: React.ReactNo
|
||||
export function AgentMarketTab() {
|
||||
const t = useTranslations('governance.agentMarket')
|
||||
const [snapshot, setSnapshot] = useState<AgentMarketGovernanceSnapshot | null>(null)
|
||||
const [technologyRadar, setTechnologyRadar] = useState<AiTechnologyRadarReadback | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
const fetchSnapshot = () => {
|
||||
setLoading(true)
|
||||
apiClient.getAgentMarketGovernanceSnapshot()
|
||||
.then((data: AgentMarketGovernanceSnapshot) => {
|
||||
setSnapshot(data)
|
||||
Promise.all([
|
||||
apiClient.getAgentMarketGovernanceSnapshot(),
|
||||
apiClient.getAiTechnologyRadarReadback(),
|
||||
])
|
||||
.then(([marketSnapshot, radarReadback]) => {
|
||||
setSnapshot(marketSnapshot)
|
||||
setTechnologyRadar(radarReadback)
|
||||
setError(false)
|
||||
})
|
||||
.catch(() => setError(true))
|
||||
@@ -215,7 +371,7 @@ export function AgentMarketTab() {
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !snapshot) {
|
||||
if (error || !snapshot || !technologyRadar) {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<GlassCard variant="subtle" padding="lg">
|
||||
@@ -250,6 +406,7 @@ export function AgentMarketTab() {
|
||||
}
|
||||
|
||||
const summary = snapshot.summary
|
||||
const radarSummary = technologyRadar.summary
|
||||
const allApprovals =
|
||||
summary.priority_upgrades_approved +
|
||||
summary.market_scorecard_updates_approved +
|
||||
@@ -259,6 +416,9 @@ export function AgentMarketTab() {
|
||||
summary.production_changes_approved +
|
||||
summary.shadow_or_canary_approved +
|
||||
summary.replacement_decisions_approved
|
||||
const radarPolicyHolds = Object.entries(technologyRadar.policy)
|
||||
.filter(([key, value]) => key !== 'read_only' && value === false)
|
||||
.length
|
||||
const watchHealth = snapshot.market_watch_health
|
||||
const watchHealthHealthy = watchHealth.status === 'healthy'
|
||||
|
||||
@@ -318,6 +478,115 @@ export function AgentMarketTab() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 9, minWidth: 0 }}>
|
||||
<CalendarClock size={15} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 14, fontWeight: 700, color: '#141413' }}>
|
||||
{t('technologyRadar.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, justifyContent: 'flex-end' }}>
|
||||
<CandidatePill value={technologyRadar.report_contract.schedule_workflow} />
|
||||
<CandidatePill value={technologyRadar.report_contract.schedule_cron_utc} muted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
|
||||
gap: 10,
|
||||
}} className="agent-market-radar-metrics-grid">
|
||||
<RadarStat label={t('technologyRadar.metrics.progress')} value={formatPercent(radarSummary.ai_technology_radar_completion_percent)} tone="ok" />
|
||||
<RadarStat label={t('technologyRadar.metrics.technologies')} value={radarSummary.technology_count} />
|
||||
<RadarStat label={t('technologyRadar.metrics.sources')} value={radarSummary.source_count} />
|
||||
<RadarStat label={t('technologyRadar.metrics.failures')} value={radarSummary.source_failures} tone={radarSummary.source_failures > 0 ? 'warn' : 'ok'} />
|
||||
<RadarStat label={t('technologyRadar.metrics.policyHolds')} value={radarPolicyHolds} tone={radarPolicyHolds > 0 ? 'warn' : 'ok'} />
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
|
||||
gap: 10,
|
||||
}} className="agent-market-radar-contract-grid">
|
||||
<DetailRow label={t('technologyRadar.status')}>
|
||||
<CandidatePill value={radarSummary.rolling_update_status} />
|
||||
</DetailRow>
|
||||
<DetailRow label={t('technologyRadar.nearRealTime')}>
|
||||
{technologyRadar.report_contract.near_real_time}
|
||||
</DetailRow>
|
||||
<DetailRow label={t('technologyRadar.reviewQueue')}>
|
||||
{t('technologyRadar.reviewQueueValue', {
|
||||
queue: radarSummary.review_queue_count,
|
||||
high: radarSummary.high_priority_count,
|
||||
changed: radarSummary.changed_technologies,
|
||||
})}
|
||||
</DetailRow>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||
<ListChecks size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('technologyRadar.domainsTitle')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||
gap: 10,
|
||||
}} className="agent-market-radar-domain-grid">
|
||||
{technologyRadar.technology_domains.map(domain => (
|
||||
<TechnologyDomainCard key={domain.technology_area} domain={domain} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||
<RefreshCw size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('technologyRadar.rollingTitle')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||
gap: 10,
|
||||
}} className="agent-market-radar-rolling-grid">
|
||||
{technologyRadar.rolling_update_controls.map(control => (
|
||||
<RollingUpdateCard key={`${control.cadence}-${control.gate}`} control={control} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<GlassCard variant="subtle" padding="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||
<ShieldCheck size={14} style={{ color: '#d97757' }} />
|
||||
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
|
||||
{t('technologyRadar.rolesTitle')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||
gap: 10,
|
||||
}} className="agent-market-radar-role-grid">
|
||||
{technologyRadar.professional_agent_roles.map(role => (
|
||||
<ProfessionalAgentRoleCard key={role.agent} role={role} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
|
||||
@@ -701,6 +970,11 @@ export function AgentMarketTab() {
|
||||
<style>{`
|
||||
@media (max-width: 900px) {
|
||||
.agent-market-kpi-grid,
|
||||
.agent-market-radar-metrics-grid,
|
||||
.agent-market-radar-contract-grid,
|
||||
.agent-market-radar-domain-grid,
|
||||
.agent-market-radar-rolling-grid,
|
||||
.agent-market-radar-role-grid,
|
||||
.agent-market-health-grid,
|
||||
.agent-market-cadence-grid,
|
||||
.agent-market-decision-grid,
|
||||
|
||||
@@ -314,6 +314,11 @@ export const apiClient = {
|
||||
return handleResponse<AgentMarketGovernanceSnapshot>(res)
|
||||
},
|
||||
|
||||
async getAiTechnologyRadarReadback() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/ai-technology-radar-readback`)
|
||||
return handleResponse<AiTechnologyRadarReadback>(res)
|
||||
},
|
||||
|
||||
async getAiAgentAutomationInventorySnapshot() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/automation-inventory-snapshot`)
|
||||
return handleResponse<AiAgentAutomationInventorySnapshot>(res)
|
||||
@@ -1082,6 +1087,92 @@ export interface AgentMarketGovernanceSnapshot {
|
||||
forbidden_actions_without_new_approval: string[]
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AI Technology Radar Readback
|
||||
// =========================================================================
|
||||
|
||||
export interface AiTechnologyRadarReadback {
|
||||
schema_version: 'ai_technology_radar_readback_v1'
|
||||
generated_at: string
|
||||
source_scope: Record<string, string>
|
||||
summary: {
|
||||
overall_completion_percent: number
|
||||
ai_technology_radar_completion_percent: number
|
||||
technology_count: number
|
||||
technology_area_count: number
|
||||
source_count: number
|
||||
changed_technologies: number
|
||||
review_queue_count: number
|
||||
source_failures: number
|
||||
high_priority_count: number
|
||||
rolling_update_status: string
|
||||
}
|
||||
policy: {
|
||||
read_only: true
|
||||
raw_chat_history_synced: false
|
||||
sdk_installation_approved: false
|
||||
paid_api_calls_approved: false
|
||||
production_routing_approved: false
|
||||
telegram_send_approved: false
|
||||
model_provider_switch_approved: false
|
||||
host_write_approved: false
|
||||
openclaw_replacement_approved: false
|
||||
}
|
||||
technology_area_counts: Record<string, number>
|
||||
technology_domains: Array<{
|
||||
technology_area: string
|
||||
technology_count: number
|
||||
high_priority_count: number
|
||||
changed_count: number
|
||||
representative_technologies: string[]
|
||||
}>
|
||||
high_priority_review_queue: Array<Record<string, unknown>>
|
||||
professional_agent_roles: Array<{
|
||||
agent: string
|
||||
professional_role: string
|
||||
auto_scope: string
|
||||
review_boundary: string
|
||||
}>
|
||||
rolling_update_controls: Array<{
|
||||
cadence: string
|
||||
cadence_source: string
|
||||
agent_auto_action: string
|
||||
output: string
|
||||
gate: string
|
||||
}>
|
||||
integration_candidates: Array<{
|
||||
technology_id: string
|
||||
display_name: string
|
||||
technology_area: string
|
||||
integration_surface: string
|
||||
awoooi_role: string
|
||||
decision: string
|
||||
changed: boolean
|
||||
recommended_actions: string[]
|
||||
}>
|
||||
priority_workplan: Array<{
|
||||
order: number
|
||||
priority: string
|
||||
work_item: string
|
||||
automation_mode: string
|
||||
done_definition: string
|
||||
}>
|
||||
blocked_gates: string[]
|
||||
report_contract: {
|
||||
api_endpoint: string
|
||||
frontend_target: string
|
||||
schedule_enabled: boolean
|
||||
schedule_workflow: string
|
||||
schedule_cron_utc: string
|
||||
near_real_time: string
|
||||
daily: string
|
||||
weekly: string
|
||||
monthly: string
|
||||
agent_auto_allowed_for: string[]
|
||||
human_review_required_for: string[]
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AI Agent Automation Inventory Snapshot
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user