dash-ai-chatbot 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_ai_chatbot-1.0.0/LICENSE +21 -0
- dash_ai_chatbot-1.0.0/MANIFEST.in +2 -0
- dash_ai_chatbot-1.0.0/PKG-INFO +128 -0
- dash_ai_chatbot-1.0.0/README.md +99 -0
- dash_ai_chatbot-1.0.0/pyproject.toml +44 -0
- dash_ai_chatbot-1.0.0/setup.cfg +4 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot/__init__.py +70 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot/_core.py +144 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot/_hooks.py +25 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/PKG-INFO +128 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/SOURCES.txt +15 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/dependency_links.txt +1 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/entry_points.txt +2 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/requires.txt +1 -0
- dash_ai_chatbot-1.0.0/src/dash_ai_chatbot.egg-info/top_level.txt +1 -0
- dash_ai_chatbot-1.0.0/tests/test_core.py +119 -0
- dash_ai_chatbot-1.0.0/tests/test_dash_app.py +175 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asyntai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-ai-chatbot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Adds the Asyntai AI chat assistant to a Plotly Dash app. It answers visitor questions from your own content, in more than 80 languages.
|
|
5
|
+
Author-email: Asyntai <hello@asyntai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://asyntai.com
|
|
8
|
+
Project-URL: Documentation, https://asyntai.com/documentation/integrations/plotly-dash/
|
|
9
|
+
Project-URL: Source, https://github.com/asyntai/dash-ai-chatbot
|
|
10
|
+
Project-URL: Issues, https://github.com/asyntai/dash-ai-chatbot/issues
|
|
11
|
+
Keywords: dash,plotly,plotly-dash,chatbot,ai,ai-chatbot,live-chat,customer-support,assistant,widget
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Dash
|
|
15
|
+
Classifier: Framework :: Flask
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: dash>=2.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# dash-ai-chatbot
|
|
31
|
+
|
|
32
|
+
Adds the [Asyntai](https://asyntai.com) AI chat assistant to a
|
|
33
|
+
[Plotly Dash](https://dash.plotly.com) app. The assistant answers visitor
|
|
34
|
+
questions from your own content, day and night, in more than 80 languages.
|
|
35
|
+
|
|
36
|
+
Free and open source. Works on every Asyntai plan, including the free plan.
|
|
37
|
+
|
|
38
|
+
- Requires Dash 2.0 or newer and Python 3.10 or newer
|
|
39
|
+
- Documentation: https://asyntai.com/documentation/integrations/plotly-dash/
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install dash-ai-chatbot
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Add one line after you create the app:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from dash import Dash, html
|
|
51
|
+
import dash_ai_chatbot
|
|
52
|
+
|
|
53
|
+
app = Dash(__name__)
|
|
54
|
+
app.layout = html.Div("Hello")
|
|
55
|
+
|
|
56
|
+
dash_ai_chatbot.init_app(app, widget_id="asyntai_ab12cd34")
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
app.run()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You get the ID from the Install page of your
|
|
63
|
+
[Asyntai dashboard](https://asyntai.com/dashboard). You can paste the whole
|
|
64
|
+
snippet; the package keeps only the ID.
|
|
65
|
+
|
|
66
|
+
That is all. The chat button appears on every page of the app, including
|
|
67
|
+
multi-page apps.
|
|
68
|
+
|
|
69
|
+
## No code change (Dash 3 and later)
|
|
70
|
+
|
|
71
|
+
On Dash 3 and later the package also works with no code change. Install it and
|
|
72
|
+
set one environment variable:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
ASYNTAI_WIDGET_ID=asyntai_ab12cd34
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every Dash app that runs with that variable gets the chat. Without the
|
|
79
|
+
variable the package does nothing.
|
|
80
|
+
|
|
81
|
+
## Keep the ID out of the code
|
|
82
|
+
|
|
83
|
+
Leave out `widget_id` and the package reads `ASYNTAI_WIDGET_ID` each time the
|
|
84
|
+
page is built:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
dash_ai_chatbot.init_app(app)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
dash_ai_chatbot.init_app(
|
|
94
|
+
app,
|
|
95
|
+
widget_id="asyntai_ab12cd34", # or the whole snippet; empty or invalid = chat off
|
|
96
|
+
script_url=None, # change only if Asyntai support asks you to
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`script_url` can also come from the `ASYNTAI_SCRIPT_URL` environment variable.
|
|
101
|
+
Only an `https` address is accepted.
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
Dash serves one HTML page for the whole app. The package puts a small script
|
|
106
|
+
just before the closing body tag of that page. The script waits for the page to
|
|
107
|
+
finish loading, then fetches the chat widget from Asyntai, so your app draws
|
|
108
|
+
first and its speed is unchanged.
|
|
109
|
+
|
|
110
|
+
The widget ID and the script address travel to the browser on a data
|
|
111
|
+
attribute, never inside the script. The package works with a custom
|
|
112
|
+
`index_string`, with a subclass that overrides `interpolate_index`, and with
|
|
113
|
+
`url_base_pathname`. It never adds the widget twice.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
pip install -e . pytest
|
|
119
|
+
pytest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Support
|
|
123
|
+
|
|
124
|
+
hello@asyntai.com
|
|
125
|
+
|
|
126
|
+
## Licence
|
|
127
|
+
|
|
128
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# dash-ai-chatbot
|
|
2
|
+
|
|
3
|
+
Adds the [Asyntai](https://asyntai.com) AI chat assistant to a
|
|
4
|
+
[Plotly Dash](https://dash.plotly.com) app. The assistant answers visitor
|
|
5
|
+
questions from your own content, day and night, in more than 80 languages.
|
|
6
|
+
|
|
7
|
+
Free and open source. Works on every Asyntai plan, including the free plan.
|
|
8
|
+
|
|
9
|
+
- Requires Dash 2.0 or newer and Python 3.10 or newer
|
|
10
|
+
- Documentation: https://asyntai.com/documentation/integrations/plotly-dash/
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install dash-ai-chatbot
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add one line after you create the app:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from dash import Dash, html
|
|
22
|
+
import dash_ai_chatbot
|
|
23
|
+
|
|
24
|
+
app = Dash(__name__)
|
|
25
|
+
app.layout = html.Div("Hello")
|
|
26
|
+
|
|
27
|
+
dash_ai_chatbot.init_app(app, widget_id="asyntai_ab12cd34")
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
app.run()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You get the ID from the Install page of your
|
|
34
|
+
[Asyntai dashboard](https://asyntai.com/dashboard). You can paste the whole
|
|
35
|
+
snippet; the package keeps only the ID.
|
|
36
|
+
|
|
37
|
+
That is all. The chat button appears on every page of the app, including
|
|
38
|
+
multi-page apps.
|
|
39
|
+
|
|
40
|
+
## No code change (Dash 3 and later)
|
|
41
|
+
|
|
42
|
+
On Dash 3 and later the package also works with no code change. Install it and
|
|
43
|
+
set one environment variable:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
ASYNTAI_WIDGET_ID=asyntai_ab12cd34
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Every Dash app that runs with that variable gets the chat. Without the
|
|
50
|
+
variable the package does nothing.
|
|
51
|
+
|
|
52
|
+
## Keep the ID out of the code
|
|
53
|
+
|
|
54
|
+
Leave out `widget_id` and the package reads `ASYNTAI_WIDGET_ID` each time the
|
|
55
|
+
page is built:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
dash_ai_chatbot.init_app(app)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
dash_ai_chatbot.init_app(
|
|
65
|
+
app,
|
|
66
|
+
widget_id="asyntai_ab12cd34", # or the whole snippet; empty or invalid = chat off
|
|
67
|
+
script_url=None, # change only if Asyntai support asks you to
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`script_url` can also come from the `ASYNTAI_SCRIPT_URL` environment variable.
|
|
72
|
+
Only an `https` address is accepted.
|
|
73
|
+
|
|
74
|
+
## How it works
|
|
75
|
+
|
|
76
|
+
Dash serves one HTML page for the whole app. The package puts a small script
|
|
77
|
+
just before the closing body tag of that page. The script waits for the page to
|
|
78
|
+
finish loading, then fetches the chat widget from Asyntai, so your app draws
|
|
79
|
+
first and its speed is unchanged.
|
|
80
|
+
|
|
81
|
+
The widget ID and the script address travel to the browser on a data
|
|
82
|
+
attribute, never inside the script. The package works with a custom
|
|
83
|
+
`index_string`, with a subclass that overrides `interpolate_index`, and with
|
|
84
|
+
`url_base_pathname`. It never adds the widget twice.
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
pip install -e . pytest
|
|
90
|
+
pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Support
|
|
94
|
+
|
|
95
|
+
hello@asyntai.com
|
|
96
|
+
|
|
97
|
+
## Licence
|
|
98
|
+
|
|
99
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dash-ai-chatbot"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Adds the Asyntai AI chat assistant to a Plotly Dash app. It answers visitor questions from your own content, in more than 80 languages."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Asyntai", email = "hello@asyntai.com" }]
|
|
14
|
+
keywords = ["dash", "plotly", "plotly-dash", "chatbot", "ai", "ai-chatbot", "live-chat", "customer-support", "assistant", "widget"]
|
|
15
|
+
dependencies = ["dash>=2.0"]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
|
+
"Environment :: Web Environment",
|
|
20
|
+
"Framework :: Dash",
|
|
21
|
+
"Framework :: Flask",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://asyntai.com"
|
|
34
|
+
Documentation = "https://asyntai.com/documentation/integrations/plotly-dash/"
|
|
35
|
+
Source = "https://github.com/asyntai/dash-ai-chatbot"
|
|
36
|
+
Issues = "https://github.com/asyntai/dash-ai-chatbot/issues"
|
|
37
|
+
|
|
38
|
+
# Dash 3 and later load this module when an app starts. It does nothing
|
|
39
|
+
# unless ASYNTAI_WIDGET_ID is set.
|
|
40
|
+
[project.entry-points.dash_hooks]
|
|
41
|
+
dash_ai_chatbot = "dash_ai_chatbot._hooks"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Asyntai AI chat assistant for Plotly Dash apps.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from dash import Dash, html
|
|
6
|
+
import dash_ai_chatbot
|
|
7
|
+
|
|
8
|
+
app = Dash(__name__)
|
|
9
|
+
app.layout = html.Div('Hello')
|
|
10
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
|
|
11
|
+
|
|
12
|
+
Or, on Dash 3 and later, set ``ASYNTAI_WIDGET_ID`` in the environment and
|
|
13
|
+
change no code at all.
|
|
14
|
+
"""
|
|
15
|
+
from ._core import (
|
|
16
|
+
DEFAULT_SCRIPT_URL,
|
|
17
|
+
clean_script_url,
|
|
18
|
+
clean_widget_id,
|
|
19
|
+
insert_widget,
|
|
20
|
+
widget_snippet,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = '1.0.0'
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
'DEFAULT_SCRIPT_URL',
|
|
27
|
+
'clean_script_url',
|
|
28
|
+
'clean_widget_id',
|
|
29
|
+
'init_app',
|
|
30
|
+
'insert_widget',
|
|
31
|
+
'widget_snippet',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_FLAG = '_asyntai_widget_settings'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def init_app(app, widget_id=None, script_url=None):
|
|
38
|
+
"""Add the chat widget to every page of a Dash app.
|
|
39
|
+
|
|
40
|
+
``widget_id`` is the ID from the Install page of the Asyntai dashboard,
|
|
41
|
+
or the whole snippet copied from there. When it is left out, the
|
|
42
|
+
``ASYNTAI_WIDGET_ID`` environment variable is read each time the page is
|
|
43
|
+
built. ``script_url`` is only for a case where Asyntai support asks you
|
|
44
|
+
to change it.
|
|
45
|
+
|
|
46
|
+
Calling it twice on the same app only updates the values. The app is
|
|
47
|
+
returned, so the call can be chained.
|
|
48
|
+
"""
|
|
49
|
+
if not hasattr(app, 'interpolate_index'):
|
|
50
|
+
raise TypeError('init_app() needs a dash.Dash app.')
|
|
51
|
+
|
|
52
|
+
settings = {'widget_id': widget_id, 'script_url': script_url}
|
|
53
|
+
|
|
54
|
+
if getattr(app, _FLAG, None) is not None:
|
|
55
|
+
getattr(app, _FLAG).update(settings)
|
|
56
|
+
return app
|
|
57
|
+
|
|
58
|
+
setattr(app, _FLAG, settings)
|
|
59
|
+
original = app.interpolate_index
|
|
60
|
+
|
|
61
|
+
def interpolate_index(**kwargs):
|
|
62
|
+
page = original(**kwargs)
|
|
63
|
+
current = getattr(app, _FLAG)
|
|
64
|
+
return insert_widget(page, current['widget_id'], current['script_url'])
|
|
65
|
+
|
|
66
|
+
# Wrapping the instance method works on Dash 2, 3 and 4, and it also
|
|
67
|
+
# works when the app sets its own index_string or overrides
|
|
68
|
+
# interpolate_index in a subclass.
|
|
69
|
+
app.interpolate_index = interpolate_index
|
|
70
|
+
return app
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Everything the package decides about the widget lives here.
|
|
2
|
+
|
|
3
|
+
Dash serves one HTML page for the whole app, built by ``Dash.interpolate_index``
|
|
4
|
+
and then passed through the ``index`` hooks. The widget goes into that page
|
|
5
|
+
once, just before the closing body tag, so it is on every view of the app,
|
|
6
|
+
including multi-page apps built with ``dash.page_registry``.
|
|
7
|
+
"""
|
|
8
|
+
import html
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
DEFAULT_SCRIPT_URL = 'https://widget.asyntai.com/static/js/chat-widget.js'
|
|
13
|
+
|
|
14
|
+
#: Environment variables read when no value is passed in code.
|
|
15
|
+
ENV_WIDGET_ID = 'ASYNTAI_WIDGET_ID'
|
|
16
|
+
ENV_SCRIPT_URL = 'ASYNTAI_SCRIPT_URL'
|
|
17
|
+
|
|
18
|
+
#: Marker that shows the widget is already on the page.
|
|
19
|
+
MARKER = 'asyntai-widget-config'
|
|
20
|
+
|
|
21
|
+
#: Shape of a widget identifier from the Asyntai dashboard.
|
|
22
|
+
ID_PATTERN = re.compile(r'^asyntai_[A-Za-z0-9]{6,64}$')
|
|
23
|
+
|
|
24
|
+
#: The identifier inside a pasted snippet.
|
|
25
|
+
SNIPPET_PATTERN = re.compile(r'data-asyntai-id\s*=\s*["\']([^"\']+)["\']')
|
|
26
|
+
|
|
27
|
+
_HTTPS_PATTERN = re.compile(r'^https://[^\s"\'<>]+$', re.IGNORECASE)
|
|
28
|
+
|
|
29
|
+
BODY_CLOSE = '</body>'
|
|
30
|
+
|
|
31
|
+
_LOADER = """<div class="asyntai-widget-config" data-asyntai-widget="{widget_id}" data-asyntai-src="{script_url}" style="display: none"></div>
|
|
32
|
+
<script>
|
|
33
|
+
// Loads the Asyntai chat widget after the page has finished loading, so
|
|
34
|
+
// the Dash app draws first. The two values arrive on data attributes,
|
|
35
|
+
// because nothing is ever written into the script itself.
|
|
36
|
+
(function () {{
|
|
37
|
+
var config = document.querySelector('.asyntai-widget-config');
|
|
38
|
+
if (!config) {{ return; }}
|
|
39
|
+
|
|
40
|
+
var widgetId = config.getAttribute('data-asyntai-widget') || '';
|
|
41
|
+
var scriptUrl = config.getAttribute('data-asyntai-src') || '';
|
|
42
|
+
if (!widgetId || scriptUrl.indexOf('https://') !== 0) {{ return; }}
|
|
43
|
+
|
|
44
|
+
var load = function () {{
|
|
45
|
+
if (document.querySelector('script[data-asyntai-id]')) {{ return; }}
|
|
46
|
+
|
|
47
|
+
var script = document.createElement('script');
|
|
48
|
+
script.src = scriptUrl;
|
|
49
|
+
script.async = true;
|
|
50
|
+
script.setAttribute('data-asyntai-id', widgetId);
|
|
51
|
+
document.head.appendChild(script);
|
|
52
|
+
}};
|
|
53
|
+
|
|
54
|
+
if (document.readyState === 'complete') {{
|
|
55
|
+
load();
|
|
56
|
+
}} else {{
|
|
57
|
+
window.addEventListener('load', load);
|
|
58
|
+
}}
|
|
59
|
+
}}());
|
|
60
|
+
</script>
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clean_widget_id(raw):
|
|
65
|
+
"""Return a usable widget identifier, or an empty string.
|
|
66
|
+
|
|
67
|
+
Accepts a bare identifier or the whole snippet from the Asyntai
|
|
68
|
+
dashboard, because people paste the snippet. An empty string means the
|
|
69
|
+
chat stays off.
|
|
70
|
+
"""
|
|
71
|
+
if not isinstance(raw, str):
|
|
72
|
+
return ''
|
|
73
|
+
|
|
74
|
+
value = raw.strip()
|
|
75
|
+
if not value:
|
|
76
|
+
return ''
|
|
77
|
+
|
|
78
|
+
match = SNIPPET_PATTERN.search(value)
|
|
79
|
+
if match:
|
|
80
|
+
value = match.group(1).strip()
|
|
81
|
+
|
|
82
|
+
return value if ID_PATTERN.match(value) else ''
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def clean_script_url(raw):
|
|
86
|
+
"""Return the script address, or an empty string when the value is bad.
|
|
87
|
+
|
|
88
|
+
Only an https address is accepted, so a bad value can never put a
|
|
89
|
+
javascript: URL on the page. None or an empty value means the Asyntai
|
|
90
|
+
address.
|
|
91
|
+
"""
|
|
92
|
+
if raw is None:
|
|
93
|
+
return DEFAULT_SCRIPT_URL
|
|
94
|
+
if not isinstance(raw, str):
|
|
95
|
+
return ''
|
|
96
|
+
|
|
97
|
+
value = raw.strip()
|
|
98
|
+
if not value:
|
|
99
|
+
return DEFAULT_SCRIPT_URL
|
|
100
|
+
|
|
101
|
+
return value if _HTTPS_PATTERN.match(value) else ''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def widget_snippet(widget_id, script_url=None):
|
|
105
|
+
"""Return the HTML that loads the widget, or an empty string.
|
|
106
|
+
|
|
107
|
+
The empty string comes back when the identifier or the address is not
|
|
108
|
+
valid, so a typo leaves the app as it was instead of breaking it.
|
|
109
|
+
"""
|
|
110
|
+
clean_id = clean_widget_id(widget_id)
|
|
111
|
+
clean_url = clean_script_url(script_url)
|
|
112
|
+
if not clean_id or not clean_url:
|
|
113
|
+
return ''
|
|
114
|
+
|
|
115
|
+
return _LOADER.format(
|
|
116
|
+
widget_id=html.escape(clean_id, quote=True),
|
|
117
|
+
script_url=html.escape(clean_url, quote=True),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def insert_widget(page, widget_id=None, script_url=None):
|
|
122
|
+
"""Return the page with the widget added before the closing body tag.
|
|
123
|
+
|
|
124
|
+
Values passed in come first; missing ones are read from the environment
|
|
125
|
+
at the moment the page is built, so a deployment can change the ID
|
|
126
|
+
without a code change. The page is returned unchanged when there is no
|
|
127
|
+
valid ID, when it has no body tag, or when the widget is already on it.
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(page, str) or MARKER in page:
|
|
130
|
+
return page
|
|
131
|
+
|
|
132
|
+
if widget_id is None:
|
|
133
|
+
widget_id = os.environ.get(ENV_WIDGET_ID, '')
|
|
134
|
+
if script_url is None:
|
|
135
|
+
script_url = os.environ.get(ENV_SCRIPT_URL) or None
|
|
136
|
+
|
|
137
|
+
snippet = widget_snippet(widget_id, script_url)
|
|
138
|
+
if not snippet or BODY_CLOSE not in page:
|
|
139
|
+
return page
|
|
140
|
+
|
|
141
|
+
# Split on the last closing tag, so a page that prints the tag inside an
|
|
142
|
+
# example or a code block is left alone.
|
|
143
|
+
head, _, tail = page.rpartition(BODY_CLOSE)
|
|
144
|
+
return head + snippet + BODY_CLOSE + tail
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Zero-code mode for Dash 3 and later.
|
|
2
|
+
|
|
3
|
+
Dash loads every module named in a ``dash_hooks`` entry point when an app
|
|
4
|
+
starts. This one registers an ``index`` hook, so the widget appears on every
|
|
5
|
+
Dash app on the machine once ``ASYNTAI_WIDGET_ID`` is set, with no code
|
|
6
|
+
change. Without that variable the hook leaves every page as it was.
|
|
7
|
+
|
|
8
|
+
Older Dash versions have no hooks; there ``init_app`` does the same job.
|
|
9
|
+
"""
|
|
10
|
+
from ._core import insert_widget
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from dash import hooks
|
|
14
|
+
except ImportError: # Dash 2 has no hooks module.
|
|
15
|
+
hooks = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _add_widget(page):
|
|
19
|
+
# Only the environment is read here. An app that called init_app() has
|
|
20
|
+
# already got the widget, and insert_widget() sees the marker and stops.
|
|
21
|
+
return insert_widget(page)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if hooks is not None and hasattr(hooks, 'index'):
|
|
25
|
+
hooks.index()(_add_widget)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-ai-chatbot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Adds the Asyntai AI chat assistant to a Plotly Dash app. It answers visitor questions from your own content, in more than 80 languages.
|
|
5
|
+
Author-email: Asyntai <hello@asyntai.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://asyntai.com
|
|
8
|
+
Project-URL: Documentation, https://asyntai.com/documentation/integrations/plotly-dash/
|
|
9
|
+
Project-URL: Source, https://github.com/asyntai/dash-ai-chatbot
|
|
10
|
+
Project-URL: Issues, https://github.com/asyntai/dash-ai-chatbot/issues
|
|
11
|
+
Keywords: dash,plotly,plotly-dash,chatbot,ai,ai-chatbot,live-chat,customer-support,assistant,widget
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Dash
|
|
15
|
+
Classifier: Framework :: Flask
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: dash>=2.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# dash-ai-chatbot
|
|
31
|
+
|
|
32
|
+
Adds the [Asyntai](https://asyntai.com) AI chat assistant to a
|
|
33
|
+
[Plotly Dash](https://dash.plotly.com) app. The assistant answers visitor
|
|
34
|
+
questions from your own content, day and night, in more than 80 languages.
|
|
35
|
+
|
|
36
|
+
Free and open source. Works on every Asyntai plan, including the free plan.
|
|
37
|
+
|
|
38
|
+
- Requires Dash 2.0 or newer and Python 3.10 or newer
|
|
39
|
+
- Documentation: https://asyntai.com/documentation/integrations/plotly-dash/
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pip install dash-ai-chatbot
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Add one line after you create the app:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from dash import Dash, html
|
|
51
|
+
import dash_ai_chatbot
|
|
52
|
+
|
|
53
|
+
app = Dash(__name__)
|
|
54
|
+
app.layout = html.Div("Hello")
|
|
55
|
+
|
|
56
|
+
dash_ai_chatbot.init_app(app, widget_id="asyntai_ab12cd34")
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
app.run()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You get the ID from the Install page of your
|
|
63
|
+
[Asyntai dashboard](https://asyntai.com/dashboard). You can paste the whole
|
|
64
|
+
snippet; the package keeps only the ID.
|
|
65
|
+
|
|
66
|
+
That is all. The chat button appears on every page of the app, including
|
|
67
|
+
multi-page apps.
|
|
68
|
+
|
|
69
|
+
## No code change (Dash 3 and later)
|
|
70
|
+
|
|
71
|
+
On Dash 3 and later the package also works with no code change. Install it and
|
|
72
|
+
set one environment variable:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
ASYNTAI_WIDGET_ID=asyntai_ab12cd34
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Every Dash app that runs with that variable gets the chat. Without the
|
|
79
|
+
variable the package does nothing.
|
|
80
|
+
|
|
81
|
+
## Keep the ID out of the code
|
|
82
|
+
|
|
83
|
+
Leave out `widget_id` and the package reads `ASYNTAI_WIDGET_ID` each time the
|
|
84
|
+
page is built:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
dash_ai_chatbot.init_app(app)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
dash_ai_chatbot.init_app(
|
|
94
|
+
app,
|
|
95
|
+
widget_id="asyntai_ab12cd34", # or the whole snippet; empty or invalid = chat off
|
|
96
|
+
script_url=None, # change only if Asyntai support asks you to
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`script_url` can also come from the `ASYNTAI_SCRIPT_URL` environment variable.
|
|
101
|
+
Only an `https` address is accepted.
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
Dash serves one HTML page for the whole app. The package puts a small script
|
|
106
|
+
just before the closing body tag of that page. The script waits for the page to
|
|
107
|
+
finish loading, then fetches the chat widget from Asyntai, so your app draws
|
|
108
|
+
first and its speed is unchanged.
|
|
109
|
+
|
|
110
|
+
The widget ID and the script address travel to the browser on a data
|
|
111
|
+
attribute, never inside the script. The package works with a custom
|
|
112
|
+
`index_string`, with a subclass that overrides `interpolate_index`, and with
|
|
113
|
+
`url_base_pathname`. It never adds the widget twice.
|
|
114
|
+
|
|
115
|
+
## Tests
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
pip install -e . pytest
|
|
119
|
+
pytest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Support
|
|
123
|
+
|
|
124
|
+
hello@asyntai.com
|
|
125
|
+
|
|
126
|
+
## Licence
|
|
127
|
+
|
|
128
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/dash_ai_chatbot/__init__.py
|
|
6
|
+
src/dash_ai_chatbot/_core.py
|
|
7
|
+
src/dash_ai_chatbot/_hooks.py
|
|
8
|
+
src/dash_ai_chatbot.egg-info/PKG-INFO
|
|
9
|
+
src/dash_ai_chatbot.egg-info/SOURCES.txt
|
|
10
|
+
src/dash_ai_chatbot.egg-info/dependency_links.txt
|
|
11
|
+
src/dash_ai_chatbot.egg-info/entry_points.txt
|
|
12
|
+
src/dash_ai_chatbot.egg-info/requires.txt
|
|
13
|
+
src/dash_ai_chatbot.egg-info/top_level.txt
|
|
14
|
+
tests/test_core.py
|
|
15
|
+
tests/test_dash_app.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dash>=2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dash_ai_chatbot
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The rules for the two stored values and for placing the widget."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from dash_ai_chatbot import (
|
|
5
|
+
DEFAULT_SCRIPT_URL,
|
|
6
|
+
clean_script_url,
|
|
7
|
+
clean_widget_id,
|
|
8
|
+
insert_widget,
|
|
9
|
+
widget_snippet,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
PAGE = '<!DOCTYPE html><html><head></head><body><div id="react-entry-point"></div></body></html>'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def no_env(monkeypatch):
|
|
17
|
+
monkeypatch.delenv('ASYNTAI_WIDGET_ID', raising=False)
|
|
18
|
+
monkeypatch.delenv('ASYNTAI_SCRIPT_URL', raising=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize('raw, expected', [
|
|
22
|
+
('asyntai_ab12cd34', 'asyntai_ab12cd34'),
|
|
23
|
+
(' asyntai_ab12cd34 ', 'asyntai_ab12cd34'),
|
|
24
|
+
('<script async src="https://widget.asyntai.com/static/js/chat-widget.js" data-asyntai-id="asyntai_ab12cd34"></script>', 'asyntai_ab12cd34'),
|
|
25
|
+
("<script data-asyntai-id='asyntai_ab12cd34'></script>", 'asyntai_ab12cd34'),
|
|
26
|
+
('asyntai_' + 'a' * 64, 'asyntai_' + 'a' * 64),
|
|
27
|
+
])
|
|
28
|
+
def test_good_ids_are_kept(raw, expected):
|
|
29
|
+
assert clean_widget_id(raw) == expected
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize('raw', [
|
|
33
|
+
'', ' ', None, 123, ['asyntai_ab12cd34'],
|
|
34
|
+
'ab12cd34', 'asyntai_', 'asyntai_ab12', 'asyntai_' + 'a' * 65,
|
|
35
|
+
'asyntai_ab12cd34"><script>alert(1)</script>', 'asyntai_ab-12cd34',
|
|
36
|
+
])
|
|
37
|
+
def test_bad_ids_switch_the_chat_off(raw):
|
|
38
|
+
assert clean_widget_id(raw) == ''
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_script_url_defaults_to_asyntai():
|
|
42
|
+
assert clean_script_url(None) == DEFAULT_SCRIPT_URL
|
|
43
|
+
assert clean_script_url('') == DEFAULT_SCRIPT_URL
|
|
44
|
+
assert clean_script_url(' ') == DEFAULT_SCRIPT_URL
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_only_https_script_urls_are_accepted():
|
|
48
|
+
assert clean_script_url('https://cdn.example.com/w.js') == 'https://cdn.example.com/w.js'
|
|
49
|
+
for bad in ['http://cdn.example.com/w.js', 'javascript:alert(1)', '//cdn.example.com/w.js',
|
|
50
|
+
'https://a.com/"onload="x', 'https://a.com/w.js x', 42]:
|
|
51
|
+
assert clean_script_url(bad) == ''
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_snippet_carries_the_values_on_data_attributes():
|
|
55
|
+
snippet = widget_snippet('asyntai_ab12cd34')
|
|
56
|
+
assert 'data-asyntai-widget="asyntai_ab12cd34"' in snippet
|
|
57
|
+
assert 'data-asyntai-src="%s"' % DEFAULT_SCRIPT_URL in snippet
|
|
58
|
+
script = snippet[snippet.index('<script>'):]
|
|
59
|
+
assert 'asyntai_ab12cd34' not in script
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_snippet_is_empty_for_bad_values():
|
|
63
|
+
assert widget_snippet('nope') == ''
|
|
64
|
+
assert widget_snippet('asyntai_ab12cd34', 'http://x.com/w.js') == ''
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_snippet_escapes_the_script_address():
|
|
68
|
+
snippet = widget_snippet('asyntai_ab12cd34', 'https://cdn.example.com/w.js?a=1&b=2')
|
|
69
|
+
assert 'data-asyntai-src="https://cdn.example.com/w.js?a=1&b=2"' in snippet
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_widget_goes_just_before_the_last_body_tag():
|
|
73
|
+
page = PAGE.replace('<body>', '<body><pre></body> </body> in a sample</pre>')
|
|
74
|
+
result = insert_widget(page, 'asyntai_ab12cd34')
|
|
75
|
+
assert result.count('class="asyntai-widget-config"') == 1
|
|
76
|
+
assert result.endswith('</script>\n</body></html>')
|
|
77
|
+
assert result.index('in a sample') < result.index('asyntai-widget-config')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_page_is_unchanged_without_a_valid_id():
|
|
81
|
+
assert insert_widget(PAGE) == PAGE
|
|
82
|
+
assert insert_widget(PAGE, 'bad') == PAGE
|
|
83
|
+
assert insert_widget(PAGE, '') == PAGE
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_page_without_body_tag_is_unchanged():
|
|
87
|
+
page = '<div>partial</div>'
|
|
88
|
+
assert insert_widget(page, 'asyntai_ab12cd34') == page
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_widget_is_never_added_twice():
|
|
92
|
+
once = insert_widget(PAGE, 'asyntai_ab12cd34')
|
|
93
|
+
assert insert_widget(once, 'asyntai_zz99yy88') == once
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_non_string_page_is_returned_as_is():
|
|
97
|
+
assert insert_widget(None, 'asyntai_ab12cd34') is None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_environment_is_used_when_no_id_is_passed(monkeypatch):
|
|
101
|
+
monkeypatch.setenv('ASYNTAI_WIDGET_ID', 'asyntai_env12345')
|
|
102
|
+
assert 'data-asyntai-widget="asyntai_env12345"' in insert_widget(PAGE)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_passed_id_beats_the_environment(monkeypatch):
|
|
106
|
+
monkeypatch.setenv('ASYNTAI_WIDGET_ID', 'asyntai_env12345')
|
|
107
|
+
result = insert_widget(PAGE, 'asyntai_code1234')
|
|
108
|
+
assert 'asyntai_code1234' in result
|
|
109
|
+
assert 'asyntai_env12345' not in result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_script_address_can_come_from_the_environment(monkeypatch):
|
|
113
|
+
monkeypatch.setenv('ASYNTAI_SCRIPT_URL', 'https://cdn.example.com/w.js')
|
|
114
|
+
assert 'data-asyntai-src="https://cdn.example.com/w.js"' in insert_widget(PAGE, 'asyntai_ab12cd34')
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_bad_script_address_in_the_environment_keeps_the_chat_off(monkeypatch):
|
|
118
|
+
monkeypatch.setenv('ASYNTAI_SCRIPT_URL', 'http://cdn.example.com/w.js')
|
|
119
|
+
assert insert_widget(PAGE, 'asyntai_ab12cd34') == PAGE
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""The widget on the page a real Dash app serves."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import dash
|
|
8
|
+
import pytest
|
|
9
|
+
from dash import Dash, html
|
|
10
|
+
|
|
11
|
+
import dash_ai_chatbot
|
|
12
|
+
|
|
13
|
+
WIDGET = 'class="asyntai-widget-config"'
|
|
14
|
+
HAS_HOOKS = hasattr(dash, 'hooks') and hasattr(getattr(dash, 'hooks'), 'index')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def no_env(monkeypatch):
|
|
19
|
+
monkeypatch.delenv('ASYNTAI_WIDGET_ID', raising=False)
|
|
20
|
+
monkeypatch.delenv('ASYNTAI_SCRIPT_URL', raising=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_app(**kwargs):
|
|
24
|
+
app = Dash(__name__, **kwargs)
|
|
25
|
+
app.layout = html.Div('Hello from Dash', id='hello')
|
|
26
|
+
return app
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def page(app, path='/'):
|
|
30
|
+
response = app.server.test_client().get(path)
|
|
31
|
+
assert response.status_code == 200
|
|
32
|
+
return response.get_data(as_text=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_init_app_puts_the_widget_on_the_page():
|
|
36
|
+
app = make_app()
|
|
37
|
+
assert dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34') is app
|
|
38
|
+
|
|
39
|
+
body = page(app)
|
|
40
|
+
assert body.count(WIDGET) == 1
|
|
41
|
+
assert 'data-asyntai-widget="asyntai_ab12cd34"' in body
|
|
42
|
+
# Dash's own parts are still there, so the app still starts.
|
|
43
|
+
assert 'id="react-entry-point"' in body
|
|
44
|
+
assert body.index(WIDGET) > body.index('id="react-entry-point"')
|
|
45
|
+
assert body.rstrip().endswith('</html>')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_a_pasted_snippet_works():
|
|
49
|
+
app = make_app()
|
|
50
|
+
dash_ai_chatbot.init_app(app, widget_id='<script async src="x" data-asyntai-id="asyntai_ab12cd34"></script>')
|
|
51
|
+
assert 'data-asyntai-widget="asyntai_ab12cd34"' in page(app)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_a_bad_id_leaves_the_app_as_it_was():
|
|
55
|
+
# Dash 4 puts a fresh token on every page, so two loads never match
|
|
56
|
+
# byte for byte. Check that nothing of ours is on the page instead.
|
|
57
|
+
app = make_app()
|
|
58
|
+
dash_ai_chatbot.init_app(app, widget_id='not-an-id')
|
|
59
|
+
body = page(app)
|
|
60
|
+
assert 'asyntai' not in body.lower()
|
|
61
|
+
assert 'id="react-entry-point"' in body
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_calling_init_app_twice_only_updates_the_id():
|
|
65
|
+
app = make_app()
|
|
66
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_first123')
|
|
67
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_second12')
|
|
68
|
+
body = page(app)
|
|
69
|
+
assert body.count(WIDGET) == 1
|
|
70
|
+
assert 'asyntai_second12' in body
|
|
71
|
+
assert 'asyntai_first123' not in body
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_init_app_reads_the_environment_on_each_page_build(monkeypatch):
|
|
75
|
+
app = make_app()
|
|
76
|
+
dash_ai_chatbot.init_app(app)
|
|
77
|
+
assert WIDGET not in page(app)
|
|
78
|
+
|
|
79
|
+
monkeypatch.setenv('ASYNTAI_WIDGET_ID', 'asyntai_env12345')
|
|
80
|
+
assert 'data-asyntai-widget="asyntai_env12345"' in page(app)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_init_app_rejects_something_that_is_not_an_app():
|
|
84
|
+
with pytest.raises(TypeError):
|
|
85
|
+
dash_ai_chatbot.init_app(object(), widget_id='asyntai_ab12cd34')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_custom_index_string():
|
|
89
|
+
app = make_app()
|
|
90
|
+
app.index_string = """<!DOCTYPE html>
|
|
91
|
+
<html>
|
|
92
|
+
<head>{%metas%}<title>Custom</title>{%favicon%}{%css%}</head>
|
|
93
|
+
<body>
|
|
94
|
+
<header>My header</header>
|
|
95
|
+
{%app_entry%}
|
|
96
|
+
<footer>{%config%}{%scripts%}{%renderer%}</footer>
|
|
97
|
+
</body>
|
|
98
|
+
</html>"""
|
|
99
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
|
|
100
|
+
body = page(app)
|
|
101
|
+
assert 'My header' in body
|
|
102
|
+
assert body.count(WIDGET) == 1
|
|
103
|
+
assert body.index('</footer>') < body.index(WIDGET)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_subclass_that_overrides_interpolate_index():
|
|
107
|
+
class MyDash(Dash):
|
|
108
|
+
def interpolate_index(self, **kwargs):
|
|
109
|
+
return (
|
|
110
|
+
'<!DOCTYPE html><html><head>{metas}{title}{favicon}{css}</head>'
|
|
111
|
+
'<body><p>Subclass page</p>{app_entry}{config}{scripts}{renderer}</body></html>'
|
|
112
|
+
).format(**kwargs)
|
|
113
|
+
|
|
114
|
+
app = MyDash(__name__)
|
|
115
|
+
app.layout = html.Div('x')
|
|
116
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
|
|
117
|
+
body = page(app)
|
|
118
|
+
assert 'Subclass page' in body
|
|
119
|
+
assert body.count(WIDGET) == 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_url_base_pathname():
|
|
123
|
+
app = make_app(url_base_pathname='/reports/')
|
|
124
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
|
|
125
|
+
assert WIDGET in page(app, '/reports/')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_two_apps_keep_their_own_ids():
|
|
129
|
+
one, two = make_app(), make_app()
|
|
130
|
+
dash_ai_chatbot.init_app(one, widget_id='asyntai_appone12')
|
|
131
|
+
dash_ai_chatbot.init_app(two, widget_id='asyntai_apptwo12')
|
|
132
|
+
assert 'asyntai_appone12' in page(one) and 'asyntai_apptwo12' not in page(one)
|
|
133
|
+
assert 'asyntai_apptwo12' in page(two) and 'asyntai_appone12' not in page(two)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.skipif(not HAS_HOOKS, reason='this Dash version has no hooks')
|
|
137
|
+
def test_no_code_mode_needs_the_environment_variable(monkeypatch):
|
|
138
|
+
app = make_app()
|
|
139
|
+
assert WIDGET not in page(app)
|
|
140
|
+
|
|
141
|
+
monkeypatch.setenv('ASYNTAI_WIDGET_ID', 'asyntai_env12345')
|
|
142
|
+
body = page(make_app())
|
|
143
|
+
assert body.count(WIDGET) == 1
|
|
144
|
+
assert 'data-asyntai-widget="asyntai_env12345"' in body
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.skipif(not HAS_HOOKS, reason='this Dash version has no hooks')
|
|
148
|
+
def test_init_app_and_no_code_mode_never_add_two_widgets(monkeypatch):
|
|
149
|
+
monkeypatch.setenv('ASYNTAI_WIDGET_ID', 'asyntai_env12345')
|
|
150
|
+
app = make_app()
|
|
151
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_code1234')
|
|
152
|
+
body = page(app)
|
|
153
|
+
assert body.count(WIDGET) == 1
|
|
154
|
+
assert 'asyntai_code1234' in body
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_the_script_on_the_page_parses():
|
|
158
|
+
node = shutil.which('node')
|
|
159
|
+
if not node:
|
|
160
|
+
pytest.skip('node is not installed')
|
|
161
|
+
|
|
162
|
+
app = make_app()
|
|
163
|
+
dash_ai_chatbot.init_app(app, widget_id='asyntai_ab12cd34')
|
|
164
|
+
body = page(app)
|
|
165
|
+
start = body.index('<script>', body.index(WIDGET)) + len('<script>')
|
|
166
|
+
source = body[start:body.index('</script>', start)]
|
|
167
|
+
|
|
168
|
+
handle, path = tempfile.mkstemp(suffix='.js')
|
|
169
|
+
with os.fdopen(handle, 'w', encoding='utf-8') as fh:
|
|
170
|
+
fh.write(source)
|
|
171
|
+
try:
|
|
172
|
+
result = subprocess.run([node, '--check', path], capture_output=True, text=True)
|
|
173
|
+
finally:
|
|
174
|
+
os.unlink(path)
|
|
175
|
+
assert result.returncode == 0, result.stderr
|