Public
Edited
Oct 7, 2024
Insert cell
Insert cell
Insert cell
driver = {
picocss;
autoAnimate(chat);
ai;

const control = htl.html`<div>
<div id="chat-history"></div>
<footer>
<form id="chat-input">
<label for="user-input" hidden>Enter your message:</label>
<textarea id="user-input" rows="5" cols="30" placeholder="Type your message here"></textarea>
<button type="submit">Send</button>
</form>
</footer>`;
const form = control.querySelector("#chat-input");
const input = control.querySelector("#user-input");
const history = control.querySelector("#chat-history");
const submitButton = control.querySelector("button");
autoAnimate(control);
autoAnimate(history);

if (chat.firstElementChild !== control) {
chat.firstElementChild.replaceWith(control);
}

input.onkeydown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submitButton.click();
}
};

form.onsubmit = async (e) => {
e.preventDefault();
toggleForm(form);
const userText = input.value;
input.value = "";
const userMessageElement = makeChatHistoryElement("user", userText);
history.appendChild(userMessageElement);
(
userMessageElement.previousElementSibling || userMessageElement
).scrollIntoView({ behavior: "smooth" });
try {
// TODO: send the input text to the backend
//await Promises.delay(1000);
const completion = await ai.createChatCompletion({
model: "gpt-3.5-turbo",
messages: getChatHistoryData()
});
console.log(completion);
const message = completion.data?.choices[0]?.message;
if (message) {
const responseElement = makeChatHistoryHtmlElement(
message.role,
message.content
);
console.log(responseElement.outerHTML);
await addChatHistoryHtmlMessage(responseElement);
}
} finally {
toggleForm(form);
input.focus();
(
userMessageElement.previousElementSibling || userMessageElement
).scrollIntoView({ behavior: "smooth" });
}
};

function makeChatHistoryElement(role, content) {
if (role === "user") content += "\n\n";
return htl.html`<p data-chatRole="${role}" data-chatContent=${content}>${(
content || ""
).trim()}</p>`;
}

function getChatHistoryData() {
return [...history.querySelectorAll("p[data-chatRole]")].map((element) => ({
role: element.getAttribute("data-chatRole"),
content: element.getAttribute("data-chatContent")
}));
}

async function addChatHistoryMessage(chatHistoryElement) {
// Clone chatHistoryElement
const clonedElement = chatHistoryElement.cloneNode(true);

// Split content into words and wrap each in a span
const content = clonedElement.getAttribute("data-chatContent") || "";
const words = content.trim().split(" ");

// Clear original content
clonedElement.textContent = "";

// Animate and append to history
autoAnimate(clonedElement);
history.appendChild(clonedElement);

// Append each word as a child span
for (const word of words) {
await Promises.delay(10);
const span = document.createElement("span");
span.textContent = word;
clonedElement.appendChild(span);
clonedElement.appendChild(document.createTextNode(" "));
}
}

function makeChatHistoryHtmlElement(role, content) {
if (role === "user") content += "\n\n";
return htl.html`<p data-chatRole="${role}" data-chatContent=${content}>${md`${
content || ""
}`}</p>`;
}

async function addChatHistoryHtmlMessage(chatHistoryElement) {
const clonedElement = chatHistoryElement.cloneNode(true);
clonedElement.innerHTML = "";
autoAnimate(clonedElement);
history.appendChild(clonedElement);
await appendChildrenWithDelay(chatHistoryElement, clonedElement);
}

async function appendChildrenWithDelay(sourceElement, targetElement) {
for (const child of sourceElement.children) {
await Promises.delay(10);
const clonedElement = child.cloneNode(true);
clonedElement.innerHTML = "";
autoAnimate(clonedElement);
targetElement.appendChild(clonedElement);
if (child.children.length > 0) {
await appendChildrenWithDelay(child, clonedElement);
}
}
}
}
Insert cell
Insert cell
Insert cell
ai = new openai.OpenAIApi(new openai.Configuration({ apiKey: OPENAI_API_KEY }))
Insert cell
ai.listEngines()
Insert cell
ai.listModels()
Insert cell
async function* $async(factory = async () => Promises.delay(1000)) {
if (this) {
yield this;
} else {
yield htl.html`<p class="$async">"Waiting..."</p>`;
}
yield await factory();
}
Insert cell
{
for await (const i of $async()) {
yield i;
}
}
Insert cell
picocss = {
// https://picocss.com/docs/themes.html
// TODO - only force light on observablehq domain
document.documentElement.setAttribute("data-theme", "light");
return htl.html`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<style>
details[disabled] summary,
details.disabled summary {
pointer-events: none; /* prevents click events */
user-select: none; /* prevents text selection */
};`;
}
Insert cell
autoAnimate = {
const autoAnimate = (await import("@formkit/auto-animate")).default;
// apply to document
autoAnimate(document.documentElement);
// apply to the "header" of the page so its not blocked on loading this lib
// by referencing directly
autoAnimate(viewof OPENAI_API_KEY);
return autoAnimate;
}
Insert cell
function toggleForm(form, disable) {
let elements = form.elements;
for (let i = 0; i < elements.length; i++) {
if (typeof disable === "undefined") {
// If 'disable' is not provided, toggle the current state
elements[i].disabled = !elements[i].disabled;
} else {
// Otherwise set the disabled state to the 'disable' parameter
elements[i].disabled = disable;
}
}
}
Insert cell
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more