From 3cdbd3fe1cb8c9450bd18e93d8747616c1a21206 Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Tue, 22 Sep 2026 14:42:16 -0400 Subject: [PATCH 01/17] Start screen: intent tabs on the composer, profile and RFP as chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new-conversation screen loses its logo and its title. The fund / need-funding choice is a pair of tabs on the composer's top edge; the open tab merges into the box and the box takes its colour, and the send button goes emerald for a researcher. The researcher's profile and the RFP they are answering ride in the composer's toolbar as chips: a plug until connected, then the connected services' logos ("Scholar, ORCID"); "Apply to RFP" until chosen, then the RFP with an × to remove it. The model and effort controls sit under the box at its right, without icons, in every workspace composer. Around it: the chat pane shows no title while the chat is the main pane (the top strip names it); the Workspace name in the strip is larger and black; the nav item moves under My Funding with a New badge; the detail block strip runs edge to edge with an even inset outline on the open block; the RFP picker keeps one width while loading and is titled "Select RFP to apply to"; the profile modal explains what connecting does and drops its section heading. Co-Authored-By: Claude Fable 5.1 --- app/layouts/Navigation.tsx | 37 ++- components/AIMode/chat/ChatPane.tsx | 70 ++++-- .../AIMode/chat/DocumentChatEmptyState.tsx | 3 - .../document/details/AllDetailsBlock.tsx | 8 +- .../AIMode/document/details/DetailBlock.tsx | 4 +- .../document/details/DetailBlockStrip.tsx | 2 +- components/AIMode/shell/AIModeHeader.tsx | 7 +- components/AIMode/start/IntentCtas.tsx | 161 ------------ components/AIMode/start/IntentTabs.tsx | 64 +++++ components/AIMode/start/IntentToggle.tsx | 42 ---- components/AIMode/start/StartContextChips.tsx | 229 ++++++++++++++++++ components/AIMode/start/StartScreen.tsx | 42 ++-- components/AgentChat/ChatComposer.tsx | 16 +- components/AgentChat/ModelControls.tsx | 11 +- .../modals/SelectFundingOpportunityModal.tsx | 12 +- .../About/ProfileInformationForm/index.tsx | 5 +- components/profile/ProfileEditModal.tsx | 4 + 17 files changed, 432 insertions(+), 285 deletions(-) delete mode 100644 components/AIMode/start/IntentCtas.tsx create mode 100644 components/AIMode/start/IntentTabs.tsx delete mode 100644 components/AIMode/start/IntentToggle.tsx create mode 100644 components/AIMode/start/StartContextChips.tsx diff --git a/app/layouts/Navigation.tsx b/app/layouts/Navigation.tsx index cd898625d..2daf2d36d 100644 --- a/app/layouts/Navigation.tsx +++ b/app/layouts/Navigation.tsx @@ -11,6 +11,7 @@ import { faHouse as faHouseLight } from '@fortawesome/pro-light-svg-icons'; import { BriefcaseBusiness, Sprout, Star } from 'lucide-react'; import { useOptionalAIMode } from '@/components/AIMode/AIModeContext'; import { AI_MODE_NAME } from '@/components/AIMode/copy'; +import { Badge } from '@/components/ui/Badge'; import { isHomeTabPath } from '@/hooks/useFundTabs'; import { useUser } from '@/contexts/UserContext'; import { isHubEditorOrModerator } from '@/utils/permissions'; @@ -36,6 +37,8 @@ interface NavigationItem { isHome?: boolean; /** Toggles the AI Mode overlay in place instead of navigating. */ isAIMode?: boolean; + /** A small pill after the label, e.g. "New" while a feature is fresh. */ + badge?: string; } interface NavigationProps { @@ -84,6 +87,14 @@ export const Navigation: React.FC = ({ requiresAuth: true, description: 'View your funding activity', }, + { + label: AI_MODE_NAME, + href: '#', + isAIMode: true, + requiresAuth: true, + badge: 'New', + description: 'Draft proposals and RFPs with the assistant', + }, { label: 'Peer Review', href: '/peer-review', @@ -102,13 +113,6 @@ export const Navigation: React.FC = ({ isLucideSprout: true, description: 'Learn about the ResearchHub Endowment', }, - { - label: AI_MODE_NAME, - href: '#', - isAIMode: true, - requiresAuth: true, - description: 'Draft proposals and RFPs with the assistant', - }, ]; const getButtonStyles = (isActive: boolean) => { @@ -187,6 +191,17 @@ export const Navigation: React.FC = ({ ? 'hidden' : 'flex w-full min-w-0 items-center tablet:max-sidebar-compact:!hidden'; + const label = ( + + {item.label} + {item.badge && ( + + {item.badge} + + )} + + ); + if (item.isAIMode) { // Same row as the links, but no navigation: the workspace opens in // place on its new-conversation screen and the URL only gains a query @@ -202,9 +217,7 @@ export const Navigation: React.FC = ({
-
- {item.label} -
+
{label}
); } @@ -239,9 +252,7 @@ export const Navigation: React.FC = ({
)}
-
- {item.label} -
+
{label}
); }; diff --git a/components/AIMode/chat/ChatPane.tsx b/components/AIMode/chat/ChatPane.tsx index 0acec2871..772ec08a6 100644 --- a/components/AIMode/chat/ChatPane.tsx +++ b/components/AIMode/chat/ChatPane.tsx @@ -13,8 +13,11 @@ import { conversationTitleFor } from './conversationTitle'; import { Button } from '@/components/ui/Button'; import { ChatTranscriptSkeleton } from '@/components/skeletons/AIModeSkeleton'; import { cn } from '@/utils/styles'; +import { layoutFor } from '../AIModeContext'; import type { AIModeChatState } from '../useAIModeChat'; import { aiModeGreeting, INTENT_COPY } from '../copy'; +import { StartContextChips } from '../start/StartContextChips'; +import { intentBoxClass } from '../start/IntentTabs'; import { StartScreen } from '../start/StartScreen'; import { DocumentChatEmptyState } from './DocumentChatEmptyState'; import { useUser } from '@/contexts/UserContext'; @@ -81,40 +84,71 @@ export function ChatPane({ const { currentTitle, title, loading: titleLoading } = conversationTitleFor(state); const onDocument = state.target.kind === 'document'; + // The new-conversation screen: white, untitled, the composer in the middle. + const onStart = chatId == null && !onDocument; + // With the chat as the main pane the workspace's top strip already names + // it, so the pane's own header carries only the controls. + const chatIsMain = layoutFor(state.target) === 'chat'; + + const modelControls = ( + + ); + const composer = ( void state.send()} + onSend={state.send} onStop={state.stop} busy={composerBusy} canStop={canStop} disabled={composerDisabled} sendDisabled={state.sendBlocked} notice={notice} - className="border-t-0 bg-gray-50" - placeholder={ - chatId == null && !onDocument ? INTENT_COPY[state.intent].placeholder : undefined + className={cn('border-t-0', onStart ? 'bg-white pt-0' : 'bg-gray-50')} + boxClassName={ + onStart + ? cn( + 'rounded-2xl rounded-tl-none px-4 py-3 shadow-sm focus-within:shadow-md', + intentBoxClass(state.intent) + ) + : undefined + } + minRows={onStart ? 3 : 1} + sendClassName={ + onStart && state.intent === 'need_funding' + ? 'bg-emerald-600 text-white hover:bg-emerald-700' + : undefined } + placeholder={onStart ? INTENT_COPY[state.intent].placeholder : undefined} toolbar={ - + // A researcher's context rides with the first message, like attachments. + onStart && state.intent === 'need_funding' ? ( + + ) : undefined } + // The model and effort sit under the box, at its right, out of the message's way. + footer={
{modelControls}
} /> ); return ( -
+
{/* No border or fill: the title and its controls float over the pane. */}
{onOpenConversations && ( @@ -138,6 +172,8 @@ export function ChatPane({ if (next && next !== (currentTitle ?? '')) state.rename(chatId, next); }} /> + ) : chatIsMain ? ( + ) : titleLoading ? (
@@ -169,8 +205,6 @@ export function ChatPane({ greeting={aiModeGreeting(user?.firstName)} intent={state.intent} onIntentChange={state.setIntent} - selectedGrant={state.selectedGrant} - onSelectGrant={state.setSelectedGrant} /> ) : chat.access === 'loading' && chat.chat == null ? ( diff --git a/components/AIMode/chat/DocumentChatEmptyState.tsx b/components/AIMode/chat/DocumentChatEmptyState.tsx index b91f2c091..8d42eb01d 100644 --- a/components/AIMode/chat/DocumentChatEmptyState.tsx +++ b/components/AIMode/chat/DocumentChatEmptyState.tsx @@ -11,9 +11,6 @@ export function DocumentChatEmptyState() {

The assistant can read this document and propose edits you accept or reject.

-

- Send a message and this chat is kept under Conversations. -

); } diff --git a/components/AIMode/document/details/AllDetailsBlock.tsx b/components/AIMode/document/details/AllDetailsBlock.tsx index d6bc021a3..6a0d69615 100644 --- a/components/AIMode/document/details/AllDetailsBlock.tsx +++ b/components/AIMode/document/details/AllDetailsBlock.tsx @@ -34,10 +34,12 @@ export function AllDetailsBlock({ type="button" onClick={onClick} aria-pressed={active} - style={{ width: ALL_DETAILS_BLOCK_WIDTH }} + style={{ minWidth: ALL_DETAILS_BLOCK_WIDTH }} className={cn( - 'flex h-full shrink-0 flex-col justify-center px-2.5 text-left transition-colors', - active ? 'bg-primary-50 ring-[3px] ring-primary-100' : 'bg-gray-50 hover:bg-gray-100' + 'flex h-full flex-1 flex-col justify-center px-2.5 text-left transition-colors', + active + ? 'relative z-10 bg-primary-50 outline outline-2 -outline-offset-2 outline-primary-400' + : 'bg-gray-50 hover:bg-gray-100' )} > diff --git a/components/AIMode/document/details/DetailBlock.tsx b/components/AIMode/document/details/DetailBlock.tsx index 2ccc99414..456a411e2 100644 --- a/components/AIMode/document/details/DetailBlock.tsx +++ b/components/AIMode/document/details/DetailBlock.tsx @@ -67,8 +67,10 @@ export function DetailBlock({ config, noteId, value, open, onOpenChange }: Detai const blockClass = (filled: boolean, open: boolean) => cn( 'flex min-w-0 flex-1 flex-col justify-center border-r h-full bg-white px-2.5 text-left transition-colors', + // The outline sits inside the block, so it is the same on every side + // whatever the neighbours' borders do. open - ? 'border-primary-400 bg-primary-50 ring-[3px] ring-primary-100' + ? 'relative z-10 border-primary-400 bg-primary-50 outline outline-2 -outline-offset-2 outline-primary-400' : filled ? 'border-gray-200 hover:bg-gray-50' : 'border-gray-300 hover:bg-gray-50' diff --git a/components/AIMode/document/details/DetailBlockStrip.tsx b/components/AIMode/document/details/DetailBlockStrip.tsx index 12a51bca4..ec0fcb3fb 100644 --- a/components/AIMode/document/details/DetailBlockStrip.tsx +++ b/components/AIMode/document/details/DetailBlockStrip.tsx @@ -57,7 +57,7 @@ export function DetailBlockStrip({ detailsOpen, onToggleDetails }: DetailBlockSt
{note != null && blocks diff --git a/components/AIMode/shell/AIModeHeader.tsx b/components/AIMode/shell/AIModeHeader.tsx index 8e2954ac1..468cb19eb 100644 --- a/components/AIMode/shell/AIModeHeader.tsx +++ b/components/AIMode/shell/AIModeHeader.tsx @@ -28,8 +28,11 @@ export function AIModeHeader({ title, publishControlsRef, onBack }: AIModeHeader
-