diff --git a/.github/workflows/mynah-ui-ci.yml b/.github/workflows/mynah-ui-ci.yml new file mode 100644 index 0000000000..895f443dc9 --- /dev/null +++ b/.github/workflows/mynah-ui-ci.yml @@ -0,0 +1,18 @@ +name: Mynah UI CI +on: + push: + branches: + - main + paths: + - 'mynah-ui/**' + pull_request: + paths: + - 'mynah-ui/**' + +jobs: + lint: + uses: ./.github/workflows/mynah-ui-lint.yml + unit-tests: + uses: ./.github/workflows/mynah-ui-unit-tests.yml + e2e-linux: + uses: ./.github/workflows/mynah-ui-e2e-linux.yml diff --git a/.github/workflows/mynah-ui-e2e-linux.yml b/.github/workflows/mynah-ui-e2e-linux.yml new file mode 100644 index 0000000000..f6978a399c --- /dev/null +++ b/.github/workflows/mynah-ui-e2e-linux.yml @@ -0,0 +1,78 @@ +name: Mynah UI - E2E Tests (Linux) +on: + workflow_call: + push: + branches: + - main + paths: + - 'mynah-ui/**' + pull_request: + paths: + - 'mynah-ui/**' + +jobs: + e2e-linux: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }} + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build E2E tests Docker Image (with cache) + if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }} + uses: docker/build-push-action@v5 + with: + context: ./mynah-ui + push: false + load: true + tags: mynah-ui-e2e:latest + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/mynah-ui-e2e:buildcache + cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/mynah-ui-e2e:buildcache,mode=max + + - name: Build E2E tests Docker Image (no cache - fork PRs) + if: ${{ github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'push' }} + uses: docker/build-push-action@v5 + with: + context: ./mynah-ui + push: false + load: true + tags: mynah-ui-e2e:latest + + - name: Run E2E tests Docker Container + run: npm run docker:run + working-directory: ./mynah-ui + env: + WEBKIT_FORCE_COMPLEX_TEXT: 0 + WEBKIT_DISABLE_COMPOSITING_MODE: 1 + PLAYWRIGHT_BROWSERS_PATH: 0 + + - name: Extract test results from Docker container + if: always() + run: npm run docker:extract + working-directory: ./mynah-ui + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: mynah-ui-test-results + path: ./mynah-ui/ui-tests/__results__ + retention-days: 30 + + report: + needs: e2e-linux + if: always() + uses: ./.github/workflows/mynah-ui-test-report.yml diff --git a/.github/workflows/mynah-ui-lint.yml b/.github/workflows/mynah-ui-lint.yml new file mode 100644 index 0000000000..4fa41e0f30 --- /dev/null +++ b/.github/workflows/mynah-ui-lint.yml @@ -0,0 +1,35 @@ +name: Mynah UI - Lint +on: + workflow_call: + push: + branches: + - main + paths: + - 'mynah-ui/**' + pull_request: + paths: + - 'mynah-ui/**' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build && cd ./ui-tests && npm install && cd .. + working-directory: ./mynah-ui + + - name: Run linter + run: npm run lint + working-directory: ./mynah-ui diff --git a/.github/workflows/mynah-ui-test-report.yml b/.github/workflows/mynah-ui-test-report.yml new file mode 100644 index 0000000000..d2239c1b26 --- /dev/null +++ b/.github/workflows/mynah-ui-test-report.yml @@ -0,0 +1,22 @@ +name: Mynah UI - Test Report +on: workflow_call + +permissions: + contents: read + actions: read + checks: write + +jobs: + report: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: dorny/test-reporter@v2.0.0 + with: + name: Mynah UI E2E Tests Report + artifact: mynah-ui-test-results + path: ./__reports__/junit.xml + reporter: jest-junit + fail-on-error: false diff --git a/.github/workflows/mynah-ui-unit-tests.yml b/.github/workflows/mynah-ui-unit-tests.yml new file mode 100644 index 0000000000..b3ee6ea181 --- /dev/null +++ b/.github/workflows/mynah-ui-unit-tests.yml @@ -0,0 +1,41 @@ +name: Mynah UI - Unit Tests +on: + workflow_call: + push: + branches: + - main + paths: + - 'mynah-ui/**' + pull_request: + paths: + - 'mynah-ui/**' + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + working-directory: ./mynah-ui + + - name: Run Unit tests + run: npm run test:unit + working-directory: ./mynah-ui + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + with: + name: mynah-ui-coverage-report + path: ./mynah-ui/coverage diff --git a/.gitignore b/.gitignore index 8f3af70b45..cd4c093346 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,12 @@ build *.tsbuildinfo app/aws-lsp-partiql-* -# Mynah -!mynah-ui/dist +# Mynah UI +mynah-ui/__results__/ +mynah-ui/e2e-results/ +mynah-ui/ui-tests/__results__/ +mynah-ui/ui-tests/__snapshots__/ +mynah-ui/**api-docs # Coverage (C8) **/coverage/ diff --git a/chat-client/package.json b/chat-client/package.json index 28c4ab64af..e0f4ce1a54 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -27,7 +27,7 @@ "@aws/chat-client-ui-types": "0.1.68", "@aws/language-server-runtimes": "^0.3.12", "@aws/language-server-runtimes-types": "^0.1.63", - "@aws/mynah-ui": "^4.38.0" + "@aws/mynah-ui": "*" }, "devDependencies": { "@types/jsdom": "^21.1.6", diff --git a/chat-client/src/client/chat.test.ts b/chat-client/src/client/chat.test.ts index b64cd7e55d..e0a40e9cff 100644 --- a/chat-client/src/client/chat.test.ts +++ b/chat-client/src/client/chat.test.ts @@ -34,12 +34,11 @@ describe('Chat', () => { before(() => { // Mock global observers for test environment - // @ts-expect-error: mock implementation for testing - global.ResizeObserver = null - // @ts-expect-error: mock implementation for testing - global.IntersectionObserver = null - // @ts-expect-error: mock implementation for testing - global.MutationObserver = null + /* eslint-disable @typescript-eslint/no-explicit-any */ + ;(global as any).ResizeObserver = null + ;(global as any).IntersectionObserver = null + ;(global as any).MutationObserver = null + /* eslint-enable @typescript-eslint/no-explicit-any */ }) beforeEach(() => { @@ -65,10 +64,10 @@ describe('Chat', () => { }) after(() => { - // @ts-expect-error: mock implementation for testing - global.ResizeObserver = undefined - // @ts-expect-error: mock implementation for testing - global.MutationObserver = undefined + /* eslint-disable @typescript-eslint/no-explicit-any */ + ;(global as any).ResizeObserver = undefined + ;(global as any).MutationObserver = undefined + /* eslint-enable @typescript-eslint/no-explicit-any */ }) it('publishes ready event when initialized', () => { diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index 58519d96ef..55dd8c5e79 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -124,7 +124,7 @@ const getDefaultTabConfig = (agenticMode?: boolean) => { } } -type ChatClientConfig = Pick & { +type ChatClientConfig = Partial> & { disclaimerAcknowledged?: boolean pairProgrammingAcknowledged?: boolean agenticMode?: boolean diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index 1f9f6c4e57..a0a4efaf92 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -405,10 +405,9 @@ describe('MynahUI', () => { const getTabDataStub = sinon.stub(mynahUi, 'getTabData') getTabDataStub.returns({ getStore: () => ({ - // @ts-expect-error partial object promptInputOptions: [{ id: 'pair-programmer-mode', value: 'false' }], }), - }) + } as any) handlePromptInputChange(mynahUi, tabId, { 'pair-programmer-mode': 'true' }) @@ -420,10 +419,9 @@ describe('MynahUI', () => { const getTabDataStub = sinon.stub(mynahUi, 'getTabData') getTabDataStub.returns({ getStore: () => ({ - // @ts-expect-error partial object promptInputOptions: [{ id: 'pair-programmer-mode', value: 'true' }], }), - }) + } as any) handlePromptInputChange(mynahUi, tabId, { 'pair-programmer-mode': 'false' }) diff --git a/mynah-ui/.eslintignore b/mynah-ui/.eslintignore new file mode 100644 index 0000000000..2b68ecc0f4 --- /dev/null +++ b/mynah-ui/.eslintignore @@ -0,0 +1,25 @@ +*.js +*.json +*.md +*.css +*.ttf +*.scss +*.svg +*.png +*.map +*.html +*.xml +*.zip +.github +.husky +example +api-docs +dist +docs +out +LICENSE +THIRD-PARTY-LICENSES +NOTICE +Dockerfile +e2e-results +coverage/ diff --git a/mynah-ui/.eslintrc.js b/mynah-ui/.eslintrc.js new file mode 100644 index 0000000000..d5fb7727ae --- /dev/null +++ b/mynah-ui/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: __dirname, + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.test.json'], + }, + plugins: ['@typescript-eslint', 'unused-imports'], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + ignorePatterns: ['ui-tests/**', 'dist/**', 'coverage/**', 'node_modules/**', '*.js'], + rules: { + semi: ['error', 'never'], + 'no-constant-condition': ['error', { checkLoops: false }], + 'no-case-declarations': 'off', + 'no-empty': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-non-null-assertion': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + 'unused-imports/no-unused-imports': 'error', + }, +} diff --git a/mynah-ui/Dockerfile b/mynah-ui/Dockerfile new file mode 100644 index 0000000000..ddcd093ba2 --- /dev/null +++ b/mynah-ui/Dockerfile @@ -0,0 +1,49 @@ +# Version-agnostic Dockerfile for Mynah UI E2E Tests +# Supports dynamic Playwright version detection +ARG PLAYWRIGHT_VERSION=latest +FROM mcr.microsoft.com/playwright:${PLAYWRIGHT_VERSION} + +# Set working directory +WORKDIR /app + +# Copy the src from the mynah-ui package +COPY ./src /app/src + +# Copy config files from mynah-ui root +COPY ./package.json /app +COPY ./webpack.config.js /app +COPY ./tsconfig.json /app + +# Copy scripts directory for version-agnostic setup +COPY ./scripts /app/scripts + +# Copy required files from ui-tests +COPY ./ui-tests/package.json /app/ui-tests/ +COPY ./ui-tests/playwright.config.ts /app/ui-tests/ +COPY ./ui-tests/tsconfig.json /app/ui-tests/ +COPY ./ui-tests/webpack.config.js /app/ui-tests/ + +# Copy the directories from ui-tests +COPY ./ui-tests/__test__ /app/ui-tests/__test__ +COPY ./ui-tests/src /app/ui-tests/src +COPY ./ui-tests/__snapshots__ /app/ui-tests/__snapshots__ + +# Install dependencies and build MynahUI +RUN npm install +RUN npm run build + +# Setup Playwright with version-agnostic approach +RUN cd ./ui-tests && node ../scripts/setup-playwright.js && npm run prepare + +# Ensure all browsers are installed with dependencies +RUN cd ./ui-tests && npx playwright install --with-deps + +# Run health check to verify installation +RUN cd ./ui-tests && node ../scripts/docker-health-check.js + +# Set environment variables for WebKit +ENV WEBKIT_FORCE_COMPLEX_TEXT=0 +ENV WEBKIT_DISABLE_COMPOSITING_MODE=1 + +# Default command to run the tests +CMD ["sh", "-c", "cd ./ui-tests && npm run e2e${BROWSER:+:$BROWSER}"] diff --git a/mynah-ui/docs/ARCHITECTURE.md b/mynah-ui/docs/ARCHITECTURE.md new file mode 100644 index 0000000000..e13e2f1644 --- /dev/null +++ b/mynah-ui/docs/ARCHITECTURE.md @@ -0,0 +1,91 @@ +# MynahUI Architecture + +## How do the Consumer and MynahUI work together? + +Before how it works, it is better to clarify how to make it work. To consume MynahUI, there is only one single requirement, `npm` access. Adding `@aws/mynah-ui` to the `package.json` dependencies will allow the consumer to grab the desired version of MynahUI and allow them to create an instance of it to be rendered in the desired dom element. + +To install: + +``` +npm install @aws/mynah-ui +``` + +And to create the instance: + +``` +const mynahUI = new MynahUI({...}); +``` + +#### So, how is the flow between the consumer and the MynahUI in general? + +As indicated above in the section, it expects data and sends events. The expected data from the MynahUI can be passed with several ways like defining them during the initialization, updating the data store directly or adding one or more chat items during the runtime. Let’s take a look to the basic flow between MynahUI and the consumer: + +![image](https://github.com/user-attachments/assets/052ff1a4-e2f8-449f-a793-32dff333f6a5) + +As we can clarify from the flow, MynahUI expects data from the consumer app and renders new elements or updates existing ones on the UI. And it is also responsible to deliver the user events to the consumer app to let them run their logic. + + + + +## How does MynahUI work under the hood? +![image](https://github.com/user-attachments/assets/f9ea537f-6db7-4249-b347-f46812646e7e) + +MynahUI relies on three core structures, the **Data Store**, **Global Event Handler** and **Dom Builder**. The combination of these 3 basically drives the MynahUI structure. + + +#### Let’s break down the **Data Store**: + +The data store consists of 2 parts. The main data store holds all the current data for the tabs. Since MynahUI supports multiple tabs, each tab has its own data. And the second block in the data store is the data for each tab. + +Global Data Store → Tab Data Store 1, Tab Data Store 2 ... Tab Data Store N + +Tab Data store holds every single content related with that tab, like chat items, tab title, background, prompt input field related information etc. + +Here’s an overview of what it looks like: + +![image](https://github.com/user-attachments/assets/f375031a-e2bb-4015-a3c1-ae88739b59cd) + + + +#### Let’s break down the Global Event Handler: + +The global event handler can be used by any component, to listen or fire a non data related event happened through the system. And more importantly, they can also fire events to inform the subscribers. +For example, when a tab gets focus (basically being selected) it fires an event through the global event system which is called `TAB_FOCUS`. And if there is any subscriber to that event, their attached handler function will be called. + +![image](https://github.com/user-attachments/assets/ea9157da-0030-4d85-8ede-4cbe918d6512) + + +#### Let’s break down the DomBuilder: + +DomBuilder is at the heart of the rendering part of MynahUI. Basically, every single UI (HTML) element is being generated from the DomBuilder. It helps to manage dom manipulation from one single space. For example when you need to add some specific attribute to any dom generated in the MynahUI, it will be handled from this **singleton** class. + +![image](https://github.com/user-attachments/assets/40ccab42-a64f-4120-95a1-57822add9f80) + + + +The main class (MynahUI) handles all the creation of these core parts and the communication between them. + +To clarify how all those structures work together, **a simplified flow can be showed as follows**: + +![image](https://github.com/user-attachments/assets/f816ad36-4ad3-4e13-913e-d6afb9939a4f) + + + +### How do components work? + +Components are using the DomBuilder to build up their HTML elements. Or, they can also use other components as well. +Each component should have their `render`, which should also be just an HTMLElement or an ExtendedHTMLElement which is the output of the DomBuilder. +For the styling of the elements and components, MynahUI uses basic css structure. However to make it as clean as possible to read and generate proper hierarchies, we’re building the output css from SCSS. + + +>But an important notice here, we’re trying to avoid using SCSS variables as much as possible and keep every possible thing as a CSS Custom property. + + +The styling of the components cannot have static values or inline values. With the support of the CSS custom properties, it is possible to theme it in every single detail like colors, paddings, sizings, fonts even animations and transitions. + +**Here’s a general look of a component structure:** + +![image](https://github.com/user-attachments/assets/d7de7181-2e0d-43c2-8118-fffa7cc36156) + + + diff --git a/mynah-ui/docs/CONFIG.md b/mynah-ui/docs/CONFIG.md new file mode 100644 index 0000000000..dea7d04a7d --- /dev/null +++ b/mynah-ui/docs/CONFIG.md @@ -0,0 +1,417 @@ +# MynahUI Config + +You can set the config from the constructor parameters while creating a new instance of `mynah-ui`. + +_**Note:** You cannot set it on runtime. It is getting used just once during the initialization._ + +```typescript +... +interface ConfigModel { + // Do not forget that you have to provide all of them + // Config allows partial set of texts + texts: { + mainTitle?: string; + feedbackFormTitle?: string; + feedbackFormDescription?: string; + feedbackFormOptionsLabel?: string; + feedbackFormCommentLabel?: string; + feedbackThanks?: string; + feedbackReportButtonLabel?: string; + codeSuggestions?: string; + files?: string; + insertAtCursorLabel?: string; + copy?: string; + showMore?: string; + save?: string; + cancel?: string; + submit?: string; + pleaseSelect?: string; + stopGenerating?: string; + copyToClipboard?: string; + noMoreTabsTooltip?: string; + codeSuggestionWithReferenceTitle?: string; + spinnerText?: string; + tabCloseConfirmationMessage?: string; + tabCloseConfirmationKeepButton?: string; + tabCloseConfirmationCloseButton?: string; + noTabsOpen: string; // Supports markdown + openNewTab: string; + commandConfirmation: string; + pinContextHint: string; + dragOverlayText: string; + }; + // Options to show up on the overlay feedback form + // after user clicks to downvote on a chat item + // and clicks 'Report' again + feedbackOptions: Array<{ + label: string; + value: string; + }>; + tabBarButtons?: TabBarMainAction[]; // Tab bar buttons will be shown on the right of the tab + maxUserInput: number; // max number of chars for the input field + userInputLengthWarningThreshold: number; // The amount of characters in the input field necessary for the character limit warning to show + codeInsertToCursorEnabled?: boolean; // show or hide copy buttons on code blocks system wide + codeCopyToClipboardEnabled?: boolean; // show or hide insert to cursor buttons on code blocks system wide + autoFocus: boolean; // auto focuses to input panel after every action + maxTabs: number; // set 1 to hide tabs panel + showPromptField: boolean; // shows prompt field (default: true) + dragOverlayIcon?: MynahIcons | MynahIconsType | CustomIcon; // icon displayed in the overlay when a file is dragged into the chat area + enableSearchKeyboardShortcut?: boolean; // if true, calls onSearchShortcut on Command + f or Ctrl + f (default: false) +} +... +``` +--- + +


