Default chat to THINKING; open citations in new tab (#106, #108).
Unit Tests / test (pull_request) Successful in 11s
Unit Tests / test (pull_request) Successful in 11s
Drop Sources card; inline [n] opens the source URL. Lock composer to THINKING while model picker stays hidden.
This commit is contained in:
+10
-16
@@ -36,7 +36,7 @@ describe('ConversationDetailCard citations (#98)', () => {
|
||||
expect(out).toContain('```\n[9]\n```');
|
||||
});
|
||||
|
||||
it('renders Sources list from citations prop', () => {
|
||||
it('does not render Sources list card', () => {
|
||||
renderCard({
|
||||
message: 'Answer with [1]',
|
||||
user_created: false,
|
||||
@@ -49,24 +49,13 @@ describe('ConversationDetailCard citations (#98)', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(screen.getByLabelText('Sources')).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: 'Example Source' });
|
||||
expect(link).toHaveAttribute('href', 'https://example.com/a');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
});
|
||||
|
||||
it('renders nothing for Sources when citations empty', () => {
|
||||
renderCard({
|
||||
message: 'No sources here',
|
||||
user_created: false,
|
||||
citations: [],
|
||||
});
|
||||
expect(screen.queryByLabelText('Sources')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Source 1' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('highlights source when inline citation clicked', async () => {
|
||||
it('opens citation URL in a new tab when inline marker clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null);
|
||||
renderCard({
|
||||
message: 'See [1]',
|
||||
user_created: false,
|
||||
@@ -75,6 +64,11 @@ describe('ConversationDetailCard citations (#98)', () => {
|
||||
],
|
||||
});
|
||||
await user.click(screen.getByRole('button', { name: 'Source 1' }));
|
||||
expect(document.getElementById('citation-source-1')).toBeInTheDocument();
|
||||
expect(openSpy).toHaveBeenCalledWith(
|
||||
'https://example.com',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
openSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Markdown from 'markdown-to-jsx';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
@@ -6,7 +6,6 @@ import { isRagFeatureNotAllowed, parseChatErrorPayload } from '../../utils/chatE
|
||||
import type { ActivityHistoryEntry, Citation } from '../../utils/wsFrames';
|
||||
import type { PromptRating } from '../../utils/promptFeedback';
|
||||
import CustomPreBlock from '../CustomPreBlock/CustomPreBlock';
|
||||
import SourcesList from '../SourcesList/SourcesList';
|
||||
import MessageActions from '../MessageActions/MessageActions';
|
||||
import ActivityIndicator from '../ActivityIndicator/ActivityIndicator';
|
||||
|
||||
@@ -209,8 +208,6 @@ const ConversationDetailCard = ({
|
||||
activityHistory = [],
|
||||
activityInterrupted = false,
|
||||
}: ConversationDetailCardProps): JSX.Element => {
|
||||
const [highlightIndex, setHighlightIndex] = useState<number | null>(null);
|
||||
|
||||
const CitationMark = useCallback(
|
||||
({ indices }: { indices?: string }) => {
|
||||
const list = (indices || '')
|
||||
@@ -229,12 +226,9 @@ const ConversationDetailCard = ({
|
||||
aria-label={`Source ${index}`}
|
||||
title={citation?.title || `Source ${index}`}
|
||||
onClick={() => {
|
||||
setHighlightIndex(index);
|
||||
if (citation?.url) {
|
||||
// Prefer scroll/highlight; URL still available from Sources list
|
||||
window.open(citation.url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
const el = document.getElementById(`citation-source-${index}`);
|
||||
el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
|
||||
}}
|
||||
>
|
||||
[{index}]
|
||||
@@ -346,10 +340,6 @@ const ConversationDetailCard = ({
|
||||
</Markdown>
|
||||
</Bubble>
|
||||
|
||||
{!user_created && citations.length > 0 && (
|
||||
<SourcesList citations={citations} highlightIndex={highlightIndex} />
|
||||
)}
|
||||
|
||||
<MessageActions
|
||||
promptId={promptId}
|
||||
rawMarkdown={rawForCopy}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Citation } from '../../utils/wsFrames';
|
||||
|
||||
const SourcesRoot = styled.aside`
|
||||
margin-top: 0.65rem;
|
||||
max-width: 80%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
|
||||
background: ${({ theme }) =>
|
||||
theme.darkMode ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.03)'};
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 92%;
|
||||
}
|
||||
`;
|
||||
|
||||
const SourcesTitle = styled.h4`
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.75;
|
||||
`;
|
||||
|
||||
const SourceList = styled.ol`
|
||||
margin: 0;
|
||||
padding-left: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
`;
|
||||
|
||||
const SourceItem = styled.li<{ $highlight: boolean }>`
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
border-radius: 0.35rem;
|
||||
padding: 0.15rem 0.25rem;
|
||||
outline: ${({ $highlight, theme }) =>
|
||||
$highlight ? `2px solid ${theme.main}` : 'none'};
|
||||
background: ${({ $highlight, theme }) =>
|
||||
$highlight
|
||||
? theme.darkMode
|
||||
? 'rgba(255,255,255,0.08)'
|
||||
: 'rgba(0,0,0,0.06)'
|
||||
: 'transparent'};
|
||||
transition: background 0.2s ease, outline 0.2s ease;
|
||||
`;
|
||||
|
||||
const SourceLink = styled.a`
|
||||
color: ${({ theme }) => (theme.darkMode ? '#a0c4ff' : theme.main)};
|
||||
text-decoration: underline;
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
const SourceMeta = styled.span`
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.65;
|
||||
margin-top: 0.1rem;
|
||||
`;
|
||||
|
||||
function domainFromUrl(url: string): string {
|
||||
try {
|
||||
return new URL(url).hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
type SourcesListProps = {
|
||||
citations: Citation[];
|
||||
highlightIndex?: number | null;
|
||||
};
|
||||
|
||||
const SourcesList = ({
|
||||
citations,
|
||||
highlightIndex = null,
|
||||
}: SourcesListProps): JSX.Element | null => {
|
||||
const itemRefs = useRef<Record<number, HTMLLIElement | null>>({});
|
||||
const [active, setActive] = useState<number | null>(highlightIndex);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(highlightIndex ?? null);
|
||||
if (highlightIndex == null) return;
|
||||
const el = itemRefs.current[highlightIndex];
|
||||
el?.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
|
||||
}, [highlightIndex]);
|
||||
|
||||
if (!citations.length) return null;
|
||||
|
||||
const sorted = [...citations].sort((a, b) => a.index - b.index);
|
||||
|
||||
return (
|
||||
<SourcesRoot aria-label="Sources">
|
||||
<SourcesTitle>Sources</SourcesTitle>
|
||||
<SourceList>
|
||||
{sorted.map((citation) => {
|
||||
const domain = domainFromUrl(citation.url);
|
||||
return (
|
||||
<SourceItem
|
||||
key={citation.index}
|
||||
id={`citation-source-${citation.index}`}
|
||||
$highlight={active === citation.index}
|
||||
ref={(node) => {
|
||||
itemRefs.current[citation.index] = node;
|
||||
}}
|
||||
>
|
||||
{citation.url ? (
|
||||
<SourceLink
|
||||
href={citation.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{citation.title || `Source ${citation.index}`}
|
||||
</SourceLink>
|
||||
) : (
|
||||
<span>{citation.title || `Source ${citation.index}`}</span>
|
||||
)}
|
||||
{(citation.published_at || domain) && (
|
||||
<SourceMeta>
|
||||
{[citation.published_at, domain].filter(Boolean).join(' · ')}
|
||||
</SourceMeta>
|
||||
)}
|
||||
</SourceItem>
|
||||
);
|
||||
})}
|
||||
</SourceList>
|
||||
</SourcesRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcesList;
|
||||
@@ -318,7 +318,7 @@ const validationSchema = Yup.object().shape({
|
||||
});
|
||||
|
||||
/** Fixed until product re-exposes a model picker (#106). */
|
||||
const DEFAULT_MODEL_NAME = "FAST";
|
||||
const DEFAULT_MODEL_NAME = "THINKING";
|
||||
|
||||
type PromptValues = {
|
||||
prompt: string;
|
||||
|
||||
Reference in New Issue
Block a user