From 1da4ba9a4e8add9a313cad9de7414b8cb3c70c4d Mon Sep 17 00:00:00 2001 From: Ryan Westfall Date: Tue, 4 Aug 2026 12:22:05 -0500 Subject: [PATCH] Default chat to THINKING; open citations in new tab (#106, #108). Drop Sources card; inline [n] opens the source URL. Lock composer to THINKING while model picker stays hidden. --- .../ConversationDetailCard.citations.test.tsx | 26 ++-- .../ConversationDetailCard.tsx | 14 +- .../components/SourcesList/SourcesList.tsx | 138 ------------------ .../pages/AsyncDashboard2/AsyncDashboard2.tsx | 2 +- 4 files changed, 13 insertions(+), 167 deletions(-) delete mode 100644 llm-fe/src/llm-fe/components/SourcesList/SourcesList.tsx diff --git a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.citations.test.tsx b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.citations.test.tsx index ac020e3..d75a836 100644 --- a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.citations.test.tsx +++ b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.citations.test.tsx @@ -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(); }); }); diff --git a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx index c744554..b9208bf 100644 --- a/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx +++ b/llm-fe/src/llm-fe/components/ConversationDetailCard/ConversationDetailCard.tsx @@ -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(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 = ({ - {!user_created && citations.length > 0 && ( - - )} - 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>({}); - const [active, setActive] = useState(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 ( - - Sources - - {sorted.map((citation) => { - const domain = domainFromUrl(citation.url); - return ( - { - itemRefs.current[citation.index] = node; - }} - > - {citation.url ? ( - - {citation.title || `Source ${citation.index}`} - - ) : ( - {citation.title || `Source ${citation.index}`} - )} - {(citation.published_at || domain) && ( - - {[citation.published_at, domain].filter(Boolean).join(' ยท ')} - - )} - - ); - })} - - - ); -}; - -export default SourcesList; diff --git a/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx b/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx index e5bd2cd..5dfdbf8 100644 --- a/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx +++ b/llm-fe/src/llm-fe/pages/AsyncDashboard2/AsyncDashboard2.tsx @@ -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;