+ + +# `tabBarButtons` + +You can put buttons on the right of the tab bar also with some inner buttons inside a menu. You can do it in two different ways. If you want the buttons globally available for every tab you can use the `tabBarButtons` in the config. If you want them set individually for different tabs check the **[DATAMODEL Documentation](./DATAMODEL.md#tabbarbuttons)**. + +```typescript +const mynahUI = new MynahUI({ + ... + config: { + ... + tabBarButtons: [ + { + id: 'clear', + description: 'Clear messages in this tab', + icon: MynahIcons.REFRESH, + }, + { + id: 'multi', + icon: MynahIcons.ELLIPSIS, + items: [ + { + id: 'menu-action-1', + text: 'Menu action 1!', + icon: MynahIcons.CHAT, + }, + { + id: 'menu-action-2', + text: 'Menu action 2!', + icon: MynahIcons.CODE_BLOCK, + }, + { + id: 'menu-action-3', + text: 'Menu action 3!' + } + ] + } + ] + } + ... +}); +``` + +

+ mainTitle +
+ mainTitle +

+ +--- + + + +# `texts` +All static texts will be shown on UI. +Please take a look at each image to identify which text belongs to which item on UI. + +## mainTitle +Default tab title text if it is not set through store data for that tab. + +

+ mainTitle +

