humanda5
api key ๋ณ€๊ฒฝ ui ์ถ”๊ฐ€
00b7ce9
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>์›”๋“œ ๊ด€์ฐฐ์ž ๋Œ€์‹œ๋ณด๋“œ</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<style>
/* ๊ธฐ๋ณธ ์Šคํƒ€์ผ ๋ฐ ๋ณ€์ˆ˜ */
:root { --bg-color: #f0f2f5; --panel-bg-color: #ffffff; --border-color: #2c3e50; --border-color: #dee2e6; --shadow: 0 4px 6px rgba(0,0,0,0.05); --primary-color: #007bff; --success-color: #28a745; --info-color: #17a2b8; --secondary-color: #6c757d; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; display: flex; height: 100vh; margin: 0; background-color: var(--bg-color); }
#main-container { display: flex; flex: 1; padding: 15px; gap: 15px; min-height: 0;}
#network-panel { flex: 3; display: flex; flex-direction: column; background: var(--panel-bg-color); border-radius: 12px; box-shadow: var(--shadow); min-width: 0; }
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); min-height: 0; }
/* ํ”Œ๋ ˆ์ด์–ด ๋Œ€ํ™” */
#player-input-area { padding: 10px; background-color: #eefc2a; border-top: 2px solid #ffeeba; }
#player-input-area p { margin: 0 0 10px 0; font-weight: 500; font-size: 14px; }
.player-input-wrapper { display: flex; gap: 10px }
#player-input-text { flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; }
#player-send-btn { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: white; background-color: var(--primary-color); }
/* API ์„ค์ • */
#api-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 1000; }
#api-modal-content { background: white; padding: 30px; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 400px; text-align: center; }
#api-modal-content h2 { margin-top: 0; }
#api-modal-content select, #api-modal-content input, #api-modal-content button { width: 100%; padding: 12px; margin-top: 10px; box-sizing: border-box; border-radius: 6px; border: 1px solid #ccc; }
#api-modal-content button { background: var(--primary-color); color: white; cursor: pointer; font-weight: bold; }
.hidden { display: none !important; }
/* ์ „์ฒด ์ปจํŠธ๋กค ํŒจ๋„ (๊ฐ€์žฅ ๋ฐ”๊นฅ์ชฝ) */
#controls { flex-shrink: 0; padding: 12px; display: flex; flex-direction: column; gap: 15px; background-color: #3e3f41; border-radius: 0 0 12px 12px;}
/* ๋ฉ”์ธ ์ปจํŠธ๋กค ๋ฒ„ํŠผ */
#main-controls { display: flex; gap: 10px; justify-content: flex-start;}
#main-controls button { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: rgb(0, 0, 0); font-size: 14px; font-weight: 500; transition: background-color 0.2s;}
#play-pause-btn.playing { background-color: var(--primary-color); }
#play-pause-btn.paused { background-color: var(--success-color); }
#tick-btn { background-color: var(--secondary-color); }
/* ๊ฐœ์ž… ๊ธฐ๋Šฅ ์„ ํƒ ํƒญ*/
#intervention-area { display: flex; gap: 15px; align-items: flex-start;}
#intervention-tabs { display: flex; flex-direction: column; gap: 10px;}
.tab-btn { padding: 8px 15px; border-radius: 6px; border: 1px solid var(--border-color); cursor: pointer; font-weight: 500; color: #495057; text-align: center;}
.tab-btn.active { background-color: var(--primary-color); color: white; border-color: var(--primary-color);}
/* ๊ฐœ์ž… ๊ธฐ๋Šฅ ํŒจ๋„ */
#intervention-panels { flex: 1; }
.control-panel { display: none; flex-direction: column; gap: 10px; height: 100%; }
.control-panel.active { display: flex; }
.control-panel select, .control-panel input, .control-panel button { padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; font-size: 14px; width: 100%; box-sizing: border-box; }
.control-panel button { background-color: var(--info-color); color: white; cursor: pointer; }
#force-relationship-btn { background-color: var(--success-color); }
/* ์‚ฌ์ด๋“œ ํŒจ๋„ ๋“ฑ */
#side-panel { flex: 1; display: flex; flex-direction: column; gap: 15px; min-width: 300px; max-width: 400px; }
.info-panel { background: var(--panel-bg-color); padding: 20px; border-radius: 12px; box-shadow: var(--shadow); overflow-y: auto; }
#npc-details { flex-basis: 45%; }
#log-panel { flex-basis: 55%; display: flex; flex-direction: column; }
#log-container { flex: 1; overflow-y: auto; font-size: 13px; line-height: 1.6; color: #495057; white-space: pre-wrap; }
h2 { margin-top: 0; margin-bottom: 15px; font-size: 18px; color: #343a40; }
</style>
</head>
<body>
<div id="api-modal-overlay">
<!-- API ํ‚ค ์ž…๋ ฅ_1 -->
<div id="api-modal-content">
<h2>์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹œ์ž‘</h2>
<p>์‚ฌ์šฉํ•  LLM ๋ชจ๋ธ๊ณผ API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.</p>
<select id="llm-model-select">
<option value="gemini-2.0-flash">[์ถ”์ฒœ] Gemini 2.0 Flash</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
</select>
<input type="password" id="api-key-input" placeholder="API Key๋ฅผ ์—ฌ๊ธฐ์— ์ž…๋ ฅํ•˜์„ธ์š”">
<button id="start-simulation-btn">์‹œ์ž‘ํ•˜๊ธฐ</button>
<p id="api-modal-message" style="color: red; margin-top: 10px;"></p>
</div>
</div>
<div id="main-container">
<div id="network-panel">
<div id="network"></div>
<div id="player-input-area" style="display: none;">
<p id="player-prompt-text"></p>
<div class="player-input-wrapper">
<input type="text" id="player-input-text" placeholder="๋Œ€์‚ฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...">
<button id="player-send-btn">์ „์†ก</button>
</div>
</div>
<div id="controls" style="flex-direction: column; align-items: stretch;">
<!-- ๋ฉ”์ธ ์ปจํŠธ๋กค -->
<div id="main-controls" style="display: flex; gap: 10px; justify-content: flex-start;">
<button id="play-pause-btn">Play / Pause</button>
<button id="tick-btn">Next Tick</button>
<button id="player-toggle-btn">ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ: ๋น„ํ™œ์„ฑํ™”</button>
<button id="fit-btn">๊ด€๊ณ„๋„ ๋ณด๊ธฐ</button>
<button id="api-settings-toggle-btn" style="margin-left: auto;">API ์„ค์ •</button>
</div>
<!-- API ํ‚ค ์ž…๋ ฅ_2 -->
<div id="ingame-api-panel" class="control-panel" style="display: none; flex-direction: row; gap: 10px; background-color: #e9ecef; padding: 10px; border-radius: 8px;">
<select id="ingame-llm-model-select" style="flex: 1;">
<option value="gemini-2.0-flash">[์ถ”์ฒœ] Gemini 2.0 Flash</option>
<option value="gemini-2.5-flash">Gemini 2.5 Flash</option>
<option value="gemini-2.5-pro">Gemini 2.5 Pro</option>
</select>
<input type="password" id="ingame-api-key-input" placeholder="API Key๋ฅผ ์—ฌ๊ธฐ์— ์ž…๋ ฅํ•˜์„ธ์š”" style="flex: 2;">
<button id="ingame-api-key-save-btn" style="flex: 1;">API ๋ณ€๊ฒฝ</button>
</div>
<!-- ๊ฐœ์ž… ๊ธฐ๋Šฅ ์„ ํƒ -->
<div id="intervention-area" style="display: flex; gap: 15px; align-items: flex-start;">
<div id="intervention-tabs">
<button id="tab-event" class="tab-btn active">์ด๋ฒคํŠธ ์ฃผ์ž…</button>
<button id="tab-relation" class="tab-btn">๊ด€๊ณ„ ์„ค์ •</button>
<button id="tab-orchestrate" class="tab-btn">์ƒํ™ฉ ์—ฐ์ถœ</button>
</div>
<!-- ๊ฐœ์ž… ๊ธฐ๋Šฅ ํŒจ๋„ -->
<div id="intervention-panels">
<div id="panel-event" class="control-panel active">
<select id="event-npc-select"></select>
<input type="text" id="event-text-input" placeholder="์ด๋ฒคํŠธ ๋‚ด์šฉ">
<button id="inject-event-btn">์ด๋ฒคํŠธ ์ฃผ์ž…</button>
</div>
<div id="panel-relation" class="control-panel">
<select id="relation-npc1-select"></select>
<select id="relation-npc2-select"></select>
<select id="relationship-type-select">
<option value="best friend">๋งค์šฐ ์นœํ•œ ์นœ๊ตฌ</option>
<option value="friend">์นœ๊ตฌ</option>
<option value="acquaintance">์ง€์ธ</option>
<option value="stranger">๋‚ฏ์„  ์‚ฌ๋žŒ</option>
<option value="nuisance">๋ถˆํŽธํ•œ ์‚ฌ๋žŒ</option>
<option value="rival">์‹ซ์€ ์‚ฌ๋žŒ</option>
<option value="enemy">์ </option>
</select>
<button id="force-relationship-btn">๊ด€๊ณ„ ์„ค์ •</button>
</div>
<div id="panel-orchestrate" class="control-panel">
<select id="orch-npc1-select" title="๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•  NPC"></select>
<select id="orch-npc2-select" title="๋Œ€ํ™” ์ƒ๋Œ€ NPC"></select>
<input type="text" id="situation-text-input" placeholder="๋Œ€ํ™” ์ฃผ์ œ ๋˜๋Š” ์ƒํ™ฉ">
<button id="orchestrate-btn">๋Œ€ํ™” ์‹œ์ž‘</button>
</div>
</div>
</div>
</div>
</div>
<div id="side-panel">
<div id="npc-details" class="info-panel">
<h2>NPC ์ƒ์„ธ ์ •๋ณด</h2>
<div id="npc-details-content">๊ด€๊ณ„๋„์—์„œ NPC๋ฅผ ์„ ํƒํ•˜์„ธ์š”.</div>
</div>
<div id="log-panel" class="info-panel">
<h2>์ด๋ฒคํŠธ ๋กœ๊ทธ</h2>
<div id="log-container">(๋กœ๊ทธ ๋กœ๋”ฉ ์ค‘...)</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async function () {
// API ์ž…๋ ฅ
const apiModalOverlay = document.getElementById('api-modal-overlay');
const llmModelSelect = document.getElementById('llm-model-select');
const apiKeyInput = document.getElementById('api-key-input');
const startSimBtn = document.getElementById('start-simulation-btn');
const apiModalMsg = document.getElementById('api-modal-message');
// API ์ž…๋ ฅ 2
const apiSettingsToggleBtn = document.getElementById('api-settings-toggle-btn');
const ingameApiPanel = document.getElementById('ingame-api-panel');
const ingameLlmModelSelect = document.getElementById('ingame-llm-model-select');
const ingameApiKeyInput = document.getElementById('ingame-api-key-input');
const ingameApiKeySaveBtn = document.getElementById('ingame-api-key-save-btn');
// UI ์š”์†Œ ๊ฐ€์ ธ์˜ค๊ธฐ
const networkContainer = document.getElementById('network');
const logContainer = document.getElementById('log-container');
const detailsContainer = document.getElementById('npc-details-content');
// ๋ฉ”์ธ ์ปจํŠธ๋กค
const playPauseBtn = document.getElementById('play-pause-btn');
const tickBtn = document.getElementById('tick-btn');
const playerToggleBtn = document.getElementById('player-toggle-btn');
const fitBtn = document.getElementById('fit-btn')
// player ๋Œ€์‚ฌ ์ž…๋ ฅ
const playerInputArea = document.getElementById('player-input-area');
const playerPromptText = document.getElementById('player-prompt-text');
const playerInputText = document.getElementById('player-input-text');
const playerSendBtn = document.getElementById('player-send-btn');
// ๊ฐœ์ž… ๊ธฐ๋Šฅ ์„ ํƒ
const tabEvent = document.getElementById('tab-event');
const tabRelation = document.getElementById('tab-relation');
const panelEvent = document.getElementById('panel-event');
const panelRelation = document.getElementById('panel-relation');
// ์ด๋ฒคํŠธ ์ฃผ์ž…
const injectEventBtn = document.getElementById('inject-event-btn');
const eventNpcSelect = document.getElementById('event-npc-select');
const eventTextInput = document.getElementById('event-text-input');
// ๊ด€๊ณ„ ์„ค์ •
const relationNpc1Select = document.getElementById('relation-npc1-select');
const relationNpc2Select = document.getElementById('relation-npc2-select');
const relationshipTypeSelect = document.getElementById('relationship-type-select');
const forceRelationshipBtn = document.getElementById('force-relationship-btn');
// ๋Œ€ํ™” ์‹œ์ž‘ํ•˜๊ธฐ
const tabOrchestrate = document.getElementById('tab-orchestrate');
const panelOrchestrate = document.getElementById('panel-orchestrate');
const orchNpc1Select = document.getElementById('orch-npc1-select');
const orchNpc2Select = document.getElementById('orch-npc2-select');
const situationTextInput = document.getElementById('situation-text-input');
const orchestrateBtn = document.getElementById('orchestrate-btn');
let network = null;
let nodesDataSet = new vis.DataSet([]);
let edgesDataSet = new vis.DataSet([]);
let autoUpdateInterval = null;
const options = {
physics: { stabilization: {iterations: 1000}, solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -80, springLength: 150, springConstant: 0.08 } },
nodes: { borderWidth: 2, shape: 'circularImage', size: 30, font: { size: 14, color: '#333' } },
edges: { width: 2, font: { align: 'top', size: 11, strokeWidth: 3, strokeColor: 'white' }, smooth: { type: 'cubicBezier' } },
interaction: { hover: true, zoomView: true, dragView: true, minZoom: 0.2, maxZoom: 4.0 }
};
// ์‹œ์ž‘ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
startSimBtn.addEventListener('click', async () => {
const model_name = llmModelSelect.value;
const api_key = apiKeyInput.value;
if (!api_key) {
apiModalMsg.textContent = 'API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.';
return;
}
startSimBtn.textContent = '์ธ์ฆ ์ค‘...';
startSimBtn.disabled = true;
apiModalMsg.textContent = '';
const response = await fetch('/api/initialize_simulation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_name, api_key })
});
const data = await response.json();
if (data.success) {
apiModalOverlay.classList.add('hidden'); // ๋ชจ๋‹ฌ ์ˆจ๊ธฐ๊ธฐ
// ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‹œ์ž‘๋˜์—ˆ์œผ๋ฏ€๋กœ, ์ด์ œ๋ถ€ํ„ฐ ์›”๋“œ ์ƒํƒœ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
await updateWorld();
if (!autoUpdateInterval) { // ์ค‘๋ณต ์‹คํ–‰ ๋ฐฉ์ง€
autoUpdateInterval = setInterval(updateWorld, 5000);
}
} else {
apiModalMsg.textContent = `์˜ค๋ฅ˜: ${data.error}`;
startSimBtn.textContent = '์‹œ์ž‘ํ•˜๊ธฐ';
startSimBtn.disabled = false;
}
});
fitBtn.addEventListener('click', () => {
if (network) {
network.fit();
}
});
// ํƒญ ๋ฒ„ํŠผ ํด๋ฆญ ์ด๋ฒคํŠธ
tabEvent.addEventListener('click', () =>{
tabEvent.classList.add('active');
panelEvent.classList.add('active');
tabRelation.classList.remove('active');
panelRelation.classList.remove('active');
tabOrchestrate.classList.remove('active');
panelOrchestrate.classList.remove('active');
});
tabRelation.addEventListener('click', () => {
tabEvent.classList.remove('active');
panelEvent.classList.remove('active');
tabRelation.classList.add('active');
panelRelation.classList.add('active');
tabOrchestrate.classList.remove('active');
panelOrchestrate.classList.remove('active');
});
// ์ƒํ™ฉ ์—ฐ์ถœ ํƒญ ํด๋ฆญ ์ด๋ฒคํŠธ
tabOrchestrate.addEventListener('click', () => {
tabEvent.classList.remove('active');
panelEvent.classList.remove('active');
tabRelation.classList.remove('active');
panelRelation.classList.remove('active');
tabOrchestrate.classList.add('active');
panelOrchestrate.classList.add('active');
});
// ๋Œ€ํ™” ์‹œ์ž‘ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
orchestrateBtn.addEventListener('click', async () => {
const npc1_name = orchNpc1Select.value;
const npc2_name = orchNpc2Select.value;
const situation = situationTextInput.value;
if (!npc1_name || !npc2_name || !situation || npc1_name === npc2_name) {
return alert ('์„œ๋กœ ๋‹ค๋ฅธ ๋‘ NPC์™€ ์ƒํ™ฉ์„ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
}
await fetch('/api/orchestrate_conversation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npc1_name, npc2_name, situation })
});
await updateWorld();
});
async function updateWorld() {
try {
const response = await fetch('/api/world_state');
const data = await response.json();
// ์„œ๋ฒ„๊ฐ€ ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Œ์„ ํ™•์ธํ•˜๊ณ  ๋Œ€๊ธฐ
if (data.status === 'needs_initialization') {
console.log("์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ดˆ๊ธฐํ™” ๋Œ€๊ธฐ ์ค‘...");
updateLog(data.log); // ์ดˆ๊ธฐ ๋กœ๊ทธ๋Š” ํ‘œ์‹œ
return;
}
if (data.error) throw new Error(data.error);
// ๋ฐ์ดํ„ฐ์…‹์„ ์ง์ ‘ ์—…๋ฐ์ดํŠธํ•˜๋ฉด vis-network๊ฐ€ ์•Œ์•„์„œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ฐ์ง€ํ•˜๊ณ  ๋‹ค์‹œ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.
nodesDataSet.update(data.nodes);
edgesDataSet.update(data.edges);
// ํ”Œ๋ ˆ์ด์–ด ์ž…๋ ฅ ๋Œ€๊ธฐ ์ƒํƒœ์— ๋”ฐ๋ผ UI ๋ณ€๊ฒฝ
if (data.waiting_for_player) {
playerInputArea.style.display = 'block';
const info = data.player_conversation;
// ์ƒ๋Œ€๋ฐฉ์˜ ๋งˆ์ง€๋ง‰ ๋ง์„ ํ”„๋กฌํ”„ํŠธ๋กœ ํ‘œ์‹œ
playerPromptText.textContent = `${info.last_utterance}`;
playerPauseBtn.disabled = true;
} else {
playerInputArea.style.display = 'none';
playPauseBtn.disabled = false;
}
if (!network) {
const networkData = { nodes: nodesDataSet, edges: edgesDataSet };
network = new vis.Network(networkContainer, networkData, options);
network.on('click', onNodeClick);
// ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ์•ˆ์ •ํ™”๋˜๋ฉด ๋ฌผ๋ฆฌ ํšจ๊ณผ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ๋…ธ๋“œ ์›€์ง์ž„์„ ๋ฉˆ์ถฅ๋‹ˆ๋‹ค.
network.on("stabilizationIterationsDone", function () {
network.setOptions({ physics: false, interaction: { hover: true, zoomView: true, dragView: true, minZoom: 0.5, maxZoom: 2.0 }});
});
} else {
nodesDataSet.update(data.nodes);
edgesDataSet.update(data.edges);
// ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ํ›„, ๋„คํŠธ์›Œํฌ๋ฅผ ๋‹ค์‹œ ์•ˆ์ •ํ™”์‹œํ‚ค๊ณ  ๋ฌผ๋ฆฌ ํšจ๊ณผ ์ข…๋ฃŒ
network.stabilize();
network.setOptions({ physics: false });
}
updateNpcSelect(data.nodes);
updateLog(data.log);
updatePlayPauseButton(data.paused);
} catch (error) { console.error("์›”๋“œ ์—…๋ฐ์ดํŠธ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ:", error); }
}
async function onNodeClick(params) {
if (params.nodes.length > 0) {
const selectedNodeId = params.nodes[0];
// vis.DataSet์—์„œ ๋…ธ๋“œ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€ ํ•œ๊ธ€ ๋ผ๋ฒจ์„ ์ฐพ์Šต๋‹ˆ๋‹ค.
const selectedNodeObject = nodesDataSet.get(selectedNodeId);
if (!selectedNodeObject) {
console.error("ํด๋ฆญ๋œ ๋…ธ๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:", selectedNodeId);
return;
}
const npcName = selectedNodeObject.label; // ์˜์–ด ID๊ฐ€ ์•„๋‹Œ ํ•œ๊ธ€ ๋ผ๋ฒจ๋กœ ์š”์ฒญ
try {
const response = await fetch(`/api/npc_details/${npcName}`);
const details = await response.json();
let detailsHtml = `<h3>${details.name} (${details.job}, ${details.age}์„ธ)</h3>`;
detailsHtml += `<p><strong>์„ฑ๊ฒฉ:</strong> ${details.personality_summary}</p>`;
detailsHtml += `<p><strong>๋ชฉํ‘œ:</strong> ${details.goals}</p>`;
detailsHtml += `<strong>๊ฐ์ •:</strong><ul>${details.emotions.length > 0 ? details.emotions.map(e => `<li>${e[0]}: ${e[1].toFixed(1)}</li>`).join('') : '<li>ํ‰์˜จํ•จ</li>'}</ul>`;
detailsHtml += `<strong>์ตœ๊ทผ ๊ธฐ์–ต:</strong><ul>${details.memories.length > 0 ? details.memories.map(m => `<li>${m.substring(0, 80)}...</li>`).join('') : '<li>๊ธฐ์–ต ์—†์Œ</li>'}</ul>`;
detailsContainer.innerHTML = detailsHtml;
} catch (error) {
detailsContainer.innerHTML = `${npcName}์˜ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.`;
}
}
}
function updateNpcSelect(npcNodes) {
const selections = [eventNpcSelect, relationNpc1Select, relationNpc2Select, orchNpc1Select, orchNpc2Select];
selections.forEach(select => {
const currentVal = select.value;
select.innerHTML = '';
npcNodes.forEach(node => {
const option = document.createElement('option');
option.value = node.label;
option.textContent = node.label;
select.appendChild(option);
});
if (currentVal) select.value = currentVal;
})
}
function updateLog(logMessages) {
logContainer.textContent = logMessages.join('\n');
logContainer.scrollTop = logContainer.scrollHeight;
}
function updatePlayPauseButton(isPaused) {
playPauseBtn.textContent = isPaused ? 'โ–ถ Play' : 'โšโš Pause';
playPauseBtn.className = isPaused ? 'paused' : 'playing';
// ์ž๋™ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๋ณต๊ตฌ
if (!isPaused && !autoUpdateInterval) {
autoUpdateInterval = setInterval(updateWorld, 5000);
} else if (isPaused && autoUpdateInterval) {
clearInterval(autoUpdateInterval);
autoUpdateInterval = null;
}
}
forceRelationshipBtn.addEventListener('click', async () => {
const npc1_name = relationNpc1Select.value;
const npc2_name = relationNpc2Select.value;
const relationship_type = relationshipTypeSelect.value;
if (!npc1_name || !npc2_name || npc1_name === npc2_name) {
return alert('์„œ๋กœ ๋‹ค๋ฅธ ๋‘ ๋ช…์˜ NPC๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.');
}
await fetch('/api/force_relationship', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npc1_name, npc2_name, relationship_type })
});
await updateWorld();
});
// ์ธ๊ฒŒ์ž„ API ์„ค์ • ํŒจ๋„ ํ† ๊ธ€ ์ด๋ฒคํŠธ
apiSettingsToggleBtn.addEventListener('click', () => {
const isHidden = ingameApiPanel.style.display === 'none';
ingameApiPanel.style.display = isHidden ? 'flex' : 'none';
});
// ์ธ๊ฒŒ์ž„ 'API ๋ณ€๊ฒฝ' ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
ingameApiKeySaveBtn.addEventListener('click', async () => {
const model_name = ingameLlmModelSelect.value;
const api_key = ingameApiKeyInput.value;
if (!api_key) {
return alert('์ƒˆ๋กœ์šด API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
}
ingameApiKeySaveBtn.textContent = '๋ณ€๊ฒฝ ์ค‘...';
const response = await fetch('/api/set_llm_config', { // 2๋‹จ๊ณ„์—์„œ ๋งŒ๋“ค API
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model_name, api_key })
});
const data = await response.json();
if (data.success) {
alert(`API ์„ค์ •์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ๋ถ€ํ„ฐ [${model_name}] ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.`);
ingameApiKeyInput.value = ''; // ์ž…๋ ฅ์ฐฝ ๋น„์šฐ๊ธฐ
} else {
alert(`API ๋ณ€๊ฒฝ ์‹คํŒจ: ${data.error}`);
}
ingameApiKeySaveBtn.textContent = 'API ๋ณ€๊ฒฝ';
});
// ๋ชจ๋“  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ณต๊ตฌ
playPauseBtn.addEventListener('click', async () => {
const response = await fetch('/api/toggle_simulation', { method: 'POST' });
const data = await response.json();
updatePlayPauseButton(data.paused);
});
tickBtn.addEventListener('click', async () => { await fetch('/api/manual_tick', { method: 'POST' }); await updateWorld(); });
injectEventBtn.addEventListener('click', async () => {
const npc_name = eventNpcSelect.value; // ์ด์ œ ํ•œ๊ธ€ ์ด๋ฆ„์ด ์ „์†ก๋ฉ๋‹ˆ๋‹ค.
const event_text = eventTextInput.value;
if (!npc_name || !event_text) return alert('NPC์™€ ์ด๋ฒคํŠธ ๋‚ด์šฉ์„ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.');
await fetch('/api/inject_event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ npc_name, event_text })
});
eventTextInput.value = '';
await updateWorld();
});
playerToggleBtn.addEventListener('click', async () => {
const response = await fetch('/api/toggle_player', {method: 'POST'});
const data = await response.json();
// ๋ฒ„ํŠผ ํ…์ŠคํŠธ ์—…๋ฐ์ดํŠธ
playerToggleBtn.textContent = data.player_is_active ? 'ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ: ํ™œ์„ฑํ™”' : 'ํ”Œ๋ ˆ์ด์–ด ์ƒํƒœ: ๋น„ํ™œ์„ฑํ™”';
});
// '์ „์†ก' ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
playerSendBtn.addEventListener('click', async () => {
const utterance = playerInputText.value;
if (!utterance) return;
playerSendBtn.disabled = true;
await fetch('/api/player_response', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ utterance })
});
playerInputText.value = '';
playerSendBtn.disabled = false;
await updateWorld(); // ์ƒํƒœ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ
});
// ์—”ํ„ฐํ‚ค๋กœ ๋Œ€ํ™” ์ „์†ก
playerInputText.addEventListener('keydown', function(event) {
if (event.key == 'Enter' && !event.shiftKey) {
event.preventDefault();
playerSendBtn.click();
}
});
// ์ดˆ๊ธฐ ๋กœ๋“œ
await updateWorld();
setInterval(updateWorld, 5000);
});
</script>
</body>
</html>