Last active
June 21, 2024 21:31
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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/[email protected]/dist/vue.global.js"></script> | |
<script src="https://unpkg.com/[email protected]/lib/index.iife.js"></script> | |
<script src="https://unpkg.com/[email protected]/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