feat(wise): WISE AI apply - branded grounded chat with citations/abstain UX

This commit is contained in:
GUARDiA 2026-07-04 18:01:54 +09:00
parent 7644d9ed8b
commit 9f24e3921d
7 changed files with 60 additions and 5 deletions

View File

@ -155,6 +155,10 @@ public class RagMesService {
defect.put("sources", res.get("sources"));
defect.put("steps", res.get("steps")); // 에이전트 추론 스텝(tool-use )
defect.put("answerId", res.get("answer_id"));
// 환각차단·근거검증 통과 필드(계약 추가·기존 불변): WISE AI 화면 인용/보류 UX
defect.put("abstained", res.get("abstained"));
defect.put("grounded", res.get("grounded"));
defect.put("faithfulness", res.get("faithfulness"));
out.put("defect", defect);
out.put("engine", "RAG_" + technique.toUpperCase());
out.put("degraded", false);
@ -230,8 +234,13 @@ public class RagMesService {
if (res.get("data") instanceof Map) interp.putAll((Map<String, Object>) res.get("data"));
if (res.get("answer") != null) interp.put("narrative", res.get("answer"));
interp.put("citations", res.get("citations"));
interp.put("sources", res.get("sources"));
interp.put("steps", res.get("steps"));
interp.put("answerId", res.get("answer_id"));
// 환각차단·근거검증 통과 필드(계약 추가·기존 불변): WISE AI 화면 인용/보류 UX
interp.put("abstained", res.get("abstained"));
interp.put("grounded", res.get("grounded"));
interp.put("faithfulness", res.get("faithfulness"));
out.put("interpretation", interp);
out.put("engine", "RAG_" + technique.toUpperCase());
out.put("degraded", false);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -50,7 +50,7 @@ const masterLinks: Link[] = [
]
const commonLinks: Link[] = [
{ to: '/analytics', label: '분석 / KPI', icon: BarChart3 },
{ to: '/ai-tools', label: 'AI 도구', icon: Sparkles },
{ to: '/ai-tools', label: 'WISE AI', icon: Sparkles },
{ to: '/ai-techniques', label: 'AI 기법 설정', icon: Cpu, min: 'MANAGER' },
]
// 업무 (UIWS 이식) — 업무일지/일정/쪽지/업무통계

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import {
Sparkles, AlertTriangle, TrendingUp, Wrench, LineChart, Search, Boxes, CalendarClock,
Cpu, ThumbsUp, ThumbsDown,
Cpu, ThumbsUp, ThumbsDown, Quote,
} from 'lucide-react'
import { Card, Btn, Field } from '../components/ui'
import {
@ -68,6 +68,50 @@ function LocalFeedbackBar({ feature, question, answer }: { feature: string; ques
)
}
/** 인용 라벨 — 문서명·위치·근거지지도. 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 (
<div className="mt-3">
{abstained && (
<div className="mb-2 flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-300">
<AlertTriangle size={13} /> ( · )
</div>
)}
{items.length > 0 ? (
<>
<div className="text-[11px] text-slate-400 mb-1.5 flex items-center gap-1"><Quote size={12} /> ({items.length})</div>
<div className="flex flex-wrap gap-1.5">
{items.map((c, i) => (
<span key={i} className="text-[10px] px-2 py-1 rounded-md bg-card border border-edge text-slate-300">{citeLabel(c)}</span>
))}
</div>
</>
) : (
<div className="text-[11px] text-slate-500"> </div>
)}
</div>
)
}
function Output({ data }: { data: any }) {
if (data == null) return null
if (typeof data === 'string') return <pre className="mt-3 text-xs text-slate-300 bg-ink border border-edge rounded p-3 whitespace-pre-wrap max-h-56 overflow-auto">{data}</pre>
@ -82,8 +126,8 @@ export default function AiTools() {
<div>
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-xl font-bold flex items-center gap-2"><Sparkles className="text-accent" size={20} /> AI </h1>
<p className="text-sm text-slate-400 mt-0.5"> · · · SPC이상 · · · (Ollama + Java )</p>
<h1 className="text-xl font-bold flex items-center gap-2"><Sparkles className="text-accent" size={20} /> WISE AI</h1>
<p className="text-sm text-slate-400 mt-0.5">Enterprise AI for Trusted Knowledge · ··( guardia-rag) + ···SPC이상 (Ollama + Java )</p>
</div>
<span className={`flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${ollama ? 'text-accent border-accent/30 bg-accent/10' : 'text-slate-400 border-edge bg-card'}`}>
<span className={`w-1.5 h-1.5 rounded-full ${ollama ? 'bg-accent' : 'bg-slate-500'}`} />
@ -93,7 +137,7 @@ export default function AiTools() {
{/* ── 최신 기법 (중앙 guardia-rag 경유) — 대표 2개 기능 ───────────── */}
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold flex items-center gap-2 text-brand"><Cpu size={15} /> AI (RAG · · )</h2>
<h2 className="text-sm font-semibold flex items-center gap-2 text-brand"><Cpu size={15} /> WISE AI · (RAG · · )</h2>
<Link to="/ai-techniques" className="text-xs text-slate-400 hover:text-brand underline"> </Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-8">
@ -137,6 +181,7 @@ function RagDefectTool() {
<Btn onClick={run} disabled={busy} size="sm"><Cpu size={13} /> {busy ? '분석 중…' : 'RCA + SPC 분석'}</Btn>
<AppliedBadge data={out} />
<Output data={out} />
<WiseEvidence node={out?.defect} />
<FeedbackBar answerId={out?.defect?.answerId} query={code} />
</Card>
)
@ -184,6 +229,7 @@ function RagPredictTool() {
<Btn onClick={run} disabled={busy} size="sm"><Cpu size={13} /> {busy ? '분석 중…' : '예측 + 해석'}</Btn>
<AppliedBadge data={out} />
<Output data={out} />
<WiseEvidence node={out?.interpretation} />
<FeedbackBar answerId={out?.interpretation?.answerId} query={kind} />
</Card>
)