import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import {
Sparkles, AlertTriangle, TrendingUp, Wrench, LineChart, Search, Boxes, CalendarClock,
Cpu, ThumbsUp, ThumbsDown, Quote,
} from 'lucide-react'
import { Card, Btn, Field } from '../components/ui'
import {
getAiStatus, aiDefectRootCause, aiForecast, aiPredictiveMaintenance,
aiSpcAnomaly, aiParseQuery, aiSafetyStock, aiScheduleOptimize,
ragDefectAnalysis, ragPredictAnalysis, ragFeedback, postAiFeedback,
} from '../api/client'
/** 적용된 기법 배지 — 응답의 applied metadata 를 시각화(토글 effect 관측). */
function AppliedBadge({ data }: { data: any }) {
const a = data?.applied
if (!a) return null
const chips: string[] = [`mode:${a.retrievalMode}`, `기법:${a.technique}`]
if (a.rerank) chips.push('rerank')
if (a.graphrag) chips.push('graphrag')
if (a.hybrid) chips.push('hybrid')
if (a.toolUse) chips.push(`agent(${a.maxSteps})`)
if (a.structured) chips.push('structured')
if (a.stream) chips.push('stream')
if (data?.degraded) chips.push('⚠ degraded(폴백)')
return (
{chips.map((c, i) => (
{c}
))}
)
}
/** 👍/👎 피드백 (중앙 /rag/feedback, solution=mes 격리). answerId 가 있을 때만 노출. */
function FeedbackBar({ answerId, query }: { answerId?: string; query?: string }) {
const [done, setDone] = useState('')
if (!answerId) return null
const send = async (verdict: 'up' | 'down') => {
try { await ragFeedback({ answerId, query, verdict }); setDone(verdict === 'up' ? '👍 반영됨' : '👎 반영됨') } catch { setDone('전송 실패') }
}
return (
이 답변이 도움이 됐나요?
{done && {done}}
)
}
/** 👍/👎 로컬 학습 피드백 (POST /api/ai/feedback → 로컬 DuckDB + 중앙 rag). 결과가 있을 때만 노출. */
function LocalFeedbackBar({ feature, question, answer }: { feature: string; question?: string; answer?: any }) {
const [done, setDone] = useState('')
if (answer == null) return null
const ans = typeof answer === 'string' ? answer : JSON.stringify(answer)
const send = async (verdict: 'up' | 'down') => {
try { await postAiFeedback({ feature, question, answer: ans, verdict }); setDone(verdict === 'up' ? '👍 반영됨' : '👎 반영됨') }
catch { setDone('전송 실패') }
}
return (
이 결과가 도움이 됐나요?
{done && {done}}
)
}
/** 인용 라벨 — 문서명·위치·근거지지도. citation/source 객체 형태 방어적 처리. */
function citeLabel(c: any): string {
if (c == null) return '문서'
if (typeof c === 'string') return c
const src = c.source || c.document || c.doc || c.title || c.chunk_id || c.id || '문서'
const loc = c.page != null ? ` p.${c.page}` : (c.location ? ` ${c.location}` : '')
const sup = c.support != null ? ` · ${Math.round(Number(c.support) * 100)}%` : ''
return `${src}${loc}${sup}`
}
/**
* WISE 근거 UX — 인용 카드(sources/citations) + 환각차단(abstained) 배지.
* node = 응답의 defect / interpretation 서브객체. 근거·보류 정보가 있을 때만 노출.
*/
function WiseEvidence({ node }: { node: any }) {
if (node == null) return null
const citations: any[] = Array.isArray(node.citations) ? node.citations : []
const sources: any[] = Array.isArray(node.sources) ? node.sources : []
const items = citations.length ? citations : sources
const abstained = node.abstained === true
if (!abstained && items.length === 0) return null
return (
{abstained && (
근거가 부족해 답변을 보류했습니다 (오류 아님 · 안내)
)}
{items.length > 0 ? (
<>
근거 인용 ({items.length})
{items.map((c, i) => (
{citeLabel(c)}
))}
>
) : (
근거 문서 없음
)}
)
}
function Output({ data }: { data: any }) {
if (data == null) return null
if (typeof data === 'string') return {data}
return {JSON.stringify(data, null, 2)}
}
export default function AiTools() {
const [ollama, setOllama] = useState(null)
useEffect(() => { getAiStatus().then(d => setOllama(!!(d?.ollamaAvailable ?? d?.available))).catch(() => setOllama(false)) }, [])
return (
WISE AI
Enterprise AI for Trusted Knowledge · 근거·인용·환각차단(중앙 guardia-rag) + 불량분석·생산예측·예지보전·SPC이상 (Ollama 온프레미스 + Java 폴백)
Ollama {ollama === null ? '확인 중' : ollama ? '온라인' : '오프라인 (Java 폴백)'}
{/* ── 최신 기법 (중앙 guardia-rag 경유) — 대표 2개 기능 ───────────── */}
WISE AI · 근거 기반 분석 (RAG · 에이전틱 · 구조화)
기법 토글 설정 →
{/* ── 기존 AI 도구 (불변, Ollama + Java 폴백) ───────────────────── */}
기본 AI 도구
)
}
/** 대표 1: 불량 원인분석 + SPC 이상감지 (SPC 수치=결정론, 원인서술=/rag/agent+structured). */
function RagDefectTool() {
const [code, setCode] = useState(''); const [ctx, setCtx] = useState('')
const [values, setValues] = useState(''); const [ucl, setUcl] = useState(''); const [lcl, setLcl] = useState(''); const [cl, setCl] = useState('')
const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => {
setBusy(true)
try {
const vals = values.split(',').map(s => s.trim()).filter(Boolean).map(Number)
const req: any = { defectCode: code, context: ctx ? [{ note: ctx }] : [] }
if (vals.length) { req.values = vals; req.ucl = Number(ucl) || 0; req.lcl = Number(lcl) || 0; req.cl = Number(cl) || 0 }
setOut(await ragDefectAnalysis(req))
} finally { setBusy(false) }
}
return (
}>
setCode(e.target.value)} placeholder="예: D-DIM-01" />
setValues(e.target.value)} placeholder="예: 10.1, 10.3, 9.8, 10.5" />
setUcl(e.target.value)} />
setLcl(e.target.value)} />
setCl(e.target.value)} />
{busy ? '분석 중…' : 'RCA + SPC 분석'}
)
}
/** 대표 2: 설비 예지보전 / 수요·생산 예측 (수치=결정론 베이스라인, 해석=/rag/agent). */
function RagPredictTool() {
const [kind, setKind] = useState<'pdm' | 'forecast'>('forecast')
const [eq, setEq] = useState(''); const [avail, setAvail] = useState(''); const [dt, setDt] = useState(''); const [mtbf, setMtbf] = useState('')
const [series, setSeries] = useState(''); const [horizon, setHorizon] = useState(7)
const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => {
setBusy(true)
try {
const req: any = { kind }
if (kind === 'pdm') { req.equipmentCode = eq; req.availability = Number(avail) || 0; req.downtimeCount = Number(dt) || 0; req.mtbfHours = Number(mtbf) || 0 }
else { req.series = series.split(',').map(s => s.trim()).filter(Boolean).map(Number); req.horizon = horizon }
setOut(await ragPredictAnalysis(req))
} finally { setBusy(false) }
}
return (
}>
{(['forecast', 'pdm'] as const).map(k => (
))}
{kind === 'pdm' ? (
<>
setEq(e.target.value)} placeholder="예: EQ-CNC-01" />
setAvail(e.target.value)} />
setDt(e.target.value)} />
setMtbf(e.target.value)} />
>
) : (
<>
setSeries(e.target.value)} placeholder="예: 100, 120, 95, 130" />
setHorizon(Number(e.target.value))} />
>
)}
{busy ? '분석 중…' : '예측 + 해석'}
)
}
function DefectTool() {
const [code, setCode] = useState(''); const [ctx, setCtx] = useState(''); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiDefectRootCause({ defectCode: code, context: ctx })) } finally { setBusy(false) } }
return (
}>
setCode(e.target.value)} placeholder="예: D-CRACK-01" />
{busy ? '분석 중…' : '원인 분석'}
)
}
function ForecastTool() {
const [item, setItem] = useState(''); const [days, setDays] = useState(30); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiForecast({ itemCode: item, days })) } finally { setBusy(false) } }
return (
}>
setItem(e.target.value)} />
setDays(Number(e.target.value))} />
{busy ? '예측 중…' : '예측'}
)
}
function MaintenanceTool() {
const [eq, setEq] = useState(''); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiPredictiveMaintenance({ equipmentCode: eq })) } finally { setBusy(false) } }
return (
}>
setEq(e.target.value)} placeholder="예: EQ-CNC-01" />
{busy ? '분석 중…' : '정비 권고'}
)
}
function SpcTool() {
const [code, setCode] = useState(''); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiSpcAnomaly({ chartCode: code })) } finally { setBusy(false) } }
return (
}>
setCode(e.target.value)} />
{busy ? '감지 중…' : '이상 감지'}
)
}
function QueryTool() {
const [q, setQ] = useState(''); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiParseQuery({ query: q })) } finally { setBusy(false) } }
return (
}>
setQ(e.target.value)} placeholder="예: 지난주 지연된 작업지시" onKeyDown={e => e.key === 'Enter' && run()} />
{busy ? '해석 중…' : '질의 해석'}
)
}
function SafetyStockTool() {
const [item, setItem] = useState(''); const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiSafetyStock({ itemCode: item })) } finally { setBusy(false) } }
return (
}>
setItem(e.target.value)} />
{busy ? '분석 중…' : '추천'}
)
}
function ScheduleTool() {
const [out, setOut] = useState(null); const [busy, setBusy] = useState(false)
const run = async () => { setBusy(true); try { setOut(await aiScheduleOptimize({})) } finally { setBusy(false) } }
return (
}>
현재 작업지시·설비 부하 기반으로 일정 최적화를 제안합니다.
{busy ? '최적화 중…' : '일정 최적화 제안'}
)
}