+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+ dataStore.base_url = `${server.config.public_base_url}/${server.config.workspace}/apps/${storeSvc.id.split(':')[1]}`;
+ console.log("Store service registered, you can access it via:", `${dataStore.base_url}/list`)
+ allAssistants = svc.assistants;
+ const assistant = allAssistants[assistantName];
+ if(assistant.code_interpreter){
+ enableCodeInterpreter = true;
+ }
+ if(enableCodeInterpreter){
+ setupCodeInterpreter();
+ }
+ if(Object.keys(allAssistants).length > 1){
+ const assistantButtons = document.getElementById('assistant-buttons');
+ assistantButtons.innerHTML = Object.values(allAssistants).map((a)=>{
+ //
BioImage Seeker (Melman)
+ return `
${a.alias}(${a.name}) `
+ }).join("\n")
+ }
+ else{
+ $('#assistant-menu-button').hide();
+ }
+ if (!assistant) {
+ throw new Error(`Assistant ${assistantName} not found`);
+ }
+ assistantIcon = assistant.icon || assistantIcon;
+ document.getElementById('assistant-menu-button').innerHTML = `${assistant.alias} (${assistant.name})`;
+ const builtinExtensions = assistant.extensions;
+ // clear extension except registered from ImJoy
+ extensions = [];
+ for(let ext of registeredExtensions){
+ extensions.push(ext)
+ }
+ builtinExtensions.forEach((ext) => {
+ extensions.push(ext)
+ });
+ for(let ext of extension_services){
+ try{
+ console.log("Getting extension service:", ext)
+ const extSvc = await server.getService(ext)
+ console.log("Got extension service:", ext, extSvc)
+ extSvc._rintf = true
+ extSvc.id = extSvc.id.split(":")[1]
+ await _registerExtension(extSvc)
+ console.log(`Registered extension: ${extSvc.name} (${extSvc.description})`, extSvc)
+ }
+ catch(e){
+ console.error("Failed to connect to extension service:", ext, e)
+ alert(`Failed to connect to extension service: ${ext}, error: ${e}`)
+ }
+ }
+ updateDropdownOptions(extensions);
+ showReadyStatus();
+ appendRobotMessage(allAssistants[assistantName]['welcome_message'], "message-0"); // Append robot message to the message container
+ return svc;
+ }
+ catch (e) {
+ // If connection fails, show an error message in the status
+ showErrorStatus(`Failed to connect to the server. ${e}`);
+ alert(`Failed to connect to BioImage.IO Chatbot server. ${e}`)
+ throw e;
+ }
+ finally {
+ $('.spinner').remove();
+ }
+ }
+ // Reset the chat session and clear chat history
+ function resetChat() {
+ sessionId = generateSessionID(); // Generate a new session ID
+ envPrompt = "";
+ chat_history.length = 0; // Clear the chat history
+ code = ''; // Reset code
+ error_message = ''; // Reset error message
+ $('.message-holder').empty(); // Clear the messages
+ initializeService();
+ }
+
+ $('#assistant-buttons li > a').click(function() {
+ const assistantName = $(this).data('assistant');
+ const url = new URL(document.location.href);
+ url.searchParams.set('assistant', assistantName);
+ document.location.href = url.toString();
+ });
+
+ $('#reset-btn').click(function () {
+ resetChat(); // Call the reset function when the "Reset" button is clicked
+ });
+
+ // Load user profile from local storage
+ const savedUserProfile = JSON.parse(localStorage.getItem('userProfile'));
+ if (savedUserProfile) {
+ $('#userNameInput').val(savedUserProfile.name);
+ $('#userOccupationInput').val(savedUserProfile.occupation);
+ $('#userBackgroundInput').val(savedUserProfile.background);
+ }
+
+ // Save button click event to save the user profile to local storage
+ $('#save-profile-btn').click(function () {
+ const userName = $('#userNameInput').val();
+ const userOccupation = $('#userOccupationInput').val();
+ const userBackground = $('#userBackgroundInput').val();
+
+ // Create a user_profile object with name, occupation, and background
+ const user_profile = {
+ name: userName,
+ occupation: userOccupation,
+ background: userBackground
+ };
+
+ // Save the user profile to local storage
+ localStorage.setItem('userProfile', JSON.stringify(user_profile));
+
+ // Collapse the profile options after saving
+ $('#profile-options').collapse('hide');
+ });
+ var code;
+ var error_message;
+ // Add this event listener to automatically resize the textarea based on its content
+ var textarea = document.getElementById('textMessageArea');
+ textarea.addEventListener('input', autoResize, false);
+ autoResize.call(textarea);
+
+ var renderer = new marked.Renderer();
+ marked.setOptions({
+ gfm: true,
+ tables: true,
+ breaks: true,
+ pedantic: false,
+ smartLists: true,
+ smartypants: false
+ });
+
+ renderer.link = function (href, title, text) {
+ return '
' + text + ' ';
+ }
+
+ function autoResize() {
+ this.style.height = (this.scrollHeight) + 'px';
+ }
+ //svc = await initializeService();
+
+ const chat_history = [];
+
+ let sessionId = generateSessionID();
+ console.log("Session ID:", sessionId);
+
+
+ async function sendMessage(e) {
+ e.preventDefault();
+
+ // Get the text message
+ const message = $('.message').val();
+
+ // Get the selected file
+ const fileInput = document.getElementById('fileUpload');
+ if(fileInput){
+ const selectedFile = fileInput.files[0];
+
+ // Check if a file is selected
+ if (selectedFile) {
+ // Read the selected file as a data URL (base64)
+ const reader = new FileReader();
+ reader.onloadend = function () {
+ const imageData = reader.result; // Extract base64 data
+ sendChatMessage(message, imageData);
+ removeFileSelected();
+ };
+ reader.readAsDataURL(selectedFile);
+
+ } else {
+ // No file selected, send only text message
+ sendChatMessage(message, null);
+ fileInput.value = ''; // Clear the file input
+ }
+
+ }
+ else{
+ sendChatMessage(message, null);
+ }
+
+ // Clear the input fields
+ $('.message').val('').focus();
+
+ }
+
+ function completeCodeBlocks(markdownText) {
+ const codeBlockIndicator = '```';
+
+ // Replace "```" in JSON strings with "```"
+ markdownText = markdownText.replace(/```/g, '```');
+
+ // Split text by code block indicator
+ const parts = markdownText.split(codeBlockIndicator);
+
+ // If there's an odd number of parts, it means a code block is not closed
+ if (parts.length > 1 && parts.length % 2 !== 0) {
+ // Append a closing code block indicator
+ markdownText += `\n${codeBlockIndicator}`;
+ }
+
+ return markdownText;
+ }
+
+
+ async function sendChatMessage(textMessage, imageData) {
+ const userName = $('#userNameInput').val();
+ const userOccupation = $('#userOccupationInput').val();
+ const userBackground = $('#userBackgroundInput').val();
+ const selectedChannel = $('#channelSelect').val();
+
+ // Create a user_profile object with name, occupation, and background
+ let user_profile = {
+ name: userName,
+ occupation: userOccupation,
+ background: userBackground
+ };
+
+ appendUserMessage(textMessage);
+
+ // Show 'Thinking...' status while waiting for the server's response
+ showThinkingStatus();
+
+ currentMessageId = "message-" + (chat_history.length+2)
+ appendRobotMessage("", currentMessageId); // Append robot message to the message container
+ $(`#spinner-${currentMessageId}`).show();
+
+ let accumulatedArgs = ""
+ function statusCallback(message) {
+ if (message.type === 'function_call') {
+ if (message.status === 'in_progress') {
+ accumulatedArgs += message.arguments
+ }
+ else {
+ accumulatedArgs = message.arguments
+ }
+ const args = accumulatedArgs.replace(/\\n/g, '\n');
+
+ content = // `
${message.name} `+
+ "## Generating response for " + message.name + "...\n\n" +
+ args;
+ // + " ";
+ $(`#content-${currentMessageId}`).html(marked(completeCodeBlocks(content), { renderer: renderer }));
+ }
+ else if (message.type === 'text') {
+ if (message.status === 'in_progress') {
+ accumulatedArgs += message.content
+ }
+ else{
+ accumulatedArgs = message.content
+ }
+ $(`#content-${currentMessageId}`).html(marked(completeCodeBlocks(accumulatedArgs), { renderer: renderer }));
+ }
+ }
+ try {
+ if(envPrompt && envPrompt.length > 0){
+ textMessage = envPrompt + textMessage
+ }
+ // Call the chat function with text and image data
+ response = await svc.chat(textMessage, chat_history, user_profile, statusCallback, sessionId, getSelectedExtensions(), assistantName);
+ // const regex = /!\[.*?\]\(data:.+?\)/g;
+ // const replacementText = '`image placeholder`';
+ // responseWithoutImage = response.replace(regex, replacementText);
+ console.log(response)
+ chat_history.push({ role: 'user', content: textMessage });
+ chat_history.push({ role: 'assistant', content: response.text });
+ showReadyStatus();
+
+ }
+ catch (e) {
+ // generate an error message to simulate how Melman from Madagascar would respond to an error
+ response = {text: "Oh no! I'm sorry, I don't know how to answer that. Please try again."};
+ showErrorStatus(`The server failed to respond, please try again. ${e}`);
+ console.error(e);
+ }
+ finally {
+ // Remove spinner and set status back to 'Ready to chat' after finishing
+ $('.spinner').remove();
+ $(`#spinner-${currentMessageId}`).hide();
+ // Convert the message to HTML using the marked library
+ const steps = response.steps;
+ let message = response.text && marked(response.text, { renderer: renderer }) || "";
+ if(steps && steps.length > 0){
+ let details = "
🔍More Details \n\n"
+ for(let step of steps){
+ details = details + `${step.name}
\n\n\`\`\`\n\n${JSON.stringify(step.details, null, 2)}\n\n\`\`\`\n\n`
+ }
+ details = details + "\n\n "
+ details = marked(details, { renderer: renderer })
+ message = message + details
+ }
+ $(`#content-${currentMessageId}`).html(message);
+ }
+ }
+
+ function appendUserMessage(message) {
+ let messageContainer = `
`;
+ $('.message-holder').append(messageContainer);
+ }
+
+ function appendRobotMessage(htmlMessage, messageId) {
+ const iconUrl = assistantIcon;
+ const messageContainer = `
+
+
+
+
+
🤔Thinking...
+
${htmlMessage}
+
+
+
+
+
+
+
`;
+ $('.message-holder').append(messageContainer);
+ }
+ // Function to update the status text
+ function updateStatus(status) {
+ $('#status-text').text(status);
+ }
+ // Function to show the status as 'Connecting to server...'
+ function showConnectingStatus() {
+ updateStatus('Connecting to server...');
+ }
+ // Function to show the status as 'Thinking...'
+ function showThinkingStatus() {
+ updateStatus('🤔Thinking...');
+ }
+
+ // Function to show the status as 'Ready to chat'
+ function showReadyStatus() {
+ updateStatus('Ready to chat! Type your message and press enter!');
+ }
+
+ // Function to show the error message in the status
+ function showErrorStatus(errorMessage) {
+ updateStatus('Error: ' + errorMessage);
+ }
+
+ // Function to generate session id
+ function generateSessionID() {
+ // Create a timestamp to ensure uniqueness
+ const timestamp = new Date().getTime();
+
+ // Generate a random number to add randomness
+ const random = Math.random();
+
+ // Combine timestamp and random number to create the session ID
+ const sessionID = `${timestamp}-${random}`;
+
+ return sessionID;
+ }
+
+ // Call the function to generate a session ID
+
+ async function showFeedbackWindow() {
+ // Show a prompt to collect user feedback
+ const feedbackMessage = prompt('Please share your thoughts about this response, thank you!', '');
+
+ if (feedbackMessage !== null) {
+ const feedbackType = $(this).hasClass('like-button') ? 'like' : 'unlike';
+
+ // get the messageId
+ const messageId = $(this).parent().attr('id');
+ // remove message- from the messageId and convert to integer
+ const messageIndex = parseInt(messageId.replace('message-', ''));
+ // get the chat history until messageIndex
+ const chatMessages = chat_history.slice(0, messageIndex);
+ const feedbackData = {
+ type: feedbackType,
+ feedback: feedbackMessage,
+ messages: chatMessages,
+ session_id: sessionId
+ };
+
+ // Call the 'svc.report()' function to send the feedback data
+ try {
+ await svc.report(feedbackData);
+ }
+ catch (e) {
+ console.error(e);
+ alert(`Failed to send feedback, error: ${e}`);
+ return;
+ }
+ // Hide the unclicked button
+ const otherButton = $(this).hasClass('like-button')
+ ? $('.unlike-button', $(this).parent())
+ : $('.like-button', $(this).parent());
+ otherButton.hide();
+
+ // Disable the clicked button
+ $(this).prop('disabled', true).off('click');
+ }
+ }
+
+ // Attach a click event handler to the 'like' and 'unlike' buttons
+ $('.message-holder').on('click', '.feedback-button', showFeedbackWindow);
+
+
+ $("#textMessageArea").on("keydown", function (event) {
+ if (event.key === "Enter") {
+ event.preventDefault(); // Prevent the default new line behavior
+ if (event.shiftKey) {
+ textarea.value += "\n"; // Add a new line when Shift is held
+ } else {
+ sendMessage(event); // Send the message when Enter is pressed
+ }
+ }
+ });
+
+ $('.send-btn').on('click', sendMessage);
+
+
+ $('#advanced-options-btn').click(function () {
+ $('#advanced-options').collapse('toggle');
+ });
+
+ // Add a click event handler for the "Feedback" button
+ $('#feedback-btn').click(function () {
+ // Show the feedback form
+ $('#feedback-form').collapse('toggle');
+
+ // Set the initial height of the textarea dynamically to show three lines
+ const lineHeight = 20; // You may need to adjust this based on your font size
+ $('#generalFeedback').css('height', (lineHeight * 5) + 'px');
+ });
+
+ // Add a click event handler for the "Save Feedback" button
+ $('#save-feedback-btn').click(async function () {
+ // Get the general feedback message from the textarea
+ const generalFeedback = $('#generalFeedback').val();
+
+ // Create a feedbackData object
+ const feedbackData = {
+ type: 'general', // Set the type to 'general feedback'
+ feedback: generalFeedback,
+ messages: chat_history, // Include chat history
+ session_id: sessionId
+ };
+
+ // Call the 'svc.report()' function to send the feedback data
+ try {
+ await svc.report(feedbackData);
+ }
+ catch (e) {
+ console.error(e);
+ alert(`Failed to send feedback, error: ${e}`);
+ return;
+ }
+
+ // Clear the input field
+ $('#generalFeedback').val('');
+
+ // Collapse the feedback form
+ $('#feedback-form').collapse('hide');
+
+ alert("Thank you for your feedback!");
+ });
+
+ });
+
+
+
-
\ No newline at end of file
+
diff --git a/public/chat/pyodide-worker.js b/public/chat/pyodide-worker.js
new file mode 100644
index 00000000..ec8b6646
--- /dev/null
+++ b/public/chat/pyodide-worker.js
@@ -0,0 +1,281 @@
+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 = []
+ await self.pyodide.runPythonAsync("await run(source, io_context)")
+ // synchronize the file system
+ for(const mountPoint of Object.keys(mountedFs)){
+ await mountedFs[mountPoint].syncfs()
+ }
+ 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.umountNativeFS(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
new file mode 100644
index 00000000..85df5c7a
--- /dev/null
+++ b/public/chat/worker-manager.js
@@ -0,0 +1,271 @@
+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
diff --git a/src/bioEngine.js b/src/bioEngine.js
index 4ceb6c05..c9bf4dda 100644
--- a/src/bioEngine.js
+++ b/src/bioEngine.js
@@ -197,7 +197,7 @@ export async function setupBioEngine() {
async callback() {
await api.createWindow({
src:
- "https://chat.bioimage.io/public/apps/bioimageio-chatbot-client/chat?disable-assistant-switch=true",
+ "https://bioimage.io/chat?disable-assistant-switch=true",
name: "BioImage.IO Chatbot"
});
await app.loadPlugin(