Skip to content
Merged

Ai mode #1088

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
43c034a
Move notebook AgentChat components to components/AgentChat
yattias Sep 6, 2026
cd148cc
Introduce ChatTransport so chat hooks serve two backend surfaces
yattias Sep 6, 2026
2f5205e
Add the AI Mode overlay shell, driven by URL state
yattias Sep 6, 2026
e13899e
AI Mode: conversation list and chat pane
yattias Sep 6, 2026
bd0af49
AI Mode: document pane with live drafting
yattias Sep 6, 2026
e7b7ae1
AI Mode: idle empty-document copy, mount the mobile document drawer l…
yattias Sep 6, 2026
5d1e200
AI Mode: starter prompts for drafting an RFP or a proposal
yattias Sep 6, 2026
eb3e1a0
Extract the assistant-version review into useNoteAgentReview
yattias Sep 6, 2026
bc07e15
Editor: optional autofocus, no window.editor global, text menu respec…
yattias Sep 6, 2026
0f39b44
AI Mode: editable document with the notebook's in-note review
yattias Sep 6, 2026
45a6206
AI Mode: document card in the transcript; no auto-open on mobile
yattias Sep 7, 2026
c161a37
SwipeableDrawer: zIndex prop; AI Mode drawers stack above the overlay
yattias Sep 7, 2026
75afeb5
AI Mode: lock the editor mid-turn without recreating it; settle empty…
yattias Sep 7, 2026
aa5fc18
AI Mode: count sections from the review's accept projection
yattias Sep 7, 2026
d2fb962
AI Mode: make the overlay a real modal
yattias Sep 7, 2026
31ad2e7
AI Mode: don't autosave an empty note on mount or on unlock
yattias Sep 7, 2026
6018f2e
AI Mode: drive URL state with history.replaceState
yattias Sep 7, 2026
159cf95
AI Mode: wider document pane, leaner conversation rows, tolerate para…
yattias Sep 7, 2026
99ebfae
AI Mode: claude.ai-style panes with draggable widths, gutter-free doc…
yattias Sep 7, 2026
ae43e8c
AI Mode: jump to latest, conversation rename/delete menu, sidebar and…
yattias Sep 7, 2026
5ac301b
AI Mode: optimistic send and rename
yattias Sep 7, 2026
5aeb784
AI Mode: skeletons, empty-save guards, bare document pane, header polish
yattias Sep 8, 2026
2864d4a
Host the publishing form in AI Mode behind a PublishingHost seam
yattias Sep 8, 2026
a2cfccb
AI Mode: ResearchHub AI name, greeting home screen with starter cards…
yattias Sep 8, 2026
47ecc9c
AI Mode: offer to delete a conversation's document along with it
yattias Sep 9, 2026
90a0752
Agent chat: larger prose, typed-out answers, and notebook-free naming
yattias Sep 10, 2026
d63a0e4
Drop unused reveal helpers left from the reverted starter screen
yattias Sep 10, 2026
d0af392
Merge remote-tracking branch 'origin/main' into ai-mode
yattias Sep 10, 2026
d851392
Put the composer's model controls on the shared menu and popover
yattias Sep 10, 2026
e5c94e7
Share the menu trigger, sort menu, choice pills, and field label
yattias Sep 10, 2026
6d698c7
Remove the search results page and everything that only served it
yattias Sep 10, 2026
946f466
Gate the assistant to moderators and hub editors; quiet the locked ef…
yattias Sep 10, 2026
def9836
Fix publishing bug in ai chat
yattias Sep 10, 2026
946331d
Fixing smoke tests
yattias Sep 11, 2026
30bd207
updating ai icon
yattias Sep 11, 2026
3ccdf22
Merge main into ai-mode
yattias Sep 11, 2026
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
4 changes: 2 additions & 2 deletions app/journal/JournalPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { FeedContent } from '@/components/Feed/FeedContent';
import { FeedSortDropdown } from '@/components/Feed/FeedSortDropdown';
import { SortMenu } from '@/components/ui/SortMenu';
import { useFeed } from '@/hooks/useFeed';
import { JournalV2FeedEntryItem } from '@/components/Journal/JournalV2FeedEntryItem';

