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.

- -
- - -
-
-
- -
-
-
- BioImage.IO Icon -

Initializing BioImage.IO Chatbot...

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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