143 lines
7.3 KiB
HTML
143 lines
7.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Negosium Agent — 협상 채팅 (테스트)</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: -apple-system, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; background: #eef1f5; color: #1c2530; }
|
|
header { background: #1f2d3d; color: #fff; padding: 12px 16px; }
|
|
header h1 { margin: 0; font-size: 16px; }
|
|
header p { margin: 4px 0 0; font-size: 12px; color: #9fb0c3; }
|
|
.wrap { max-width: 760px; margin: 0 auto; padding: 12px; }
|
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; background: #fff; padding: 10px; border-radius: 8px; margin-bottom: 10px; }
|
|
.controls label { font-size: 12px; color: #51607a; display: flex; flex-direction: column; gap: 2px; }
|
|
.controls input, .controls select { padding: 6px 8px; border: 1px solid #cdd6e3; border-radius: 6px; font-size: 13px; }
|
|
.controls input[type=number] { width: 110px; }
|
|
button { cursor: pointer; border: 0; border-radius: 6px; padding: 8px 12px; font-size: 13px; font-weight: 600; }
|
|
.btn-reset { background: #5b6b82; color: #fff; }
|
|
.chat { background: #fff; border-radius: 8px; height: 56vh; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
.row { display: flex; }
|
|
.row.agent { justify-content: flex-start; }
|
|
.row.user { justify-content: flex-end; }
|
|
.row.sys { justify-content: center; }
|
|
.bubble { max-width: 80%; padding: 9px 12px; border-radius: 12px; font-size: 13px; line-height: 1.55; white-space: pre-wrap; }
|
|
.agent .bubble { background: #eef1f6; color: #28323f; border-bottom-left-radius: 3px; }
|
|
.user .bubble { background: #2f6fed; color: #fff; border-bottom-right-radius: 3px; }
|
|
.sys .bubble { background: #fff5d6; color: #6b5500; font-size: 12px; border: 1px solid #f0e2a8; }
|
|
.card { margin-top: 7px; font-size: 12px; background: #0f1b2d; color: #9fe0c0; border-radius: 6px; padding: 6px 8px; }
|
|
.controls-area { margin-top: 10px; background: #fff; padding: 10px; border-radius: 8px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-height: 52px; }
|
|
.opt { background: #2f6fed; color: #fff; }
|
|
.opt.alt { background: #5b6b82; }
|
|
.price-in { padding: 8px 10px; border: 1px solid #cdd6e3; border-radius: 6px; font-size: 14px; width: 160px; }
|
|
.btn-send { background: #1f9d57; color: #fff; }
|
|
.note { font-size: 11px; color: #6b7890; margin-top: 8px; line-height: 1.5; }
|
|
.pill { display:inline-block; background:#0f1b2d; color:#9fe0c0; border-radius:4px; padding:1px 6px; font-size:11px; margin-left:6px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Negosium Agent — 협상 채팅 <span class="pill" id="srv">/v1/chat</span></h1>
|
|
<p>KT 구매자 관점: 협력사(판매자)가 제시가를 입력 → <b>앵커가 이하면 우선협상(타결)</b>, 초과면 카드로 인하 협상(카드 소진까지). 낮게 매입할수록 KT 이득.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
<div class="controls">
|
|
<label>테넌트
|
|
<select id="tenant" onchange="reset()">
|
|
<option value="ktcommerce">ktcommerce (데모상사 A)</option>
|
|
<option value="imarketkorea">imarketkorea (데모상사 B)</option>
|
|
</select>
|
|
</label>
|
|
<label>유형
|
|
<select id="rq" onchange="reset()"><option value="재협상">재협상</option><option value="재견적">재견적</option></select>
|
|
</label>
|
|
<label>목표가(KT 매입)<input type="number" id="target" value="10000" oninput="suggestAnchor()" /></label>
|
|
<label>앵커가(KT 앵커링, ≤목표)<input type="number" id="anchor" value="9900" /></label>
|
|
<button class="btn-reset" onclick="reset()">새 협상 시작</button>
|
|
</div>
|
|
|
|
<div class="chat" id="chat"></div>
|
|
<div class="controls-area" id="controls"></div>
|
|
<div class="note" id="note"></div>
|
|
</div>
|
|
|
|
<script>
|
|
let sid = null, ended = false;
|
|
const $ = id => document.getElementById(id);
|
|
|
|
// 갑(KT)이 앵커가를 직접 입력. 목표가 변경 시 기본 제안값(target*0.99)을 채워주되 편집 가능.
|
|
function suggestAnchor() {
|
|
const t = Number($('target').value);
|
|
if (t > 0) $('anchor').value = Math.round(t * 0.99);
|
|
}
|
|
|
|
function bubble(side, html) {
|
|
const row = document.createElement('div'); row.className = 'row ' + side;
|
|
const b = document.createElement('div'); b.className = 'bubble'; b.innerHTML = html;
|
|
row.appendChild(b); $('chat').appendChild(row); $('chat').scrollTop = 1e9;
|
|
}
|
|
const sys = t => bubble('sys', t);
|
|
|
|
async function reset() {
|
|
sid = null; ended = false; $('chat').innerHTML = ''; $('controls').innerHTML = '';
|
|
sys(`새 협상 · ${$('tenant').value} · ${$('rq').value} · 앵커 ${$('anchor').value} → 목표 ${$('target').value}`);
|
|
await send(null);
|
|
}
|
|
|
|
async function send(userInput) {
|
|
if (ended) return;
|
|
const body = { session_id: sid, rq_type: $('rq').value, user_input: userInput,
|
|
anchor_price: Number($('anchor').value), target_price: Number($('target').value) };
|
|
let res;
|
|
try {
|
|
const r = await fetch('/v1/chat', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': $('tenant').value },
|
|
body: JSON.stringify(body) });
|
|
res = await r.json();
|
|
} catch (e) { sys('서버 호출 실패: ' + e); return; }
|
|
|
|
sid = res.session_id;
|
|
if (res.result && !res.result.success) { sys('처리 불가: ' + res.result.desc + ' — ' + (res.msg||'')); return; }
|
|
|
|
let html = (res.script || '').replace(/\n/g, '<br>');
|
|
if (res.card_id) {
|
|
html += `<div class="card">🃏 제시 카드 <b>${res.card_id}</b> · 학습 Q→${(res.updated_q||0).toFixed(3)} · visit ${res.visit_count||0} · reward ${(res.reward_total!=null?res.reward_total.toFixed(3):'-')}</div>`;
|
|
}
|
|
if (res.outcome) {
|
|
html += `<div class="card">📌 협상 ${res.outcome==='success'?'타결':'결렬'} · 종료보상 ${(res.reward_total!=null?res.reward_total.toFixed(3):'-')}</div>`;
|
|
}
|
|
bubble('agent', html);
|
|
renderControls(res);
|
|
if (res.chat_end) { ended = true; $('controls').innerHTML = '<span style="color:#6b7890;font-size:12px">대화가 종료되었습니다. "새 협상 시작"을 누르세요.</span>'; }
|
|
}
|
|
|
|
function renderControls(res) {
|
|
const c = $('controls'); c.innerHTML = '';
|
|
if (res.chat_end) return;
|
|
const mode = res.input_mode;
|
|
if (mode === 'price') {
|
|
const inp = document.createElement('input'); inp.type = 'number'; inp.className = 'price-in'; inp.placeholder = '제시 가격 입력'; inp.value = '';
|
|
const btn = document.createElement('button'); btn.className = 'btn-send'; btn.textContent = '제시';
|
|
const go = () => { const v = inp.value.trim(); if (!v) return; userSay(v + '원 제시'); send(v); };
|
|
btn.onclick = go; inp.onkeydown = e => { if (e.key === 'Enter') go(); };
|
|
c.appendChild(inp); c.appendChild(btn); inp.focus();
|
|
} else if (Array.isArray(res.input_options) && res.input_options.length) {
|
|
res.input_options.forEach((opt, i) => {
|
|
const btn = document.createElement('button'); btn.className = 'opt' + (i > 0 ? ' alt' : '');
|
|
btn.textContent = opt; btn.onclick = () => { userSay(opt); send(opt); };
|
|
c.appendChild(btn);
|
|
});
|
|
} else {
|
|
const btn = document.createElement('button'); btn.className = 'opt'; btn.textContent = '계속';
|
|
btn.onclick = () => send(null); c.appendChild(btn);
|
|
}
|
|
}
|
|
function userSay(t){ bubble('user', t); }
|
|
|
|
reset();
|
|
</script>
|
|
</body>
|
|
</html>
|