Skip to content

Commit 6c7700f

Browse files
committed
Initial commit
1 parent 877e68f commit 6c7700f

22 files changed

+15628
-0
lines changed

Diff for: .gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/node_modules
2+
/package-lock.json
3+
/js/highlightjs/core*
4+
/js/highlightjs/languages

Diff for: README.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Chatty GPT is a simple alternative to ChatGPT
2+
3+
It works by using the OpenAI chat completions API (currently using the model: gpt-3.5-turbo).
4+
You will need to have (paid) access to this API.
5+
6+
All chats are kept in localStorage, so you won't be able to access it from any other browser or device.
7+
I plan to make a simple export and import feature though.
8+
9+
## Why?
10+
11+
This is meant as a learning experiment.
12+
I wanted to try out the OpenAI API and at the same time, I wanted to see how easy it would be, to write an interface in plain Web Components with Tailwind CSS.
13+
I wanted the development to be as simple as possible, preferably without any build steps and definitely NO npm packages.
14+
So just plain ol' load a file into your browser and start coding.
15+
The structure is currently a bit messy, as I have been trying out different things.
16+
17+
This is the result!
18+
It runs entirely from GitHub pages, in-browser, with no external calls, except to the OpenAI API.
19+
20+
Unfortunately, TailwindCSS requires a build step, unless you want to load the entire CSS lib into your browser.
21+
So I accepted that I have to run a small tailwindcss CLI, that watches the code and rebuilds the CSS file on changes.
22+
23+
And to use imports, everything needs to be served as a proper website - I.e.: http://, not file:/// .
24+
So I have to run a small webserver (npx http-server) in this directory, when developing.
25+
26+
At least these are FAST compared to using anything like Webpack, Vite, etc.
27+
And there is no build step - it's just a couple of small dev services.
28+
29+
## Conclusions (so far)
30+
31+
While it's possible to avoid frameworks, just by using Web Components, you don't need many external dependencies, before your life is imply easier with something like [Vite](https://vitejs.dev).
32+
Vite will help you keep all external dependencies local (served by you) and up to date.
33+
34+
Using Web Components alone, will only work for modern browsers, so you might as well go Framework shopping, if you need to support any browser, that is no longer supported by their creators (e.g. Internet Explorer, older Chrome, Safari, etc.).
35+
36+
## TODO
37+
- [ ] When something happens in the chat view, that concerns e.g. the left menu, it should dispatch an event
38+
- [ ] Index should call update() on left-menu, when there's relevant events from e.g. chat-view
39+
- [ ] Add an abort controller to the OpenAI calls
40+
- [ ] Should "Return" send the message?
41+
- [ ] Minus icon is missing, when expanding "Advanced settings" in "Create a new chat"
42+
- [ ] Settings for the chat (including the name)
43+
- [ ] It should be possible to delete a chat
44+
- [ ] Make sure that changes to chat settings are used immediately after
45+
- [ ] Add notifications for success and error actions
46+
- [ ] (Re-)generate streaming response
47+
- [ ] Icon or button, to re-generate last response
48+
- [ ] Export all data from localStorage (expect key and id)
49+
- [ ] Import all data to localStorage (expect key and id)
50+
- [ ] Come up with an indicator for when the stream is done (or working - like the ... indicator that openai uses)
51+
- [ ] Scroll when appropriate
52+
- [ ] Scroll to bottom on load
53+
- [ ] Scroll to bottom on stream (when already at bottom - check before inserting delta)
54+
- [ ] Scroll to bottom, when inserting new message
55+
- [ ] Add header on code examples, should contain language and a copy button/link (:has doesn't work in firefox...)
56+
57+
## TODO later / maybe
58+
- [ ] Add some template system messages (e.g. frontend developer, Go developer, marketing expert, ...)
59+
60+
## UI / UX TODO
61+
- [ ] Dismiss modals with [ESC]
62+
- [ ] Settings: Reflect that data has been cleared, right after clearing
63+

Diff for: components/chatty-icon.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
class ChattyIcon extends HTMLElement {
2+
static get observedAttributes () {
3+
return ['name', 'class']
4+
}
5+
6+
constructor () {
7+
super()
8+
}
9+
10+
#render () {
11+
let svgPaths = ''
12+
13+
switch (this.getAttribute('name')) {
14+
case 'chat-bubble-left-right':
15+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />`
16+
break
17+
18+
case 'cog-6-tooth':
19+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
20+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />`
21+
break
22+
23+
case 'eye':
24+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
25+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />`
26+
break
27+
28+
case 'eye-slash':
29+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />`
30+
break
31+
32+
case 'github':
33+
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
34+
${this.getAttribute('class') ? `class="${this.getAttribute('class')}" ` : ''}
35+
fill="currentColor" stroke="currentColor" viewbox="0 0 98 96">
36+
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" />
37+
</svg>`
38+
return
39+
40+
case 'information-circle':
41+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />`
42+
break
43+
44+
case 'minus':
45+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M18 12H6" />`
46+
break
47+
48+
case 'openai':
49+
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
50+
${this.getAttribute('class') ? `class="${this.getAttribute('class')}" ` : ''}
51+
fill="currentColor" stroke="currentColor" viewBox="140 140 520 520">
52+
<path d="m617.24 354a126.36 126.36 0 0 0 -10.86-103.79 127.8 127.8 0 0 0 -137.65-61.32 126.36 126.36 0 0 0 -95.31-42.49 127.81 127.81 0 0 0 -121.92 88.49 126.4 126.4 0 0 0 -84.5 61.3 127.82 127.82 0 0 0 15.72 149.86 126.36 126.36 0 0 0 10.86 103.79 127.81 127.81 0 0 0 137.65 61.32 126.36 126.36 0 0 0 95.31 42.49 127.81 127.81 0 0 0 121.96-88.54 126.4 126.4 0 0 0 84.5-61.3 127.82 127.82 0 0 0 -15.76-149.81zm-190.66 266.49a94.79 94.79 0 0 1 -60.85-22c.77-.42 2.12-1.16 3-1.7l101-58.34a16.42 16.42 0 0 0 8.3-14.37v-142.39l42.69 24.65a1.52 1.52 0 0 1 .83 1.17v117.92a95.18 95.18 0 0 1 -94.97 95.06zm-204.24-87.23a94.74 94.74 0 0 1 -11.34-63.7c.75.45 2.06 1.25 3 1.79l101 58.34a16.44 16.44 0 0 0 16.59 0l123.31-71.2v49.3a1.53 1.53 0 0 1 -.61 1.31l-102.1 58.95a95.16 95.16 0 0 1 -129.85-34.79zm-26.57-220.49a94.71 94.71 0 0 1 49.48-41.68c0 .87-.05 2.41-.05 3.48v116.68a16.41 16.41 0 0 0 8.29 14.36l123.31 71.19-42.69 24.65a1.53 1.53 0 0 1 -1.44.13l-102.11-59a95.16 95.16 0 0 1 -34.79-129.81zm350.74 81.62-123.31-71.2 42.69-24.64a1.53 1.53 0 0 1 1.44-.13l102.11 58.95a95.08 95.08 0 0 1 -14.69 171.55c0-.88 0-2.42 0-3.49v-116.68a16.4 16.4 0 0 0 -8.24-14.36zm42.49-63.95c-.75-.46-2.06-1.25-3-1.79l-101-58.34a16.46 16.46 0 0 0 -16.59 0l-123.31 71.2v-49.3a1.53 1.53 0 0 1 .61-1.31l102.1-58.9a95.07 95.07 0 0 1 141.19 98.44zm-267.11 87.87-42.7-24.65a1.52 1.52 0 0 1 -.83-1.17v-117.92a95.07 95.07 0 0 1 155.9-73c-.77.42-2.11 1.16-3 1.7l-101 58.34a16.41 16.41 0 0 0 -8.3 14.36zm23.19-50 54.92-31.72 54.92 31.7v63.42l-54.92 31.7-54.92-31.7z" />
53+
</svg>`
54+
return
55+
56+
case 'paper-airplane':
57+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />`
58+
break
59+
60+
case 'paper-airplane-solid':
61+
svgPaths = `<path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />`
62+
break
63+
64+
case 'plus':
65+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />`
66+
break
67+
68+
case 'plus-circle':
69+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />`
70+
break
71+
72+
case 'terminal':
73+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />`
74+
break
75+
76+
case 'trash':
77+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />`
78+
break
79+
80+
case 'user':
81+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />`
82+
break
83+
84+
85+
case 'user-circle':
86+
svgPaths = `<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />`
87+
break
88+
}
89+
90+
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg"
91+
${this.getAttribute('class') ? `class="${this.getAttribute('class')}" ` : ''}
92+
fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
93+
${svgPaths}
94+
</svg>`
95+
96+
97+
98+
}
99+
100+
connectedCallback () {
101+
this.#render()
102+
}
103+
104+
attributeChangedCallback (name, oldValue, newValue) {
105+
if (oldValue === newValue) {
106+
return
107+
}
108+
109+
if (this.name === 'name') {
110+
this.#render()
111+
return
112+
}
113+
}
114+
}
115+
116+
customElements.define('chatty-icon', ChattyIcon)

Diff for: components/chatty-menu-item.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class ChattyMenuItem extends HTMLElement {
2+
static get observedAttributes () {
3+
return ['href', 'target'];
4+
}
5+
6+
constructor () {
7+
super()
8+
9+
this.attachShadow({ mode: 'open' });
10+
11+
let href = this.getAttribute('href')
12+
let target = this.getAttribute('target')
13+
14+
this.shadowRoot.innerHTML = `
15+
<link href="css/global.min.css" rel="stylesheet">
16+
17+
<a${href ? ` href="${href}"` : ''}${target ? ' target="' + target + '"' : ''}
18+
class="cursor-pointer text-gray-300 hover:bg-gray-700 hover:text-white group flex items-center rounded-md px-2 py-2 text-sm font-medium">
19+
<slot>Menu item</slot>
20+
</a>
21+
`
22+
}
23+
}
24+
25+
customElements.define('chatty-menu-item', ChattyMenuItem)

Diff for: components/chatty-menu.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class ChattyMenu extends HTMLElement {
2+
constructor () {
3+
super()
4+
5+
this.attachShadow({ mode: 'open' });
6+
7+
this.shadowRoot.innerHTML = `
8+
<link href="css/global.min.css" rel="stylesheet">
9+
10+
<!-- Static sidebar for desktop -->
11+
<div class="fixed inset-y-0 flex w-64 flex-col">
12+
<div class="flex min-h-0 flex-1 flex-col bg-gray-800">
13+
<div class="flex flex-1 flex-col overflow-y-auto pt-5 pb-4">
14+
<div class="flex flex-shrink-0 items-center px-4 text-gray-500">
15+
ChattyGPT
16+
<!-- <img class="h-8 w-auto" src="..."
17+
alt="Your Company"> -->
18+
</div>
19+
20+
<nav class="mt-5 flex-1 space-y-1 px-2">
21+
<slot></slot>
22+
</nav>
23+
</div>
24+
25+
<div class="flex flex-shrink-0 bg-gray-700 p-4 flex-col">
26+
<slot name="bottom"></slot>
27+
</div>
28+
</div>
29+
</div>
30+
31+
`
32+
}
33+
}
34+
35+
customElements.define('chatty-menu', ChattyMenu)

Diff for: components/chatty-modal.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
class ChattyModal extends HTMLElement {
2+
static get observedAttributes () {
3+
return ['visible'];
4+
}
5+
6+
constructor () {
7+
super()
8+
9+
this.attachShadow({ mode: 'open' });
10+
11+
this.shadowRoot.innerHTML = `
12+
<link href="css/global.min.css" rel="stylesheet">
13+
14+
<div class="hidden relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
15+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
16+
17+
<div class="fixed inset-0 z-10 overflow-y-auto">
18+
<div id="chatty-overlay" class="flex min-h-full items-end justify-center p-4 sm:items-center sm:p-0">
19+
<slot></slot>
20+
</div>
21+
</div>
22+
</div>
23+
`
24+
}
25+
26+
connectedCallback () {
27+
this.shadowRoot.querySelector('#chatty-overlay').addEventListener('click', this.#hide.bind(this));
28+
}
29+
30+
disconnectedCallback () {
31+
this.shadowRoot.querySelector('#chatty-overlay').removeEventListener('click', this.#hide.bind(this));
32+
}
33+
34+
attributeChangedCallback (name, oldValue, newValue) {
35+
if (oldValue === newValue) {
36+
return;
37+
}
38+
39+
if (name === 'visible') {
40+
if (!newValue || newValue === 'false') {
41+
this.shadowRoot.querySelector('div').classList.add('hidden')
42+
} else {
43+
this.shadowRoot.querySelector('div').classList.remove('hidden')
44+
}
45+
}
46+
}
47+
48+
#hide (evt) {
49+
if (evt.target.id === 'chatty-overlay') {
50+
evt.preventDefault()
51+
evt.stopPropagation()
52+
this.setAttribute('visible', false)
53+
}
54+
}
55+
}
56+
57+
customElements.define('chatty-modal', ChattyModal)

Diff for: components/chatty-prompt.js

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
class ChattyPrompt extends HTMLElement {
2+
static get observedAttributes () {
3+
return ['visible'];
4+
}
5+
6+
constructor () {
7+
super()
8+
9+
this.attachShadow({ mode: 'open' });
10+
11+
this.shadowRoot.innerHTML = `
12+
<link href="css/global.min.css" rel="stylesheet">
13+
14+
<div id="chatty-prompt" class="hidden relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
15+
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
16+
17+
<div class="fixed inset-0 z-10 overflow-y-auto">
18+
<div id="chatty-overlay" class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
19+
<div class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
20+
<div class="sm:flex sm:items-start">
21+
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
22+
<slot name="icon">
23+
<chatty-icon name="trash" class="h-6 w-6 text-red-600"></chatty-icon>
24+
</slot>
25+
</div>
26+
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
27+
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
28+
<slot name="title">Title</slot>
29+
</h3>
30+
<div class="mt-2">
31+
<p class="text-sm text-gray-500">
32+
<slot name="description">Description.</slot>
33+
</p>
34+
</div>
35+
</div>
36+
</div>
37+
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
38+
<button id="ok-button" type="button" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto">
39+
<slot name="ok-label">Yep! Do it!</slot>
40+
</button>
41+
<button id="cancel-button" type="button" class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto">
42+
<slot name="cancel-label">Ooops! No thanks.</slot>
43+
</button>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
</div>
49+
`
50+
}
51+
52+
connectedCallback () {
53+
this.shadowRoot.querySelector('div').addEventListener('click', this.#hide.bind(this));
54+
this.shadowRoot.querySelector('#cancel-button').addEventListener('click', this.#hide.bind(this));
55+
this.shadowRoot.querySelector('#ok-button').addEventListener('click', (evt) => {
56+
this.dispatchEvent(new CustomEvent('ok', { composed: true, bubbles: true }))
57+
});
58+
}
59+
60+
disconnectedCallback () {
61+
this.shadowRoot.querySelector('div').removeEventListener('click', this.#hide.bind(this));
62+
}
63+
64+
attributeChangedCallback (name, oldValue, newValue) {
65+
if (oldValue === newValue) {
66+
return;
67+
}
68+
69+
if (name === 'visible') {
70+
if (!newValue || newValue === 'false') {
71+
this.shadowRoot.querySelector('div').classList.add('hidden')
72+
} else {
73+
this.shadowRoot.querySelector('div').classList.remove('hidden')
74+
}
75+
}
76+
}
77+
78+
#hide (evt) {
79+
if (evt.target.id === 'chatty-overlay' || evt.target.id === 'cancel-button' || evt.target.slot === 'cancel-label') {
80+
evt.preventDefault()
81+
evt.stopPropagation()
82+
this.setAttribute('visible', false)
83+
}
84+
}
85+
}
86+
87+
customElements.define('chatty-prompt', ChattyPrompt)

Diff for: css/global.css

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+

Diff for: css/global.min.css

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)