mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-20 20:48:11 -05:00
chore: further improvements
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Chat } from "@/components/contextual/Chat/Chat";
|
||||
import { useCopilotChatPage } from "./useCopilotChatPage";
|
||||
|
||||
export default function CopilotChatPage() {
|
||||
const { isFlagReady, isChatEnabled, sessionId, prompt } =
|
||||
useCopilotChatPage();
|
||||
|
||||
if (!isFlagReady || isChatEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<Chat
|
||||
className="flex-1"
|
||||
urlSessionId={sessionId}
|
||||
initialPrompt={prompt}
|
||||
showNewChatButton={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { getHomepageRoute } from "@/lib/constants";
|
||||
import {
|
||||
Flag,
|
||||
type FlagValues,
|
||||
useGetFlag,
|
||||
} from "@/services/feature-flags/use-get-flag";
|
||||
import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useCopilotChatPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const flags = useFlags<FlagValues>();
|
||||
const homepageRoute = getHomepageRoute(isChatEnabled);
|
||||
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
|
||||
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
|
||||
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
|
||||
const isFlagReady =
|
||||
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
|
||||
|
||||
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() {
|
||||
if (!isFlagReady) return;
|
||||
if (isChatEnabled === false) {
|
||||
router.replace(homepageRoute);
|
||||
}
|
||||
},
|
||||
[homepageRoute, isChatEnabled, isFlagReady, router],
|
||||
);
|
||||
|
||||
return {
|
||||
isFlagReady,
|
||||
isChatEnabled,
|
||||
sessionId,
|
||||
prompt: prompt ?? storedPrompt,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
postV2CreateSession,
|
||||
getGetV2ListSessionsQueryKey,
|
||||
useGetV2GetSession,
|
||||
} from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { Key, storage } from "@/services/storage/local-storage";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer";
|
||||
@@ -17,13 +18,13 @@ import {
|
||||
filterVisibleSessions,
|
||||
getCurrentSessionId,
|
||||
mergeCurrentSessionIntoList,
|
||||
shouldAutoSelectSession,
|
||||
} from "./helpers";
|
||||
|
||||
export function useCopilotShell() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const breakpoint = useBreakpoint();
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const isMobile =
|
||||
@@ -47,7 +48,6 @@ export function useCopilotShell() {
|
||||
isFetching: isSessionsFetching,
|
||||
hasNextPage,
|
||||
areAllSessionsLoaded,
|
||||
totalCount,
|
||||
fetchNextPage,
|
||||
reset: resetPagination,
|
||||
} = useSessionsPagination({
|
||||
@@ -66,104 +66,34 @@ export function useCopilotShell() {
|
||||
});
|
||||
|
||||
const [hasAutoSelectedSession, setHasAutoSelectedSession] = useState(false);
|
||||
const hasCreatedSessionRef = useRef(false);
|
||||
const hasAutoSelectedRef = useRef(false);
|
||||
const paramPrompt = searchParams.get("prompt");
|
||||
|
||||
// Mark as auto-selected when sessionId is in URL
|
||||
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}`,
|
||||
);
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
hasCreatedSessionRef.current = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 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 || 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;
|
||||
runCreateSession();
|
||||
return;
|
||||
}
|
||||
|
||||
const visibleSessions = filterVisibleSessions(accumulatedSessions);
|
||||
|
||||
const autoSelect = shouldAutoSelectSession(
|
||||
areAllSessionsLoaded,
|
||||
hasAutoSelectedRef.current,
|
||||
paramSessionId,
|
||||
visibleSessions,
|
||||
accumulatedSessions,
|
||||
isSessionsLoading,
|
||||
totalCount,
|
||||
);
|
||||
|
||||
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;
|
||||
runCreateSession();
|
||||
} else if (totalCount === 0) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
}, [
|
||||
isOnHomepage,
|
||||
areAllSessionsLoaded,
|
||||
accumulatedSessions,
|
||||
paramSessionId,
|
||||
paramPrompt,
|
||||
router,
|
||||
isSessionsLoading,
|
||||
totalCount,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paramSessionId) {
|
||||
if (paramSessionId && !hasAutoSelectedRef.current) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
}, [paramSessionId]);
|
||||
|
||||
// Reset pagination and auto-selection when query becomes disabled
|
||||
// On homepage without sessionId, mark as ready immediately
|
||||
useEffect(() => {
|
||||
if (isOnHomepage && !paramSessionId && !hasAutoSelectedRef.current) {
|
||||
hasAutoSelectedRef.current = true;
|
||||
setHasAutoSelectedSession(true);
|
||||
}
|
||||
}, [isOnHomepage, paramSessionId]);
|
||||
|
||||
// Invalidate sessions list when navigating to homepage (to show newly created sessions)
|
||||
useEffect(() => {
|
||||
if (isOnHomepage && !paramSessionId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListSessionsQueryKey(),
|
||||
});
|
||||
}
|
||||
}, [isOnHomepage, paramSessionId, queryClient]);
|
||||
|
||||
// Reset pagination when query becomes disabled
|
||||
const prevPaginationEnabledRef = useRef(paginationEnabled);
|
||||
useEffect(() => {
|
||||
if (prevPaginationEnabledRef.current && !paginationEnabled) {
|
||||
@@ -171,7 +101,7 @@ export function useCopilotShell() {
|
||||
resetAutoSelect();
|
||||
}
|
||||
prevPaginationEnabledRef.current = paginationEnabled;
|
||||
}, [paginationEnabled]);
|
||||
}, [paginationEnabled, resetPagination]);
|
||||
|
||||
const sessions = mergeCurrentSessionIntoList(
|
||||
accumulatedSessions,
|
||||
@@ -179,6 +109,8 @@ export function useCopilotShell() {
|
||||
currentSessionData,
|
||||
);
|
||||
|
||||
const visibleSessions = filterVisibleSessions(sessions);
|
||||
|
||||
const sidebarSelectedSessionId =
|
||||
isOnHomepage && !paramSessionId ? null : currentSessionId;
|
||||
|
||||
@@ -194,21 +126,28 @@ export function useCopilotShell() {
|
||||
);
|
||||
|
||||
function handleSelectSession(sessionId: string) {
|
||||
router.push(`/copilot/chat?sessionId=${sessionId}`);
|
||||
// Navigate using replaceState to avoid full page reload
|
||||
window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`);
|
||||
// Force a re-render by updating the URL through router
|
||||
router.replace(`/copilot?sessionId=${sessionId}`);
|
||||
if (isMobile) handleCloseDrawer();
|
||||
}
|
||||
|
||||
function handleNewChat() {
|
||||
storage.clean(Key.CHAT_SESSION_ID);
|
||||
resetAutoSelect();
|
||||
router.push("/copilot");
|
||||
// Invalidate sessions list to ensure newly created sessions appear
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListSessionsQueryKey(),
|
||||
});
|
||||
window.history.replaceState(null, "", "/copilot");
|
||||
router.replace("/copilot");
|
||||
if (isMobile) handleCloseDrawer();
|
||||
}
|
||||
|
||||
function resetAutoSelect() {
|
||||
hasAutoSelectedRef.current = false;
|
||||
setHasAutoSelectedSession(false);
|
||||
hasCreatedSessionRef.current = false;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -216,9 +155,9 @@ export function useCopilotShell() {
|
||||
isDrawerOpen,
|
||||
isLoggedIn,
|
||||
hasActiveSession:
|
||||
Boolean(currentSessionId) && (!isOnHomepage || paramSessionId),
|
||||
Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)),
|
||||
isLoading: isSessionsLoading || !areAllSessionsLoaded,
|
||||
sessions,
|
||||
sessions: visibleSessions,
|
||||
currentSessionId: sidebarSelectedSessionId,
|
||||
handleSelectSession,
|
||||
handleOpenDrawer,
|
||||
|
||||
@@ -1,28 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Chat } from "@/components/contextual/Chat/Chat";
|
||||
import { getHomepageRoute } from "@/lib/constants";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import {
|
||||
Flag,
|
||||
type FlagValues,
|
||||
useGetFlag,
|
||||
} from "@/services/feature-flags/use-get-flag";
|
||||
import { ArrowUpIcon } from "@phosphor-icons/react";
|
||||
import { useEffect } from "react";
|
||||
import { useCopilotHome } from "./useCopilotHome";
|
||||
import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { getGreetingName, getQuickActions } from "./helpers";
|
||||
|
||||
type PageState =
|
||||
| { type: "welcome" }
|
||||
| { type: "creating"; prompt: string }
|
||||
| { type: "chat"; sessionId: string; initialPrompt?: string };
|
||||
|
||||
export default function CopilotPage() {
|
||||
const {
|
||||
greetingName,
|
||||
value,
|
||||
quickActions,
|
||||
isFlagReady,
|
||||
isChatEnabled,
|
||||
isUserLoading,
|
||||
isLoggedIn,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
handleKeyDown,
|
||||
handleQuickAction,
|
||||
} = useCopilotHome();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { user, isLoggedIn, isUserLoading } = useSupabase();
|
||||
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const flags = useFlags<FlagValues>();
|
||||
const homepageRoute = getHomepageRoute(isChatEnabled);
|
||||
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
|
||||
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
|
||||
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
|
||||
const isFlagReady =
|
||||
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
|
||||
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [pageState, setPageState] = useState<PageState>({ type: "welcome" });
|
||||
const initialPromptRef = useRef<Map<string, string>>(new Map());
|
||||
|
||||
const urlSessionId = searchParams.get("sessionId");
|
||||
|
||||
// Sync with URL sessionId (preserve initialPrompt from ref)
|
||||
useEffect(
|
||||
function syncSessionFromUrl() {
|
||||
if (urlSessionId) {
|
||||
// If we're already in chat state with this sessionId, don't overwrite
|
||||
if (
|
||||
pageState.type === "chat" &&
|
||||
pageState.sessionId === urlSessionId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Get initialPrompt from ref or current state
|
||||
const storedInitialPrompt = initialPromptRef.current.get(urlSessionId);
|
||||
const currentInitialPrompt =
|
||||
storedInitialPrompt ||
|
||||
(pageState.type === "creating"
|
||||
? pageState.prompt
|
||||
: pageState.type === "chat"
|
||||
? pageState.initialPrompt
|
||||
: undefined);
|
||||
if (currentInitialPrompt) {
|
||||
initialPromptRef.current.set(urlSessionId, currentInitialPrompt);
|
||||
}
|
||||
setPageState({
|
||||
type: "chat",
|
||||
sessionId: urlSessionId,
|
||||
initialPrompt: currentInitialPrompt,
|
||||
});
|
||||
} else if (pageState.type === "chat") {
|
||||
setPageState({ type: "welcome" });
|
||||
}
|
||||
},
|
||||
[urlSessionId],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function ensureAccess() {
|
||||
if (!isFlagReady) return;
|
||||
if (isChatEnabled === false) {
|
||||
router.replace(homepageRoute);
|
||||
}
|
||||
},
|
||||
[homepageRoute, isChatEnabled, isFlagReady, router],
|
||||
);
|
||||
|
||||
const greetingName = useMemo(
|
||||
function getName() {
|
||||
return getGreetingName(user);
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const quickActions = useMemo(function getActions() {
|
||||
return getQuickActions();
|
||||
}, []);
|
||||
|
||||
async function startChatWithPrompt(prompt: string) {
|
||||
if (!prompt?.trim()) return;
|
||||
if (pageState.type === "creating") return;
|
||||
|
||||
const trimmedPrompt = prompt.trim();
|
||||
setPageState({ type: "creating", prompt: trimmedPrompt });
|
||||
setInputValue("");
|
||||
|
||||
try {
|
||||
// Create session
|
||||
const sessionResponse = await postV2CreateSession({
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
|
||||
throw new Error("Failed to create session");
|
||||
}
|
||||
|
||||
const sessionId = sessionResponse.data.id;
|
||||
|
||||
// Store initialPrompt in ref so it persists across re-renders
|
||||
initialPromptRef.current.set(sessionId, trimmedPrompt);
|
||||
|
||||
// Update URL and show Chat with initial prompt
|
||||
// Chat will handle sending the message and streaming
|
||||
window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`);
|
||||
setPageState({ type: "chat", sessionId, initialPrompt: trimmedPrompt });
|
||||
} catch (error) {
|
||||
console.error("[CopilotPage] Failed to start chat:", error);
|
||||
setPageState({ type: "welcome" });
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!inputValue.trim()) return;
|
||||
startChatWithPrompt(inputValue.trim());
|
||||
}
|
||||
|
||||
function handleKeyDown(
|
||||
event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) {
|
||||
if (event.key !== "Enter") return;
|
||||
if (event.shiftKey) return;
|
||||
event.preventDefault();
|
||||
if (!inputValue.trim()) return;
|
||||
startChatWithPrompt(inputValue.trim());
|
||||
}
|
||||
|
||||
function handleQuickAction(action: string) {
|
||||
startChatWithPrompt(action);
|
||||
}
|
||||
|
||||
// Auto-grow textarea
|
||||
useEffect(() => {
|
||||
const textarea = document.getElementById(
|
||||
"copilot-prompt",
|
||||
@@ -39,12 +171,39 @@ export default function CopilotPage() {
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
textarea.style.overflowY =
|
||||
textarea.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||
}, [value]);
|
||||
}, [inputValue]);
|
||||
|
||||
if (!isFlagReady || isChatEnabled === false || !isLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show Chat when we have an active session
|
||||
if (pageState.type === "chat") {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<Chat
|
||||
key={pageState.sessionId ?? "welcome"}
|
||||
className="flex-1"
|
||||
urlSessionId={pageState.sessionId}
|
||||
initialPrompt={pageState.initialPrompt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state while creating session and sending first message
|
||||
if (pageState.type === "creating") {
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9] px-6 py-10">
|
||||
<LoadingSpinner size="large" />
|
||||
<Text variant="body" className="mt-4 text-zinc-500">
|
||||
Starting your chat...
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show Welcome screen
|
||||
const isLoading = isUserLoading;
|
||||
|
||||
return (
|
||||
@@ -83,8 +242,8 @@ export default function CopilotPage() {
|
||||
label="Copilot prompt"
|
||||
hideLabel
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
placeholder='You can search or just ask - e.g. "create a blog post outline"'
|
||||
@@ -97,7 +256,7 @@ export default function CopilotPage() {
|
||||
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()}
|
||||
disabled={!inputValue.trim()}
|
||||
>
|
||||
<ArrowUpIcon className="h-4 w-4" weight="bold" />
|
||||
</Button>
|
||||
@@ -108,6 +267,7 @@ export default function CopilotPage() {
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={() => handleQuickAction(action)}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { getHomepageRoute } from "@/lib/constants";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import {
|
||||
Flag,
|
||||
type FlagValues,
|
||||
useGetFlag,
|
||||
} from "@/services/feature-flags/use-get-flag";
|
||||
import { useFlags } from "launchdarkly-react-client-sdk";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getGreetingName, getQuickActions } from "./helpers";
|
||||
|
||||
export function useCopilotHome() {
|
||||
const router = useRouter();
|
||||
const { user, isLoggedIn, isUserLoading } = useSupabase();
|
||||
const [value, setValue] = useState("");
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const flags = useFlags<FlagValues>();
|
||||
const homepageRoute = getHomepageRoute(isChatEnabled);
|
||||
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
|
||||
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
|
||||
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
|
||||
const isFlagReady =
|
||||
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
|
||||
|
||||
const greetingName = useMemo(
|
||||
function getName() {
|
||||
return getGreetingName(user);
|
||||
},
|
||||
[user],
|
||||
);
|
||||
|
||||
const quickActions = useMemo(function getActions() {
|
||||
return getQuickActions();
|
||||
}, []);
|
||||
|
||||
useEffect(
|
||||
function ensureAccess() {
|
||||
if (!isFlagReady) return;
|
||||
if (isChatEnabled === false) {
|
||||
router.replace(homepageRoute);
|
||||
}
|
||||
},
|
||||
[homepageRoute, isChatEnabled, isFlagReady, router],
|
||||
);
|
||||
|
||||
function handleChange(
|
||||
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) {
|
||||
setValue(event.target.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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
await createSessionAndNavigate(value.trim());
|
||||
}
|
||||
|
||||
async function handleQuickAction(action: string) {
|
||||
await createSessionAndNavigate(action);
|
||||
}
|
||||
|
||||
return {
|
||||
greetingName,
|
||||
value,
|
||||
quickActions,
|
||||
isFlagReady,
|
||||
isChatEnabled,
|
||||
isLoggedIn,
|
||||
isUserLoading,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
handleKeyDown,
|
||||
handleQuickAction,
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
|
||||
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
|
||||
import { ChatLoader } from "./components/ChatLoader/ChatLoader";
|
||||
@@ -11,22 +9,12 @@ import { useChat } from "./useChat";
|
||||
|
||||
export interface ChatProps {
|
||||
className?: string;
|
||||
showHeader?: boolean;
|
||||
showSessionInfo?: boolean;
|
||||
showNewChatButton?: boolean;
|
||||
onNewChat?: () => void;
|
||||
headerActions?: ReactNode;
|
||||
urlSessionId?: string | null;
|
||||
initialPrompt?: string | null;
|
||||
initialPrompt?: string;
|
||||
}
|
||||
|
||||
export function Chat({
|
||||
className,
|
||||
showHeader = true,
|
||||
showSessionInfo = true,
|
||||
showNewChatButton = true,
|
||||
onNewChat,
|
||||
headerActions,
|
||||
urlSessionId,
|
||||
initialPrompt,
|
||||
}: ChatProps) {
|
||||
@@ -37,44 +25,14 @@ export function Chat({
|
||||
error,
|
||||
sessionId,
|
||||
createSession,
|
||||
clearSession,
|
||||
showLoader,
|
||||
} = useChat({ urlSessionId });
|
||||
|
||||
function handleNewChat() {
|
||||
clearSession();
|
||||
onNewChat?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full flex-col", className)}>
|
||||
{/* Header */}
|
||||
{showHeader && (
|
||||
<header className="shrink-0 bg-[#f8f8f9] p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{showSessionInfo && sessionId && (
|
||||
<>
|
||||
{showNewChatButton && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={handleNewChat}
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{headerActions}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<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) */}
|
||||
{/* Loading State */}
|
||||
{showLoader && (isLoading || isCreating) && (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { useCallback } from "react";
|
||||
import { usePageContext } from "../../usePageContext";
|
||||
import { ChatInput } from "../ChatInput/ChatInput";
|
||||
import { MessageList } from "../MessageList/MessageList";
|
||||
@@ -10,29 +10,28 @@ import { useChatContainer } from "./useChatContainer";
|
||||
export interface ChatContainerProps {
|
||||
sessionId: string | null;
|
||||
initialMessages: SessionDetailResponse["messages"];
|
||||
initialPrompt?: string;
|
||||
className?: string;
|
||||
initialPrompt?: string | null;
|
||||
}
|
||||
|
||||
export function ChatContainer({
|
||||
sessionId,
|
||||
initialMessages,
|
||||
className,
|
||||
initialPrompt,
|
||||
className,
|
||||
}: ChatContainerProps) {
|
||||
const { messages, streamingChunks, isStreaming, sendMessage } =
|
||||
useChatContainer({
|
||||
sessionId,
|
||||
initialMessages,
|
||||
initialPrompt,
|
||||
});
|
||||
|
||||
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(
|
||||
async (content: string, isUserMessage: boolean = true) => {
|
||||
const context = capturePageContext();
|
||||
@@ -41,18 +40,6 @@ export function ChatContainer({
|
||||
[sendMessage, capturePageContext],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function handleInitialPrompt() {
|
||||
if (!initialPrompt) return;
|
||||
if (hasSentInitialRef.current) return;
|
||||
if (!sessionId) return;
|
||||
if (messages.length > 0) return;
|
||||
hasSentInitialRef.current = true;
|
||||
void sendMessageWithContext(initialPrompt);
|
||||
},
|
||||
[initialPrompt, messages.length, sendMessageWithContext, sessionId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -60,7 +47,7 @@ export function ChatContainer({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Messages or Welcome Screen - Scrollable */}
|
||||
{/* Messages - Scrollable */}
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex min-h-full flex-col justify-end">
|
||||
<MessageList
|
||||
@@ -74,7 +61,7 @@ export function ChatContainer({
|
||||
</div>
|
||||
|
||||
{/* Input - Fixed at bottom */}
|
||||
<div className="relative px-3 pb-4 pt-2">
|
||||
<div className="relative px-3 pb-6 pt-2">
|
||||
<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}
|
||||
@@ -82,7 +69,7 @@ export function ChatContainer({
|
||||
placeholder={
|
||||
isMobile
|
||||
? "You can search or just ask"
|
||||
: "You can search or just ask — e.g. “create a blog post outline”"
|
||||
: "You can search or just ask — e.g. \"create a blog post outline\""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import {
|
||||
SessionKey,
|
||||
sessionStorage,
|
||||
} from "@/services/storage/session-storage";
|
||||
import type { ToolResult } from "@/types/chat";
|
||||
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
||||
|
||||
export function hasSentInitialPrompt(sessionId: string): boolean {
|
||||
try {
|
||||
const sent = JSON.parse(
|
||||
sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
|
||||
);
|
||||
return sent[sessionId] === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function markInitialPromptSent(sessionId: string): void {
|
||||
try {
|
||||
const sent = JSON.parse(
|
||||
sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
|
||||
);
|
||||
sent[sessionId] = true;
|
||||
sessionStorage.set(SessionKey.CHAT_SENT_INITIAL_PROMPTS, JSON.stringify(sent));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function removePageContext(content: string): string {
|
||||
// Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats)
|
||||
let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, "");
|
||||
|
||||
@@ -30,18 +30,9 @@ export function handleTextEnded(
|
||||
_chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
) {
|
||||
console.log("[Text Ended] Saving streamed text as assistant message");
|
||||
const completedText = deps.streamingChunksRef.current.join("");
|
||||
if (completedText.trim()) {
|
||||
deps.setMessages((prev) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
console.log("[Text Ended] Previous message:", {
|
||||
type: lastMessage?.type,
|
||||
toolName:
|
||||
lastMessage?.type === "tool_call" ? lastMessage.toolName : undefined,
|
||||
content: completedText.substring(0, 200),
|
||||
});
|
||||
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
@@ -68,22 +59,12 @@ export function handleToolCallStart(
|
||||
timestamp: new Date(),
|
||||
};
|
||||
deps.setMessages((prev) => [...prev, toolCallMessage]);
|
||||
console.log("[Tool Call Start]", {
|
||||
toolId: toolCallMessage.toolId,
|
||||
toolName: toolCallMessage.toolName,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function handleToolResponse(
|
||||
chunk: StreamChunk,
|
||||
deps: HandlerDependencies,
|
||||
) {
|
||||
console.log("[Tool Response] Received:", {
|
||||
toolId: chunk.tool_id,
|
||||
toolName: chunk.tool_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
let toolName = chunk.tool_name || "unknown";
|
||||
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
||||
deps.setMessages((prev) => {
|
||||
@@ -140,19 +121,8 @@ export function handleToolResponse(
|
||||
if (toolCallIndex !== -1) {
|
||||
const newMessages = [...prev];
|
||||
newMessages[toolCallIndex] = responseMessage;
|
||||
console.log(
|
||||
"[Tool Response] Replaced tool_call with matching tool_id:",
|
||||
chunk.tool_id,
|
||||
"at index:",
|
||||
toolCallIndex,
|
||||
);
|
||||
return newMessages;
|
||||
}
|
||||
console.warn(
|
||||
"[Tool Response] No tool_call found with tool_id:",
|
||||
chunk.tool_id,
|
||||
"appending instead",
|
||||
);
|
||||
return [...prev, responseMessage];
|
||||
});
|
||||
}
|
||||
@@ -177,50 +147,19 @@ export function handleStreamEnd(
|
||||
deps: HandlerDependencies,
|
||||
) {
|
||||
const completedContent = deps.streamingChunksRef.current.join("");
|
||||
// Only save message if there are uncommitted chunks
|
||||
// (text_ended already saved if there were tool calls)
|
||||
if (completedContent.trim()) {
|
||||
console.log(
|
||||
"[Stream End] Saving remaining streamed text as assistant message",
|
||||
);
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedContent,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
deps.setMessages((prev) => {
|
||||
const updated = [...prev, assistantMessage];
|
||||
console.log("[Stream End] Final state:", {
|
||||
localMessages: updated.map((m) => ({
|
||||
type: m.type,
|
||||
...(m.type === "message" && {
|
||||
role: m.role,
|
||||
contentLength: m.content.length,
|
||||
}),
|
||||
...(m.type === "tool_call" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
}),
|
||||
...(m.type === "tool_response" && {
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
success: m.success,
|
||||
}),
|
||||
})),
|
||||
streamingChunks: deps.streamingChunksRef.current,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
} else {
|
||||
console.log("[Stream End] No uncommitted chunks, message already saved");
|
||||
deps.setMessages((prev) => [...prev, assistantMessage]);
|
||||
}
|
||||
deps.setStreamingChunks([]);
|
||||
deps.streamingChunksRef.current = [];
|
||||
deps.setHasTextChunks(false);
|
||||
deps.setIsStreamingInitiated(false);
|
||||
console.log("[Stream End] Stream complete, messages in local state");
|
||||
}
|
||||
|
||||
export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useChatStream } from "../../useChatStream";
|
||||
import { usePageContext } from "../../usePageContext";
|
||||
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
|
||||
import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
|
||||
import {
|
||||
createUserMessage,
|
||||
filterAuthMessages,
|
||||
hasSentInitialPrompt,
|
||||
isToolCallArray,
|
||||
isValidMessage,
|
||||
markInitialPromptSent,
|
||||
parseToolResponse,
|
||||
removePageContext,
|
||||
} from "./helpers";
|
||||
@@ -16,9 +19,10 @@ import {
|
||||
interface Args {
|
||||
sessionId: string | null;
|
||||
initialMessages: SessionDetailResponse["messages"];
|
||||
initialPrompt?: string;
|
||||
}
|
||||
|
||||
export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
export function useChatContainer({ sessionId, initialMessages, initialPrompt }: Args) {
|
||||
const [messages, setMessages] = useState<ChatMessageData[]>([]);
|
||||
const [streamingChunks, setStreamingChunks] = useState<string[]>([]);
|
||||
const [hasTextChunks, setHasTextChunks] = useState(false);
|
||||
@@ -29,7 +33,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
|
||||
const allMessages = useMemo(() => {
|
||||
const processedInitialMessages: ChatMessageData[] = [];
|
||||
// Map to track tool calls by their ID so we can look up tool names for tool responses
|
||||
const toolCallMap = new Map<string, string>();
|
||||
|
||||
for (const msg of initialMessages) {
|
||||
@@ -45,13 +48,9 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
? new Date(msg.timestamp as string)
|
||||
: undefined;
|
||||
|
||||
// Remove page context from user messages when loading existing sessions
|
||||
if (role === "user") {
|
||||
content = removePageContext(content);
|
||||
// Skip user messages that become empty after removing page context
|
||||
if (!content.trim()) {
|
||||
continue;
|
||||
}
|
||||
if (!content.trim()) continue;
|
||||
processedInitialMessages.push({
|
||||
type: "message",
|
||||
role: "user",
|
||||
@@ -61,19 +60,15 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle assistant messages first (before tool messages) to build tool call map
|
||||
if (role === "assistant") {
|
||||
// Strip <thinking> tags from content
|
||||
content = content
|
||||
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "")
|
||||
.trim();
|
||||
|
||||
// If assistant has tool calls, create tool_call messages for each
|
||||
if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) {
|
||||
for (const toolCall of toolCalls) {
|
||||
const toolName = toolCall.function.name;
|
||||
const toolId = toolCall.id;
|
||||
// Store tool name for later lookup
|
||||
toolCallMap.set(toolId, toolName);
|
||||
|
||||
try {
|
||||
@@ -96,7 +91,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only add assistant message if there's content after stripping thinking tags
|
||||
if (content.trim()) {
|
||||
processedInitialMessages.push({
|
||||
type: "message",
|
||||
@@ -106,7 +100,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
});
|
||||
}
|
||||
} else if (content.trim()) {
|
||||
// Assistant message without tool calls, but with content
|
||||
processedInitialMessages.push({
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
@@ -117,7 +110,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle tool messages - look up tool name from tool call map
|
||||
if (role === "tool") {
|
||||
const toolCallId = (msg.tool_call_id as string) || "";
|
||||
const toolName = toolCallMap.get(toolCallId) || "unknown";
|
||||
@@ -133,7 +125,6 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle other message types (system, etc.)
|
||||
if (content.trim()) {
|
||||
processedInitialMessages.push({
|
||||
type: "message",
|
||||
@@ -154,7 +145,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
context?: { url: string; content: string },
|
||||
) {
|
||||
if (!sessionId) {
|
||||
console.error("Cannot send message: no session ID");
|
||||
console.error("[useChatContainer] Cannot send message: no session ID");
|
||||
return;
|
||||
}
|
||||
if (isUserMessage) {
|
||||
@@ -167,6 +158,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
streamingChunksRef.current = [];
|
||||
setHasTextChunks(false);
|
||||
setIsStreamingInitiated(true);
|
||||
|
||||
const dispatcher = createStreamEventDispatcher({
|
||||
setHasTextChunks,
|
||||
setStreamingChunks,
|
||||
@@ -175,6 +167,7 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
sessionId,
|
||||
setIsStreamingInitiated,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendStreamMessage(
|
||||
sessionId,
|
||||
@@ -184,8 +177,12 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
context,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to send message:", err);
|
||||
console.error("[useChatContainer] Failed to send message:", err);
|
||||
setIsStreamingInitiated(false);
|
||||
|
||||
// Don't show error toast for AbortError (expected during cleanup)
|
||||
if (err instanceof Error && err.name === "AbortError") return;
|
||||
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to send message";
|
||||
toast.error("Failed to send message", {
|
||||
@@ -196,6 +193,22 @@ export function useChatContainer({ sessionId, initialMessages }: Args) {
|
||||
[sessionId, sendStreamMessage],
|
||||
);
|
||||
|
||||
const { capturePageContext } = usePageContext();
|
||||
|
||||
// Send initial prompt if provided (for new sessions from homepage)
|
||||
useEffect(
|
||||
function handleInitialPrompt() {
|
||||
if (!initialPrompt || !sessionId) return;
|
||||
if (initialMessages.length > 0) return;
|
||||
if (hasSentInitialPrompt(sessionId)) return;
|
||||
|
||||
markInitialPromptSent(sessionId);
|
||||
const context = capturePageContext();
|
||||
sendMessage(initialPrompt, true, context);
|
||||
},
|
||||
[initialPrompt, sessionId, initialMessages.length, sendMessage, capturePageContext],
|
||||
);
|
||||
|
||||
return {
|
||||
messages: allMessages,
|
||||
streamingChunks,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowUpIcon } from "@phosphor-icons/react";
|
||||
@@ -24,41 +25,43 @@ export function ChatInput({
|
||||
inputId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn("relative flex-1", className)}>
|
||||
<Input
|
||||
id={inputId}
|
||||
label="Chat message input"
|
||||
hideLabel
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
wrapperClassName="mb-0 relative"
|
||||
className="pr-12"
|
||||
/>
|
||||
<span id="chat-input-hint" className="sr-only">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</span>
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={disabled || !value.trim()}
|
||||
className={cn(
|
||||
"absolute right-3 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full",
|
||||
"border border-zinc-800 bg-zinc-800 text-white",
|
||||
"hover:border-zinc-900 hover:bg-zinc-900",
|
||||
"disabled:border-zinc-200 disabled:bg-zinc-200 disabled:text-white disabled:opacity-50",
|
||||
"transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950",
|
||||
"disabled:pointer-events-none",
|
||||
)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<ArrowUpIcon className="h-3 w-3" weight="bold" />
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={cn("relative flex-1", className)}>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={inputId}
|
||||
label="Chat message input"
|
||||
hideLabel
|
||||
type="textarea"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
rows={1}
|
||||
wrapperClassName="mb-0"
|
||||
className="!rounded-full border-transparent !py-5 pr-12 !text-[1rem] resize-none [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
/>
|
||||
<span id="chat-input-hint" className="sr-only">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</span>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="icon"
|
||||
size="icon"
|
||||
aria-label="Send message"
|
||||
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={disabled || !value.trim()}
|
||||
>
|
||||
<ArrowUpIcon className="h-4 w-4" weight="bold" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,29 +45,8 @@ export function useMessageItem({
|
||||
success: true,
|
||||
timestamp: message.timestamp,
|
||||
} as ChatMessageData;
|
||||
|
||||
console.log(
|
||||
"[MessageItem] Converting assistant message to tool output:",
|
||||
{
|
||||
content: message.content.substring(0, 100),
|
||||
prevToolName: prevMessage.toolName,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Log for debugging
|
||||
if (message.type === "message" && message.role === "assistant") {
|
||||
const prevMessageToolName =
|
||||
prevMessage?.type === "tool_call" ? prevMessage.toolName : undefined;
|
||||
console.log("[MessageItem] Assistant message:", {
|
||||
index,
|
||||
content: message.content.substring(0, 200),
|
||||
fullContent: message.content,
|
||||
prevMessageType: prevMessage?.type,
|
||||
prevMessageToolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isFinalMessage =
|
||||
|
||||
@@ -189,10 +189,7 @@ export function useChatSession({
|
||||
|
||||
const refreshSession = useCallback(
|
||||
async function refreshSession() {
|
||||
if (!sessionId) {
|
||||
console.log("[refreshSession] Skipping - no session ID");
|
||||
return;
|
||||
}
|
||||
if (!sessionId) return;
|
||||
try {
|
||||
setError(null);
|
||||
await refetch();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ToolArguments, ToolResult } from "@/types/chat";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
@@ -151,13 +151,14 @@ export function useChatStream() {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const stopStreaming = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
const controller = abortControllerRef.current;
|
||||
if (controller) {
|
||||
try {
|
||||
if (!abortControllerRef.current.signal.aborted) {
|
||||
abortControllerRef.current.abort();
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort();
|
||||
}
|
||||
} catch {
|
||||
// Ignore abort errors - signal may already be aborted or invalid
|
||||
// Ignore abort errors
|
||||
}
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
@@ -168,12 +169,6 @@ export function useChatStream() {
|
||||
setIsStreaming(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopStreaming();
|
||||
};
|
||||
}, [stopStreaming]);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (
|
||||
sessionId: string,
|
||||
@@ -238,11 +233,9 @@ export function useChatStream() {
|
||||
onChunk({ type: "stream_end" });
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
reader.cancel().catch(() => {
|
||||
// Ignore cancel errors
|
||||
});
|
||||
};
|
||||
function cleanup() {
|
||||
reader.cancel().catch(() => {});
|
||||
}
|
||||
|
||||
async function readStream() {
|
||||
try {
|
||||
@@ -283,10 +276,8 @@ export function useChatStream() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Call the chunk handler
|
||||
onChunk(chunk);
|
||||
|
||||
// Handle stream lifecycle
|
||||
if (chunk.type === "stream_end") {
|
||||
didDispatchStreamEnd = true;
|
||||
cleanup();
|
||||
@@ -303,9 +294,8 @@ export function useChatStream() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
console.warn("Failed to parse SSE chunk:", err, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,6 +303,9 @@ export function useChatStream() {
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
cleanup();
|
||||
dispatchStreamEnd();
|
||||
stopStreaming();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -336,9 +329,7 @@ export function useChatStream() {
|
||||
isUserMessage,
|
||||
context,
|
||||
true,
|
||||
).catch((_err) => {
|
||||
// Retry failed
|
||||
});
|
||||
).catch(() => {});
|
||||
}, retryDelay);
|
||||
} else {
|
||||
setError(streamError);
|
||||
@@ -358,6 +349,10 @@ export function useChatStream() {
|
||||
readStream();
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
setIsStreaming(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
const streamError =
|
||||
err instanceof Error ? err : new Error("Failed to start stream");
|
||||
setError(streamError);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { environment } from "../environment";
|
||||
|
||||
export enum SessionKey {
|
||||
CHAT_SENT_INITIAL_PROMPTS = "chat_sent_initial_prompts",
|
||||
}
|
||||
|
||||
function get(key: SessionKey) {
|
||||
if (environment.isServerSide()) {
|
||||
Sentry.captureException(new Error("Session storage is not available"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return window.sessionStorage.getItem(key);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function set(key: SessionKey, value: string) {
|
||||
if (environment.isServerSide()) {
|
||||
Sentry.captureException(new Error("Session storage is not available"));
|
||||
return;
|
||||
}
|
||||
return window.sessionStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function clean(key: SessionKey) {
|
||||
if (environment.isServerSide()) {
|
||||
Sentry.captureException(new Error("Session storage is not available"));
|
||||
return;
|
||||
}
|
||||
return window.sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
export const sessionStorage = {
|
||||
clean,
|
||||
get,
|
||||
set,
|
||||
};
|
||||
Reference in New Issue
Block a user