diff --git a/wax-prosemirror-core/src/components/icons/icons.js b/wax-prosemirror-core/src/components/icons/icons.js index e79ca05f4d9554966aff3aae9bbb73bc441aa7f8..f812211f3eac37c21c89971ceaacd5e18fc6bf95 100644 --- a/wax-prosemirror-core/src/components/icons/icons.js +++ b/wax-prosemirror-core/src/components/icons/icons.js @@ -911,4 +911,56 @@ L 5.65 14.6 15.4 9.4 5.65 4.2 6.65 8.9 9.25 9.4 6.65 9.9 Z" </g> </svg> ), + AskKb: ({ className }) => { + return ( + <svg + className={className} + fill="#000000" + version="1.1" + viewBox="0 0 147.923 147.923" + xmlns="http://www.w3.org/2000/svg" + xmlSpace="preserve" + > + <g> + <path + d="M92.336,116.398c1.979,0.609,4.007,1.072,6.065,1.377c-3.021,8.427-11.016,14.516-20.46,14.516H21.824 + C9.789,132.291,0,122.5,0,110.468V41.88c0-12.035,9.789-21.824,21.824-21.824h16.797C40.149,9.513,49.164,1.35,60.127,1.35h30.279 + c12.032,0,21.823,9.789,21.823,21.824V38.04c-2.046-0.42-4.129-0.688-6.235-0.779V23.174c0-8.592-6.99-15.588-15.588-15.588H60.127 + c-7.526,0-13.825,5.362-15.271,12.471h33.091c10.528,0,19.333,7.496,21.373,17.421c-2.058,0.25-4.086,0.701-6.107,1.267 + c-1.449-7.097-7.745-12.453-15.259-12.453H21.836c-8.592,0-15.588,6.997-15.588,15.589v68.588c0,8.586,6.997,15.589,15.588,15.589 + h56.118C84.433,126.05,89.998,122.049,92.336,116.398z M123.05,110.322c-17.993,10.388-41.077,4.201-51.462-13.786 + C61.199,78.555,67.389,55.468,85.37,45.085c17.987-10.382,41.077-4.205,51.466,13.786 + C147.224,76.855,141.025,99.934,123.05,110.322z M119.933,104.927c15.01-8.659,20.161-27.925,11.496-42.938 + c-8.659-15.016-27.925-20.18-42.94-11.512c-15.013,8.668-20.168,27.925-11.503,42.94S104.916,113.592,119.933,104.927z + M132.513,108.008l-16.21,9.354l12.471,21.592l16.21-9.354L132.513,108.008z M146.664,132.535l-16.204,9.353 + c2.582,4.481,8.306,6.017,12.775,3.423C147.711,142.729,149.258,137.011,146.664,132.535z M60.201,42.646H18.7v4.677h41.5V42.646z + M50.656,63.436H18.7v4.676h31.956V63.436z M18.7,88.895h31.956v-4.677H18.7V88.895z M18.7,109.676h41.5V105H18.7V109.676z + M98.133,84.188l-9.682-10.727l-4.628,4.187l13.695,15.149l26.755-22.283l-3.994-4.789L98.133,84.188z" + /> + </g> + </svg> + ); + }, + CustomPromptsOn: ({ className }) => { + return ( + <svg + className={className} + fill="#000000" + viewBox="0 0 52 52" + xmlns="http://www.w3.org/2000/svg" + xmlSpace="preserve" + > + <path + clipRule="evenodd" + d="M47,4.5H5c-1.7,0-3,1.3-3,3v30.6c0,1.7,1.3,3,3,3h14.5l3.6,5.2 + c1,1.4,2.8,1.7,4.2,0.7c0.2-0.2,0.4-0.4,0.6-0.6l4.2-5.3H47c1.7,0,3-1.3,3-3V7.5C50,5.8,48.7,4.5,47,4.5z M21.3,32L21.3,32 + c-0.2,0.2-0.3,0.3-0.5,0.3l-5,1.2c-0.5,0.1-0.9-0.3-0.8-0.8l1.2-5c0-0.1,0.1-0.3,0.2-0.4l0.1-0.1c0.1-0.1,0.4-0.1,0.6,0l4.2,4.2 + C21.4,31.6,21.4,31.9,21.3,32z M33.4,19.7L23.1,30c-0.2,0.2-0.5,0.2-0.6,0l-4.1-4.1c-0.2-0.1-0.2-0.4,0-0.6L28.6,15 + c0.2-0.2,0.5-0.2,0.6,0l4.1,4.1C33.5,19.3,33.5,19.5,33.4,19.7z M36.5,16.7l-1.2,1.2c-0.2,0.2-0.5,0.2-0.6,0l-4.1-4.1 + c-0.2-0.2-0.2-0.5,0-0.6l1.1-1.2c0.7-0.7,1.9-0.7,2.6,0l2.2,2.2C37.2,14.9,37.2,16,36.5,16.7z" + fillRule="evenodd" + /> + </svg> + ); + }, }; diff --git a/wax-prosemirror-services/src/AiService/components/AiSettingsMenu.js b/wax-prosemirror-services/src/AiService/components/AiSettingsMenu.js index b53e50665d8a19b55c371ce8aa42933775141212..9bbf95db227a0756c7e0fd5784e75c8f73e9f2b9 100644 --- a/wax-prosemirror-services/src/AiService/components/AiSettingsMenu.js +++ b/wax-prosemirror-services/src/AiService/components/AiSettingsMenu.js @@ -2,12 +2,12 @@ import React, { useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { th } from '@pubsweet/ui-toolkit'; -import { WaxContext } from 'wax-prosemirror-core'; +import { WaxContext, icons } from 'wax-prosemirror-core'; import { keys } from 'lodash'; import SwitchComponent from './Switch'; // #region CONSTANTS --------------------------------------------- -const LABEL_POS = 'left'; +// const LABEL_POS = 'left'; const SETTINGS = [ { @@ -85,44 +85,76 @@ export const ToggleInput = styled(SwitchComponent)` } `; -const SettingsDropdown = styled.ul` - --item-height: var(--ai-settings-menu-height, 30px); - --items-length: ${p => p.$listlng}; - --max-height: calc(var(--item-height) * var(--items-length)); - - background-color: #f8f8f8; - border: ${p => (p.$show ? 'var(--ai-tool-border)' : '0 solid #0000')}; - border-radius: 0 0 0.3rem 0.3rem; - border-top: none; +// const SettingsDropdown = styled.ul` +// --item-height: var(--ai-settings-menu-height, 30px); +// --items-length: ${p => p.$listlng}; +// --max-height: calc(var(--item-height) * var(--items-length)); + +// background-color: #f8f8f8; +// border: ${p => (p.$show ? 'var(--ai-tool-border)' : '0 solid #0000')}; +// border-radius: 0 0 0.3rem 0.3rem; +// border-top: none; +// display: flex; +// flex-direction: column; +// list-style: none; +// margin: 0; +// max-height: ${p => (p.$show ? 'var(--max-height)' : 0)}; +// overflow: hidden; +// padding: 0 0.5rem; +// position: absolute; +// right: calc(-1 * var(--ai-tool-border-width)); +// top: calc(100% + var(--ai-tool-border-width)); +// transition: all ${p => `${0.15 * p.$listlng}s`}; +// z-index: 15; + +// > li { +// align-items: center; +// display: flex; +// height: var(--item-height); +// justify-content: ${LABEL_POS === 'right' ? 'flex-start' : 'flex-end'}; +// padding: 0; +// user-select: none; +// } + +// > li:not(:first-child) { +// border-top: 1px solid #0001; +// } +// `; + +const SettingsContainer = styled.div` + align-items: center; + background-color: #fafafa; + border-bottom: var(--ai-tool-border); display: flex; - flex-direction: column; - list-style: none; - margin: 0; - max-height: ${p => (p.$show ? 'var(--max-height)' : 0)}; - overflow: hidden; - padding: 0 0.5rem; - position: absolute; - right: calc(-1 * var(--ai-tool-border-width)); - top: calc(100% + var(--ai-tool-border-width)); - transition: all ${p => `${0.15 * p.$listlng}s`}; - z-index: 15; - - > li { - align-items: center; + gap: 5px; + justify-content: space-between; + padding: 2px 5px; + width: 100%; + + span { display: flex; - height: var(--item-height); - justify-content: ${LABEL_POS === 'right' ? 'flex-start' : 'flex-end'}; - padding: 0; - user-select: none; + gap: 5px; } +`; + +const SettingsButton = styled.button.attrs({ type: 'button' })` + align-items: center; + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + /* outline: ${p => (p.$checked ? 'var(--ai-tool-border)' : 'none')}; */ + padding: 2px; - > li:not(:first-child) { - border-top: 1px solid #0001; + > svg { + fill: ${p => (p.$checked ? '#000' : '#aaa')}; + width: 15px; } `; // #endregion STYLED COMPONENTS --------------------------------------------- -const AiSettingsMenu = ({ show, aiService, optionsState, setOptionsState }) => { +const AiSettingsMenu = ({ aiService, optionsState, setOptionsState }) => { const ctx = useContext(WaxContext); const onAiService = ({ key }) => keys(aiService).includes(key); @@ -155,16 +187,18 @@ const AiSettingsMenu = ({ show, aiService, optionsState, setOptionsState }) => { const renderSettings = ({ key, label, type }) => { const value = optionsState[key]; + const Icon = icons[key] ?? null; const typeBasedSettingUI = { boolean: ( - <li key={key}> - <ToggleInput - checked={optionsState[key]} - label={label} - labelPosition={LABEL_POS} - onChange={handlers(key, type, value)} - /> - </li> + <SettingsButton + $checked={optionsState[key]} + key={key} + onClick={handlers(key, type, value)} + title={`${label} (${value ? 'enabled' : 'disabled'})`} + > + {Icon ? <Icon /> : ''} + {Icon ? '' : label} + </SettingsButton> ), }; @@ -172,24 +206,21 @@ const AiSettingsMenu = ({ show, aiService, optionsState, setOptionsState }) => { }; return ( - <SettingsDropdown - $listlng={enabledSettings.length} - $show={show} + <SettingsContainer onClick={e => e.stopPropagation()} // to prevent !showSettings > - {enabledSettings?.map(renderSettings)} - </SettingsDropdown> + <span>AI Wizard</span> + <span>{enabledSettings?.map(renderSettings)}</span> + </SettingsContainer> ); }; AiSettingsMenu.propTypes = { - show: PropTypes.bool, aiService: PropTypes.shape({}), optionsState: PropTypes.shape({}), setOptionsState: PropTypes.func, }; AiSettingsMenu.defaultProps = { - show: false, aiService: {}, optionsState: {}, setOptionsState: () => {}, diff --git a/wax-prosemirror-services/src/AiService/components/AskAIOverlay.js b/wax-prosemirror-services/src/AiService/components/AskAIOverlay.js index 99aee0d2a69e7d31d33d5e4e22c2f912a1587dcd..f73dd4fa1b6435b1e1861e4baa8544225f032169 100644 --- a/wax-prosemirror-services/src/AiService/components/AskAIOverlay.js +++ b/wax-prosemirror-services/src/AiService/components/AskAIOverlay.js @@ -1,7 +1,7 @@ /* eslint-disable react/jsx-props-no-spreading */ import React, { useRef, useLayoutEffect, useContext, useState } from 'react'; import styled from 'styled-components'; -import { capitalize, isEmpty } from 'lodash'; +import { capitalize, isEmpty, keys } from 'lodash'; import { useTranslation } from 'react-i18next'; import { WaxContext, icons } from 'wax-prosemirror-core'; import { PropTypes } from 'prop-types'; @@ -17,6 +17,8 @@ const DEFAULT_KEY = 'content'; // #region MAIN & MISC------------------- const Root = styled.div` + --ai-tool-result-height: 500px; + --ai-tool-width: 100%; --ai-tool-border-width: 1px; --ai-tool-border: var(--ai-tool-border-width) solid #0001; align-items: flex-end; @@ -25,6 +27,10 @@ const Root = styled.div` display: flex; filter: drop-shadow(0 0 1px #0002); flex-direction: column; + margin: 0 10px 10px; + max-width: 95%; + min-width: 500px; + width: var(--ai-tool-width); div { ::-webkit-scrollbar { @@ -34,7 +40,6 @@ const Root = styled.div` ::-webkit-scrollbar-thumb { background: var(--scrollbar-color, #0004); - border-radius: 5px; width: 5px; } @@ -66,6 +71,11 @@ const FlexRow = styled.div` flex-direction: row; width: 100%; `; +const FlexCol = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; const ButtonBase = styled.button.attrs({ type: 'button' })` align-items: center; background: none; @@ -83,15 +93,11 @@ const ButtonBase = styled.button.attrs({ type: 'button' })` // #region FORM ------------------------- -const AskAIForm = styled.form` +const AskAIForm = styled(FlexCol)` background: #fafafa; - border-radius: 0.3rem 0.3rem 0 0; display: ${p => (p.$show ? 'flex' : 'none')}; - flex-direction: column; - justify-content: space-between; padding: 8px 12px; position: relative; - width: 458px; `; const PromptInput = styled.input` @@ -107,19 +113,9 @@ const PromptInput = styled.input` `; const SendButton = styled(ButtonBase)` - border-right: 1px solid #0001; margin-bottom: 3px; - padding: 0 8px; -`; - -const SettingsButton = styled(ButtonBase)` - padding: 0 0 0 8px; - - > svg { - fill: #777; - height: 16px; - width: 16px; - } + padding: 0 0 0 5px; + transform: scale(1.3); `; // #endregion FORM -------------------------- @@ -132,35 +128,31 @@ const ResultContainer = styled.div` display: flex; flex-direction: column; gap: 0; - height: 150px; + height: fit-content; justify-content: flex-start; - max-height: ${p => (p.$show ? '150px' : '0')}; + max-height: ${p => (p.$show ? 'var(--ai-tool-result-height)' : '0')}; + min-height: ${p => (p.$show ? '200px' : '0')}; overflow-y: auto; padding: 0; position: relative; transition: all 0.3s; - width: 458px; + width: 100%; `; const ResultHeading = styled(Heading)` align-items: flex-end; + padding-left: 0; `; const ResultTab = styled(ButtonBase)` background: ${p => (p.$selected ? '#fff' : '#fff4')}; border: var(--ai-tool-border); border-bottom-color: ${p => (p.$selected ? '#fff' : '#0001')}; - border-radius: 5px 5px 0 0; - color: #777; - font-size: 11px; - font-style: italic; margin-bottom: -1px; padding: 3px 0.5rem; text-decoration: underline; text-decoration-color: #bbb0; text-underline-offset: 5px; - transform: scale(${p => (p.$selected ? '1' : '0.93')}); - transform-origin: bottom; transition: all 0.2s; z-index: 9; @@ -233,12 +225,11 @@ const CustomPromptContainer = styled.div` border-top: ${p => (p.$show ? 'var(--ai-tool-border)' : '0 solid #0000')}; display: flex; flex-direction: column; - font-style: italic; max-height: ${p => (p.$show ? '205px' : '0')}; overflow: hidden; padding: 0; transition: all 0.3s linear; - width: 458px; + width: 100%; `; const CustomPromptsHeading = styled(Heading)` @@ -306,6 +297,7 @@ const AskAIOverlay = ({ setPosition, position, config }) => { options, } = ctx; const inputRef = useRef(null); + const resultRef = useRef(null); const [userPrompt, setUserPrompt] = useState(''); const [optionsState, setOptionsState] = useState({ ...options }); @@ -314,7 +306,6 @@ const AskAIOverlay = ({ setPosition, position, config }) => { const [result, setResult] = useState({ [DEFAULT_KEY]: '' }); const [resultKey, setResultKey] = useState(DEFAULT_KEY); - const [showSettings, setShowSettings] = useState(false); const aiService = app.config.get('config.AskAiContentService'); const { AskAiContentTransformation } = config; @@ -364,11 +355,6 @@ const AskAIOverlay = ({ setPosition, position, config }) => { } }; - const handleShowSettings = e => { - e.stopPropagation(); // to prevent !showSettings - setShowSettings(!showSettings); - }; - const handleSubmit = async () => { if (!enabled.send) { inputRef.current.focus(); @@ -388,11 +374,12 @@ const AskAIOverlay = ({ setPosition, position, config }) => { askKb: optionsState.AskKb, }); const processedRes = safeParse(response, DEFAULT_KEY); + setResultKey(keys(processedRes)[0] ?? DEFAULT_KEY); + setResult(processedRes); } catch (error) { setResult({ [DEFAULT_KEY]: error }); } finally { - setResultKey(DEFAULT_KEY); setIsSubmitted(true); setIsLoading(false); } @@ -465,7 +452,12 @@ const AskAIOverlay = ({ setPosition, position, config }) => { // #endregion UI ------------------------------- return enabled.component ? ( - <Root id={AI_TOOL_ID} onClick={() => setShowSettings(false)}> + <Root id={AI_TOOL_ID}> + <AiSettingsMenu + aiService={aiService} + optionsState={optionsState} + setOptionsState={setOptionsState} + /> <AskAIForm $show> <FlexRow> <PromptInput @@ -484,16 +476,7 @@ const AskAIOverlay = ({ setPosition, position, config }) => { <SendButton disabled={!enabled.send} onClick={handleSubmit}> {submitIcon} </SendButton> - <SettingsButton onClick={handleShowSettings}> - <icons.settings /> - </SettingsButton> </FlexRow> - <AiSettingsMenu - aiService={aiService} - optionsState={optionsState} - setOptionsState={setOptionsState} - show={showSettings} - /> </AskAIForm> <ResultContainer $show={enabled.results}> @@ -509,12 +492,11 @@ const AskAIOverlay = ({ setPosition, position, config }) => { ))} </ResultHeading> <ResultContent + contentEditable dangerouslySetInnerHTML={{ - __html: `<h4>${capitalize(resultKey)}</h4>${resultsToHtml( - resultKey, - result[resultKey], - )}</br>`, + __html: resultsToHtml(resultKey, result[resultKey]), }} + ref={resultRef} // to get the text from this innerHTML in the future /> <ResultActions $show={enabled.results}> {Object.values(resultActions).map(({ title, Icon, ...rest }) => (