+ +--- + +## feedbackFormTitle, feedbackFormDescription, feedbackFormOptionsLabel, feedbackFormCommentLabel, submit, cancel +

+ feedbackForm +

+ + +--- + +## fileTreeTitle, rootFolderTitle, feedbackFormCommentLabel, submit, cancel +

+ fileTree +

+ + +--- + +## pleaseSelect +

+ feedbackForm +

+ +--- + +## feedbackThanks, feedbackReportButtonLabel, showMore +

+ voteAndSourceActions +

+ +--- + +## stopGenerating +

+ stopGenerating +

+ +--- + +## insertAtCursorLabel, copy +

+ copyInsertToCursor +

+ +--- + +## codeSuggestions, files, codeSuggestionWithReferenceTitle +

+ codeFileSuggestions +

+ +--- + +## spinnerText +

+ spinnerText +

+ +--- + +## tabCloseConfirmationMessage, tabCloseConfirmationKeepButton, tabCloseConfirmationCloseButton +

+ tabCloseConfirmation +

+ +--- + +## noMoreTabsTooltip +

+ noMoreTabsTooltip +

+ +--- + +## noTabsOpen, openNewTab +

+ noTabsOpen +

+ +--- + +## commandConfirmation +

+ commandConfirmation +

+ +## pinContextHint +

+ pinContextHint +

+--- + +## dragOverlayText +

+ dragOverlayText +

+--- + +


+ +# `feedbackOptions` + +Feedback type options to be shown on feedback form. +defaults: +```typescript +... +feedbackOptions: [ + { + value: 'inaccurate-response', + label: 'Inaccurate response', + }, + { + value: 'harmful-content', + label: 'Harmful content' + }, + { + value: 'overlap', + label: 'Overlaps with existing content' + }, + { + value: 'incorrect-syntax', + label: 'Incorrect syntax' + }, + { + value: 'buggy-code', + label: 'Buggy code' + }, + { + value: 'low-quality', + label: 'Low quality' + }, + { + value: 'other', + label: 'Other' + } + ], +... +``` + +

+ feedbackOptions +

+ +--- + +


+ +# `maxTabs` +Maximum number of tabs user/system can open in a single instance of `mynah-ui`. + +default: `1000` + +An important note here is that if you provide **`1`** to maxTabs, it will not show the tab bar at all. However you still need to add a tab then initially to show a content. + +And finally, if you try to add tabs more than given `maxTabs` amount while initializing the MynahUI with [Constructor Properties](./PROPERTIES.md), it will only generate the tabs till it reaches the `maxTabs` limit. + +_Assume that you've provided `1` for `maxTabs`._ + + +

+ maxTabs1 +

+ +--- + +


+ +# `autoFocus` +Just auto focus to prompt input field after every response arrival or initialization. + +default: `true` + +--- +

+ feedbackOptions +

+


+ +# `userInputLengthWarningThreshold` +The amount of characters in the prompt input necessary for the character limit warning overlay to show up. +> [!NOTE] +> In older versions, the character count used to always show underneath the input, but this was changed in a recent release. + +default: `3500` + +

+ feedbackOptions +

+ +--- + +


+ +# `maxUserInput` +Max number of chars user can insert into the prompt field. But, as might know you can also add code attachments under the prompt field. A treshold of `96` chars will be automatically reduced from the `maxUserInput`. + +**So beware that if you want 4000 chars exact, you need to give 4096 to the config.** + +default: `4096` + +--- + +## `codeInsertToCursorEnabled` and `codeCopyToClipboardEnabled` (default: true) +These two parameters allow you to make copy and insert buttons disabled system wide. If you want to disable it specifically for a message you can do it through `ChatItem` object. Please see [DATAMODEL Documentation](./DATAMODEL.md#codeinserttocursorenabled-and-codecopytoclipboardenabled-default-true). + +

+ codeInsertAndCopy +

+ +--- + +## `codeBlockActions` +With this parameter, you can add global code block actions to the code blocks. But, you can override them through [ChatItem Data Model](./DATAMODEL.md#codeBlockActions). + +### Note +If you want to show that action only for certain coding languages, you can set the array for `acceptedLanguages` parameter. Keep in mind that it will check an exact mathc. If the incoming language is same with one of the acceptedLanguages list, it will show the action. + +#### flash +You can also make the code block actions flash once or foverer when user hovers the the containing card. Until user hovers to the action itself, whenever they hover to the card it will flash the code block action. It you set it to `once` it will only flash once for every hover to the container card, if you set it to `infinite` it will keep flashing forever every 3 seconds until user hovers to the action itself. Whe user hovers to the action, it will not flash again. + +#### By default, we add `copy` and `insert to cursor position` ones: + +```typescript +{ + codeBlockActions: { + ...(codeCopyToClipboardEnabled !== false + ? { + copy: { + id: 'copy', + label: texts.copy, + icon: MynahIcons.COPY + } + } + : {}), + ...(codeInsertToCursorEnabled !== false + ? { + 'insert-to-cursor': { + id: 'insert-to-cursor', + label: texts.insertAtCursorLabel, + icon: MynahIcons.CURSOR_INSERT + } + } + : {}), + } +} +``` + +

+ codeInsertAndCopy +

+ +--- + +


+ +# `showPromptField` +Show or hide the prompt input field completely. You may want to hide the prompt field by setting `showPromptField` to `false` to make the chat work one way only. Just to provide answers or information. + +default: `true` + +_If you set `showPromptField` to `false`_ + +

+ noPrompt +

+ +--- + +## dragOverlayIcon + +**Type:** `MynahIcons | MynahIconsType | CustomIcon` + +**Description:** +Specifies the icon to display in the drag-and-drop overlay for adding files (such as images) to the chat context. This allows consumers to customize the overlay icon. + +**Default:** `MynahIcons.IMAGE` + +

+ noPrompt +

