Skip to content

Instantly share code, notes, and snippets.

@DarrenSem
Last active June 21, 2024 21:31
Show Gist options
  • Save DarrenSem/b6a6ae4b81a3a6993fab62b41f008e57 to your computer and use it in GitHub Desktop.
Save DarrenSem/b6a6ae4b81a3a6993fab62b41f008e57 to your computer and use it in GitHub Desktop.
VueJS-ChatGPT-Playground (minimal, proof of concept direct API fetch) - Pinia for state management, CTRL+ENTER = send, CTRL + UP/DOWN = prompt history
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT Playground (starting up)</title>
<style>
.byline {
font-family: Verdana;
font-size: 80%;
padding-bottom: 0.7rem;
}
pre {
margin-bottom: 3rem;
}
.message {
margin-bottom: 10px;
}
textarea {
width: 100%;
height: 60px;
margin-bottom: 10px;
}
button {
display: block;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- https://unpkg.com/browse/vue/dist/ -->
<script src="https://unpkg.com/vue@3.4.29/dist/vue.global.js"></script>
<script src="https://unpkg.com/vue-demi@0.14.8/lib/index.iife.js"></script>
<script src="https://unpkg.com/pinia@2.1.7/dist/pinia.iife.js"></script>
<script defer>
let elPrompt, elSend;
const PROCESSING = "Processing...";
const setPlaceholder = processing => {
elPrompt.setAttribute( "placeholder", processing ? PROCESSING : elPrompt.dataset.placeholder );
elSend.disabled = !!processing;
setTimeout( () => elPrompt.focus(), 40 );
};
const q = (sel, root) => (root || document).querySelector(sel ?? null);
const { createApp, ref, onMounted } = Vue;
const { createPinia, defineStore } = Pinia;
const ChatGPTPlayground = {
template: `
<div>
<h1>
VueJS+Pinia chatbot Playground (minimal) via OpenAI API direct access</h1>
<p class="byline">By <a href="https://github.com/DarrenSem/Vue-ChatGPT">Darren Semotiuk</a></p>
<div v-for="(msg, index) in chat.messages" :key="index" class="message">
<p><b>{{ msg.role.toUpperCase() }}:</b></p>
<pre>{{ msg.content }}</pre>
</div>
<textarea
data-prompt="prompt" v-model="chat.userMessage"
@keydown="chat.handleCTRLkey"
data-placeholder="Enter user message...
[CTRL]+[UP/DOWN] for prompt history"
></textarea>
<button title="[CTRL]+[ENTER] to send" data-send="send" @click="chat.sendMessage">Send</button>
</div>
`,
setup() {
const chat = useChatStore();
onMounted(() => {
chat.loadApiKey();
document.title = q("h1").innerText;
elPrompt = q('[data-prompt="prompt"]');
elSend = q('[data-send="send"]');
setPlaceholder(!"processing");
});
return { chat };
}
};
const useChatStore = defineStore('chat', {
state: () => ({
model: "gpt-4o", // "gpt-3.5-turbo";
messages: [
{ role: "system", content: "You are an expert at XYZ (context defined later).\nLet's think step by step.\n\nXYZ context is defined as" }
],
userMessage: "",
historyIndex: null,
}),
actions: {
async loadApiKey() {
let apiKey = localStorage.getItem("apiKey");
if (!apiKey) {
apiKey = prompt("Please enter your OpenAI API key: (will be kept in localStorage only)");
if (apiKey) {
localStorage.setItem("apiKey", apiKey);
}
}
return apiKey;
},
async callChatApi(message) {
const apiKey = await this.loadApiKey();
if (!apiKey) return;
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: message
})
});
if (!response.ok) {
localStorage.removeItem("apiKey");
alert("API call failed; please verify your API key is correct.");
return;
};
const data = await response.json();
return data.choices[0].message;
},
async sendMessage() {
if (!this.userMessage.trim()) return;
this.messages.push({ role: "user", content: this.userMessage });
this.userMessage = "";
this.historyIndex = null;
setPlaceholder(!!"processing");
const botReply = await this.callChatApi(this.messages);
setPlaceholder(!"processing");
if (botReply) {
this.messages.push(botReply);
}
},
handleCTRLkey(event) {
if (!event.ctrlKey) return;
// if (elPrompt.getAttribute("placeholder") === PROCESSING) return;
if (event.key === "Enter") {
event.preventDefault();
elSend.click();
};
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault();
const rotateHistory = offset => {
const userPrompts = this.messages.reduce( (acc, el, i) => ( i % 2 && acc.push(el), acc ), [] );
const L = userPrompts.length;
if(!L) return;
const prev = this.historyIndex ?? (offset < 0 ? L : -1);
const next = ( prev + offset + L ) % L;
const userPrompt = userPrompts[next];
this.userMessage = userPrompt.content;
this.historyIndex = next;
};
rotateHistory( event.key === "ArrowUp" ? -1 : 1 );
};
}
} // actions: {
}); // const useChatStore = defineStore('chat', {
const pinia = createPinia();
createApp(ChatGPTPlayground).use(pinia).mount("#app");
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment