diff --git a/.github/workflows/build-site.yml b/.github/workflows/build-site.yml
index 4ad34b47..cd429c5d 100644
--- a/.github/workflows/build-site.yml
+++ b/.github/workflows/build-site.yml
@@ -8,6 +8,7 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
+ workflow_dispatch:
jobs:
build:
@@ -39,7 +40,7 @@ jobs:
name: built-output
path: ./dist
- deploy-plugin:
+ deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
@@ -49,6 +50,15 @@ jobs:
with:
name: built-output
path: ./dist
+ # download the static files from https://github.com/bioimage-io/bioimageio-chatbot/archive/refs/heads/main.zip
+ # then extract the files under bioimageio_chatbot/static and store it under dist/chat
+ - name: Download and extract static files
+ run: |
+ curl -LOk https://github.com/bioimage-io/bioimageio-chatbot/archive/refs/heads/main.zip
+ unzip main.zip
+ mkdir -p dist/chat
+ cp -r bioimageio-chatbot-main/bioimageio_chatbot/static/* dist/chat
+ rm -rf main.zip bioimageio-chatbot-main
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3.5.0
env:
diff --git a/public/chat/bioimage-model-zoo-extension.imjoy.html b/public/chat/bioimage-model-zoo-extension.imjoy.html
deleted file mode 100644
index d75e8fc5..00000000
--- a/public/chat/bioimage-model-zoo-extension.imjoy.html
+++ /dev/null
@@ -1,187 +0,0 @@
-
-[TODO: write documentation for this plugin.]
-
-
-
-{
- "name": "SearchBioImageModelZoo",
- "type": "web-python",
- "version": "0.1.0",
- "description": "BioImage.IO Chatbot Extension for getting information about models, applications, datasets, etc. in the BioImage Model Zoo.",
- "tags": [],
- "ui": "",
- "cover": "",
- "inputs": null,
- "outputs": null,
- "flags": [],
- "icon": "extension",
- "api_version": "0.1.8",
- "env": "",
- "permissions": [],
- "requirements": ["pydantic"],
- "dependencies": []
-}
-
-
-
diff --git a/public/chat/imagej-js-extension.imjoy.html b/public/chat/imagej-js-extension.imjoy.html
deleted file mode 100644
index 80bb54be..00000000
--- a/public/chat/imagej-js-extension.imjoy.html
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-[TODO: write documentation for this plugin.]
-
-
-
-{
- "name": "ImageJ.JS Chatbot Extension",
- "type": "web-worker",
- "tags": [],
- "ui": "",
- "version": "0.1.0",
- "cover": "",
- "description": "Run ImageJ.JS macro in the chatbot",
- "icon": "extension",
- "inputs": null,
- "outputs": null,
- "api_version": "0.1.8",
- "env": "",
- "permissions": [],
- "requirements": [],
- "dependencies": []
-}
-
-
-
diff --git a/public/chat/index.html b/public/chat/index.html
deleted file mode 100644
index f5bf9c1e..00000000
--- a/public/chat/index.html
+++ /dev/null
@@ -1,1973 +0,0 @@
-
-
-
-
-
-
- BioImage.IO Chatbot
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Thank you for trying out the BioImage.IO Chatbot!
-
-
Please provide feedback by clicking
-
-
-
- or
-
-
- for each response, or by clicking the 'Feedback' button below.
-
-
-
Note that the chatbot is still in beta and is being actively developed, we will log the message you input into
- the chatbot for further investigation of issues and support our development. See the
- Disclaimer for more details. If you want to to remove your chat logs, please contact us via this form .
-
OK, got it!
-
-
-
- Don't ask me again
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to BioImage.IO Chatbot
-
Login
-
-
-
Sign up here
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Send
-
-
- Mount Files
-
-
- Code Interpreter
-
-
-
- Edit Profile
-
-
-
- Reset
-
-
-
- Feedback
-
-
- Options
-
-
-
-
-
-
-
-
-
-
-
-
- Save
-
-
-
-
-
-
-
-
- Submit Feedback
-
-
-
-
-
- Extensions:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/chat/pyodide-worker.js b/public/chat/pyodide-worker.js
deleted file mode 100644
index fb70a723..00000000
--- a/public/chat/pyodide-worker.js
+++ /dev/null
@@ -1,282 +0,0 @@
-const indexURL = 'https://cdn.jsdelivr.net/pyodide/v0.25.0/full/'
-importScripts(`${indexURL}pyodide.js`);
-
-(async () => {
- self.pyodide = await loadPyodide({ indexURL })
- await self.pyodide.loadPackage("micropip");
- const micropip = self.pyodide.pyimport("micropip");
- await micropip.install(['numpy', 'imjoy-rpc', 'pyodide-http']);
- // NOTE: We intentionally avoid runPythonAsync here because we don't want this to pre-load extra modules like matplotlib.
- self.pyodide.runPython(setupCode)
- self.postMessage({loading: true}) // Inform the main thread that we finished loading.
-})()
-
-let outputs = []
-
-function write(type, content) {
- self.postMessage({ type, content })
- outputs.push({ type, content })
- return content.length
-}
-
-function logService(type, url, attrs) {
- outputs.push({type, content: url, attrs: attrs?.toJs({dict_converter : Object.fromEntries})})
- self.postMessage({ type, content: url, attrs: attrs?.toJs({dict_converter : Object.fromEntries}) })
-}
-
-function show(type, url, attrs) {
- const turl = url.length > 32 ? url.slice(0, 32) + "..." : url
- outputs.push({type, content: turl, attrs: attrs?.toJs({dict_converter : Object.fromEntries})})
- self.postMessage({ type, content: url, attrs: attrs?.toJs({dict_converter : Object.fromEntries}) })
-}
-
-function store_put(key, value) {
- self.postMessage({ type: "store", key, content: `${value}` })
-}
-
-// Stand-in for `time.sleep`, which does not actually sleep.
-// To avoid a busy loop, instead import asyncio and await asyncio.sleep().
-function spin(seconds) {
- const time = performance.now() + seconds * 1000
- while (performance.now() < time);
-}
-
-// NOTE: eval(compile(source, "", "exec", ast.PyCF_ALLOW_TOP_LEVEL_AWAIT))
-// returns a coroutine if `source` contains a top-level await, and None otherwise.
-
-const setupCode = `
-import array
-import ast
-import base64
-import contextlib
-import io
-import js
-import pyodide
-import sys
-import time
-import traceback
-import wave
-import pyodide_http
-
-pyodide_http.patch_all() # Patch all libraries
-
-time.sleep = js.spin
-
-# patch hypha services
-import imjoy_rpc.hypha
-_connect_to_server = imjoy_rpc.hypha.connect_to_server
-
-async def patched_connect_to_server(*args, **kwargs):
- server = await _connect_to_server(*args, **kwargs)
- _register_service = server.register_service
- async def patched_register_service(*args, **kwargs):
- svc_info = await _register_service(*args, **kwargs)
- service_id = svc_info['id'].split(':')[1]
- service_url = f"{server.config['public_base_url']}/{server.config['workspace']}/services/{service_id}"
- js.logService("service", service_url, svc_info)
- return svc_info
- server.register_service = patched_register_service
- server.registerService = patched_register_service
- return server
-
-imjoy_rpc.hypha.connect_to_server = patched_connect_to_server
-
-# For redirecting stdout and stderr later.
-class JSOutWriter(io.TextIOBase):
- def write(self, s):
- return js.write("stdout", s)
-
-class JSErrWriter(io.TextIOBase):
- def write(self, s):
- return js.write("stderr", s)
-
-def setup_matplotlib():
- import matplotlib
- matplotlib.use('Agg')
- import matplotlib.pyplot as plt
-
- def show():
- buf = io.BytesIO()
- plt.savefig(buf, format='png')
- img = 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('utf-8')
- js.show("img", img)
- plt.clf()
-
- plt.show = show
-
-def show_image(image, **attrs):
- from PIL import Image
- if not isinstance(image, Image.Image):
- image = Image.fromarray(image)
- buf = io.BytesIO()
- image.save(buf, format='png')
- data = 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('utf-8')
- js.show("img", data, attrs)
-
-_store = {}
-def store_put(key, value):
- _store[key] = value
- js.store_put(key, value)
-
-def store_get(key):
- return _store.get(key)
-
-def show_animation(frames, duration=100, format="apng", loop=0, **attrs):
- from PIL import Image
- buf = io.BytesIO()
- img, *imgs = [frame if isinstance(frame, Image.Image) else Image.fromarray(frame) for frame in frames]
- img.save(buf, format='png' if format == "apng" else format, save_all=True, append_images=imgs, duration=duration, loop=0)
- img = f'data:image/{format};base64,' + base64.b64encode(buf.getvalue()).decode('utf-8')
- js.show("img", img, attrs)
-
-def convert_audio(data):
- try:
- import numpy as np
- is_numpy = isinstance(data, np.ndarray)
- except ImportError:
- is_numpy = False
- if is_numpy:
- if len(data.shape) == 1:
- channels = 1
- if len(data.shape) == 2:
- channels = data.shape[0]
- data = data.T.ravel()
- else:
- raise ValueError("Too many dimensions (expected 1 or 2).")
- return ((data * (2**15 - 1)).astype("", "exec", ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
-
- result = eval(code, context)
- if result is not None:
- result = await result
- if last_expression:
- if isinstance(last_expression.value, ast.Await):
- # If last expression is an await, compile and execute it as async
- last_expr_code = compile(ast.Expression(last_expression.value), "", "eval", flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
- result = await eval(last_expr_code, context)
- else:
- # If last expression is not an await, compile and evaluate it normally
- last_expr_code = compile(ast.Expression(last_expression.value), "", "eval")
- result = eval(last_expr_code, context)
- if result is not None:
- print(result)
- for op in outputs:
- if op not in context:
- raise Exception("Error: The script did not produce an variable named: " + op)
- store_put(op, context[op])
- except:
- traceback.print_exc()
- raise
-`
-const mountedFs = {}
-
-self.onmessage = async (event) => {
- if(event.data.source){
- try{
- const { source, io_context } = event.data
- self.pyodide.globals.set("source", source)
- self.pyodide.globals.set("io_context", io_context && self.pyodide.toPy(io_context))
- outputs = []
- // see https://github.com/pyodide/pyodide/blob/b177dba277350751f1890279f5d1a9096a87ed13/src/js/api.ts#L546
- // sync native ==> browser
- await new Promise((resolve, _) => self.pyodide.FS.syncfs(true, resolve));
- await self.pyodide.runPythonAsync("await run(source, io_context)")
- // sync browser ==> native
- await new Promise((resolve, _) => self.pyodide.FS.syncfs(false, resolve)),
- console.log("Execution done", outputs)
- self.postMessage({ executionDone: true, outputs })
- outputs = []
- }
- catch(e){
- console.error("Execution Error", e)
- self.postMessage({ executionError: e.message })
- }
- }
- if(event.data.mount){
- try{
- const { mountPoint, dirHandle } = event.data.mount
- if(mountedFs[mountPoint]){
- console.log("Unmounting native FS:", mountPoint)
- await self.pyodide.FS.unmount(mountPoint)
- delete mountedFs[mountPoint]
- }
- const nativefs = await self.pyodide.mountNativeFS(mountPoint, dirHandle)
- mountedFs[mountPoint] = nativefs
- console.log("Native FS mounted:", mountPoint, nativefs)
- self.postMessage({ mounted: mountPoint })
- }
- catch(e){
- self.postMessage({ mountError: e.message })
- }
- }
-
-}
diff --git a/public/chat/worker-manager.js b/public/chat/worker-manager.js
deleted file mode 100644
index 85df5c7a..00000000
--- a/public/chat/worker-manager.js
+++ /dev/null
@@ -1,271 +0,0 @@
-class PyodideWorkerManager {
- hyphaServices = {}
- workers = {}
- workerApps = {}
- subscribers = []
- workerRecords = {}
- // native file system handle
- constructor(dirHandle, mountPoint) {
- this.workers = {}
- this.workerRecords = {}
- this.dirHandle = dirHandle
- this.mountPoint = mountPoint || "/mnt"
- }
-
- getDirHandle() {
- return this.dirHandle
- }
-
- // Subscribe method
- subscribe(callback) {
- this.subscribers.push(callback)
-
- // Return an unsubscribe function
- return () => {
- this.subscribers = this.subscribers.filter(sub => sub !== callback)
- }
- }
-
- // Call this method whenever the workers list changes
- notify() {
- this.subscribers.forEach(callback => callback())
- }
-
- getWorkerApps() {
- // return appInfo
- return Object.values(this.workerApps)
- }
-
- async createWorker(info) {
- const id = Math.random().toString(36).substring(7)
- console.log("Creating worker:", id)
- const worker = new Worker("/chat/pyodide-worker.js")
- await new Promise(resolve => (worker.onmessage = () => resolve()))
- this.workers[id] = worker
- this.workerRecords[id] = []
- this.hyphaServices[id] = []
- const self = this
- const appService = {
- id,
- appInfo: info,
- worker,
- async runScript(script, ioContext) {
- return await self.runScript(id, script, ioContext)
- },
- async run_script(script, io_context) {
- return await self.runScript(id, script, io_context)
- },
- async mount(mountPoint, dirHandle) {
- return await self.mountNativeFs(id, mountPoint, dirHandle)
- },
- async render(container) {
- self.render(id, container)
- },
- async renderSummary(container) {
- return self.renderSummary(id, container)
- },
- async close() {
- await self.closeWorker(id)
- },
- getLogs() {
- return self.workerRecords[id]
- },
- get_logs() {
- return self.workerRecords[id]
- },
- async listHyphaServices() {
- return self.hyphaServices[id]
- },
- async list_hypha_services() {
- return self.hyphaServices[id]
- }
- }
- this.workerApps[id] = appService
- if (this.dirHandle) {
- await this.mountNativeFs(id)
- }
- this.notify()
- return appService
- }
-
- async closeWorker(id) {
- if (this.workers[id]) {
- this.workers[id].terminate()
- delete this.workers[id]
- delete this.workerRecords[id]
- delete this.workerApps[id]
- this.notify()
- }
- }
-
- async getWorker(id) {
- if (id && this.workers[id]) {
- return this.workers[id]
- } else {
- throw new Error("No worker found with ID: " + id)
- }
- }
-
- async mountNativeFs(workerId, mountPoint, dirHandle) {
- if (!workerId) {
- throw new Error("No worker ID provided and no current worker available.")
- }
- const worker = await this.getWorker(workerId)
- return new Promise((resolve, reject) => {
- const handler = e => {
- if (e.data.mounted) {
- worker.removeEventListener("message", handler)
- resolve(true)
- } else if (e.data.mountError) {
- worker.removeEventListener("message", handler)
- reject(new Error(e.data.mountError))
- }
- }
- worker.addEventListener("message", handler)
- worker.postMessage({
- mount: {
- mountPoint: mountPoint || this.mountPoint,
- dirHandle: dirHandle || this.dirHandle
- }
- })
- })
- }
-
- addToRecord(workerId, record) {
- if (!this.workerRecords[workerId]) {
- this.workerRecords[workerId] = []
- }
- this.workerRecords[workerId].push(record)
- }
-
- renderOutputSummary(container, record) {
- // return a string preview of the output
- if (record.type === "store") {
- return `Store: ${record.key}`
- }
- else if (record.type === "script") {
- return `Script>>>:\n\`\`\`python\n${record.content}\n\`\`\`\n`
- } else if (record.type === "stdout") {
- if(record.content.trim() === "\n") {
- return "\n"
- }
- return `${record.content}\n`
- } else if (record.type === "stderr") {
- if(record.content.trim() === "\n") {
- return "\n"
- }
- return `${record.content}\n`
- } else if (record.type === "service") {
- return `Service: ${record.content}`
- } else if (record.type === "audio" || record.type === "img") {
- return `Image: `
- }
- }
-
- renderOutput(container, record) {
- if (record.type === "stdout" || record.type === "stderr") {
- if(record.content.trim() !== "\n" && record.content.trim() !== ""){
- const outputEl = document.createElement("pre")
- if (record.type === "stderr") {
- outputEl.style.color = "red"
- }
- outputEl.textContent = record.content
- container.appendChild(outputEl)
- }
- }
- else if (record.type === "store") {
- const storeEl = document.createElement("pre")
- storeEl.textContent = `Store: ${record.key}`
- container.appendChild(storeEl)
- }
- else if (record.type === "script") {
- const scriptEl = document.createElement("pre")
- scriptEl.textContent = `Script: ${record.content}`
- container.appendChild(scriptEl)
- } else if (record.type === "service") {
- // display service info
- const serviceEl = document.createElement("div")
- serviceEl.textContent = `Service: ${record.content}`
- container.appendChild(serviceEl)
- } else if (record.type === "audio" || record.type === "img") {
- const el = document.createElement(record.type)
- el.src = record.content
- if (record.attrs) {
- record.attrs.forEach(([attr, value]) => {
- el.setAttribute(attr, value)
- })
- }
- if (record.type === "audio") {
- el.controls = true
- }
- container.appendChild(el)
- }
- }
-
- async readStoreItem(workerId, key) {
- const records = this.workerRecords[workerId]
- return records.filter(record => record.type === "store" && (!key || record.key === key))[0]
- }
-
- async runScript(workerId, script, ioContext) {
- const outputContainer = ioContext && ioContext.output_container
- if(outputContainer) {
- delete ioContext.output_container
- }
- const worker = await this.getWorker(workerId)
- return new Promise((resolve, reject) => {
- worker.onerror = e => console.error(e)
- const outputs = []
- const handler = e => {
- if (e.data.type !== undefined) {
- if(!ioContext || !ioContext.skip_record)
- this.addToRecord(workerId, e.data)
- outputs.push(e.data)
- if (outputContainer) {
- this.renderOutput(outputContainer, e.data)
- }
- if (e.data.type === "service") {
- this.hyphaServices[workerId].push(e.data.attrs)
- }
- } else if (e.data.executionDone) {
- worker.removeEventListener("message", handler)
- resolve(outputs)
- } else if (e.data.executionError) {
- console.error("Execution Error", e.data.executionError)
- worker.removeEventListener("message", handler)
- reject(e.data.executionError)
- }
- }
- worker.addEventListener("message", handler)
- if(!ioContext || !ioContext.skip_record)
- this.addToRecord(workerId, { type: 'script', content: script });
- worker.postMessage({ source: script, io_context: ioContext })
- })
- }
-
- render(workerId, container) {
- const records = this.workerRecords[workerId]
- if (!records) {
- console.error("No records found for worker:", workerId)
- return
- }
- records.forEach(record => this.renderOutput(container, record))
- }
-
- renderSummary(workerId, container) {
- const records = this.workerRecords[workerId]
- if (!records) {
- console.error("No records found for worker:", workerId)
- return
- }
-
- let outputSummay = ""
- records.forEach(record => {
- const summary = this.renderOutputSummary(container, record)
- outputSummay += summary
- })
- return outputSummay
- }
-}
-
-window.PyodideWorkerManager = PyodideWorkerManager;
\ No newline at end of file