+ +## enableSearchKeyboardShortcut + +**Type:** `boolean` + +When set to `true`, this option enables capturing the search keyboard shortcut. When enabled, pressing Command+F (Mac) or Ctrl+F (Windows/Linux) will trigger the `onSearchShortcut` event instead of the browser's default search behavior. This allows implementing custom search functionality within the chat interface. + +Default: `false` \ No newline at end of file diff --git a/mynah-ui/docs/DATAMODEL.md b/mynah-ui/docs/DATAMODEL.md new file mode 100644 index 0000000000..9b95f3644a --- /dev/null +++ b/mynah-ui/docs/DATAMODEL.md @@ -0,0 +1,3816 @@ +# MynahUI Data Model (with how the things appear on screen) + +There are a number of models for the various items on the screen for MynahUI. Let's start from the top and go in detail one-by-one. + +## Tab Data Store + +All information you can set related to a tab. + +```typescript +interface MynahUIDataModel { + /** + * Tab title + * */ + tabTitle?: string; + /** + * Tab icon + * */ + tabIcon?: MynahIcons | MynahIconsType | null; + /** + * is tab pinned + * */ + pinned?: boolean; + /** + * Tab title + * */ + tabBackground?: boolean; + /** + * If tab is running an action (loadingChat = true) this markdown will be shown before close in a popup + */ + tabCloseConfirmationMessage?: string | null; + /** + * Keep tab open button text + */ + tabCloseConfirmationKeepButton?: string | null; + /** + * Close tab button text + */ + tabCloseConfirmationCloseButton?: string | null; + /** + * Chat screen loading animation state (mainly use during the stream or getting the initial answer) + */ + loadingChat?: boolean; + /** + * Show chat avatars or not + * */ + showChatAvatars?: boolean; + /** + * Show cancel button while loading the chat + * */ + cancelButtonWhenLoading?: boolean; + /** + * Quick Action commands to show when user hits / to the input initially + */ + quickActionCommands?: QuickActionCommandGroup[]; + /** + * Context commands to show when user hits @ to the input any point + */ + contextCommands?: QuickActionCommandGroup[]; + /** + * Placeholder to be shown on prompt input + */ + promptInputPlaceholder?: string; + /** + * Prompt input text + */ + promptInputText?: string; + /** + * Label to be shown on top of the prompt input + */ + promptInputLabel?: string | null; + /** + * Label to be shown on top of the prompt input + */ + promptInputVisible?: boolean; + /** + * Info block to be shown under prompt input + */ + promptInputInfo?: string; + /** + * A sticky chat item card on top of the prompt input + */ + promptInputStickyCard?: Partial | null; + /** + * Prompt input field disabled state, set to tru to disable it + */ + promptInputDisabledState?: boolean; + /** + * Prompt input progress field + */ + promptInputProgress?: ProgressField | null; + /** + * Prompt input options/form items + */ + promptInputOptions?: FilterOption[] | null; + /** + * Prompt input button items + */ + promptInputButtons?: ChatItemButton[] | null; + /** + * List of chat item objects to be shown on the web suggestions search screen + */ + chatItems?: ChatItem[]; + /** + * Attached code under the prompt input field + */ + selectedCodeSnippet?: string; + /** + * Tab bar buttons next to the tab items + */ + tabBarButtons?: TabBarMainAction[]; + /** + * Tab content compact mode which keeps everything in the middle + */ + compactMode?: boolean; + /** + * Tab content header details, only visibile when not null / undefined + */ + tabHeaderDetails?: TabHeaderDetails | null; + /** + * A lightweight key-value store for essential tab-specific primitive metadata. + * Not intended for storing large amounts of data - use appropriate + * application state management for that purpose. + */ + tabMetadata?: { [key: string]: string | boolean | number }; + /** + * Custom context commands to be inserted into the prompt input. + */ + customContextCommand: [] +} +``` + +You can set tab data with this model for `defaults`, initial `tabs` which can be set through [Constructor Properties](./PROPERTIES.md) or update a tab on runtime by using `mynahUI.updateStore(...)`. + +Let's see which items is what. + +### `tabTitle` (default: `"AWS Q"`) +Basically it is the tab title. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabTitle: 'Chat' +}) +``` + +### `tabIcon` (default: undefined) +Basically it is an icon you can give to the tab. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabTitle: '', + tabIcon: MynahIcons.MENU, + pinned: true +}) +``` + +### + +

+ pinnedTab +

+ + +### `pinned` (default: `false`) +You can pin the tabs to the beginning. But when you pin a tab, end user cannot close them anymore. It will disable the middle mouse click to close a tab and remove the close button too. The tab will be basically pinned. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabTitle: '', + tabIcon: MynahIcons.MENU, + pinned: true +}) +``` + +### + +

+ pinnedTab +

+ +### `tabBackground` (default: `false`) +Shows or hides the gradient background on the tab. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabBackground: true +}) +``` + + +### + +

+ tabBackground +

+ +--- + +### `tabCloseConfirmationMessage`, `tabCloseConfirmationKeepButton` and `tabCloseConfirmationCloseButton` + +Custom texts for each tab for the message and the buttons of the popup to confirm the tab close. Check **[Config/TEXTS](./CONFIG.md#texts)** for defaults. + +

+ onTabRemove +

+ +--- + +### `loadingChat` (default: `false`) +Basically it is the tab title. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + loadingChat: true +}) +``` +When you set `loadingChat` to true, if there is a streaming card it will start to animate the spinner in two different way. If the card body is empty it will show the `spinnerText` from the texts inside the config right next to a spinning circle. If the card has a body (after it is updated for example) it will show a sliding colored bottom border animation. + +In addition to the spinner, if `onStopChatResponse` is attached globally through MynahUI main class constructor properties _(see [Constructor properties](./PROPERTIES.md) for details)_ and `cancelButtonWhenLoading` is not set to false specifically for that tab it will show the stop generating button too. + +

+ mainTitle +

+

+ mainTitle +

+ +--- + + +### `cancelButtonWhenLoading` (default: `true`) +If `onStopChatResponse` is attached globally through `MynahUI` main class constructor properties _(see [Constructor properties](./PROPERTIES.md) for details)_ it will show a stop generating button to let the user cancel the ongoing action. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + loadingChat: true, + cancelButtonWhenLoading: true +}) +``` + +

+ mainTitle +

+ +--- + + +### `quickActionCommands` (default: `[]`) +Quick action commands are the predefined commands which user can pick between. When users hit `/` from their keyboard as the initial char in the input, if there is an available list it will show up as a overlay menu. + +If you want a command immediately run after the selection and trigger `onChatPrompt` event (attached to the `MynahUI` main instance through the [Constructor properties](./PROPERTIES.md)) leave the `placeholder` attribute undefined. MynahUI will decide that it doesn't allow additional prompt text for that command and immediately run the trigger. _(See command-2 in the example)_ + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + quickActionCommands: [ + { + icon: MynahIcons.CODE, + groupName: 'Command Group 1', + commands: [ + { + command: '/command-1', + placeholder: 'Command which accepts a prompt after the command selection', + description: 'Command 1 description', + }, + { + command: '/command-2', + description: 'Command 2 description', + }, + ], + }, + { + groupName: 'Command Group 2', + commands: [ + { + command: '/command-3', + placeholder: 'Command which accepts a prompt after the command selection', + description: 'Command 3 description', + }, + ], + }, + { + // Command Group without title + commands: [ + { + command: '/command-4', + placeholder: 'Command which accepts a prompt after the command selection', + description: 'Command 4 description', + }, + ], + }, + ] +}) +``` + +

+ quickActionCommands +

+ +To handle the incoming command (if there is) check it with the prompt object in the `onChatPrompt` event. + +```typescript +const mynahUI = new MynahUI({ + ... + onChatPrompt: (prompt)=>{ + if(prompt.command !== undefined){ + switch (prompt.command) { + case '/command-1': + console.log(`Command 1 selected with prompt: ${prompt.prompt}`); + break; + case '/command-2': + console.log('Command 2 selected'); + break; + default: + ... + break; + } + } + } +}); +``` + +--- + +### `contextCommands` (default: `[]`) +Context commands are the predefined context items which user can pick between but unlike quick action commands, they can be picked several times at any point in the prompt text. When users hit `@` from their keyboard in the input, if there is an available list of context items provided through store it will show up as an overlay menu. + +#### Disabled Commands +Commands can be disabled by setting the `disabled` property to `true`. When a command is disabled, you can optionally provide `disabledText` to display custom text instead of the usual arrow icon for items with children. This is useful for showing status information like "pending". + + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { +contextCommands: [ + { + commands: [ + { + command: 'workspace', + icon: MynahIcons.ASTERISK, + placeholder: 'Yes, you selected workspace :P', + description: 'Reference all code in workspace.', + disabled: true, + disabledText: 'pending' + }, + { + command: 'folder', + icon: MynahIcons.FOLDER, + children: [ + { + groupName: 'Folders', + commands: [ + { + command: 'src', + icon: MynahIcons.FOLDER, + children: [ + { + groupName: 'src/', + commands: [ + { + command: 'index.ts', + icon: MynahIcons.FILE, + } + ] + } + ] + }, + { + command: 'main', + description: './src/', + icon: MynahIcons.FOLDER, + }, + { + command: 'src', + description: './example/', + icon: MynahIcons.FOLDER, + } + ] + } + ], + placeholder: 'Mention a specific folder', + description: 'All files within a specific folder' + }, + { + command: 'file', + icon: MynahIcons.FILE, + children: [ + { + groupName: 'Files', + commands: [ + { + command: 'monarch.ts', + description: './src/', + icon: MynahIcons.FILE, + }, + { + command: '_dark.scss', + description: './src/styles/', + icon: MynahIcons.FILE, + } + ] + } + ], + placeholder: 'Mention a specific file', + description: 'Reference a specific file' + }, + { + command: 'symbols', + icon: MynahIcons.CODE_BLOCK, + children: [ + { + groupName: 'Symbols', + commands: [ + { + command: 'DomBuilder', + icon: MynahIcons.CODE_BLOCK, + description: 'The DomGeneration function in dom.ts file' + } + ] + } + ], + placeholder: 'Select a symbol', + description: 'After that mention a specific file/folder, or leave blank for full project' + }, + { + command: 'prompts', + icon: MynahIcons.CHAT, + description: 'Saved prompts, to reuse them in your current prompt', + children: [ + { + groupName: 'Prompts', + actions: [ + { + id: 'add-new-prompt', + icon: 'plus', + text: 'Add', + description: 'Add new prompt' + } + ], + commands: [ + { + command: 'python_expert', + icon: MynahIcons.CHAT, + description: 'Expert on python stuff' + }, + { + command: 'javascript_expert', + icon: MynahIcons.CHAT, + description: 'Expert on Javascript and typescript' + }, + { + command: 'Add Prompt', + icon: MynahIcons.PLUS, + } + ] + } + ] + } + ] + } + ] +}) +``` + +

