From 13957f383aa1f8f505dfbc0e1cc412b1fd1e3fea Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 13 Feb 2024 11:57:07 -0800 Subject: [PATCH 01/25] Adding AI integration to playbooks --- webapp/src/ai_integration.ts | 22 ++++++++ webapp/src/components/assets/icons/ai.tsx | 31 +++++++++++ webapp/src/components/rhs/rhs_post_update.tsx | 7 +++ .../components/rhs/rhs_post_update_button.tsx | 54 ++++++++++++++++--- 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 webapp/src/ai_integration.ts create mode 100644 webapp/src/components/assets/icons/ai.tsx diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts new file mode 100644 index 0000000000..0e52b39552 --- /dev/null +++ b/webapp/src/ai_integration.ts @@ -0,0 +1,22 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {GlobalState} from 'mattermost-webapp/packages/types/src/store'; +import {useSelector} from 'react-redux'; + +export const aiPluginID = 'mattermost-ai'; + +export const useAIAvailable = () => { + //@ts-ignore plugins state is a thing + return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); +}; + +export type AIStatusUpdateClickedFunc = ((playbookRunId: string) => string) | undefined; + +export const useAIStatusUpdateClicked = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.aiStatusUpdateClicked; + }); +}; + diff --git a/webapp/src/components/assets/icons/ai.tsx b/webapp/src/components/assets/icons/ai.tsx new file mode 100644 index 0000000000..d3ee100339 --- /dev/null +++ b/webapp/src/components/assets/icons/ai.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import Svg from 'src/components/assets/svg'; + +const IconAI = () => ( + + + + + + +); + +export default IconAI; + diff --git a/webapp/src/components/rhs/rhs_post_update.tsx b/webapp/src/components/rhs/rhs_post_update.tsx index 64f30fb777..ab989c2037 100644 --- a/webapp/src/components/rhs/rhs_post_update.tsx +++ b/webapp/src/components/rhs/rhs_post_update.tsx @@ -19,6 +19,7 @@ import TutorialTourTip, {useMeasurePunchouts, useShowTutorialStep} from 'src/com import {RunDetailsTutorialSteps, TutorialTourCategories} from 'src/components/tutorial/tours'; import {useNow} from 'src/hooks'; +import {useAIStatusUpdateClicked} from 'src/ai_integration'; interface Props { collapsed: boolean; @@ -41,6 +42,7 @@ const RHSPostUpdate = (props: Props) => { RunDetailsTutorialSteps.PostUpdate, TutorialTourCategories.RUN_DETAILS ); + const aiStatusUpdateClicked = useAIStatusUpdateClicked(); const isNextUpdateScheduled = props.playbookRun.previous_reminder !== 0; const timestamp = getTimestamp(props.playbookRun, isNextUpdateScheduled); @@ -113,6 +115,11 @@ const RHSPostUpdate = (props: Props) => { props.playbookRun.channel_id, )); }} + onAIClick={() => { + if (aiStatusUpdateClicked) { + aiStatusUpdateClicked(props.playbookRun.id); + } + }} isDue={isDue} /> {showRunDetailsPostUpdateStep && ( diff --git a/webapp/src/components/rhs/rhs_post_update_button.tsx b/webapp/src/components/rhs/rhs_post_update_button.tsx index b3bcf8472d..dfab9c9718 100644 --- a/webapp/src/components/rhs/rhs_post_update_button.tsx +++ b/webapp/src/components/rhs/rhs_post_update_button.tsx @@ -2,11 +2,15 @@ // See LICENSE.txt for license information. import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; import styled, {css} from 'styled-components'; import {DestructiveButton, PrimaryButton, TertiaryButton} from 'src/components/assets/buttons'; +import IconAI from 'src/components/assets/icons/ai'; +import Tooltip from 'src/components/widgets/tooltip'; +import {useAIAvailable} from 'src/ai_integration'; + interface Props { collapsed: boolean; isDue: boolean; @@ -14,9 +18,13 @@ interface Props { updatesExist: boolean; disabled: boolean; onClick: () => void; + onAIClick: () => void; } const RHSPostUpdateButton = (props: Props) => { + const {formatMessage} = useIntl(); + const aiAvailable = useAIAvailable(); + let ButtonComponent = PostUpdatePrimaryButton; if (props.isDue) { @@ -26,13 +34,31 @@ const RHSPostUpdateButton = (props: Props) => { } return ( - - - + + + + + { aiAvailable && + + + + + + + + } + ); }; @@ -40,6 +66,18 @@ interface CollapsedProps { collapsed: boolean; } +const ButtonsContainer = styled.div` + flex-grow: 1; + display: flex; + flex-direction: row; + gap: 2px; +`; + +const AIButtonContainer = styled.div` + display: flex; + flex-grow: 0; +`; + const PostUpdateButtonCommon = css` justify-content: center; flex: 1; From 99fce532ec71fe3a678592336e2b01b8374a16ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 21 Nov 2024 16:03:00 +0100 Subject: [PATCH 02/25] WIP --- webapp/src/actions.ts | 2 +- webapp/src/ai_modal.tsx | 96 +++++++++++++++++++ webapp/src/client.ts | 22 +++++ .../modals/update_run_status_modal.tsx | 36 ++++++- webapp/src/index.tsx | 3 + webapp/src/manifest.js | 47 +++++++++ webapp/src/types/websocket_events.ts | 1 + webapp/src/websocket.tsx | 30 ++++++ 8 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 webapp/src/ai_modal.tsx create mode 100644 webapp/src/manifest.js create mode 100644 webapp/src/websocket.tsx diff --git a/webapp/src/actions.ts b/webapp/src/actions.ts index df0f4ef139..5687eacce7 100644 --- a/webapp/src/actions.ts +++ b/webapp/src/actions.ts @@ -151,7 +151,7 @@ export function openUpdateRunStatusModal( hasPermission: boolean, message?: string, reminderInSeconds?: number, - finishRunChecked?: boolean + finishRunChecked?: boolean, ) { return modals.openModal(makeUpdateRunStatusModalDefinition({ playbookRunId, diff --git a/webapp/src/ai_modal.tsx b/webapp/src/ai_modal.tsx new file mode 100644 index 0000000000..c4f7136c19 --- /dev/null +++ b/webapp/src/ai_modal.tsx @@ -0,0 +1,96 @@ +import {WebSocketMessage} from '@mattermost/client'; +import React, {ChangeEvent, useEffect, useState, useCallback} from 'react'; +import styled from 'styled-components'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import {generateStatusUpdate} from './client'; + +import postEventListener, {PostUpdateWebsocketMessage} from './websocket'; + +const Textbox = window.Components.Textbox; + +type Props = { + playbookRunId: string + generating: boolean + onGeneratingChanged: (generating: boolean) => void +} + +const AIModal = ({generating, playbookRunId, onGeneratingChanged}: Props) => { + const intl = useIntl(); + const [update, setUpdate] = useState(''); + + useEffect(() => { + postEventListener.registerPostUpdateListener('playbooks_post_update', (msg: WebSocketMessage) => { + const data = msg.data; + if (!data.control) { + onGeneratingChanged(true); + setUpdate(data.next); + } else if (data.control === 'end') { + onGeneratingChanged(false); + } + }); + + generateStatusUpdate(playbookRunId); + + return () => { + postEventListener.unregisterPostUpdateListener('playbooks_post_update'); + }; + }, []); + + const regenerate = useCallback(() => { + generateStatusUpdate(playbookRunId); + }, [playbookRunId]); + + const copyText = useCallback(() => { + navigator.clipboard.writeText(update); + }, [update]) + + return ( + + ) => setUpdate(e.target.value)} + characterLimit={10000} + createMessage={''} + onKeyPress={() => true} + openWhenEmpty={true} + channelId={''} + disabled={false} + /> + {generating && + + } + {!generating && + + } + {!generating && + + } + {!generating && + + } + + + ); +}; + +const AIModalContainer = styled.div` + width: 110%; + position: absolute; + z-index: 1000; +`; + +export default AIModal; diff --git a/webapp/src/client.ts b/webapp/src/client.ts index a0b12fab05..3931845744 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -62,6 +62,10 @@ export const setSiteUrl = (url?: string): void => { apiUrl = `${basePath}/plugins/${manifest.id}/api/v0`; }; +function playbookRunRoute(playbookRunID: string): string { + return `${basePath}/plugins/mattermost-ai/playbook_run/${playbookRunID}`; +} + export const getSiteUrl = (): string => { return siteURL; }; @@ -811,3 +815,21 @@ export async function getTeamTopPlaybooks(timeRange: string, page: number, perPa } return data as InsightsResponse; } + + +export async function generateStatusUpdate(playbookRunID: string) { + const url = `${playbookRunRoute(playbookRunID)}/generate_status`; + const response = await fetch(url, Client4.getOptions({ + method: 'GET', + })); + + if (response.ok) { + return; + } + + throw new ClientError(Client4.url, { + message: '', + status_code: response.status, + url, + }); +} diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index 12c5d0e8ef..c71721a8c8 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -10,17 +10,22 @@ import React, { import {Link} from 'react-router-dom'; import {useDispatch, useSelector} from 'react-redux'; import styled from 'styled-components'; -import {useIntl} from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; import {DateTime, Duration} from 'luxon'; import {GlobalState} from '@mattermost/types/store'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {TertiaryButton} from 'src/components/assets/buttons'; + import {ApolloProvider, useQuery} from '@apollo/client'; import GenericModal, {Description, Label} from 'src/components/widgets/generic_modal'; import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; +import IconAI from 'src/components/assets/icons/ai'; +import AIModal from 'src/ai_modal' +import {useAIAvailable} from 'src/ai_integration'; import { Mode, @@ -51,10 +56,13 @@ import {useFinishRunConfirmationMessage} from 'src/components/backstage/playbook import {getPlaybooksGraphQLClient} from 'src/graphql_client'; import {getFragmentData, graphql} from 'src/graphql/generated'; import {DefaultMessageFragment, ReminderTimerFragment} from 'src/graphql/generated/graphql'; +import {useAIStatusUpdateClicked} from 'src/ai_integration'; const ID = 'playbooks_update_run_status_dialog'; const NAMES_ON_TOOLTIP = 5; +const Textbox = window.Components.Textbox; + type Props = { playbookRunId: string; channelId: string; @@ -102,6 +110,10 @@ const UpdateRunStatusModal = ({ const dispatch = useDispatch(); const {formatMessage, formatList} = useIntl(); const currentUserId = useSelector(getCurrentUserId); + const [aiModalOpen, setAIModalOpen] = useState(false); + const [generating, setGenerating] = useState(false); + const aiAvailable = useAIAvailable(); + const aiStatusUpdateClicked = useAIStatusUpdateClicked(); const {data} = useQuery(runStatusModalQueryDocument, { variables: { runID: playbookRunId, @@ -261,6 +273,24 @@ const UpdateRunStatusModal = ({ + { aiAvailable && + { + setAIModalOpen(true); + setGenerating(true); + }}> + + + + } + { aiAvailable && aiModalOpen && + + setGenerating(generating)} + /> + + } { const client = getPlaybooksGraphQLClient(); return ; diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 6f9c7e21ce..48ae9d38b4 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -54,6 +54,7 @@ import { WEBSOCKET_PLAYBOOK_RESTORED, WEBSOCKET_PLAYBOOK_RUN_CREATED, WEBSOCKET_PLAYBOOK_RUN_UPDATED, + WEBSOCKET_MATTERMOST_AI_POSTUPDATE, } from 'src/types/websocket_events'; import { fetchGlobalSettings, @@ -73,6 +74,7 @@ import {setPlaybooksGraphQLClient} from './graphql_client'; import {RHSTitlePlaceholder} from './rhs_title_remote_render'; import {ApolloWrapper, makeGraphqlClient} from './graphql/apollo'; import PresetTemplates from './components/templates/template_data'; +import postEventListener from './websocket'; const GlobalHeaderCenter = () => { return null; @@ -239,6 +241,7 @@ export default class Plugin { registry.registerWebSocketEventHandler(WebsocketEvents.POST_DELETED, handleWebsocketPostEditedOrDeleted(store.getState, store.dispatch)); registry.registerWebSocketEventHandler(WebsocketEvents.POST_EDITED, handleWebsocketPostEditedOrDeleted(store.getState, store.dispatch)); registry.registerWebSocketEventHandler(WebsocketEvents.CHANNEL_UPDATED, handleWebsocketChannelUpdated(store.getState, store.dispatch)); + registry.registerWebSocketEventHandler(WEBSOCKET_MATTERMOST_AI_POSTUPDATE, postEventListener.handlePostUpdateWebsockets); // Local slash commands registry.registerSlashCommandWillBePostedHook(makeSlashCommandHook(store)); diff --git a/webapp/src/manifest.js b/webapp/src/manifest.js new file mode 100644 index 0000000000..19ec8b232a --- /dev/null +++ b/webapp/src/manifest.js @@ -0,0 +1,47 @@ +// This file is automatically generated. Do not modify it manually. + +const manifest = JSON.parse(` +{ + "id": "playbooks", + "name": "Playbooks", + "description": "Mattermost Playbooks enable reliable and repeatable processes for your teams using checklists, automation, and retrospectives.", + "homepage_url": "https://github.com/mattermost/mattermost-plugin-playbooks/", + "support_url": "https://github.com/mattermost/mattermost-plugin-playbooks/issues", + "release_notes_url": "https://github.com/mattermost/mattermost-plugin-playbooks/releases/tag/v1.33.0+alpha.4", + "icon_path": "assets/plugin_icon.svg", + "version": "1.33.0+alpha.4+29103d5b", + "min_server_version": "6.3.0", + "server": { + "executables": { + "darwin-amd64": "server/dist/plugin-darwin-amd64", + "darwin-arm64": "server/dist/plugin-darwin-arm64", + "linux-amd64": "server/dist/plugin-linux-amd64", + "linux-arm64": "server/dist/plugin-linux-arm64", + "windows-amd64": "server/dist/plugin-windows-amd64.exe" + }, + "executable": "" + }, + "webapp": { + "bundle_path": "webapp/dist/main.js" + }, + "settings_schema": { + "header": "", + "footer": "", + "settings": [ + { + "key": "EnableExperimentalFeatures", + "display_name": "Enable Experimental Features:", + "type": "bool", + "help_text": "Enable experimental features that come with in-progress UI, bugs, and cool stuff.", + "placeholder": "", + "default": null + } + ] + } +} +`); + +export default manifest; +export const id = manifest.id; +export const version = manifest.version; +export const pluginId = manifest.id; diff --git a/webapp/src/types/websocket_events.ts b/webapp/src/types/websocket_events.ts index 9c51582ad8..e3b0cb5702 100644 --- a/webapp/src/types/websocket_events.ts +++ b/webapp/src/types/websocket_events.ts @@ -8,3 +8,4 @@ export const WEBSOCKET_PLAYBOOK_RUN_CREATED = `custom_${manifest.id}_playbook_ru export const WEBSOCKET_PLAYBOOK_CREATED = `custom_${manifest.id}_playbook_created`; export const WEBSOCKET_PLAYBOOK_ARCHIVED = `custom_${manifest.id}_playbook_archived`; export const WEBSOCKET_PLAYBOOK_RESTORED = `custom_${manifest.id}_playbook_restored`; +export const WEBSOCKET_MATTERMOST_AI_POSTUPDATE = `custom_mattermost-ai_postupdate`; diff --git a/webapp/src/websocket.tsx b/webapp/src/websocket.tsx new file mode 100644 index 0000000000..20e33ebaf2 --- /dev/null +++ b/webapp/src/websocket.tsx @@ -0,0 +1,30 @@ +import {WebSocketMessage} from '@mattermost/client'; + +export interface PostUpdateWebsocketMessage { + next: string + post_id: string + control?: string +} + +type WebsocketListener = (msg: WebSocketMessage) => void +type WebsocketListeners = Map + +class PostEventListener { + postUpdateWebsocketListeners: WebsocketListeners = new Map(); + + public registerPostUpdateListener = (postID: string, listener: WebsocketListener) => { + this.postUpdateWebsocketListeners.set(postID, listener); + }; + + public unregisterPostUpdateListener = (postID: string) => { + this.postUpdateWebsocketListeners.delete(postID); + }; + + public handlePostUpdateWebsockets = (msg: WebSocketMessage) => { + const postID = msg.data.post_id; + this.postUpdateWebsocketListeners.get(postID)?.(msg); + }; +} + +const postEventListener = new PostEventListener() +export default postEventListener; From 2251a92b0a1a670429da27d2c961fc5f0da0e313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 11 Dec 2024 13:01:01 +0100 Subject: [PATCH 03/25] Improving the interface --- webapp/package-lock.json | 189 ++++++------ webapp/package.json | 2 +- webapp/src/ai_integration.ts | 14 + webapp/src/ai_modal.tsx | 279 +++++++++++++++--- webapp/src/client.ts | 5 +- .../modals/update_run_status_modal.tsx | 48 ++- .../components/rhs/rhs_post_update_button.tsx | 16 - 7 files changed, 384 insertions(+), 169 deletions(-) diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 6f935f380e..ea792f2a00 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@apollo/client": "3.7.3", "@floating-ui/react-dom-interactions": "0.6.3", - "@mattermost/compass-icons": "0.1.32", + "@mattermost/compass-icons": "0.1.47", "@mattermost/types": "7.1.0", "@mdi/js": "^6.5.95", "@mdi/react": "1.5.0", @@ -424,7 +424,6 @@ "node_modules/@babel/compat-data": { "version": "7.16.4", "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -432,7 +431,6 @@ "node_modules/@babel/core": { "version": "7.16.7", "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.7", @@ -496,7 +494,6 @@ "node_modules/@babel/helper-compilation-targets": { "version": "7.16.7", "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.16.4", "@babel/helper-validator-option": "^7.16.7", @@ -640,7 +637,6 @@ "node_modules/@babel/helper-module-transforms": { "version": "7.16.7", "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", @@ -705,7 +701,6 @@ "node_modules/@babel/helper-simple-access": { "version": "7.16.7", "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, "dependencies": { "@babel/types": "^7.16.7" }, @@ -753,7 +748,6 @@ "node_modules/@babel/helper-validator-option": { "version": "7.16.7", "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -775,7 +769,6 @@ "node_modules/@babel/helpers": { "version": "7.16.7", "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", - "dev": true, "dependencies": { "@babel/template": "^7.16.7", "@babel/traverse": "^7.16.7", @@ -5130,9 +5123,9 @@ } }, "node_modules/@mattermost/compass-icons": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.32.tgz", - "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==" + "version": "0.1.47", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.47.tgz", + "integrity": "sha512-vI4j1m/B9qQu51g5YnMQBft8gDh/ZWp3JHBgZXJwb522uY2o6WjwOpCbFwdJxWnD2uq/N2D9wDNmdutJNddf+w==" }, "node_modules/@mattermost/types": { "version": "7.1.0", @@ -5692,8 +5685,7 @@ }, "node_modules/@types/node": { "version": "14.18.5", - "integrity": "sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==", - "dev": true + "integrity": "sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -7611,7 +7603,6 @@ "node_modules/browserslist": { "version": "4.19.1", "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, "dependencies": { "caniuse-lite": "^1.0.30001286", "electron-to-chromium": "^1.4.17", @@ -7744,7 +7735,6 @@ "node_modules/caniuse-lite": { "version": "1.0.30001296", "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/browserslist" @@ -8992,8 +8982,7 @@ }, "node_modules/electron-to-chromium": { "version": "1.4.37", - "integrity": "sha512-XIvFB1omSAxYgHYX48sC+HR8i/p7lx7R+0cX9faElg1g++h9IilCrJ12+bQuY+d96Wp7zkBiJwMOv+AhLtLrTg==", - "dev": true + "integrity": "sha512-XIvFB1omSAxYgHYX48sC+HR8i/p7lx7R+0cX9faElg1g++h9IilCrJ12+bQuY+d96Wp7zkBiJwMOv+AhLtLrTg==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -9146,7 +9135,6 @@ "node_modules/escalade": { "version": "3.1.1", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -10541,7 +10529,6 @@ "node_modules/gensync": { "version": "1.0.0-beta.2", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -10795,7 +10782,7 @@ "version": "5.11.2", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.2.tgz", "integrity": "sha512-4EiZ3/UXYcjm+xFGP544/yW1+DVI8ZpKASFbzrV5EDTFWJp0ZvLl4Dy2fSZAzz9imKp5pZMIcjB0x/H69Pv/6w==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -13855,7 +13842,6 @@ "node_modules/json5": { "version": "2.2.0", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -15434,8 +15420,7 @@ }, "node_modules/node-releases": { "version": "2.0.1", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -16017,8 +16002,7 @@ }, "node_modules/picocolors": { "version": "1.0.0", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -17635,7 +17619,6 @@ "node_modules/semver": { "version": "6.3.0", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -20255,13 +20238,11 @@ }, "@babel/compat-data": { "version": "7.16.4", - "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", - "dev": true + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==" }, "@babel/core": { "version": "7.16.7", "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", - "dev": true, "requires": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.7", @@ -20309,7 +20290,6 @@ "@babel/helper-compilation-targets": { "version": "7.16.7", "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", - "dev": true, "requires": { "@babel/compat-data": "^7.16.4", "@babel/helper-validator-option": "^7.16.7", @@ -20411,7 +20391,6 @@ "@babel/helper-module-transforms": { "version": "7.16.7", "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", - "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", @@ -20461,7 +20440,6 @@ "@babel/helper-simple-access": { "version": "7.16.7", "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", - "dev": true, "requires": { "@babel/types": "^7.16.7" } @@ -20493,8 +20471,7 @@ }, "@babel/helper-validator-option": { "version": "7.16.7", - "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", - "dev": true + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==" }, "@babel/helper-wrap-function": { "version": "7.16.7", @@ -20510,7 +20487,6 @@ "@babel/helpers": { "version": "7.16.7", "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", - "dev": true, "requires": { "@babel/template": "^7.16.7", "@babel/traverse": "^7.16.7", @@ -22515,7 +22491,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", - "dev": true + "dev": true, + "requires": {} }, "has-flag": { "version": "4.0.0", @@ -22566,7 +22543,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true + "dev": true, + "requires": {} }, "yargs": { "version": "17.3.1", @@ -22869,7 +22847,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -22942,7 +22921,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -23169,7 +23149,8 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -23208,7 +23189,8 @@ }, "@graphql-typed-document-node/core": { "version": "3.1.1", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", + "requires": {} }, "@guyplusplus/turndown-plugin-gfm": { "version": "1.0.7", @@ -23249,7 +23231,8 @@ }, "@icons/material": { "version": "0.2.4", - "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==" + "integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==", + "requires": {} }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -23798,14 +23781,15 @@ } }, "@mattermost/compass-icons": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.32.tgz", - "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==" + "version": "0.1.47", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.47.tgz", + "integrity": "sha512-vI4j1m/B9qQu51g5YnMQBft8gDh/ZWp3JHBgZXJwb522uY2o6WjwOpCbFwdJxWnD2uq/N2D9wDNmdutJNddf+w==" }, "@mattermost/types": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@mattermost/types/-/types-7.1.0.tgz", - "integrity": "sha512-SVw5oDXwflNdXUWzl3QuTT5npEddglq6bB+kqdHjowPopOv1Fxi4zJh08/LvaRJYQENEk1QnEVIRTNhnoV0/tg==" + "integrity": "sha512-SVw5oDXwflNdXUWzl3QuTT5npEddglq6bB+kqdHjowPopOv1Fxi4zJh08/LvaRJYQENEk1QnEVIRTNhnoV0/tg==", + "requires": {} }, "@mdi/js": { "version": "6.5.95", @@ -23907,7 +23891,8 @@ "@restart/context": { "version": "2.1.4", "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "dev": true + "dev": true, + "requires": {} }, "@restart/hooks": { "version": "0.3.27", @@ -24261,8 +24246,7 @@ }, "@types/node": { "version": "14.18.5", - "integrity": "sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==", - "dev": true + "integrity": "sha512-LMy+vDDcQR48EZdEx5wRX1q/sEl6NdGuHXPnfeL8ixkwCOSZ2qnIyIZmcCbdX0MeRqHhAcHmX+haCbrS8Run+A==" }, "@types/parse-json": { "version": "4.0.0", @@ -24908,7 +24892,8 @@ "@webpack-cli/configtest": { "version": "1.1.0", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.4.0", @@ -24921,7 +24906,8 @@ "@webpack-cli/serve": { "version": "1.6.0", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true + "dev": true, + "requires": {} }, "@whatwg-node/fetch": { "version": "0.5.4", @@ -25022,13 +25008,15 @@ "acorn-import-assertions": { "version": "1.8.0", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -25103,7 +25091,8 @@ "ajv-keywords": { "version": "3.5.2", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "ansi-escapes": { "version": "4.3.2", @@ -25816,7 +25805,6 @@ "browserslist": { "version": "4.19.1", "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001286", "electron-to-chromium": "^1.4.17", @@ -25909,8 +25897,7 @@ }, "caniuse-lite": { "version": "1.0.30001296", - "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==", - "dev": true + "integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==" }, "capital-case": { "version": "1.0.4", @@ -25971,7 +25958,8 @@ "chartjs-plugin-annotation": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz", - "integrity": "sha512-kmEp2WtpogwnKKnDPO3iO3mVwvVGtmG5BkZVtAEZm5YzJ9CYxojjYEgk7OTrFbJ5vU098b84UeJRe8kRfNcq5g==" + "integrity": "sha512-kmEp2WtpogwnKKnDPO3iO3mVwvVGtmG5BkZVtAEZm5YzJ9CYxojjYEgk7OTrFbJ5vU098b84UeJRe8kRfNcq5g==", + "requires": {} }, "chokidar": { "version": "3.5.2", @@ -26899,8 +26887,7 @@ }, "electron-to-chromium": { "version": "1.4.37", - "integrity": "sha512-XIvFB1omSAxYgHYX48sC+HR8i/p7lx7R+0cX9faElg1g++h9IilCrJ12+bQuY+d96Wp7zkBiJwMOv+AhLtLrTg==", - "dev": true + "integrity": "sha512-XIvFB1omSAxYgHYX48sC+HR8i/p7lx7R+0cX9faElg1g++h9IilCrJ12+bQuY+d96Wp7zkBiJwMOv+AhLtLrTg==" }, "elliptic": { "version": "6.5.4", @@ -27021,8 +27008,7 @@ }, "escalade": { "version": "3.1.1", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -27514,7 +27500,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.0.tgz", "integrity": "sha512-8rokf6NvxC10ugA1VNmzEIO75CzId7IDF3Ai2GNXl0Xr4VORpb8u+bxsjRuE+2BS8MfDbrK/MHUQZI2G9qQyyA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-no-relative-import-paths": { "version": "1.5.0", @@ -27565,7 +27552,8 @@ "eslint-plugin-react-hooks": { "version": "4.3.0", "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-unused-imports": { "version": "2.0.0", @@ -28069,8 +28057,7 @@ }, "gensync": { "version": "1.0.0-beta.2", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "2.0.5", @@ -28217,7 +28204,8 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.2.0.tgz", "integrity": "sha512-NkANeMnaHrlaSSlpKGyvn2R4rqUDeE/9E5YHx+b4nwo0R8dZyAqcih8/gxpCZvqWP9Vf6xuLpMSzSgdVEIM78g==", - "dev": true + "dev": true, + "requires": {} }, "minimatch": { "version": "4.2.1", @@ -28253,7 +28241,8 @@ "version": "5.11.2", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.2.tgz", "integrity": "sha512-4EiZ3/UXYcjm+xFGP544/yW1+DVI8ZpKASFbzrV5EDTFWJp0ZvLl4Dy2fSZAzz9imKp5pZMIcjB0x/H69Pv/6w==", - "dev": true + "devOptional": true, + "requires": {} }, "handle-thing": { "version": "2.0.1", @@ -28574,7 +28563,8 @@ "icss-utils": { "version": "5.1.0", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "identity-obj-proxy": { "version": "3.0.0", @@ -29080,7 +29070,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "dev": true + "dev": true, + "requires": {} }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -29815,7 +29806,8 @@ "jest-pnp-resolver": { "version": "1.2.2", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.4.0", @@ -30476,7 +30468,6 @@ "json5": { "version": "2.2.0", "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -31377,7 +31368,8 @@ "redux-thunk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", - "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==" + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "requires": {} }, "sass": { "version": "1.56.1", @@ -31483,7 +31475,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", - "dev": true + "dev": true, + "requires": {} }, "methods": { "version": "1.1.2", @@ -31707,8 +31700,7 @@ }, "node-releases": { "version": "2.0.1", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==" }, "normalize-path": { "version": "3.0.0", @@ -32133,8 +32125,7 @@ }, "picocolors": { "version": "1.0.0", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "picomatch": { "version": "2.3.1", @@ -32234,7 +32225,8 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -32537,7 +32529,8 @@ "react-chartjs-2": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz", - "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==" + "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==", + "requires": {} }, "react-color": { "version": "2.19.3", @@ -32896,7 +32889,8 @@ }, "react-universal-interface": { "version": "0.6.2", - "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==" + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "requires": {} }, "react-use": { "version": "17.3.2", @@ -32927,7 +32921,8 @@ "react-virtualized-auto-sizer": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", - "integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==" + "integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==", + "requires": {} }, "react-window": { "version": "1.8.8", @@ -32941,7 +32936,8 @@ "react-window-infinite-loader": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/react-window-infinite-loader/-/react-window-infinite-loader-1.0.8.tgz", - "integrity": "sha512-907ZLAiZZfBHuZyiY0V7uiSL4P/rI6UQyCF9wES1cDWTeyNLgGLaxu+BZkcUW3R5tSCQcbCcWBl0jVIpYzrKGQ==" + "integrity": "sha512-907ZLAiZZfBHuZyiY0V7uiSL4P/rI6UQyCF9wES1cDWTeyNLgGLaxu+BZkcUW3R5tSCQcbCcWBl0jVIpYzrKGQ==", + "requires": {} }, "reactcss": { "version": "1.2.3", @@ -32987,7 +32983,8 @@ }, "redux-batched-actions": { "version": "0.5.0", - "integrity": "sha512-6orZWyCnIQXMGY4DUGM0oj0L7oYnwTACsfsru/J7r94RM3P9eS7SORGpr3LCeRCMoIMQcpfKZ7X4NdyFHBS8Eg==" + "integrity": "sha512-6orZWyCnIQXMGY4DUGM0oj0L7oYnwTACsfsru/J7r94RM3P9eS7SORGpr3LCeRCMoIMQcpfKZ7X4NdyFHBS8Eg==", + "requires": {} }, "redux-mock-store": { "version": "1.5.4", @@ -32999,12 +32996,14 @@ }, "redux-persist": { "version": "6.0.0", - "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==" + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "requires": {} }, "redux-thunk": { "version": "2.4.1", "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "dev": true + "dev": true, + "requires": {} }, "regenerate": { "version": "1.4.2", @@ -33326,8 +33325,7 @@ }, "semver": { "version": "6.3.0", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "send": { "version": "0.17.2", @@ -33866,7 +33864,8 @@ "style-loader": { "version": "3.3.1", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true + "dev": true, + "requires": {} }, "styled-components": { "version": "5.3.3", @@ -34467,11 +34466,13 @@ "use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} }, "use-memo-one": { "version": "1.1.2", - "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", + "requires": {} }, "util-deprecate": { "version": "1.0.2", @@ -34874,7 +34875,8 @@ "ws": { "version": "8.4.0", "integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -35024,7 +35026,8 @@ "ws": { "version": "7.5.6", "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", - "dev": true + "dev": true, + "requires": {} }, "xml": { "version": "1.0.1", diff --git a/webapp/package.json b/webapp/package.json index cf0a2a63d6..191a2a0254 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -7,7 +7,7 @@ "dependencies": { "@apollo/client": "3.7.3", "@floating-ui/react-dom-interactions": "0.6.3", - "@mattermost/compass-icons": "0.1.32", + "@mattermost/compass-icons": "0.1.47", "@mattermost/types": "7.1.0", "@mdi/js": "^6.5.95", "@mdi/react": "1.5.0", diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 0e52b39552..3aea200f35 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -20,3 +20,17 @@ export const useAIStatusUpdateClicked = () => { }); }; +export const useAIAvailableBots = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.bots; + }); +}; + +export const useBotSelector = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.botSelector; + }); +}; + diff --git a/webapp/src/ai_modal.tsx b/webapp/src/ai_modal.tsx index c4f7136c19..888fd07cae 100644 --- a/webapp/src/ai_modal.tsx +++ b/webapp/src/ai_modal.tsx @@ -1,96 +1,291 @@ import {WebSocketMessage} from '@mattermost/client'; -import React, {ChangeEvent, useEffect, useState, useCallback} from 'react'; +import {useSelector} from 'react-redux'; +import {Client4} from 'mattermost-redux/client'; +import React, {useEffect, useState, useCallback, useRef} from 'react'; import styled from 'styled-components'; import {FormattedMessage, useIntl} from 'react-intl'; +import IconAI from 'src/components/assets/icons/ai'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {generateStatusUpdate} from './client'; import postEventListener, {PostUpdateWebsocketMessage} from './websocket'; -const Textbox = window.Components.Textbox; +const UserAvatar = window.Components.Avatar; type Props = { playbookRunId: string generating: boolean + currentBot: any onGeneratingChanged: (generating: boolean) => void + onAccept: (text: string) => void } -const AIModal = ({generating, playbookRunId, onGeneratingChanged}: Props) => { +const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, currentBot}: Props) => { const intl = useIntl(); + const currentUser = useSelector(getCurrentUser); + const [prevMessages, setPrevMessages] = useState([]); const [update, setUpdate] = useState(''); + const [instructions, setInstructions] = useState([]); + const [instruction, setInstruction] = useState(''); + const suggestionBox = useRef() useEffect(() => { - postEventListener.registerPostUpdateListener('playbooks_post_update', (msg: WebSocketMessage) => { - const data = msg.data; - if (!data.control) { + generateStatusUpdate(playbookRunId, instructions); + }, []); + + useEffect(() => { + if (generating) { + postEventListener.registerPostUpdateListener('playbooks_post_update', (msg: WebSocketMessage) => { + const data = msg.data; + if (!data.control) { onGeneratingChanged(true); setUpdate(data.next); - } else if (data.control === 'end') { + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + } else if (data.control === 'end') { onGeneratingChanged(false); - } - }); - - generateStatusUpdate(playbookRunId); - + } + }); + } return () => { postEventListener.unregisterPostUpdateListener('playbooks_post_update'); }; - }, []); + }, [generating]); const regenerate = useCallback(() => { - generateStatusUpdate(playbookRunId); - }, [playbookRunId]); + setUpdate('') + onGeneratingChanged(true); + generateStatusUpdate(playbookRunId, instructions); + }, [playbookRunId, instructions]); const copyText = useCallback(() => { navigator.clipboard.writeText(update); }, [update]) + const onInputEnter = useCallback((e: React.KeyboardEvent) => { + // Detect hitting enter and run the generateStatusUpdate + if (e.key === 'Enter') { + setPrevMessages([...prevMessages, update]) + setUpdate('') + generateStatusUpdate(playbookRunId, [...instructions, instruction]); + setInstructions([...instructions, instruction]) + setInstruction('') + onGeneratingChanged(true); + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + } + }, [instructions, instruction, playbookRunId, prevMessages, update]) + + const stopGenerating = useCallback(() => { + onGeneratingChanged(false); + }, []) + return ( - ) => setUpdate(e.target.value)} - characterLimit={10000} - createMessage={''} - onKeyPress={() => true} - openWhenEmpty={true} - channelId={''} - disabled={false} - /> + + {prevMessages.map((msg, idx) => ( + <> + + + {currentBot.displayName} + + {msg} + + + + + + + {instructions[idx]} + + ))} + + + + {currentBot.displayName} + + {update} + + {generating && - + } {!generating && - + } {!generating && - + + + } {!generating && - + + + } - + + + setInstruction(e.target.value)} + value={instruction} + onKeyUp={onInputEnter} + /> + ); }; +const IconButton = styled.span` + display: inline-block; + margin: 12px 0px; + border-radius: 4px; + background: transparent; + text-decoration: none; + color: var(--center-channel-color-64); + padding: 8px; + cursor: pointer; +` + +const StopGeneratingButton = styled.button` + display: inline-block; + margin: 12 0px; + border-radius: 4px; + padding: 8px 16px; + display: inline-block; + margin: 12px 0px; + border-radius: 4px; + background: var(--center-channel-color-08); + padding: 8px 16px 8px 8px; + color: var(--center-channel-color-64); + cursor: pointer; + text-decoration: none; + font-weight: 600; + text-align: center; + border: 0px; + &:hover { + background: var(--center-channel-color-16); + } + &:active { + background: var(--center-channel-color-24); + } + &:focus { + outline: none; + } +` + const AIModalContainer = styled.div` - width: 110%; + width: 580px; + left: -23px; + top: -2px; position: absolute; z-index: 1000; + background: var(--center-channel-bg); + border: 1px solid var(--center-channel-color-16); + border-radius: 4px; + padding: 10px; + + &&&& textarea { + min-height: 220px; + max-height: 220px; + border: 0; + outline: 0; + box-shadow: none; + &:focus, &:hover, &:active { + border: 0; + outline: 0; + } + } + .autosize_textarea_placeholder { + display: none; + } `; +const ExtraInstructionsInput = styled.div` + display: flex; + width: 100%; + padding: 10px; + border-radius: 4px; + border: 1px solid var(--center-channel-color-16); + align-items: center; + &:focus { + border: 1px solid var(--center-channel-color-24); + } + input { + border: 0px; + width: 100%; + margin-left: 8px; + } + svg { + color: var(--center-channel-color-64); + } +` + +const InsertButton = styled.button` + display: inline-block; + margin: 12px 0px; + border-radius: 4px; + background: var(--button-bg-08); + padding: 8px 16px 8px 8px; + color: var(--button-bg); + cursor: pointer; + text-decoration: none; + font-weight: 600; + text-align: center; + border: 0; + &:hover { + background: var(--button-bg-16); + } + &:active { + background: var(--button-bg-24); + } + &:focus { + outline: none; + } +` + +const Assistant = styled.div` + display: flex; + .Avatar { + width: 50px; + min-width: 50px; + height: 50px; + margin-right: 14px; + } +` + +const Username = styled.span` + font-weight: 600; +` + +const Messages = styled.div` + margin-top: -24px; + white-space: pre-wrap; + padding-left: 64px; + margin-bottom: 16px; +` + +const AssistantMessageBox = styled.div` + max-height: 200px; + height: 200px; + overflow-y: auto; +` + export default AIModal; diff --git a/webapp/src/client.ts b/webapp/src/client.ts index 3931845744..dd9da95de9 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -817,10 +817,11 @@ export async function getTeamTopPlaybooks(timeRange: string, page: number, perPa } -export async function generateStatusUpdate(playbookRunID: string) { +export async function generateStatusUpdate(playbookRunID: string, instructions: string[]) { const url = `${playbookRunRoute(playbookRunID)}/generate_status`; const response = await fetch(url, Client4.getOptions({ - method: 'GET', + method: 'POST', + body: JSON.stringify(instructions || []) })); if (response.ok) { diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index c71721a8c8..8abd6f38ce 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -25,7 +25,7 @@ import GenericModal, {Description, Label} from 'src/components/widgets/generic_m import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; import IconAI from 'src/components/assets/icons/ai'; import AIModal from 'src/ai_modal' -import {useAIAvailable} from 'src/ai_integration'; +import {useAIAvailable, useAIAvailableBots, useBotSelector} from 'src/ai_integration'; import { Mode, @@ -61,8 +61,6 @@ import {useAIStatusUpdateClicked} from 'src/ai_integration'; const ID = 'playbooks_update_run_status_dialog'; const NAMES_ON_TOOLTIP = 5; -const Textbox = window.Components.Textbox; - type Props = { playbookRunId: string; channelId: string; @@ -109,10 +107,13 @@ const UpdateRunStatusModal = ({ }: Props) => { const dispatch = useDispatch(); const {formatMessage, formatList} = useIntl(); + const [currentBot, setCurrentBot] = useState(null); const currentUserId = useSelector(getCurrentUserId); const [aiModalOpen, setAIModalOpen] = useState(false); const [generating, setGenerating] = useState(false); const aiAvailable = useAIAvailable(); + const aiAvailableBots = useAIAvailableBots(); + const BotSelector = useBotSelector() as any; const aiStatusUpdateClicked = useAIStatusUpdateClicked(); const {data} = useQuery(runStatusModalQueryDocument, { variables: { @@ -270,24 +271,32 @@ const UpdateRunStatusModal = ({ const form = ( {description()} - - { aiAvailable && - { - setAIModalOpen(true); - setGenerating(true); - }}> - - - - } + + + { aiAvailable && + { + if (!aiModalOpen) { + setCurrentBot(bot) + setAIModalOpen(true); + setGenerating(true); + } + }} + /> + } + { aiAvailable && aiModalOpen && setGenerating(generating)} + onAccept={(text) => { setMessage(text); setAIModalOpen(false); }} /> } @@ -548,6 +557,15 @@ const AiModalContainer = styled.div` position: relative; ` +const LastChangeSince = styled.div` + display: flex; + justify-content: space-between; + button { + height: 26px; + margin-top: 20px; + } +` + const ApolloWrappedModal = (props: Props) => { const client = getPlaybooksGraphQLClient(); return ; diff --git a/webapp/src/components/rhs/rhs_post_update_button.tsx b/webapp/src/components/rhs/rhs_post_update_button.tsx index dfab9c9718..6c089c67a8 100644 --- a/webapp/src/components/rhs/rhs_post_update_button.tsx +++ b/webapp/src/components/rhs/rhs_post_update_button.tsx @@ -42,22 +42,6 @@ const RHSPostUpdateButton = (props: Props) => { > - { aiAvailable && - - - - - - - - } ); }; From 07238c3a3bb48c275698e4e3f53f459107b6d245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 11 Dec 2024 13:42:41 +0100 Subject: [PATCH 04/25] Improving history of chat and bot selection --- webapp/src/ai_modal.tsx | 23 ++++++++++++++++++----- webapp/src/client.ts | 8 ++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/webapp/src/ai_modal.tsx b/webapp/src/ai_modal.tsx index 888fd07cae..38544b607d 100644 --- a/webapp/src/ai_modal.tsx +++ b/webapp/src/ai_modal.tsx @@ -24,6 +24,7 @@ type Props = { const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, currentBot}: Props) => { const intl = useIntl(); const currentUser = useSelector(getCurrentUser); + const [copied, setCopied] = useState(false); const [prevMessages, setPrevMessages] = useState([]); const [update, setUpdate] = useState(''); const [instructions, setInstructions] = useState([]); @@ -31,7 +32,7 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr const suggestionBox = useRef() useEffect(() => { - generateStatusUpdate(playbookRunId, instructions); + generateStatusUpdate(playbookRunId, currentBot.id, []); }, []); useEffect(() => { @@ -55,11 +56,13 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr const regenerate = useCallback(() => { setUpdate('') onGeneratingChanged(true); - generateStatusUpdate(playbookRunId, instructions); - }, [playbookRunId, instructions]); + generateStatusUpdate(playbookRunId, currentBot.id, instructions, [...prevMessages, update]); + }, [playbookRunId, instructions, prevMessages, update, currentBot.id]); const copyText = useCallback(() => { navigator.clipboard.writeText(update); + setCopied(true); + setTimeout(() => setCopied(false), 1000); }, [update]) const onInputEnter = useCallback((e: React.KeyboardEvent) => { @@ -67,13 +70,13 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr if (e.key === 'Enter') { setPrevMessages([...prevMessages, update]) setUpdate('') - generateStatusUpdate(playbookRunId, [...instructions, instruction]); + generateStatusUpdate(playbookRunId, currentBot.id, [...instructions, instruction], [...prevMessages, update]); setInstructions([...instructions, instruction]) setInstruction('') onGeneratingChanged(true); setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) } - }, [instructions, instruction, playbookRunId, prevMessages, update]) + }, [instructions, instruction, playbookRunId, prevMessages, update, currentBot.id]) const stopGenerating = useCallback(() => { onGeneratingChanged(false); @@ -140,6 +143,9 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr } + + + ` + color: var(--center-channel-color-64); + margin: 12px 0px; + transition: opacity 1s; + opacity: ${(props) => (props.copied ? 1 : 0)}; +` + export default AIModal; diff --git a/webapp/src/client.ts b/webapp/src/client.ts index dd9da95de9..275bfd2a27 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -817,11 +817,15 @@ export async function getTeamTopPlaybooks(timeRange: string, page: number, perPa } -export async function generateStatusUpdate(playbookRunID: string, instructions: string[]) { +export async function generateStatusUpdate(playbookRunID: string, botId: string, instructions: string[], messages: string[]) { const url = `${playbookRunRoute(playbookRunID)}/generate_status`; const response = await fetch(url, Client4.getOptions({ method: 'POST', - body: JSON.stringify(instructions || []) + body: JSON.stringify({ + instructions, + messages, + bot: botId, + }) })); if (response.ok) { From d3c8292aa640d79c70eaa09b6497dea24a8d6134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 11 Dec 2024 16:22:46 +0100 Subject: [PATCH 05/25] Moving ai_modal to the right place in the directories structure --- webapp/src/{ => components/modals}/ai_modal.tsx | 0 webapp/src/components/modals/update_run_status_modal.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename webapp/src/{ => components/modals}/ai_modal.tsx (100%) diff --git a/webapp/src/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx similarity index 100% rename from webapp/src/ai_modal.tsx rename to webapp/src/components/modals/ai_modal.tsx diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index 8abd6f38ce..dc36eab8ec 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -24,7 +24,7 @@ import {ApolloProvider, useQuery} from '@apollo/client'; import GenericModal, {Description, Label} from 'src/components/widgets/generic_modal'; import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; import IconAI from 'src/components/assets/icons/ai'; -import AIModal from 'src/ai_modal' +import AIModal from 'src/components/modals/ai_modal'; import {useAIAvailable, useAIAvailableBots, useBotSelector} from 'src/ai_integration'; import { From 7250eb29997caba49360b9c7c36cb222f6469b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 18 Dec 2024 13:35:52 +0100 Subject: [PATCH 06/25] Applying required updates on the UX --- webapp/src/components/modals/ai_modal.tsx | 214 +++++++++--------- .../modals/update_run_status_modal.tsx | 38 ++-- 2 files changed, 124 insertions(+), 128 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 38544b607d..77b0684e0f 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,38 +1,46 @@ import {WebSocketMessage} from '@mattermost/client'; -import {useSelector} from 'react-redux'; -import {Client4} from 'mattermost-redux/client'; import React, {useEffect, useState, useCallback, useRef} from 'react'; import styled from 'styled-components'; import {FormattedMessage, useIntl} from 'react-intl'; import IconAI from 'src/components/assets/icons/ai'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {Textbox} from 'src/webapp_globals'; -import {generateStatusUpdate} from './client'; +import {generateStatusUpdate} from 'src/client'; +import {useAIAvailableBots, useBotSelector} from 'src/ai_integration'; -import postEventListener, {PostUpdateWebsocketMessage} from './websocket'; +import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; -const UserAvatar = window.Components.Avatar; +type Version = { + instruction: string + prevValue: string + value: string +} type Props = { playbookRunId: string - generating: boolean - currentBot: any - onGeneratingChanged: (generating: boolean) => void onAccept: (text: string) => void + onClose: () => void } -const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, currentBot}: Props) => { +const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { const intl = useIntl(); - const currentUser = useSelector(getCurrentUser); const [copied, setCopied] = useState(false); - const [prevMessages, setPrevMessages] = useState([]); - const [update, setUpdate] = useState(''); - const [instructions, setInstructions] = useState([]); const [instruction, setInstruction] = useState(''); const suggestionBox = useRef() + const aiAvailableBots = useAIAvailableBots(); + const BotSelector = useBotSelector() as any; + const [currentBot, setCurrentBot] = useState(aiAvailableBots.length > 0 ? aiAvailableBots[0] : null); + const [currentVersion, setCurrentVersion] = useState(1); + const [versions, setVersions] = useState([]); + const [generating, setGenerating] = useState(null); useEffect(() => { - generateStatusUpdate(playbookRunId, currentBot.id, []); + if (currentBot?.id) { + setCurrentVersion(versions.length + 1) + setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) + setGenerating(true); + generateStatusUpdate(playbookRunId, currentBot.id, [], []); + } }, []); useEffect(() => { @@ -40,11 +48,13 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr postEventListener.registerPostUpdateListener('playbooks_post_update', (msg: WebSocketMessage) => { const data = msg.data; if (!data.control) { - onGeneratingChanged(true); - setUpdate(data.next); + setGenerating(true); + const newVersions = [...versions] + newVersions[versions.length-1] = {...newVersions[versions.length-1], value: data.next} + setVersions(newVersions) setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) } else if (data.control === 'end') { - onGeneratingChanged(false); + setGenerating(false); } }); } @@ -53,72 +63,63 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr }; }, [generating]); + const onBotChange = useCallback((bot: any) => { + setCurrentBot(bot) + setCurrentVersion(versions.length + 1) + setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) + setGenerating(true); + generateStatusUpdate(playbookRunId, bot.id, [], []); + }, [versions, playbookRunId]) + const regenerate = useCallback(() => { - setUpdate('') - onGeneratingChanged(true); - generateStatusUpdate(playbookRunId, currentBot.id, instructions, [...prevMessages, update]); - }, [playbookRunId, instructions, prevMessages, update, currentBot.id]); + setGenerating(true); + generateStatusUpdate(playbookRunId, currentBot?.id, [versions[currentVersion-1].instruction], [versions[currentVersion-1].prevValue]); + setCurrentVersion(versions.length + 1) + setVersions([...versions, {...versions[currentVersion-1], value: ''}]) + }, [versions, playbookRunId, instruction, versions, currentVersion, currentBot?.id]); const copyText = useCallback(() => { - navigator.clipboard.writeText(update); + navigator.clipboard.writeText(versions[currentVersion-1].value); setCopied(true); setTimeout(() => setCopied(false), 1000); - }, [update]) + }, [versions, currentVersion]) const onInputEnter = useCallback((e: React.KeyboardEvent) => { // Detect hitting enter and run the generateStatusUpdate if (e.key === 'Enter') { - setPrevMessages([...prevMessages, update]) - setUpdate('') - generateStatusUpdate(playbookRunId, currentBot.id, [...instructions, instruction], [...prevMessages, update]); - setInstructions([...instructions, instruction]) + generateStatusUpdate(playbookRunId, currentBot?.id, [instruction], [versions[currentVersion-1].value]); + setVersions([...versions, {instruction, prevValue: versions[currentVersion-1].value, value: ''}]) + setCurrentVersion(versions.length + 1) setInstruction('') - onGeneratingChanged(true); + setGenerating(true); setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) } - }, [instructions, instruction, playbookRunId, prevMessages, update, currentBot.id]) + }, [versions, instruction, playbookRunId, versions, currentVersion, currentBot?.id]) const stopGenerating = useCallback(() => { - onGeneratingChanged(false); + setGenerating(false); }, []) + if (!currentBot?.id) { + return null + } + return ( + + + setCurrentVersion(currentVersion-1)}/> + + setCurrentVersion(currentVersion+1)}/> + + + - {prevMessages.map((msg, idx) => ( - <> - - - {currentBot.displayName} - - {msg} - - - - - - - {instructions[idx]} - - ))} - - - - {currentBot.displayName} - - {update} + {generating && @@ -128,20 +129,21 @@ const AIModal = ({generating, playbookRunId, onGeneratingChanged, onAccept, curr } {!generating && - onAccept(update)}> - - - - } - {!generating && - - - - } - {!generating && - - - + <> + + + + + + + + + + onAccept(versions[currentVersion-1].value)}> + + + + } @@ -198,9 +200,9 @@ const StopGeneratingButton = styled.button` ` const AIModalContainer = styled.div` - width: 580px; - left: -23px; - top: -2px; + width: 480px; + right: -2px; + top: -10px; position: absolute; z-index: 1000; background: var(--center-channel-bg); @@ -246,7 +248,7 @@ const ExtraInstructionsInput = styled.div` const InsertButton = styled.button` display: inline-block; - margin: 12px 0px; + margin: 12px 12px 12px 0; border-radius: 4px; background: var(--button-bg-08); padding: 8px 16px 8px 8px; @@ -267,31 +269,15 @@ const InsertButton = styled.button` } ` -const Assistant = styled.div` - display: flex; - .Avatar { - width: 50px; - min-width: 50px; - height: 50px; - margin-right: 14px; - } -` - -const Username = styled.span` - font-weight: 600; -` - -const Messages = styled.div` - margin-top: -24px; - white-space: pre-wrap; - padding-left: 64px; - margin-bottom: 16px; -` - const AssistantMessageBox = styled.div` max-height: 200px; height: 200px; overflow-y: auto; + &&&& .custom-textarea { + border: none; + box-shadow: none; + height: 100%; + } ` const Copied = styled.span<{copied: boolean}>` @@ -301,4 +287,24 @@ const Copied = styled.span<{copied: boolean}>` opacity: ${(props) => (props.copied ? 1 : 0)}; ` +const TopBar = styled.div` + display: flex; + justify-content: space-between; +` + +const Versions = styled.div` + display: flex; + align-items: center; + color: var(--center-channel-color-75); + .icon { + cursor: pointer; + opacity: 0.5; + } + .icon.disabled { + cursor: auto; + pointer-events: none; + opacity: 0.2; + } +` + export default AIModal; diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index dc36eab8ec..dff9ccfd46 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -25,7 +25,7 @@ import GenericModal, {Description, Label} from 'src/components/widgets/generic_m import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; import IconAI from 'src/components/assets/icons/ai'; import AIModal from 'src/components/modals/ai_modal'; -import {useAIAvailable, useAIAvailableBots, useBotSelector} from 'src/ai_integration'; +import {useAIAvailable} from 'src/ai_integration'; import { Mode, @@ -56,7 +56,6 @@ import {useFinishRunConfirmationMessage} from 'src/components/backstage/playbook import {getPlaybooksGraphQLClient} from 'src/graphql_client'; import {getFragmentData, graphql} from 'src/graphql/generated'; import {DefaultMessageFragment, ReminderTimerFragment} from 'src/graphql/generated/graphql'; -import {useAIStatusUpdateClicked} from 'src/ai_integration'; const ID = 'playbooks_update_run_status_dialog'; const NAMES_ON_TOOLTIP = 5; @@ -107,14 +106,9 @@ const UpdateRunStatusModal = ({ }: Props) => { const dispatch = useDispatch(); const {formatMessage, formatList} = useIntl(); - const [currentBot, setCurrentBot] = useState(null); const currentUserId = useSelector(getCurrentUserId); const [aiModalOpen, setAIModalOpen] = useState(false); - const [generating, setGenerating] = useState(false); const aiAvailable = useAIAvailable(); - const aiAvailableBots = useAIAvailableBots(); - const BotSelector = useBotSelector() as any; - const aiStatusUpdateClicked = useAIStatusUpdateClicked(); const {data} = useQuery(runStatusModalQueryDocument, { variables: { runID: playbookRunId, @@ -271,32 +265,25 @@ const UpdateRunStatusModal = ({ const form = ( {description()} - + { aiAvailable && - { - if (!aiModalOpen) { - setCurrentBot(bot) - setAIModalOpen(true); - setGenerating(true); - } - }} - /> + { + setAIModalOpen(true); + }}> + + + } { aiAvailable && aiModalOpen && setGenerating(generating)} onAccept={(text) => { setMessage(text); setAIModalOpen(false); }} + onClose={() => setAIModalOpen(false)} /> } @@ -560,9 +547,12 @@ const AiModalContainer = styled.div` const LastChangeSince = styled.div` display: flex; justify-content: space-between; - button { - height: 26px; + >div { + margin: 24px 0 8px 0; + } + >button { margin-top: 20px; + height: 24px; } ` From 67a7800e32ce7f8c11869ff5880c8d13b34c7a57 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Sun, 5 Jan 2025 17:52:27 +0500 Subject: [PATCH 07/25] Updating UI for ai status modal --- webapp/src/components/assets/buttons.tsx | 25 ++++ webapp/src/components/assets/icons/ai.tsx | 4 +- webapp/src/components/markdown_textbox.tsx | 11 +- webapp/src/components/modals/ai_modal.tsx | 110 +++++++++++++----- .../modals/update_run_status_modal.tsx | 30 +++-- .../src/components/widgets/generic_modal.tsx | 3 +- 6 files changed, 143 insertions(+), 40 deletions(-) diff --git a/webapp/src/components/assets/buttons.tsx b/webapp/src/components/assets/buttons.tsx index 05b62eade8..b8496023cb 100644 --- a/webapp/src/components/assets/buttons.tsx +++ b/webapp/src/components/assets/buttons.tsx @@ -154,6 +154,31 @@ export const InvertedTertiaryButton = styled(Button)` } `; +export const QuaternaryButton = styled(Button)` + transition: all 0.15s ease-out; + + && { + color: var(--center-channel-color-64); + background-color: rgba(var(--center-channel-color-rgb), 0.08); + } + + &&:hover:not([disabled]) { + color: var(--center-channel-color-75); + background-color: rgba(var(--center-channel-color-rgb), 0.12); + } + + &&:active:not([disabled]) { + color: var(--button-bg-rgb); + background: rgba(var(--button-bg-rgb), 0.16); + } + + &&:focus:not([disabled]) { + color: var(--button-bg-rgb); + background-color: rgba(var(--button-color-rgb), 0.08); + box-shadow: inset 0px 0px 0px 2px var(--sidebar-text-active-border-rgb); + } +`; + export const SecondaryButton = styled(TertiaryButton)` background: var(--button-color-rgb); border: 1px solid var(--button-bg); diff --git a/webapp/src/components/assets/icons/ai.tsx b/webapp/src/components/assets/icons/ai.tsx index d3ee100339..1c88b89d68 100644 --- a/webapp/src/components/assets/icons/ai.tsx +++ b/webapp/src/components/assets/icons/ai.tsx @@ -4,8 +4,8 @@ import Svg from 'src/components/assets/svg'; const IconAI = () => ( diff --git a/webapp/src/components/markdown_textbox.tsx b/webapp/src/components/markdown_textbox.tsx index 227ef27acd..926bc332c6 100644 --- a/webapp/src/components/markdown_textbox.tsx +++ b/webapp/src/components/markdown_textbox.tsx @@ -35,6 +35,7 @@ type Props = { hideHelpBar?: boolean; previewByDefault?: boolean; autoFocus?: boolean; + minHeight?: string; } & ComponentProps; const MarkdownTextbox = ({ @@ -48,6 +49,7 @@ const MarkdownTextbox = ({ previewByDefault, autoFocus, hideHelpBar, + minHeight = '104px', ...textboxProps }: Props) => { const [showPreview, setShowPreview] = useState(previewByDefault); @@ -63,7 +65,10 @@ const MarkdownTextbox = ({ }); return ( - + ` .textarea-wrapper { margin-bottom: 6px; } @@ -121,7 +126,7 @@ const Wrapper = styled.div` } height: unset; - min-height: 104px; + min-height: ${(props) => props.$minHeight || '104px'}; max-height: 324px; overflow: auto; padding: 12px 30px 12px 16px; diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 77b0684e0f..d3b153e973 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -108,9 +108,14 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { - setCurrentVersion(currentVersion-1)}/> + setCurrentVersion(currentVersion-1)} className={currentVersion === 1 ? 'disabled' : ''}> + + + - setCurrentVersion(currentVersion+1)}/> + setCurrentVersion(currentVersion+1)} className={currentVersion === versions.length ? 'disabled' : ''}> + + { {generating && - - + + } {!generating && - <> + @@ -140,10 +145,10 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { onAccept(versions[currentVersion-1].value)}> - - + + - + } @@ -163,43 +168,71 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { const IconButton = styled.span` display: inline-block; - margin: 12px 0px; + height: 24px; + width: 24px; border-radius: 4px; background: transparent; text-decoration: none; color: var(--center-channel-color-64); padding: 8px; cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + + .icon { + font-size: 14.4px; + } + + &:hover { + background: var(--center-channel-color-08); + color: var(--center-channel-color-75); + } + + &:active { + background: var(--center-channel-color-08); + color: var(--center-channel-color-80); + } + + &.disabled { + color: var(--center-channel-color-56); + pointer-events: none; + cursor: not-allowed; + } ` const StopGeneratingButton = styled.button` - display: inline-block; + display: inline-flex; margin: 12 0px; border-radius: 4px; - padding: 8px 16px; - display: inline-block; + gap: 4px; margin: 12px 0px; border-radius: 4px; background: var(--center-channel-color-08); - padding: 8px 16px 8px 8px; color: var(--center-channel-color-64); cursor: pointer; text-decoration: none; font-weight: 600; + padding: 8px 12px; + font-size: 12px; text-align: center; border: 0px; + &:hover { background: var(--center-channel-color-16); } + &:active { background: var(--center-channel-color-24); } + &:focus { outline: none; } ` const AIModalContainer = styled.div` + box-shadow: var(--elevation-6); width: 480px; right: -2px; top: -10px; @@ -208,7 +241,7 @@ const AIModalContainer = styled.div` background: var(--center-channel-bg); border: 1px solid var(--center-channel-color-16); border-radius: 4px; - padding: 10px; + padding: 12px; &&&& textarea { min-height: 220px; @@ -226,21 +259,42 @@ const AIModalContainer = styled.div` } `; +const AIModalFooter = styled.div` + margin: 12px 0 0; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 4px; + padding-left: 8px; +`; + + const ExtraInstructionsInput = styled.div` display: flex; width: 100%; - padding: 10px; + padding: 6px 16px; + min-height: 40px; border-radius: 4px; border: 1px solid var(--center-channel-color-16); align-items: center; - &:focus { - border: 1px solid var(--center-channel-color-24); + transition: border-color 0.15s ease; + + &:focus-within { + border: 2px solid var(--button-bg); + padding: 5px 15px; /* Reduce padding by 1px to maintain same size with 2px border */ } + input { + font-size: 14px; border: 0px; width: 100%; margin-left: 8px; + + &:focus { + outline: none; + } } + svg { color: var(--center-channel-color-64); } @@ -248,14 +302,15 @@ const ExtraInstructionsInput = styled.div` const InsertButton = styled.button` display: inline-block; - margin: 12px 12px 12px 0; border-radius: 4px; background: var(--button-bg-08); - padding: 8px 16px 8px 8px; + padding: 0px 10px 0 6px; + height: 24px; color: var(--button-bg); cursor: pointer; text-decoration: none; font-weight: 600; + font-size: 11px; text-align: center; border: 0; &:hover { @@ -282,7 +337,8 @@ const AssistantMessageBox = styled.div` const Copied = styled.span<{copied: boolean}>` color: var(--center-channel-color-64); - margin: 12px 0px; + margin: 8px 0px; + font-size: 11px; transition: opacity 1s; opacity: ${(props) => (props.copied ? 1 : 0)}; ` @@ -290,20 +346,20 @@ const Copied = styled.span<{copied: boolean}>` const TopBar = styled.div` display: flex; justify-content: space-between; + padding-bottom: 12px; ` const Versions = styled.div` display: flex; align-items: center; color: var(--center-channel-color-75); - .icon { - cursor: pointer; - opacity: 0.5; - } - .icon.disabled { - cursor: auto; + font-size: 12px; + gap: 4px; + font-weight: 600; + + &.disabled { + color: var(--center-channel-color-64); pointer-events: none; - opacity: 0.2; } ` diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index dff9ccfd46..c1d15f676d 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -17,7 +17,7 @@ import {GlobalState} from '@mattermost/types/store'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {TertiaryButton} from 'src/components/assets/buttons'; +import {TertiaryButton, QuaternaryButton} from 'src/components/assets/buttons'; import {ApolloProvider, useQuery} from '@apollo/client'; @@ -270,12 +270,21 @@ const UpdateRunStatusModal = ({ {formatMessage({defaultMessage: 'Change since last update'})} { aiAvailable && - { - setAIModalOpen(true); - }}> - - - + (aiModalOpen ? ( + { + setAIModalOpen(true); + }}> + + + + ) : ( + { + setAIModalOpen(true); + }}> + + + + )) } { aiAvailable && aiModalOpen && @@ -289,6 +298,7 @@ const UpdateRunStatusModal = ({ } {hasPermission ? form : warning} @@ -528,6 +539,7 @@ const FooterContainer = styled.div` display: flex; flex-direction: row-reverse; align-items: center; + width: 100%; `; const StyledCheckboxInput = styled(CheckboxInput)` @@ -542,6 +554,7 @@ const StyledCheckboxInput = styled(CheckboxInput)` const AiModalContainer = styled.div` position: relative; + box-shadow: var(--elevation-6); ` const LastChangeSince = styled.div` @@ -553,6 +566,9 @@ const LastChangeSince = styled.div` >button { margin-top: 20px; height: 24px; + gap: 6px; + padding: 0 10px; + font-size: 12px; } ` diff --git a/webapp/src/components/widgets/generic_modal.tsx b/webapp/src/components/widgets/generic_modal.tsx index 1b048d9a05..005b2d1ee9 100644 --- a/webapp/src/components/widgets/generic_modal.tsx +++ b/webapp/src/components/widgets/generic_modal.tsx @@ -13,6 +13,7 @@ type Props = { className?: string; onHide: () => void; onExited?: () => void; + compassDesign?: boolean; modalHeaderText?: React.ReactNode; modalHeaderSideText?: React.ReactNode; modalHeaderIcon?: React.ReactNode; @@ -211,7 +212,7 @@ export const ModalHeading = styled.h1` font-size: 22px; line-height: 28px; color: var(--center-channel-color); - margin: 0; + margin: 8px 0 0; `; export const ModalSideheading = styled.h6` From c3c49aed6d7ef5739f97617158741e7c4e656d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Jan 2025 16:42:58 +0100 Subject: [PATCH 08/25] Adding the close button to the generate interface --- webapp/src/components/modals/ai_modal.tsx | 24 ++++++++++++------- .../modals/update_run_status_modal.tsx | 3 ++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index d3b153e973..320017702e 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,6 +1,8 @@ import {WebSocketMessage} from '@mattermost/client'; import React, {useEffect, useState, useCallback, useRef} from 'react'; +import ReactDOM from 'react-dom'; import styled from 'styled-components'; +import {createGlobalStyle} from "styled-components"; import {FormattedMessage, useIntl} from 'react-intl'; import IconAI from 'src/components/assets/icons/ai'; import {Textbox} from 'src/webapp_globals'; @@ -20,9 +22,10 @@ type Props = { playbookRunId: string onAccept: (text: string) => void onClose: () => void + isOpen: boolean } -const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { +const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); @@ -30,18 +33,18 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { const aiAvailableBots = useAIAvailableBots(); const BotSelector = useBotSelector() as any; const [currentBot, setCurrentBot] = useState(aiAvailableBots.length > 0 ? aiAvailableBots[0] : null); - const [currentVersion, setCurrentVersion] = useState(1); + const [currentVersion, setCurrentVersion] = useState(0); const [versions, setVersions] = useState([]); const [generating, setGenerating] = useState(null); useEffect(() => { - if (currentBot?.id) { + if (currentBot?.id && isOpen) { setCurrentVersion(versions.length + 1) setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) setGenerating(true); generateStatusUpdate(playbookRunId, currentBot.id, [], []); } - }, []); + }, [isOpen]); useEffect(() => { if (generating) { @@ -104,6 +107,10 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { return null } + if (!isOpen) { + return null; + } + return ( @@ -122,6 +129,10 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { activeBot={currentBot} setActiveBot={onBotChange} /> + @@ -141,9 +152,6 @@ const AIModal = ({playbookRunId, onAccept, onClose}: Props) => { - - - onAccept(versions[currentVersion-1].value)}> @@ -237,7 +245,6 @@ const AIModalContainer = styled.div` right: -2px; top: -10px; position: absolute; - z-index: 1000; background: var(--center-channel-bg); border: 1px solid var(--center-channel-color-16); border-radius: 4px; @@ -356,6 +363,7 @@ const Versions = styled.div` font-size: 12px; gap: 4px; font-weight: 600; + flex-grow: 1; &.disabled { color: var(--center-channel-color-64); diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index c1d15f676d..f3f6b437d9 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -287,12 +287,13 @@ const UpdateRunStatusModal = ({ )) } - { aiAvailable && aiModalOpen && + { aiAvailable && { setMessage(text); setAIModalOpen(false); }} onClose={() => setAIModalOpen(false)} + isOpen={aiModalOpen} /> } From 220055ac0ca8ca3a77525b1559e7613a90ff2855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Jan 2025 16:49:10 +0100 Subject: [PATCH 09/25] A couple of small fixes --- webapp/src/ai_integration.ts | 2 +- webapp/src/components/modals/ai_modal.tsx | 1 + webapp/src/components/modals/update_run_status_modal.tsx | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 3aea200f35..19ac12832a 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -23,7 +23,7 @@ export const useAIStatusUpdateClicked = () => { export const useAIAvailableBots = () => { return useSelector((state) => { //@ts-ignore plugins state is a thing - return state['plugins-' + aiPluginID]?.bots; + return state['plugins-' + aiPluginID]?.bots || []; }); }; diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 320017702e..8e72356f2e 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -245,6 +245,7 @@ const AIModalContainer = styled.div` right: -2px; top: -10px; position: absolute; + z-index: 1000; background: var(--center-channel-bg); border: 1px solid var(--center-channel-color-16); border-radius: 4px; diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index f3f6b437d9..516d1ab743 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -25,7 +25,7 @@ import GenericModal, {Description, Label} from 'src/components/widgets/generic_m import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; import IconAI from 'src/components/assets/icons/ai'; import AIModal from 'src/components/modals/ai_modal'; -import {useAIAvailable} from 'src/ai_integration'; +import {useAIAvailable, useAIAvailableBots} from 'src/ai_integration'; import { Mode, @@ -109,6 +109,7 @@ const UpdateRunStatusModal = ({ const currentUserId = useSelector(getCurrentUserId); const [aiModalOpen, setAIModalOpen] = useState(false); const aiAvailable = useAIAvailable(); + const aiAvailableBots = useAIAvailableBots(); const {data} = useQuery(runStatusModalQueryDocument, { variables: { runID: playbookRunId, @@ -269,7 +270,7 @@ const UpdateRunStatusModal = ({ - { aiAvailable && + { aiAvailable && aiAvailableBots.length > 0 && (aiModalOpen ? ( { setAIModalOpen(true); @@ -287,7 +288,7 @@ const UpdateRunStatusModal = ({ )) } - { aiAvailable && + { aiAvailable && aiAvailableBots.length > 0 && Date: Tue, 7 Jan 2025 17:41:22 +0100 Subject: [PATCH 10/25] Fixing initial load of the bots --- webapp/src/ai_integration.ts | 7 +++++ webapp/src/components/modals/ai_modal.tsx | 26 +++++++++---------- .../modals/update_run_status_modal.tsx | 2 +- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 19ac12832a..843ad0aba3 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -34,3 +34,10 @@ export const useBotSelector = () => { }); }; +export const useBotsLoaderHook = () => { + return useSelector((state) => { + //@ts-ignore plugins state is a thing + return state['plugins-' + aiPluginID]?.botsLoaderHook || (() => null); + }); +}; + diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 8e72356f2e..b4b2fcd79e 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -8,7 +8,7 @@ import IconAI from 'src/components/assets/icons/ai'; import {Textbox} from 'src/webapp_globals'; import {generateStatusUpdate} from 'src/client'; -import {useAIAvailableBots, useBotSelector} from 'src/ai_integration'; +import {useAIAvailableBots, useBotSelector, useBotsLoaderHook} from 'src/ai_integration'; import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; @@ -30,19 +30,19 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); const suggestionBox = useRef() - const aiAvailableBots = useAIAvailableBots(); const BotSelector = useBotSelector() as any; - const [currentBot, setCurrentBot] = useState(aiAvailableBots.length > 0 ? aiAvailableBots[0] : null); + const useBotlist = useBotsLoaderHook() as any; + const {bots, activeBot, setActiveBot} = useBotlist() const [currentVersion, setCurrentVersion] = useState(0); const [versions, setVersions] = useState([]); const [generating, setGenerating] = useState(null); useEffect(() => { - if (currentBot?.id && isOpen) { + if (activeBot?.id && isOpen) { setCurrentVersion(versions.length + 1) setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) setGenerating(true); - generateStatusUpdate(playbookRunId, currentBot.id, [], []); + generateStatusUpdate(playbookRunId, activeBot.id, [], []); } }, [isOpen]); @@ -67,7 +67,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { }, [generating]); const onBotChange = useCallback((bot: any) => { - setCurrentBot(bot) + setActiveBot(bot) setCurrentVersion(versions.length + 1) setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) setGenerating(true); @@ -76,10 +76,10 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const regenerate = useCallback(() => { setGenerating(true); - generateStatusUpdate(playbookRunId, currentBot?.id, [versions[currentVersion-1].instruction], [versions[currentVersion-1].prevValue]); + generateStatusUpdate(playbookRunId, activeBot?.id, [versions[currentVersion-1].instruction], [versions[currentVersion-1].prevValue]); setCurrentVersion(versions.length + 1) setVersions([...versions, {...versions[currentVersion-1], value: ''}]) - }, [versions, playbookRunId, instruction, versions, currentVersion, currentBot?.id]); + }, [versions, playbookRunId, instruction, versions, currentVersion, activeBot?.id]); const copyText = useCallback(() => { navigator.clipboard.writeText(versions[currentVersion-1].value); @@ -90,20 +90,20 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const onInputEnter = useCallback((e: React.KeyboardEvent) => { // Detect hitting enter and run the generateStatusUpdate if (e.key === 'Enter') { - generateStatusUpdate(playbookRunId, currentBot?.id, [instruction], [versions[currentVersion-1].value]); + generateStatusUpdate(playbookRunId, activeBot?.id, [instruction], [versions[currentVersion-1].value]); setVersions([...versions, {instruction, prevValue: versions[currentVersion-1].value, value: ''}]) setCurrentVersion(versions.length + 1) setInstruction('') setGenerating(true); setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) } - }, [versions, instruction, playbookRunId, versions, currentVersion, currentBot?.id]) + }, [versions, instruction, playbookRunId, versions, currentVersion, activeBot?.id]) const stopGenerating = useCallback(() => { setGenerating(false); }, []) - if (!currentBot?.id) { + if (!activeBot?.id) { return null } @@ -125,8 +125,8 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { - - - - - - {generating && - - - - - } - {!generating && - - - - - - - - onAccept(versions[currentVersion-1].value)}> - - - - - } - - - - - - setInstruction(e.target.value)} - value={instruction} - onKeyUp={onInputEnter} - /> - - + onAccept(versions[currentVersion-1].value)}> + + + + + } + + + + + + setInstruction(e.target.value)} + value={instruction} + onKeyUp={onInputEnter} + /> + + + ); }; @@ -240,20 +244,12 @@ const StopGeneratingButton = styled.button` ` const AIModalContainer = styled.div` - box-shadow: var(--elevation-6); - width: 480px; - right: -2px; - top: -10px; - position: absolute; - z-index: 1000; - background: var(--center-channel-bg); - border: 1px solid var(--center-channel-color-16); - border-radius: 4px; - padding: 12px; + position: relative; + top: -57px; &&&& textarea { - min-height: 220px; - max-height: 220px; + min-height: 250px; + max-height: 250px; border: 0; outline: 0; box-shadow: none; @@ -355,6 +351,7 @@ const TopBar = styled.div` display: flex; justify-content: space-between; padding-bottom: 12px; + margin-right: 24px; ` const Versions = styled.div` From 2301d6259911e5c4bb48ddbcaddfe2b0148b16b3 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Wed, 8 Jan 2025 22:11:24 +0500 Subject: [PATCH 12/25] updating ui --- webapp/src/components/assets/icons/ai.tsx | 11 +- webapp/src/components/modals/ai_modal.tsx | 265 ++++++++++++---------- 2 files changed, 149 insertions(+), 127 deletions(-) diff --git a/webapp/src/components/assets/icons/ai.tsx b/webapp/src/components/assets/icons/ai.tsx index 1c88b89d68..57983df6a8 100644 --- a/webapp/src/components/assets/icons/ai.tsx +++ b/webapp/src/components/assets/icons/ai.tsx @@ -2,10 +2,14 @@ import React from 'react'; import Svg from 'src/components/assets/svg'; -const IconAI = () => ( +interface Props { + size?: number; +} + +const IconAI = ({size = 16}: Props) => ( @@ -28,4 +32,3 @@ const IconAI = () => ( ); export default IconAI; - diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index eb7fb6e363..f9d099ca40 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,22 +1,22 @@ -import {WebSocketMessage} from '@mattermost/client'; -import React, {useEffect, useState, useCallback, useRef} from 'react'; +import { WebSocketMessage } from '@mattermost/client'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import ReactDOM from 'react-dom'; import styled from 'styled-components'; -import {createGlobalStyle} from "styled-components"; -import {FormattedMessage, useIntl} from 'react-intl'; +import { createGlobalStyle } from "styled-components"; +import { FormattedMessage, useIntl } from 'react-intl'; import IconAI from 'src/components/assets/icons/ai'; import GenericModal from 'src/components/widgets/generic_modal'; -import {Textbox} from 'src/webapp_globals'; +import { Textbox } from 'src/webapp_globals'; -import {generateStatusUpdate} from 'src/client'; -import {useAIAvailableBots, useBotSelector, useBotsLoaderHook} from 'src/ai_integration'; +import { generateStatusUpdate } from 'src/client'; +import { useAIAvailableBots, useBotSelector, useBotsLoaderHook } from 'src/ai_integration'; -import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; +import postEventListener, { PostUpdateWebsocketMessage } from 'src/websocket'; type Version = { - instruction: string - prevValue: string - value: string + instruction: string + prevValue: string + value: string } type Props = { @@ -26,40 +26,58 @@ type Props = { isOpen: boolean } -const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { +const StyledAIModal = styled(GenericModal)` + &&& { + + .modal-content { + width: 90%; + margin-left: 5%; + } + + .modal-body { + padding: 0 24px; + } + + .modal-header .close { + z-index: 5; + } + } +`; + +const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); const suggestionBox = useRef() const BotSelector = useBotSelector() as any; const useBotlist = useBotsLoaderHook() as any; - const {bots, activeBot, setActiveBot} = useBotlist() + const { bots, activeBot, setActiveBot } = useBotlist() const [currentVersion, setCurrentVersion] = useState(0); const [versions, setVersions] = useState([]); const [generating, setGenerating] = useState(null); useEffect(() => { if (activeBot?.id && isOpen) { - setCurrentVersion(versions.length + 1) - setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) - setGenerating(true); - generateStatusUpdate(playbookRunId, activeBot.id, [], []); + setCurrentVersion(versions.length + 1) + setVersions([...versions, { instruction: '', value: '', prevValue: '' }]) + setGenerating(true); + generateStatusUpdate(playbookRunId, activeBot.id, [], []); } }, [isOpen]); useEffect(() => { if (generating) { postEventListener.registerPostUpdateListener('playbooks_post_update', (msg: WebSocketMessage) => { - const data = msg.data; - if (!data.control) { - setGenerating(true); - const newVersions = [...versions] - newVersions[versions.length-1] = {...newVersions[versions.length-1], value: data.next} - setVersions(newVersions) - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) - } else if (data.control === 'end') { - setGenerating(false); - } + const data = msg.data; + if (!data.control) { + setGenerating(true); + const newVersions = [...versions] + newVersions[versions.length - 1] = { ...newVersions[versions.length - 1], value: data.next } + setVersions(newVersions) + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + } else if (data.control === 'end') { + setGenerating(false); + } }); } return () => { @@ -68,36 +86,36 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { }, [generating]); const onBotChange = useCallback((bot: any) => { - setActiveBot(bot) - setCurrentVersion(versions.length + 1) - setVersions([...versions, {instruction: '', value: '', prevValue: ''}]) - setGenerating(true); - generateStatusUpdate(playbookRunId, bot.id, [], []); + setActiveBot(bot) + setCurrentVersion(versions.length + 1) + setVersions([...versions, { instruction: '', value: '', prevValue: '' }]) + setGenerating(true); + generateStatusUpdate(playbookRunId, bot.id, [], []); }, [versions, playbookRunId]) const regenerate = useCallback(() => { setGenerating(true); - generateStatusUpdate(playbookRunId, activeBot?.id, [versions[currentVersion-1].instruction], [versions[currentVersion-1].prevValue]); + generateStatusUpdate(playbookRunId, activeBot?.id, [versions[currentVersion - 1].instruction], [versions[currentVersion - 1].prevValue]); setCurrentVersion(versions.length + 1) - setVersions([...versions, {...versions[currentVersion-1], value: ''}]) + setVersions([...versions, { ...versions[currentVersion - 1], value: '' }]) }, [versions, playbookRunId, instruction, versions, currentVersion, activeBot?.id]); const copyText = useCallback(() => { - navigator.clipboard.writeText(versions[currentVersion-1].value); - setCopied(true); - setTimeout(() => setCopied(false), 1000); + navigator.clipboard.writeText(versions[currentVersion - 1].value); + setCopied(true); + setTimeout(() => setCopied(false), 1000); }, [versions, currentVersion]) const onInputEnter = useCallback((e: React.KeyboardEvent) => { - // Detect hitting enter and run the generateStatusUpdate - if (e.key === 'Enter') { - generateStatusUpdate(playbookRunId, activeBot?.id, [instruction], [versions[currentVersion-1].value]); - setVersions([...versions, {instruction, prevValue: versions[currentVersion-1].value, value: ''}]) - setCurrentVersion(versions.length + 1) - setInstruction('') - setGenerating(true); - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) - } + // Detect hitting enter and run the generateStatusUpdate + if (e.key === 'Enter') { + generateStatusUpdate(playbookRunId, activeBot?.id, [instruction], [versions[currentVersion - 1].value]); + setVersions([...versions, { instruction, prevValue: versions[currentVersion - 1].value, value: '' }]) + setCurrentVersion(versions.length + 1) + setInstruction('') + setGenerating(true); + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + } }, [versions, instruction, playbookRunId, versions, currentVersion, activeBot?.id]) const stopGenerating = useCallback(() => { @@ -109,72 +127,71 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { } if (!isOpen) { - return null; + return null; } return ( - - - - - setCurrentVersion(currentVersion-1)} className={currentVersion === 1 ? 'disabled' : ''}> - - - - - setCurrentVersion(currentVersion+1)} className={currentVersion === versions.length ? 'disabled' : ''}> - - - - - - - - - - {generating && - - - - - } - {!generating && - - - - - - - - onAccept(versions[currentVersion-1].value)}> - - - - - } - - - - - - setInstruction(e.target.value)} - value={instruction} - onKeyUp={onInputEnter} - /> - - - + + + + setCurrentVersion(currentVersion - 1)} className={currentVersion === 1 ? 'disabled' : ''}> + + + + setCurrentVersion(currentVersion + 1)} className={currentVersion === versions.length ? 'disabled' : ''}> + + + + + + + + + + {generating && + + + + + } + {!generating && + + + + + + + + onAccept(versions[currentVersion - 1].value)}> + + + + + + + + } + + + setInstruction(e.target.value)} + value={instruction} + onKeyUp={onInputEnter} + /> + + + ); }; @@ -202,8 +219,8 @@ const IconButton = styled.span` } &:active { - background: var(--center-channel-color-08); - color: var(--center-channel-color-80); + color: rgba(var(--button-bg-rgb)); + background: rgba(var(--button-bg-rgb), 0.16); } &.disabled { @@ -215,37 +232,39 @@ const IconButton = styled.span` const StopGeneratingButton = styled.button` display: inline-flex; - margin: 12 0px; - border-radius: 4px; + margin: 12px 0; + align-items: center; gap: 4px; - margin: 12px 0px; border-radius: 4px; - background: var(--center-channel-color-08); + background: rgba(var(--center-channel-color-rgb), 0.08); + padding: 0px 10px 0 6px; + height: 24px; color: var(--center-channel-color-64); cursor: pointer; text-decoration: none; font-weight: 600; - padding: 8px 12px; - font-size: 12px; + font-size: 11px; text-align: center; - border: 0px; + border: 0; &:hover { - background: var(--center-channel-color-16); + background: rgba(var(--center-channel-color-rgb), 0.12); + color: var(--center-channel-color-75); } &:active { - background: var(--center-channel-color-24); + background: rgba(var(--center-channel-color-rgb), 0.16); } &:focus { - outline: none; + outline: none; } -` +`; const AIModalContainer = styled.div` position: relative; - top: -57px; + margin-top: -56px; + padding-bottom: 24px; &&&& textarea { min-height: 250px; @@ -264,12 +283,11 @@ const AIModalContainer = styled.div` `; const AIModalFooter = styled.div` - margin: 12px 0 0; + margin: 12px 0; display: flex; justify-content: flex-start; align-items: center; gap: 4px; - padding-left: 8px; `; @@ -339,9 +357,10 @@ const AssistantMessageBox = styled.div` } ` -const Copied = styled.span<{copied: boolean}>` - color: var(--center-channel-color-64); - margin: 8px 0px; +const Copied = styled.span<{ copied: boolean }>` + color: var(--online-indicator); + font-weight: 600; + margin: 0 4px; font-size: 11px; transition: opacity 1s; opacity: ${(props) => (props.copied ? 1 : 0)}; From 63f67e60a8963e2bf9757e8b5a610b12bd898f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 11:00:02 +0100 Subject: [PATCH 13/25] Removing unnecesary code --- webapp/src/ai_integration.ts | 7 ------- webapp/src/components/rhs/rhs_post_update.tsx | 6 ------ webapp/src/components/rhs/rhs_post_update_button.tsx | 1 - 3 files changed, 14 deletions(-) diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 843ad0aba3..bc1ae5d683 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -13,13 +13,6 @@ export const useAIAvailable = () => { export type AIStatusUpdateClickedFunc = ((playbookRunId: string) => string) | undefined; -export const useAIStatusUpdateClicked = () => { - return useSelector((state) => { - //@ts-ignore plugins state is a thing - return state['plugins-' + aiPluginID]?.aiStatusUpdateClicked; - }); -}; - export const useAIAvailableBots = () => { return useSelector((state) => { //@ts-ignore plugins state is a thing diff --git a/webapp/src/components/rhs/rhs_post_update.tsx b/webapp/src/components/rhs/rhs_post_update.tsx index ab989c2037..4bad5f795d 100644 --- a/webapp/src/components/rhs/rhs_post_update.tsx +++ b/webapp/src/components/rhs/rhs_post_update.tsx @@ -42,7 +42,6 @@ const RHSPostUpdate = (props: Props) => { RunDetailsTutorialSteps.PostUpdate, TutorialTourCategories.RUN_DETAILS ); - const aiStatusUpdateClicked = useAIStatusUpdateClicked(); const isNextUpdateScheduled = props.playbookRun.previous_reminder !== 0; const timestamp = getTimestamp(props.playbookRun, isNextUpdateScheduled); @@ -115,11 +114,6 @@ const RHSPostUpdate = (props: Props) => { props.playbookRun.channel_id, )); }} - onAIClick={() => { - if (aiStatusUpdateClicked) { - aiStatusUpdateClicked(props.playbookRun.id); - } - }} isDue={isDue} /> {showRunDetailsPostUpdateStep && ( diff --git a/webapp/src/components/rhs/rhs_post_update_button.tsx b/webapp/src/components/rhs/rhs_post_update_button.tsx index 6c089c67a8..c5c875060e 100644 --- a/webapp/src/components/rhs/rhs_post_update_button.tsx +++ b/webapp/src/components/rhs/rhs_post_update_button.tsx @@ -18,7 +18,6 @@ interface Props { updatesExist: boolean; disabled: boolean; onClick: () => void; - onAIClick: () => void; } const RHSPostUpdateButton = (props: Props) => { From d84286839780d193658b520c44b916c6ea93b9ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 11:08:43 +0100 Subject: [PATCH 14/25] Fixing linter complains --- webapp/src/client.ts | 9 +- webapp/src/components/modals/ai_modal.tsx | 119 ++++++++++-------- .../modals/update_run_status_modal.tsx | 63 +++++----- webapp/src/components/rhs/rhs_post_update.tsx | 1 - .../components/rhs/rhs_post_update_button.tsx | 10 -- webapp/src/index.tsx | 2 +- webapp/src/types/websocket_events.ts | 2 +- webapp/src/websocket.tsx | 2 +- 8 files changed, 109 insertions(+), 99 deletions(-) diff --git a/webapp/src/client.ts b/webapp/src/client.ts index 275bfd2a27..9681dba77d 100644 --- a/webapp/src/client.ts +++ b/webapp/src/client.ts @@ -816,16 +816,15 @@ export async function getTeamTopPlaybooks(timeRange: string, page: number, perPa return data as InsightsResponse; } - export async function generateStatusUpdate(playbookRunID: string, botId: string, instructions: string[], messages: string[]) { const url = `${playbookRunRoute(playbookRunID)}/generate_status`; const response = await fetch(url, Client4.getOptions({ method: 'POST', body: JSON.stringify({ - instructions, - messages, - bot: botId, - }) + instructions, + messages, + bot: botId, + }), })); if (response.ok) { diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index f9d099ca40..d85cb7acc1 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,17 +1,21 @@ -import { WebSocketMessage } from '@mattermost/client'; -import React, { useEffect, useState, useCallback, useRef } from 'react'; -import ReactDOM from 'react-dom'; +import {WebSocketMessage} from '@mattermost/client'; +import React, { + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import styled from 'styled-components'; -import { createGlobalStyle } from "styled-components"; -import { FormattedMessage, useIntl } from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; + import IconAI from 'src/components/assets/icons/ai'; import GenericModal from 'src/components/widgets/generic_modal'; -import { Textbox } from 'src/webapp_globals'; +import {Textbox} from 'src/webapp_globals'; -import { generateStatusUpdate } from 'src/client'; -import { useAIAvailableBots, useBotSelector, useBotsLoaderHook } from 'src/ai_integration'; +import {generateStatusUpdate} from 'src/client'; +import {useBotSelector, useBotsLoaderHook} from 'src/ai_integration'; -import postEventListener, { PostUpdateWebsocketMessage } from 'src/websocket'; +import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; type Version = { instruction: string @@ -44,22 +48,22 @@ const StyledAIModal = styled(GenericModal)` } `; -const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { +const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); - const suggestionBox = useRef() + const suggestionBox = useRef(); const BotSelector = useBotSelector() as any; const useBotlist = useBotsLoaderHook() as any; - const { bots, activeBot, setActiveBot } = useBotlist() + const {bots, activeBot, setActiveBot} = useBotlist(); const [currentVersion, setCurrentVersion] = useState(0); const [versions, setVersions] = useState([]); const [generating, setGenerating] = useState(null); useEffect(() => { if (activeBot?.id && isOpen) { - setCurrentVersion(versions.length + 1) - setVersions([...versions, { instruction: '', value: '', prevValue: '' }]) + setCurrentVersion(versions.length + 1); + setVersions([...versions, {instruction: '', value: '', prevValue: ''}]); setGenerating(true); generateStatusUpdate(playbookRunId, activeBot.id, [], []); } @@ -71,10 +75,10 @@ const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { const data = msg.data; if (!data.control) { setGenerating(true); - const newVersions = [...versions] - newVersions[versions.length - 1] = { ...newVersions[versions.length - 1], value: data.next } - setVersions(newVersions) - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + const newVersions = [...versions]; + newVersions[versions.length - 1] = {...newVersions[versions.length - 1], value: data.next}; + setVersions(newVersions); + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0); } else if (data.control === 'end') { setGenerating(false); } @@ -86,44 +90,44 @@ const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { }, [generating]); const onBotChange = useCallback((bot: any) => { - setActiveBot(bot) - setCurrentVersion(versions.length + 1) - setVersions([...versions, { instruction: '', value: '', prevValue: '' }]) + setActiveBot(bot); + setCurrentVersion(versions.length + 1); + setVersions([...versions, {instruction: '', value: '', prevValue: ''}]); setGenerating(true); generateStatusUpdate(playbookRunId, bot.id, [], []); - }, [versions, playbookRunId]) + }, [versions, playbookRunId]); const regenerate = useCallback(() => { setGenerating(true); generateStatusUpdate(playbookRunId, activeBot?.id, [versions[currentVersion - 1].instruction], [versions[currentVersion - 1].prevValue]); - setCurrentVersion(versions.length + 1) - setVersions([...versions, { ...versions[currentVersion - 1], value: '' }]) + setCurrentVersion(versions.length + 1); + setVersions([...versions, {...versions[currentVersion - 1], value: ''}]); }, [versions, playbookRunId, instruction, versions, currentVersion, activeBot?.id]); const copyText = useCallback(() => { navigator.clipboard.writeText(versions[currentVersion - 1].value); setCopied(true); setTimeout(() => setCopied(false), 1000); - }, [versions, currentVersion]) + }, [versions, currentVersion]); const onInputEnter = useCallback((e: React.KeyboardEvent) => { // Detect hitting enter and run the generateStatusUpdate if (e.key === 'Enter') { generateStatusUpdate(playbookRunId, activeBot?.id, [instruction], [versions[currentVersion - 1].value]); - setVersions([...versions, { instruction, prevValue: versions[currentVersion - 1].value, value: '' }]) - setCurrentVersion(versions.length + 1) - setInstruction('') + setVersions([...versions, {instruction, prevValue: versions[currentVersion - 1].value, value: ''}]); + setCurrentVersion(versions.length + 1); + setInstruction(''); setGenerating(true); - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0) + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0); } - }, [versions, instruction, playbookRunId, versions, currentVersion, activeBot?.id]) + }, [versions, instruction, playbookRunId, versions, currentVersion, activeBot?.id]); const stopGenerating = useCallback(() => { setGenerating(false); - }, []) + }, []); if (!activeBot?.id) { - return null + return null; } if (!isOpen) { @@ -140,12 +144,21 @@ const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { - setCurrentVersion(currentVersion - 1)} className={currentVersion === 1 ? 'disabled' : ''}> - + setCurrentVersion(currentVersion - 1)} + className={currentVersion === 1 ? 'disabled' : ''} + > + - - setCurrentVersion(currentVersion + 1)} className={currentVersion === versions.length ? 'disabled' : ''}> - + + setCurrentVersion(currentVersion + 1)} + className={currentVersion === versions.length ? 'disabled' : ''} + > + { /> - + {generating && - - + + } {!generating && - + - + onAccept(versions[currentVersion - 1].value)}> @@ -182,9 +198,9 @@ const AIModal = ({ playbookRunId, onAccept, onClose, isOpen }: Props) => { } - + setInstruction(e.target.value)} value={instruction} onKeyUp={onInputEnter} @@ -228,7 +244,7 @@ const IconButton = styled.span` pointer-events: none; cursor: not-allowed; } -` +`; const StopGeneratingButton = styled.button` display: inline-flex; @@ -290,7 +306,6 @@ const AIModalFooter = styled.div` gap: 4px; `; - const ExtraInstructionsInput = styled.div` display: flex; width: 100%; @@ -320,7 +335,7 @@ const ExtraInstructionsInput = styled.div` svg { color: var(--center-channel-color-64); } -` +`; const InsertButton = styled.button` display: inline-block; @@ -344,7 +359,7 @@ const InsertButton = styled.button` &:focus { outline: none; } -` +`; const AssistantMessageBox = styled.div` max-height: 200px; @@ -355,7 +370,7 @@ const AssistantMessageBox = styled.div` box-shadow: none; height: 100%; } -` +`; const Copied = styled.span<{ copied: boolean }>` color: var(--online-indicator); @@ -364,14 +379,14 @@ const Copied = styled.span<{ copied: boolean }>` font-size: 11px; transition: opacity 1s; opacity: ${(props) => (props.copied ? 1 : 0)}; -` +`; const TopBar = styled.div` display: flex; justify-content: space-between; padding-bottom: 12px; margin-right: 24px; -` +`; const Versions = styled.div` display: flex; @@ -386,6 +401,6 @@ const Versions = styled.div` color: var(--center-channel-color-64); pointer-events: none; } -` +`; export default AIModal; diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index 22c5101d09..cc128328fe 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -17,10 +17,10 @@ import {GlobalState} from '@mattermost/types/store'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; -import {TertiaryButton, QuaternaryButton} from 'src/components/assets/buttons'; - import {ApolloProvider, useQuery} from '@apollo/client'; +import {QuaternaryButton, TertiaryButton} from 'src/components/assets/buttons'; + import GenericModal, {Description, Label} from 'src/components/widgets/generic_modal'; import UnsavedChangesModal from 'src/components/widgets/unsaved_changes_modal'; import IconAI from 'src/components/assets/icons/ai'; @@ -267,36 +267,43 @@ const UpdateRunStatusModal = ({ {description()} - - { aiAvailable && aiAvailableBots.length > 0 && + + { aiAvailable && aiAvailableBots.length > 0 && (aiModalOpen ? ( - { - setAIModalOpen(true); - }}> - - - + { + setAIModalOpen(true); + }} + > + + + ) : ( - { - setAIModalOpen(true); - }}> - - - + { + setAIModalOpen(true); + }} + > + + + )) - } + } { aiAvailable && - + { setMessage(text); setAIModalOpen(false); }} - onClose={() => setAIModalOpen(false)} - isOpen={aiModalOpen} + playbookRunId={playbookRunId} + onAccept={(text) => { + setMessage(text); + setAIModalOpen(false); + }} + onClose={() => setAIModalOpen(false)} + isOpen={aiModalOpen} /> - + } { const client = getPlaybooksGraphQLClient(); diff --git a/webapp/src/components/rhs/rhs_post_update.tsx b/webapp/src/components/rhs/rhs_post_update.tsx index 4bad5f795d..64f30fb777 100644 --- a/webapp/src/components/rhs/rhs_post_update.tsx +++ b/webapp/src/components/rhs/rhs_post_update.tsx @@ -19,7 +19,6 @@ import TutorialTourTip, {useMeasurePunchouts, useShowTutorialStep} from 'src/com import {RunDetailsTutorialSteps, TutorialTourCategories} from 'src/components/tutorial/tours'; import {useNow} from 'src/hooks'; -import {useAIStatusUpdateClicked} from 'src/ai_integration'; interface Props { collapsed: boolean; diff --git a/webapp/src/components/rhs/rhs_post_update_button.tsx b/webapp/src/components/rhs/rhs_post_update_button.tsx index c5c875060e..957246a9aa 100644 --- a/webapp/src/components/rhs/rhs_post_update_button.tsx +++ b/webapp/src/components/rhs/rhs_post_update_button.tsx @@ -7,8 +7,6 @@ import styled, {css} from 'styled-components'; import {DestructiveButton, PrimaryButton, TertiaryButton} from 'src/components/assets/buttons'; -import IconAI from 'src/components/assets/icons/ai'; -import Tooltip from 'src/components/widgets/tooltip'; import {useAIAvailable} from 'src/ai_integration'; interface Props { @@ -21,9 +19,6 @@ interface Props { } const RHSPostUpdateButton = (props: Props) => { - const {formatMessage} = useIntl(); - const aiAvailable = useAIAvailable(); - let ButtonComponent = PostUpdatePrimaryButton; if (props.isDue) { @@ -56,11 +51,6 @@ const ButtonsContainer = styled.div` gap: 2px; `; -const AIButtonContainer = styled.div` - display: flex; - flex-grow: 0; -`; - const PostUpdateButtonCommon = css` justify-content: center; flex: 1; diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 48ae9d38b4..4b2732d8d0 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -49,12 +49,12 @@ import { handleWebsocketUserRemoved, } from 'src/websocket_events'; import { + WEBSOCKET_MATTERMOST_AI_POSTUPDATE, WEBSOCKET_PLAYBOOK_ARCHIVED, WEBSOCKET_PLAYBOOK_CREATED, WEBSOCKET_PLAYBOOK_RESTORED, WEBSOCKET_PLAYBOOK_RUN_CREATED, WEBSOCKET_PLAYBOOK_RUN_UPDATED, - WEBSOCKET_MATTERMOST_AI_POSTUPDATE, } from 'src/types/websocket_events'; import { fetchGlobalSettings, diff --git a/webapp/src/types/websocket_events.ts b/webapp/src/types/websocket_events.ts index e3b0cb5702..7dd964923b 100644 --- a/webapp/src/types/websocket_events.ts +++ b/webapp/src/types/websocket_events.ts @@ -8,4 +8,4 @@ export const WEBSOCKET_PLAYBOOK_RUN_CREATED = `custom_${manifest.id}_playbook_ru export const WEBSOCKET_PLAYBOOK_CREATED = `custom_${manifest.id}_playbook_created`; export const WEBSOCKET_PLAYBOOK_ARCHIVED = `custom_${manifest.id}_playbook_archived`; export const WEBSOCKET_PLAYBOOK_RESTORED = `custom_${manifest.id}_playbook_restored`; -export const WEBSOCKET_MATTERMOST_AI_POSTUPDATE = `custom_mattermost-ai_postupdate`; +export const WEBSOCKET_MATTERMOST_AI_POSTUPDATE = 'custom_mattermost-ai_postupdate'; diff --git a/webapp/src/websocket.tsx b/webapp/src/websocket.tsx index 20e33ebaf2..73e204712c 100644 --- a/webapp/src/websocket.tsx +++ b/webapp/src/websocket.tsx @@ -26,5 +26,5 @@ class PostEventListener { }; } -const postEventListener = new PostEventListener() +const postEventListener = new PostEventListener(); export default postEventListener; From 3e93d3c8da3c86f72cc85f8a1a50f350f2afd277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 11:09:22 +0100 Subject: [PATCH 15/25] Removing unnecesary code --- webapp/src/ai_integration.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index bc1ae5d683..93d8ab6688 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -11,8 +11,6 @@ export const useAIAvailable = () => { return useSelector((state) => Boolean(state.plugins?.plugins?.[aiPluginID])); }; -export type AIStatusUpdateClickedFunc = ((playbookRunId: string) => string) | undefined; - export const useAIAvailableBots = () => { return useSelector((state) => { //@ts-ignore plugins state is a thing From 1e3d156a79c8818aaaf59da1e139f04be2bc8003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 11:12:29 +0100 Subject: [PATCH 16/25] Removing more unnecesary changes --- .../components/rhs/rhs_post_update_button.tsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/webapp/src/components/rhs/rhs_post_update_button.tsx b/webapp/src/components/rhs/rhs_post_update_button.tsx index 957246a9aa..b3bcf8472d 100644 --- a/webapp/src/components/rhs/rhs_post_update_button.tsx +++ b/webapp/src/components/rhs/rhs_post_update_button.tsx @@ -2,13 +2,11 @@ // See LICENSE.txt for license information. import React from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {FormattedMessage} from 'react-intl'; import styled, {css} from 'styled-components'; import {DestructiveButton, PrimaryButton, TertiaryButton} from 'src/components/assets/buttons'; -import {useAIAvailable} from 'src/ai_integration'; - interface Props { collapsed: boolean; isDue: boolean; @@ -28,15 +26,13 @@ const RHSPostUpdateButton = (props: Props) => { } return ( - - - - - + + + ); }; @@ -44,13 +40,6 @@ interface CollapsedProps { collapsed: boolean; } -const ButtonsContainer = styled.div` - flex-grow: 1; - display: flex; - flex-direction: row; - gap: 2px; -`; - const PostUpdateButtonCommon = css` justify-content: center; flex: 1; From bd8c6b808fa60dc022c9c62b788341acdb67e33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:17:33 +0100 Subject: [PATCH 17/25] fixing ci --- webapp/src/components/modals/ai_modal.tsx | 2 +- webapp/src/components/modals/update_run_status_modal.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index d85cb7acc1..456df94672 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -52,7 +52,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); - const suggestionBox = useRef(); + const suggestionBox = useRef(null); const BotSelector = useBotSelector() as any; const useBotlist = useBotsLoaderHook() as any; const {bots, activeBot, setActiveBot} = useBotlist(); diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index cc128328fe..dfcdf5f987 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -566,9 +566,11 @@ const AiModalContainer = styled.div` box-shadow: var(--elevation-6); `; -const LastChangeSince = styled.div` +const LastChangeSince = styled.div<{disabled: boolean}>` display: flex; justify-content: space-between; + pointer-events: ${props => props.disabled ? 'none' : 'auto'}; + >div { margin: 24px 0 8px 0; } From 3ef6e5330842238cbc3d2edf2e834fe4089fcfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:24:04 +0100 Subject: [PATCH 18/25] Fixing ci --- webapp/src/components/modals/update_run_status_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/modals/update_run_status_modal.tsx b/webapp/src/components/modals/update_run_status_modal.tsx index dfcdf5f987..43432b46c6 100644 --- a/webapp/src/components/modals/update_run_status_modal.tsx +++ b/webapp/src/components/modals/update_run_status_modal.tsx @@ -569,7 +569,7 @@ const AiModalContainer = styled.div` const LastChangeSince = styled.div<{disabled: boolean}>` display: flex; justify-content: space-between; - pointer-events: ${props => props.disabled ? 'none' : 'auto'}; + pointer-events: ${(props) => (props.disabled ? 'none' : 'auto')}; >div { margin: 24px 0 8px 0; From b93eccfdad7181fc99b7bbe739cf38011364c2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:37:27 +0100 Subject: [PATCH 19/25] more typesafety --- webapp/src/ai_integration.ts | 8 +++++--- webapp/src/components/modals/ai_modal.tsx | 10 ++++++---- webapp/src/types/ai.tsx | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 webapp/src/types/ai.tsx diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 93d8ab6688..5c13a87aed 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -4,6 +4,8 @@ import {GlobalState} from 'mattermost-webapp/packages/types/src/store'; import {useSelector} from 'react-redux'; +import {BotSelector, Bot, BotsLoaderHook} from './types/ai'; + export const aiPluginID = 'mattermost-ai'; export const useAIAvailable = () => { @@ -12,21 +14,21 @@ export const useAIAvailable = () => { }; export const useAIAvailableBots = () => { - return useSelector((state) => { + return useSelector((state) => { //@ts-ignore plugins state is a thing return state['plugins-' + aiPluginID]?.bots || []; }); }; export const useBotSelector = () => { - return useSelector((state) => { + return useSelector((state) => { //@ts-ignore plugins state is a thing return state['plugins-' + aiPluginID]?.botSelector; }); }; export const useBotsLoaderHook = () => { - return useSelector((state) => { + return useSelector((state) => { //@ts-ignore plugins state is a thing return state['plugins-' + aiPluginID]?.botsLoaderHook || (() => null); }); diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 456df94672..7e808feb0f 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -15,6 +15,8 @@ import {Textbox} from 'src/webapp_globals'; import {generateStatusUpdate} from 'src/client'; import {useBotSelector, useBotsLoaderHook} from 'src/ai_integration'; +import {Bot} from 'src/types/ai'; + import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; type Version = { @@ -53,12 +55,12 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); const suggestionBox = useRef(null); - const BotSelector = useBotSelector() as any; - const useBotlist = useBotsLoaderHook() as any; + const BotSelector = useBotSelector(); + const useBotlist = useBotsLoaderHook(); const {bots, activeBot, setActiveBot} = useBotlist(); const [currentVersion, setCurrentVersion] = useState(0); const [versions, setVersions] = useState([]); - const [generating, setGenerating] = useState(null); + const [generating, setGenerating] = useState(false); useEffect(() => { if (activeBot?.id && isOpen) { @@ -89,7 +91,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { }; }, [generating]); - const onBotChange = useCallback((bot: any) => { + const onBotChange = useCallback((bot: Bot) => { setActiveBot(bot); setCurrentVersion(versions.length + 1); setVersions([...versions, {instruction: '', value: '', prevValue: ''}]); diff --git a/webapp/src/types/ai.tsx b/webapp/src/types/ai.tsx new file mode 100644 index 0000000000..91c92f4ea0 --- /dev/null +++ b/webapp/src/types/ai.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export type Bot = { + id: string +} + +type BotSelectorProps = { + bots: Bot[] + activeBot: Bot + setActiveBot: (bot: Bot) => void +} + +export type BotsLoaderHook = () => BotSelectorProps +export type BotSelector = React.FunctionComponent From fc67290cc89be13877bd0232f7bf27f75339b27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:44:26 +0100 Subject: [PATCH 20/25] feat: Add accessibility labels for version navigation buttons --- webapp/src/components/modals/ai_modal.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 7e808feb0f..725bade128 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -149,6 +149,8 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { setCurrentVersion(currentVersion - 1)} className={currentVersion === 1 ? 'disabled' : ''} + aria-label={intl.formatMessage({defaultMessage: 'Go to previous version'})} + aria-disabled={currentVersion === 1} > @@ -159,6 +161,8 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { setCurrentVersion(currentVersion + 1)} className={currentVersion === versions.length ? 'disabled' : ''} + aria-label={intl.formatMessage({defaultMessage: 'Go to next version'})} + aria-disabled={currentVersion === versions.length} > From 8f98aab9510a9346f7017f041ef61bb1400f4c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Fri, 10 Jan 2025 15:44:27 +0100 Subject: [PATCH 21/25] feat: Improve accessibility of AI modal with ARIA labels and roles --- webapp/src/components/modals/ai_modal.tsx | 38 +++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 725bade128..6d9c6c17aa 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -51,6 +51,7 @@ const StyledAIModal = styled(GenericModal)` `; const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { + const modalRef = useRef(null); const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); @@ -142,8 +143,14 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { onHide={onClose} id={'generateStatusUpdate'} compassDesign={true} + role="dialog" + aria-labelledby="ai-modal-title" + aria-modal={true} > - + +

+ +

{ @@ -188,13 +198,22 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { } {!generating && - + - + - onAccept(versions[currentVersion - 1].value)}> + onAccept(versions[currentVersion - 1].value)} + aria-label={intl.formatMessage({defaultMessage: 'Accept and insert generated content'})} + > @@ -210,6 +229,8 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { onChange={(e) => setInstruction(e.target.value)} value={instruction} onKeyUp={onInputEnter} + aria-label={intl.formatMessage({defaultMessage: 'Additional instructions for AI'})} + role="textbox" />
@@ -252,7 +273,10 @@ const IconButton = styled.span` } `; -const StopGeneratingButton = styled.button` +const StopGeneratingButton = styled.button.attrs({ + 'aria-label': 'Stop generating content', + type: 'button', +})` display: inline-flex; margin: 12px 0; align-items: center; @@ -343,7 +367,9 @@ const ExtraInstructionsInput = styled.div` } `; -const InsertButton = styled.button` +const InsertButton = styled.button.attrs({ + type: 'button', +})` display: inline-block; border-radius: 4px; background: var(--button-bg-08); From 4f76f1faa0fe08ea06d45495ea0fac240a857efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:53:36 +0100 Subject: [PATCH 22/25] Fixing aria-labels --- webapp/src/components/modals/ai_modal.tsx | 29 ++++++----------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 6d9c6c17aa..407189d27e 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -51,7 +51,6 @@ const StyledAIModal = styled(GenericModal)` `; const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { - const modalRef = useRef(null); const intl = useIntl(); const [copied, setCopied] = useState(false); const [instruction, setInstruction] = useState(''); @@ -143,14 +142,10 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { onHide={onClose} id={'generateStatusUpdate'} compassDesign={true} - role="dialog" - aria-labelledby="ai-modal-title" + aria-label={intl.formatMessage({defaultMessage: 'AI Status Update Generator'})} aria-modal={true} > - -

- -

+ { @@ -198,19 +190,19 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { } {!generating && - - - onAccept(versions[currentVersion - 1].value)} aria-label={intl.formatMessage({defaultMessage: 'Accept and insert generated content'})} > @@ -229,8 +221,6 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { onChange={(e) => setInstruction(e.target.value)} value={instruction} onKeyUp={onInputEnter} - aria-label={intl.formatMessage({defaultMessage: 'Additional instructions for AI'})} - role="textbox" />
@@ -273,10 +263,7 @@ const IconButton = styled.span` } `; -const StopGeneratingButton = styled.button.attrs({ - 'aria-label': 'Stop generating content', - type: 'button', -})` +const StopGeneratingButton = styled.button` display: inline-flex; margin: 12px 0; align-items: center; @@ -367,9 +354,7 @@ const ExtraInstructionsInput = styled.div` } `; -const InsertButton = styled.button.attrs({ - type: 'button', -})` +const InsertButton = styled.button` display: inline-block; border-radius: 4px; background: var(--button-bg-08); From 5aa20c214d5e9dbe6a0fa52230e7fba6b508d5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino=20=28aider=29?= Date: Fri, 10 Jan 2025 15:57:36 +0100 Subject: [PATCH 23/25] refactor: Extract setTimeout milliseconds to constants in ai_modal --- webapp/src/components/modals/ai_modal.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index 407189d27e..a48e0ef8a0 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,4 +1,8 @@ import {WebSocketMessage} from '@mattermost/client'; + +// Timeout Constants +const COPY_FEEDBACK_TIMEOUT_MS = 1000; +const SCROLL_TO_BOTTOM_TIMEOUT_MS = 0; import React, { useCallback, useEffect, @@ -80,7 +84,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const newVersions = [...versions]; newVersions[versions.length - 1] = {...newVersions[versions.length - 1], value: data.next}; setVersions(newVersions); - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0); + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), SCROLL_TO_BOTTOM_TIMEOUT_MS); } else if (data.control === 'end') { setGenerating(false); } @@ -109,7 +113,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { const copyText = useCallback(() => { navigator.clipboard.writeText(versions[currentVersion - 1].value); setCopied(true); - setTimeout(() => setCopied(false), 1000); + setTimeout(() => setCopied(false), COPY_FEEDBACK_TIMEOUT_MS); }, [versions, currentVersion]); const onInputEnter = useCallback((e: React.KeyboardEvent) => { @@ -120,7 +124,7 @@ const AIModal = ({playbookRunId, onAccept, onClose, isOpen}: Props) => { setCurrentVersion(versions.length + 1); setInstruction(''); setGenerating(true); - setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), 0); + setTimeout(() => suggestionBox.current?.scrollTo(0, suggestionBox.current?.scrollHeight), SCROLL_TO_BOTTOM_TIMEOUT_MS); } }, [versions, instruction, playbookRunId, versions, currentVersion, activeBot?.id]); From 40b8e6163f0a24e923e9a02d3f915d840ffe25e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 Jan 2025 15:58:14 +0100 Subject: [PATCH 24/25] refactor: Reorganize imports and constants in ai_modal.tsx --- webapp/src/components/modals/ai_modal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/modals/ai_modal.tsx b/webapp/src/components/modals/ai_modal.tsx index a48e0ef8a0..3049b4bf44 100644 --- a/webapp/src/components/modals/ai_modal.tsx +++ b/webapp/src/components/modals/ai_modal.tsx @@ -1,8 +1,5 @@ import {WebSocketMessage} from '@mattermost/client'; -// Timeout Constants -const COPY_FEEDBACK_TIMEOUT_MS = 1000; -const SCROLL_TO_BOTTOM_TIMEOUT_MS = 0; import React, { useCallback, useEffect, @@ -23,6 +20,9 @@ import {Bot} from 'src/types/ai'; import postEventListener, {PostUpdateWebsocketMessage} from 'src/websocket'; +const COPY_FEEDBACK_TIMEOUT_MS = 1000; +const SCROLL_TO_BOTTOM_TIMEOUT_MS = 0; + type Version = { instruction: string prevValue: string From fc96f7d9fc247f25cf59c0422b8d0add5825f892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sun, 12 Jan 2025 09:22:08 +0100 Subject: [PATCH 25/25] Fixing linter errors --- webapp/src/ai_integration.ts | 2 +- webapp/src/types/ai.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/src/ai_integration.ts b/webapp/src/ai_integration.ts index 5c13a87aed..cb0d6b2fdc 100644 --- a/webapp/src/ai_integration.ts +++ b/webapp/src/ai_integration.ts @@ -4,7 +4,7 @@ import {GlobalState} from 'mattermost-webapp/packages/types/src/store'; import {useSelector} from 'react-redux'; -import {BotSelector, Bot, BotsLoaderHook} from './types/ai'; +import {Bot, BotSelector, BotsLoaderHook} from './types/ai'; export const aiPluginID = 'mattermost-ai'; diff --git a/webapp/src/types/ai.tsx b/webapp/src/types/ai.tsx index 91c92f4ea0..1e06d5d8cf 100644 --- a/webapp/src/types/ai.tsx +++ b/webapp/src/types/ai.tsx @@ -1,13 +1,13 @@ -import React from 'react' +import React from 'react'; export type Bot = { - id: string + id: string } type BotSelectorProps = { - bots: Bot[] - activeBot: Bot - setActiveBot: (bot: Bot) => void + bots: Bot[] + activeBot: Bot + setActiveBot: (bot: Bot) => void } export type BotsLoaderHook = () => BotSelectorProps