Skip to content

Commit

Permalink
🌈 Interactive @Bind on a static preview (#988)
Browse files Browse the repository at this point in the history
  • Loading branch information
fonsp authored Apr 2, 2021
1 parent 9f4b23b commit b9d567d
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 22 deletions.
39 changes: 39 additions & 0 deletions frontend/common/PlutoHash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export const base64_arraybuffer = async (/** @type {BufferSource} */ data) => {
const base64url = await new Promise((r) => {
const reader = new FileReader()
reader.onload = () => r(reader.result)
reader.readAsDataURL(new Blob([data]))
})

return base64url.split(",", 2)[1]
}

export const hash_arraybuffer = async (/** @type {BufferSource} */ data) => {
// @ts-ignore
const hashed_buffer = await window.crypto.subtle.digest("SHA-256", data)
return await base64_arraybuffer(hashed_buffer)
}

export const hash_str = async (/** @type {string} */ s) => {
const data = new TextEncoder().encode(s)
return await hash_arraybuffer(data)
}

export const debounced_promises = (async_function) => {
let currently_running = false
let rerun_when_done = false

return async () => {
if (currently_running) {
rerun_when_done = true
} else {
currently_running = true
rerun_when_done = true
while (rerun_when_done) {
rerun_when_done = false
await async_function()
}
currently_running = false
}
}
}
88 changes: 88 additions & 0 deletions frontend/common/SliderServerClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { trailingslash } from "./Binder.js"
import { hash_arraybuffer, debounced_promises, base64_arraybuffer } from "./PlutoHash.js"
import { pack, unpack } from "./MsgPack.js"
import immer from "../imports/immer.js"
import _ from "../imports/lodash.js"

export const nothing_actions = ({ actions }) => Object.fromEntries(Object.keys(actions).map((k) => [k, () => {}]))

export const slider_server_actions = ({ setStatePromise, launch_params, actions, get_original_state, get_current_state, apply_notebook_patches }) => {
const notebookfile_hash = fetch(launch_params.notebookfile)
.then((r) => r.arrayBuffer())
.then(hash_arraybuffer)

notebookfile_hash.then((x) => console.log("Notebook file hash:", x))

const bond_connections = notebookfile_hash
.then((hash) => fetch(trailingslash(launch_params.slider_server_url) + "bondconnections/" + encodeURIComponent(hash) + "/"))
.then((r) => r.arrayBuffer())
.then((b) => unpack(new Uint8Array(b)))

bond_connections.then((x) => console.log("Bond connections:", x))

const mybonds = {}
const bonds_to_set = {
current: new Set(),
}
const request_bond_response = debounced_promises(async () => {
const base = trailingslash(launch_params.slider_server_url)
const hash = await notebookfile_hash
const graph = await bond_connections

if (bonds_to_set.current.size > 0) {
const to_send = new Set(bonds_to_set.current)
bonds_to_set.current.forEach((varname) => (graph[varname] ?? []).forEach((x) => to_send.add(x)))
console.debug("Requesting bonds", bonds_to_set.current, to_send)
bonds_to_set.current = new Set()

const mybonds_filtered = Object.fromEntries(Object.entries(mybonds).filter(([k, v]) => to_send.has(k)))

const packed = pack(mybonds_filtered)

const url = base + "staterequest/" + encodeURIComponent(hash) + "/"

try {
const use_get = url.length + (packed.length * 4) / 3 + 20 < 8000

const response = use_get
? await fetch(url + encodeURIComponent(await base64_arraybuffer(packed)), {
method: "GET",
})
: await fetch(url, {
method: "POST",
body: packed,
})

const { patches, ids_of_cells_that_ran } = unpack(new Uint8Array(await response.arrayBuffer()))

await apply_notebook_patches(
patches,
immer((state) => {
const original = get_original_state()
ids_of_cells_that_ran.forEach((id) => {
state.cell_results[id] = original.cell_results[id]
})
})(get_current_state())
)
} catch (e) {
console.error(e)
}
}
})

return {
...nothing_actions({ actions }),
set_bond: async (symbol, value, is_first_value) => {
setStatePromise(
immer((state) => {
state.notebook.bonds[symbol] = { value: value }
})
)
if (mybonds[symbol] == null || !_.isEqual(mybonds[symbol].value, value)) {
mybonds[symbol] = { value: value }
bonds_to_set.current.add(symbol)
await request_bond_response()
}
},
}
}
63 changes: 41 additions & 22 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { html, Component, useState, useEffect, useMemo } from "../imports/Preact
import immer, { applyPatches, produceWithPatches } from "../imports/immer.js"
import _ from "../imports/lodash.js"

import { create_pluto_connection, resolvable_promise, ws_address_from_base } from "../common/PlutoConnection.js"
import { create_pluto_connection } from "../common/PlutoConnection.js"
import { init_feedback } from "../common/Feedback.js"

import { FilePicker } from "./FilePicker.js"
Expand All @@ -19,11 +19,12 @@ import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js"
import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js"
import { handle_log } from "../common/Logging.js"
import { PlutoContext, PlutoBondsContext } from "../common/PlutoContext.js"
import { pack, unpack } from "../common/MsgPack.js"
import { unpack } from "../common/MsgPack.js"
import { useDropHandler } from "./useDropHandler.js"
import { start_binder, BinderPhase } from "../common/Binder.js"
import { read_Uint8Array_with_progress, FetchProgress } from "./FetchProgress.js"
import { BinderButton } from "./BinderButton.js"
import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js"

const default_path = "..."
const DEBUG_DIFFING = false
Expand Down Expand Up @@ -195,6 +196,8 @@ export class Editor extends Component {
disable_ui: !!(url_params.get("disable_ui") ?? window.pluto_disable_ui),
//@ts-ignore
binder_url: url_params.get("binder_url") ?? window.pluto_binder_url,
//@ts-ignore
slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url,
}

this.state = {
Expand Down Expand Up @@ -651,17 +654,31 @@ patch: ${JSON.stringify(
connect_metadata: { notebook_id: this.state.notebook.notebook_id },
}).then(on_establish_connection)

const real_actions = this.actions
const fake_actions = Object.fromEntries(Object.keys(this.actions).map((k) => [k, () => {}]))
this.real_actions = this.actions
this.fake_actions =
this.launch_params.slider_server_url != null
? slider_server_actions({
setStatePromise: this.setStatePromise,
actions: this.actions,
launch_params: this.launch_params,
apply_notebook_patches,
get_original_state: () => this.original_state,
get_current_state: () => this.state.notebook,
})
: nothing_actions({
actions: this.actions,
})

this.on_disable_ui = () => {
document.body.classList.toggle("disable_ui", this.state.disable_ui)
document.head.querySelector("link[data-pluto-file='hide-ui']").setAttribute("media", this.state.disable_ui ? "all" : "print")
//@ts-ignore
this.actions = this.state.disable_ui ? fake_actions : real_actions //heyo
this.actions =
this.state.disable_ui || (this.launch_params.slider_server_url != null && !this.state.connected) ? this.fake_actions : this.real_actions //heyo
}
this.on_disable_ui()

this.original_state = null
if (this.state.static_preview) {
;(async () => {
const r = await fetch(this.launch_params.statefile)
Expand All @@ -670,8 +687,10 @@ patch: ${JSON.stringify(
statefile_download_progress: progress,
})
})
const state = unpack(data)
this.original_state = state
this.setState({
notebook: unpack(data),
notebook: state,
initializing: false,
binder_phase: this.state.offer_binder ? BinderPhase.wait_for_user : null,
})
Expand Down Expand Up @@ -870,22 +889,22 @@ patch: ${JSON.stringify(
}
})

// Disabled because we don't want to accidentally delete cells
// or we can enable it with a prompt
// Even better would be excel style: grey out until you paste it. If you paste within the same notebook, then it is just a move.
// document.addEventListener("cut", (e) => {
// if (!in_textarea_or_input()) {
// const serialized = this.serialize_selected()
// if (serialized) {
// navigator.clipboard
// .writeText(serialized)
// .then(() => this.delete_selected("Cut"))
// .catch((err) => {
// alert(`Error cutting cells: ${e}`)
// })
// }
// }
// })
document.addEventListener("cut", (e) => {
// Disabled because we don't want to accidentally delete cells
// or we can enable it with a prompt
// Even better would be excel style: grey out until you paste it. If you paste within the same notebook, then it is just a move.
// if (!in_textarea_or_input()) {
// const serialized = this.serialize_selected()
// if (serialized) {
// navigator.clipboard
// .writeText(serialized)
// .then(() => this.delete_selected("Cut"))
// .catch((err) => {
// alert(`Error cutting cells: ${e}`)
// })
// }
// }
})

document.addEventListener("paste", async (e) => {
const topaste = e.clipboardData.getData("text/plain")
Expand Down

0 comments on commit b9d567d

Please sign in to comment.