mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
chore: more wip
This commit is contained in:
@@ -175,6 +175,8 @@ While server components and actions are cool and cutting-edge, they introduce a
|
||||
|
||||
- Prefer [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) for server state, colocated near consumers (see [state colocation](https://kentcdodds.com/blog/state-colocation-will-make-your-react-app-faster))
|
||||
- Co-locate UI state inside components/hooks; keep global state minimal
|
||||
- Avoid `useMemo` and `useCallback` unless you have a measured performance issue
|
||||
- Do not abuse `useEffect`; prefer state colocation and derive values directly when possible
|
||||
|
||||
### Styling and components
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/services/feature-flags/use-get-flag";
|
||||
import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useCopilotChatPage() {
|
||||
const router = useRouter();
|
||||
@@ -24,6 +24,22 @@ export function useCopilotChatPage() {
|
||||
|
||||
const sessionId = searchParams.get("sessionId");
|
||||
const prompt = searchParams.get("prompt");
|
||||
const [storedPrompt, setStoredPrompt] = useState<string | null>(null);
|
||||
|
||||
useEffect(
|
||||
function loadStoredPrompt() {
|
||||
if (prompt) return;
|
||||
try {
|
||||
const storedValue = sessionStorage.getItem("copilot_initial_prompt");
|
||||
if (!storedValue) return;
|
||||
sessionStorage.removeItem("copilot_initial_prompt");
|
||||
setStoredPrompt(storedValue);
|
||||
} catch {
|
||||
// Ignore storage errors (private mode, etc.)
|
||||
}
|
||||
},
|
||||
[prompt],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function guardAccess() {
|
||||
@@ -39,6 +55,6 @@ export function useCopilotChatPage() {
|
||||
isFlagReady,
|
||||
isChatEnabled,
|
||||
sessionId,
|
||||
prompt,
|
||||
prompt: prompt ?? storedPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export function CopilotShell({ children }: Props) {
|
||||
isDrawerOpen,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
hasActiveSession,
|
||||
sessions,
|
||||
currentSessionId,
|
||||
handleSelectSession,
|
||||
@@ -38,7 +39,7 @@ export function CopilotShell({ children }: Props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex overflow-hidden bg-zinc-50"
|
||||
className="flex overflow-hidden bg-[#EFEFF0]"
|
||||
style={{ height: `calc(100vh - ${NAVBAR_HEIGHT_PX}px)` }}
|
||||
>
|
||||
{!isMobile && (
|
||||
@@ -51,10 +52,11 @@ export function CopilotShell({ children }: Props) {
|
||||
onSelectSession={handleSelectSession}
|
||||
onFetchNextPage={fetchNextPage}
|
||||
onNewChat={handleNewChat}
|
||||
hasActiveSession={Boolean(hasActiveSession)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
{isMobile && <MobileHeader onOpenDrawer={handleOpenDrawer} />}
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{isReadyToShowContent ? children : <LoadingState />}
|
||||
@@ -74,6 +76,7 @@ export function CopilotShell({ children }: Props) {
|
||||
onNewChat={handleNewChat}
|
||||
onClose={handleCloseDrawer}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
hasActiveSession={Boolean(hasActiveSession)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
onFetchNextPage: () => void;
|
||||
onNewChat: () => void;
|
||||
hasActiveSession: boolean;
|
||||
}
|
||||
|
||||
export function DesktopSidebar({
|
||||
@@ -26,9 +27,10 @@ export function DesktopSidebar({
|
||||
onSelectSession,
|
||||
onFetchNextPage,
|
||||
onNewChat,
|
||||
hasActiveSession,
|
||||
}: Props) {
|
||||
return (
|
||||
<aside className="flex h-full w-80 flex-col border-r border-zinc-100 bg-white">
|
||||
<aside className="flex h-full w-80 flex-col border-r border-zinc-100 bg-zinc-50">
|
||||
<div className="shrink-0 px-6 py-4">
|
||||
<Text variant="h3" size="body-medium">
|
||||
Your chats
|
||||
@@ -50,17 +52,19 @@ export function DesktopSidebar({
|
||||
onFetchNextPage={onFetchNextPage}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 bg-white p-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onNewChat}
|
||||
className="w-full"
|
||||
leftIcon={<Plus width="1rem" height="1rem" />}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
{hasActiveSession && (
|
||||
<div className="shrink-0 bg-zinc-50 p-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onNewChat}
|
||||
className="w-full"
|
||||
leftIcon={<Plus width="1rem" height="1rem" />}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sess
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { scrollbarStyles } from "@/components/styles/scrollbars";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Plus, X } from "@phosphor-icons/react";
|
||||
import { PlusIcon, X } from "@phosphor-icons/react";
|
||||
import { Drawer } from "vaul";
|
||||
import { SessionsList } from "../SessionsList/SessionsList";
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Props {
|
||||
onNewChat: () => void;
|
||||
onClose: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
hasActiveSession: boolean;
|
||||
}
|
||||
|
||||
export function MobileDrawer({
|
||||
@@ -32,12 +33,13 @@ export function MobileDrawer({
|
||||
onNewChat,
|
||||
onClose,
|
||||
onOpenChange,
|
||||
hasActiveSession,
|
||||
}: Props) {
|
||||
return (
|
||||
<Drawer.Root open={isOpen} onOpenChange={onOpenChange} direction="left">
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay className="fixed inset-0 z-[60] bg-black/10 backdrop-blur-sm" />
|
||||
<Drawer.Content className="fixed left-0 top-0 z-[70] flex h-full w-80 flex-col border-r border-zinc-200 bg-white">
|
||||
<Drawer.Content className="fixed left-0 top-0 z-[70] flex h-full w-80 flex-col border-r border-zinc-200 bg-zinc-50">
|
||||
<div className="shrink-0 border-b border-zinc-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Drawer.Title className="text-lg font-semibold text-zinc-800">
|
||||
@@ -69,17 +71,19 @@ export function MobileDrawer({
|
||||
onFetchNextPage={onFetchNextPage}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 bg-white p-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onNewChat}
|
||||
className="w-full"
|
||||
leftIcon={<Plus width="1rem" height="1rem" />}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
{hasActiveSession && (
|
||||
<div className="shrink-0 bg-white p-3 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={onNewChat}
|
||||
className="w-full"
|
||||
leftIcon={<PlusIcon width="1rem" height="1rem" />}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { List } from "@phosphor-icons/react";
|
||||
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
||||
import { ListIcon } from "@phosphor-icons/react";
|
||||
|
||||
interface Props {
|
||||
onOpenDrawer: () => void;
|
||||
@@ -7,15 +8,15 @@ interface Props {
|
||||
|
||||
export function MobileHeader({ onOpenDrawer }: Props) {
|
||||
return (
|
||||
<header className="flex items-center justify-between px-4 py-3">
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Open sessions"
|
||||
onClick={onOpenDrawer}
|
||||
>
|
||||
<List width="1.25rem" height="1.25rem" />
|
||||
</Button>
|
||||
</header>
|
||||
<Button
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Open sessions"
|
||||
onClick={onOpenDrawer}
|
||||
className="fixed z-50 bg-white shadow-md"
|
||||
style={{ left: "1rem", top: `${NAVBAR_HEIGHT_PX + 20}px` }}
|
||||
>
|
||||
<ListIcon width="1.25rem" height="1.25rem" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { Key, storage } from "@/services/storage/local-storage";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer";
|
||||
import { useSessionsPagination } from "./components/SessionsList/useSessionsPagination";
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ export function useCopilotShell() {
|
||||
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
|
||||
|
||||
const isOnHomepage = pathname === "/copilot";
|
||||
const paramSessionId = searchParams.get("sessionId");
|
||||
|
||||
const {
|
||||
isDrawerOpen,
|
||||
@@ -38,7 +39,7 @@ export function useCopilotShell() {
|
||||
handleDrawerOpenChange,
|
||||
} = useMobileDrawer();
|
||||
|
||||
const paginationEnabled = !isMobile || isDrawerOpen;
|
||||
const paginationEnabled = !isMobile || isDrawerOpen || !!paramSessionId;
|
||||
|
||||
const {
|
||||
sessions: accumulatedSessions,
|
||||
@@ -54,54 +55,56 @@ export function useCopilotShell() {
|
||||
});
|
||||
|
||||
const storedSessionId = storage.get(Key.CHAT_SESSION_ID) ?? null;
|
||||
const currentSessionId = useMemo(
|
||||
() => getCurrentSessionId(searchParams, storedSessionId),
|
||||
[searchParams, storedSessionId],
|
||||
);
|
||||
const currentSessionId = getCurrentSessionId(searchParams, storedSessionId)
|
||||
|
||||
const { data: currentSessionData, isLoading: isCurrentSessionLoading } =
|
||||
useGetV2GetSession(currentSessionId || "", {
|
||||
query: {
|
||||
enabled: !!currentSessionId && paginationEnabled,
|
||||
enabled: !!currentSessionId,
|
||||
select: okData,
|
||||
},
|
||||
});
|
||||
|
||||
const [hasAutoSelectedSession, setHasAutoSelectedSession] = useState(false);
|
||||
const hasCreatedSessionRef = useRef(false);
|
||||
const paramSessionId = searchParams.get("sessionId");
|
||||
const hasAutoSelectedRef = useRef(false);
|
||||
const paramPrompt = searchParams.get("prompt");
|
||||
|
||||
const createSessionAndNavigate = useCallback(
|
||||
function createSessionAndNavigate() {
|
||||
useEffect(() => {
|
||||
function runCreateSession() {
|
||||
postV2CreateSession({ body: JSON.stringify({}) })
|
||||
.then((response) => {
|
||||
if (response.status === 200 && response.data) {
|
||||
const promptParam = paramPrompt ? `&prompt=${encodeURIComponent(paramPrompt)}` : "";
|
||||
router.push(`/copilot/chat?sessionId=${response.data.id}${promptParam}`);
|
||||
const promptParam = paramPrompt
|
||||
? `&prompt=${encodeURIComponent(paramPrompt)}`
|
||||
: "";
|
||||
router.push(
|
||||
`/copilot/chat?sessionId=${response.data.id}${promptParam}`,
|
||||
);
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
hasCreatedSessionRef.current = false;
|
||||
});
|
||||
},
|
||||
[router, paramPrompt],
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Don't auto-select or auto-create sessions on homepage
|
||||
if (isOnHomepage) {
|
||||
setHasAutoSelectedSession(true);
|
||||
// Don't auto-select or auto-create sessions on homepage without an explicit sessionId
|
||||
if (isOnHomepage && !paramSessionId) {
|
||||
if (!hasAutoSelectedRef.current) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!areAllSessionsLoaded || hasAutoSelectedSession) return;
|
||||
if (!areAllSessionsLoaded || hasAutoSelectedRef.current) return;
|
||||
|
||||
// If there's a prompt parameter, create a new session (don't auto-select existing)
|
||||
if (paramPrompt && !paramSessionId && !hasCreatedSessionRef.current) {
|
||||
hasCreatedSessionRef.current = true;
|
||||
createSessionAndNavigate();
|
||||
runCreateSession();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +112,7 @@ export function useCopilotShell() {
|
||||
|
||||
const autoSelect = shouldAutoSelectSession(
|
||||
areAllSessionsLoaded,
|
||||
hasAutoSelectedSession,
|
||||
hasAutoSelectedRef.current,
|
||||
paramSessionId,
|
||||
visibleSessions,
|
||||
accumulatedSessions,
|
||||
@@ -118,24 +121,28 @@ export function useCopilotShell() {
|
||||
);
|
||||
|
||||
if (paramSessionId) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't auto-select existing sessions if there's a prompt (user wants new session)
|
||||
if (paramPrompt) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoSelect.shouldSelect && autoSelect.sessionIdToSelect) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
router.push(`/copilot/chat?sessionId=${autoSelect.sessionIdToSelect}`);
|
||||
} else if (autoSelect.shouldCreate && !hasCreatedSessionRef.current) {
|
||||
// Only auto-create on chat page when no sessions exist, not homepage
|
||||
hasCreatedSessionRef.current = true;
|
||||
createSessionAndNavigate();
|
||||
runCreateSession();
|
||||
} else if (totalCount === 0) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
}, [
|
||||
@@ -144,43 +151,48 @@ export function useCopilotShell() {
|
||||
accumulatedSessions,
|
||||
paramSessionId,
|
||||
paramPrompt,
|
||||
hasAutoSelectedSession,
|
||||
router,
|
||||
isSessionsLoading,
|
||||
totalCount,
|
||||
createSessionAndNavigate,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paramSessionId) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
}, [paramSessionId]);
|
||||
|
||||
function resetAutoSelect() {
|
||||
setHasAutoSelectedSession(false);
|
||||
hasCreatedSessionRef.current = false;
|
||||
}
|
||||
|
||||
// Reset pagination and auto-selection when query becomes disabled
|
||||
const prevPaginationEnabledRef = useRef(paginationEnabled);
|
||||
useEffect(() => {
|
||||
if (!paginationEnabled) {
|
||||
if (prevPaginationEnabledRef.current && !paginationEnabled) {
|
||||
resetPagination();
|
||||
resetAutoSelect();
|
||||
}
|
||||
}, [paginationEnabled, resetPagination]);
|
||||
prevPaginationEnabledRef.current = paginationEnabled;
|
||||
}, [paginationEnabled]);
|
||||
|
||||
const sessions = useMemo(
|
||||
function getSessions() {
|
||||
return mergeCurrentSessionIntoList(
|
||||
accumulatedSessions,
|
||||
currentSessionId,
|
||||
currentSessionData,
|
||||
);
|
||||
},
|
||||
[accumulatedSessions, currentSessionId, currentSessionData],
|
||||
const sessions = mergeCurrentSessionIntoList(
|
||||
accumulatedSessions,
|
||||
currentSessionId,
|
||||
currentSessionData,
|
||||
);
|
||||
|
||||
const sidebarSelectedSessionId =
|
||||
isOnHomepage && !paramSessionId ? null : currentSessionId;
|
||||
|
||||
const isReadyToShowContent = isOnHomepage
|
||||
? true
|
||||
: checkReadyToShowContent(
|
||||
areAllSessionsLoaded,
|
||||
paramSessionId,
|
||||
accumulatedSessions,
|
||||
isCurrentSessionLoading,
|
||||
currentSessionData,
|
||||
hasAutoSelectedSession,
|
||||
);
|
||||
|
||||
function handleSelectSession(sessionId: string) {
|
||||
router.push(`/copilot/chat?sessionId=${sessionId}`);
|
||||
if (isMobile) handleCloseDrawer();
|
||||
@@ -189,39 +201,24 @@ export function useCopilotShell() {
|
||||
function handleNewChat() {
|
||||
storage.clean(Key.CHAT_SESSION_ID);
|
||||
resetAutoSelect();
|
||||
createSessionAndNavigate();
|
||||
router.push("/copilot");
|
||||
if (isMobile) handleCloseDrawer();
|
||||
}
|
||||
|
||||
const isReadyToShowContent = useMemo(() => {
|
||||
// On homepage, always show content (welcome screen) immediately
|
||||
if (isOnHomepage) return true;
|
||||
|
||||
return checkReadyToShowContent(
|
||||
areAllSessionsLoaded,
|
||||
paramSessionId,
|
||||
accumulatedSessions,
|
||||
isCurrentSessionLoading,
|
||||
currentSessionData,
|
||||
hasAutoSelectedSession,
|
||||
);
|
||||
}, [
|
||||
isOnHomepage,
|
||||
areAllSessionsLoaded,
|
||||
paramSessionId,
|
||||
accumulatedSessions,
|
||||
isCurrentSessionLoading,
|
||||
currentSessionData,
|
||||
hasAutoSelectedSession,
|
||||
]);
|
||||
function resetAutoSelect() {
|
||||
hasAutoSelectedRef.current = false;
|
||||
setHasAutoSelectedSession(false);
|
||||
hasCreatedSessionRef.current = false;
|
||||
}
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isDrawerOpen,
|
||||
isLoggedIn,
|
||||
hasActiveSession: Boolean(currentSessionId) && (!isOnHomepage || paramSessionId),
|
||||
isLoading: isSessionsLoading || !areAllSessionsLoaded,
|
||||
sessions,
|
||||
currentSessionId,
|
||||
currentSessionId: sidebarSelectedSessionId,
|
||||
handleSelectSession,
|
||||
handleOpenDrawer,
|
||||
handleCloseDrawer,
|
||||
|
||||
@@ -46,66 +46,67 @@ export default function CopilotPage() {
|
||||
const isLoading = isUserLoading;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto px-6 py-10">
|
||||
<div className="w-full max-w-2xl text-center">
|
||||
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto px-6 py-10 bg-[#f8f8f9]">
|
||||
<div className="w-full text-center">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Skeleton className="mx-auto mb-3 h-8 w-64" />
|
||||
<Skeleton className="mx-auto mb-8 h-6 w-80" />
|
||||
<div className="mb-8">
|
||||
<Skeleton className="mx-auto h-14 w-full max-w-2xl rounded-lg" />
|
||||
<Skeleton className="mx-auto h-14 w-full rounded-lg" />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-9 w-48 rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Text variant="h3" className="mb-3 text-zinc-700 text-[1.5rem]">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<Text variant="h3" className="mb-3 text-zinc-700 !text-[1.375rem]">
|
||||
Hey, <span className="text-violet-600">{greetingName}</span>
|
||||
</Text>
|
||||
<Text variant="h3" className="mb-8">
|
||||
<Text variant="h3" className="mb-8 !font-normal">
|
||||
What do you want to automate?
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mb-8">
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="copilot-prompt"
|
||||
label="Copilot prompt"
|
||||
hideLabel
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
placeholder='You can search or just ask - e.g. "create a blog post outline"'
|
||||
wrapperClassName="mb-0"
|
||||
className="!py-5 !rounded-full pr-12 !text-[1rem]"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Submit prompt"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 border-zinc-800 bg-zinc-800 text-white hover:border-zinc-900 hover:bg-zinc-900"
|
||||
disabled={!value.trim()}
|
||||
>
|
||||
<ArrowUpIcon className="h-4 w-4" weight="bold" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
<form onSubmit={handleSubmit} className="mb-6">
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="copilot-prompt"
|
||||
label="Copilot prompt"
|
||||
hideLabel
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
placeholder='You can search or just ask - e.g. "create a blog post outline"'
|
||||
wrapperClassName="mb-0"
|
||||
className="!py-5 !rounded-full pr-12 !text-[1rem] border-transparent [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Submit prompt"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 border-zinc-800 bg-zinc-800 text-white hover:border-zinc-900 hover:bg-zinc-900"
|
||||
disabled={!value.trim()}
|
||||
>
|
||||
<ArrowUpIcon className="h-4 w-4" weight="bold" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex flex-nowrap items-center justify-center gap-3 overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action}
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={() => handleQuickAction(action)}
|
||||
className="border-zinc-600 text-zinc-600 text-[1rem] !py-2 !px-4 h-auto"
|
||||
className="border-zinc-600 text-zinc-600 text-[1rem] !py-2 !px-4 h-auto shrink-0"
|
||||
>
|
||||
{action}
|
||||
</Button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { getHomepageRoute } from "@/lib/constants";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import {
|
||||
@@ -11,7 +12,6 @@ import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
buildCopilotChatUrl,
|
||||
getGreetingName,
|
||||
getQuickActions,
|
||||
} from "./helpers";
|
||||
@@ -56,24 +56,40 @@ export function useCopilotHome() {
|
||||
setValue(event.target.value);
|
||||
}
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!value.trim()) return;
|
||||
router.push(buildCopilotChatUrl(value));
|
||||
async function createSessionAndNavigate(prompt: string) {
|
||||
try {
|
||||
const response = await postV2CreateSession({ body: JSON.stringify({}) });
|
||||
if (response.status === 200 && response.data) {
|
||||
try {
|
||||
sessionStorage.setItem("copilot_initial_prompt", prompt);
|
||||
} catch {
|
||||
// Ignore storage errors (private mode, etc.)
|
||||
}
|
||||
router.push(`/copilot/chat?sessionId=${response.data.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create session:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!value.trim()) return;
|
||||
await createSessionAndNavigate(value.trim());
|
||||
}
|
||||
|
||||
async function handleKeyDown(
|
||||
event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) {
|
||||
if (event.key !== "Enter") return;
|
||||
if (event.shiftKey) return;
|
||||
event.preventDefault();
|
||||
if (!value.trim()) return;
|
||||
router.push(buildCopilotChatUrl(value));
|
||||
await createSessionAndNavigate(value.trim());
|
||||
}
|
||||
|
||||
function handleQuickAction(action: string) {
|
||||
router.push(buildCopilotChatUrl(action));
|
||||
async function handleQuickAction(action: string) {
|
||||
await createSessionAndNavigate(action);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -73,7 +73,7 @@ export function Chat({
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex min-h-0 w-full flex-1 flex-col overflow-hidden">
|
||||
<main className="flex min-h-0 w-full flex-1 flex-col overflow-hidden bg-[#f8f8f9]">
|
||||
{/* Loading State - show loader when loading or creating a session (with 300ms delay) */}
|
||||
{showLoader && (isLoading || isCreating) && (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
||||
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { usePageContext } from "../../usePageContext";
|
||||
@@ -24,8 +25,11 @@ export function ChatContainer({
|
||||
sessionId,
|
||||
initialMessages,
|
||||
});
|
||||
|
||||
const { capturePageContext } = usePageContext();
|
||||
const hasSentInitialRef = useRef(false);
|
||||
const breakpoint = useBreakpoint();
|
||||
const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
|
||||
|
||||
// Wrap sendMessage to automatically capture page context
|
||||
const sendMessageWithContext = useCallback(
|
||||
@@ -56,7 +60,7 @@ export function ChatContainer({
|
||||
)}
|
||||
>
|
||||
{/* Messages or Welcome Screen - Scrollable */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex min-h-full flex-col justify-end">
|
||||
<MessageList
|
||||
messages={messages}
|
||||
@@ -69,12 +73,12 @@ export function ChatContainer({
|
||||
</div>
|
||||
|
||||
{/* Input - Fixed at bottom */}
|
||||
<div className="relative pb-4 pt-2">
|
||||
<div className="relative pb-4 pt-2 px-3">
|
||||
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
onSend={sendMessageWithContext}
|
||||
disabled={isStreaming || !sessionId}
|
||||
placeholder="You can search or just ask — e.g. “create a blog post outline”"
|
||||
placeholder={isMobile ? "You can search or just ask" : "You can search or just ask — e.g. “create a blog post outline”"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ArrowClockwise,
|
||||
ArrowsClockwiseIcon,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CopyIcon
|
||||
} from "@phosphor-icons/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -258,7 +258,7 @@ export function ChatMessage({
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-1",
|
||||
"flex gap-0",
|
||||
isUser ? "justify-end" : "justify-start",
|
||||
)}
|
||||
>
|
||||
@@ -269,7 +269,7 @@ export function ChatMessage({
|
||||
onClick={handleTryAgain}
|
||||
aria-label="Try again"
|
||||
>
|
||||
<ArrowClockwise className="size-3 text-neutral-500" />
|
||||
<ArrowsClockwiseIcon className="size-4 text-zinc-600" />
|
||||
</Button>
|
||||
)}
|
||||
{(isUser || isFinalMessage) && (
|
||||
@@ -280,9 +280,9 @@ export function ChatMessage({
|
||||
aria-label="Copy message"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="size-3 text-green-600" />
|
||||
<CheckIcon className="size-4 text-green-600" />
|
||||
) : (
|
||||
<CopyIcon className="size-3 text-neutral-500" />
|
||||
<CopyIcon className="size-4 text-zinc-600" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -32,15 +32,19 @@ export function MessageList({
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto overflow-x-hidden",
|
||||
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex min-w-0 flex-col hyphens-auto break-words py-4">
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
{/* Top fade shadow */}
|
||||
<div className="pointer-events-none absolute top-0 z-10 h-8 w-full bg-gradient-to-b from-[#f8f8f9] to-transparent" />
|
||||
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto overflow-x-hidden",
|
||||
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex min-w-0 flex-col hyphens-auto break-words py-4">
|
||||
{/* Render all persisted messages */}
|
||||
{(() => {
|
||||
const lastAssistantMessageIndex = findLastMessageIndex(
|
||||
@@ -99,7 +103,11 @@ export function MessageList({
|
||||
|
||||
{/* Invisible div to scroll to */}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom fade shadow */}
|
||||
<div className="pointer-events-none absolute bottom-0 z-10 h-8 w-full bg-gradient-to-t from-[#f8f8f9] to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,13 @@ export function useChatStream() {
|
||||
|
||||
const stopStreaming = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
try {
|
||||
if (!abortControllerRef.current.signal.aborted) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
} catch {
|
||||
// Ignore abort errors - signal may already be aborted or invalid
|
||||
}
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
if (retryTimeoutRef.current) {
|
||||
|
||||
Reference in New Issue
Block a user