+ contextCommands +

+ +When hovered, context items will display a tooltip with the same information provided in the context menu list: + +

+ contextItem +

+ +Groups can have as many children as you'd like, which allows for a tree-like structure. Items with children will display a right-arrow icon when hovered / focused: + +

+ hoveredContextItem +

+ +Groups can have actions (see `add-new-prompt` action in the example code block above), which adds an action button on the top right: + +

+ groupAction +

+ +To see which context is used, check the incoming string array in the prompt object comes with the `onChatPrompt` event. + +```typescript +const mynahUI = new MynahUI({ + ... + onChatPrompt: (prompt)=>{ + if(prompt.context != null && prompt.context.indexOf('@ws') { + // Use whole workspace! + } + } +}); +``` + +--- + +### `promptInputPlaceholder` (default: `''`) + +This is the placeholder text for the prompt input + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputPlaceholder: 'Ask a question or “/” for capabilities' +}) +``` + +

+ mainTitle +

+ +--- +### `promptTopBarTitle` (default: `''`) + +This is the title displayed in the prompt top bar. When set, it enables a top bar that can be used for pinned context items. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptTopBarTitle: '@Pin Context' +}) +``` + +

+ prompt top bar title +

+ +--- + +### `promptTopBarContextItems` (default: `[]`) + +These are the context items pinned to the prompt top bar. They appear as pills that can be removed by the user. Top bar only appears when `promptTopBarTitle` is not empty. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptTopBarContextItems: [ + { + command: 'ex-dom.ts', + icon: MynahIcons.FILE, + description: '.src/helper' + }, + { + command: 'main', + icon: MynahIcons.FOLDER, + description: '.src/' + } + ] +}) +``` + +

+ prompt top bar context items +

+ +--- + +### `promptTopBarButton` (default: `null`) + +This is a button displayed at the end of the prompt top bar. Clicking on the button will call onPromptTopBarButtonClick(). Button only appears when `promptTopBarTitle` is not empty. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptTopBarButton: { + id: 'project-rules', + icon: MynahIcons.CHECK_LIST, + text: 'Rules' + } +}) +``` + +

+ prompt top bar button +

+ +--- + +### `promptInputText` (default: `''`) + +This is the text inside the prompt input. You can set it anytime, but be careful, it will override what is already written in the text input. +A nice trick to use it is to open the quick actions command picker too. If you send `"/"` or `"/some-matching-text"` it will open the quick actions command selector automatically and also filter the list with the following text if given. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputText: '/dev' +}) +``` + +

+ Prompt input text +

+ +--- + +### `promptInputLabel` (default: `''`) + +This is label for the prompt input text. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputLabel: 'Prompt input text label' +}) +``` + +

+ prompt input label +

+ +--- + +### `promptInputVisible` (default: `true`) + +This is a control point for the visibility of the prompt input field. Unlike the `showPromptField` in [global CONFIG](./CONFIG.md#showpromptfield) it allows you to change the visibility of the prompt input field for each individual tab on runtime. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputVisible: false, +}) +``` + +

+ mainTitle +

+ +--- + +### `promptInputInfo` (default: `''`) + +This is a info field under the bottom of the prompt input field, like a footer text + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputInfo: 'Use of Amazon Q is subject to the [AWS Responsible AI Policy](https://aws.com).', +}) +``` + +

+ mainTitle +

+ +--- + +### `promptInputStickyCard` (default: `null`) + +This is a chat item card which will be shown on top of the prompt input field. Main usage scneario for this is to inform the user with a card view, which means that it can also have some actions etc. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + }, + ... + onInBodyButtonClicked: (tabId: string, messageId: string, action) => { + if(messageId === 'sticky-card'){ + // clear the card + mynahUI.updateStore(tabId, {promptInputStickyCard: null}); + } + ... + }, + ... +}); + +mynahUI.updateStore(tabId, { + promptInputStickyCard: { + messageId: 'sticky-card', + body: `Please read the [terms and conditions change](#) and after that click the **Acknowledge** button below!`, + status: 'info', + icon: MynahIcons.INFO, + buttons: [ + { + // you can also simply set this to false to remove the card automatically + keepCardAfterClick: true, + text: 'Acknowledge', + id: 'acknowledge', + status: 'info', + icon: MynahIcons.OK + }, + ], + } +}); + +``` + +

+ mainTitle +

+ +--- + +### `promptInputDisabledState` (default: `false`) + +This is the disabled state if the prompt input field. When set to true, user cannot focus to the input and cannot click to the send button. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + promptInputDisabledState: true, +}) +``` + +

+ mainTitle +

+ +--- + +### `promptInputProgress` + +This determines whether the progress bar shows up, and what its styling and progress value is. The `value` should be a number representing the progress, and the `valueText` is the text that shows right next to the regular `text` to indicate the progress in the bar. A number of `actions` can be added to dispatch events. Different statuses are available, namely: `default` | `info` | `success` | `warning` | `error`. + +**In progress:** +```typescript +mynahUI.updateStore('tab-1', { + promptInputProgress: { + status: 'default', + text: 'Work in progress...', + value: -1, + actions: [{ + id: 'cancel-running-task', + text: 'Cancel', + icon: MynahIcons.CANCEL, + disabled: false, + }] + } +}); +``` + +**Completed:** +```typescript +mynahUI.updateStore('tab-1', { + promptInputProgress: { + status: 'success', + text: 'Completed...', + valueText: '', + value: 100, + actions: [] + } +}); +``` + +

+ mainTitle +

+ +--- + +### `promptInputOptions` + +Under the prompt input field, it is possible to add form items too for several options. For example a toggle can be placed to let user pick the type of the prompt. To listen the value changes on these options please check [onPromptInputOptionChange in Constructor properties](./PROPERTIES.md#onPromptInputOptionChange) and the see how they are being passed to prompt please check [onChatPrompt in Constructor properties](./PROPERTIES.md#onChatPrompt). + +To cleanup, simply set to `null` or an empty array. + +```typescript +mynahUI.updateStore('tab-1', { + promptInputOptions: [ + { + type: 'toggle', + id: 'prompt-type', + value: 'ask', + options: [{ + value: 'ask', + icon: MynahIcons.CHAT + },{ + value: 'do', + icon: MynahIcons.FLASH + }] + } + ] +}); +``` + +

+ promptOptions +

+ +------ + +### `promptInputButtons` + +Under the prompt input field, it is possible to add buttons too. To listen the click events on these options please check [onPromptInputButtonClick in Constructor properties](./PROPERTIES.md#onPromptInputButtonClick). + +To cleanup, simply set to `null` or an empty array. + +```typescript +mynahUI.updateStore('tab-1', { + promptInputButtons: [ + { + id: 'upgrade-q', + icon: 'bug', + } + ] +}); +``` + +

+ promptButtons +

+ +--- + +### `selectedCodeSnippet` + +This is the attached code block text right under the prompt input field.. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + selectedCodeSnippet: `const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + isSelected: true, + .....`, +}); +``` + +

+ mainTitle +

