Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions app/my-funding/components/MyFundingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client';

import { useEffect, type ReactNode } from 'react';
import { FundingDirectionIcon } from '@/components/Funding/FundingDirectionIcon';
import type { FundingDirection } from '@/components/Funding/fundingDirection';
import { useRouter, useSearchParams } from 'next/navigation';
import { ArrowDownLeft, ArrowUpRight, type LucideIcon } from 'lucide-react';
import { PageLayout } from '@/app/layouts/PageLayout';
import { FundsGiven } from '@/components/Funding/dashboard/FundsGiven';
import { ModeratorViewAsFunder } from '@/components/Funding/dashboard/ModeratorViewAsFunder';
Expand All @@ -17,12 +18,10 @@
* funds given, into it for funds received. The arrow inherits the tab's own
* color so it picks up the active/inactive treatment for free.
*/
function TabLabel({ icon: DirectionIcon, children }: { icon: LucideIcon; children: ReactNode }) {
function TabLabel({ direction, children }: { direction: FundingDirection; children: ReactNode }) {

Check warning on line 21 in app/my-funding/components/MyFundingPage.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaDIYAYwdIIktb-R7pO0&open=AaDIYAYwdIIktb-R7pO0&pullRequest=1119
return (
<span className="flex items-center gap-2">
<span className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-gray-100">
<DirectionIcon className="h-3.5 w-3.5" />
</span>
<FundingDirectionIcon direction={direction} colored={false} />
{children}
</span>
);
Expand All @@ -31,12 +30,12 @@
const MY_FUNDING_TABS = [
{
id: 'given',
label: <TabLabel icon={ArrowUpRight}>Funds given</TabLabel>,
label: <TabLabel direction="giving">Funds given</TabLabel>,
href: '/my-funding?tab=given',
},
{
id: 'received',
label: <TabLabel icon={ArrowDownLeft}>Funds received</TabLabel>,
label: <TabLabel direction="receiving">Funds received</TabLabel>,
href: '/my-funding?tab=received',
// Money coming in reads green, matching the emerald treatment the Fund
// tabs already use for the inbound side.
Expand Down
12 changes: 0 additions & 12 deletions components/AIMode/AIModeOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { ConversationList } from './ConversationList';
import { DocumentCard } from './DocumentCard';
import { DocumentPane, type DocumentPaneView } from './DocumentPane';
import { useAIModeChat } from './useAIModeChat';
import type { PublishingDefaultArticleType } from '@/contexts/PublishingHostContext';
import { useAIModeDocument } from './useAIModeDocument';
import { AI_MODE_NAME } from './copy';

Expand Down Expand Up @@ -119,15 +118,6 @@ export function AIModeOverlay() {
setDocumentView('document');
}, [noteId]);

// What the conversation set out to write, from its opening message, so the
// details form preselects the matching work type for a note that has none.
const defaultArticleType = useMemo<PublishingDefaultArticleType | null>(() => {
const opening = state.chat.chat?.messages.find((message) => message.role === 'user')?.content;
if (!opening) return null;
if (/request for proposals|\bRFP\b/i.test(opening)) return 'grant';
if (/proposal/i.test(opening)) return 'preregistration';
return null;
}, [state.chat.chat?.messages]);
const openDocument = useCallback(() => setDocumentOpen(true), []);
const closeDocument = useCallback(() => setDocumentOpen(false), []);
const showDocument = noteId != null && documentOpen;
Expand Down Expand Up @@ -330,7 +320,6 @@ export function AIModeOverlay() {
chat={state.chat.chat}
view={documentView}
onViewChange={setDocumentView}
defaultArticleType={defaultArticleType}
publishControlsSlot={publishControlsSlot}
/>
</aside>
Expand Down Expand Up @@ -362,7 +351,6 @@ export function AIModeOverlay() {
chat={state.chat.chat}
view={documentView}
onViewChange={setDocumentView}
defaultArticleType={defaultArticleType}
presentation="drawer"
readOnly
className="-mx-4 -mt-2"
Expand Down
77 changes: 9 additions & 68 deletions components/AIMode/ChatPane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { Menu } from 'lucide-react';
import { ChatComposer } from '@/components/AgentChat/ChatComposer';
import { ChatTranscript } from '@/components/AgentChat/ChatTranscript';
Expand All @@ -11,11 +11,10 @@ import { ConversationMenu } from './ConversationMenu';
import { ConversationTitleField } from './ConversationTitleField';
import { Button } from '@/components/ui/Button';
import { ChatTranscriptSkeleton } from '@/components/skeletons/AIModeSkeleton';
import { Logo } from '@/components/ui/Logo';
import { cn } from '@/utils/styles';
import type { AIModeChatState } from './useAIModeChat';
import { aiModeGreeting, AI_MODE_STARTER_PROMPTS } from './copy';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { aiModeGreeting, INTENT_COPY } from './copy';
import { StartScreen } from './start/StartScreen';
import { useUser } from '@/contexts/UserContext';

interface ChatPaneProps {
Expand Down Expand Up @@ -69,16 +68,7 @@ export function ChatPane({
setRenaming(false);
}, [chatId]);

// A starter card is a complete first message: send it and start the
// conversation rather than leaving it in the box to be sent by hand.
const { user } = useUser();
const startFromCard = useCallback(
(message: string) => {
state.clearNotice();
void state.send(message);
},
[state]
);

const listBlocked = list.access === 'hidden';
const chatUnavailable =
Expand Down Expand Up @@ -110,7 +100,7 @@ export function ChatPane({
sendDisabled={state.sendBlocked}
notice={notice}
className="border-t-0 bg-gray-50"
placeholder="Describe what you want to work on…"
placeholder={chatId == null ? INTENT_COPY[state.intent].placeholder : undefined}
toolbar={
<ModelControls
models={modelSelection.models}
Expand Down Expand Up @@ -175,11 +165,13 @@ export function ChatPane({
{listBlocked ? (
<AccessBlocked detail={list.accessDetail} />
) : chatId == null ? (
<EmptyState
<StartScreen
composer={composer}
greeting={aiModeGreeting(user?.firstName)}
onSelectStarter={startFromCard}
disabled={composerBusy}
intent={state.intent}
onIntentChange={state.setIntent}
selectedGrant={state.selectedGrant}
onSelectGrant={state.setSelectedGrant}
/>
) : chat.access === 'loading' && chat.chat == null ? (
<ChatTranscriptSkeleton />
Expand Down Expand Up @@ -249,54 +241,3 @@ function AccessBlocked({ detail }: { readonly detail: string | null }) {
</div>
);
}

function EmptyState({
composer,
greeting,
onSelectStarter,
disabled,
}: {
readonly composer: ReactNode;
readonly greeting: string;
readonly onSelectStarter: (message: string) => void;
readonly disabled: boolean;
}) {
return (
<div className="flex min-h-[60vh] flex-col justify-center gap-6">
<div className="flex flex-col items-center gap-4 text-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary-50">
<Logo size={34} noText />
</div>
<h2 className="font-serif text-3xl tracking-tight text-gray-900">{greeting}</h2>
</div>

<div className="-mx-3">{composer}</div>

{/* Picking a card starts the conversation with its message. */}
<div className="grid grid-cols-1 gap-3 tablet:!grid-cols-3">
{AI_MODE_STARTER_PROMPTS.map((prompt) => (
<button
key={prompt.id}
type="button"
onClick={() => onSelectStarter(prompt.message)}
disabled={disabled}
className={cn(
'group flex h-full min-h-[120px] flex-col items-center justify-center gap-2.5 rounded-xl border border-gray-200 bg-white px-3 py-4 text-center transition-colors',
'hover:border-primary-200 hover:bg-primary-50',
'focus:outline-none focus-visible:border-primary-400 focus-visible:ring-2 focus-visible:ring-primary-500',
'disabled:cursor-not-allowed disabled:opacity-60'
)}
>
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-gray-100 transition-colors group-hover:bg-white">
<FontAwesomeIcon icon={prompt.icon} className="h-[18px] w-[18px] text-gray-700" />
</span>
<span className="text-sm font-semibold tracking-[0.01em] text-gray-900">
{prompt.title}
</span>
<span className="text-xs text-gray-600">{prompt.description}</span>
</button>
))}
</div>
</div>
);
}
12 changes: 2 additions & 10 deletions components/AIMode/DocumentPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ import {
usePublishingCompletion,
} from '@/components/Notebook/PublishingForm';
import { ButtonGroup } from '@/components/ui/ButtonGroup';
import {
PublishingHostProvider,
type PublishingDefaultArticleType,
type PublishingHost,
} from '@/contexts/PublishingHostContext';
import { PublishingHostProvider, type PublishingHost } from '@/contexts/PublishingHostContext';
import { useNoteDetailsSaver } from '@/hooks/useNoteDetailsSaver';
import { NoteReviewControls } from '@/components/Notebook/NoteReview/NoteReviewControls';
import { noteDiffPersistableDoc } from '@/components/Notebook/NoteReview/noteDiffOverlay';
Expand Down Expand Up @@ -48,8 +44,6 @@ interface DocumentPaneProps {
readonly chat: AgentChat | null;
readonly view: DocumentPaneView;
readonly onViewChange: (view: DocumentPaneView) => void;
/** Work type to preselect in the details form for a note without one. */
readonly defaultArticleType?: PublishingDefaultArticleType | null;
/**
* A column beside the chat, or the drawer below the tablet breakpoint. The
* drawer trades the block strip for a Document | Details switch and keeps
Expand Down Expand Up @@ -80,7 +74,6 @@ export function DocumentPane({
chat,
view,
onViewChange,
defaultArticleType = null,
presentation = 'pane',
publishControlsSlot = null,
readOnly = false,
Expand Down Expand Up @@ -143,9 +136,8 @@ export function DocumentPane({
isLoading: loading,
saveDetailsSoon,
saveDetailsNow,
defaultArticleType,
}),
[content, editor, loading, saveDetailsSoon, saveDetailsNow, defaultArticleType]
[content, editor, loading, saveDetailsSoon, saveDetailsNow]
);

const persistEditorState = useCallback(async () => {
Expand Down
59 changes: 11 additions & 48 deletions components/AIMode/copy.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,23 @@
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import {
faBullhorn,
faFileSignature,
faMagnifyingGlassDollar,
} from '@fortawesome/pro-light-svg-icons';
import type { FundingIntent } from '@/components/Funding/fundingDirection';

/**
* User-facing copy for AI Mode, in one place so the product name and the
* empty-state wording can change without touching components.
* new-conversation wording can change without touching components.
*/
export const AI_MODE_NAME = 'ResearchHub AI';

/** Greeting on the new-conversation screen; the name is filled in at render. */
export const aiModeGreeting = (firstName: string | null | undefined): string =>
firstName?.trim() ? `Welcome, ${firstName.trim()}` : 'Welcome';

export interface StarterPrompt {
readonly id: string;
readonly title: string;
readonly description: string;
/** Same icon family as the sidebar's Publish menu. */
readonly icon: IconDefinition;
/** Sent as the conversation's first message when the card is picked. */
readonly message: string;
}

/**
* Starter cards for the new-conversation screen. Picking one starts the
* conversation with its message; the backend does not supply suggestions.
* Titles and subtext mirror the sidebar's Publish menu.
*/
export const AI_MODE_STARTER_PROMPTS: readonly StarterPrompt[] = [
{
id: 'draft-rfp',
title: 'Draft a Request for Proposal',
description: 'Fund specific research you care about',
icon: faBullhorn,
message:
'Help me draft a request for proposals. Ask me for anything you still need to know about ' +
'the work I want to fund, then create a note and write the RFP into it.',
},
{
id: 'draft-proposal',
title: 'Draft a Research Proposal',
description: 'Raise money for your research',
icon: faFileSignature,
message:
'Help me draft a research proposal. Ask me for anything you still need to know about the ' +
'work, then create a note and write the proposal into it, starting with three hypotheses.',
/** What the intent toggle calls each side, and what the composer asks for. */
export const INTENT_COPY: Record<FundingIntent, { label: string; placeholder: string }> = {
fund: {
label: 'I want to fund',
placeholder: 'Describe the research you want to fund…',
},
{
id: 'funding',
title: 'Find me funding',
description: 'Open RFPs that fit your work',
icon: faMagnifyingGlassDollar,
message:
'Find open RFPs I could apply to based on my expertise, and tell me why each one is a match.',
need_funding: {
label: 'I need funding',
placeholder: 'Describe the research you need funding for…',
},
];
};
Loading
Loading