Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chat Template Tester</title> | |
| <script src="https://mozilla.github.io/nunjucks/files/nunjucks.min.js"></script> | |
| <style> | |
| /* CSS Reset and Base styles */ | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: system-ui, -apple-system, sans-serif; | |
| background-color: #f5f5f5; | |
| line-height: 1.6; | |
| color: #333; | |
| } | |
| /* Container */ | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 12px; | |
| } | |
| } | |
| /* Typography */ | |
| h1 { | |
| font-size: clamp(1.5rem, 4vw, 2rem); | |
| margin-bottom: 1.5rem; | |
| color: #333; | |
| } | |
| h2 { | |
| font-size: clamp(1.2rem, 3vw, 1.5rem); | |
| margin-bottom: 1rem; | |
| color: #444; | |
| } | |
| /* Card Layout */ | |
| .card { | |
| background: white; | |
| padding: clamp(16px, 3vw, 24px); | |
| border-radius: 12px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| margin-bottom: 20px; | |
| } | |
| /* Form Elements */ | |
| .input-group { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| @media (max-width: 600px) { | |
| .input-group { | |
| flex-direction: column; | |
| } | |
| .input-group select, | |
| .input-group input, | |
| .input-group button { | |
| width: 100%; | |
| } | |
| } | |
| textarea, | |
| input[type="text"], | |
| select { | |
| padding: 12px; | |
| border: 1.5px solid #ddd; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| background: #fff; | |
| transition: border-color 0.2s; | |
| } | |
| textarea:focus, | |
| input[type="text"]:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: #4CAF50; | |
| } | |
| select { | |
| min-width: 120px; | |
| } | |
| @media (max-width: 600px) { | |
| select { | |
| min-width: 100%; | |
| } | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 120px; | |
| resize: vertical; | |
| font-family: inherit; | |
| } | |
| /* Buttons */ | |
| button { | |
| background-color: #4CAF50; | |
| color: white; | |
| padding: 12px 20px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| transition: all 0.2s; | |
| white-space: nowrap; | |
| } | |
| button:hover { | |
| background-color: #45a049; | |
| transform: translateY(-1px); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button.secondary { | |
| background-color: #6c757d; | |
| } | |
| button.danger { | |
| background-color: #dc3545; | |
| } | |
| button.small { | |
| padding: 6px 12px; | |
| font-size: 13px; | |
| } | |
| /* Messages Section */ | |
| .message { | |
| background-color: #f8f9fa; | |
| padding: 16px; | |
| border-radius: 8px; | |
| margin-bottom: 12px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| @media (max-width: 600px) { | |
| .message { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| } | |
| .message-content { | |
| flex-grow: 1; | |
| min-width: 200px; | |
| word-break: break-word; | |
| } | |
| .message strong { | |
| color: #495057; | |
| margin-right: 8px; | |
| display: inline-block; | |
| } | |
| .message-actions { | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| } | |
| @media (max-width: 600px) { | |
| .message-actions { | |
| justify-content: flex-end; | |
| margin-top: 8px; | |
| } | |
| } | |
| /* Configuration section */ | |
| .config-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| margin-bottom: 16px; | |
| flex-wrap: wrap; | |
| } | |
| @media (max-width: 600px) { | |
| .config-group { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| } | |
| /* Output section */ | |
| #output { | |
| white-space: pre-wrap; | |
| background-color: #f8f9fa; | |
| padding: 16px; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| border: 1.5px solid #ddd; | |
| overflow-x: auto; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| #error { | |
| white-space: pre-wrap; | |
| color: #ffffff; | |
| background-color: #f8090a; | |
| padding: 16px; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| border: 1.5px solid #ddd; | |
| overflow-x: auto; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| /* Checkbox styling */ | |
| .checkbox-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| input[type="checkbox"] { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| /* Loading state */ | |
| .loading { | |
| opacity: 0.7; | |
| pointer-events: none; | |
| } | |
| /* Focus styles for accessibility */ | |
| :focus-visible { | |
| outline: 2px solid #4CAF50; | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Chat Template Tester</h1> | |
| <div class="card"> | |
| <h2>Messages</h2> | |
| <div class="input-group"> | |
| <select id="role"> | |
| <option value="user">User</option> | |
| <option value="assistant">Assistant</option> | |
| <option value="system">System</option> | |
| </select> | |
| <input type="text" id="content" placeholder="Type your message here..." style="flex-grow: 1"> | |
| <button onclick="addMessage()">Add Message</button> | |
| </div> | |
| <div id="messages"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>Configuration</h2> | |
| <div class="config-group"> | |
| <div class="checkbox-wrapper"> | |
| <input type="checkbox" id="addGenerationPrompt"> | |
| <label for="addGenerationPrompt">Add Generation Prompt</label> | |
| </div> | |
| <div class="input-group" style="margin-bottom: 0; flex-grow: 1;"> | |
| <input type="text" id="bosToken" placeholder="BOS Token (e.g., <s>)" style="flex-grow: 1;"> | |
| <input type="text" id="eosToken" placeholder="EOS Token (e.g., </s>)" style="flex-grow: 1;"> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <input type="text" id="repoUrl" placeholder="Enter Hugging Face Repo URL" style="flex-grow: 1"> | |
| <button onclick="handleFetchConfig()">Fetch Config</button> | |
| </div> | |
| <h2>Template</h2> | |
| <textarea id="template" placeholder="Enter your template here..."></textarea> | |
| <button onclick="applyTemplate()" style="margin-top: 16px">Apply Template</button> | |
| </div> | |
| <div class="card"> | |
| <h2>Output</h2> | |
| <pre id="output"></pre> | |
| <pre id="error" style="display: none"></pre> | |
| </div> | |
| </div> | |
| <script> | |
| let messages = []; | |
| let editingIndex = null; | |
| function addMessage() { | |
| const role = document.getElementById('role').value; | |
| const content = document.getElementById('content').value; | |
| if (content.trim()) { | |
| messages.push({ role, content }); | |
| updateMessageDisplay(); | |
| document.getElementById('content').value = ''; | |
| document.getElementById('content').focus(); | |
| } | |
| } | |
| function updateMessageDisplay() { | |
| const messagesDiv = document.getElementById('messages'); | |
| messagesDiv.innerHTML = messages.map((msg, index) => ` | |
| <div class="message"> | |
| <div class="message-content"> | |
| <strong>${msg.role}:</strong> | |
| ${editingIndex === index | |
| ? `<input type="text" id="editContent" value="${msg.content}" style="width: 100%;">` | |
| : msg.content} | |
| </div> | |
| <div class="message-actions"> | |
| ${editingIndex === index | |
| ? `<button class="small" onclick="saveEdit(${index})">Save</button>` | |
| : `<button class="small secondary" onclick="editMessage(${index})">Edit</button>`} | |
| <button class="small danger" onclick="removeMessage(${index})">Remove</button> | |
| <button class="small" onclick="moveMessageUp(${index})" ${index === 0 ? 'disabled' : ''}>↑</button> | |
| <button class="small" onclick="moveMessageDown(${index})" ${index === messages.length - 1 ? 'disabled' : ''}>↓</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function removeMessage(index) { | |
| messages.splice(index, 1); | |
| updateMessageDisplay(); | |
| } | |
| function editMessage(index) { | |
| editingIndex = index; | |
| updateMessageDisplay(); | |
| setTimeout(() => { | |
| const editInput = document.getElementById('editContent'); | |
| if (editInput) { | |
| editInput.focus(); | |
| editInput.select(); | |
| } | |
| }, 0); | |
| } | |
| function saveEdit(index) { | |
| const newContent = document.getElementById('editContent').value; | |
| if (newContent.trim()) { | |
| messages[index].content = newContent; | |
| editingIndex = null; | |
| updateMessageDisplay(); | |
| } | |
| } | |
| function moveMessageUp(index) { | |
| if (index > 0) { | |
| [messages[index - 1], messages[index]] = [messages[index], messages[index - 1]]; | |
| updateMessageDisplay(); | |
| } | |
| } | |
| function moveMessageDown(index) { | |
| if (index < messages.length - 1) { | |
| [messages[index], messages[index + 1]] = [messages[index + 1], messages[index]]; | |
| updateMessageDisplay(); | |
| } | |
| } | |
| function raiseException(string) { | |
| document.getElementById("output").style.display = "none"; | |
| document.getElementById("error").style.display = "block"; | |
| document.getElementById('error').textContent = `Error: ${string}`; | |
| } | |
| function applyTemplate() { | |
| document.getElementById("output").style.display = "block"; | |
| document.getElementById("error").style.display = "none"; | |
| const template = document.getElementById('template').value; | |
| const addGenerationPrompt = document.getElementById('addGenerationPrompt').checked; | |
| const bosToken = document.getElementById('bosToken').value; | |
| const eosToken = document.getElementById('eosToken').value; | |
| const context = { | |
| messages: messages, | |
| add_generation_prompt: addGenerationPrompt, | |
| bos_token: bosToken, | |
| eos_token: eosToken, | |
| raise_exception: raiseException, | |
| }; | |
| try { | |
| document.getElementById('output').parentElement.classList.add('loading'); | |
| const result = nunjucks.renderString(template, context); | |
| const decodedResult = result.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); | |
| document.getElementById('output').textContent = decodedResult; | |
| } catch (error) { | |
| document.getElementById('output').textContent = `Error: ${error.message}`; | |
| } finally { | |
| document.getElementById('output').parentElement.classList.remove('loading'); | |
| } | |
| } | |
| // Keyboard shortcuts and accessibility | |
| document.addEventListener('keydown', function(e) { | |
| // Add message on Enter in the content input | |
| if (e.target.id === 'content' && e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| addMessage(); | |
| } | |
| // Save edit on Enter in the edit input | |
| if (e.target.id === 'editContent' && e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| saveEdit(editingIndex); | |
| } | |
| // Cancel edit on Escape | |
| if (e.key === 'Escape' && editingIndex !== null) { | |
| editingIndex = null; | |
| updateMessageDisplay(); | |
| } | |
| }); | |
| // Initialize | |
| updateMessageDisplay(); | |
| // New Functions for fetching tokenizer config | |
| function parseRepoUrl(url) { | |
| const hfUrlRegex = /^(?:https?:\/\/)?(?:huggingface\.co|hf\.co)\/([^/]+)\/([^/]+)$/; | |
| const shortRegex = /^([^/]+)\/([^/]+)$/; | |
| let match = url.match(hfUrlRegex); | |
| if (!match) { | |
| match = url.match(shortRegex); | |
| } | |
| if (match) { | |
| return { user: match[1], repo: match[2] }; | |
| } | |
| return null; | |
| } | |
| async function fetchTokenizerConfig(user, repo) { | |
| const apiUrl = `https://huggingface.co/${user}/${repo}/raw/main/tokenizer_config.json`; | |
| try { | |
| const response = await fetch(apiUrl); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error("Error fetching tokenizer config:", error); | |
| displayError(`Failed to fetch tokenizer config: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| function displayError(message) { | |
| console.error(message); | |
| } | |
| function populateConfigFields(config) { | |
| const bosTokenInput = document.getElementById('bosToken'); | |
| const eosTokenInput = document.getElementById('eosToken'); | |
| const templateTextarea = document.getElementById('template'); | |
| bosTokenInput.value = config?.bos_token ?? ""; | |
| eosTokenInput.value = config?.eos_token ?? ""; | |
| let chatTemplate = config?.chat_template ?? ""; | |
| // Decode HTML entities | |
| if (chatTemplate) { | |
| const tempElement = document.createElement('div'); | |
| tempElement.innerHTML = chatTemplate; | |
| chatTemplate = tempElement.textContent; | |
| } | |
| templateTextarea.value = chatTemplate; | |
| } | |
| async function handleFetchConfig() { | |
| const repoUrl = document.getElementById('repoUrl').value; | |
| const repoInfo = parseRepoUrl(repoUrl); | |
| if (!repoInfo) { | |
| displayError("Invalid Hugging Face repository URL format."); | |
| return; | |
| } | |
| document.getElementById('repoUrl').parentElement.classList.add('loading'); | |
| const { user, repo } = repoInfo; | |
| const config = await fetchTokenizerConfig(user, repo); | |
| document.getElementById('repoUrl').parentElement.classList.remove('loading'); | |
| if (config) { | |
| console.log("Tokenizer Config:", JSON.stringify(config, null, 2)); | |
| populateConfigFields(config); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |