Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>zodiac, by faetalize</title> | |
<link rel="shortcut icon" href="https://upload.wikimedia.org/wikipedia/commons/f/f0/Google_Bard_logo.svg" | |
type="image/x-icon"> | |
<link rel="stylesheet" | |
href="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/github-dark.css"> | |
<link rel="stylesheet" href="style.css"> | |
<script src="https://cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="sidebar"> | |
<div class="header"> | |
<button class="material-symbols-outlined btn-textual" id="btn-hide-sidebar"> | |
arrow_back_ios_new | |
</button> | |
<img src="https://upload.wikimedia.org/wikipedia/commons/f/f0/Google_Bard_logo.svg" id="gemin-pro-logo"> | |
<div id="title-div"> | |
<div id="zodiac-branding">zodiac </div> | |
<div id="gemini-pro-branding">powered by Gemini Pro</div> | |
</div> | |
</div> | |
<div class="navbar"> | |
<div class="navbar-tab">Personalities</div> | |
<div class="navbar-tab">Settings</div> | |
<div class="navbar-tab-highlight"></div> | |
</div> | |
<div id="sidebar-content"> | |
<div class="sidebar-section" id="personalitySection"> | |
<label class="card-personality" | |
style="background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('https://images.fonearena.com/blog/wp-content/uploads/2023/12/Google-Gemini-AI-1024x577.jpg')"> | |
<input type="radio" name="personality" value="zodiac3" checked> | |
<div> | |
<h3 class="personality-title">zodiac</h3> | |
<p class="personality-description">zodiac is a cheerful assistant, always ready to help you with | |
your tasks.</p> | |
<p class="personality-prompt">You are zodiac, a helpful assistant created by faetalize, built | |
upon Google's Gemini Pro model. Gemini Pro is a new LLM (Large Language Model) release by | |
Google on December 2023. Your purpose is being a helpful assistant to the user.</p> | |
</div> | |
<button class="btn-textual btn-edit-card material-symbols-outlined" | |
id="btn-edit-personality-default">edit</button> | |
<button class="btn-textual btn-share-card material-symbols-outlined" | |
id="btn-share-personality-default">share</button> | |
</label> | |
<div class="btn-array" id="btn-array-personality-section"> | |
<button id="btn-add-personality">Add Personality</button> | |
<button id="btn-import-personality">Import</button> | |
</div> | |
</div> | |
<div class="sidebar-section"> | |
<input type="text" placeholder="Paste API key here" id="apiKeyInput" class="input-field"></input> | |
<h3>Generation Settings:</h3> | |
<label for="maxTokens">Max Output Tokens:</label> | |
<input type="number" id="maxTokens" class="input-field" min="1" max="4000" value="1000"></input> | |
<label for="safetySettings">Safety Settings:</label> | |
<select id="safetySettings" class="input-field"> | |
<option value="safe">Safe</option> | |
<option value="moderate">Moderate</option> | |
<option value="risky">Risky</option> | |
</select> | |
</div> | |
</div> | |
<div class="credits"> | |
Made by fætalize | |
<a href="https://github.com/faetalize/zodiac">Source Code</a> | |
</div> | |
</div> | |
<div id="mainContent"> | |
<div class="header"> | |
<button class="material-symbols-outlined btn-textual" id="btn-show-sidebar"> | |
menu | |
</button> | |
</div> | |
<div class="message-container"></div> | |
<div class="message-box"> | |
<button class="material-symbols-outlined" id="btn-reset-chat">rotate_left</button> | |
<textarea type="text" placeholder="Type your message here" id="messageInput" | |
class="input-field"></textarea> | |
<button type="submit" class="btn-textual material-symbols-outlined" id="btn-send">send</button> | |
</div> | |
</div> | |
</div> | |
<div class="overlay"> | |
<div class="header"> | |
<button class="btn-textual" id="btn-hide-overlay">BACK</button> | |
</div> | |
<div class="form" id="form-add-personality"> | |
<h1>Add Personality</h1> | |
<label for="personalityNameInput">Personality Name:</label> | |
<input type="text" placeholder="Personality Name" id="personalityNameInput" class="input-field"></input> | |
<label for="personalityDescriptionInput">Personality Description:</label> | |
<textarea id="personalityDescriptionInput" placeholder="Personality Description" | |
class="prompt-field"></textarea> | |
<label for="personalityImageURLInput">Personality Image URL:</label> | |
<input type="text" placeholder="Personality Image URL" id="personalityImageURLInput" | |
class="input-field"></input> | |
<label for="personalityPromptInput">Prompt:</label> | |
<textarea id="personalityPromptInput" placeholder="Personality Prompt" class="prompt-field"></textarea> | |
<button id="btn-submit-personality">Add Personality</button> | |
</div> | |
<div class="form" id="form-edit-personality"> | |
<h1>Edit Personality</h1> | |
<label for="personalityNameInput">Personality Name:</label> | |
<input type="text" placeholder="Personality Name" id="personalityNameInput" class="input-field"></input> | |
<label for="personalityDescriptionInput">Personality Description:</label> | |
<textarea id="personalityDescriptionInput" placeholder="Personality Description" | |
class="prompt-field"></textarea> | |
<label for="personalityImageURLInput">Personality Image URL:</label> | |
<input type="text" placeholder="Personality Image URL" id="personalityImageURLInput" | |
class="input-field"></input> | |
<label for="personalityPromptInput">Prompt:</label> | |
<textarea id="personalityPromptInput" placeholder="Personality Prompt" class="prompt-field"></textarea> | |
<button id="updatePersonality">Save</button> | |
</div> | |
</div> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"@google/generative-ai": "https://esm.run/@google/generative-ai" | |
} | |
} | |
</script> | |
<script type="module"> | |
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; | |
import { GoogleGenerativeAI } from "@google/generative-ai"; | |
import { HarmBlockThreshold, HarmCategory } from "@google/generative-ai"; | |
const safetySettings = [ | |
{ | |
category: HarmCategory.HARM_CATEGORY_HARASSMENT, | |
threshold: HarmBlockThreshold.BLOCK_NONE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, | |
threshold: HarmBlockThreshold.BLOCK_NONE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, | |
threshold: HarmBlockThreshold.BLOCK_NONE, | |
}, | |
{ | |
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, | |
threshold: HarmBlockThreshold.BLOCK_NONE, | |
} | |
]; | |
const systemPrompt = "If needed, format your answer using markdown." + | |
"Today's date is" + new Date().toDateString() + "." + | |
"End of system prompt."; | |
//load api key from local storage into input field | |
const API_KEY = document.querySelector("#apiKeyInput"); | |
const maxTokens = document.querySelector("#maxTokens"); | |
API_KEY.value = localStorage.getItem("API_KEY"); | |
maxTokens.value = localStorage.getItem("maxTokens"); | |
if (maxTokens.value == "") maxTokens.value = 1000; | |
//function to hide element smoothly then setting display none after the transition time elapsed | |
function hideElement(element) { | |
element.style.transition = 'opacity 0.2s'; | |
element.style.opacity = '0'; | |
setTimeout(function () { | |
element.style.display = 'none'; | |
}, 200); | |
} | |
function getSanitized(string) { | |
return DOMPurify.sanitize(string.replace(/</g, "<").replace(/>/g, ">").trim()); | |
} | |
function showElement(element) { | |
// Wait for other transitions to complete (0.2s delay) | |
setTimeout(function () { | |
// Change display property | |
element.style.display = 'flex'; | |
// Wait for next frame for display change to take effect | |
requestAnimationFrame(function () { | |
// Start opacity transition | |
element.style.transition = 'opacity 0.2s'; | |
element.style.opacity = '1'; | |
}); | |
}, 200); | |
} | |
const tabs = document.querySelectorAll(".navbar-tab"); | |
const sidebarViews = document.querySelectorAll(".sidebar-section"); | |
const highlight = document.querySelector(".navbar-tab-highlight"); | |
highlight.style.width = `calc(100% / ${tabs.length})`; | |
let currentTab = undefined; | |
function navigateTo(tab) { | |
if (tab == tabs[currentTab]) { | |
return; | |
} | |
// set the highlight to match the size of the tab element | |
let tabIndex = [...tabs].indexOf(tab); | |
if (tabIndex < 0 || tabIndex >= sidebarViews.length) { | |
console.error("Invalid tab index: " + tabIndex); | |
return; | |
} | |
if (currentTab != undefined) { | |
hideElement(sidebarViews[currentTab]); | |
} | |
showElement(sidebarViews[tabIndex]); | |
currentTab = tabIndex; | |
highlight.style.left = `calc(100% / ${tabs.length} * ${tabIndex})`; | |
} | |
tabs.forEach(element => { | |
element.addEventListener("click", () => { | |
navigateTo(element); | |
}) | |
}); | |
sidebarViews.forEach(view => { | |
hideElement(view); | |
}); | |
navigateTo(tabs[0]); | |
const personalityCardsInnerInput = document.querySelectorAll(".card-personality input"); | |
let personalityCards = document.querySelectorAll(".card-personality"); | |
const formsOverlay = document.querySelector(".overlay"); | |
const hideOverlayButton = document.querySelector("#btn-hide-overlay"); | |
const addPersonalityForm = document.querySelector("#form-add-personality"); | |
const addPersonalityButton = document.querySelector("#btn-add-personality"); | |
const editDefaultPersonalityForm = document.querySelector("#form-edit-personality"); | |
const editDefaultPersonalityButton = document.querySelector("#btn-edit-personality-default"); | |
const submitNewPersonalityButton = document.querySelector("#btn-submit-personality"); | |
const updatePersonalityButton = document.querySelector("#updatePersonality"); | |
const resetChatButton = document.querySelector("#btn-reset-chat"); | |
const messageContainer = document.querySelector(".message-container"); | |
const sendMessageButton = document.querySelector("#btn-send"); | |
function darkenBg(element) { | |
let elementBackgroundImageURL = element.style.backgroundImage.match(/url\((.*?)\)/)[1].replace(/('|")/g, ''); | |
element.style.backgroundImage = `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('${elementBackgroundImageURL}')`; | |
} | |
function lightenBg(element) { | |
let elementBackgroundImageURL = element.style.backgroundImage.match(/url\((.*?)\)/)[1].replace(/('|")/g, ''); | |
element.style.backgroundImage = `url('${elementBackgroundImageURL}')`; | |
} | |
function sharePersonality(personality) { | |
//export personality to json | |
const personalityJSON = { | |
name: personality.querySelector(".personality-title").innerText, | |
description: personality.querySelector(".personality-description").innerText, | |
prompt: personality.querySelector(".personality-prompt").innerText, | |
//base64 encode image | |
image: personality.style.backgroundImage.match(/url\((.*?)\)/)[1].replace(/('|")/g, '') | |
} | |
const personalityJSONString = JSON.stringify(personalityJSON); | |
//download | |
const element = document.createElement('a'); | |
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(personalityJSONString)); | |
element.setAttribute('download', `${personalityJSON.name}.json`); | |
element.style.display = 'none'; | |
document.body.appendChild(element); | |
element.click(); | |
document.body.removeChild(element); | |
} | |
//handle share button click | |
personalityCards.forEach(card => { | |
const shareButton = card.querySelector(".btn-share-card"); | |
shareButton.addEventListener("click", () => { | |
sharePersonality(card); | |
}) | |
}); | |
const importPersonalityButton = document.querySelector("#btn-import-personality"); | |
importPersonalityButton.addEventListener("click", () => { | |
const fileInput = document.createElement('input'); | |
fileInput.type = 'file'; | |
fileInput.addEventListener('change', () => { | |
const file = fileInput.files[0]; | |
const reader = new FileReader(); | |
reader.onload = function (e) { | |
const personalityJSON = JSON.parse(e.target.result); | |
insertPersonality(personalityJSON); | |
}; | |
reader.readAsText(file); | |
}); | |
fileInput.click(); | |
}); | |
//handle radio input check change | |
personalityCardsInnerInput.forEach(input => { | |
input.addEventListener("change", () => { | |
// Darken all cards | |
personalityCards.forEach(card => { | |
const cardBackgroundImageURL = card.style.backgroundImage.match(/url\((.*?)\)/)[1].replace(/('|")/g, ''); | |
card.style.outline = "0px solid rgb(150 203 236)"; | |
darkenBg(card); | |
}) | |
// Lighten selected card | |
input.parentElement.style.outline = "3px solid rgb(150 203 236)"; | |
lightenBg(input.parentElement); | |
}) | |
// Set initial outline | |
if (input.checked) { | |
lightenBg(input.parentElement); | |
input.parentElement.style.outline = "3px solid rgb(150 203 236)"; | |
} | |
}) | |
function showAddPersonalityForm() { | |
showElement(formsOverlay); | |
showElement(addPersonalityForm); | |
} | |
function showEditPersonalityForm() { | |
showElement(formsOverlay); | |
showElement(editDefaultPersonalityForm); | |
} | |
function closeOverlay() { | |
hideElement(formsOverlay); | |
hideElement(addPersonalityForm); | |
hideElement(editDefaultPersonalityForm); | |
} | |
function insertPersonality(personalityJSON) { | |
const personalitySection = document.querySelector("#personalitySection"); | |
const personalitySectionActions = document.querySelector("#btn-array-personality-section"); | |
const personalityCard = document.createElement("label"); | |
personalityCard.classList.add("card-personality"); | |
personalityCard.style.backgroundImage = `url('${personalityJSON.image}')`; | |
personalityCard.innerHTML = ` | |
<input type="radio" name="personality" value="${personalityJSON.name}"> | |
<div> | |
<h3 class="personality-title">${personalityJSON.name}</h3> | |
<p class="personality-description">${personalityJSON.description}</p> | |
<p class="personality-prompt">${personalityJSON.prompt}</p> | |
</div> | |
<button class="btn-textual btn-edit-card material-symbols-outlined" id="btn-edit-personality-${personalityJSON.name}">edit</button> | |
<button class="btn-textual btn-share-card material-symbols-outlined" id="btn-share-personality-${personalityJSON.name}">share</button> | |
`; | |
//insert personality card before the button array | |
personalitySection.insertBefore(personalityCard, personalitySectionActions); | |
darkenBg(personalityCard); | |
personalityCards = document.querySelectorAll(".card-personality"); | |
//add input event listener | |
personalityCard.querySelector("input").addEventListener("change", () => { | |
// Darken all cards | |
personalityCards.forEach(card => { | |
const cardBackgroundImageURL = card.style.backgroundImage.match(/url\((.*?)\)/)[1].replace(/('|")/g, ''); | |
card.style.outline = "0px solid rgb(150 203 236)"; | |
darkenBg(card); | |
}) | |
// Lighten selected card | |
personalityCard.style.outline = "3px solid rgb(150 203 236)"; | |
lightenBg(personalityCard); | |
}) | |
const sharebtn = personalityCard.querySelector(".btn-share-card"); | |
sharebtn.addEventListener("click", () => { | |
sharePersonality(personalityCard); | |
}); | |
} | |
function submitNewPersonality() { | |
const personalityName = document.querySelector("#form-add-personality #personalityNameInput"); | |
const personalityDescription = document.querySelector("#form-add-personality #personalityDescriptionInput"); | |
const personalityImageURL = document.querySelector("#form-add-personality #personalityImageURLInput"); | |
const personalityPrompt = document.querySelector("#form-add-personality #personalityPromptInput"); | |
const personalitySection = document.querySelector("#personalitySection"); | |
if (personalityName.value == "") { | |
alert("Please enter a personality name"); | |
return; | |
} | |
if (personalityPrompt.value == "") { | |
alert("Please enter a personality prompt"); | |
return; | |
} | |
//to json | |
const personalityJSON = { | |
name: personalityName.value, | |
description: personalityDescription.value, | |
prompt: personalityPrompt.value, | |
image: personalityImageURL.value | |
} | |
insertPersonality(personalityJSON); | |
closeOverlay(); | |
} | |
function getSanitized(string){ | |
return DOMPurify.sanitize(string.replace(/</g, "<").replace(/>/g, ">").trim()); | |
} | |
async function run() { | |
const msg = document.querySelector("#messageInput"); | |
let msgText = getSanitized(msg.value); | |
msg.value = ""; | |
document.getElementById('messageInput').style.height = "2.5rem"; //This will reset messageInput box to its normal size. | |
if (msgText == "") { | |
return; | |
} | |
const maxTokens = document.querySelector("#maxTokens"); | |
const API_KEY = document.querySelector("#apiKeyInput"); | |
const selectedPersonalityTitle = document.querySelector("input[name='personality']:checked + div .personality-title").innerText; | |
const selectedPersonalityDescription = document.querySelector("input[name='personality']:checked + div .personality-description").innerText; | |
const selectedPersonalityPrompt = document.querySelector("input[name='personality']:checked + div .personality-prompt").innerText; | |
//chat history | |
let chatHistory = []; | |
//get chat history from message container | |
const messageElements = messageContainer.querySelectorAll(".message"); | |
messageElements.forEach(element => { | |
const messageroleapi = element.querySelector(".message-role-api").innerText; | |
const messagetext = element.querySelector(".message-text").innerText; | |
chatHistory.push({ | |
role: messageroleapi, | |
parts: [{ text: messagetext }] | |
}) | |
}) | |
//reverse order of chat history | |
chatHistory.reverse(); | |
// | |
const toneExamples = []; | |
if (API_KEY.value == "") { | |
alert("Please enter an API key"); | |
return; | |
} | |
const generationConfig = { | |
maxOutputTokens: maxTokens.value, | |
temperature: 0.9 | |
}; | |
const genAI = new GoogleGenerativeAI(API_KEY.value); | |
const model = genAI.getGenerativeModel({ model: "gemini-pro" }); | |
const chat = model.startChat({ | |
generationConfig, safetySettings, | |
history: [ | |
{ | |
role: "user", | |
parts: [{ text: `Personality Name: ${selectedPersonalityTitle}, Personality Description: ${selectedPersonalityDescription}, Personality Prompt: ${selectedPersonalityPrompt}. ${systemPrompt}` }] | |
}, | |
{ | |
role: "model", | |
parts: [{ text: `Okay. From now on, I shall play the role of ${selectedPersonalityTitle}. Your prompt and described personality will be used for the rest of the conversation.` }] | |
}, | |
...toneExamples, | |
...chatHistory | |
] | |
}) | |
//create new message div for the user's message then append to message container's top | |
const newMessage = document.createElement("div"); | |
newMessage.classList.add("message"); | |
newMessage.innerHTML = ` | |
<h3 class="message-role">You:</h3> | |
<div class="message-role-api" style="display: none;">user</div> | |
<p class="message-text">${getSanitized(msgText)}</p> | |
//<p class="message-text">${msgText}</p> | |
`; | |
messageContainer.insertBefore(newMessage, messageContainer.firstChild); | |
const result = await chat.sendMessageStream(msgText); | |
//create new message div for the model's reply then append to message container's top | |
const newReply = document.createElement("div"); | |
newReply.classList.add("message"); | |
newReply.classList.add("message-model"); | |
newReply.innerHTML = ` | |
<h3 class="message-role">${selectedPersonalityTitle}:</h3> | |
<div class="message-role-api" style="display: none;">model</div> | |
<p class="message-text">${DOMPurify.sanitize(marked.parse(rawText))}</p> | |
//get the p element inside the message div | |
const replyText = newReply.querySelector(".message-text"); | |
messageContainer.insertBefore(newReply, messageContainer.firstChild); | |
let rawText = ""; | |
for await (const chunk of result.stream) { | |
rawText += chunk.text(); | |
replyText.innerHTML = DOMPurify.sanitize(marked.parse(rawText)); | |
void replyText.offsetHeight; // Force reflow | |
hljs.highlightAll(); | |
} | |
//save api key to local storage | |
localStorage.setItem("API_KEY", API_KEY.value); | |
localStorage.setItem("maxTokens", maxTokens.value); | |
} | |
//reset chat button (async) | |
resetChatButton.addEventListener("click", () => { | |
//remove all messages | |
messageContainer.innerHTML = ""; | |
}); | |
const messageInput = document.querySelector("#messageInput"); | |
//if more than one line, set height to scrollheight, and revert upon delete | |
messageInput.addEventListener("input", () => { | |
if (messageInput.value.split("\n").length == 1) { | |
messageInput.style.height = "2.5rem"; | |
} | |
else { | |
messageInput.style.height = ""; | |
messageInput.style.height = messageInput.scrollHeight + "px"; | |
} | |
}) | |
const sidebarDismissButton = document.querySelector("#btn-hide-sidebar"); | |
sidebarDismissButton.addEventListener("click", () => { | |
hideElement(document.querySelector(".sidebar")); | |
}) | |
const showSidebarButton = document.querySelector("#btn-show-sidebar"); | |
showSidebarButton.addEventListener("click", () => { | |
showElement(document.querySelector(".sidebar")); | |
}) | |
//unhide sidebar if width > 768px (event hander) | |
window.addEventListener("resize", () => { | |
if (window.innerWidth > 768) { | |
showElement(document.querySelector(".sidebar")); | |
} | |
}) | |
hideOverlayButton.addEventListener("click", closeOverlay); | |
addPersonalityButton.addEventListener("click", showAddPersonalityForm); | |
editDefaultPersonalityButton.addEventListener("click", showEditPersonalityForm); | |
submitNewPersonalityButton.addEventListener("click", submitNewPersonality); | |
sendMessageButton.addEventListener("click", run); | |
// ... | |
</script> | |
</body> | |
</html> |