mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 00:14:02 +00:00
Open the sources list from a citation that has no card
Only the first three sources are shown as cards, so a pill for [4] or later scrolled to nothing; it now opens the full list.
This commit is contained in:
1 parent
c0c587de81
commit
1c9cf7b693
4 files changed
+58
-3
No files matched your search
@@ -28,6 +28,7 @@ type AnswerFlowProps = {
|
||||
isStreaming?: boolean;
|
||||
/** How many sources the answer can cite; see `MarkdownAnswer`. */
|
||||
sourceCount?: number;
|
||||
onOpenSources?: () => void;
|
||||
agentId?: string;
|
||||
/** Set when the bubble already carries its own progress UI (a research run). */
|
||||
suppressStatusLine?: boolean;
|
||||
@@ -54,6 +55,7 @@ export default function AnswerFlow({
|
||||
segments,
|
||||
isStreaming,
|
||||
sourceCount,
|
||||
onOpenSources,
|
||||
agentId,
|
||||
suppressStatusLine,
|
||||
artifacts,
|
||||
@@ -140,6 +142,7 @@ export default function AnswerFlow({
|
||||
content={message}
|
||||
isStreaming={isStreaming}
|
||||
sourceCount={sourceCount}
|
||||
onOpenSources={onOpenSources}
|
||||
artifacts={artifacts}
|
||||
turnArtifacts={turnArtifacts}
|
||||
onOpenArtifact={onOpenArtifact}
|
||||
|
||||
@@ -16,7 +16,14 @@ import {
|
||||
ThumbsUp,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { forwardRef, Fragment, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
@@ -126,6 +133,7 @@ const ConversationBubble = forwardRef<
|
||||
const [shouldShowToggle, setShouldShowToggle] = useState(false);
|
||||
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
|
||||
const openSources = useCallback(() => setIsSidebarOpen(true), []);
|
||||
const editableQueryRef = useRef<HTMLDivElement>(null);
|
||||
const [isQuestionCollapsed, setIsQuestionCollapsed] = useState(true);
|
||||
|
||||
@@ -452,6 +460,7 @@ const ConversationBubble = forwardRef<
|
||||
// A citation pill jumps to a source card, so an answer whose
|
||||
// sources are not shown has nothing to cite.
|
||||
sourceCount={showSources ? (sources?.length ?? 0) : 0}
|
||||
onOpenSources={openSources}
|
||||
agentId={agentId}
|
||||
// A research run already narrates itself above; the status line
|
||||
// would be a second live indicator away from the point of action.
|
||||
|
||||
@@ -51,7 +51,11 @@ afterEach(() => {
|
||||
container.remove();
|
||||
});
|
||||
|
||||
type RenderOptions = { isStreaming?: boolean; sourceCount?: number };
|
||||
type RenderOptions = {
|
||||
isStreaming?: boolean;
|
||||
sourceCount?: number;
|
||||
onOpenSources?: () => void;
|
||||
};
|
||||
|
||||
function render(content: string, options: RenderOptions = {}) {
|
||||
act(() => root.render(<MarkdownAnswer content={content} {...options} />));
|
||||
@@ -418,6 +422,39 @@ describe('citations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('citation pills', () => {
|
||||
function click(label: string) {
|
||||
const pill = Array.from(container.querySelectorAll('button')).find(
|
||||
(node) => node.textContent === label,
|
||||
);
|
||||
act(() => pill?.click());
|
||||
}
|
||||
|
||||
it('scrolls to the source card when it is shown', () => {
|
||||
const card = document.createElement('div');
|
||||
card.id = 'source-1';
|
||||
card.scrollIntoView = vi.fn();
|
||||
document.body.appendChild(card);
|
||||
const onOpenSources = vi.fn();
|
||||
try {
|
||||
render('Per [2].', { sourceCount: 5, onOpenSources });
|
||||
click('2');
|
||||
expect(card.scrollIntoView).toHaveBeenCalled();
|
||||
expect(onOpenSources).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
card.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it('opens the sources list for a source with no card', () => {
|
||||
// Only the first three sources get a card; `[5]` has none to scroll to.
|
||||
const onOpenSources = vi.fn();
|
||||
render('Per [5].', { sourceCount: 5, onOpenSources });
|
||||
click('5');
|
||||
expect(onOpenSources).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mermaid', () => {
|
||||
it('renders a closed mermaid fence as a diagram, source untouched', () => {
|
||||
render(
|
||||
|
||||
@@ -83,6 +83,7 @@ export default function MarkdownAnswer({
|
||||
artifacts: artifactsProp,
|
||||
turnArtifacts: turnArtifactsProp,
|
||||
onOpenArtifact,
|
||||
onOpenSources,
|
||||
}: {
|
||||
content: string;
|
||||
isStreaming?: boolean;
|
||||
@@ -100,6 +101,8 @@ export default function MarkdownAnswer({
|
||||
/** This turn's own artifacts; the filename fallback prefers them. */
|
||||
turnArtifacts?: SandboxArtifact[];
|
||||
onOpenArtifact?: (artifact: { id: string; toolName: string }) => void;
|
||||
/** Opens the full sources list, for a citation whose card is not shown. */
|
||||
onOpenSources?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
@@ -194,6 +197,9 @@ export default function MarkdownAnswer({
|
||||
() => el.classList.remove('ring-3', 'ring-primary'),
|
||||
2000,
|
||||
);
|
||||
} else {
|
||||
// Only the first few sources get a card to scroll to.
|
||||
onOpenSources?.();
|
||||
}
|
||||
}}
|
||||
className="mx-0.5 h-5 min-w-5"
|
||||
@@ -321,7 +327,7 @@ export default function MarkdownAnswer({
|
||||
return <td className="px-6 py-3">{children}</td>;
|
||||
},
|
||||
};
|
||||
}, [t, isDarkTheme, artifacts, turnArtifacts, onOpenArtifact]);
|
||||
}, [t, isDarkTheme, artifacts, turnArtifacts, onOpenArtifact, onOpenSources]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user