Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] end-user-programming "Bots" exploration #145

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@
"webpack-cli": "^3.1.0"
},
"dependencies": {
"@types/classnames": "^2.2.6",
"@types/lodash": "^4.14.115",
"@types/micromatch": "^3.1.0",
"@types/node-localstorage": "^1.3.0",
"@types/react": "^16.4.14",
"@types/react-dom": "^16.0.8",
"@types/react-portal": "^4.0.1",
"@types/react-transition-group": "^2.0.14",
"@types/stats.js": "^0.17.0",
"@types/text-encoding": "^0.0.34",
"automerge": "^0.9.1",
"browser-process-hrtime": "^0.1.2",
"bs58": "^4.0.1",
Expand All @@ -70,6 +80,7 @@
"js-crc": "^0.2.0",
"lodash": "^4.17.10",
"micromatch": "^3.1.10",
"node-localstorage": "^1.3.1",
"random-access-chrome-file": "^1.1.1",
"react": "^16.5.2",
"react-dom": "^16.5.2",
Expand All @@ -79,7 +90,9 @@
"rxjs": "^6.3.2",
"stats.js": "^0.17.0",
"tap-browser-el": "^2.0.0",
"text-encoding": "^0.7.0",
"utp": "github:pvh/utp",
"ws": "^6.1.0"
"ws": "^6.1.0",
"yargs": "^12.0.2"
}
}
1 change: 1 addition & 0 deletions src/apps/make-bot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default/
57 changes: 57 additions & 0 deletions src/apps/make-bot/Bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from "react"
import * as Reify from "../../data/Reify"
import * as Widget from "../../components/Widget"
import { AnyDoc } from "automerge/frontend"

export interface Model {
id: string
code: string
}

interface Props extends Widget.Props<Model> {}

interface State {
err?: string
}

class Bot extends React.Component<Props, State> {
state = {
err: undefined,
}

static reify(doc: AnyDoc): Model {
return {
id: Reify.string(doc.id),
code: Reify.string(doc.string),
}
}

componentDidMount() {
this.runCode()
}

componentDidUpdate(prevProps: Props) {
if (this.props.doc.code !== prevProps.doc.code) {
this.runCode()
}
}

runCode = () => {
const { code } = this.props.doc
let err

try {
eval(`(() => { ${code} })()`)
} catch (e) {
err = e
}

this.setState({ err })
}

render() {
return null
}
}

export default Widget.create("Bot", Bot, Bot.reify)
45 changes: 45 additions & 0 deletions src/apps/make-bot/bots/journaling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const last = arr => arr[arr.length - 1]

const addTimestamp = type => {
const url = Content.create("Text")

Content.change(url, doc => {
doc.content =
type === "date"
? new Date().toDateString().split("")
: new Date().toTimeString().split("")
})

Content.open(Content.store.getWorkspace()).once(workspace => {
const boardUrl =
workspace.navStack.length > 0
? last(workspace.navStack).url
: workspace.rootUrl

const id = UUID.create()

const card = {
id,
x: 50,
y: 50,
z: 100,
width: type === "date" ? 200 : 400,
height: 40,
url,
}

Content.open(boardUrl).change(doc => {
doc.cards[id] = card
})
})
}

makeBot("journaling", bot => {
bot.action("Add time", () => {
addTimestamp("time")
})

bot.action("Add date", () => {
addTimestamp("date")
})
})
65 changes: 65 additions & 0 deletions src/apps/make-bot/bots/organizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const last = arr => arr[arr.length - 1]

const cleanUp = () => {
// constants for gallery
const MARGIN = 20
const WINDOW_HEIGHT = window.innerHeight

// open workspace
Content.open(Content.store.getWorkspace())
.once(workspace => {
// grab a board
const boardUrl =
workspace.navStack.length > 0
? last(workspace.navStack).url
: workspace.rootUrl

Content.change(boardUrl, board => {
// all cards that are not a bot
const nonBotCards = Object.values(board.cards).filter(
card => card.url.indexOf("Bot") < 0,
)

// calculate average width of a card
const avgWidth =
nonBotCards.reduce((memo, card) => card.width + memo, 0) /
nonBotCards.length

// some imperative code to arrange in columns
let column = 0
let topOffset = 0

nonBotCards.forEach(card => {
// update card aspect ratio
const aspect = card.width / card.height
card.width = avgWidth
card.height = avgWidth / aspect

// move to new column if needed
if (topOffset + card.height + MARGIN * 2 > WINDOW_HEIGHT) {
column++
topOffset = 0
}

// update x & y pos
card.x = column * (avgWidth + MARGIN) + MARGIN
card.y = topOffset + MARGIN

// store offset
topOffset = card.y + card.height
})
}).close()
})
.close()
}

