mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
update frontend
This commit is contained in:
@@ -182,13 +182,14 @@ export function parseToolResponse(
|
||||
if (parsedResult && typeof parsedResult === "object") {
|
||||
const responseType = parsedResult.type as string | undefined;
|
||||
|
||||
// Handle no_results response
|
||||
// Handle no_results response - treat as a successful tool response
|
||||
if (responseType === "no_results") {
|
||||
return {
|
||||
type: "no_results",
|
||||
message: (parsedResult.message as string) || "No results found",
|
||||
suggestions: (parsedResult.suggestions as string[]) || [],
|
||||
sessionId: parsedResult.session_id as string | undefined,
|
||||
type: "tool_response",
|
||||
toolId,
|
||||
toolName,
|
||||
result: parsedResult.message || "No results found",
|
||||
success: true,
|
||||
timestamp: timestamp || new Date(),
|
||||
};
|
||||
}
|
||||
@@ -221,6 +222,25 @@ export function parseToolResponse(
|
||||
};
|
||||
}
|
||||
|
||||
// Handle need_login response
|
||||
if (responseType === "need_login") {
|
||||
return {
|
||||
type: "login_needed",
|
||||
message:
|
||||
(parsedResult.message as string) ||
|
||||
"Please sign in to use chat and agent features",
|
||||
sessionId: (parsedResult.session_id as string) || "",
|
||||
agentInfo: parsedResult.agent_info as
|
||||
| {
|
||||
graph_id: string;
|
||||
name: string;
|
||||
trigger_type: string;
|
||||
}
|
||||
| undefined,
|
||||
timestamp: timestamp || new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle setup_requirements - return null so caller can handle it specially
|
||||
if (responseType === "setup_requirements") {
|
||||
return null;
|
||||
|
||||
@@ -21,7 +21,7 @@ interface UseChatContainerResult {
|
||||
streamingChunks: string[];
|
||||
isStreaming: boolean;
|
||||
error: Error | null;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
sendMessage: (content: string, isUserMessage?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useChatContainer({
|
||||
@@ -139,13 +139,13 @@ export function useChatContainer({
|
||||
* - On stream_end, local messages cleared and replaced by refreshed initialMessages
|
||||
*/
|
||||
const sendMessage = useCallback(
|
||||
async function sendMessage(content: string) {
|
||||
async function sendMessage(content: string, isUserMessage: boolean = true) {
|
||||
if (!sessionId) {
|
||||
console.error("Cannot send message: no session ID");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message immediately
|
||||
// Add user message immediately (only if it's a user message)
|
||||
const userMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "user",
|
||||
@@ -155,13 +155,24 @@ export function useChatContainer({
|
||||
|
||||
// Remove any pending credentials_needed or login_needed messages when user sends a new message
|
||||
// This prevents them from persisting after the user has taken action
|
||||
setMessages((prev) => {
|
||||
const filtered = prev.filter(
|
||||
(msg) =>
|
||||
msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
// Only add user message to UI if isUserMessage is true
|
||||
if (isUserMessage) {
|
||||
setMessages((prev) => {
|
||||
const filtered = prev.filter(
|
||||
(msg) =>
|
||||
msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
);
|
||||
return [...filtered, userMessage];
|
||||
});
|
||||
} else {
|
||||
// For system messages, just remove the login/credentials prompts
|
||||
setMessages((prev) =>
|
||||
prev.filter(
|
||||
(msg) =>
|
||||
msg.type !== "credentials_needed" && msg.type !== "login_needed",
|
||||
),
|
||||
);
|
||||
return [...filtered, userMessage];
|
||||
});
|
||||
}
|
||||
|
||||
// Clear streaming chunks and reset text flag
|
||||
setStreamingChunks([]);
|
||||
@@ -182,8 +193,21 @@ export function useChatContainer({
|
||||
return updated;
|
||||
});
|
||||
} else if (chunk.type === "text_ended") {
|
||||
// Close the streaming text box
|
||||
console.log("[Text Ended] Closing streaming text box");
|
||||
// Save the completed text as an assistant message before clearing
|
||||
console.log("[Text Ended] Saving streamed text as assistant message");
|
||||
const completedText = streamingChunksRef.current.join("");
|
||||
|
||||
if (completedText.trim()) {
|
||||
const assistantMessage: ChatMessageData = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: completedText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
}
|
||||
|
||||
// Clear streaming state
|
||||
setStreamingChunks([]);
|
||||
streamingChunksRef.current = [];
|
||||
setHasTextChunks(false);
|
||||
@@ -211,11 +235,32 @@ export function useChatContainer({
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Find the matching tool_call to get the tool name if missing
|
||||
let toolName = chunk.tool_name || "unknown";
|
||||
if (!chunk.tool_name || chunk.tool_name === "unknown") {
|
||||
setMessages((prev) => {
|
||||
const matchingToolCall = [...prev]
|
||||
.reverse()
|
||||
.find(
|
||||
(msg) =>
|
||||
msg.type === "tool_call" &&
|
||||
msg.toolId === chunk.tool_id,
|
||||
);
|
||||
if (
|
||||
matchingToolCall &&
|
||||
matchingToolCall.type === "tool_call"
|
||||
) {
|
||||
toolName = matchingToolCall.toolName;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Use helper function to parse tool response
|
||||
const responseMessage = parseToolResponse(
|
||||
chunk.result!,
|
||||
chunk.tool_id!,
|
||||
chunk.tool_name!,
|
||||
toolName,
|
||||
new Date(),
|
||||
);
|
||||
|
||||
@@ -277,12 +322,18 @@ export function useChatContainer({
|
||||
);
|
||||
return [...prev, responseMessage];
|
||||
});
|
||||
} else if (chunk.type === "login_needed") {
|
||||
} else if (
|
||||
chunk.type === "login_needed" ||
|
||||
chunk.type === "need_login"
|
||||
) {
|
||||
// Add login needed message
|
||||
const loginNeededMessage: ChatMessageData = {
|
||||
type: "login_needed",
|
||||
message: chunk.message || "Authentication required to continue",
|
||||
message:
|
||||
chunk.message ||
|
||||
"Please sign in to use chat and agent features",
|
||||
sessionId: chunk.session_id || sessionId,
|
||||
agentInfo: chunk.agent_info,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setMessages((prev) => [...prev, loginNeededMessage]);
|
||||
@@ -351,6 +402,7 @@ export function useChatContainer({
|
||||
}
|
||||
// TODO: Handle usage for display
|
||||
},
|
||||
isUserMessage,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to send message:", err);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Card } from "@/components/atoms/Card/Card";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Key, Check, Warning } from "@phosphor-icons/react";
|
||||
@@ -50,10 +50,22 @@ export function ChatCredentialsSetup({
|
||||
const { selectedCredentials, isAllComplete, handleCredentialSelect } =
|
||||
useChatCredentialsSetup(credentials);
|
||||
|
||||
// Track if we've already called completion to prevent double calls
|
||||
const hasCalledCompleteRef = useRef(false);
|
||||
|
||||
// Reset the completion flag when credentials change (new credential setup flow)
|
||||
useEffect(
|
||||
function resetCompletionFlag() {
|
||||
hasCalledCompleteRef.current = false;
|
||||
},
|
||||
[credentials],
|
||||
);
|
||||
|
||||
// Auto-call completion when all credentials are configured
|
||||
useEffect(
|
||||
function autoCompleteWhenReady() {
|
||||
if (isAllComplete) {
|
||||
if (isAllComplete && !hasCalledCompleteRef.current) {
|
||||
hasCalledCompleteRef.current = true;
|
||||
onAllCredentialsComplete();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useState, useRef } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { useChatSession } from "@/hooks/useChatSession";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useChatStream } from "@/hooks/useChatStream";
|
||||
|
||||
interface UseChatPageResult {
|
||||
session: ReturnType<typeof useChatSession>["session"];
|
||||
@@ -18,9 +20,14 @@ interface UseChatPageResult {
|
||||
export function useChatPage(): UseChatPageResult {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const urlSessionId = searchParams.get("session");
|
||||
// Support both 'session' and 'session_id' query parameters
|
||||
const urlSessionId =
|
||||
searchParams.get("session_id") || searchParams.get("session");
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
const hasCreatedSessionRef = useRef(false);
|
||||
const hasClaimedSessionRef = useRef(false);
|
||||
const { user } = useSupabase();
|
||||
const { sendMessage: sendStreamMessage } = useChatStream();
|
||||
|
||||
const {
|
||||
session,
|
||||
@@ -31,6 +38,7 @@ export function useChatPage(): UseChatPageResult {
|
||||
error,
|
||||
createSession,
|
||||
refreshSession,
|
||||
claimSession,
|
||||
clearSession: clearSessionBase,
|
||||
} = useChatSession({
|
||||
urlSessionId,
|
||||
@@ -68,9 +76,54 @@ export function useChatPage(): UseChatPageResult {
|
||||
[urlSessionId, isCreating, sessionIdFromHook, createSession],
|
||||
);
|
||||
|
||||
// Note: Session claiming is handled explicitly by UI components when needed
|
||||
// - Locally created sessions: backend sets user_id from JWT automatically
|
||||
// - URL sessions: claiming happens in specific user flows, not automatically
|
||||
// Auto-claim session if user is logged in and session has no user_id
|
||||
useEffect(
|
||||
function autoClaimSession() {
|
||||
// Only claim if:
|
||||
// 1. We have a session loaded
|
||||
// 2. Session has no user_id (anonymous session)
|
||||
// 3. User is logged in
|
||||
// 4. Haven't already claimed this session
|
||||
// 5. Not currently loading
|
||||
if (
|
||||
session &&
|
||||
!session.user_id &&
|
||||
user &&
|
||||
!hasClaimedSessionRef.current &&
|
||||
!isLoading &&
|
||||
sessionIdFromHook
|
||||
) {
|
||||
console.log("[autoClaimSession] Claiming anonymous session for user");
|
||||
hasClaimedSessionRef.current = true;
|
||||
claimSession(sessionIdFromHook)
|
||||
.then(() => {
|
||||
console.log(
|
||||
"[autoClaimSession] Session claimed successfully, sending login notification",
|
||||
);
|
||||
// Send login notification message to backend after successful claim
|
||||
// This notifies the agent that the user has logged in
|
||||
sendStreamMessage(
|
||||
sessionIdFromHook,
|
||||
"User has successfully logged in.",
|
||||
() => {
|
||||
// Empty chunk handler - we don't need to process responses for this system message
|
||||
},
|
||||
false, // isUserMessage = false
|
||||
).catch((err) => {
|
||||
console.error(
|
||||
"[autoClaimSession] Failed to send login notification:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("[autoClaimSession] Failed to claim session:", err);
|
||||
hasClaimedSessionRef.current = false; // Reset on error to allow retry
|
||||
});
|
||||
}
|
||||
},
|
||||
[session, user, isLoading, sessionIdFromHook, claimSession, sendStreamMessage],
|
||||
);
|
||||
|
||||
// Monitor online/offline status
|
||||
useEffect(function monitorNetworkStatus() {
|
||||
@@ -102,7 +155,10 @@ export function useChatPage(): UseChatPageResult {
|
||||
|
||||
function clearSession() {
|
||||
clearSessionBase();
|
||||
// Remove session from URL
|
||||
// Reset the created session flag so a new session can be created
|
||||
hasCreatedSessionRef.current = false;
|
||||
hasClaimedSessionRef.current = false;
|
||||
// Remove session from URL and trigger new session creation
|
||||
router.push("/chat");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { environment } from "@/services/environment";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
@@ -14,6 +14,8 @@ export function useLoginPage() {
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [captchaKey, setCaptchaKey] = useState(0);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const returnUrl = searchParams.get("returnUrl");
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
@@ -140,8 +142,11 @@ export function useLoginPage() {
|
||||
setIsLoading(false);
|
||||
setFeedback(null);
|
||||
|
||||
const next =
|
||||
(result?.next as string) || (result?.onboarding ? "/onboarding" : "/");
|
||||
// Prioritize returnUrl from query params over backend's onboarding logic
|
||||
const next = returnUrl
|
||||
? returnUrl
|
||||
: (result?.next as string) ||
|
||||
(result?.onboarding ? "/onboarding" : "/");
|
||||
if (next) router.push(next);
|
||||
} catch (error) {
|
||||
toast({
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function GET(
|
||||
const { sessionId } = await params;
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const message = searchParams.get("message");
|
||||
const isUserMessage = searchParams.get("is_user_message");
|
||||
|
||||
if (!message) {
|
||||
return new Response("Missing message parameter", { status: 400 });
|
||||
@@ -31,6 +32,11 @@ export async function GET(
|
||||
);
|
||||
streamUrl.searchParams.set("message", message);
|
||||
|
||||
// Pass is_user_message parameter if provided
|
||||
if (isUserMessage !== null) {
|
||||
streamUrl.searchParams.set("is_user_message", isUserMessage);
|
||||
}
|
||||
|
||||
// Forward request to backend with auth header
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "text/event-stream",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { SignIn, UserPlus, Shield } from "@phosphor-icons/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface AuthPromptWidgetProps {
|
||||
message: string;
|
||||
sessionId: string;
|
||||
agentInfo?: {
|
||||
graph_id: string;
|
||||
name: string;
|
||||
trigger_type: string;
|
||||
};
|
||||
returnUrl?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AuthPromptWidget({
|
||||
message,
|
||||
sessionId,
|
||||
agentInfo,
|
||||
returnUrl = "/chat",
|
||||
className,
|
||||
}: AuthPromptWidgetProps) {
|
||||
const router = useRouter();
|
||||
|
||||
function handleSignIn() {
|
||||
// Store session info to return after auth
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("pending_chat_session", sessionId);
|
||||
if (agentInfo) {
|
||||
localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
|
||||
}
|
||||
}
|
||||
|
||||
// Build return URL with session ID (using session_id to match chat page parameter)
|
||||
const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
|
||||
const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
|
||||
router.push(`/login?returnUrl=${encodedReturnUrl}`);
|
||||
}
|
||||
|
||||
function handleSignUp() {
|
||||
// Store session info to return after auth
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("pending_chat_session", sessionId);
|
||||
if (agentInfo) {
|
||||
localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
|
||||
}
|
||||
}
|
||||
|
||||
// Build return URL with session ID (using session_id to match chat page parameter)
|
||||
const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
|
||||
const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
|
||||
router.push(`/signup?returnUrl=${encodedReturnUrl}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"my-4 overflow-hidden rounded-lg border border-violet-200 dark:border-violet-800",
|
||||
"bg-gradient-to-br from-violet-50 to-purple-50 dark:from-violet-950/30 dark:to-purple-950/30",
|
||||
"duration-500 animate-in fade-in-50 slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="px-6 py-5">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-violet-600">
|
||||
<Shield size={20} weight="fill" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Authentication Required
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Sign in to set up and manage agents
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-5 rounded-md bg-white/50 p-4 dark:bg-neutral-900/50">
|
||||
<p className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{message}
|
||||
</p>
|
||||
{agentInfo && (
|
||||
<div className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
|
||||
<p>
|
||||
Ready to set up:{" "}
|
||||
<span className="font-medium">{agentInfo.name}</span>
|
||||
</p>
|
||||
<p>
|
||||
Type:{" "}
|
||||
<span className="font-medium">{agentInfo.trigger_type}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleSignIn}
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="flex-1"
|
||||
>
|
||||
<SignIn size={16} weight="bold" className="mr-2" />
|
||||
Sign In
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSignUp}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="flex-1"
|
||||
>
|
||||
<UserPlus size={16} weight="bold" className="mr-2" />
|
||||
Create Account
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center text-xs text-neutral-500 dark:text-neutral-500">
|
||||
Your chat session will be preserved after signing in
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { AuthPromptWidget } from "./AuthPromptWidget";
|
||||
export type { AuthPromptWidgetProps } from "./AuthPromptWidget";
|
||||
@@ -1,15 +1,17 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Robot, User } from "@phosphor-icons/react";
|
||||
import { Robot, User, CheckCircle } from "@phosphor-icons/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { MessageBubble } from "@/components/atoms/MessageBubble/MessageBubble";
|
||||
import { MarkdownContent } from "@/components/atoms/MarkdownContent/MarkdownContent";
|
||||
import { ToolCallMessage } from "@/components/molecules/ToolCallMessage/ToolCallMessage";
|
||||
import { ToolResponseMessage } from "@/components/molecules/ToolResponseMessage/ToolResponseMessage";
|
||||
import { LoginPrompt } from "@/components/molecules/LoginPrompt/LoginPrompt";
|
||||
import { AuthPromptWidget } from "@/components/molecules/AuthPromptWidget/AuthPromptWidget";
|
||||
import { ChatCredentialsSetup } from "@/app/(platform)/chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
|
||||
import { NoResultsMessage } from "@/components/molecules/NoResultsMessage/NoResultsMessage";
|
||||
import { AgentCarouselMessage } from "@/components/molecules/AgentCarouselMessage/AgentCarouselMessage";
|
||||
import { ExecutionStartedMessage } from "@/components/molecules/ExecutionStartedMessage/ExecutionStartedMessage";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useChatMessage, type ChatMessageData } from "./useChatMessage";
|
||||
|
||||
export interface ChatMessageProps {
|
||||
@@ -17,7 +19,7 @@ export interface ChatMessageProps {
|
||||
className?: string;
|
||||
onDismissLogin?: () => void;
|
||||
onDismissCredentials?: () => void;
|
||||
onSendMessage?: (content: string) => void;
|
||||
onSendMessage?: (content: string, isUserMessage?: boolean) => void;
|
||||
}
|
||||
|
||||
export function ChatMessage({
|
||||
@@ -28,6 +30,7 @@ export function ChatMessage({
|
||||
onSendMessage,
|
||||
}: ChatMessageProps) {
|
||||
const router = useRouter();
|
||||
const { user } = useSupabase();
|
||||
const {
|
||||
formattedTimestamp,
|
||||
isUser,
|
||||
@@ -55,19 +58,22 @@ export function ChatMessage({
|
||||
}
|
||||
}
|
||||
|
||||
function handleAllCredentialsComplete() {
|
||||
// Send a user message that explicitly asks to retry the setup
|
||||
// This ensures the LLM calls get_required_setup_info again and proceeds with execution
|
||||
if (onSendMessage) {
|
||||
onSendMessage(
|
||||
"I've configured the required credentials. Please check if everything is ready and proceed with setting up the agent.",
|
||||
);
|
||||
}
|
||||
// Optionally dismiss the credentials prompt
|
||||
if (onDismissCredentials) {
|
||||
onDismissCredentials();
|
||||
}
|
||||
}
|
||||
const handleAllCredentialsComplete = useCallback(
|
||||
function handleAllCredentialsComplete() {
|
||||
// Send a user message that explicitly asks to retry the setup
|
||||
// This ensures the LLM calls get_required_setup_info again and proceeds with execution
|
||||
if (onSendMessage) {
|
||||
onSendMessage(
|
||||
"I've configured the required credentials. Please check if everything is ready and proceed with setting up the agent.",
|
||||
);
|
||||
}
|
||||
// Optionally dismiss the credentials prompt
|
||||
if (onDismissCredentials) {
|
||||
onDismissCredentials();
|
||||
}
|
||||
},
|
||||
[onSendMessage, onDismissCredentials],
|
||||
);
|
||||
|
||||
function handleCancelCredentials() {
|
||||
// Dismiss the credentials prompt
|
||||
@@ -92,13 +98,41 @@ export function ChatMessage({
|
||||
|
||||
// Render login needed messages
|
||||
if (isLoginNeeded && message.type === "login_needed") {
|
||||
// If user is already logged in, show success message instead of auth prompt
|
||||
if (user) {
|
||||
return (
|
||||
<div className={cn("px-4 py-2", className)}>
|
||||
<div className="my-4 overflow-hidden rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-emerald-50 dark:border-green-800 dark:from-green-950/30 dark:to-emerald-950/30">
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-600">
|
||||
<CheckCircle size={20} weight="fill" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Successfully Authenticated
|
||||
</h3>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
You're now signed in and ready to continue
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show auth prompt if not logged in
|
||||
return (
|
||||
<LoginPrompt
|
||||
message={message.message}
|
||||
onLogin={handleLogin}
|
||||
onContinueAsGuest={handleContinueAsGuest}
|
||||
className={className}
|
||||
/>
|
||||
<div className={cn("px-4 py-2", className)}>
|
||||
<AuthPromptWidget
|
||||
message={message.message}
|
||||
sessionId={message.sessionId}
|
||||
agentInfo={message.agentInfo}
|
||||
returnUrl="/chat"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ export type ChatMessageData =
|
||||
type: "login_needed";
|
||||
message: string;
|
||||
sessionId: string;
|
||||
agentInfo?: {
|
||||
graph_id: string;
|
||||
name: string;
|
||||
trigger_type: string;
|
||||
};
|
||||
timestamp?: string | Date;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { LoginPrompt } from "./LoginPrompt";
|
||||
|
||||
const meta = {
|
||||
title: "Molecules/LoginPrompt",
|
||||
component: LoginPrompt,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
onLogin: () => console.log("Login clicked"),
|
||||
onContinueAsGuest: () => console.log("Continue as guest clicked"),
|
||||
},
|
||||
} satisfies Meta<typeof LoginPrompt>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
message: "Please log in to save your chat history and access your account",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomMessage: Story = {
|
||||
args: {
|
||||
message:
|
||||
"To continue with this agent and save your progress, please sign in to your account",
|
||||
},
|
||||
};
|
||||
|
||||
export const ShortMessage: Story = {
|
||||
args: {
|
||||
message: "Sign in to continue",
|
||||
},
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from "react";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { SignIn, UserCircle } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface LoginPromptProps {
|
||||
message: string;
|
||||
onLogin: () => void;
|
||||
onContinueAsGuest: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoginPrompt({
|
||||
message,
|
||||
onLogin,
|
||||
onContinueAsGuest,
|
||||
className,
|
||||
}: LoginPromptProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-4 my-2 flex flex-col items-center gap-4 rounded-lg border border-blue-200 bg-blue-50 p-6 dark:border-blue-900 dark:bg-blue-950",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-500">
|
||||
<UserCircle size={32} weight="fill" className="text-white" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="text-center">
|
||||
<Text variant="h3" className="mb-2 text-blue-900 dark:text-blue-100">
|
||||
Login Required
|
||||
</Text>
|
||||
<Text variant="body" className="text-blue-700 dark:text-blue-300">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={onLogin}
|
||||
variant="primary"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SignIn size={20} weight="bold" />
|
||||
Login
|
||||
</Button>
|
||||
<Button onClick={onContinueAsGuest} variant="secondary">
|
||||
Continue as Guest
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Text variant="small" className="text-blue-600 dark:text-blue-400">
|
||||
Logging in will save your chat history to your account
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -208,26 +208,6 @@ export const WithToolResponses: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLoginPrompt: Story = {
|
||||
args: {
|
||||
messages: [
|
||||
{
|
||||
type: "message" as const,
|
||||
role: "user" as const,
|
||||
content: "Run this agent for me",
|
||||
timestamp: new Date(Date.now() - 3 * 60 * 1000),
|
||||
},
|
||||
{
|
||||
type: "login_needed" as const,
|
||||
message:
|
||||
"To run agents and save your chat history, please log in to your account.",
|
||||
sessionId: "session-123",
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 1000),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCredentialsPrompt: Story = {
|
||||
args: {
|
||||
messages: [
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface StreamChunk {
|
||||
| "tool_call_start"
|
||||
| "tool_response"
|
||||
| "login_needed"
|
||||
| "need_login"
|
||||
| "credentials_needed"
|
||||
| "error"
|
||||
| "usage"
|
||||
@@ -29,6 +30,11 @@ export interface StreamChunk {
|
||||
idx?: number; // Index for tool_call_start
|
||||
// Login needed fields
|
||||
session_id?: string;
|
||||
agent_info?: {
|
||||
graph_id: string;
|
||||
name: string;
|
||||
trigger_type: string;
|
||||
};
|
||||
// Credentials needed fields
|
||||
provider?: string;
|
||||
provider_name?: string;
|
||||
@@ -45,6 +51,7 @@ interface UseChatStreamResult {
|
||||
sessionId: string,
|
||||
message: string,
|
||||
onChunk: (chunk: StreamChunk) => void,
|
||||
isUserMessage?: boolean,
|
||||
) => Promise<void>;
|
||||
stopStreaming: () => void;
|
||||
}
|
||||
@@ -88,6 +95,7 @@ export function useChatStream(): UseChatStreamResult {
|
||||
sessionId: string,
|
||||
message: string,
|
||||
onChunk: (chunk: StreamChunk) => void,
|
||||
isUserMessage: boolean = true,
|
||||
) {
|
||||
// Stop any existing stream
|
||||
stopStreaming();
|
||||
@@ -111,7 +119,7 @@ export function useChatStream(): UseChatStreamResult {
|
||||
// EventSource doesn't support custom headers, so we use a Next.js API route
|
||||
// that acts as a proxy and adds authentication headers server-side
|
||||
// This matches the pattern from PR #10905 where SSE went through the same server
|
||||
const url = `/api/chat/sessions/${sessionId}/stream?message=${encodeURIComponent(message)}`;
|
||||
const url = `/api/chat/sessions/${sessionId}/stream?message=${encodeURIComponent(message)}&is_user_message=${isUserMessage}`;
|
||||
|
||||
// Create EventSource for SSE (connects to our Next.js proxy)
|
||||
const eventSource = new EventSource(url);
|
||||
@@ -171,7 +179,7 @@ export function useChatStream(): UseChatStreamResult {
|
||||
|
||||
retryTimeoutRef.current = setTimeout(() => {
|
||||
// Retry by recursively calling sendMessage
|
||||
sendMessage(sessionId, message, onChunk).catch((err) => {
|
||||
sendMessage(sessionId, message, onChunk, isUserMessage).catch((err) => {
|
||||
console.error("Retry failed:", err);
|
||||
});
|
||||
}, retryDelay);
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
|
||||
export class ChatPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async goto(sessionId?: string) {
|
||||
await this.page.goto(sessionId ? `/chat?session=${sessionId}` : "/chat");
|
||||
}
|
||||
|
||||
// Selectors
|
||||
getChatInput(): Locator {
|
||||
return this.page.locator('textarea[placeholder*="Type a message"]');
|
||||
}
|
||||
|
||||
getSendButton(): Locator {
|
||||
return this.page.getByRole("button", { name: /send/i });
|
||||
}
|
||||
|
||||
getMessages(): Locator {
|
||||
return this.page.locator('[data-testid="chat-message"]');
|
||||
}
|
||||
|
||||
getMessageByIndex(index: number): Locator {
|
||||
return this.getMessages().nth(index);
|
||||
}
|
||||
|
||||
getStreamingMessage(): Locator {
|
||||
return this.page.locator('[data-testid="streaming-message"]');
|
||||
}
|
||||
|
||||
getNewChatButton(): Locator {
|
||||
return this.page.getByRole("button", { name: /new chat/i });
|
||||
}
|
||||
|
||||
getQuickActionButton(text: string): Locator {
|
||||
return this.page.getByRole("button", { name: new RegExp(text, "i") });
|
||||
}
|
||||
|
||||
// Tool-specific message selectors
|
||||
getToolCallMessage(): Locator {
|
||||
return this.page.locator('[data-testid="tool-call-message"]').first();
|
||||
}
|
||||
|
||||
getToolResponseMessage(): Locator {
|
||||
return this.page.locator('[data-testid="tool-response-message"]').first();
|
||||
}
|
||||
|
||||
getLoginPrompt(): Locator {
|
||||
return this.page.getByText("Login Required").first();
|
||||
}
|
||||
|
||||
getCredentialsPrompt(): Locator {
|
||||
return this.page.getByText("Credentials Required").first();
|
||||
}
|
||||
|
||||
getNoResultsMessage(): Locator {
|
||||
return this.page.getByText("No Results Found").first();
|
||||
}
|
||||
|
||||
getAgentCarouselMessage(): Locator {
|
||||
return this.page.getByText(/Found \d+ Agents?/).first();
|
||||
}
|
||||
|
||||
getExecutionStartedMessage(): Locator {
|
||||
return this.page.getByText("Execution Started").first();
|
||||
}
|
||||
|
||||
// Actions
|
||||
async sendMessage(text: string): Promise<void> {
|
||||
const input = this.getChatInput();
|
||||
await input.waitFor({ state: "visible" });
|
||||
await input.fill(text);
|
||||
|
||||
const sendButton = this.getSendButton();
|
||||
await sendButton.waitFor({ state: "visible" });
|
||||
await sendButton.click();
|
||||
}
|
||||
|
||||
async clickQuickAction(text: string): Promise<void> {
|
||||
const button = this.getQuickActionButton(text);
|
||||
await button.waitFor({ state: "visible" });
|
||||
await button.click();
|
||||
}
|
||||
|
||||
async startNewChat(): Promise<void> {
|
||||
const button = this.getNewChatButton();
|
||||
await button.waitFor({ state: "visible" });
|
||||
await button.click();
|
||||
}
|
||||
|
||||
async waitForResponse(): Promise<void> {
|
||||
// Wait for a new assistant message to appear
|
||||
await this.page.waitForSelector('[data-testid="chat-message"]', {
|
||||
state: "visible",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForStreaming(): Promise<void> {
|
||||
await this.getStreamingMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForToolCall(): Promise<void> {
|
||||
await this.getToolCallMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForToolResponse(): Promise<void> {
|
||||
await this.getToolResponseMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForLoginPrompt(): Promise<void> {
|
||||
await this.getLoginPrompt().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForCredentialsPrompt(): Promise<void> {
|
||||
await this.getCredentialsPrompt().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForNoResults(): Promise<void> {
|
||||
await this.getNoResultsMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForAgentCarousel(): Promise<void> {
|
||||
await this.getAgentCarouselMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async waitForExecutionStarted(): Promise<void> {
|
||||
await this.getExecutionStartedMessage().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user