Skip to content

Commit 769fd91

Browse files
authored
feat(assistants_web): new citations style (cohere-ai#583)
1 parent 3924b32 commit 769fd91

17 files changed

+170
-1160
lines changed

src/interfaces/assistants_web/src/app/(main)/(chat)/Chat.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ const Chat: React.FC<{ agentId?: string; conversationId?: string }> = ({
5656

5757
// Reset citations and file params when switching between conversations
5858
useEffect(() => {
59-
resetCitations();
6059
resetFileParams();
6160

6261
const agentTools = (agent?.tools

src/interfaces/assistants_web/src/components/Agents/AgentRightPanel.tsx

+1-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Transition } from '@headlessui/react';
44
import { uniqBy } from 'lodash';
55
import { useMemo, useState } from 'react';
66

7-
import { CitationsTab } from '@/components/Agents/CitationsTab';
87
import { IconButton } from '@/components/IconButton';
98
import { Banner, Button, Icon, Switch, Tabs, Text, Tooltip } from '@/components/Shared';
109
import { TOOL_GOOGLE_DRIVE_ID, TOOL_READ_DOCUMENT_ID, TOOL_SEARCH_FILE_ID } from '@/constants';
@@ -93,10 +92,6 @@ const AgentRightPanel: React.FC<Props> = () => {
9392
<Icon name="folder" kind="outline" />
9493
Knowledge
9594
</span>,
96-
<span className="flex items-center gap-x-2" key="citations">
97-
<Icon name="link" kind="outline" />
98-
Citations
99-
</span>,
10095
]}
10196
tabGroupClassName="h-full"
10297
tabPanelClassName="h-full"
@@ -220,7 +215,7 @@ const AgentRightPanel: React.FC<Props> = () => {
220215
</Text>
221216
</section>
222217
</div>
223-
<CitationsTab />
218+
<></>
224219
</Tabs>
225220
);
226221
};

src/interfaces/assistants_web/src/components/Agents/CitationsTab.tsx

-51
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,94 @@
1-
'use client';
1+
import { useState } from 'react';
22

3-
import { Transition } from '@headlessui/react';
4-
import { flatten, sortBy, uniqBy } from 'lodash';
5-
import React, { useRef } from 'react';
6-
import { useMemo, useState } from 'react';
7-
8-
import { Document } from '@/cohere-client';
9-
import { CitationDocument } from '@/components/Citations/CitationDocument';
10-
import { IconButton } from '@/components/IconButton';
11-
import { Text } from '@/components/Shared/Text';
12-
import { ReservedClasses } from '@/constants';
13-
import { CitationStyles, useCalculateCitationTranslateY } from '@/hooks/citations';
3+
import { DocumentIcon, Icon, Text } from '@/components/Shared';
4+
import { TOOL_ID_TO_DISPLAY_INFO, TOOL_WEB_SEARCH_ID } from '@/constants';
145
import { useCitationsStore } from '@/stores';
15-
import { cn, pluralize } from '@/utils';
6+
import { getSafeUrl, getWebDomain } from '@/utils';
7+
8+
const getWebSourceName = (toolId?: string | null) => {
9+
if (!toolId) {
10+
return '';
11+
} else if (toolId === TOOL_WEB_SEARCH_ID) {
12+
return 'from the web';
13+
}
14+
return `from ${toolId}`;
15+
};
1616

1717
type Props = {
1818
generationId: string;
19-
message: string;
20-
isLastStreamed?: boolean;
21-
styles?: CitationStyles;
22-
className?: string;
19+
citationKey: string;
2320
};
2421

25-
export const DEFAULT_NUM_VISIBLE_DOCS = 3;
26-
27-
/**
28-
* Placeholder component for a citation.
29-
* This component is in charge of rendering the citations for a given generation.
30-
* @params {string} generationId - the id of the generation
31-
* @params {string} message - the message that was sent
32-
* @params {boolean} isLastStreamed - if the citation is for the last streamed message
33-
* @params {number} styles - top and bottom styling, depending on the associated message row
34-
* @params {string} className - additional class names to add to the citation
35-
*/
36-
export const Citation = React.forwardRef<HTMLDivElement, Props>(function CitationInternal(
37-
{ generationId, message, className = '', styles, isLastStreamed = false },
38-
ref
39-
) {
40-
const {
41-
citations: { citationReferences, selectedCitation, hoveredGenerationId },
42-
hoverCitation,
43-
} = useCitationsStore();
44-
45-
const containerRef = useRef<HTMLDivElement>(null);
46-
const [keyword, setKeyword] = useState('');
47-
const isSelected = selectedCitation?.generationId === generationId;
48-
const isSomeSelected = !!selectedCitation?.generationId;
49-
const isHovered = hoveredGenerationId === generationId;
50-
const [isAllDocsVisible, setIsAllDocsVisible] = useState(false);
51-
52-
const startEndKeyToDocs = citationReferences[generationId];
53-
const documents: Document[] = useMemo(() => {
54-
if (!startEndKeyToDocs) {
55-
return [];
56-
}
57-
58-
if (selectedCitation && generationId === selectedCitation.generationId) {
59-
setKeyword(message.slice(Number(selectedCitation.start), Number(selectedCitation.end)));
60-
return startEndKeyToDocs[`${selectedCitation.start}-${selectedCitation.end}`];
61-
} else {
62-
const firstCitedTextKey = Object.keys(startEndKeyToDocs)[0];
63-
const [start, end] = firstCitedTextKey.split('-');
64-
setKeyword(message.slice(Number(start), Number(end)));
65-
return startEndKeyToDocs[firstCitedTextKey];
66-
}
67-
}, [startEndKeyToDocs, selectedCitation, generationId, message]);
68-
69-
const translateY = useCalculateCitationTranslateY({
70-
generationId,
71-
citationRef: containerRef,
72-
});
73-
74-
if (!startEndKeyToDocs || documents.length === 0 || (!isSelected && !!selectedCitation)) {
75-
return null;
76-
}
77-
78-
const highlightedDocumentIds = documents
79-
.slice(0, DEFAULT_NUM_VISIBLE_DOCS)
80-
.map((doc) => doc.document_id);
81-
82-
const uniqueDocuments = sortBy(
83-
uniqBy(flatten(Object.values(startEndKeyToDocs)), 'document_id'),
84-
'document_id'
85-
);
86-
const uniqueDocumentsUrls = uniqBy(uniqueDocuments, 'url');
87-
88-
const handleMouseEnter = () => {
89-
hoverCitation(generationId);
90-
};
91-
92-
const handleMouseLeave = () => {
93-
hoverCitation(null);
94-
};
95-
96-
const handleToggleAllDocsVisible = () => {
97-
setIsAllDocsVisible(!isAllDocsVisible);
98-
};
22+
export const Citation: React.FC<Props> = ({ generationId, citationKey }) => {
23+
const [selectedIndex, setSelectedIndex] = useState(0);
24+
const { citations } = useCitationsStore();
25+
const citationsMap = citations.citationReferences[generationId];
26+
const documents = citationsMap[citationKey];
27+
const document = documents[selectedIndex];
28+
const safeUrl = document.url ? getSafeUrl(document.url) : undefined;
9929

10030
return (
101-
<Transition
102-
as="div"
103-
id={generationId ? `citation-${generationId}` : undefined}
104-
show={true}
105-
enter="delay-300 duration-300 ease-out transition-[transform,opacity]" // delay to wait for the citation side panel to open
106-
enterFrom="translate-x-2 opacity-0"
107-
enterTo="translate-x-0 opacity-100"
108-
leave="duration-300 ease-in transition-[transform,opacity]"
109-
leaveFrom="translate-x-0 opacity-100"
110-
leaveTo="translate-x-2 opacity-0"
111-
ref={containerRef}
112-
style={{
113-
...styles,
114-
...(translateY !== 0 && isSelected
115-
? {
116-
'--selectedTranslateY': `${translateY}px`,
117-
}
118-
: {}),
119-
}}
120-
className={cn(
121-
'w-[260px] max-w-[260px] rounded',
122-
'bg-marble-1000 transition-[transform,top] duration-300 ease-in-out dark:bg-volcanic-200',
123-
'md:absolute',
124-
{
125-
'md:-translate-x-1': isHovered,
126-
'md:z-selected-citation': isSelected || isAllDocsVisible || isHovered,
127-
'md:translate-y-[var(--selectedTranslateY)] md:shadow-lg': isSelected,
128-
}
129-
)}
130-
>
131-
<div
132-
ref={ref}
133-
className={cn(
134-
ReservedClasses.CITATION,
135-
'rounded md:p-3',
136-
'transition-[colors,opacity] duration-300 ease-in-out',
137-
{
138-
'opacity-60 dark:opacity-100':
139-
!isSelected && !isHovered && (!isLastStreamed || isSomeSelected),
140-
'opacity-90 dark:opacity-100': !isSelected && isHovered,
141-
'bg-mushroom-400/[0.08 dark:bg-volcanic-200': !isSelected,
142-
'bg-coral-700/[0.08] dark:bg-volcanic-200': isSelected,
143-
'flex flex-col gap-y-4 lg:gap-y-6': isSelected,
144-
},
145-
className
146-
)}
147-
onMouseEnter={handleMouseEnter}
148-
onMouseLeave={handleMouseLeave}
149-
>
150-
<Text className="text-coral-300 md:hidden dark:text-marble-950">{keyword}</Text>
151-
152-
<div className={cn('mb-4 flex items-center justify-between', { hidden: isSelected })}>
153-
<Text as="span" styleAs="caption" className="text-volcanic-300 dark:text-marble-800">
154-
{uniqueDocumentsUrls.length} {pluralize('reference', uniqueDocumentsUrls.length)}
155-
</Text>
156-
{uniqueDocumentsUrls.length > DEFAULT_NUM_VISIBLE_DOCS && (
157-
<IconButton
158-
className={cn(
159-
'h-4 w-4 fill-volcanic-300 transition delay-75 duration-200 ease-in-out dark:fill-marble-800',
160-
{
161-
'rotate-180': isAllDocsVisible,
162-
}
163-
)}
164-
onClick={handleToggleAllDocsVisible}
165-
iconName="chevron-down"
166-
/>
167-
)}
31+
<div className="space-y-4">
32+
<header className="flex items-center justify-between">
33+
<div className="flex gap-2">
34+
<div className="grid size-8 place-items-center rounded bg-white dark:bg-volcanic-150">
35+
{document.url ? (
36+
<a href={safeUrl} target="_blank" data-connectorid={document.tool_name}>
37+
<DocumentIcon url={safeUrl} />
38+
</a>
39+
) : document.tool_name ? (
40+
<Icon name={TOOL_ID_TO_DISPLAY_INFO[document.tool_name].icon} />
41+
) : (
42+
<Icon name="file" />
43+
)}
44+
</div>
45+
<div>
46+
{document.url ? (
47+
<>
48+
<Text styleAs="p-xs" className="uppercase dark:text-marble-800">
49+
{getWebDomain(safeUrl) + ' ' + getWebSourceName(document.tool_name)}
50+
</Text>
51+
<Text styleAs="p-sm" className="uppercase dark:text-marble-950">
52+
{document.title || 'Untitled'}
53+
</Text>
54+
</>
55+
) : (
56+
<>
57+
<Text styleAs="p-xs" className="uppercase dark:text-marble-800">
58+
Tool
59+
</Text>
60+
<Text styleAs="p-sm" className="uppercase dark:text-marble-950">
61+
{document.tool_name}
62+
</Text>
63+
</>
64+
)}
65+
</div>
16866
</div>
169-
170-
<div className="flex w-full flex-col gap-y-4">
171-
{isSelected
172-
? uniqueDocuments.map((doc) => {
173-
const isVisible = highlightedDocumentIds.includes(doc.document_id);
174-
175-
if (!isVisible) {
176-
return null;
177-
}
178-
179-
return (
180-
<CitationDocument
181-
key={doc.document_id}
182-
isExpandable={isSelected}
183-
document={doc}
184-
keyword={keyword}
185-
/>
186-
);
187-
})
188-
: uniqueDocumentsUrls.map((doc, index) => {
189-
const isVisible = isAllDocsVisible || index < DEFAULT_NUM_VISIBLE_DOCS;
190-
191-
if (!isVisible) {
192-
return null;
193-
}
194-
195-
return (
196-
<CitationDocument
197-
key={doc.url}
198-
isExpandable={isSelected}
199-
document={doc}
200-
keyword={keyword}
201-
/>
202-
);
203-
})}
204-
</div>
205-
</div>
206-
</Transition>
67+
{documents.length > 1 && (
68+
<div className="flex flex-shrink-0 items-center">
69+
<button
70+
className="py-[3px] pr-2"
71+
onClick={() =>
72+
setSelectedIndex((prev) => (prev - 1 + documents.length) % documents.length)
73+
}
74+
>
75+
<Icon name="chevron-left" />
76+
</button>
77+
<Text className="text-p-sm">
78+
{selectedIndex + 1} of {documents.length}
79+
</Text>
80+
<button
81+
className="py-[3px] pl-2"
82+
onClick={() => setSelectedIndex((prev) => (prev + 1) % documents.length)}
83+
>
84+
<Icon name="chevron-right" />
85+
</button>
86+
</div>
87+
)}
88+
</header>
89+
<article className="max-h-64 overflow-y-auto">
90+
<Text className="font-variable">{document.text}</Text>
91+
</article>
92+
</div>
20793
);
208-
});
94+
};

0 commit comments

Comments
 (0)