forked from kimhyungsik/ax_hub_mcp_tool
feat: Add playground.html for dynamic tool testing
This commit is contained in:
515
axhub-gateway/src/main/resources/static/playground.html
Normal file
515
axhub-gateway/src/main/resources/static/playground.html
Normal file
@@ -0,0 +1,515 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Shinhan AI Tool Playground</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Pretendard:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Pretendard', 'sans-serif'],
|
||||
outfit: ['Outfit', 'sans-serif'],
|
||||
mono: ['Fira Code', 'monospace']
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
900: '#1e3a8a',
|
||||
950: '#172554',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'float': 'float 8s ease-in-out infinite',
|
||||
'float-delayed': 'float 8s ease-in-out 4s infinite',
|
||||
'spin-slow': 'spin 3s linear infinite',
|
||||
'pulse-glow': 'pulse-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'float': {
|
||||
'0%, 100%': { transform: 'translateY(0px) scale(1)' },
|
||||
'50%': { transform: 'translateY(-20px) scale(1.05)' },
|
||||
},
|
||||
'pulse-glow': {
|
||||
'0%, 100%': { opacity: 0.5, transform: 'scale(1)' },
|
||||
'50%': { opacity: 1, transform: 'scale(1.05)' },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #020617; /* slate-950 */
|
||||
color: #f8fafc;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-size: 40px 40px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
mask-image: radial-gradient(circle at center, black 30%, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(circle at center, black 30%, transparent 80%);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.input-premium {
|
||||
background: rgba(30, 41, 59, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #f8fafc;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.input-premium:focus {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select.input-premium option, select.input-premium optgroup {
|
||||
background: #1e293b;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.json-pre {
|
||||
background: transparent;
|
||||
color: #cbd5e1;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.btn-glow {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.btn-glow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899, #3b82f6);
|
||||
background-size: 200% auto;
|
||||
z-index: -1;
|
||||
border-radius: 12px;
|
||||
filter: blur(10px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
animation: gradient-shift 3s linear infinite;
|
||||
}
|
||||
.btn-glow:hover::before {
|
||||
opacity: 0.8;
|
||||
}
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
#loadingOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
#loadingOverlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.modern-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(59, 130, 246, 0.1);
|
||||
border-top-color: #3b82f6;
|
||||
border-right-color: #8b5cf6;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: rgba(15, 23, 42, 0.5); }
|
||||
::-webkit-scrollbar-thumb { background: rgba(71, 85, 105, 0.8); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(100, 116, 139, 1); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="antialiased selection:bg-brand-500/30 selection:text-brand-50">
|
||||
|
||||
<div class="bg-grid"></div>
|
||||
|
||||
<div class="fixed top-0 left-[20%] w-[500px] h-[500px] bg-brand-600/20 rounded-full mix-blend-screen filter blur-[120px] animate-float pointer-events-none"></div>
|
||||
<div class="fixed bottom-0 right-[20%] w-[600px] h-[600px] bg-purple-600/20 rounded-full mix-blend-screen filter blur-[150px] animate-float-delayed pointer-events-none"></div>
|
||||
|
||||
<div class="max-w-[850px] w-full px-5 z-10 relative flex flex-col">
|
||||
|
||||
<div class="text-center space-y-4 mb-8 shrink-0 pt-6 relative">
|
||||
<div class="absolute right-0 top-2 flex items-center bg-slate-800/50 border border-slate-700/50 rounded-lg px-3 py-1.5 shadow-sm">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
<input type="text" id="agentId" name="agentId" value="TESTER-DEV" class="bg-transparent border-none text-xs text-slate-300 font-mono focus:outline-none w-20 text-right" readonly>
|
||||
</div>
|
||||
<div class="inline-flex items-center px-3 py-1 rounded-full border border-purple-500/30 bg-purple-500/10 text-purple-400 text-[10px] font-bold uppercase tracking-[0.2em] mb-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mr-2 animate-pulse"></span>
|
||||
Developer Sandbox
|
||||
</div>
|
||||
<h1 class="text-5xl font-outfit font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-br from-white via-slate-200 to-slate-500 drop-shadow-sm">
|
||||
Tool Playground
|
||||
</h1>
|
||||
<p class="text-sm text-slate-400 font-medium tracking-wide">
|
||||
동적 파라미터 입력 및 실데이터 기반 연동 테스트 UI
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel rounded-[2rem] p-8 relative shrink-0">
|
||||
|
||||
<div id="loadingOverlay">
|
||||
<div class="relative w-16 h-16 mb-6">
|
||||
<div class="modern-spinner absolute inset-0"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-brand-400 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-outfit font-semibold text-white tracking-wider">처리중 입니다...</h3>
|
||||
<p class="text-slate-400 text-sm mt-2 font-medium">실데이터 기반 통신 중입니다</p>
|
||||
</div>
|
||||
|
||||
<form id="mcpForm" class="space-y-6">
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="group">
|
||||
<label for="toolName" class="flex items-center text-xs font-bold text-slate-400 mb-2 uppercase tracking-widest transition-colors group-focus-within:text-brand-400">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
테스트할 대상 툴 (Function Tool)
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select id="toolName" name="toolName" class="input-premium block w-full pl-4 pr-10 py-3 text-sm rounded-xl appearance-none cursor-pointer font-medium">
|
||||
<option value="">로딩 중...</option>
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-500">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEW: Dynamic Form Container -->
|
||||
<div id="dynamicFormContainer" class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-4 p-5 bg-slate-900/40 rounded-xl border border-slate-700/50 hidden">
|
||||
<div class="col-span-full">
|
||||
<h3 class="text-sm font-bold text-slate-300 mb-2 border-b border-white/5 pb-2">파라미터 입력 (Parameters)</h3>
|
||||
</div>
|
||||
<!-- Fields will be dynamically injected here -->
|
||||
</div>
|
||||
|
||||
<div class="group pt-2">
|
||||
<label for="userPrompt" class="flex items-center text-xs font-bold text-slate-400 mb-2 uppercase tracking-widest transition-colors group-focus-within:text-brand-400">
|
||||
<svg class="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
|
||||
가상 유저 프롬프트 (Mock User Prompt)
|
||||
</label>
|
||||
<textarea id="userPrompt" name="userPrompt" rows="1" class="input-premium block w-full py-3 px-4 text-sm rounded-xl resize-none placeholder-slate-600 font-medium" placeholder="[테스트용] 사용자 발화 내용을 입력하세요."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4 mt-2 border-t border-white/5">
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs font-mono bg-slate-900/80 px-3 py-2 rounded-lg border border-slate-700/50 flex items-center shadow-inner">
|
||||
<span class="text-emerald-400 font-bold mr-2 text-[10px] tracking-wider">POST</span>
|
||||
<span class="text-slate-300">/mcp/api/v1/tools/call</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-glow">
|
||||
<button type="submit" class="relative inline-flex items-center px-8 py-3 text-sm font-bold rounded-xl text-white bg-gradient-to-r from-purple-600 to-brand-600 hover:from-purple-500 hover:to-brand-500 shadow-lg shadow-purple-500/25 transition-all transform hover:-translate-y-0.5 active:translate-y-0">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
Execute Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="resultContainer" class="hidden transition-all duration-500 ease-out opacity-0 translate-y-8 mt-6 pb-6">
|
||||
<div class="bg-[#0d1117] rounded-2xl shadow-2xl border border-slate-700/50 flex flex-col overflow-hidden relative group">
|
||||
<div class="bg-[#161b22] px-5 py-3 flex justify-between items-center border-b border-slate-800 shrink-0">
|
||||
<div class="flex space-x-2">
|
||||
<div class="w-3 h-3 rounded-full bg-[#ff5f56] border border-[#e0443e]"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dea123]"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-[#27c93f] border border-[#1aab29]"></div>
|
||||
</div>
|
||||
<div class="text-[10px] font-mono font-medium text-slate-500 tracking-widest uppercase">Test Result</div>
|
||||
<span id="statusBadge" class="inline-flex items-center px-2.5 py-1 rounded-md text-[10px] font-bold font-mono hidden"></span>
|
||||
</div>
|
||||
<div class="p-6 relative">
|
||||
<pre id="jsonResponse" class="json-pre m-0 whitespace-pre-wrap font-mono relative z-10 font-medium"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let actionPrompts = {};
|
||||
let currentSchema = null;
|
||||
|
||||
const toolSelect = document.getElementById('toolName');
|
||||
const dynamicFormContainer = document.getElementById('dynamicFormContainer');
|
||||
|
||||
async function fetchTools() {
|
||||
try {
|
||||
const response = await fetch('/mcp/api/v1/tools/list', {
|
||||
headers: { 'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999' }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
toolSelect.innerHTML = '<option value="" disabled selected>테스트할 툴을 선택하세요...</option>';
|
||||
actionPrompts = {};
|
||||
|
||||
const tools = data.result?.tools || [];
|
||||
|
||||
if (tools.length === 0) {
|
||||
toolSelect.innerHTML = '<option value="">등록된 툴이 없습니다</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedTools = {};
|
||||
tools.forEach(tool => {
|
||||
const group = tool.domainGroup || '기타 그룹';
|
||||
if (!groupedTools[group]) groupedTools[group] = [];
|
||||
groupedTools[group].push(tool);
|
||||
});
|
||||
|
||||
Object.keys(groupedTools).sort().forEach(group => {
|
||||
const optgroup = document.createElement('optgroup');
|
||||
optgroup.label = group;
|
||||
|
||||
groupedTools[group].forEach(tool => {
|
||||
const option = document.createElement('option');
|
||||
option.value = tool.toolName;
|
||||
option.textContent = tool.description ? `[${tool.integrationType || 'HTTP'}] ${tool.description} (${tool.toolName})` : tool.toolName;
|
||||
option.dataset.schema = JSON.stringify(tool.parametersSchema);
|
||||
option.dataset.prompts = JSON.stringify(tool.actionPrompts || {});
|
||||
optgroup.appendChild(option);
|
||||
|
||||
if (tool.actionPrompts) {
|
||||
Object.assign(actionPrompts, tool.actionPrompts);
|
||||
}
|
||||
});
|
||||
toolSelect.appendChild(optgroup);
|
||||
});
|
||||
} catch (err) {
|
||||
toolSelect.innerHTML = '<option value="">데이터 로드 실패</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDynamicForm() {
|
||||
// Remove old fields except the header
|
||||
const fields = dynamicFormContainer.querySelectorAll('.dynamic-field');
|
||||
fields.forEach(f => f.remove());
|
||||
|
||||
const selectedOption = toolSelect.options[toolSelect.selectedIndex];
|
||||
if (!selectedOption || !selectedOption.dataset.schema) {
|
||||
dynamicFormContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaStr = selectedOption.dataset.schema;
|
||||
if (schemaStr === "null") {
|
||||
dynamicFormContainer.classList.add('hidden');
|
||||
currentSchema = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(schemaStr);
|
||||
currentSchema = schema;
|
||||
|
||||
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
||||
dynamicFormContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicFormContainer.classList.remove('hidden');
|
||||
|
||||
for (const [key, value] of Object.entries(schema.properties)) {
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = 'group dynamic-field';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center text-xs font-bold text-slate-400 mb-2 transition-colors group-focus-within:text-brand-400';
|
||||
label.textContent = `${key} ${value.description ? ' (' + value.description + ')' : ''}`;
|
||||
fieldDiv.appendChild(label);
|
||||
|
||||
let input;
|
||||
if (value.type === 'boolean') {
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.id = 'param_' + key;
|
||||
input.className = 'w-4 h-4 text-brand-500 bg-slate-800 border-slate-600 rounded focus:ring-brand-500 focus:ring-2 mt-2 block';
|
||||
} else {
|
||||
input = document.createElement('input');
|
||||
input.type = value.type === 'number' || value.type === 'integer' ? 'number' : 'text';
|
||||
input.id = 'param_' + key;
|
||||
input.className = 'input-premium block w-full py-2 px-3 text-sm rounded-lg placeholder-slate-600 font-medium';
|
||||
input.placeholder = `${key} 입력...`;
|
||||
}
|
||||
fieldDiv.appendChild(input);
|
||||
dynamicFormContainer.appendChild(fieldDiv);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Schema parse error", e);
|
||||
dynamicFormContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePrompt() {
|
||||
const toolName = toolSelect.value;
|
||||
document.getElementById('userPrompt').value = actionPrompts[toolName] || "테스트 실행";
|
||||
}
|
||||
|
||||
toolSelect.addEventListener('change', () => {
|
||||
updatePrompt();
|
||||
renderDynamicForm();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', fetchTools);
|
||||
|
||||
function collectArguments() {
|
||||
if (!currentSchema || !currentSchema.properties) return {};
|
||||
const args = {};
|
||||
for (const [key, value] of Object.entries(currentSchema.properties)) {
|
||||
const input = document.getElementById('param_' + key);
|
||||
if (input) {
|
||||
if (value.type === 'boolean') {
|
||||
args[key] = input.checked;
|
||||
} else if (value.type === 'number' || value.type === 'integer') {
|
||||
args[key] = input.value ? Number(input.value) : 0;
|
||||
} else {
|
||||
args[key] = input.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
document.getElementById('mcpForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
if(!toolSelect.value) {
|
||||
alert("테스트할 툴을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const jsonResponse = document.getElementById('jsonResponse');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
|
||||
resultContainer.classList.add('opacity-0', 'translate-y-8');
|
||||
setTimeout(() => resultContainer.classList.add('hidden'), 300);
|
||||
|
||||
loadingOverlay.classList.add('active');
|
||||
|
||||
const actualArguments = collectArguments();
|
||||
|
||||
const payload = {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: toolSelect.value,
|
||||
arguments: actualArguments
|
||||
},
|
||||
id: Math.floor(Math.random() * 10000)
|
||||
};
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await fetch('/mcp/api/v1/tools/call', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': 'SHINHAN_MCP_TEST_KEY_9999',
|
||||
'X-Trace-Id': "MCP-REQ-" + Math.floor(Math.random() * 90000 + 10000),
|
||||
'X-Agent-Id': document.getElementById('agentId').value,
|
||||
'X-User-Prompt': encodeURIComponent(document.getElementById('userPrompt').value || '테스트')
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
let formattedJson = JSON.stringify(data, null, 4)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
let cls = 'text-slate-300';
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) cls = 'text-cyan-400 font-medium';
|
||||
else cls = 'text-emerald-400';
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'text-amber-400 font-medium';
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'text-slate-500 italic';
|
||||
} else {
|
||||
cls = 'text-pink-400';
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
});
|
||||
|
||||
jsonResponse.innerHTML = formattedJson;
|
||||
|
||||
statusBadge.classList.remove('hidden');
|
||||
if(response.ok) {
|
||||
statusBadge.innerHTML = `200 OK <span class="ml-2 opacity-60 font-medium">${latency}ms</span>`;
|
||||
statusBadge.className = 'inline-flex items-center px-2.5 py-1 rounded-md text-[10px] font-bold font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20';
|
||||
} else {
|
||||
statusBadge.innerHTML = `${response.status} ERR <span class="ml-2 opacity-60 font-medium">${latency}ms</span>`;
|
||||
statusBadge.className = 'inline-flex items-center px-2.5 py-1 rounded-md text-[10px] font-bold font-mono bg-rose-500/10 text-rose-400 border border-rose-500/20';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
jsonResponse.textContent = "Error: " + error.message;
|
||||
statusBadge.innerHTML = `FAILED`;
|
||||
statusBadge.className = 'inline-flex items-center px-2.5 py-1 rounded-md text-[10px] font-bold font-mono bg-rose-500/10 text-rose-400 border border-rose-500/20';
|
||||
statusBadge.classList.remove('hidden');
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
loadingOverlay.classList.remove('active');
|
||||
resultContainer.classList.remove('hidden');
|
||||
void resultContainer.offsetWidth;
|
||||
resultContainer.classList.remove('opacity-0', 'translate-y-8');
|
||||
}, 800);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user