From 716510db06d26db09e6a046e644fbbf6208135da Mon Sep 17 00:00:00 2001 From: Doug Chestnut Date: Wed, 29 May 2024 14:29:09 -0400 Subject: [PATCH] added bot chatbox component from sprint --- packages/chat-box/.editorconfig | 29 ++++ packages/chat-box/.gitignore | 24 ++++ packages/chat-box/.husky/pre-commit | 1 + packages/chat-box/.storybook/main.js | 8 ++ packages/chat-box/LICENSE | 21 +++ packages/chat-box/README.md | 76 ++++++++++ packages/chat-box/demo/index.html | 30 ++++ packages/chat-box/package.json | 92 ++++++++++++ packages/chat-box/src/ChatBox.ts | 150 ++++++++++++++++++++ packages/chat-box/src/ChatInput.ts | 44 ++++++ packages/chat-box/src/ChatMessage.ts | 55 +++++++ packages/chat-box/src/chat-box.ts | 3 + packages/chat-box/src/chat-input.ts | 3 + packages/chat-box/src/chat-message.ts | 3 + packages/chat-box/src/index.ts | 1 + packages/chat-box/stories/index.stories.ts | 60 ++++++++ packages/chat-box/test/chat-box.test.ts | 32 +++++ packages/chat-box/tsconfig.json | 22 +++ packages/chat-box/web-dev-server.config.js | 27 ++++ packages/chat-box/web-test-runner.config.js | 41 ++++++ 20 files changed, 722 insertions(+) create mode 100644 packages/chat-box/.editorconfig create mode 100644 packages/chat-box/.gitignore create mode 100644 packages/chat-box/.husky/pre-commit create mode 100644 packages/chat-box/.storybook/main.js create mode 100644 packages/chat-box/LICENSE create mode 100644 packages/chat-box/README.md create mode 100644 packages/chat-box/demo/index.html create mode 100644 packages/chat-box/package.json create mode 100644 packages/chat-box/src/ChatBox.ts create mode 100644 packages/chat-box/src/ChatInput.ts create mode 100644 packages/chat-box/src/ChatMessage.ts create mode 100644 packages/chat-box/src/chat-box.ts create mode 100644 packages/chat-box/src/chat-input.ts create mode 100644 packages/chat-box/src/chat-message.ts create mode 100644 packages/chat-box/src/index.ts create mode 100644 packages/chat-box/stories/index.stories.ts create mode 100644 packages/chat-box/test/chat-box.test.ts create mode 100644 packages/chat-box/tsconfig.json create mode 100644 packages/chat-box/web-dev-server.config.js create mode 100644 packages/chat-box/web-test-runner.config.js diff --git a/packages/chat-box/.editorconfig b/packages/chat-box/.editorconfig new file mode 100644 index 000000000..c8c2d2aaf --- /dev/null +++ b/packages/chat-box/.editorconfig @@ -0,0 +1,29 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.json] +indent_size = 2 + +[*.{html,js,md}] +block_comment_start = /** +block_comment = * +block_comment_end = */ diff --git a/packages/chat-box/.gitignore b/packages/chat-box/.gitignore new file mode 100644 index 000000000..5c4d921fe --- /dev/null +++ b/packages/chat-box/.gitignore @@ -0,0 +1,24 @@ +## editors +/.idea +/.vscode + +## system files +.DS_Store + +## npm +/node_modules/ +/npm-debug.log + +## testing +/coverage/ + +## temp folders +/.tmp/ + +# build +/_site/ +/dist/ +/out-tsc/ + +storybook-static +custom-elements.json diff --git a/packages/chat-box/.husky/pre-commit b/packages/chat-box/.husky/pre-commit new file mode 100644 index 000000000..f568382f8 --- /dev/null +++ b/packages/chat-box/.husky/pre-commit @@ -0,0 +1 @@ +./node_modules/.bin/lint-staged \ No newline at end of file diff --git a/packages/chat-box/.storybook/main.js b/packages/chat-box/.storybook/main.js new file mode 100644 index 000000000..efe8a072e --- /dev/null +++ b/packages/chat-box/.storybook/main.js @@ -0,0 +1,8 @@ +const config = { + stories: ['../**/dist/stories/*.stories.{js,md,mdx}'], + framework: { + name: '@web/storybook-framework-web-components', + }, +}; + +export default config; \ No newline at end of file diff --git a/packages/chat-box/LICENSE b/packages/chat-box/LICENSE new file mode 100644 index 000000000..7ca8b85f1 --- /dev/null +++ b/packages/chat-box/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 chat-box + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/chat-box/README.md b/packages/chat-box/README.md new file mode 100644 index 000000000..c4a811660 --- /dev/null +++ b/packages/chat-box/README.md @@ -0,0 +1,76 @@ +# \ + +This webcomponent follows the [open-wc](https://github.com/open-wc/open-wc) recommendation. + +## Installation + +```bash +npm i chat-box +``` + +## Usage + +```html + + + +``` + +## Linting and formatting + +To scan the project for linting and formatting errors, run + +```bash +npm run lint +``` + +To automatically fix linting and formatting errors, run + +```bash +npm run format +``` + +## Testing with Web Test Runner + +To execute a single test run: + +```bash +npm run test +``` + +To run the tests in interactive watch mode run: + +```bash +npm run test:watch +``` + +## Demoing with Storybook + +To run a local instance of Storybook for your component, run + +```bash +npm run storybook +``` + +To build a production version of Storybook, run + +```bash +npm run storybook:build +``` + + +## Tooling configs + +For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project. + +If you customize the configuration a lot, you can consider moving them to individual files. + +## Local Demo with `web-dev-server` + +```bash +npm start +``` + +To run a local development server that serves the basic demo located in `demo/index.html` diff --git a/packages/chat-box/demo/index.html b/packages/chat-box/demo/index.html new file mode 100644 index 000000000..d4b306206 --- /dev/null +++ b/packages/chat-box/demo/index.html @@ -0,0 +1,30 @@ + + + + + + + + +
+ + + + diff --git a/packages/chat-box/package.json b/packages/chat-box/package.json new file mode 100644 index 000000000..42764a6e2 --- /dev/null +++ b/packages/chat-box/package.json @@ -0,0 +1,92 @@ +{ + "name": "chat-box", + "description": "Webcomponent chat-box following open-wc recommendations", + "license": "MIT", + "author": "chat-box", + "version": "0.0.0", + "type": "module", + "main": "dist/src/index.js", + "module": "dist/src/index.js", + "exports": { + ".": "./dist/src/index.js", + "./chat-box.js": "./dist/src/chat-box.js" + }, + "scripts": { + "analyze": "cem analyze --litelement", + "start": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"web-dev-server\"", + "build": "tsc && npm run analyze -- --exclude dist", + "prepublish": "tsc && npm run analyze -- --exclude dist", + "lint": "eslint --ext .ts,.html . --ignore-path .gitignore && prettier \"**/*.ts\" --check --ignore-path .gitignore", + "format": "eslint --ext .ts,.html . --fix --ignore-path .gitignore && prettier \"**/*.ts\" --write --ignore-path .gitignore", + "prepare": "husky", + "test": "tsc && wtr --coverage", + "test:watch": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch\"", + "storybook": "tsc && npm run analyze -- --exclude dist && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"storybook dev -p 8080\"", + "storybook:build": "tsc && npm run analyze -- --exclude dist && storybook build" + }, + "dependencies": { + "@material/web": "^1.4.1", + "firebase": "^10.12.0", + "lit": "^3.0.2" + }, + "devDependencies": { + "@custom-elements-manifest/analyzer": "^0.10.2", + "@open-wc/eslint-config": "^12.0.3", + "@open-wc/testing": "^4.0.0", + "@storybook/addon-a11y": "^7.5.3", + "@storybook/addon-essentials": "^7.5.3", + "@storybook/addon-links": "^7.5.3", + "@storybook/web-components": "^7.5.3", + "@types/mocha": "^10.0.6", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", + "@web/dev-server": "^0.4.5", + "@web/storybook-builder": "^0.1.16", + "@web/storybook-framework-web-components": "^0.1.2", + "@web/test-runner": "^0.18.2", + "concurrently": "^8.2.2", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "husky": "^9.0.11", + "lint-staged": "^15.2.5", + "prettier": "^3.2.5", + "storybook": "^7.5.3", + "tslib": "^2.6.2", + "typescript": "^5.4.5" + }, + "customElements": "custom-elements.json", + "eslintConfig": { + "parser": "@typescript-eslint/parser", + "extends": [ + "@open-wc", + "prettier" + ], + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error" + ], + "import/no-unresolved": "off", + "import/extensions": [ + "error", + "always", + { + "ignorePackages": true + } + ] + } + }, + "prettier": { + "singleQuote": true, + "arrowParens": "avoid" + }, + "lint-staged": { + "*.ts": [ + "eslint --fix", + "prettier --write" + ] + } +} \ No newline at end of file diff --git a/packages/chat-box/src/ChatBox.ts b/packages/chat-box/src/ChatBox.ts new file mode 100644 index 000000000..9e50eafaa --- /dev/null +++ b/packages/chat-box/src/ChatBox.ts @@ -0,0 +1,150 @@ +// web-src/ts/chat-box.ts +import { LitElement, html, css, property } from 'lit'; +import { initializeApp, FirebaseApp } from 'firebase/app'; +import { getDatabase, ref, push, onValue, remove, Database } from 'firebase/database'; +import { getAuth, onAuthStateChanged, Auth, User } from 'firebase/auth'; +import '@material/web/button/filled-button.js'; +import './chat-message.js'; +import './chat-input.js'; + +interface FirebaseConfig { + apiKey: string; + authDomain: string; + databaseURL: string; + projectId: string; + storageBucket: string; + messagingSenderId: string; + appId: string; +} + +interface Message { + sender: string; + senderName: string; + text: string; + timestamp: string; +} + +export class ChatBox extends LitElement { + @property({ type: Array }) + messages: Message[] = []; + + @property({ type: String }) + userId: string | null = null; + + @property({ type: String }) + path: string = 'chats'; // Default path if not provided + + @property({ type: Object }) + firebaseConfig: FirebaseConfig | null = null; + + private app: FirebaseApp | null = null; + private database: Database | null = null; + private auth: Auth | null = null; + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + if (this.firebaseConfig) { + this.initializeFirebase(this.firebaseConfig); + } + } + + private initializeFirebase(config: FirebaseConfig) { + this.app = initializeApp(config); + this.database = getDatabase(this.app); + this.auth = getAuth(this.app); + onAuthStateChanged(this.auth, (user) => { + if (user) { + this.userId = user.uid; + this.listenForMessages(); + } else { + this.userId = null; + } + }); + } + + private listenForMessages() { + if (this.userId && this.database) { + const messagesRef = ref(this.database, `${this.path}/${this.userId}`); + onValue(messagesRef, (snapshot) => { + const data = snapshot.val(); + this.messages = data ? Object.values(data) : []; + this.scrollToBottom(); + }); + } + } + + private handleSendMessage(event: CustomEvent) { + const newMessage: Message = { + sender: 'user', + senderName: 'You', // Adjust as needed to reflect the user's actual name + text: event.detail.text, + timestamp: new Date().toISOString(), + }; + this.saveMessage(newMessage); + } + + private saveMessage(message: Message) { + if (this.userId && this.database) { + const messagesRef = ref(this.database, `${this.path}/${this.userId}`); + push(messagesRef, message); + } + } + + private resetConversation() { + if (this.userId && this.database) { + const messagesRef = ref(this.database, `${this.path}/${this.userId}`); + remove(messagesRef).then(() => { + this.messages = []; + }); + } + } + + private scrollToBottom() { + const messagesContainer = this.shadowRoot?.querySelector('.messages'); + if (messagesContainer) { + (messagesContainer as HTMLElement).scrollTop = messagesContainer.scrollHeight; + } + } + + render() { + return html` +
+
+ ${this.messages.map( + (message) => html`` + )} +
+ + Reset +
+ `; + } + + static styles = css` + .chat-box { + display: flex; + flex-direction: column; + height: 100%; + max-height: 500px; + padding: 16px; + border: 1px solid var(--md-sys-color-outline); + border-radius: 8px; + background-color: var(--md-sys-color-surface); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + .messages { + flex-grow: 1; + overflow-y: auto; + padding-bottom: 8px; + display: flex; + flex-direction: column; + } + md-filled-button { + margin-top: 8px; + } + `; +} diff --git a/packages/chat-box/src/ChatInput.ts b/packages/chat-box/src/ChatInput.ts new file mode 100644 index 000000000..2d9d959fc --- /dev/null +++ b/packages/chat-box/src/ChatInput.ts @@ -0,0 +1,44 @@ +// web-src/ts/chat-input.ts +import { LitElement, html, css, property } from 'lit'; +import '@material/web/button/filled-button.js'; +import '@material/web/textfield/filled-text-field.js'; + +export class ChatInput extends LitElement { + static styles = css` + :host { + display: flex; + align-items: center; + padding: 8px; + border-top: 1px solid var(--md-sys-color-outline); + } + md-filled-text-field { + flex-grow: 1; + margin-right: 8px; + } + `; + + render() { + return html` + + Send + `; + } + + private handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault(); + this.sendMessage(); + } + } + + private sendMessage() { + const input = this.shadowRoot?.getElementById('message-input') as HTMLInputElement; + if (input) { + const event = new CustomEvent('send-message', { + detail: { text: input.value }, + }); + this.dispatchEvent(event); + input.value = ''; + } + } +} \ No newline at end of file diff --git a/packages/chat-box/src/ChatMessage.ts b/packages/chat-box/src/ChatMessage.ts new file mode 100644 index 000000000..87aea8f38 --- /dev/null +++ b/packages/chat-box/src/ChatMessage.ts @@ -0,0 +1,55 @@ +// web-src/ts/chat-message.ts +import { LitElement, html, css, property } from 'lit'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { marked } from 'marked'; + +interface Message { + sender: string; + senderName: string; + text: string; +} + +export class ChatMessage extends LitElement { + @property({ type: Object }) + message: Message; + + static styles = css` + :host { + display: block; + margin-bottom: 8px; + } + .message { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 8px; + border-radius: 8px; + max-width: 75%; + } + .user { + background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + align-self: flex-end; + margin-left: auto; + } + .bot { + background-color: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface-variant); + align-self: flex-start; + margin-right: auto; + } + .sender { + font-weight: bold; + margin-bottom: 4px; + } + `; + + render() { + return html` +
+
${this.message.senderName}
+ ${unsafeHTML(marked(this.message.text))} +
+ `; + } +} \ No newline at end of file diff --git a/packages/chat-box/src/chat-box.ts b/packages/chat-box/src/chat-box.ts new file mode 100644 index 000000000..3ac2eac9f --- /dev/null +++ b/packages/chat-box/src/chat-box.ts @@ -0,0 +1,3 @@ +import { ChatBox } from './ChatBox.js'; + +window.customElements.define('chat-box', ChatBox); diff --git a/packages/chat-box/src/chat-input.ts b/packages/chat-box/src/chat-input.ts new file mode 100644 index 000000000..cf780d865 --- /dev/null +++ b/packages/chat-box/src/chat-input.ts @@ -0,0 +1,3 @@ +import { ChatInput } from './ChatInput.js'; + +window.customElements.define('chat-input', ChatInput); diff --git a/packages/chat-box/src/chat-message.ts b/packages/chat-box/src/chat-message.ts new file mode 100644 index 000000000..55dce7bfe --- /dev/null +++ b/packages/chat-box/src/chat-message.ts @@ -0,0 +1,3 @@ +import { ChatMessage } from './ChatMessage.js'; + +window.customElements.define('chat-message', ChatMessage); diff --git a/packages/chat-box/src/index.ts b/packages/chat-box/src/index.ts new file mode 100644 index 000000000..1f48559f8 --- /dev/null +++ b/packages/chat-box/src/index.ts @@ -0,0 +1 @@ +export { ChatBox } from './ChatBox.js'; diff --git a/packages/chat-box/stories/index.stories.ts b/packages/chat-box/stories/index.stories.ts new file mode 100644 index 000000000..c1faf5c3b --- /dev/null +++ b/packages/chat-box/stories/index.stories.ts @@ -0,0 +1,60 @@ +import { html, TemplateResult } from 'lit'; +import '../src/chat-box.js'; + +export default { + title: 'ChatBox', + component: 'chat-box', + argTypes: { + header: { control: 'text' }, + counter: { control: 'number' }, + textColor: { control: 'color' }, + }, +}; + +interface Story { + (args: T): TemplateResult; + args?: Partial; + argTypes?: Record; +} + +interface ArgTypes { + header?: string; + counter?: number; + textColor?: string; + slot?: TemplateResult; +} + +const Template: Story = ({ + header = 'Hello world', + counter = 5, + textColor, + slot, +}: ArgTypes) => html` + + ${slot} + +`; + +export const Regular = Template.bind({}); + +export const CustomHeader = Template.bind({}); +CustomHeader.args = { + header: 'My header', +}; + +export const CustomCounter = Template.bind({}); +CustomCounter.args = { + counter: 123456, +}; + +export const SlottedContent = Template.bind({}); +SlottedContent.args = { + slot: html`