+ +--- + +### `tabBarButtons` + +You can put buttons on the right of the tab bar also with some inner buttons inside a menu. You can do it in two different ways. If you want the buttons belong to specific tab, you can use the `tabBarButtons` for tab store. If you want them globally available for every tab, check the **[Config Documentation](./CONFIG.md#tabbarbuttons)**. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabBarButtons: [ + { + id: 'clear', + description: 'Clear messages in this tab', + icon: MynahIcons.REFRESH, + }, + { + id: 'multi', + icon: MynahIcons.ELLIPSIS, + items: [ + { + id: 'menu-action-1', + text: 'Menu action 1!', + icon: MynahIcons.CHAT, + }, + { + id: 'menu-action-2', + text: 'Menu action 2!', + icon: MynahIcons.CODE_BLOCK, + }, + { + id: 'menu-action-3', + text: 'Menu action 3!' + } + ] + } + ], +}) +``` + +

+ mainTitle +
+ mainTitle +

+ +--- + +### `compactMode` + +You can enable/disable compact mode. In compact mode, there will be more paddings from every edge. In addition to the paddings, the chat content will be middle placed (15% more pushed from the bottom) instead of being stretched to the available height. However, it will not exceed the available height for its own space. +While doing the transition for the compact mode switch, there is also a nice and smooth animation. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + compactMode: true, +}) +``` + +

+ compactMode +

+ +--- + +### `tabHeaderDetails` (default: `null`) + +There is a chance to add a detailed header on top of the tab content. Which can have an icon, title and the description. +**NOTE:** When you give `tabHeaderDetails` it will also adjust the alignment of the chat items to top. So until the content section reaches the max height available, they'll start to be ordered from top to bottom. Which means that it will also take space as their available content height. This will make the prompt field also moves up under the content. If the content height is more than available space, prompt input will still fit under the bottom of the screen. + +**NOTE:** When you provide `tabHeaderDetails` it will also make the chat cards width stretch to full available width of the screen. So they'll not get their width depending on their content and up to 90%. Instead, it will always be 100%. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabHeaderDetails: { + icon: MynahIcons.Q, + title: "Welcome to Q Developer", + description: "What kind of questions you have?" + }, +}) +``` + +

+ tabHeaderDetails +
+ tabHeaderDetails 2 +

+ +--- + +### `tabMetaData` (default: `{}`) + +A lightweight key-value store for essential tab-specific metadata. Not intended for storing large amounts of data - use appropriate application state management for that purpose. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + tabMetaData: { + 'test': 'hi' + } +}) +``` + +--- + +### `chatItems` (default: `[]`) + +This is holding the chat items. If you provide it through the `defaults` or inside a tab item in the initial `tabs` property in the [Constructor properties](./PROPERTIES.md) you can give the whole set. + +**BUT** if you will set it through `updateStore` it will append the items in the list to the current chatItems list. In case if you need to update the list with a new one manually on runtime, you need to send an empty list first and than send the desired new list. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.updateStore('tab-1', { + chatItems: [], +}) +``` + +


+ +--- + +


+ +# `ChatItem` (and how they appear on screen) + +There are basically 2 main types of chat items. One is to show a file list and the other one is for all other items with a body of markdown string content. + +Let's start with the model definition: + +```typescript +// There can be more types on the ChatItemType enum list +// However those are the only ones getting used by MynahUI +enum ChatItemType { + CODE_RESULT = 'code-result', + ANSWER_STREAM = 'answer-stream', + DIRECTIVE = 'directive', + ANSWER = 'answer', + PROMPT = 'prompt', + SYSTEM_PROMPT = 'system-prompt' +} + +interface ChatItemAction extends ChatPrompt { + type?: string; + pillText: string; + disabled?: boolean; + description?: string; + status?: 'info' | 'success' | 'warning' | 'error'; + icon?: MynahIcons; +} + +interface ChatItemButton { + keepCardAfterClick?: boolean; + waitMandatoryFormItems?: boolean; + text: string; + id: string; + disabled?: boolean; + description?: string; + status?: 'info' | 'success' | 'warning' | 'error'; + icon?: MynahIcons; +} + +type ChatItemFormItem = TextBasedFormItem | DropdownFormItem | RadioGroupFormItem | CheckboxFormItem | ListFormItem | Stars | PillboxFormItem; + +export interface ValidationPattern { + pattern: string | RegExp; + errorMessage?: string; +} + +interface BaseFormItem { + id: string; + mandatory?: boolean; + hideMandatoryIcon?: boolean; + title?: string; + placeholder?: string; + value?: string; + description?: string; + tooltip?: string; + icon?: MynahIcons | MynahIconsType; + boldTitle?: boolean; +} + +export type TextBasedFormItem = BaseFormItem & { + type: 'textarea' | 'textinput' | 'numericinput' | 'email'; + autoFocus?: boolean; + checkModifierEnterKeyPress?: boolean; + validateOnChange?: boolean; + validationPatterns?: { + operator?: 'and' | 'or'; + genericValidationErrorMessage?: string; + patterns: ValidationPattern[]; + }; + disabled?: boolean; +}; + +type DropdownFormItem = BaseFormItem & { + type: 'select'; + border?: boolean; + autoWidth?: boolean; + options?: Array<{ + value: string; + label: string; + description?: string; + }>; + disabled?: boolean; + selectTooltip?: string; +}; + +type Stars = BaseFormItem & { + type: 'stars'; + options?: Array<{ + value: string; + label: string; + }>; +}; + +type RadioGroupFormItem = BaseFormItem & { + type: 'radiogroup' | 'toggle'; + options?: Array<{ + value: string; + label?: string; + icon?: MynahIcons | MynahIconsType; + }>; + disabled?: boolean; +}; + +type CheckboxFormItem = BaseFormItem & { + type: 'switch' | 'checkbox'; + value?: 'true' | 'false'; + label?: string; + alternateTooltip?: string; +}; + +export interface ListFormItem { + type: 'list'; + id: string; + mandatory?: boolean; + hideMandatoryIcon?: boolean; + title?: string; + description?: string; + tooltip?: string; + icon?: MynahIcons | MynahIconsType; + items: SingularFormItem[]; + value: ListItemEntry[]; + disabled?: boolean; +}; + +export interface ListItemEntry { + persistent?: boolean; + value: Record; +} + +type PillboxFormItem = BaseFormItem & { + type: 'pillbox'; + value?: string; +}; + +interface FileNodeAction { + name: string; + label?: string; + disabled?: boolean; + description?: string; + status?: Status; + icon: MynahIcons | MynahIconsType; +} + +interface TreeNodeDetails { + status?: Status; + icon?: MynahIcons | MynahIconsType | null; + iconForegroundStatus?: Status; + label?: string; + changes?: { + added?: number; + deleted?: number; + total?: number; + }; + description?: string; + clickable?: boolean; + data?: Record; +} + +interface SourceLink { + title: string; + id?: string; + url: string; + body?: string; + type?: string; + metadata?: Record; +} + +interface ReferenceTrackerInformation { + licenseName?: string; + repository?: string; + url?: string; + recommendationContentSpan?: { + start: number; + end: number; + }; + information: string; +} + +interface ChatItemBodyRenderer extends GenericDomBuilderAttributes { + type: AllowedTagsInCustomRenderer; + children?: Array | undefined; + attributes?: Partial> | undefined; +} + +interface CodeBlockAction { + id: 'copy' | 'insert-to-cursor' | string; + label: string; + description?: string; + icon?: MynahIcons; + data?: any; + flash?: 'infinite' | 'once'; + acceptedLanguages?: string[]; +} +type CodeBlockActions = Record<'copy' | 'insert-to-cursor' | string, CodeBlockAction | undefined | null>; + +// ################################# +interface ChatItemContent { + header?: (ChatItemContent & { + icon?: MynahIcons | MynahIconsType | CustomIcon; + iconStatus?: 'main' | 'primary' | 'clear' | Status; + iconForegroundStatus?: Status; + status?: { + status?: Status; + position?: 'left' | 'right'; + description?: string; + icon?: MynahIcons | MynahIconsType; + text?: string; + }; + }) | null; + body?: string | null; + customRenderer?: string | ChatItemBodyRenderer | ChatItemBodyRenderer[] | null; + followUp?: { + text?: string; + options?: ChatItemAction[]; + } | null; + relatedContent?: { + title?: string; + content: SourceLink[]; + } | null; + codeReference?: ReferenceTrackerInformation[] | null; + fileList?: { + fileTreeTitle?: string; + rootFolderTitle?: string; + rootFolderStatusIcon?: MynahIcons | MynahIconsType; + rootFolderStatusIconForegroundStatus?: Status; + rootFolderLabel?: string; + filePaths?: string[]; + deletedFiles?: string[]; + flatList?: boolean; + folderIcon?: MynahIcons | MynahIconsType | null; + collapsed?: boolean; + hideFileCount?: boolean; + renderAsPills?: boolean; // When true (header only), renders files as inline pills instead of tree + actions?: Record; + details?: Record; + } | null; + buttons?: ChatItemButton[] | null; + formItems?: ChatItemFormItem[] | null; + footer?: ChatItemContent | null; + informationCard?: { + title?: string; + status?: { + status?: Status; + icon?: MynahIcons | MynahIconsType; + body?: string; + }; + description?: string; + icon?: MynahIcons | MynahIconsType; + content: ChatItemContent; + } | null; + summary?: { + isCollapsed?: boolean; + content?: ChatItemContent; + collapsedContent?: ChatItemContent[]; + } | null; + tabbedContent?: Array | null; + codeBlockActions?: CodeBlockActions | null; + quickSettings?: DropdownFactoryProps | null; + fullWidth?: boolean; + padding?: boolean; + wrapCodes?: boolean; + muted?: boolean; +} + +interface ChatItem extends ChatItemContent { + type: ChatItemType; + messageId?: string; + snapToTop?: boolean; + autoCollapse?: boolean; + contentHorizontalAlignment?: 'default' | 'center'; + canBeVoted?: boolean; + canBeDismissed?: boolean; + title?: string; + icon?: MynahIcons | MynahIconsType | CustomIcon; + iconForegroundStatus?: Status; + iconStatus?: 'main' | 'primary' | 'clear' | Status; + hoverEffect?: boolean; + status?: Status; + shimmer?: boolean; +} +// ################################# +``` + +Let's see all kind of examples and what parameter reflects to what. + +## `type` + +### ChatItemType.`ANSWER_STREAM` _(position: left)_ +Use for streaming cards. It is better to start with an empty string to let the initial spinner rotate. As far as the `loadingState` is true for the tab which holds this chat item, it will show the spinner (rotating circle for empty state and bottom border for with a body). + +When you add a new chat item with type `ANSWER_STREAM` MynahUI will set it as the streaming card and when you call `updateLastChatAnswer` it will update this. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.ANSWER_STREAM, + body: '' +}); + +// After a moment +mynahUI.updateLastChatAnswer('tab-1', { + body: `### How to create a React stateless function component + +*React .14* introduced a simpler way to define components called stateless functional components. + ` +}); +``` + +

