diff --git a/doc/guardia-signage_개발자지침서_v1.0.pptx b/doc/guardia-signage_개발자지침서_v1.0.pptx new file mode 100644 index 0000000..8e14000 Binary files /dev/null and b/doc/guardia-signage_개발자지침서_v1.0.pptx differ diff --git a/doc/guardia-signage_사용자지침서_v1.0.pptx b/doc/guardia-signage_사용자지침서_v1.0.pptx new file mode 100644 index 0000000..dfb0377 Binary files /dev/null and b/doc/guardia-signage_사용자지침서_v1.0.pptx differ diff --git a/doc/guardia-signage_아키텍처설계서_v1.0.pptx b/doc/guardia-signage_아키텍처설계서_v1.0.pptx new file mode 100644 index 0000000..9473126 Binary files /dev/null and b/doc/guardia-signage_아키텍처설계서_v1.0.pptx differ diff --git a/doc/guardia-signage_운영자지침서_v1.0.pptx b/doc/guardia-signage_운영자지침서_v1.0.pptx new file mode 100644 index 0000000..01831d7 Binary files /dev/null and b/doc/guardia-signage_운영자지침서_v1.0.pptx differ diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index dde2587..6f8075d 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -43,7 +43,7 @@ const groups: { title: string; items: NavItem[] }[] = [ { title: 'AI · 시스템', items: [ - { to: '/ai', icon: 'ai', label: 'AI 어시스턴트' }, + { to: '/ai', icon: 'ai', label: 'WISE AI' }, { to: '/ai-platform', icon: 'cpu', label: 'AI 플랫폼 설정' }, { to: '/audit', icon: 'audit', label: '감사 로그' }, { to: '/settings', icon: 'settings', label: '시스템 설정' }, diff --git a/frontend/src/pages/AiAssistant.tsx b/frontend/src/pages/AiAssistant.tsx index 6d940ac..f149441 100644 --- a/frontend/src/pages/AiAssistant.tsx +++ b/frontend/src/pages/AiAssistant.tsx @@ -4,32 +4,83 @@ import { PageHeader, Button } from '../components/ui' import { Icon } from '../lib/icons' import { aiChat, aiAnalyzeAlarm, aiGenerateSignText, aiRecommendAd } from '../api/client' -// AI 어시스턴트 — 온프레미스/Claude(AiTextRouter) 경유. 알람 분석·간판 문구·광고 추천·챗. -type Msg = { role: 'user' | 'ai'; text: string } +// WISE AI 어시스턴트 — 온프레미스/Claude(AiTextRouter) 경유. 알람 분석·간판 문구·광고 추천·챗. +// 응답에 근거(citations/sources)·환각차단(abstained)·degraded 필드가 있으면 인용/보류 UX 로 표기. +type Meta = { citations?: any[]; sources?: any[]; abstained?: boolean; degraded?: boolean } +type Msg = { role: 'user' | 'ai'; text: string; meta?: Meta } + +// 인용 라벨 — 문서명·위치·근거지지도. citation/source 객체 형태 방어적 처리. +function sgnCite(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}` +} + +// 근거/보류 배지 블록 — 근거·보류·degraded 정보가 있을 때만 노출(없으면 null). +function Evidence({ meta }: { meta?: Meta }) { + if (!meta) return null + const items = (meta.citations?.length ? meta.citations : meta.sources) || [] + const abstained = meta.abstained === true + const degraded = meta.degraded === true + if (!abstained && !degraded && items.length === 0) return null + return ( +
+ {abstained && ( +
+ 근거가 부족해 답변을 보류했습니다 (오류 아님 · 안내) +
+ )} + {degraded && !abstained && ( +
+ degraded · 폴백 응답 +
+ )} + {items.length > 0 && ( +
+
근거 인용 ({items.length})
+
+ {items.map((c: any, i: number) => ( + {sgnCite(c)} + ))} +
+
+ )} +
+ ) +} export default function AiAssistant() { const [msgs, setMsgs] = useState([]) const [input, setInput] = useState('') const push = (m: Msg) => setMsgs(p => [...p, m]) - const asText = (r: any) => r?.text || r?.result || r?.analysis || r?.recommendation || r?.message || JSON.stringify(r) + const asText = (r: any) => r?.text || r?.result || r?.analysis || r?.recommendation || r?.response || r?.answer || r?.message || JSON.stringify(r) + const metaOf = (r: any): Meta => ({ + citations: Array.isArray(r?.citations) ? r.citations : undefined, + sources: Array.isArray(r?.sources) ? r.sources : undefined, + abstained: r?.abstained === true, + degraded: r?.degraded === true, + }) const chatMut = useMutation({ mutationFn: (m: string) => aiChat(m), - onSuccess: r => push({ role: 'ai', text: asText(r) }), - onError: () => push({ role: 'ai', text: 'AI 응답을 가져오지 못했습니다. (degraded)' }), + onSuccess: r => push({ role: 'ai', text: asText(r), meta: metaOf(r) }), + onError: () => push({ role: 'ai', text: 'AI 응답을 가져오지 못했습니다. (일시 불가)', meta: { degraded: true } }), }) const alarmMut = useMutation({ mutationFn: () => aiAnalyzeAlarm({ message: input || '게이트웨이 오프라인 다수 발생', alarmType: 'DEVICE', severity: 'HIGH' }), - onSuccess: r => push({ role: 'ai', text: '[알람 분석]\n' + asText(r) }), + onSuccess: r => push({ role: 'ai', text: '[알람 분석]\n' + asText(r), meta: metaOf(r) }), }) const signMut = useMutation({ mutationFn: () => aiGenerateSignText({ bizName: input || '지오분식', message: '신메뉴 출시' }), - onSuccess: r => push({ role: 'ai', text: '[간판 문구]\n' + asText(r) }), + onSuccess: r => push({ role: 'ai', text: '[간판 문구]\n' + asText(r), meta: metaOf(r) }), }) const adMut = useMutation({ mutationFn: () => aiRecommendAd({ storeCode: 'STORE001', season: '여름' }), - onSuccess: r => push({ role: 'ai', text: '[광고 추천]\n' + asText(r) }), + onSuccess: r => push({ role: 'ai', text: '[광고 추천]\n' + asText(r), meta: metaOf(r) }), }) const send = () => { @@ -43,7 +94,7 @@ export default function AiAssistant() { return (
- +
@@ -59,10 +110,11 @@ export default function AiAssistant() {
)} {msgs.map((m, i) => ( -
+
{m.text}
+ {m.role === 'ai' && }
))} {busy &&
AI 응답 생성 중...
}