Slotted content

`, +}; +SlottedContent.argTypes = { + slot: { table: { disable: true } }, +}; diff --git a/packages/chat-box/test/chat-box.test.ts b/packages/chat-box/test/chat-box.test.ts new file mode 100644 index 000000000..6beb90044 --- /dev/null +++ b/packages/chat-box/test/chat-box.test.ts @@ -0,0 +1,32 @@ +import { html } from 'lit'; +import { fixture, expect } from '@open-wc/testing'; +import { ChatBox } from '../src/ChatBox.js'; +import '../src/chat-box.js'; + +describe('ChatBox', () => { +// it('has a default header "Hey there" and counter 5', async () => { +// const el = await fixture(html``); +// +// expect(el.header).to.equal('Hey there'); +// expect(el.counter).to.equal(5); +// }); +// +// it('increases the counter on button click', async () => { +// const el = await fixture(html``); +// el.shadowRoot!.querySelector('button')!.click(); +// +// expect(el.counter).to.equal(6); +// }); +// +// it('can override the header via attribute', async () => { +// const el = await fixture(html``); +// +// expect(el.header).to.equal('attribute header'); +// }); +// +// it('passes the a11y audit', async () => { +// const el = await fixture(html``); +// +// await expect(el).shadowDom.to.be.accessible(); +// }); +}); diff --git a/packages/chat-box/tsconfig.json b/packages/chat-box/tsconfig.json new file mode 100644 index 000000000..92bd777f8 --- /dev/null +++ b/packages/chat-box/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmitOnError": true, + "lib": ["es2021", "dom", "DOM.Iterable"], + "strict": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "importHelpers": true, + "outDir": "dist", + "sourceMap": true, + "inlineSources": true, + "rootDir": "./", + "declaration": true, + "incremental": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/packages/chat-box/web-dev-server.config.js b/packages/chat-box/web-dev-server.config.js new file mode 100644 index 000000000..e09b6bbee --- /dev/null +++ b/packages/chat-box/web-dev-server.config.js @@ -0,0 +1,27 @@ +// import { hmrPlugin, presets } from '@open-wc/dev-server-hmr'; + +/** Use Hot Module replacement by adding --hmr to the start command */ +const hmr = process.argv.includes('--hmr'); + +export default /** @type {import('@web/dev-server').DevServerConfig} */ ({ + open: '/demo/', + /** Use regular watch mode if HMR is not enabled. */ + watch: !hmr, + /** Resolve bare module imports */ + nodeResolve: { + exportConditions: ['browser', 'development'], + }, + + /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */ + // esbuildTarget: 'auto' + + /** Set appIndex to enable SPA routing */ + // appIndex: 'demo/index.html', + + plugins: [ + /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */ + // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.lit] }), + ], + + // See documentation for all available options +}); diff --git a/packages/chat-box/web-test-runner.config.js b/packages/chat-box/web-test-runner.config.js new file mode 100644 index 000000000..a5f9f8b52 --- /dev/null +++ b/packages/chat-box/web-test-runner.config.js @@ -0,0 +1,41 @@ +// import { playwrightLauncher } from '@web/test-runner-playwright'; + +const filteredLogs = ['Running in dev mode', 'Lit is in dev mode']; + +export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({ + /** Test files to run */ + files: 'dist/test/**/*.test.js', + + /** Resolve bare module imports */ + nodeResolve: { + exportConditions: ['browser', 'development'], + }, + + /** Filter out lit dev mode logs */ + filterBrowserLogs(log) { + for (const arg of log.args) { + if (typeof arg === 'string' && filteredLogs.some(l => arg.includes(l))) { + return false; + } + } + return true; + }, + + /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */ + // esbuildTarget: 'auto', + + /** Amount of browsers to run concurrently */ + // concurrentBrowsers: 2, + + /** Amount of test files per browser to test concurrently */ + // concurrency: 1, + + /** Browsers to run tests on */ + // browsers: [ + // playwrightLauncher({ product: 'chromium' }), + // playwrightLauncher({ product: 'firefox' }), + // playwrightLauncher({ product: 'webkit' }), + // ], + + // See documentation for all available options +});