Expand Down Expand Up @@ -56,7 +56,7 @@ export function JournalPageContent() {
loadMore={loadMore}
filters={
<div className="mb-2 mt-2 flex items-center justify-end sm:mt-4">
<FeedSortDropdown
<SortMenu
options={JOURNAL_SORT_OPTIONS}
value={journalSort}
onChange={(sort) => changeJournalSort(getJournalSort(sort))}
Expand Down
36 changes: 31 additions & 5 deletions app/layouts/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
faBars,
} from '@fortawesome/pro-light-svg-icons';
import { faXTwitter, faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { Sprout, Star } from 'lucide-react';
import { Sparkles, Sprout, Star } from 'lucide-react';
import { ChangelogLink } from '@/components/changelog/ChangelogLink';
import { FundingPowerBar } from '@/components/Funding/FundingPowerBar';
import { Icon } from '@/components/ui/icons';
Expand All @@ -24,6 +24,9 @@
import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useScrollContainer } from '@/contexts/ScrollContainerContext';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { isHubEditorOrModerator } from '@/utils/permissions';

interface NavItem {
label: string;
Expand All @@ -32,6 +35,8 @@
isMore?: boolean;
requiresAuth?: boolean;
isHome?: boolean;
/** Toggles the AI Mode overlay in place instead of navigating. */
isAIMode?: boolean;
}

// Additional navigation items not in the bottom bar
Expand Down Expand Up @@ -74,11 +79,17 @@
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { showUSD, toggleCurrency } = useCurrencyPreference();
const scrollContainerRef = useScrollContainer();
const { user } = useUser();
const aiMode = useOptionalAIMode();

// Moderators and hub editors, the only users the assistant admits, get it
// in the bar where Peer Review sits for everyone else.
const mainNavItems: NavItem[] = [
{ label: 'Home', href: '/', iconKey: 'home', isHome: true },
{ label: 'My Funding', href: '/my-funding', iconKey: 'fund', requiresAuth: true },
{ label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
isHubEditorOrModerator(user)
? { label: 'Assistant', iconKey: 'assistant', isAIMode: true }
: { label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
{ label: 'Wallet', href: '/researchcoin', iconKey: 'wallet' },
{ label: 'More', isMore: true, iconKey: 'more' },
];
Expand Down Expand Up @@ -109,6 +120,10 @@
setIsMoreOpen(true);
return;
}
if (item.isAIMode) {
aiMode?.toggle();
return;
}

if (item.requiresAuth) {
executeAuthenticatedAction(() => router.push(item.href!));
Expand Down Expand Up @@ -139,6 +154,15 @@
color={iconColor}
/>
);
case 'assistant':
return (
<Sparkles
size={iconSize}
color={iconColor}
strokeWidth={isActive ? 2.25 : 2}
fill={isActive ? iconColor : 'none'}
/>
);
case 'peer-review':
return (
<Star
Expand Down Expand Up @@ -234,9 +258,11 @@
{mainNavItems.map((item) => {
const isActive = item.isMore
? isMoreActive || isMoreOpen
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;
: item.isAIMode
? Boolean(aiMode?.isOpen)
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;

Check warning on line 265 in app/layouts/MobileBottomNav.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaCNM2wQVBkTChWOoyvP&open=AaCNM2wQVBkTChWOoyvP&pullRequest=1088

return (
<button
Expand Down
62 changes: 53 additions & 9 deletions app/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import { IconName } from '@/components/ui/icons/Icon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHouse as faHouseSolid } from '@fortawesome/pro-solid-svg-icons';
import { faHouse as faHouseLight } from '@fortawesome/pro-light-svg-icons';
import { Sprout, Star } from 'lucide-react';
import { Sparkles, Sprout, Star } from 'lucide-react';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { AI_MODE_NAME } from '@/components/AIMode/copy';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { isHubEditorOrModerator } from '@/utils/permissions';
import { cn } from '@/utils/styles';

interface NavIcon {
Expand All @@ -30,6 +34,8 @@ interface NavigationItem {
isLucideStar?: boolean;
isLucideSprout?: boolean;
isHome?: boolean;
/** Toggles the AI Mode overlay in place instead of navigating. */
isAIMode?: boolean;
}

interface NavigationProps {
Expand Down Expand Up @@ -62,6 +68,10 @@ export const Navigation: React.FC<NavigationProps> = ({
onUnimplementedFeature,
forceMinimize = false,
}) => {
const { user } = useUser();
// The assistant is gated server-side to moderators and hub editors; nobody
// else gets a door to a room they cannot enter.
const canUseAssistant = isHubEditorOrModerator(user);
const navigationItems: NavigationItem[] = [
{
label: 'Home',
Expand Down Expand Up @@ -103,11 +113,16 @@ export const Navigation: React.FC<NavigationProps> = ({
isLucideSprout: true,
description: 'Learn about the ResearchHub Endowment',
},
{
label: AI_MODE_NAME,
href: '#',
isAIMode: true,
requiresAuth: true,
description: 'Chat with the research assistant',
},
];

const getButtonStyles = (path: string, isHome?: boolean) => {
const isActive = isPathActive(path, isHome);

const getButtonStyles = (isActive: boolean) => {
return cn(
'flex w-full items-center rounded-lg px-3 py-2.5 text-[15px] font-medium transition-colors',
forceMinimize
Expand Down Expand Up @@ -149,8 +164,9 @@ export const Navigation: React.FC<NavigationProps> = ({
}> = ({ item, onUnimplementedFeature }) => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const router = useRouter();
const buttonStyles = getButtonStyles(item.href, item.isHome);
const isActive = isPathActive(item.href, item.isHome);
const aiMode = useOptionalAIMode();
const isActive = item.isAIMode ? Boolean(aiMode?.isOpen) : isPathActive(item.href, item.isHome);
const buttonStyles = getButtonStyles(isActive);

const iconColor = isActive ? '#3971ff' : '#404040';

Expand Down Expand Up @@ -186,6 +202,32 @@ export const Navigation: React.FC<NavigationProps> = ({
? 'hidden'
: 'flex w-full min-w-0 items-center tablet:max-sidebar-compact:!hidden';

if (item.isAIMode) {
// Same row as the links, but it is a toggle: the overlay opens in place
// and the URL only gains a query param.
return (
<button
type="button"
onClick={() => executeAuthenticatedAction(() => aiMode?.toggle())}
className={buttonStyles}
aria-pressed={isActive}
title={item.description}
>
<div className={iconContainerClass}>
<Sparkles
size={22}
color={iconColor}
strokeWidth={2}
fill={isActive ? iconColor : 'none'}
/>
</div>
<div className={textContainerClass}>
<span className="inline-flex min-w-0 items-center gap-2 truncate">{item.label}</span>
</div>
</button>
);
}

return (
<Link
href={item.href}
Expand Down Expand Up @@ -232,9 +274,11 @@ export const Navigation: React.FC<NavigationProps> = ({
)}
>
<div className="space-y-2">
{navigationItems.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
{navigationItems
.filter((item) => !item.isAIMode || canUseAssistant)
.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
</div>
</nav>
);
Expand Down
30 changes: 3 additions & 27 deletions app/layouts/topbar/TopBarSearchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { Suspense, useLayoutEffect, useState } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useLayoutEffect, useState } from 'react';
import { Search as SearchIcon } from 'lucide-react';

function getSearchShortcutLabel(): string {
Expand All @@ -22,12 +21,8 @@
onClick: () => void;
}

function TopBarSearchButtonInner({
onClick,
currentSearchQuery,
}: TopBarSearchButtonProps & { currentSearchQuery: string | null }) {
export function TopBarSearchButton({ onClick }: TopBarSearchButtonProps) {

Check warning on line 24 in app/layouts/topbar/TopBarSearchButton.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=AaCNM2u6VBkTChWOoyvO&open=AaCNM2u6VBkTChWOoyvO&pullRequest=1088
const shortcutLabel = useSearchShortcutLabel();
const displayText = currentSearchQuery || 'Search';

return (
<div className="relative">
Expand All @@ -36,12 +31,7 @@
className="flex items-center w-full md:!w-60 max-w-md mx-auto h-9 px-4 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-full transition-colors text-left group"
>
<SearchIcon className="h-4 w-4 text-gray-600 mr-3 flex-shrink-0" />
<span
className={`text-sm flex-1 truncate ${currentSearchQuery ? 'text-gray-900' : 'text-gray-500'}`}
>
<span className="tablet:!hidden">{displayText}</span>
<span className="hidden tablet:!inline">{displayText}</span>
</span>
<span className="text-sm flex-1 truncate text-gray-500">Search</span>
{shortcutLabel && (
<div className="hidden md:!flex items-center space-x-1 ml-2 flex-shrink-0">
<span className="text-[12px] -mr-1 text-gray-600 bg-gray-200 px-2 py-0.5 rounded-full font-medium">
Expand All @@ -53,17 +43,3 @@
</div>
);
}

function TopBarSearchButtonWithQuery(props: TopBarSearchButtonProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const currentSearchQuery = pathname === '/search' ? searchParams.get('q') : null;

return <TopBarSearchButtonInner {...props} currentSearchQuery={currentSearchQuery} />;
}

export const TopBarSearchButton = (props: TopBarSearchButtonProps) => (
<Suspense fallback={<TopBarSearchButtonInner {...props} currentSearchQuery={null} />}>
<TopBarSearchButtonWithQuery {...props} />
</Suspense>
);
8 changes: 0 additions & 8 deletions app/layouts/topbar/pageRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
faBookmark as faBookmarkLight,
faCommentsQuestion,
faGrid3 as faGrid3Light,
faMagnifyingGlass,
} from '@fortawesome/pro-light-svg-icons';
import { ChartNoAxesColumnIncreasing, Shield, Hash, Users, Activity, Settings } from 'lucide-react';
import Image from 'next/image';
Expand Down Expand Up @@ -62,13 +61,6 @@ const ROUTE_RULES: RouteRule[] = [
icon: <Icon name="fund" size={24} className="text-gray-900" />,
}),
},
{
match: (p) => p === '/search',
getInfo: () => ({
title: 'Search',
icon: <FontAwesomeIcon icon={faMagnifyingGlass} fontSize={24} color="#000" />,
}),
},
{
match: (p) => p === '/notifications',
getInfo: () => ({
Expand Down
4 changes: 2 additions & 2 deletions app/peer-review/ReviewsPageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { BountyFeedItem } from '@/components/Bounty/BountyFeedItem';
import { FeedContent } from '@/components/Feed/FeedContent';
import { FeedSortDropdown } from '@/components/Feed/FeedSortDropdown';
import { SortMenu } from '@/components/ui/SortMenu';
import { useBounties } from '@/hooks/useBounties';

const SORT_OPTIONS = [
Expand Down Expand Up @@ -34,7 +34,7 @@ export function ReviewsPageContent() {
ordering={sort}
filters={
<div className="mb-2 mt-2 flex items-center justify-end sm:mt-4">
<FeedSortDropdown options={SORT_OPTIONS} value={sort} onChange={handleSortChange} />
<SortMenu options={SORT_OPTIONS} value={sort} onChange={handleSortChange} />
</div>
}
skeletonVariant="proposalWork"
Expand Down
Loading
Loading