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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more