langxlang
Version:
LLM wrapper for OpenAI GPT and Google Gemini and PaLM 2 models
692 lines (615 loc) • 18.9 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LXL Studio v1</title>
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Noto+Serif:ital,wght@0,100..900;1,100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet">
</head>
<style>
/* light mode */
/* :root {
--pageBg: #fff;
--panelBg: #f0f0f0;
--fg: #000;
--chatBg: #f8f8f8;
--borders: #ccc;
} */
/* dark mode */
:root {
--pageBg: #111;
--panelBg: #333;
--fg: #fff;
--chatBg: #222;
--borders: #444;
--btnSuggest: #825F07;
--btnSuggestBorder: #7B5B08;
/* --btnSuggest: rgb(136, 115, 0); */
}
body {
margin: 0;
font-family: "Noto Sans", sans-serif;
background-color: var(--pageBg);
color: var(--fg);
}
a {
color: lightblue;
text-decoration: none;
}
button {
font-family: "Noto Sans", sans-serif;
}
.container {
/* margin-top: 10px; */
margin-left: 1vw;
margin-right: 1vw;
}
.bar {
display: grid;
grid-template-columns: minmax(max-content, 1fr) auto minmax(max-content, 1fr);
margin: 8px;
>div {
display: flex;
align-items: center;
}
.buttons {
justify-content: flex-end;
}
}
.content {
display: grid;
grid-template-columns: 212px calc(98vw - 212px);
.panel {
color: var(--fg);
.panel-box {
padding-top: 0;
padding: 8px;
/* padding-right: 10px; */
border-radius: 5px;
background-color: var(--panelBg);
margin-bottom: 1vh;
.panel-title {
font-weight: bold;
}
}
.options {
padding-top: 5px;
padding-bottom: 5px;
.name {
font-weight: bold;
color: #E0E0E0;
font-family: monospace;
}
.ovalue {
/* text-align: center; */
margin-left: -4px;
display: flex;
vertical-align: middle;
justify-content: space-between;
align-items: center;
}
.auxinput {
width: 66px;
margin-left: 2px;
border: none;
color: var(--fg);
text-align: center;
border-radius: 5px;
vertical-align: middle;
background-color: var(--panelBg);
}
.oinput {
margin-top: 5px;
border: 0.5px solid var(--borders);
border-radius: 5px;
vertical-align: middle;
background-color: blue;
accent-color: darkgray;
}
.option {
padding-top: 5px;
padding-bottom: 5px;
>div {
vertical-align: middle;
/* text-align: right; */
}
}
.oinput:focus {
outline: 1.5px solid blue;
}
}
}
.conversation {
background-color: var(--chatBg);
color: var(--fg);
border-radius: 5px;
padding: 1rem;
margin-left: 1%;
}
}
progress {
/* width: 100%; */
/* height: 10px; */
/* border-radius: 10px; */
/* background-color: #1A1A1A; */
accent-color: darkgray;
}
</style>
<style>
.message {
display: flex;
margin-top: 4px;
padding-top: 2px;
padding-bottom: 2px;
/* spacing between */
.user {
margin-right: 5px;
margin-top: 6px;
width: 80px;
max-width: 80px;
.modelName {
font-weight: bold;
text-align: center;
border-radius: 6px;
padding: 6px;
background-color: #1A1A1A;
&:hover {
cursor: pointer;
background-color: #0A0A0A;
transition: 0.5s background-color;
}
}
.small {
margin-left: -10px;
padding-top: 2px;
font-size: 0.75rem;
color: #888;
text-align: right;
}
}
.text {
margin-left: 5px;
margin-top: 8px;
padding-bottom: 6px;
width: 100%;
& pre {
white-space: pre-wrap;
margin-top: 0;
margin-bottom: 0;
}
}
.edittext {
margin-top: -4px;
background-color: var(--panelBg);
color: var(--fg);
width: calc(100% - 1rem);
padding: 10px;
border-radius: 5px;
border: 1px solid var(--borders);
}
}
.messagebar {
width: 100%;
padding-top: 20px;
.message-text textarea {
background-color: var(--panelBg);
color: var(--fg);
width: calc(100% - 1rem);
padding: 10px;
border-radius: 5px;
border: 1px solid var(--borders);
}
.bottomrow {
margin-top: 10px;
.left {
display: inline-block;
>div {
display: inline-block;
font-size: 0.75rem;
vertical-align: middle;
text-emphasis: center;
}
}
}
.right {
/* display: inline-block; */
float: right;
}
}
.textarea {
background-color: var(--panelBg);
color: var(--fg);
width: calc(100% - 1rem);
padding: 10px;
border-radius: 5px;
border: 1px solid var(--borders);
}
.select {
/* margin-top: 10px; */
margin-left: 5px;
padding: 10px;
border-radius: 5px;
background-color: #1A1A1A;
color: var(--fg);
border: 1px solid var(--borders);
&:hover {
cursor: pointer;
background-color: #0A0A0A;
transition: 0.5s background-color;
}
&:disabled {
background-color: #505050;
color: #808080;
border: none;
cursor: not-allowed;
}
}
.btn {
margin-left: 5px;
padding: 10px;
border-radius: 5px;
background-color: #1A1A1A;
color: var(--fg);
border: 1px solid var(--borders);
&:hover {
cursor: pointer;
background-color: #0A0A0A;
transition: 0.5s background-color;
}
&:disabled {
background-color: #505050;
color: #808080;
border: none;
cursor: not-allowed;
}
}
.btn-suggest {
font-weight: bold;
background-color: var(--btnSuggest);
background-color: gainsboro;
color: black;
/* border: 1px solid var(--btnSuggestBorder); */
&:hover {
/* background-color: var(--btnSuggestBorder); */
background-color: darkgray;
}
}
#submission-text {
height: 80px;
}
.rendertext {
& ul,
ol,
p {
margin-top: 0;
margin-bottom: 6px;
}
& ul,
ol {
padding-left: 20px;
padding-bottom: 10px;
}
}
</style>
<body>
<dialog id="loading-modal" open>
<style>
/* add a backdrop to .container */
.container {
/* background-color: rgba(0, 0, 0, 0.5); */
filter: blur(2px);
}
</style>
<p>Please wait while the connection to the LXL server is established...</p>
</dialog>
</body>
<script>
window.debugging = true
</script>
<!-- <script src="d:\Development\Projects\Nodejs\node-basic-ipc\dist\basic-ipc.js"></script> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"
integrity="sha512-LhccdVNGe2QMEfI3x4DVV3ckMRe36TfydKss6mJpdHjNFiV07dFpS2xzeZedptKZrwxfICJpez09iNioiSZ3hA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="C:\Users\extre\Development\Projects\ML\LXL\dist\langxlang.js"></script>
<script type="module">
import { h, render } from 'https://esm.sh/preact';
import { useState, useEffect } from 'https://esm.sh/preact/hooks';
import htm from 'https://esm.sh/htm';
// Initialize htm with Preact
const html = htm.bind(h);
const playground = window.lxl.createSession({
serverAddress: 'ws://localhost:8091'
})
playground.bindForm({
generationOptions: {
model: 'model'
}
})
let isReady
playground.on('ready', () => {
document.getElementById('loading-modal').remove()
console.log('Models', playground.models)
playground.setModelsList(playground.models)
playground.updateForModel(playground.models[0].model)
playground.messages = []
isReady = true
})
setTimeout(() => {
if (!isReady) {
// reload the page
window.location.reload()
}
}, 5000)
window.playground = playground
const nextId = () => (Date.now() << 8) | (Math.random() * 0xff)
function setMessageContent(id, to) {
to = to || ''
document.getElementById(id + '-edit').textContent = to
document.getElementById(id + '-render').textContent = to
}
function appendMessageContent(id, to) {
to = to || ''
document.getElementById(id + '-edit').textContent += to
document.getElementById(id + '-render').textContent += to
}
function md2html(text) {
const converter = new showdown.Converter()
return converter.makeHtml(text)
}
function countTokens(text) {
return window.lxl.tools.tokenizer.tokenize('gpt-4', text).length
}
class ChatSession {
constructor() {
this.messages = []
}
// Update our messages with the messages in DOM's textareas
updateMessagesFromDOM(containerId) {
const elements = document.querySelectorAll(`#${containerId} .edittext`)
elements.forEach((element) => {
const messageId = element.id.split('-')[1]
const message = this.messages.find(msg => msg.id === messageId)
if (message) {
message.text.raw = element.value
message.text.html = md2html(element.value)
}
})
}
async sendMessage(msg, model) {
// role: { name: 'User' }, text: { html: 'Hello, how are you?', raw: 'Hello, how are you?' }
this.messages.push({ role: { id: 'user' }, id: nextId(), text: { html: msg, raw: msg } })
const newModelMsg = { role: { id: 'model' }, id: nextId(), pending: true, text: { html: '', raw: '' } }
const effect = window.lxl.tools.createTypeWriterEffectStream({
write(chunk) {
appendMessageContent('message-' + newModelMsg.id, chunk)
}
})
const request = playground.sendChatCompletionRequest(this.messages.map(msg => {
return { role: msg.role.id, content: msg.text.raw }
}), { model }, (chunk) => {
effect(chunk)
newModelMsg.text.raw += (chunk.content || '')
})
playground.emit('updateError', null)
this.messages.push(newModelMsg)
playground.emit('conversationUpdate')
const response = await request
console.log('Complete Response', response)
if (response.error) {
playground.emit('updateError', response.error)
}
newModelMsg.pending = false
const [result] = response.result
newModelMsg.text.raw = result.content
playground.emit('conversationUpdate')
return response
}
countTokens() {
return this.messages.reduce((acc, msg) => acc + countTokens(msg.text.raw), 0)
}
}
const chatSession = new ChatSession()
window.chatSession = chatSession
function Bar() {
return html`<div class="bar">
<div></div>
<div class="title"><strong>LXL Studio</strong></div>
<div class="buttons">
<button class="btn" stylez="color:gold;font-weight: bold;">Export / Share 📤</button>
<button class="btn">Accounts</button>
</div>
</div>`
}
function PanelOptions() {
const options = {
temperature: { name: 'Temperature', range: [0, 2], default: 1 },
maxOutputTokens: { name: 'Output Tokens', range: [0, 1_000_000], default: 1_000_000 },
top_k: { name: 'Top K', range: [0, 100], default: 0 },
top_p: { name: 'Top P', range: [0, 1], default: 0 },
}
const optionsHtml = Object.entries(options).map(([key, opts]) => {
return html`<div class="option">
<div class="name">${opts.name}</div>
<div class="ovalue">
<input class="oinput" type="range" min="${opts.range[0]}" max="${opts.range[1]}" value="${opts.default}" />
<input class="auxinput" type="text" value="${opts.default}" />
</div>
</div>`
})
return html`<div class="panel-box">
<div class="panel-title">Options</div>
<div class="options">
${optionsHtml}
</div>
</div>`
}
function Panel() {
return html`<div class="panel">
<${PanelOptions} />
</div>`
}
function pushMessageAndSubmit(userText, model) {
if (!model) {
return
}
chatSession.updateMessagesFromDOM()
chatSession.sendMessage(userText, typeof model === 'string' ? JSON.parse(model) : model)
}
// Handle tabs
function _onKeyDown(event) {
// console.log('Key down', event)
if (event.key === 'Tab') {
event.target.setRangeText(' ', event.target.selectionStart, event.target.selectionEnd, 'end');
event.preventDefault();
}
}
function ConversationMessage({ message: { id, role, text, pending } }) {
function onKeyDown(event) {
_onKeyDown(event)
}
const roleName = { user: 'User', model: 'Model' }[role.id]
const rendered = { __html: md2html(text.raw || '') }
// console.log('Rendering message', [text.raw, rendered])
return html`<div class="message" id="message-${id}">
<div class="user">
<div class="modelName">${roleName}</div>
</div>
<div class="text">
${pending
? html`<pre class="rendertext" id="message-${id}-render">${text.raw || ''}</pre>`
: html`<div class="rendertext" id="message-${id}-render" dangerouslySetInnerHTML=${rendered}></div>`
}
<textarea class="edittext" id="message-${id}-edit" style="display:none" onkeydown=${onKeyDown}>${text.raw}</textarea>
</div>
</div>`
}
function ConversationSubmissionBar({ updateMessages }) {
const [activeError, setActiveError] = useState(false)
const [tokenCount, setTokenCount] = useState(0)
const [aggregateTokenCount, setAggregateTokenCount] = useState(0)
const [models, setModels] = useState(playground.models || [])
const [activeModel, setActiveModel] = useState('')
useEffect(() => {
playground.on('modelsListUpdate', () => {
setModels(playground.models)
// set active to gpt-3.5-turbo
// const DEFAULT_MODEL = 'gpt-3.5-turbo'
const DEFAULT_MODEL = 'gemini-1.0-pro'
const model = playground.models.find(model => model.model === DEFAULT_MODEL)
console.log('Setting active model', model)
const modelValue = JSON.stringify({ service: model.service, author: model.author, model: model.model })
document.getElementById('model').value = modelValue
setActiveModel(modelValue)
})
playground.on('conversationUpdate', () => {
setAggregateTokenCount(chatSession.countTokens())
})
playground.on('updateError', (error) => {
setActiveError(error)
})
}, [])
function onModelChange(event) {
const model = event.target.value
setActiveModel(model)
}
function onKeyDown(event) {
_onKeyDown(event)
// if we get a control + enter, submit the message
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
if (!activeModel) {
return
}
event.preventDefault()
const text = event.target.value
pushMessageAndSubmit(text, activeModel)
event.target.value = ''
setTokenCount(0)
} else {
setTokenCount(countTokens(event.target.value))
}
}
function addMessage(roleId) {
const el = document.getElementById('submission-text')
const currentText = el.value
console.log('Adding message', roleId, currentText)
chatSession.messages.push({
role: { id: roleId },
text: { raw: currentText }
})
updateMessages()
el.value = ''
setTokenCount(0)
}
function onSubmitClick() {
const text = document.getElementById('submission-text').value
pushMessageAndSubmit(text, activeModel)
updateMessages()
}
// - <a href="javascript:void">Show tokens</a>
return html`<div class="messagebar">
<div class="message-text">
<textarea id="submission-text" placeholder="Type a message" onkeydown=${onKeyDown}><mark>Hello world!</mark></textarea>
</div>
<div class="bottomrow">
<div class="left">
<button class="btn" onClick=${() => addMessage('user')}>Add User</button>
<button class="btn" onClick=${() => addMessage('model')}>Add Model</button>
<div style="padding-left:8px;text-align:center;width:fit-content;line-height: 1.5;">
<div>GPT-4 Tokens</div>
<div>${aggregateTokenCount} ${tokenCount ? html`+ ${tokenCount} pending` : null}</div>
</div>
</div>
<div class="right">
<progress value="50" max="100"></progress>
<select class="select" name="model" id="model" onChange=${onModelChange} disabled=${models.length === 0} value=${activeModel}>
${models.length === 0 ? html`<option value="" disabled>Please wait...</option>` : null}
${models.map(model => html`<option value=${JSON.stringify({ service: model.service, model: model.model })}>${model.displayName}</option>`)}
</select>
<button class="btn btn-suggest" onClick=${onSubmitClick} disabled=${!activeModel}>Run</button>
</div>
</div>
<div>
${activeError ? html`<p style="color: red; text-align: center;">${activeError}</p>` : null}
</div>
</div>`
}
function ConversationMessages({ messages }) {
return html`<div class="messages">
${messages.map(msg => html`<${ConversationMessage} message=${msg} />`)}
</div>`
}
function Conversation() {
const testMessages = [
{ role: { name: 'User' }, text: { html: 'Hello, how are you?', raw: 'Hello, how are you?' } },
{ role: { name: 'Model' }, text: { html: 'I am fine, thank you.', raw: 'I am fine, thank you.' } },
]
const [messages, setMessages] = useState([...chatSession.messages])
function updateMessages() {
setMessages([...chatSession.messages])
}
useEffect(() => {
playground.on('conversationUpdate', updateMessages)
}, [])
return html`<div class="conversation">
<div style="font-weight: bold;">Conversation</div>
<${ConversationMessages} updateMessages=${updateMessages} messages=${messages} id="convo-messages" />
<${ConversationSubmissionBar} updateMessages=${updateMessages} />
</div>`
}
function Content() {
return html`<div class="content">
<${Panel} />
<${Conversation} />
</div>`
}
function App() {
return html`<div class="container">
<${Bar} />
<${Content} />
</div>`
}
render(html`<${App} />`, document.body);
</script>
</html>