Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Dependencies

This document is to note the reasons for needing the dependencies we have.

## nanostores and @nanostores/react

Used in the sidebar code, nanostores is a small framework-agnostic library for reactive state management, while the associated react package provides a hook for optimised component rendering on state changes. react-redux had some stale state issues when used alongside the native `<dialog>` element, causing it to not render with the correct state.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,25 @@
"dependencies": {
"@eeacms/volto-matomo": "6.0.0",
"@microsoft/fetch-event-source": "2.0.1",
"@nanostores/react": "1.0.0",
"dequal": "2.0.3",
"fast-json-patch": "3.1.1",
"highlight.js": "11.10.0",
"luxon": "3.5.0",
"marked": "13.0.3",
"nanostores": "1.0.1",
"node-fetch": "2.7.0",
"react-markdown": "6.0.3",
"react-textarea-autosize": "^8.5.3",
"rehype-prism-plus": "1.6.0",
"remark-gfm": "3.0.1",
"unist-util-visit": "5.0.0",
"uuid": "10.0.0"
},
"peerDependencies": {
"@plone/volto": ">18.0.0 < 19.0.0"
},
"imports": {
"#stores/*": "./src/sidebar/stores/*"
}
}
48 changes: 37 additions & 11 deletions src/ChatBlock/ChatBlockView.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
import React from 'react';
import withDanswerData from './withDanswerData';
import ChatWindow from './ChatWindow';
import superagent from 'superagent';
import ChatWindow from './ChatWindow';
import withDanswerData from './withDanswerData';

import { SidebarChatbotStartButton } from '@eeacms/volto-chatbot/sidebar/components/SidebarChatbotStartButton';

function ChatBlockView(props) {
const OnPageChat = withDanswerData((props) => [
'assistantData',
typeof props.data?.assistant !== 'undefined'
? superagent.get(`/_da/persona/${props.data.assistant}`).type('json')
: null,
props.data?.assistant,
])(function OnPageChat(props) {
const { assistantData, data, isEditMode } = props;

return assistantData ? (
<ChatWindow persona={assistantData} isEditMode={isEditMode} {...data} />
) : (
<div>Chatbot</div>
);
}
});

