Spaces:
Sleeping
Sleeping
<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 ; } | |
/* ์ ์ฒด ์ปจํธ๋กค ํจ๋ (๊ฐ์ฅ ๋ฐ๊นฅ์ชฝ) */ | |
#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> | |