+ mainTitle +

+ +--- + + + +### ChatItemType.`DIRECTIVE` _(position: left)_ +Use for directions. Those chat item cards will not have a background, will not have a padding and border at all. But they'll support all chatitem functionalities as is. + + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.DIRECTIVE, + body: '_Starting with a directive_' +}); +``` + +

+ directive +

+ +--- + +### ChatItemType.`ANSWER` or ChatItemType.`CODE_RESULT` _(position: left)_ +Use for all kind of answers. Including the followups etc. + +And yes, except the `fileList` you can combine followups and markdown string content chat items at once. Which means that a single chat item can also contain the `followUp` at the same time with `body`. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.ANSWER, + body: 'Hi, I\'m Amazon Q. I can answer your software development questions. Ask me to explain, debug, or optimize your code. You can enter `/` to see a list of quick actions.' + followUp:{ + text: 'Or you can select one of these', + options: [ + { + pillText: 'Explain selected code', + }, + { + pillText: 'How can Amazon Q help me?', + prompt: 'How can Amazon Q help me?', + } + ], + } +}); +``` + +

+ mainTitle +

+ +--- + +### ChatItemType.`PROMPT` _(position: right)_ +Use for user prompts. You can also send followups to let them appear on right. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.PROMPT, + body: 'Who are you?' +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.PROMPT, + followUp:{ + text: 'Or you can select one of these', + options: [ + { + pillText: 'Explain selected code', + }, + { + pillText: 'How can Amazon Q help me?', + prompt: 'How can Amazon Q help me?', + } + ], + } +}); +``` + +

+ mainTitle +

+ +--- + +### ChatItemType.`SYSTEM_PROMPT` _(position: right)_ +Use for sysyem prompts. Only difference with `PROMPT` is the color of the chat card. (Depends on your **[Styling Configuration](STYLING.md)**) You can also send followups to let them appear on right. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.SYSTEM_PROMPT, + body: 'This is a system prompt' +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.SYSTEM_PROMPT, + followUp: { + text: 'Or you can select one of these', + options: [ + { + pillText: 'Explain selected code', + }, + { + pillText: 'How can Amazon Q help me?', + prompt: 'How can Amazon Q help me?', + } + ], + } +}); +``` + +

+ mainTitle +

+ +--- + +## `header` +With this parameter, you can add a `ChatItem` at the top of a ChatItem, before the body, but still within the card itself. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem(tabId, { + type: ChatItemType.ANSWER, + body: `SOME CONTENT`, + header: { + // icon: MynahIcons.CODE_BLOCK; + // status: { + // position: 'right', + // status: 'success', + // icon: MynahIcons.OK, + // text: 'Accepted', + // }, + fileList: { // For example, want to show which file is used to generate that answer + rootFolderTitle: undefined, + fileTreeTitle: '', + filePaths: ['./src/index.ts'], + details: { + './src/index.ts': { + icon: MynahIcons.FILE, + description: `SOME DESCRIPTION.` + } + } + } + } +}); +``` + +

+ header +

+ +You can also provide an icon specifically for the header, as well as a separate status section on right or left of the whole header defined by its `position` value with a tooltip too. + +Here's another example for that: + +```typescript +mynahUI.addChatItem(tabId, { + messageId: 'MY_UNIQUE_ID', + type: ChatItemType.ANSWER, + fullWidth: true, + padding: false, + header: { + icon: MynahIcons.CODE_BLOCK, + status: { + position: 'right', + icon: MynahIcons.PROGRESS, + description: 'Hello!', + text: 'Working', + status: 'warning' + }, + buttons: [{ + id: 'stop', + icon: MynahIcons.CANCEL, + }], + fileList: { + fileTreeTitle: '', + filePaths: ['package.json'], + details: { + 'package.json': { + icon: null, + label: 'Creating', + changes: { + added: 36, + deleted: 0, + total: 36 + } + } + } + } + } +}); +``` + + +

+ headerMore +

+--- + +## `body` +Basically the body of the card. Which you can send a full markdown string. Allows code blocks, links etc. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + type: ChatItemType.ANSWER, + body: "## Here'a heading 2\nAnd also here are some code blocks which supports most common languages.\n```typescript\nconst a = 5;\n```\n You can also use some `inline code` items too.\n And also for example [a link](https://aws.com)" +}); +``` + +

+ mainTitle +

+ +--- + +## `customRenderer` +Custom renderers can be provided in 3 different types *(string, ChatItemBodyRenderer object or ChatItemBodyRenderer object array)* and they are here to help you in case you need to create some static content on the client side rather than a data arrived from the backend. Or, maybe it is not possible or so hard to do it just with markdown. + +##### Note: It can be combined with `body`, so you don't need to choose one of them. + + +### Using with `string` type +If you give a string to the `customRenderer` mynah-ui will consider that it as an html markup string and will render it that way. + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +mynahUI.addChatItem('tab-1', { + messageId: (new Date().getTime()).toString(), + type: ChatItemType.ANSWER, + canBeVoted: true, + customRenderer: ` +

Custom renderer's with HTML markup string

+

+ Here you will find some custom html rendering examples which may not be available with markdown or pretty hard to generate. +

+
+ +

Table (inside a blockqote)

+
+ Most popular JS frameworks + + +
+ + + + + + + + + + + + + + + + + + + + + + +
Name + Weekly Downloads +
Vanillainf.
React24 million
JQuery10.6 million
VUE4.75 million
+
+
+ + +
+
+ +

Code block and Links

+ +
+            import { MynahUI } from '@aws/mynah-ui';
+
+            const mynahUI = new MynahUI({});
+        
+

+ You can find more information and references + + HERE! + . +

+ +
+ + +
+
+ +

Embeds and Media elements

+ +

Iframe embed (Youtube example)

+ +
+ +

Video element

+ +
+ +

Audio element

+ +
+ +

Image

+ Powered by AWS +
+ + +
+
+ +

+ There might be infinite number of possible examples with all supported tags and their attributes. + It doesn't make so much sense to demonstrate all of them here. + You should go take a look to the + + documentation + + for details and limitations. +

` +}); +``` + +

+ customRendererHTML +