export default withDanswerData((props) => [
'assistantData',
typeof props.data?.assistant !== 'undefined'
? superagent.get(`/_da/persona/${props.data.assistant}`).type('json')
: null,
props.data?.assistant,
])(ChatBlockView);
export default function ChatBlockView(props) {
const { data, isEditMode } = props;


if (data.displayMode === 'sidebar') {
if (isEditMode) {
return (
<div inert="">
<SidebarChatbotStartButton
assistant={data.assistant}
title={data.sidebarStartButtonText}
/>
</div>
);
}
return (
<SidebarChatbotStartButton
assistant={data.assistant}
title={data.sidebarStartButtonText}
/>
);
}

return <OnPageChat {...props} />;
}
2 changes: 1 addition & 1 deletion src/ChatBlock/ChatMessageBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import visit from 'unist-util-visit';
import { visit } from 'unist-util-visit';
import loadable from '@loadable/component';
import { Button, Message, MessageContent } from 'semantic-ui-react';
import { trackEvent } from '@eeacms/volto-matomo/utils';
Expand Down
1 change: 1 addition & 0 deletions src/ChatBlock/ChatWindow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ function ChatWindow({
{...data}
persona={persona}
onChoice={handleStarterPromptChoice}
starterPromptsHeading={data.displayMode === 'sidebar' ? null : data.starterPromptsHeading}
/>
)}
</>
Expand Down
41 changes: 35 additions & 6 deletions src/ChatBlock/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ export function ChatBlockSchema({ assistants, data }) {
'assistant',
'qgenAsistantId',
'placeholderPrompt',
'height',
'starterPromptsHeading',
'enableStarterPrompts',
...(data.enableStarterPrompts ? ['starterPrompts'] : []),
'starterPromptsHeading',
'starterPromptsPosition',
'showAssistantPrompts',
'enableQgen',
'enableShowTotalFailMessage',
Expand All @@ -92,13 +90,23 @@ export function ChatBlockSchema({ assistants, data }) {
'enableFeedback',
...(data.enableFeedback ? ['feedbackReasons'] : []),
'enableMatomoTracking',
],
},
{
id: 'displaySettings',
title: 'Display settings',
fields: [
'height',
'starterPromptsPosition',
'scrollToInput',
'showToolCalls',
'showAssistantTitle',
'showAssistantDescription',
'chatTitle',
],
},
'displayMode',
...(data.displayMode === "sidebar" ? ['sidebarStartButtonText'] : []),
]
}
],
properties: {
enableShowTotalFailMessage: {
Expand Down Expand Up @@ -301,7 +309,7 @@ range is from 0 to 100`,
default: 'top',
},
starterPromptsHeading: {
title: 'Prompts Heading',
title: data.displayMode === 'sidebar' ? 'Sidebar title' : 'Prompts Heading',
type: 'string',
description:
'Heading shown above the starter prompts (e.g. "Try the following questions")',
Expand Down Expand Up @@ -363,6 +371,27 @@ range is from 0 to 100`,
title: 'Scroll the page to focus on the chat input',
type: 'boolean',
},
displayMode: {
title: 'Display',
type: 'string',
factory: 'Choice',
choices: [
['page', 'On page'],
['sidebar', 'In sidebar'],
],
// Simulate default value without actually setting it so it isn't saved in data.
placeholder: 'above',
noValueOption: false,
},
sidebarStartButtonText: {
title: 'Start button text',
type: 'string'
}
// showInSidebar: {
// title: 'Global mode',
// description: 'Render the chatbot within a sidebar which can be shown by clicking a button. First block on the page has the controls.',
// type: 'boolean'
// }
},
required: [],
};
Expand Down
2 changes: 1 addition & 1 deletion src/ChatBlock/useBackendChat.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function upsertToCompleteMessageMap({
}
}
const newCompleteMessageDetail = {
sessionId: chatSessionId || completeMessageDetail.sessionId,
sessionId: chatSessionId || completeMessageDetail?.sessionId || null, // TODO: sessionid can be null because it was an initial error. what should happen?
messageMap: newCompleteMessageMap,
};
setCompleteMessageDetail(newCompleteMessageDetail);
Expand Down
18 changes: 18 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import installChatBlock from './ChatBlock';
import loadable from '@loadable/component';

import { SidebarEntrypoint } from "@eeacms/volto-chatbot/sidebar/components/SidebarEntrypoint";

