guardia-messenger/app/(tabs)/ai_decision.tsx

115 lines
6.0 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
import { ITSM_BASE } from '../../services/api';
interface Decision { id: string; question: string; recommendation: string; confidence: number; alternatives: string[]; impact: string; ts: string }
const MOCK_DECISIONS: Decision[] = [
{ id: 'D-001', question: 'db-01 디스크 85% — 즉시 조치 필요?', recommendation: '로그 파일 정리 + SR 등록', confidence: 0.91, alternatives: ['서버 증설 요청', '아카이브 이전', '임시 파일 삭제'], impact: '서비스 중단 없이 2시간 내 해결 가능', ts: new Date().toISOString() },
{ id: 'D-002', question: '토요일 오전 2시 긴급 패치 배포 승인?', recommendation: '승인 — 위험 낮음, 영향 범위 최소', confidence: 0.78, alternatives: ['일정 연기 (다음 주)', '부분 배포 (1개 서버만)'], impact: '서비스 다운타임 예상 15분', ts: new Date().toISOString() },
];
export default function AIDecisionScreen() {
const [decisions, setDecisions] = useState<Decision[]>(MOCK_DECISIONS);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState<Decision | null>(null);
const fetchDecisions = async () => {
setLoading(true);
try {
const r = await fetch(`${ITSM_BASE}/api/ai/decisions/pending`);
if (r.ok) { const d = await r.json(); if (d.items?.length) setDecisions(d.items); }
} catch {}
setLoading(false);
};
useEffect(() => { fetchDecisions(); }, []);
const applyDecision = async (dec: Decision, choice: string) => {
try {
await fetch(`${ITSM_BASE}/api/ai/decisions/${dec.id}/apply`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ choice }),
});
setDecisions(prev => prev.filter(d => d.id !== dec.id));
setSelected(null);
} catch {}
};
if (selected) {
return (
<ScrollView style={s.container}>
<TouchableOpacity style={s.backBtn} onPress={() => setSelected(null)}>
<Text style={s.backText}> </Text>
</TouchableOpacity>
<Text style={s.detailTitle}>{selected.question}</Text>
<View style={s.confCard}>
<Text style={s.confLabel}>AI </Text>
<Text style={s.confVal}>{Math.round(selected.confidence * 100)}%</Text>
</View>
<View style={s.card}>
<Text style={s.sectionLbl}>AI </Text>
<Text style={s.recommendation}>{selected.recommendation}</Text>
</View>
<View style={s.card}>
<Text style={s.sectionLbl}> </Text>
<Text style={s.impact}>{selected.impact}</Text>
</View>
<Text style={s.sectionLbl}></Text>
{[selected.recommendation, ...selected.alternatives].map((alt, i) => (
<TouchableOpacity key={i} style={[s.altBtn, i === 0 && s.altBtnPrimary]} onPress={() => applyDecision(selected, alt)}>
<Text style={[s.altBtnText, i === 0 && s.altBtnTextPrimary]}>{i === 0 ? '✅ ' : ''}{alt}</Text>
</TouchableOpacity>
))}
</ScrollView>
);
}
return (
<ScrollView style={s.container}>
<Text style={s.title}>AI </Text>
<Text style={s.sub}>GUARDiA AI가 </Text>
{loading && <ActivityIndicator color="#00A0C8" />}
{decisions.map(dec => (
<TouchableOpacity key={dec.id} style={s.decCard} onPress={() => setSelected(dec)}>
<View style={s.decHeader}>
<View style={[s.confBadge, { backgroundColor: dec.confidence >= 0.85 ? '#44bb44' : '#ffbb00' }]}>
<Text style={s.confBadgeText}>{Math.round(dec.confidence * 100)}%</Text>
</View>
</View>
<Text style={s.question}>{dec.question}</Text>
<Text style={s.recoPreview}>{dec.recommendation}</Text>
<Text style={s.ts}>{new Date(dec.ts).toLocaleString()}</Text>
</TouchableOpacity>
))}
{decisions.length === 0 && <Text style={s.empty}> AI </Text>}
</ScrollView>
);
}
const s = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0A0E1A', padding: 16 },
title: { color: '#fff', fontSize: 20, fontWeight: '700', marginBottom: 4 },
sub: { color: '#888', fontSize: 13, marginBottom: 16 },
backBtn: { marginBottom: 16 }, backText: { color: '#00A0C8', fontSize: 15 },
detailTitle: { color: '#fff', fontSize: 17, fontWeight: '700', marginBottom: 16 },
confCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderWidth: 1, borderColor: '#333' },
confLabel: { color: '#aaa', fontSize: 14 }, confVal: { color: '#44bb44', fontSize: 28, fontWeight: '700' },
card: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: '#333' },
sectionLbl: { color: '#888', fontSize: 12, marginBottom: 8, fontWeight: '600' },
recommendation: { color: '#fff', fontSize: 15, fontWeight: '600' },
impact: { color: '#aaa', fontSize: 14 },
altBtn: { backgroundColor: '#1A1F2E', borderRadius: 10, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
altBtnPrimary: { backgroundColor: '#003366', borderColor: '#00A0C8' },
altBtnText: { color: '#aaa', fontSize: 14 },
altBtnTextPrimary: { color: '#fff', fontWeight: '700' },
decCard: { backgroundColor: '#1A1F2E', borderRadius: 12, padding: 14, marginBottom: 10, borderWidth: 1, borderColor: '#333' },
decHeader: { flexDirection: 'row', marginBottom: 8 },
confBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 },
confBadgeText: { color: '#fff', fontSize: 11, fontWeight: '700' },
question: { color: '#fff', fontWeight: '600', fontSize: 14, marginBottom: 6 },
recoPreview: { color: '#00A0C8', fontSize: 12, marginBottom: 6 },
ts: { color: '#555', fontSize: 11 },
empty: { color: '#555', textAlign: 'center', marginTop: 40, fontSize: 14 },
});