@eeacms/volto-chatbot
Version:
@eeacms/volto-chatbot: Volto add-on
162 lines (149 loc) • 5.26 kB
JSX
import React from 'react';
import { Tab, TabPane, Button } from 'semantic-ui-react';
import LinkIcon from '@eeacms/volto-chatbot/icons/external-link.svg';
import { SVGIcon } from '@eeacms/volto-chatbot/ChatBlock/utils';
import { RenderClaimView } from './RenderClaimView';
const VISIBLE_SEGMENTS = 50; // Number of citations to show by default
export function ClaimSegments({ segmentIds, segments, citedSources }) {
const joinedSources = citedSources.reduce((acc, source) => {
source.startIndex = acc.length;
const sep = acc ? '\n' : '';
return acc + sep + source.halloumiContext; // + '\n---\n';
}, '');
const snippets = (segmentIds || [])
.map((id) => {
const segment = segments[id];
if (!segment) {
// eslint-disable-next-line no-console
console.warn(`Could not find segment ${id} in `, segments);
}
return segment;
})
.filter((segment) => !!segment)
.map((segment) => {
const text = joinedSources.slice(
Math.max(0, segment.startOffset), // sometimes startOffset comes as -1
segment.endOffset,
);
const source = citedSources.find((cit) => cit.text.indexOf(text) > -1);
return {
...segment,
text,
// expandedText,
source_id: source?.id,
};
});
const sourcesWithSnippets = citedSources
.map((source) => ({
...source,
snippets: snippets.filter((s) => s.source_id === source.id),
}))
.filter((source) => source.snippets.length > 0)
.sort((sa, sb) => sa.index - sb.index);
// eslint-disable-next-line no-console
// console.log({ snippets, sourcesWithSnippets, segments, citedSources });
const [activeTab, setActiveTab] = React.useState(0);
const [visibleSegmentId, setVisibleSegment] = React.useState();
const [showAllButtons, setShowAllButtons] = React.useState(false);
const segmentContainerRef = React.useRef(null);
const spanRefs = React.useRef({});
const panes = sourcesWithSnippets.map((source, i) => {
const snippetButtons = source.snippets || [];
const segmentButtons = showAllButtons
? snippetButtons
: snippetButtons.slice(0, VISIBLE_SEGMENTS);
return {
menuItem: {
key: i,
content: (
<span title={source?.semantic_identifier}>
{source?.semantic_identifier}
</span>
),
className: `${activeTab === i ? 'active' : ''}`,
onClick: () => {
setActiveTab(i);
},
},
render: () => (
<TabPane>
<div className="claim-source-header">
{source?.link ? (
<a
href={source.link}
rel="noreferrer"
target="_blank"
className="claim-source-link"
>
<h5 className="claim-source-title">
{source.semantic_identifier}
<SVGIcon name={LinkIcon} size="16" />
</h5>
</a>
) : (
<h5 className="claim-source-title">
{source?.semantic_identifier}
</h5>
)}
</div>
<div className="citation-buttons">
<h5 className="citations-header">Citations:</h5>
<div className="citation-buttons-container">
{segmentButtons.map(({ id }) => (
<Button
key={id}
onClick={() => {
const container = segmentContainerRef.current;
const target = spanRefs.current[id];
if (container && target) {
const containerTop =
container.getBoundingClientRect().top;
const targetTop = target.getBoundingClientRect().top;
const scrollOffset =
targetTop - containerTop + container.scrollTop;
container.scrollTo({
top: scrollOffset - 50,
behavior: 'smooth',
});
}
setVisibleSegment(id);
}}
>
Line {id}
</Button>
))}
{snippetButtons.length > VISIBLE_SEGMENTS && (
<Button
className="toggle-text"
role="button"
tabIndex={0}
onClick={() => setShowAllButtons(!showAllButtons)}
>
{showAllButtons ? 'Less' : '... More'}
</Button>
)}
</div>
</div>
<RenderClaimView
contextText={joinedSources}
value={source.halloumiContext}
visibleSegmentId={visibleSegmentId}
segmentContainerRef={segmentContainerRef}
spanRefs={spanRefs}
sourceStartIndex={source.startIndex}
segments={Object.values(segments)}
/>
</TabPane>
),
};
});
return (
<div className="chat-window">
<Tab
menu={{ secondary: true, pointing: true }}
panes={panes}
activeIndex={activeTab}
/>
</div>
);
}