const applyConfig = (config) => {
if (__SERVER__) {
const express = require('express');
Expand Down Expand Up @@ -40,6 +42,22 @@ const applyConfig = (config) => {

installChatBlock(config);

config.settings.appExtras = [
...config.settings.appExtras,
{
match: "",
component: SidebarEntrypoint,
},
];

config.settings["volto-chatbot"] = {
...(config.settings["volto-chatbot"] || {}),
sidebar: {
startButtonTitle: "Start assistant chat",
sidebarTitle: "Help using this site",
}
};

return config;
};

Expand Down
9 changes: 9 additions & 0 deletions src/sidebar/components/DefaultChatbotStartButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Button } from "semantic-ui-react";

export function DefaultChatbotStartButton({ onClick, title }) {
return (
<Button primary onClick={onClick} size="big">
{title}
</Button>
);
}
24 changes: 24 additions & 0 deletions src/sidebar/components/SidebarChatbotStartButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { selectedSidebarChatbot } from '#stores/sidebarStore';
import config from '@plone/registry';
import { DefaultChatbotStartButton } from './DefaultChatbotStartButton';

export function SidebarChatbotStartButton({
assistant,
title = 'Start assistant chat',
}) {
const ChatbotStartButton =
config.getComponent('ChatbotStartButton')?.component ||
DefaultChatbotStartButton;

// TODO: Hide the start button until we've checked we support the dialog element and JS is loaded
return (
<div className="block danswerChat">
<ChatbotStartButton
onClick={() => {
selectedSidebarChatbot.set(assistant);
}}
title={title || 'Start assistant chat'}
/>
</div>
);
}
79 changes: 79 additions & 0 deletions src/sidebar/components/SidebarDisplay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { selectedSidebarChatbot } from "#stores/sidebarStore";
import ChatWindow from "@eeacms/volto-chatbot/ChatBlock/ChatWindow";
import { useStore } from "@nanostores/react";
import Icon from "@plone/volto/components/theme/Icon/Icon";
import { Button } from "semantic-ui-react";

// ChatBlock
import { getBlocksFieldname } from "@plone/volto/helpers";
import clearSVG from "@plone/volto/icons/clear.svg";
import { forwardRef } from "react";
import superagent from "superagent";
import withDanswerData from "../../ChatBlock/withDanswerData";

import config from "@plone/registry";

const ChatBlockDisplay = withDanswerData(({ assistant }) => [
"assistantData",
typeof assistant !== "undefined" && assistant !== null
? superagent.get(`/_da/persona/${assistant}`).type("json")
: null,
assistant,
])(function ChatBlockDisplay({ data, assistantData }) {
if (!assistantData) {
return null;
}
return <ChatWindow persona={assistantData} {...data} />;
});

export const SidebarDisplay = forwardRef(function SidebarDisplay(
{ content },
ref,
) {
const $selectedSidebarChatbot = useStore(selectedSidebarChatbot);

const blocksFieldname = getBlocksFieldname(content) || "blocks";

const sidebarBlockData = Object.values(content?.[blocksFieldname] || {}).find(
(block) =>
block["@type"] === "danswerChat" &&
block.assistant == $selectedSidebarChatbot,
);
const sidebarTitle =
sidebarBlockData?.starterPromptsHeading ||
config.settings["volto-chatbot"]?.sidebar?.sidebarTitle ||
"Help using this site";

return (
<>
<div id="chatbot-sidebar">
<dialog
aria-modal="true"
id="chatbot-sidebar-dialog"
aria-labelledby="dialog_heading"
ref={ref}
>
<div className="dialogContent">
<div className="heading">
<Button
type="button"
basic
aria-label={"Close"}
onClick={() => {
selectedSidebarChatbot.set(null);
}}
>
<Icon circled name={clearSVG} size="48px" />
</Button>
<h2 id="dialog_heading">{sidebarTitle}</h2>
</div>
<ChatBlockDisplay
assistant={$selectedSidebarChatbot}
data={sidebarBlockData}
/>
</div>
</dialog>
</div>
</>
);
});
33 changes: 33 additions & 0 deletions src/sidebar/components/SidebarEntrypoint.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { selectedSidebarChatbot } from "#stores/sidebarStore";

import { useStore } from "@nanostores/react";
import { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { SidebarDisplay } from "./SidebarDisplay";

import './SidebarEntrypoint.scss';

export function SidebarEntrypoint() {
const sidebarRef = useRef();
const $selectedSidebarChatbot = useStore(selectedSidebarChatbot);
const content = useSelector((state) => state.content.data);

// Effect for programmatic open/ close via store from elsewhere in the app
useEffect(() => {
const isOpen = $selectedSidebarChatbot !== null;
if (sidebarRef.current) {
if (isOpen && !sidebarRef.current.open) {
sidebarRef.current.showModal();
} else if (!isOpen && sidebarRef.current.open) {
sidebarRef.current.close();
}
}
}, [$selectedSidebarChatbot]);

// TODO: Hide the start button until we've checked we support the dialog element and JS is loaded
return (
<>
<SidebarDisplay content={content} ref={sidebarRef} />
</>
);
}
Loading