feat(wise): WISE AI apply - branded grounded chat with citations/abstain UX
This commit is contained in:
parent
0dbcf198bd
commit
f455563328
BIN
doc/guardia-signage_개발자지침서_v1.0.pptx
Normal file
BIN
doc/guardia-signage_개발자지침서_v1.0.pptx
Normal file
Binary file not shown.
BIN
doc/guardia-signage_사용자지침서_v1.0.pptx
Normal file
BIN
doc/guardia-signage_사용자지침서_v1.0.pptx
Normal file
Binary file not shown.
BIN
doc/guardia-signage_아키텍처설계서_v1.0.pptx
Normal file
BIN
doc/guardia-signage_아키텍처설계서_v1.0.pptx
Normal file
Binary file not shown.
BIN
doc/guardia-signage_운영자지침서_v1.0.pptx
Normal file
BIN
doc/guardia-signage_운영자지침서_v1.0.pptx
Normal file
Binary file not shown.
@ -43,7 +43,7 @@ const groups: { title: string; items: NavItem[] }[] = [
|
|||||||
{
|
{
|
||||||
title: 'AI · 시스템',
|
title: 'AI · 시스템',
|
||||||
items: [
|
items: [
|
||||||
{ to: '/ai', icon: 'ai', label: 'AI 어시스턴트' },
|
{ to: '/ai', icon: 'ai', label: 'WISE AI' },
|
||||||
{ to: '/ai-platform', icon: 'cpu', label: 'AI 플랫폼 설정' },
|
{ to: '/ai-platform', icon: 'cpu', label: 'AI 플랫폼 설정' },
|
||||||
{ to: '/audit', icon: 'audit', label: '감사 로그' },
|
{ to: '/audit', icon: 'audit', label: '감사 로그' },
|
||||||
{ to: '/settings', icon: 'settings', label: '시스템 설정' },
|
{ to: '/settings', icon: 'settings', label: '시스템 설정' },
|
||||||
|
|||||||
@ -4,32 +4,83 @@ import { PageHeader, Button } from '../components/ui'
|
|||||||
import { Icon } from '../lib/icons'
|
import { Icon } from '../lib/icons'
|
||||||
import { aiChat, aiAnalyzeAlarm, aiGenerateSignText, aiRecommendAd } from '../api/client'
|
import { aiChat, aiAnalyzeAlarm, aiGenerateSignText, aiRecommendAd } from '../api/client'
|
||||||
|
|
||||||
// AI 어시스턴트 — 온프레미스/Claude(AiTextRouter) 경유. 알람 분석·간판 문구·광고 추천·챗.
|
// WISE AI 어시스턴트 — 온프레미스/Claude(AiTextRouter) 경유. 알람 분석·간판 문구·광고 추천·챗.
|
||||||
type Msg = { role: 'user' | 'ai'; text: string }
|
// 응답에 근거(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 (
|
||||||
|
<div className="mt-1.5 max-w-[75%] space-y-1">
|
||||||
|
{abstained && (
|
||||||
|
<div className="text-[11px] px-2 py-1 rounded bg-amber-500/10 border border-amber-500/30 text-amber-300">
|
||||||
|
근거가 부족해 답변을 보류했습니다 (오류 아님 · 안내)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{degraded && !abstained && (
|
||||||
|
<div className="text-[11px] px-2 py-1 rounded bg-gray-500/10 border border-edge text-gray-400">
|
||||||
|
degraded · 폴백 응답
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-gray-500 mb-1">근거 인용 ({items.length})</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{items.map((c: any, i: number) => (
|
||||||
|
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-panel border border-edge text-gray-300">{sgnCite(c)}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function AiAssistant() {
|
export default function AiAssistant() {
|
||||||
const [msgs, setMsgs] = useState<Msg[]>([])
|
const [msgs, setMsgs] = useState<Msg[]>([])
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
|
|
||||||
const push = (m: Msg) => setMsgs(p => [...p, m])
|
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({
|
const chatMut = useMutation({
|
||||||
mutationFn: (m: string) => aiChat(m),
|
mutationFn: (m: string) => aiChat(m),
|
||||||
onSuccess: r => push({ role: 'ai', text: asText(r) }),
|
onSuccess: r => push({ role: 'ai', text: asText(r), meta: metaOf(r) }),
|
||||||
onError: () => push({ role: 'ai', text: 'AI 응답을 가져오지 못했습니다. (degraded)' }),
|
onError: () => push({ role: 'ai', text: 'AI 응답을 가져오지 못했습니다. (일시 불가)', meta: { degraded: true } }),
|
||||||
})
|
})
|
||||||
const alarmMut = useMutation({
|
const alarmMut = useMutation({
|
||||||
mutationFn: () => aiAnalyzeAlarm({ message: input || '게이트웨이 오프라인 다수 발생', alarmType: 'DEVICE', severity: 'HIGH' }),
|
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({
|
const signMut = useMutation({
|
||||||
mutationFn: () => aiGenerateSignText({ bizName: input || '지오분식', message: '신메뉴 출시' }),
|
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({
|
const adMut = useMutation({
|
||||||
mutationFn: () => aiRecommendAd({ storeCode: 'STORE001', season: '여름' }),
|
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 = () => {
|
const send = () => {
|
||||||
@ -43,7 +94,7 @@ export default function AiAssistant() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageHeader title="AI 어시스턴트" subtitle="온프레미스 sLLM / Claude(AiTextRouter) 경유 · 외부 호출은 Claude 프로바이더 예외만" />
|
<PageHeader title="WISE AI" subtitle="Enterprise AI for Trusted Knowledge · 온프레미스 sLLM / Claude(AiTextRouter) 경유 · 근거·인용·환각차단" />
|
||||||
|
|
||||||
<div className="flex gap-2 mb-4 flex-wrap">
|
<div className="flex gap-2 mb-4 flex-wrap">
|
||||||
<Button variant="ghost" icon="alarm" onClick={() => alarmMut.mutate()} disabled={busy}>알람 원인 분석</Button>
|
<Button variant="ghost" icon="alarm" onClick={() => alarmMut.mutate()} disabled={busy}>알람 원인 분석</Button>
|
||||||
@ -59,10 +110,11 @@ export default function AiAssistant() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{msgs.map((m, i) => (
|
{msgs.map((m, i) => (
|
||||||
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
<div key={i} className={`flex flex-col ${m.role === 'user' ? 'items-end' : 'items-start'}`}>
|
||||||
<div className={`max-w-[75%] rounded-lg px-3 py-2 text-sm whitespace-pre-wrap ${m.role === 'user' ? 'bg-brand text-white' : 'bg-panel border border-edge text-gray-200'}`}>
|
<div className={`max-w-[75%] rounded-lg px-3 py-2 text-sm whitespace-pre-wrap ${m.role === 'user' ? 'bg-brand text-white' : 'bg-panel border border-edge text-gray-200'}`}>
|
||||||
{m.text}
|
{m.text}
|
||||||
</div>
|
</div>
|
||||||
|
{m.role === 'ai' && <Evidence meta={m.meta} />}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{busy && <div className="text-gray-500 text-sm">AI 응답 생성 중...</div>}
|
{busy && <div className="text-gray-500 text-sm">AI 응답 생성 중...</div>}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user