makeBot("organizer", bot => {
// and now, instead of working as a button-based action
// bot.action("Clean Up!", cleanUp)

// we can make it autonomous!
bot.autonomous(
"Board", // act on any change on board
cleanUp,
)
})
3 changes: 3 additions & 0 deletions src/apps/make-bot/make-bot
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

../../../node_modules/.bin/cross-env TS_NODE_FILES=1 TS_NODE_CACHE_DIRECTORY=.cache TS_NODE_SKIP_IGNORE=1 node -r ts-node/register ./make-bot.ts $@
118 changes: 118 additions & 0 deletions src/apps/make-bot/make-bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const { argv } = require("yargs")
import * as fs from "fs"

const { workspace, id: botId } = argv
const fileName = argv._[0]

if (!workspace || !botId || !fileName || !fs.existsSync(fileName)) {
console.log(
"Usage: ./make-bot --workspace workspaceId --id botId bot-code.js",
)
process.exit(0)
}

const code = fs.readFileSync(fileName, "utf-8")

import { LocalStorage } from "node-localstorage"

interface Global {
localStorage: LocalStorage
}
declare var global: Global

global.localStorage = new LocalStorage("./localstorage")

const raf = require("random-access-file")
import { Doc } from "automerge/frontend"
import { last, once } from "lodash"

import * as Link from "../../data/Link"
import Store from "../../data/Store"
import StoreBackend from "../../data/StoreBackend"

import CloudClient from "discovery-cloud/Client"
import { Hypermerge, FrontendManager } from "hypermerge"

import "./Bot" // we have local bot implementation since the Capstone one uses css imports
import * as Board from "../../components/Board"
import * as DataImport from "../../components/DataImport"
import * as Workspace from "../../components/Workspace"
import Content from "../../components/Content"

const hm = new Hypermerge({ storage: raf })
const storeBackend = new StoreBackend(hm)
Content.store = new Store()

storeBackend.sendQueue.subscribe(msg => Content.store.onMessage(msg))
Content.store.sendQueue.subscribe(msg => storeBackend.onMessage(msg))

hm.joinSwarm(
new CloudClient({
url: "wss://discovery-cloud.herokuapp.com",
id: hm.id,
stream: hm.stream,
}),
)

hm.ready.then(hm => {
console.log("Ready!")

Content.open<Workspace.Model>(workspace).once(workspace => {
console.log("Opened workspace", workspace)

const boardUrl =
workspace.navStack.length > 0
? last(workspace.navStack)!.url
: workspace.rootUrl

if (!boardUrl) {
console.log("Can't find a board, exiting...")
return
}

console.log(`Using board: ${boardUrl}`)

const boardHandle = Content.open<Board.Model>(boardUrl).once(doc => {
const botExists = !!doc.cards[botId]

// console.log("board doc", doc)

if (botExists) {
console.log(`Updating bot ${botId}`)

const botUrl = doc.cards[botId]!.url

if (!botUrl) return

// update
const botHandle = Content.open(botUrl).change(bot => {
bot.code = code
})
} else {
console.log(`Creating new bot: ${botId}`)

// create
const botUrl = Content.create("Bot")

const botHandle = Content.open(botUrl).change(doc => {
doc.id = botId
doc.code = code
})

boardHandle.change(board => {
const card = {
id: botId,
z: 0,
x: 0,
y: 0,
width: 200,
height: 200,
url: botUrl,
}

board.cards[botId] = card
})
}
})
})
})
12 changes: 11 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ import "./Text"
import "./Table"
import "./Workspace"
import "./HTML"
import "./Bot"

import * as Workspace from "./Workspace"

const log = Debug("component:app")

require("events").EventEmitter.prototype._maxListeners = 1000

type State = {
url?: string
shouldHideFPSCounter?: boolean
}

export default class App extends React.Component<State> {
type Props = {}

import * as UUID from "../data/UUID"
window.UUID = UUID

export default class App extends React.Component<Props, State> {
state: State = {}

initWorkspace() {
Expand Down
Loading