From c8a4e1a5ac3e99dcd7fcb70b8f2087c67bd6702b Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 9 Aug 2023 14:39:44 -0400 Subject: [PATCH 1/2] initial gpt dashboard plus backend to go with it --- .../communication_protocol/query.py | 5 +- .../package-lock.json | 36 +++++ modules/lo_dash_react_components/package.json | 1 + .../src/lib/components/LOPanelLayout.scss | 1 - modules/wo_bulk_essay_analysis/setup.cfg | 16 ++ modules/wo_bulk_essay_analysis/setup.py | 10 ++ .../wo_bulk_essay_analysis/assets/scripts.js | 142 ++++++++++++++++++ .../dashboard/layout.py | 119 +++++++++++++++ .../wo_bulk_essay_analysis/module.py | 106 +++++++++++++ .../writing_observer/writing_observer/gpt.py | 36 +++++ .../writing_observer/module.py | 11 +- 11 files changed, 479 insertions(+), 4 deletions(-) create mode 100644 modules/wo_bulk_essay_analysis/setup.cfg create mode 100644 modules/wo_bulk_essay_analysis/setup.py create mode 100644 modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/assets/scripts.js create mode 100644 modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py create mode 100644 modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/module.py create mode 100644 modules/writing_observer/writing_observer/gpt.py diff --git a/learning_observer/learning_observer/communication_protocol/query.py b/learning_observer/learning_observer/communication_protocol/query.py index 74594496..17798056 100644 --- a/learning_observer/learning_observer/communication_protocol/query.py +++ b/learning_observer/learning_observer/communication_protocol/query.py @@ -90,7 +90,7 @@ def join(LEFT, RIGHT, LEFT_ON=None, RIGHT_ON=None): } -def map(function, values, value_path=None, func_kwargs=None): +def map(function, values, value_path=None, func_kwargs=None, parallel=False): """ Map is used to run a function on the server, similar to `call`, over a list of values. @@ -100,7 +100,8 @@ def map(function, values, value_path=None, func_kwargs=None): "function_name": function.__lo_name__, "values": values, "value_path": value_path, - "func_kwargs": func_kwargs + "func_kwargs": func_kwargs, + "parallel": parallel } diff --git a/modules/lo_dash_react_components/package-lock.json b/modules/lo_dash_react_components/package-lock.json index 1b99cdf5..acc0c347 100644 --- a/modules/lo_dash_react_components/package-lock.json +++ b/modules/lo_dash_react_components/package-lock.json @@ -22,6 +22,7 @@ "react-router": "^6.8.2", "react-router-dom": "^6.8.2", "react-scripts": "^5.0.1", + "react-tooltip": "^5.20.0", "recharts": "^2.4.3", "web-vitals": "^2.1.4" }, @@ -1956,6 +1957,28 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", + "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "dependencies": { + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", + "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "dependencies": { + "@floating-ui/core": "^1.4.1", + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -24417,6 +24440,19 @@ "react-dom": ">=15.0.0" } }, + "node_modules/react-tooltip": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.20.0.tgz", + "integrity": "sha512-LWBIHEZjwDW9ZJ/Dn2xeZrsz+WKMii61CIsx2XPfs1IiIRnWyvKJXrgy6uEGOXYvrnCd4jiEvurn8Y+zJ1bw5Q==", + "dependencies": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/modules/lo_dash_react_components/package.json b/modules/lo_dash_react_components/package.json index 708d3eb4..43b4cbcd 100644 --- a/modules/lo_dash_react_components/package.json +++ b/modules/lo_dash_react_components/package.json @@ -47,6 +47,7 @@ "react-router": "^6.8.2", "react-router-dom": "^6.8.2", "react-scripts": "^5.0.1", + "react-tooltip": "^5.20.0", "recharts": "^2.4.3", "web-vitals": "^2.1.4" }, diff --git a/modules/lo_dash_react_components/src/lib/components/LOPanelLayout.scss b/modules/lo_dash_react_components/src/lib/components/LOPanelLayout.scss index 3f53fcde..4a8996a2 100644 --- a/modules/lo_dash_react_components/src/lib/components/LOPanelLayout.scss +++ b/modules/lo_dash_react_components/src/lib/components/LOPanelLayout.scss @@ -7,7 +7,6 @@ } .side-panel { - overflow: hidden; transition: all 0.3s ease-in-out; } } \ No newline at end of file diff --git a/modules/wo_bulk_essay_analysis/setup.cfg b/modules/wo_bulk_essay_analysis/setup.cfg new file mode 100644 index 00000000..ed1c2464 --- /dev/null +++ b/modules/wo_bulk_essay_analysis/setup.cfg @@ -0,0 +1,16 @@ +[metadata] +name = Writing Observer Automated Essay Feedback +description = Dashboard for interfacing a classroom of essays with automated feedback +url = https://github.com/ETS-Next-Gen/writing_observer +version = 0.1 + +[options] +packages = wo_bulk_essay_analysis +include_package_data = true + +[options.entry_points] +lo_modules = + wo_bulk_essay_analysis = wo_bulk_essay_analysis.module + +[options.package_data] +wo_bulk_essay_analysis = dashboard/* \ No newline at end of file diff --git a/modules/wo_bulk_essay_analysis/setup.py b/modules/wo_bulk_essay_analysis/setup.py new file mode 100644 index 00000000..31e74c9d --- /dev/null +++ b/modules/wo_bulk_essay_analysis/setup.py @@ -0,0 +1,10 @@ +''' +Rather minimalistic install script. To install, run `python +setup.py develop` or just install via requirements.txt +''' + +from setuptools import setup, find_packages + +setup( + name="wo_bulk_essay_analysis" +) diff --git a/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/assets/scripts.js b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/assets/scripts.js new file mode 100644 index 00000000..9353d7eb --- /dev/null +++ b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/assets/scripts.js @@ -0,0 +1,142 @@ +/** + * General scripts used for the bulk essay analysis dashboard + */ + +if (!window.dash_clientside) { + window.dash_clientside = {} +} + +const createStudentCard = function (student) { + const header = { + namespace: 'dash_bootstrap_components', + type: 'CardHeader', + props: { children: student.profile.name.full_name } + } + const studentText = { + namespace: 'lo_dash_react_components', + type: 'WOAnnotatedText', + props: { text: student.text, breakpoints: [] } + } + const feedback = { + namespace: 'dash_html_components', + type: 'Div', + props: { + children: student?.feedback ? student.feedback : '', + className: student?.feedback ? 'border-start p-1 overflow-auto' : '', + style: { whiteSpace: 'pre-line' } + } + } + const body = { + namespace: 'lo_dash_react_components', + type: 'LOPanelLayout', + props: { + children: studentText, + panels: [{ children: feedback, id: 'feedback-text', width: '40%' }], + shown: feedback.props.children.length > 0 ? ['feedback-text'] : [], + className: 'overflow-auto p-1' + } + } + const card = { + namespace: 'dash_bootstrap_components', + type: 'Card', + props: { + children: [header, body], + style: { maxHeight: '300px' } + } + } + return { + namespace: 'dash_bootstrap_components', + type: 'Col', + props: { + children: card, + id: student.user_id, + width: 4 + } + } +} + +const createChatCard = function (chat, user) { + const teacher = (user === 'teacher') + const card = { + namespace: 'dash_bootstrap_components', + type: 'Card', + props: { children: chat, body: true, color: teacher ? '#6cc3d540' : '#fff' } + } + return { + namespace: 'dash_bootstrap_components', + type: 'Col', + props: { + children: card, + align: teacher ? 'end' : 'start', + width: 9 + } + } +} + +window.dash_clientside.bulk_essay_feedback = { + send_to_loconnection: async function (state, hash, clicks, query) { + if (state === undefined) { + return window.dash_clientside.no_update + } + if (state.readyState === 1) { + if (hash.length === 0) { return window.dash_clientside.no_update } + const decoded = decode_string_dict(hash.slice(1)) + if (!decoded.course_id) { return window.dash_clientside.no_update } + + decoded.gpt_prompt = '' + decoded.message_id = '' + + const trig = window.dash_clientside.callback_context.triggered[0] + if (trig.prop_id.includes('bulk-essay-analysis-submit-btn')) { + decoded.gpt_prompt = query + } + + const message = { + wo: { + execution_dag: 'writing_observer', + target_exports: ['gpt_bulk'], + kwargs: decoded + } + } + return JSON.stringify(message) + } + return window.dash_clientside.no_update + }, + + update_ui_upon_query_submission: async function (clicks, text, children) { + if (clicks > 0) { + const loading = { + namespace: 'dash_bootstrap_components', + type: 'Col', + props: { + children: { + namespace: 'dash_bootstrap_components', + type: 'Spinner', + props: { color: 'primary' } + }, + align: 'start', + width: 8, + id: 'loading' + } + } + const newChildren = [loading, createChatCard(text, 'teacher')].concat(children) + return ['', newChildren] + } + }, + + update_student_grid: function (message) { + const cards = message.map((x) => { + return createStudentCard(x) + }) + return cards + }, + + add_response_to_chat: function (message, children) { + const chatCard = createChatCard('Your feedback will appear next to each student card.', 'gpt') + const index = children.findIndex(item => item.props.id === 'loading') + if (index > -1) { + children[index] = chatCard + } + return children + } +} diff --git a/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py new file mode 100644 index 00000000..e95acd8b --- /dev/null +++ b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py @@ -0,0 +1,119 @@ +''' +Define layout for dashboard that allows teachers to interface +student essays with LLMs. +''' +import dash_bootstrap_components as dbc +import lo_dash_react_components as lodrc + +from dash import html, dcc, callback, Patch, clientside_callback, ClientsideFunction, Output, Input, State + +prefix = 'bulk-essay-analysis' +websocket = f'{prefix}-websocket' +ws_store = f'{prefix}-ws-store' + +query_input = f'{prefix}-query-input' +submit = f'{prefix}-submit-btn' +chat = f'{prefix}-chat-panel' +grid = f'{prefix}-essay-grid' + +welcome_message = ['Hello, welcome to the bulk essay analysis.', 'Provide a prompt and we will pass it along with each text into ChatGPT.'] + + +def layout(): + ''' + Generic layout function to create dashboard + ''' + main = html.Div([ + html.H2('Bulk Essay Analysis'), + dbc.Row(id=grid, class_name='g-2') + ], className='vh-100', style={'overflowY': 'auto', 'overflowX': 'hidden'}) + starting_message = dbc.Col( + dbc.Card( + [html.P(message) for message in welcome_message], + body=True, + color='#fff' + ), + width=9, + align='start' + ) + chat_panel = dbc.Row([ + dbc.Col( + dbc.Row([starting_message], id=chat, class_name='h-100 flex-column-reverse flex-nowrap overflow-auto g-1'), + class_name='flex-grow-1 overflow-auto', + ), + dbc.Col( + dbc.InputGroup([ + dbc.Textarea(id=query_input, placeholder='Type request here...', autofocus=True, value=''), + dbc.Button('Submit', id=submit, n_clicks=0, disabled=True), + ], class_name='p-1'), + width=12, + align='end' + ), + ], class_name='vh-100 flex-column') + cont = dbc.Container([ + lodrc.LOPanelLayout( + main, + panels=[{'children': chat_panel, 'width': '25%', 'id': 'chat'}], + shown=['chat'] + ), + lodrc.LOConnection(id=websocket), + dcc.Store(id=ws_store, data={}) + ], fluid=True) + return dcc.Loading(cont) + + +# send request to LOConnection +clientside_callback( + ClientsideFunction(namespace='bulk_essay_feedback', function_name='send_to_loconnection'), + Output(websocket, 'send'), + Input(websocket, 'state'), # used for initial setup + Input('_pages_location', 'hash'), + Input(submit, 'n_clicks'), + State(query_input, 'value'), +) + +clientside_callback( + '''function (value) { + if (value.length === 0) { return true } + return false + } + ''', + Output(submit, 'disabled'), + Input(query_input, 'value') +) + +clientside_callback( + ClientsideFunction(namespace='bulk_essay_feedback', function_name='update_ui_upon_query_submission'), + Output(query_input, 'value'), + Output(chat, 'children'), + Input(submit, 'n_clicks'), + State(query_input, 'value'), + State(chat, 'children') +) + +# store message from LOConnection in storage for later use +clientside_callback( + ''' + function(message) { + const data = JSON.parse(message.data).wo.gpt_bulk + console.log(data) + return data + } + ''', + Output(ws_store, 'data'), + Input(websocket, 'message') +) + +clientside_callback( + ClientsideFunction(namespace='bulk_essay_feedback', function_name='update_student_grid'), + Output(grid, 'children'), + Input(ws_store, 'data') +) + +clientside_callback( + ClientsideFunction(namespace='bulk_essay_feedback', function_name='add_response_to_chat'), + Output(chat, 'children', allow_duplicate=True), + Input(ws_store, 'data'), + State(chat, 'children'), + prevent_initial_call=True +) diff --git a/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/module.py b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/module.py new file mode 100644 index 00000000..96bb1f4e --- /dev/null +++ b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/module.py @@ -0,0 +1,106 @@ +import os.path + +import dash_bootstrap_components as dbc + +from learning_observer.dash_integration import thirdparty_url, static_url + +import wo_bulk_essay_analysis.dashboard.layout + + +NAME = "Writing Observer - Bulk Essay Analysis and Feedback Dashboard" + +DASH_PAGES = [ + { + "MODULE": wo_bulk_essay_analysis.dashboard.layout, + "LAYOUT": wo_bulk_essay_analysis.dashboard.layout.layout, + "ASSETS": 'assets', + "TITLE": "Bulk Essay Analysis and Feedback Dashboard", + "DESCRIPTION": "The Bulk Essay Analysis and Feedback Dashboard is a robust educational tool that leverages AI to simultaneously analyze and provide feedback on large batches of essays, delivering comprehensive insights and constructive critiques for educators in diverse group settings.", + "SUBPATH": "bulk-essay-analysis", + "CSS": [ + thirdparty_url("css/bootstrap.min.css"), + thirdparty_url("css/fontawesome_all.css") + ], + "SCRIPTS": [ + static_url("liblo.js") + ] + } +] + +THIRD_PARTY = { + "css/bootstrap.min.css": { + "url": dbc.themes.MINTY, + "hash": { + "old": "b361dc857ee7c817afa9c3370f1d317db2c4be5572dd5ec3171caeb812281" + "cf900a5a9141e5d6c7069408e2615df612fbcd31094223996154e16f2f80a348532", + "5.1.3": "c03f5bfd8deb11ad6cec84a6201f4327f28a640e693e56466fd80d983ed54" + "16deff1548a0f6bbad013ec278b9750d1d253bd9c5bd1f53c85fcd62adba5eedc59" + } + }, + "css/fontawesome_all.css": { + "url": dbc.icons.FONT_AWESOME, + "hash": { + "6.1.1": "535a5f3e40bc8ddf475b56c1a39a5406052b524413dea331c4e683ca99e39" + "6dbbc11fdce1f8355730a73c52ac6a1062de1938406c6af8e4361fd346106acb6b0", + "6.3.0": "1496214e7421773324f4b332127ea77bec822fc6739292ebb19c6abcc22a5" + "6248e0634b4e0ca0c2fcac14dc10b8d01fa17febaa35f46731201d1ffd0ab482dd7" + } + }, + "webfonts/fa-solid-900.woff2": { + "url": os.path.dirname(os.path.dirname(dbc.icons.FONT_AWESOME)) + "/webfonts/fa-solid-900.woff2", + "hash": { + "6.1.1": "6d3fe769cc40a5790ea2e09fb775f1bd3b130d2fdae1dd552f69559e7ca4c" + "a047862f795da0024737e59e3bcc7446f6eec1bab173758aef0b97ba89d722ffbde", + "6.3.0": "d50c68cd4b3312f50deb66ac8ab5c37b2d4161f4e00ea077" + "326ae76769dac650dd19e65dee8d698ba2f86a69537f38cf4010ff45227211cee8b382d9b567257a" + } + }, + "webfonts/fa-solid-900.ttf": { + "url": os.path.dirname(os.path.dirname(dbc.icons.FONT_AWESOME)) + "/webfonts/fa-solid-900.ttf", + "hash": { + "6.1.1": "0fdd341671021d04304186c197001cf2e888d3028baaf9a5dec0f0e496959" + "666e8a2e34aae8e79904f8e9b4c0ccae40249897cce5f5ae58d12cc1b3985e588d6", + "6.3.0": "5a2c2b010a2496e4ed832ede8620f3bbfa9374778f3d63e4" + "5a4aab041e174dafd9fffd3229b8b36f259cf2ef46ae7bf5cb041e280f2939884652788fc1e8ce58" + } + } +} + +# As of today, our goal isn't to have consistent versions installed, +# so much as to verify hashes to block man-in-the-middle +# attacks. We're keeping versions which on different systems here. +# +# We may (and should) remove deprecated versions in the future, but we +# do expect to continue to work with more than one version. +# +# A better design would map version URLs to sha hashes, under +# DRY. That can be done once we either kill "old" above or figure out +# what URL that came from. At that point, we can replace Minty_URLs +# with THIRD_PARTY["css/bootstrap.min.css"]["hash"] + +Minty_URLs = [ + 'https://cdn.jsdelivr.net/npm/bootswatch@5.1.3/dist/minty/bootstrap.min.css', +] + +if (dbc.themes.MINTY not in Minty_URLs): + print("WARN:: Unrecognized Minty URL detected: {}".format(dbc.themes.MINTY)) + print("You will need to update dash bootstrap components hash value.\n") + +FontAwesome_URLs = [ + "https://use.fontawesome.com/releases/v6.3.0/css/all.css", + "https://use.fontawesome.com/releases/v6.1.1/css/all.css" +] + +if (dbc.icons.FONT_AWESOME not in FontAwesome_URLs): + print("WARN:: Unrecognized Fontawesome URL detected: {}".format(dbc.icons.FONT_AWESOME)) + print("You will need to update the FontAwesome bootstrap components hash value.\n") + + +COURSE_DASHBOARDS = [{ + 'name': NAME, + 'url': "/wo_bulk_essay_analysis/dash/bulk-essay-analysis", + "icon": { + "type": "fas", + "icon": "fa-pen-nib" + } +}] diff --git a/modules/writing_observer/writing_observer/gpt.py b/modules/writing_observer/writing_observer/gpt.py new file mode 100644 index 00000000..713c2f59 --- /dev/null +++ b/modules/writing_observer/writing_observer/gpt.py @@ -0,0 +1,36 @@ +import openai + +import learning_observer.communication_protocol.integration + + +model = 'gpt-3.5-turbo-16k' +template = """{question}\n\n{text}""" + + +@learning_observer.communication_protocol.integration.publish_function('writing_observer.gpt_essay_prompt') +async def process_student_essay(text, prompt): + ''' + This method processes text with a prompt through GPT. + + We use a closure to allow the system to connect to the memoization KVS. + ''' + + @learning_observer.cache.async_memoization() + async def gpt(gpt_prompt): + completion = openai.ChatCompletion.create( + model=model, + messages=[{"role": "user", "content": gpt_prompt}] + ) + return completion["choices"][0]["message"]["content"] + + if len(prompt) == 0: + output = { + 'text': text, + 'feedback': '' + } + else: + output = { + 'text': text, + 'feedback': await gpt(template.format(question=prompt, text=text)) + } + return output diff --git a/modules/writing_observer/writing_observer/module.py b/modules/writing_observer/writing_observer/module.py index c505ef11..fa4c7da5 100644 --- a/modules/writing_observer/writing_observer/module.py +++ b/modules/writing_observer/writing_observer/module.py @@ -15,6 +15,7 @@ import writing_observer.aggregator import writing_observer.writing_analysis import writing_observer.languagetool +import writing_observer.gpt from writing_observer.nlp_indicators import INDICATOR_JSONS @@ -28,6 +29,7 @@ determine_activity = q.call('writing_observer.activity_map') languagetool = q.call('writing_observer.languagetool') update_via_google = q.call('writing_observer.update_reconstruct_with_google_api') +gpt_bulk_essay = q.call('writing_observer.gpt_essay_prompt') EXECUTION_DAG = { @@ -48,7 +50,9 @@ 'single_student_lt': languagetool(texts=q.variable('single_student_doc')), 'single_lt_combined': q.join(LEFT=q.variable('single_student_lt'), LEFT_ON='providence.providence.STUDENT.value.user_id', RIGHT=q.variable('roster'), RIGHT_ON='user_id'), 'overall_lt': languagetool(texts=q.variable('docs')), - 'lt_combined': q.join(LEFT=q.variable('overall_lt'), LEFT_ON='providence.providence.STUDENT.value.user_id', RIGHT=q.variable('roster'), RIGHT_ON='user_id') + 'lt_combined': q.join(LEFT=q.variable('overall_lt'), LEFT_ON='providence.providence.STUDENT.value.user_id', RIGHT=q.variable('roster'), RIGHT_ON='user_id'), + 'gpt_map': q.map(gpt_bulk_essay, values=q.variable('docs'), value_path='text', func_kwargs={'prompt': q.parameter('gpt_prompt')}), + 'gpt_bulk': q.join(LEFT=q.variable('gpt_map'), LEFT_ON='providence.value.providence.providence.STUDENT.value.user_id', RIGHT=q.variable('roster'), RIGHT_ON='user_id') }, "exports": { "docs_with_roster": { @@ -75,6 +79,11 @@ 'returns': 'lt_combined', 'parameters': ['course_id'], 'output': '' + }, + 'gpt_bulk': { + 'returns': 'gpt_bulk', + 'parameters': ['course_id', 'gpt_prompt'], + 'output': '' } }, } From 230568491ee877458829d1579a9757c30fac786b Mon Sep 17 00:00:00 2001 From: Bradley Erickson Date: Wed, 9 Aug 2023 18:52:23 -0400 Subject: [PATCH 2/2] removed console statement --- .../wo_bulk_essay_analysis/dashboard/layout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py index e95acd8b..c62cee45 100644 --- a/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py +++ b/modules/wo_bulk_essay_analysis/wo_bulk_essay_analysis/dashboard/layout.py @@ -96,7 +96,6 @@ def layout(): ''' function(message) { const data = JSON.parse(message.data).wo.gpt_bulk - console.log(data) return data } ''',