SOY NV AI
feat: ?์? ๆนฒ๊ณ์ปฒ chunking ๆดั์ฝ ่ซ??๋ฑ๋ผ??ๆฟย็ฑ??์์ ๏งย ้บ๊พจโ
d5b7e8c
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>๋ฉ์์ง ํ์ธ - 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: 12px; | |
| align-items: center; | |
| } | |
| .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: 1400px; | |
| 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; | |
| } | |
| .filters { | |
| display: flex; | |
| gap: 12px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .filter-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .filter-group label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: #5f6368; | |
| } | |
| .filter-group select, | |
| .filter-group input { | |
| padding: 8px 12px; | |
| border: 1px solid #dadce0; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| font-family: inherit; | |
| } | |
| .filter-group select:focus, | |
| .filter-group input:focus { | |
| outline: none; | |
| border-color: #1a73e8; | |
| box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1); | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| thead { | |
| background: #f8f9fa; | |
| } | |
| th, td { | |
| padding: 12px; | |
| text-align: left; | |
| border-bottom: 1px solid #e8eaed; | |
| } | |
| th { | |
| font-weight: 500; | |
| font-size: 14px; | |
| color: #5f6368; | |
| } | |
| td { | |
| font-size: 14px; | |
| } | |
| .message-content { | |
| max-width: 500px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .message-content-full { | |
| max-width: none; | |
| white-space: normal; | |
| word-wrap: break-word; | |
| } | |
| .role-badge { | |
| display: inline-block; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .role-user { | |
| background: #e8f0fe; | |
| color: #1967d2; | |
| } | |
| .role-ai { | |
| background: #e8f5e9; | |
| color: #137333; | |
| } | |
| .pagination { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 20px; | |
| } | |
| .pagination button { | |
| padding: 8px 12px; | |
| border: 1px solid #dadce0; | |
| background: white; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .pagination button:hover:not(:disabled) { | |
| background: #f1f3f4; | |
| } | |
| .pagination button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .pagination .page-info { | |
| padding: 8px 12px; | |
| color: #5f6368; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal.active { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 24px; | |
| width: 90%; | |
| max-width: 800px; | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .modal-title { | |
| font-size: 20px; | |
| font-weight: 500; | |
| } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: #5f6368; | |
| } | |
| .message-view { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| } | |
| .message-item { | |
| padding: 12px; | |
| border-radius: 8px; | |
| border-left: 4px solid; | |
| } | |
| .message-item.user { | |
| background: #e8f0fe; | |
| border-color: #1a73e8; | |
| } | |
| .message-item.ai { | |
| background: #e8f5e9; | |
| border-color: #34a853; | |
| } | |
| .message-item-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .message-item-role { | |
| font-weight: 500; | |
| font-size: 14px; | |
| } | |
| .message-item-time { | |
| font-size: 12px; | |
| color: #5f6368; | |
| } | |
| .message-item-content { | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| line-height: 1.6; | |
| } | |
| .alert { | |
| padding: 12px 16px; | |
| border-radius: 6px; | |
| margin-bottom: 16px; | |
| font-size: 14px; | |
| } | |
| .alert.error { | |
| background: #fce8e6; | |
| color: #c5221f; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="header"> | |
| <div class="header-title"> | |
| <span>๐ค</span> | |
| <span>SOY NV AI ๋ฉ์์ง ํ์ธ</span> | |
| </div> | |
| <div class="header-actions"> | |
| <span style="margin-right: 12px; color: #5f6368;">{{ current_user.nickname or current_user.username }}</span> | |
| <a href="{{ url_for('main.admin') }}" class="btn btn-secondary">์ฌ์ฉ์ ๊ด๋ฆฌ</a> | |
| <a href="{{ url_for('main.admin_webnovels') }}" class="btn btn-secondary">์น์์ค ๊ด๋ฆฌ</a> | |
| <a href="{{ url_for('main.index') }}" class="btn btn-secondary">๋ฉ์ธ์ผ๋ก</a> | |
| <a href="{{ url_for('main.logout') }}" class="btn btn-secondary">๋ก๊ทธ์์</a> | |
| </div> | |
| </div> | |
| <div class="container"> | |
| <div class="page-header"> | |
| <h1>์ ์ฒด ๋ฉ์์ง ํ์ธ</h1> | |
| <p>๋ชจ๋ ์ฌ์ฉ์์ ๋ํ ๋ฉ์์ง๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.</p> | |
| </div> | |
| <div id="alertContainer"></div> | |
| <!-- ๋ํ ์ธ์ ๋ชฉ๋ก --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title">๋ํ ์ธ์ ๋ชฉ๋ก</div> | |
| </div> | |
| <div class="filters"> | |
| <div class="filter-group"> | |
| <label>์ฌ์ฉ์ ํํฐ</label> | |
| <select id="userFilter"> | |
| <option value="">์ ์ฒด ์ฌ์ฉ์</option> | |
| </select> | |
| </div> | |
| <div class="filter-group"> | |
| <label>๊ฒ์</label> | |
| <input type="text" id="searchInput" placeholder="์ธ์ ์ ๋ชฉ ๊ฒ์..."> | |
| </div> | |
| <div class="filter-group" style="align-items: flex-end;"> | |
| <button class="btn btn-primary" onclick="loadSessions()">์กฐํ</button> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>์ฌ์ฉ์</th> | |
| <th>์ ๋ชฉ</th> | |
| <th>๋ชจ๋ธ</th> | |
| <th>๋ฉ์์ง ์</th> | |
| <th>์์ฑ์ผ</th> | |
| <th>์์ ์ผ</th> | |
| <th>์์ </th> | |
| </tr> | |
| </thead> | |
| <tbody id="sessionsTableBody"> | |
| <tr> | |
| <td colspan="8" style="text-align: center; padding: 20px; color: #5f6368;">๋ก๋ฉ ์ค...</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div class="pagination" id="sessionsPagination"></div> | |
| </div> | |
| <!-- ๋ฉ์์ง ๋ชฉ๋ก --> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <div class="card-title">๋ฉ์์ง ๋ชฉ๋ก</div> | |
| </div> | |
| <div class="filters"> | |
| <div class="filter-group"> | |
| <label>์ธ์ ID</label> | |
| <input type="number" id="sessionIdFilter" placeholder="์ธ์ ID"> | |
| </div> | |
| <div class="filter-group" style="align-items: flex-end;"> | |
| <button class="btn btn-primary" onclick="loadMessages()">์กฐํ</button> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>ID</th> | |
| <th>์ธ์ ID</th> | |
| <th>์ญํ </th> | |
| <th>๋ด์ฉ</th> | |
| <th>์๊ฐ</th> | |
| <th>์์ </th> | |
| </tr> | |
| </thead> | |
| <tbody id="messagesTableBody"> | |
| <tr> | |
| <td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">์ธ์ ์ ์ ํํ๊ฑฐ๋ ํํฐ๋ฅผ ์ฌ์ฉํ์ธ์</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <div class="pagination" id="messagesPagination"></div> | |
| </div> | |
| </div> | |
| <!-- ๋ฉ์์ง ์์ธ ๋ณด๊ธฐ ๋ชจ๋ฌ --> | |
| <div id="messageModal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <div class="modal-title">๋ฉ์์ง ์์ธ</div> | |
| <button class="modal-close" onclick="closeMessageModal()">×</button> | |
| </div> | |
| <div id="messageModalContent"></div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentSessionsPage = 1; | |
| let currentMessagesPage = 1; | |
| let selectedSessionId = null; | |
| function showAlert(message, type = 'error') { | |
| const container = document.getElementById('alertContainer'); | |
| container.innerHTML = `<div class="alert ${type}">${message}</div>`; | |
| setTimeout(() => { | |
| container.innerHTML = ''; | |
| }, 5000); | |
| } | |
| // ์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋ | |
| async function loadUsers() { | |
| try { | |
| const response = await fetch('/api/admin/users'); | |
| const data = await response.json(); | |
| const userFilter = document.getElementById('userFilter'); | |
| userFilter.innerHTML = '<option value="">์ ์ฒด ์ฌ์ฉ์</option>'; | |
| if (data.users) { | |
| data.users.forEach(user => { | |
| const option = document.createElement('option'); | |
| option.value = user.id; | |
| option.textContent = `${user.nickname || user.username} (${user.username})`; | |
| userFilter.appendChild(option); | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('์ฌ์ฉ์ ๋ชฉ๋ก ๋ก๋ ์ค๋ฅ:', error); | |
| } | |
| } | |
| // ๋ํ ์ธ์ ๋ชฉ๋ก ๋ก๋ | |
| async function loadSessions(page = 1) { | |
| try { | |
| const userId = document.getElementById('userFilter').value; | |
| const search = document.getElementById('searchInput').value; | |
| let url = `/api/admin/sessions?page=${page}&per_page=20`; | |
| if (userId) { | |
| url += `&user_id=${userId}`; | |
| } | |
| const response = await fetch(url); | |
| const data = await response.json(); | |
| const tbody = document.getElementById('sessionsTableBody'); | |
| tbody.innerHTML = ''; | |
| if (data.sessions && data.sessions.length > 0) { | |
| data.sessions.forEach(session => { | |
| const row = document.createElement('tr'); | |
| const createdDate = new Date(session.created_at).toLocaleString('ko-KR'); | |
| const updatedDate = new Date(session.updated_at).toLocaleString('ko-KR'); | |
| // ๊ฒ์ ํํฐ ์ ์ฉ | |
| if (search && !session.title.toLowerCase().includes(search.toLowerCase())) { | |
| return; | |
| } | |
| row.innerHTML = ` | |
| <td>${session.id}</td> | |
| <td>${session.nickname || session.username || 'Unknown'}</td> | |
| <td>${session.title || '-'}</td> | |
| <td>${session.model_name || '-'}</td> | |
| <td>${session.message_count || 0}</td> | |
| <td>${createdDate}</td> | |
| <td>${updatedDate}</td> | |
| <td> | |
| <button class="btn btn-secondary" onclick="viewSessionMessages(${session.id})" style="padding: 4px 8px; font-size: 12px;">๋ฉ์์ง ๋ณด๊ธฐ</button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } else { | |
| tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 20px; color: #5f6368;">๋ํ ์ธ์ ์ด ์์ต๋๋ค.</td></tr>'; | |
| } | |
| // ํ์ด์ง๋ค์ด์ | |
| updateSessionsPagination(data.current_page, data.pages); | |
| currentSessionsPage = data.current_page; | |
| } catch (error) { | |
| showAlert(`๋ํ ์ธ์ ์กฐํ ์ค๋ฅ: ${error.message}`, 'error'); | |
| console.error('๋ํ ์ธ์ ์กฐํ ์ค๋ฅ:', error); | |
| } | |
| } | |
| // ๋ฉ์์ง ๋ชฉ๋ก ๋ก๋ | |
| async function loadMessages(page = 1) { | |
| try { | |
| const sessionId = document.getElementById('sessionIdFilter').value; | |
| let url = `/api/admin/messages?page=${page}&per_page=50`; | |
| if (sessionId) { | |
| url += `&session_id=${sessionId}`; | |
| } else if (selectedSessionId) { | |
| url += `&session_id=${selectedSessionId}`; | |
| } | |
| const response = await fetch(url); | |
| const data = await response.json(); | |
| const tbody = document.getElementById('messagesTableBody'); | |
| tbody.innerHTML = ''; | |
| if (data.messages && data.messages.length > 0) { | |
| data.messages.forEach(msg => { | |
| const row = document.createElement('tr'); | |
| const date = new Date(msg.created_at).toLocaleString('ko-KR'); | |
| const contentPreview = msg.content.length > 100 ? msg.content.substring(0, 100) + '...' : msg.content; | |
| row.innerHTML = ` | |
| <td>${msg.id}</td> | |
| <td>${msg.session_id}</td> | |
| <td><span class="role-badge role-${msg.role}">${msg.role === 'user' ? '์ฌ์ฉ์' : 'AI'}</span></td> | |
| <td class="message-content">${escapeHtml(contentPreview)}</td> | |
| <td>${date}</td> | |
| <td> | |
| <button class="btn btn-secondary" onclick="viewMessage(${msg.id}, '${msg.role}', ${JSON.stringify(msg.content).replace(/"/g, '"')})" style="padding: 4px 8px; font-size: 12px;">์์ธ ๋ณด๊ธฐ</button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } else { | |
| tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 20px; color: #5f6368;">๋ฉ์์ง๊ฐ ์์ต๋๋ค.</td></tr>'; | |
| } | |
| // ํ์ด์ง๋ค์ด์ | |
| updateMessagesPagination(data.current_page, data.pages); | |
| currentMessagesPage = data.current_page; | |
| } catch (error) { | |
| showAlert(`๋ฉ์์ง ์กฐํ ์ค๋ฅ: ${error.message}`, 'error'); | |
| console.error('๋ฉ์์ง ์กฐํ ์ค๋ฅ:', error); | |
| } | |
| } | |
| // ์ธ์ ๋ฉ์์ง ๋ณด๊ธฐ | |
| function viewSessionMessages(sessionId) { | |
| selectedSessionId = sessionId; | |
| document.getElementById('sessionIdFilter').value = sessionId; | |
| loadMessages(1); | |
| } | |
| // ๋ฉ์์ง ์์ธ ๋ณด๊ธฐ | |
| function viewMessage(messageId, role, content) { | |
| const modal = document.getElementById('messageModal'); | |
| const modalContent = document.getElementById('messageModalContent'); | |
| modalContent.innerHTML = ` | |
| <div class="message-view"> | |
| <div class="message-item ${role}"> | |
| <div class="message-item-header"> | |
| <span class="message-item-role role-badge role-${role}">${role === 'user' ? '์ฌ์ฉ์' : 'AI'}</span> | |
| <span class="message-item-time">๋ฉ์์ง ID: ${messageId}</span> | |
| </div> | |
| <div class="message-item-content">${escapeHtml(content)}</div> | |
| </div> | |
| </div> | |
| `; | |
| modal.classList.add('active'); | |
| } | |
| function closeMessageModal() { | |
| document.getElementById('messageModal').classList.remove('active'); | |
| } | |
| // ํ์ด์ง๋ค์ด์ ์ ๋ฐ์ดํธ | |
| function updateSessionsPagination(currentPage, totalPages) { | |
| const pagination = document.getElementById('sessionsPagination'); | |
| pagination.innerHTML = ''; | |
| pagination.appendChild(createPaginationButton('์ด์ ', currentPage > 1, () => loadSessions(currentPage - 1))); | |
| pagination.appendChild(createPaginationInfo(`${currentPage} / ${totalPages}`)); | |
| pagination.appendChild(createPaginationButton('๋ค์', currentPage < totalPages, () => loadSessions(currentPage + 1))); | |
| } | |
| function updateMessagesPagination(currentPage, totalPages) { | |
| const pagination = document.getElementById('messagesPagination'); | |
| pagination.innerHTML = ''; | |
| pagination.appendChild(createPaginationButton('์ด์ ', currentPage > 1, () => loadMessages(currentPage - 1))); | |
| pagination.appendChild(createPaginationInfo(`${currentPage} / ${totalPages}`)); | |
| pagination.appendChild(createPaginationButton('๋ค์', currentPage < totalPages, () => loadMessages(currentPage + 1))); | |
| } | |
| function createPaginationButton(text, enabled, onClick) { | |
| const button = document.createElement('button'); | |
| button.textContent = text; | |
| button.disabled = !enabled; | |
| if (enabled) { | |
| button.onclick = onClick; | |
| } | |
| return button; | |
| } | |
| function createPaginationInfo(text) { | |
| const span = document.createElement('span'); | |
| span.className = 'page-info'; | |
| span.textContent = text; | |
| return span; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // ๋ชจ๋ฌ ์ธ๋ถ ํด๋ฆญ ์ ๋ซ๊ธฐ | |
| document.getElementById('messageModal').addEventListener('click', function(e) { | |
| if (e.target === this) { | |
| closeMessageModal(); | |
| } | |
| }); | |
| // ๊ฒ์ ์ํฐ ํค | |
| document.getElementById('searchInput').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| loadSessions(1); | |
| } | |
| }); | |
| // ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ | |
| window.addEventListener('load', () => { | |
| loadUsers(); | |
| loadSessions(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |