Public
Edited
Oct 20, 2023
1 fork
Importers
11 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
restartAction = Generators.observe((notify) => {
const onrestart = () => {
console.log("restart");
viewof display.fortune.value = "";
viewof display.name.value = "";
viewof display.question.value = "";
viewof display.name.singleton.disabled = false;
viewof display.question.singleton.disabled = false;
viewof display.cards.style.display = "none";
viewof display.deck.reset();
notify();
};
viewof display.restart.addEventListener("click", onrestart);
invalidation.then(() =>
viewof display.restart.removeEventListener("click", onrestart)
);

notify();
})
Insert cell
Insert cell
Insert cell
shareId = {
console.log("share id");
const search = new URLSearchParams(location.search).get("share");
if (search) return search;
const path = location.pathname.split("/").slice(-1)[0];
if (!path.match(/[.$\[\]#\/]/)) return path;
}
Insert cell
Insert cell
previousFortune = {
console.log("previousFortune");
const snapshot = await firebase
.database()
.ref(`/@tomlarkworthy/tarot-backend/calls/${shareId}`)
.once("value");
return snapshot.val();
}
Insert cell
Insert cell
loadPreviousFortune = {
console.log("loadPreviousFortune");
if (previousFortune) {
viewof display.fortune.value =
previousFortune.reading.choices[0].text ||
previousFortune.reading.choices[0].message.content;
viewof display.name.value = previousFortune.name;
viewof display.question.value = previousFortune.question;
viewof display.cards.cards.value = await findCardsByName(
previousFortune.cards
);
viewof display.share.value = `${baseURL}/${shareId}`;
viewof display.deck.value = 3;
viewof display.deck.style.display = "none";
viewof display.name.singleton.disabled = true;
viewof display.question.singleton.disabled = true;
}
}
Insert cell
Insert cell
transitions = {
display;
restartAction; // recompute when display changes
loadPreviousFortune; // Load history (happens once)
console.log("transition");
viewof display.style.display = "block";
viewof display.name.style.display = "block";
viewof display.question.style.display = "block";
viewof display.deck.style.display = "block";

if (display.name.length == 0) {
// User has not filled in their name, hide everything except the name control
viewof display.question.style.display = "none";
viewof display.deck.style.display = "none";
viewof display.cards.style.display = "none";
viewof display.fortune.style.display = "none";
viewof display.share.style.display = "none";
viewof display.restart.style.display = "none";
var state = "askName";
} else if (display.question.length == 0) {
viewof display.deck.style.display = "none";
viewof display.cards.style.display = "none";
viewof display.fortune.style.display = "none";
viewof display.share.style.display = "none";
viewof display.restart.style.display = "none";
display.cards.cards = await getCards({ numCards: 3 });
var state = "askQuestion";
} else if (display.deck < 3) {
viewof display.cards.style.display = "none";
viewof display.fortune.style.display = "none";
viewof display.restart.style.display = "none";
} else if (display.deck >= 3) {
viewof display.cards.style.display = "block";
viewof display.fortune.style.display = "block";
viewof display.name.style.display = "none";
viewof display.question.style.display = "none";
viewof display.deck.style.display = "none";
viewof display.restart.style.display = "inline-block";
var state = "showCards";
}

if (viewof display.fortune.value.length === 0 && display.deck >= 0) {
// generate fortune *once*, and only once we are messing with cards
viewof display.fortune.value = "...";
viewof display.share.style.display = "none";
getFortune({
token: user.getIdToken(),
name: display.name,
cards: display.cards.cards,
question: display.question
})
.then((fortune) => {
viewof display.share.style.display = "inline-block";
viewof display.share.value = `${baseURL}/${fortune.id}`;
viewof display.fortune.value = fortune.reading;
// We will also fetch it to prewarm the cache
fetch(viewof display.share.value);
})
.catch((err) => {
viewof display.fortune.value = err.message;
});
viewof display.name.singleton.disabled = true;
viewof display.question.singleton.disabled = true;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof name = whoInput()
Insert cell
Insert cell
Insert cell
Insert cell
viewof question = questionInput()
Insert cell
Insert cell
Insert cell
Insert cell
viewof fortuneOutputExample = fortuneOutput()
Insert cell
(viewof fortuneOutputExample.value = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
`)
Insert cell
Insert cell
Insert cell
Insert cell
(viewof shareButtonExample.value = "https://cool2.com ")
Insert cell
Insert cell
Insert cell
viewof restartButtonExamplerestartButton
Insert cell
Insert cell
cardBack = FileAttachment("image-4.webp").image()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pickCardsExample
Insert cell
viewof pickCardsExample.reset()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
socialData = (
await adminFirebase
.database()
.ref(`@tomlarkworthy/tarot-backend/calls/${socialImageParams.shareId}`)
.once("value")
).val()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
socialImageInner = async ({ reading, cards } = {}) => {
const promises = [];
const ui = svg`<svg viewBox="0 0 ${boardw} 618" width="100%">
<image width="100%" href=${await FileAttachment(
"imgonline-com-ua-TextureSeamless-ddu5gFbCzzWeXp (1) (1).webp"
).url()} />
${cards.map(
(c, i) => htl.svg`<g transform="translate(${
cpad * i + coffsetx
} ${coffsety})">
<rect x="0" y="${texty}" width="${cwidth}px" height="40px" stroke="red" stroke-width="3px" />
<text x=${cwidth / 2} y="${
texty + 25
}" style="font: 40px serif; fill: red;" dominant-baseline="middle" text-anchor="middle" >${
i == 0 ? "PAST" : i == 1 ? "PRESENT" : "FUTURE"
}</text>
<image width=${cwidth} href=${c.imgURL} />
`
)}

<foreignObject x="50" width="${boardw - 100}px" y="${
(boardh * 2) / 3 - 30
}" height="${boardh / 3}">
<div style="background-color: ${textBackground}; border-radius: 10px; border: solid ${borderColor};">
<div class="fortune" style="color:white; width:${
boardw - 140
}px; height:${boardh / 3 - 5}px;font-family: Montserrat">
${reading}
</div>
</div>
</foreignObject>
</svg>`;

await Promise.all(
[...ui.querySelectorAll("image")].map((img) => img.decode())
).catch(() => {});
return ui;
}
Insert cell
fitImage = {
await textFit(image.querySelector(".fortune"), {
alignHoriz: true,
alignVert: true
});
// textFit did not look like it was always applied so I think we need to slow it down
return new Promise((resolve) => setTimeout(() => resolve(image), 100));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
readingOrEmpty = viewof reading.value || ""
Insert cell
Insert cell
Insert cell
Insert cell
getFortune = async ({ name, token, cards, question } = {}) => {
const url = `${apiServer.href}?config=${btoa(
JSON.stringify({
name,
cards: cards.map((c) => c.name),
question
})
)}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${await user.getIdToken()}`
}
});
if (response.status === 200) return await response.json();
else {
throw new Error(`Error ${response.status}: ${await response.text()}`);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Inputs.button("trigger health check", {
required: true,
reduce: async () => {
return await Promise.all(
Array.from({ length: 5 }).map(async () =>
viewof config.send({
health: true
})
)
);
}
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof debugResults = Inputs.button("run debug case", {
required: true,
reduce: async () =>
viewof config.send({
...parsedDebugConfig,
OPENAI_API_KEY,
ADMIN_SERVICE_ACCOUNT,
token: await user.getIdToken()
})
})
Insert cell
debugResults
Insert cell
Insert cell
Insert cell
Insert cell
viewof config = flowQueue({ timeout_ms: 45000 })
Insert cell
config
Insert cell
Insert cell
validatedConfig = {
var msg = "unknown";

if (config.health) {
return viewof config.respond("ok"); // Check queue health if param is 'health'
}

if (!config.question || config.question == "") {
msg = "No question";
} else if (config.question.length >= QUESTION_MAX_LENGTH) {
msg = "Question too long";
} else if (!config.name || config.name == "") {
msg = "No name";
} else if (config.name.length >= NAME_MAX_LENGTH) {
msg = "Name too long";
} else {
return config;
}

const err = new Error(msg);
err.status = 400;
viewof config.reject(err);
throw new Error(`Invalid request ${msg}`);
}
Insert cell
Insert cell
Insert cell
Insert cell
currentUsersRequestsInLastDay = requestsInLastDay(currentUser.uid)
Insert cell
rateLimitOk = {
if (currentUsersRequestsInLastDay < 20) {
return true;
} else {
console.log("currentUsersRequestsInLastDay", currentUsersRequestsInLastDay);
const err = new Error("You have exceeded your quota");
err.status = 402;
viewof config.reject(err);
return invalidation;
}
}
Insert cell
Insert cell
openapi_reponse = (rateLimitOk,
recordMeteredUse(currentUser.uid),
fetchp("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: `Pretend you are legendary fortune teller. "${config.name}" asks "${config.question}". The cards are "${config.cards[0]}" (past position), "${config.cards[1]}" (present) and the "${config.cards[2]}" (future). Please respond with just what you would say, including dramatic flare.`
}
],
...settings
})
}))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
result = openapi_reponse.status === 200
? openapi_reponse.json()
: viewof config.reject(new Error(await openapi_reponse.text()))
Insert cell
Insert cell
id = {
try {
return persistHistory({
...viewof config.value,
settings,
reading: result
});
} catch (err) {
viewof config.respond(result);
throw err;
}
}
Insert cell
Insert cell
persistHistory = async ({ name, cards, question, reading, settings } = {}) => {
const snap = await adminFirebase
.database()
.ref(`/@tomlarkworthy/tarot-backend/calls/`)
.push({
name,
question,
cards,
reading,
settings,
time: { ".sv": "timestamp" }
});
return snap.key;
}
Insert cell
Insert cell
// Not needed anymore
classification = 0 /*contentFilter({
content: result.choices[0].text,
API_KEY: config.OPENAI_API_KEY
})*/
Insert cell
Insert cell
Inputs.button("record use", {
reduce: () => recordMeteredUse(user.uid)
})
Insert cell
Insert cell
requestsInLastDay = async (uid) => {
const snap = await adminFirebase
.database()
.ref(`/@tomlarkworthy/tarot-backend/users/${uid}/history`)
.once("value");
return Object.values(snap.val() || {}).reduce(
(sum, timestamp) =>
timestamp > Date.now() - 1000 * 60 * 60 * 24 ? sum + 1 : sum,
0
);
}
Insert cell
quota = htl.html`<a target="_blank" href="https://console.firebase.google.com/u/0/project/larkworthy-dfb11/database/larkworthy-dfb11-default-rtdb/data/@tomlarkworthy/tarot-backend/users/${user.uid}/history">quota records`
Insert cell
Insert cell
contentFilter = async ({ content, API_KEY } = {}) => {
const response = await fetch(
`https://api.openai.com/v1/engines/content-filter-alpha/completions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`
},
body: JSON.stringify({
prompt: `<|endoftext|>${content}\n--\nLabel:`,
temperature: 0,
max_tokens: 1,
top_p: 0,
logprobs: 10
})
}
);

if (response.status !== 200) throw new Error(await response.text());

const responseJson = await response.json();
var output_label = responseJson["choices"][0]["text"];

const toxic_threshold = -0.355;

if (output_label == "2") {
const logprobs = responseJson["choices"][0]["logprobs"]["top_logprobs"][0];
if (logprobs["2"] < toxic_threshold) {
const logprob_0 = logprobs["0"];
const logprob_1 = logprobs["1"];

if (logprob_0 && logprob_1) {
if (logprob_0 >= logprob_1) {
output_label = "0";
} else {
output_label = "1";
}
} else if (logprob_0) {
output_label = "0";
} else {
output_label = "1";
}
}
}
if (!["0", "1", "2"].includes(output_label)) {
output_label = "2";
}
return Number.parseInt(output_label);
}
Insert cell
viewof exampleFilter = Inputs.button("testContentFilter", {
reduce: async () => {
const text = loremIpsum({
count: 1, // Number of "words", "sentences", or "paragraphs"
format: "plain", // "plain" or "html"
paragraphLowerBound: 3, // Min. number of sentences per paragraph.
paragraphUpperBound: 7, // Max. number of sentences per paragarph.
random: Math.random, // A PRNG function
sentenceLowerBound: 5, // Min. number of words per sentence.
sentenceUpperBound: 15, // Max. number of words per sentence.
suffix: "\n", // Line ending, defaults to "\n" or "\r\n" (win32)
units: "paragraph" // paragraph(s), "sentence(s)", or "word(s)"
});
return await contentFilter({
API_KEY: OPENAI_API_KEY,
content: text
});
}
})
Insert cell
Insert cell
uploadObject = async ({ name, access_token, content_type, content } = {}) => {
/*
curl -X POST --data-binary @data.txt \
-H "Authorization: Bearer $OAUTH2_TOKEN" \
-H "Content-Type: application/txt" \
"https://storage.googleapis.com/upload/storage/v1/b/larkworthy-dfb11.appspot.com/o?uploadType=media&name=data.txt"
*/
const bucket = "larkworthy-dfb11.appspot.com";

const response = await fetch(
`https://storage.googleapis.com/upload/storage/v1/b/${bucket}/o?uploadType=media&name=${encodeURIComponent(
name
)}`,
{
method: "POST",
headers: {
"content-type": content_type,
Authorization: `Bearer ${access_token}`
},
body: content
}
);
if (response.status !== 200) throw new Error(`${await response.text()}`);
else return response.json();
}
Insert cell
Insert cell
fortuneImg = {
try {
return socialImage({
shareId: id
});
} catch (err) {
viewof config.respond(result);
throw err;
}
}
Insert cell
fortuneImageData = await fetch(fortuneImg)
.then((res) => res.blob())
.then((res) => res.arrayBuffer())
Insert cell
Insert cell
fortuneImageData
Insert cell
Insert cell
cloudImage = ({
upload: await uploadObject({
name: `@tomlarkworthy/tarot-backend/images/${id}`,
content_type: "image/jpeg",
access_token,
content: fortuneImageData
}),
id
})
Insert cell
Insert cell
page = ({
name,
question,
imgURL,
shareId,
debug = false
} = {}) => `<!DOCTYPE html>
<head>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js" as="script" />
<link rel="preload" href="https://api.observablehq.com/@tomlarkworthy/tarot-backend.js?v=3" as="script" />

<title>${name ? `Tarot Reading for ${name}` : "Tarot Reader"}</title>
<meta name="viewport" content="width=device-width;initial-scale=1.0;user-scalable=no;user-scalable=0;">
<meta property="og:title" content="${
name ? `Tarot Reading for ${name}` : "Tarot Reader"
}">
<meta property="og:description" content="${
question || "Ask a question for the cards"
}">
<meta name="description" content="${
question || "Ask a question for the cards"
}">

<meta property="og:type" content="article" />
<meta property="og:image" content="${
imgURL ||
"https://storage.googleapis.com/larkworthy-dfb11.appspot.com/%40tomlarkworthy/tarot-backend/images/-MyWC6L4ZE1HtVWM1SRc"
}">
<meta property="og:url" content="${
shareId ? `${baseURL}/${shareId}` : "https://thetarot.online"
}">
<meta name="twitter:card" content="summary_large_image">

</head>
<body style="background-color:black;">
<div id="display"></div>
<div id="debugger"></div>
<script type="module">
import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js";
import notebook from "https://api.observablehq.com/@tomlarkworthy/tarot-backend.js?v=3";
new Runtime().module(notebook, name => {

if (name === "viewof display") {
return new Inspector(document.querySelector("#display"));
} else if (name === "transitions") {
return true;
} else if (name === "ndd" && ${debug}) {
return new Inspector(document.querySelector("#debugger"));
}
});
</script>
<script defer data-domain="thetarot.online" src="https://plausible.io/js/plausible.js"></script>`
Insert cell
Insert cell
uploads = ({
html: await uploadObject({
name: `@tomlarkworthy/tarot-backend/pages/${id}`,
content_type: "text/html",
access_token,
content: page({
shareId: id,
name: config.name,
question: config.question,
imgURL: cloudImage.upload.mediaLink
})
}),
img: cloudImage.upload,
id
})
Insert cell
Insert cell
responder = {
uploads;
try {
if (classification == 0) {
viewof config.respond({
id,
reading: result.choices[0].message.content
});
} else {
const err = new Error(
`Response was classified as sensative or unsafe. Try a different question.`
);
err.status = 400;
throw err;
}
} catch (err) {
viewof config.reject(err);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
deploy(
"index",
(req, res) => {
res.send(page({ debug: true }));
},
{
hostNotebook: "@tomlarkworthy/tarot-backend"
}
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
endpoint(
"variables",
async (req, res) => {
res.json(
(await notebookSnapshot("trackingVariable_e3366d24de62")).map(
(variable) => ({
state: variable.state,
name: variable.name,
// Note these cells might contain personal information, so we only allow errors values to leave the environment
...(variable.state === "rejected" && { value: variable.value })
})
)
);
},
{
hostNotebook: "@tomlarkworthy/tarot-backend"
}
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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