+ +### Using with `ChatItemBodyRenderer` or `ChatItemBodyRenderer[]` type +Even though you can build exactly the same html elements and node tree with the `string` type, this option will give you more flexibility especially on repeating items. We all know that it is not easy to read code which loops inside a string. **But more importantly, you can also bind events with this option**. + +Another `+1` for this option is related with its interface declaration. With an object structure which is properly typed, your IDE should give you the available values list during the code completion. Which means that you don't need to guess or go back and forth between the documentation and your project to see which tags you can use in the `type` attribute (html tag), which attributes are supported for the `attributes` or which events are available for the `events`. + +Let's take a look how we write with `ChatItemBodyRenderer[]` interface: + +```typescript +const mynahUI = new MynahUI({ + tabs: { + 'tab-1': { + ... + } + } +}); + +// Lets' use a super dumb array instead of copy pasting the items inside the customRenderer. +const topFrameworks: Record = {'Vanilla': 'inf.', 'React': '24', 'JQuery': '10.6', 'VUE': '4.75'}; + +mynahUI.addChatItem('tab-1', { + messageId: (new Date().getTime()).toString(), + type: ChatItemType.ANSWER, + canBeVoted: true, + customRenderer: [ + { + type: 'h3', + children: ['Custom renderer\'s with JSON dom builder objects'] + }, + { + type: 'p', + children: ['Here you will find some custom html rendering examples which may not be available with markdown or pretty hard to generate. But in this examples they are rendered from JSON definitions.'] + }, + { + type: 'p', + children: ['There is no difference between using a markup string or a JSON dom. You can create same accepted tags with same accepted attributes.'] + }, + { + type: 'p', + children: [ + 'Except 1 thing: ', + {type: 'strong', children: ['attaching events! Like click or mousemove etc.']} + ] + }, + { type: 'br' }, + { + type: 'h3', + events: { + click: (event) => { alert('Why you click to title?'); } + }, + children: ['Table (inside a blockqote)'] + }, + { + type: 'p', + children: ['This is basically the same table one card above with markup strings, but in this one ', {type: 'b', children: ['you can click to the table titles!']}] + }, + { type: 'br' }, + { + type: 'blockquote', + children: [ + 'Most popular JS frameworks', + { type: 'hr' }, // Divider + { + type: 'table', + children: [ + { + type: 'tr', + children: [ + { + type: 'th', + events: { + click: () => { alert('Why you click this title?'); } + }, + attributes: { align: 'left' }, + children: ['Name'] + }, + { + type: 'th', + events: { + click: () => { alert('Why you click to this title?'); } + }, + attributes: { align: 'right' }, + children: ['Weekly Downloads'] + } + ] + }, + // Mapping our dumb array to create the rows + ...Object.keys(topFrameworks).map(fw => ({ + type: 'tr', + children: [ + { type: 'td', children: [fw]}, + { type: 'td', + attributes: { align: 'right' }, + children: [ + topFrameworks[fw], + ...(!isNaN(parseFloat(topFrameworks[fw])) ? [{type: 'small', children: [' million']}] : []) + ] + } + ] + } as ChatItemBodyRenderer + )), + ] + } + ] + }, + { type: 'br' }, // Add more space + { + type: 'p', + children: ['Or you can click below image to remove it!'] + }, + { type: 'br' }, + { + type: 'img', + events: { + click: (event: MouseEvent)=>{ + (event.target as HTMLElement).remove(); + } + }, + attributes: { + src: 'https://d1.awsstatic.com/logos/aws-logo-lockups/poweredbyaws/PB_AWS_logo_RGB_stacked_REV_SQ.91cd4af40773cbfbd15577a3c2b8a346fe3e8fa2.png', + alt: 'Powered by AWS!' + } + } + ] + }); +``` + +

+ customRendererJson +

+ +## BUT: There are some `LIMITATIONS!` + +### We know that you're extremely careful while building custom html blocks and you're an expert on CSS. However, we still need to assure that the look & feel of the UI is not broken and it works as expected with all the functionalities. Because of these reasons with the addition of the safety concerns we have to **`sanitize`** the HTML contents you provide. + +**And,** the sanitization requirement it is not just limited with the above. We're also automatically applying the functionalities we have on the original chat item body like *highlighting the code syntaxes, adding copy to clipboard and insert at cursor position buttons or adding the event controls for links etc.*. +For example, you can check how the code blocks provided inside `customRenderer` look like (and do they have the copy buttons?) in the above examples. + +**NOTE:** Below limitations are applicable for all of the `string`, `ChatItemBodyRenderer` and `ChatItemBodyRenderer[]` type usages. + +### List of available tags: + +``` +[ + 'a', 'audio', 'b', 'blockquote', + 'br', 'hr', 'canvas', + 'code', 'col', 'colgroup', + 'data', 'div', 'em', + 'embed', 'figcaption', 'figure', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'i', 'iframe', + 'img', 'li', 'map', + 'mark', 'object', 'ol', + 'p', 'pre', 'q', + 's', 'small', 'source', + 'span', 'strong', 'sub', + 'sup', 'table', 'tbody', + 'td', 'tfoot', 'th', + 'thead', 'tr', 'track', + 'u', 'ul', 'video', +] +``` + +**NOTE:** As you can see in the above list, **form items are also not available**. Since this is a chat interface we should keep it as conversational as possible instead of using select/input/click structures to interact if they are not helping the end user. But in case you need some small forms and inputs from the user other than the prompts, you can use the **[`formIems`](#formitems)**. + +### List of available attributes: + +``` +[ + 'accept','accept-charset','accesskey', + 'align','allow','allowfullscreen', + 'alt', 'as','async','autocapitalize', + 'autoplay','charset','class', + 'cols','colspan','controls', + 'crossorigin','data','data-*', + 'datetime','decoding','default', + 'dir','download','headers', + 'hidden','high','href', + 'hreflang','id','ismap',' + itemprop','kind','lang', + 'language','loop','low', + 'media','muted','optimum', + 'ping','playsinline','poster', + 'preload','referrerpolicy', + 'rel','reversed','role', + 'rowspan','sandbox','scope', + 'shape','size','sizes','slot', + 'span','spellcheck','src', + 'srcdoc','srclang','srcset', + 'start', 'style', 'target','title', + 'translate','usemap', + 'wrap','aspect-ratio' +] +``` + +## Important Tips for `customRenderer` + +### Tip 1 +As you might see there is also no `width` and `height` attributes are available. +As we've told you above, we know you're so good at styling components but our concern is the HTML itself. Since `mynah-ui` has a responsive design nature, we cannot let you write a static width or height to an `img` for example. + +### But you're free to write custom styles for each tag you can create. But don't forget that you're getting the responsibility of a broken UI. So be careful with the styles and try not to be so extreme on that. + +It applies to `iframe`s, `video`s and other similar media elements too. +So, **avoid writing static sizes** and learn **what is the aspect ratio of your media content**. + +### Tip 2 +In general, those items *(except `img`)* will automatically stretched to 100% width and will stay that way as the max width is 100%. Yes, you cannot use static width and heights, **but** you can define their aspect ratios. Here's an example: + +``` + +``` +When you provide a value to the `aspect-ratio` attribyte, it will automatically set the `width` of the element to `100%` and apply the aspect ratio for the height. + +### Tip 3 +So, are you looking for the available `aspect-ratio` values? +Here they are: `16:9`, `9:16`, `21:9`, `9:21`, `4:3`, `3:4`, `3:2`, `2:3`, `1:1` + +If you need more aspect-ratios, please raise a feature request. + +### Tip 4 +**But,** of course we cannot control your imagination and lower down your expertise on html element structures. + +For example; you can say that oldies are goldies and still have some emotional connection to the `table`s. How we can understand that you used a `table` and used some `colspan`s for the `td`s to adjust the width as the half of the wrapper card for the element you put inside which will not break the responsive structure... + +``` + + +
+ ', + '', + '', + '
', + '
', + '', + ] + + xssPayloads.forEach((payload, index) => { + const element = domBuilder.build({ + type: 'div', + innerHTML: payload, + }) + + // These dangerous elements should be completely removed + expect(element.innerHTML).not.toContain('alert') + expect(element.innerHTML).not.toContain('prompt') + expect(element.innerHTML).not.toContain('javascript:') + expect(element.innerHTML).not.toContain('onerror') + expect(element.innerHTML).not.toContain('onload') + expect(element.innerHTML).not.toContain('onfocus') + expect(element.innerHTML).not.toContain('ontoggle') + expect(element.innerHTML).not.toContain('onstart') + expect(element.innerHTML).not.toContain(' { + it('should handle user input in chat messages', () => { + const userMessage = '">' + + const chatElement = domBuilder.build({ + type: 'div', + classNames: ['mynah-chat-message'], + innerHTML: userMessage, + }) + + expect(chatElement.innerHTML).not.toContain('onerror') + expect(chatElement.innerHTML).not.toContain('prompt') + expect(chatElement.innerHTML).not.toContain('document.domain') + }) + + it('should handle malicious markdown content', () => { + const maliciousMarkdown = '[Click me](javascript:alert("XSS"))' + + const element = domBuilder.build({ + type: 'div', + innerHTML: maliciousMarkdown, + }) + + // The content should be treated as plain text, not parsed as markdown + expect(element.innerHTML).not.toContain('' + + const element = domBuilder.build({ + type: 'div', + innerHTML: maliciousCustomContent, + }) + + expect(element.innerHTML).toContain('

Title

') + expect(element.innerHTML).not.toContain('