soyailabs / templates /admin_settings.html
GitHub Actions
Auto-deploy from GitHub Actions - 2025-12-12 13:10:39
8d65b61
<!DOCTYPE html>
<html lang="ko">
<head>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "ujskfvh0bu");
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI ์„ค์ • ๊ด€๋ฆฌ - SOY NV AI</title>
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
color: #202124;
}
.header {
background: white;
border-bottom: 1px solid #dadce0;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.header-title {
font-size: 20px;
font-weight: 500;
display: flex;
align-items: center;
gap: 12px;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
/* ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด ์Šคํƒ€์ผ */
.dropdown {
position: relative;
display: inline-block;
}
/* ๋ฒ„ํŠผ๊ณผ ๋ฉ”๋‰ด ์‚ฌ์ด 'ํ‹ˆ'์—์„œ hover๊ฐ€ ๋Š๊ฒจ ๋ฉ”๋‰ด๊ฐ€ ๋‹ซํžˆ๋Š” ํ˜„์ƒ ๋ฐฉ์ง€ */
.dropdown::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 100%;
height: 8px;
}
.dropdown-toggle {
padding: 8px 16px;
background: #f1f3f4;
color: #202124;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.dropdown-toggle:hover {
background: #e8eaed;
}
.dropdown-toggle::after {
content: 'โ–ผ';
font-size: 10px;
transition: transform 0.2s;
}
.dropdown:hover .dropdown-toggle::after {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
margin-top: 0;
background: white;
border: 1px solid #dadce0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
z-index: 10000;
padding: 4px 0;
pointer-events: none;
}
.dropdown:hover .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-item {
display: block;
padding: 10px 16px;
color: #202124;
text-decoration: none;
font-size: 14px;
transition: background 0.2s;
}
.dropdown-item:hover {
background: #f8f9fa;
}
.dropdown-item:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.dropdown-item:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.menu-toggle {
display: none;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 8px;
color: #202124;
}
.mobile-menu {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.mobile-menu.active {
display: block;
}
.mobile-menu-content {
position: fixed;
top: 0;
right: -100%;
width: 280px;
max-width: 80%;
height: 100%;
background: white;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
overflow-y: auto;
z-index: 1001;
}
.mobile-menu.active .mobile-menu-content {
right: 0;
}
.mobile-menu-header {
padding: 16px 20px;
border-bottom: 1px solid #dadce0;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
position: sticky;
top: 0;
z-index: 10;
}
.mobile-menu-title {
font-size: 18px;
font-weight: 500;
}
.mobile-menu-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #202124;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-menu-items {
padding: 8px 0;
}
.mobile-menu-item {
display: block;
padding: 12px 20px;
color: #202124;
text-decoration: none;
border-bottom: 1px solid #f1f3f4;
transition: background 0.2s;
}
.mobile-menu-item:hover {
background: #f8f9fa;
}
.mobile-menu-user {
padding: 16px 20px;
border-bottom: 1px solid #dadce0;
color: #5f6368;
font-size: 14px;
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
}
.header-title {
font-size: 18px;
}
.header-title span:first-child {
display: none;
}
.menu-toggle {
display: block;
}
.header-actions {
display: none;
}
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #1a73e8;
color: white;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-secondary {
background: #f1f3f4;
color: #202124;
}
.btn-secondary:hover {
background: #e8eaed;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.page-header p {
color: #5f6368;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 500;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #dadce0;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
}
.form-group input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
}
.alert.error {
background: #fce8e6;
color: #c5221f;
}
.alert.success {
background: #e8f5e9;
color: #137333;
}
</style>
</head>
<body>
<div class="header">
<div class="header-title">
<span>โš™๏ธ</span>
<span>AI ์„ค์ • ๊ด€๋ฆฌ</span>
</div>
<button class="menu-toggle" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ์—ด๊ธฐ">โ˜ฐ</button>
<div class="header-actions">
<span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span>
{# ์‚ฌ์ดํŠธ ๊ด€๋ฆฌ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin') }}" class="dropdown-item">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_tokens') }}" class="dropdown-item">ํ† ํฐ ํ†ต๊ณ„</a>
</div>
</div>
{# ์›น์†Œ์„ค ๊ด€๋ฆฌ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์›น์†Œ์„ค ๊ด€๋ฆฌ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_webnovels') }}" class="dropdown-item">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_messages') }}" class="dropdown-item">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
</div>
</div>
{# AI ์„ค์ • #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">AI ์„ค์ •</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_settings') }}" class="dropdown-item">AI ์„ค์ •</a>
<a href="{{ url_for('main.admin_prompts') }}" class="dropdown-item">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
</div>
</div>
{# ์ฑ—๋ด‡ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">์ฑ—๋ด‡</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_tags') }}" class="dropdown-item">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
</div>
</div>
{# ํŽธ์˜๊ธฐ๋Šฅ #}
<div class="dropdown">
<button type="button" class="dropdown-toggle">ํŽธ์˜๊ธฐ๋Šฅ</button>
<div class="dropdown-menu">
<a href="{{ url_for('main.admin_utils') }}" class="dropdown-item">์œ ํ‹ธ</a>
</div>
</div>
{# ๋ฉ”์ธ์œผ๋กœ #}
<a href="{{ url_for('main.index') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px; margin-right: 4px;">๋ฉ”์ธ์œผ๋กœ</a>
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary" style="padding: 8px 16px; font-size: 14px;">๋กœ๊ทธ์•„์›ƒ</a>
</div>
</div>
<!-- ๋ชจ๋ฐ”์ผ ๋ฉ”๋‰ด -->
<div class="mobile-menu" id="mobileMenu" onclick="closeMobileMenuOnBackdrop(event)">
<div class="mobile-menu-content" onclick="event.stopPropagation()">
<div class="mobile-menu-header">
<div class="mobile-menu-title">๋ฉ”๋‰ด</div>
<button class="mobile-menu-close" onclick="toggleMobileMenu()" aria-label="๋ฉ”๋‰ด ๋‹ซ๊ธฐ">&times;</button>
</div>
<div class="mobile-menu-user">{{ current_user.nickname or current_user.username }}</div>
<div class="mobile-menu-items">
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4;">์‚ฌ์ดํŠธ ๊ด€๋ฆฌ</div>
<a href="{{ url_for('main.admin') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์‚ฌ์šฉ์ž ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_tokens') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ† ํฐ ํ†ต๊ณ„</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">์›น์†Œ์„ค ๊ด€๋ฆฌ</div>
<a href="{{ url_for('main.admin_webnovels') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์›น์†Œ์„ค ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ</a>
<a href="{{ url_for('main.admin_messages') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์‹œ์ง€ ํ™•์ธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">AI ์„ค์ •</div>
<a href="{{ url_for('main.admin_settings') }}" class="mobile-menu-item" onclick="closeMobileMenu()">AI ์„ค์ •</a>
<a href="{{ url_for('main.admin_prompts') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํ”„๋กฌํ”„ํŠธ ๊ด€๋ฆฌ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">์ฑ—๋ด‡</div>
<a href="{{ url_for('main.admin_tags') }}" class="mobile-menu-item" onclick="closeMobileMenu()">ํƒœ๊ทธ/ํ”„๋กฌํ”„ํŠธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">ํŽธ์˜๊ธฐ๋Šฅ</div>
<a href="{{ url_for('main.admin_utils') }}" class="mobile-menu-item" onclick="closeMobileMenu()">์œ ํ‹ธ</a>
<div style="padding: 8px 20px; font-size: 11px; font-weight: 600; color: #5f6368; text-transform: uppercase; border-bottom: 1px solid #f1f3f4; margin-top: 8px;">๊ธฐํƒ€</div>
<a href="{{ url_for('main.index') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋ฉ”์ธ์œผ๋กœ</a>
<a href="{{ url_for('main.logout') }}" class="mobile-menu-item" onclick="closeMobileMenu()">๋กœ๊ทธ์•„์›ƒ</a>
</div>
</div>
</div>
<div class="container">
<div class="page-header">
<h1>AI ์„ค์ • ๊ด€๋ฆฌ</h1>
<p>Gemini API ํ‚ค์™€ AI ๋ชจ๋ธ๋ณ„ ํ† ํฐ ์ˆ˜๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
</div>
<div id="alertContainer"></div>
<!-- Gemini API ํ‚ค ์„ค์ • ์„น์…˜ -->
<div class="card">
<div class="card-header">
<div class="card-title">Gemini API ํ‚ค ์„ค์ •</div>
</div>
<div style="padding: 16px 0;">
<div class="form-group">
<label for="geminiApiKey">Gemini API ํ‚ค</label>
<div style="display: flex; gap: 8px;">
<input type="password" id="geminiApiKey" placeholder="Gemini API ํ‚ค๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;">
<button class="btn btn-primary" onclick="saveGeminiApiKey()">์ €์žฅ</button>
<button class="btn btn-secondary" onclick="loadGeminiApiKey()">ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ</button>
</div>
<small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
Google AI Studio(<a href="https://aistudio.google.com/app/apikey" target="_blank">https://aistudio.google.com/app/apikey</a>)์—์„œ API ํ‚ค๋ฅผ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
</small>
<div id="geminiApiKeyStatus" style="margin-top: 8px; font-size: 13px;"></div>
</div>
</div>
</div>
<!-- Hugging Face ํ† ํฐ ์„ค์ • ์„น์…˜ -->
<div class="card">
<div class="card-header">
<div class="card-title">Hugging Face ํ† ํฐ ์„ค์ •</div>
</div>
<div style="padding: 16px 0;">
<div class="form-group">
<label for="huggingfaceToken">Hugging Face ํ† ํฐ</label>
<div style="display: flex; gap: 8px;">
<input type="password" id="huggingfaceToken" placeholder="Hugging Face ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š”" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;">
<button class="btn btn-primary" onclick="saveHuggingFaceToken()">์ €์žฅ</button>
<button class="btn btn-secondary" onclick="loadHuggingFaceToken()">ํ˜„์žฌ ์ƒํƒœ ํ™•์ธ</button>
</div>
<small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
Hugging Face(<a href="https://huggingface.co/settings/tokens" target="_blank">https://huggingface.co/settings/tokens</a>)์—์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
</small>
<div id="huggingfaceTokenStatus" style="margin-top: 8px; font-size: 13px;"></div>
</div>
</div>
</div>
<!-- ๊ธฐ๋ณธ AI ๋ชจ๋ธ ์„ค์ • ์„น์…˜ -->
<div class="card">
<div class="card-header">
<div class="card-title">๊ธฐ๋ณธ AI ๋ชจ๋ธ ์„ค์ •</div>
<button class="btn btn-secondary" onclick="loadDefaultModels()">์ƒˆ๋กœ๊ณ ์นจ</button>
</div>
<div style="padding: 16px 0;">
<div id="defaultModelsStatus" style="margin-bottom: 16px; font-size: 13px;"></div>
<div style="display: grid; gap: 16px;">
<div class="form-group">
<label for="defaultAnalysisModel">๊ธฐ๋ณธ ์งˆ๋ฌธ ๋ถ„์„์šฉ AI ๋ชจ๋ธ (AI ๋ชจ๋ธ ์„ ํƒ)</label>
<div style="display: flex; gap: 8px;">
<select id="defaultAnalysisModel" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;">
<option value="">๊ธฐ๋ณธ๊ฐ’ ์—†์Œ</option>
</select>
<button class="btn btn-primary" onclick="saveDefaultModels()">์ €์žฅ</button>
</div>
<small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
์‚ฌ์šฉ์ž ํ™”๋ฉด์˜ "AI ๋ชจ๋ธ ์„ ํƒ" ๋“œ๋กญ๋‹ค์šด์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์„ ํƒ๋  ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.
</small>
</div>
<div class="form-group">
<label for="defaultAnswerModel">๊ธฐ๋ณธ ๋‹ต๋ณ€ ์ƒ์„ฑ์šฉ AI ๋ชจ๋ธ (์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ AI ๋ชฉ๋ก)</label>
<div style="display: flex; gap: 8px;">
<select id="defaultAnswerModel" style="flex: 1; padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;">
<option value="">๊ธฐ๋ณธ๊ฐ’ ์—†์Œ</option>
</select>
<button class="btn btn-primary" onclick="saveDefaultModels()">์ €์žฅ</button>
</div>
<small style="color: #5f6368; font-size: 12px; display: block; margin-top: 4px;">
์‚ฌ์šฉ์ž ํ™”๋ฉด์˜ "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ AI ๋ชฉ๋ก" ๋“œ๋กญ๋‹ค์šด์—์„œ ๊ธฐ๋ณธ์œผ๋กœ ์„ ํƒ๋  ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.
</small>
</div>
</div>
</div>
</div>
<!-- AI ๋ชจ๋ธ๋ณ„ ํ† ํฐ ์ˆ˜ ๊ด€๋ฆฌ ์„น์…˜ -->
<div class="card">
<div class="card-header">
<div class="card-title">AI ๋ชจ๋ธ๋ณ„ ํ† ํฐ ์ˆ˜ ๊ด€๋ฆฌ</div>
<button class="btn btn-secondary" onclick="loadModelTokens()">์ƒˆ๋กœ๊ณ ์นจ</button>
</div>
<div style="padding: 16px 0;">
<div id="modelTokensStatus" style="margin-bottom: 16px; font-size: 13px;"></div>
<div id="modelTokensList" style="display: grid; gap: 12px;">
<div style="text-align: center; padding: 20px; color: #5f6368;">๋กœ๋”ฉ ์ค‘...</div>
</div>
</div>
</div>
</div>
<script>
function toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.toggle('active');
document.body.style.overflow = menu.classList.contains('active') ? 'hidden' : '';
}
function closeMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.remove('active');
document.body.style.overflow = '';
}
function closeMobileMenuOnBackdrop(event) {
if (event.target.id === 'mobileMenu') {
closeMobileMenu();
}
}
function showAlert(message, type = 'success') {
const container = document.getElementById('alertContainer');
container.innerHTML = `<div class="alert ${type}">${message}</div>`;
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
// Gemini API ํ‚ค ๊ด€๋ จ ํ•จ์ˆ˜
async function loadGeminiApiKey() {
try {
const response = await fetch('/api/admin/gemini-api-key', {
credentials: 'include'
});
// ์‘๋‹ต์ด JSON์ธ์ง€ ํ™•์ธ
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text.substring(0, 200));
const statusDiv = document.getElementById('geminiApiKeyStatus');
statusDiv.innerHTML = `<span style="color: #ea4335;">์„œ๋ฒ„ ์˜ค๋ฅ˜: ์‘๋‹ต ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</span>`;
return;
}
const data = await response.json();
const statusDiv = document.getElementById('geminiApiKeyStatus');
if (!response.ok) {
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}</span>`;
return;
}
if (data.has_api_key) {
statusDiv.innerHTML = `<span style="color: #137333;">โœ“ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (${data.masked_key})</span>`;
} else {
statusDiv.innerHTML = `<span style="color: #ea4335;">โš  API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค</span>`;
}
} catch (error) {
console.error('Gemini API ํ‚ค ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
const statusDiv = document.getElementById('geminiApiKeyStatus');
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.message}</span>`;
}
}
async function saveGeminiApiKey() {
const apiKeyInput = document.getElementById('geminiApiKey');
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
showAlert('API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
try {
const response = await fetch('/api/admin/gemini-api-key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ api_key: apiKey })
});
// ์‘๋‹ต์ด JSON์ธ์ง€ ํ™•์ธ
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text.substring(0, 200));
showAlert('์„œ๋ฒ„ ์˜ค๋ฅ˜: ์‘๋‹ต ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.', 'error');
return;
}
const data = await response.json();
if (response.ok) {
showAlert(data.message, 'success');
apiKeyInput.value = ''; // ๋ณด์•ˆ์„ ์œ„ํ•ด ์ž…๋ ฅ ํ•„๋“œ ์ดˆ๊ธฐํ™”
loadGeminiApiKey(); // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
} else {
showAlert(data.error || 'API ํ‚ค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('Gemini API ํ‚ค ์ €์žฅ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
}
// Hugging Face ํ† ํฐ ๊ด€๋ จ ํ•จ์ˆ˜
async function loadHuggingFaceToken() {
try {
const response = await fetch('/api/admin/huggingface-token', {
credentials: 'include'
});
// ์‘๋‹ต์ด JSON์ธ์ง€ ํ™•์ธ
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text.substring(0, 200));
const statusDiv = document.getElementById('huggingfaceTokenStatus');
statusDiv.innerHTML = `<span style="color: #ea4335;">์„œ๋ฒ„ ์˜ค๋ฅ˜: ์‘๋‹ต ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</span>`;
return;
}
const data = await response.json();
const statusDiv = document.getElementById('huggingfaceTokenStatus');
if (!response.ok) {
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}</span>`;
return;
}
if (data.has_token) {
statusDiv.innerHTML = `<span style="color: #137333;">โœ“ ํ† ํฐ์ด ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (${data.masked_token})</span>`;
} else {
statusDiv.innerHTML = `<span style="color: #ea4335;">โš  ํ† ํฐ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค</span>`;
}
} catch (error) {
console.error('Hugging Face ํ† ํฐ ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
const statusDiv = document.getElementById('huggingfaceTokenStatus');
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.message}</span>`;
}
}
async function saveHuggingFaceToken() {
const tokenInput = document.getElementById('huggingfaceToken');
const token = tokenInput.value.trim();
if (!token) {
showAlert('ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
try {
const response = await fetch('/api/admin/huggingface-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ token: token })
});
// ์‘๋‹ต์ด JSON์ธ์ง€ ํ™•์ธ
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text.substring(0, 200));
showAlert('์„œ๋ฒ„ ์˜ค๋ฅ˜: ์‘๋‹ต ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.', 'error');
return;
}
const data = await response.json();
if (response.ok) {
showAlert(data.message, 'success');
tokenInput.value = ''; // ๋ณด์•ˆ์„ ์œ„ํ•ด ์ž…๋ ฅ ํ•„๋“œ ์ดˆ๊ธฐํ™”
loadHuggingFaceToken(); // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
} else {
showAlert(data.error || 'ํ† ํฐ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('Hugging Face ํ† ํฐ ์ €์žฅ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
}
// AI ๋ชจ๋ธ๋ณ„ ํ† ํฐ ์ˆ˜ ๊ด€๋ฆฌ (์ž…๋ ฅ/์ถœ๋ ฅ ๋ถ„๋ฆฌ)
async function loadModelTokens() {
const statusDiv = document.getElementById('modelTokensStatus');
const listDiv = document.getElementById('modelTokensList');
try {
statusDiv.innerHTML = '<span style="color: #1a73e8;">ํ† ํฐ ์ˆ˜ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...</span>';
listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #5f6368;">๋กœ๋”ฉ ์ค‘...</div>';
const response = await fetch('/api/admin/model-tokens', {
credentials: 'include'
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: '์„œ๋ฒ„ ์˜ค๋ฅ˜' }));
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.error || 'ํ† ํฐ ์ˆ˜ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค'}</span>`;
listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #ea4335;">ํ† ํฐ ์ˆ˜ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</div>';
return;
}
const data = await response.json();
if (!data.models || data.models.length === 0) {
statusDiv.innerHTML = '<span style="color: #5f6368;">์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ AI ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.</span>';
listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #5f6368;">์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ AI ๋ชจ๋ธ์ด ์—†์Šต๋‹ˆ๋‹ค.</div>';
return;
}
statusDiv.innerHTML = `<span style="color: #137333;">โœ“ ${data.models.length}๊ฐœ ๋ชจ๋ธ ๋ฐœ๊ฒฌ</span>`;
let html = '';
data.models.forEach(modelName => {
// ์•ˆ์ „ํ•˜๊ฒŒ ํ† ํฐ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ (undefined ์ฒดํฌ)
const inputTokens = data.input_tokens || {};
const outputTokens = data.output_tokens || {};
const parentChunkTokens = data.parent_chunk_tokens || {};
const defaultInputTokens = data.default_input_tokens || {};
const defaultOutputTokens = data.default_output_tokens || {};
const defaultParentChunkTokens = data.default_parent_chunk_tokens || {};
const currentInputTokens = inputTokens[modelName] || '';
const currentOutputTokens = outputTokens[modelName] || '';
const currentParentChunkTokens = parentChunkTokens[modelName] || '';
const defaultInputToken = defaultInputTokens[modelName] || 100000;
const defaultOutputToken = defaultOutputTokens[modelName] || 2000;
const defaultParentChunkToken = defaultParentChunkTokens[modelName] || 8192;
const modelType = modelName.startsWith('gemini:') ? 'Gemini' : 'Ollama';
const displayName = modelName.startsWith('gemini:') ? modelName.replace('gemini:', '') : modelName;
// ์‹ค์ œ ์‚ฌ์šฉ ์ค‘์ธ ๊ฐ’ ๊ณ„์‚ฐ
const actualInputToken = currentInputTokens ? parseInt(currentInputTokens) : defaultInputToken;
const actualOutputToken = currentOutputTokens ? parseInt(currentOutputTokens) : defaultOutputToken;
const actualParentChunkToken = currentParentChunkTokens ? parseInt(currentParentChunkTokens) : defaultParentChunkToken;
// ์‚ฌ์šฉ ์ค‘์ธ ๊ฐ’ ํ‘œ์‹œ ํ…์ŠคํŠธ ์ƒ์„ฑ
const getTokenStatusText = (actual, defaultVal, isUsingDefault) => {
if (isUsingDefault) {
return `์‹ค์ œ ์‚ฌ์šฉ: ${actual.toLocaleString()} (๊ธฐ๋ณธ๊ฐ’)`;
} else {
return `์‹ค์ œ ์‚ฌ์šฉ: ${actual.toLocaleString()} (์„ค์ •๊ฐ’)`;
}
};
const inputStatusText = getTokenStatusText(actualInputToken, defaultInputToken, !currentInputTokens);
const outputStatusText = getTokenStatusText(actualOutputToken, defaultOutputToken, !currentOutputTokens);
const parentChunkStatusText = getTokenStatusText(actualParentChunkToken, defaultParentChunkToken, !currentParentChunkTokens);
// ์•ˆ์ „ํ•œ ID ์ƒ์„ฑ (ํŠน์ˆ˜๋ฌธ์ž ์ฒ˜๋ฆฌ)
const safeId = modelNameToId(modelName);
html += `
<div style="padding: 16px; background: #f8f9fa; border-radius: 6px; border: 1px solid #dadce0; margin-bottom: 12px;" data-model-name="${escapeHtml(modelName)}">
<div style="margin-bottom: 12px;">
<div style="font-weight: 500; margin-bottom: 4px; font-size: 15px;">${escapeHtml(displayName)}</div>
<div style="font-size: 12px; color: #5f6368; line-height: 1.6;">
<div><strong>ํƒ€์ž…:</strong> ${modelType}</div>
<div><strong>์ž…๋ ฅ ํ† ํฐ:</strong> ${inputStatusText}</div>
<div><strong>์ถœ๋ ฅ ํ† ํฐ:</strong> ${outputStatusText}</div>
<div><strong>Parent Chunk ํ† ํฐ:</strong> ${parentChunkStatusText}</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px;">
<div>
<label style="display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #202124;">์ž…๋ ฅ ํ† ํฐ</label>
<div style="display: flex; gap: 8px;">
<input
type="number"
id="input_tokens_${safeId}"
data-model-name="${escapeHtml(modelName)}"
value="${currentInputTokens || ''}"
placeholder="๊ธฐ๋ณธ๊ฐ’: ${defaultInputToken.toLocaleString()}"
min="1"
style="flex: 1; padding: 6px 10px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;"
>
<button
class="btn btn-primary"
onclick="saveModelTokens('${escapeHtml(modelName)}', 'input')"
style="padding: 6px 12px; font-size: 13px; white-space: nowrap;"
>
์ €์žฅ
</button>
</div>
</div>
<div>
<label style="display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #202124;">์ถœ๋ ฅ ํ† ํฐ</label>
<div style="display: flex; gap: 8px;">
<input
type="number"
id="output_tokens_${safeId}"
data-model-name="${escapeHtml(modelName)}"
value="${currentOutputTokens || ''}"
placeholder="๊ธฐ๋ณธ๊ฐ’: ${defaultOutputToken.toLocaleString()}"
min="1"
style="flex: 1; padding: 6px 10px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;"
>
<button
class="btn btn-primary"
onclick="saveModelTokens('${escapeHtml(modelName)}', 'output')"
style="padding: 6px 12px; font-size: 13px; white-space: nowrap;"
>
์ €์žฅ
</button>
</div>
</div>
<div>
<label style="display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: #202124;">Parent Chunk ํ† ํฐ</label>
<div style="display: flex; gap: 8px;">
<input
type="number"
id="parent_chunk_tokens_${safeId}"
data-model-name="${escapeHtml(modelName)}"
value="${currentParentChunkTokens || ''}"
placeholder="๊ธฐ๋ณธ๊ฐ’: ${defaultParentChunkToken.toLocaleString()}"
min="1"
style="flex: 1; padding: 6px 10px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px;"
>
<button
class="btn btn-primary"
onclick="saveModelTokens('${escapeHtml(modelName)}', 'parent_chunk')"
style="padding: 6px 12px; font-size: 13px; white-space: nowrap;"
>
์ €์žฅ
</button>
</div>
</div>
</div>
</div>
`;
});
listDiv.innerHTML = html;
} catch (error) {
console.error('ํ† ํฐ ์ˆ˜ ์„ค์ • ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.message}</span>`;
listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #ea4335;">ํ† ํฐ ์ˆ˜ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</div>';
}
}
async function saveModelTokens(modelName, tokenType) {
// tokenType์— ๋”ฐ๋ผ input ID ๊ฒฐ์ • (์•ˆ์ „ํ•œ ID ์‚ฌ์šฉ)
const safeId = modelNameToId(modelName);
let inputId;
if (tokenType === 'input') {
inputId = `input_tokens_${safeId}`;
} else if (tokenType === 'output') {
inputId = `output_tokens_${safeId}`;
} else if (tokenType === 'parent_chunk') {
inputId = `parent_chunk_tokens_${safeId}`;
} else {
showAlert('์ž˜๋ชป๋œ ํ† ํฐ ํƒ€์ž…์ž…๋‹ˆ๋‹ค.', 'error');
return;
}
const input = document.getElementById(inputId);
if (!input) {
console.error(`Input element not found: ${inputId}`);
showAlert('์ž…๋ ฅ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.', 'error');
return;
}
const tokens = input.value.trim();
console.log(`[ํ† ํฐ ์ €์žฅ] ๋ชจ๋ธ: ${modelName}, ํƒ€์ž…: ${tokenType}, ๊ฐ’: ${tokens}`);
// ๋นˆ ๊ฐ’์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ (์„ค์ • ์‚ญ์ œ)
if (!tokens) {
if (!confirm('์ž…๋ ฅ๊ฐ’์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋˜๋Œ๋ฆฌ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?')) {
return;
}
try {
const response = await fetch('/api/admin/model-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
model_name: modelName,
token_type: tokenType,
tokens: null // null์„ ๋ณด๋‚ด๋ฉด ์‚ญ์ œ
})
});
const data = await response.json();
if (response.ok) {
showAlert('๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋˜๋Œ๋ ธ์Šต๋‹ˆ๋‹ค.', 'success');
loadModelTokens(); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
} else {
showAlert(data.error || '์„ค์ • ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('์„ค์ • ์‚ญ์ œ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
return;
}
try {
const tokensNum = parseInt(tokens);
if (isNaN(tokensNum) || tokensNum < 1) {
showAlert('ํ† ํฐ ์ˆ˜๋Š” 1 ์ด์ƒ์˜ ์ •์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.', 'error');
return;
}
const response = await fetch('/api/admin/model-tokens', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
model_name: modelName,
token_type: tokenType,
tokens: tokensNum
})
});
const data = await response.json();
console.log(`[ํ† ํฐ ์ €์žฅ ์‘๋‹ต]`, data);
if (response.ok) {
showAlert(data.message || 'ํ† ํฐ ์ˆ˜๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', 'success');
// ์•ฝ๊ฐ„์˜ ์ง€์—ฐ ํ›„ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ (์„œ๋ฒ„ ๋ฐ˜์˜ ์‹œ๊ฐ„ ๊ณ ๋ ค)
setTimeout(() => {
loadModelTokens(); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
}, 300);
} else {
console.error(`[ํ† ํฐ ์ €์žฅ ์‹คํŒจ]`, data);
showAlert(data.error || 'ํ† ํฐ ์ˆ˜ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
}
} catch (error) {
console.error('ํ† ํฐ ์ˆ˜ ์ €์žฅ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ๋ชจ๋ธ๋ช…์„ ์•ˆ์ „ํ•œ ID๋กœ ๋ณ€ํ™˜ (ํŠน์ˆ˜๋ฌธ์ž ์ฒ˜๋ฆฌ)
function modelNameToId(modelName) {
// ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ์–ธ๋”์Šค์ฝ”์–ด๋กœ ๋ณ€ํ™˜
return modelName.replace(/[^a-zA-Z0-9]/g, '_');
}
// ID๋ฅผ ๋ชจ๋ธ๋ช…์œผ๋กœ ์—ญ๋ณ€ํ™˜ (์–ธ๋”์Šค์ฝ”์–ด๋ฅผ ์›๋ž˜ ํŠน์ˆ˜๋ฌธ์ž๋กœ ๋ณต์›)
// ์ฃผ์˜: ์ด ๋ฐฉ๋ฒ•์€ ์™„๋ฒฝํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๋Œ€์‹  data ์†์„ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค
function idToModelName(id) {
// ์ด ํ•จ์ˆ˜๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ - data ์†์„ฑ ์‚ฌ์šฉ
return id;
}
// ๊ธฐ๋ณธ AI ๋ชจ๋ธ ์„ค์ • ๊ด€๋ จ ํ•จ์ˆ˜
async function loadDefaultModels() {
const statusDiv = document.getElementById('defaultModelsStatus');
const analysisSelect = document.getElementById('defaultAnalysisModel');
const answerSelect = document.getElementById('defaultAnswerModel');
try {
statusDiv.innerHTML = '<span style="color: #1a73e8;">๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...</span>';
// ๋ชจ๋ธ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
const modelsResponse = await fetch('/api/admin/ollama/models', {
credentials: 'include'
});
if (!modelsResponse.ok) {
throw new Error('๋ชจ๋ธ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.');
}
const modelsData = await modelsResponse.json();
// ๋“œ๋กญ๋‹ค์šด ์ดˆ๊ธฐํ™”
analysisSelect.innerHTML = '<option value="">๊ธฐ๋ณธ๊ฐ’ ์—†์Œ</option>';
answerSelect.innerHTML = '<option value="">๊ธฐ๋ณธ๊ฐ’ ์—†์Œ</option>';
// ๋ชจ๋ธ์„ ํƒ€์ž…๋ณ„๋กœ ๊ทธ๋ฃนํ™”
const ollamaModels = [];
const geminiModels = [];
if (modelsData.models && modelsData.models.length > 0) {
modelsData.models.forEach(model => {
if (model.type === 'ollama') {
ollamaModels.push(model);
} else if (model.type === 'gemini') {
geminiModels.push(model);
}
});
}
// Ollama ๋ชจ๋ธ ์ถ”๊ฐ€
if (ollamaModels.length > 0) {
const optgroup1 = document.createElement('optgroup');
optgroup1.label = 'Ollama ๋ชจ๋ธ';
ollamaModels.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
optgroup1.appendChild(option);
});
analysisSelect.appendChild(optgroup1);
const optgroup2 = document.createElement('optgroup');
optgroup2.label = 'Ollama ๋ชจ๋ธ';
ollamaModels.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name;
optgroup2.appendChild(option);
});
answerSelect.appendChild(optgroup2);
}
// Gemini ๋ชจ๋ธ ์ถ”๊ฐ€
if (geminiModels.length > 0) {
const optgroup1 = document.createElement('optgroup');
optgroup1.label = 'Gemini ๋ชจ๋ธ';
geminiModels.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name.replace('gemini:', '');
optgroup1.appendChild(option);
});
analysisSelect.appendChild(optgroup1);
const optgroup2 = document.createElement('optgroup');
optgroup2.label = 'Gemini ๋ชจ๋ธ';
geminiModels.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.name.replace('gemini:', '');
optgroup2.appendChild(option);
});
answerSelect.appendChild(optgroup2);
}
// ํ˜„์žฌ ์„ค์ •๋œ ๊ธฐ๋ณธ ๋ชจ๋ธ ๊ฐ€์ ธ์˜ค๊ธฐ
const defaultResponse = await fetch('/api/admin/default-models', {
credentials: 'include'
});
if (defaultResponse.ok) {
const defaultData = await defaultResponse.json();
if (defaultData.default_analysis_model) {
analysisSelect.value = defaultData.default_analysis_model;
}
if (defaultData.default_answer_model) {
answerSelect.value = defaultData.default_answer_model;
}
statusDiv.innerHTML = '<span style="color: #137333;">โœ“ ๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค.</span>';
} else {
statusDiv.innerHTML = '<span style="color: #ea4335;">๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ •์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.</span>';
}
} catch (error) {
console.error('๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ • ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.message}</span>`;
}
}
async function saveDefaultModels() {
const analysisSelect = document.getElementById('defaultAnalysisModel');
const answerSelect = document.getElementById('defaultAnswerModel');
const statusDiv = document.getElementById('defaultModelsStatus');
const defaultAnalysisModel = analysisSelect.value;
const defaultAnswerModel = answerSelect.value;
try {
statusDiv.innerHTML = '<span style="color: #1a73e8;">์ €์žฅ ์ค‘...</span>';
const response = await fetch('/api/admin/default-models', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
default_analysis_model: defaultAnalysisModel,
default_answer_model: defaultAnswerModel
})
});
const data = await response.json();
if (response.ok) {
showAlert(data.message || '๊ธฐ๋ณธ AI ๋ชจ๋ธ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', 'success');
statusDiv.innerHTML = '<span style="color: #137333;">โœ“ ๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ •์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</span>';
} else {
showAlert(data.error || '๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ • ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'error');
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${data.error || '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'}</span>`;
}
} catch (error) {
console.error('๊ธฐ๋ณธ ๋ชจ๋ธ ์„ค์ • ์ €์žฅ ์˜ค๋ฅ˜:', error);
showAlert(`์˜ค๋ฅ˜: ${error.message}`, 'error');
statusDiv.innerHTML = `<span style="color: #ea4335;">์˜ค๋ฅ˜: ${error.message}</span>`;
}
}
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ API ํ‚ค ์ƒํƒœ ํ™•์ธ ๋ฐ ํ† ํฐ ์ˆ˜ ์„ค์ • ๋กœ๋“œ
window.addEventListener('load', () => {
loadGeminiApiKey();
loadHuggingFaceToken();
loadModelTokens();
loadDefaultModels();
});
</script>
</body>
</html>