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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 95 additions & 94 deletions packages/mcp-cloudflare/src/client/components/chat/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef, useMemo } from "react";
import { useMemo } from "react";
import { Loader2, AlertCircle } from "lucide-react";
import { Button } from "../ui/button";
import { MessagePart } from ".";
Expand Down Expand Up @@ -61,108 +61,109 @@ function processMessages(
return allParts;
}

export const ChatMessages = forwardRef<HTMLDivElement, ChatMessagesProps>(
({ messages, isChatLoading, error, onRetry }, ref) => {
const { handleOAuthLogin } = useAuth();

const processedParts = useMemo(
() => processMessages(messages, isChatLoading),
[messages, isChatLoading],
);

// Simple error handling - just check if it's auth or not
const errorIsAuth = error ? isAuthError(error) : false;
const errorMessage = error ? getErrorMessage(error) : null;
return (
<div ref={ref} className="flex-1 mx-6 mt-6 space-y-4">
{/* Empty State when no messages */}
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full">
<div className="max-w-md w-full space-y-6">
<div className="text-slate-400 hidden [@media(min-height:500px)]:block">
<img
src="/flow-transparent.png"
alt="Flow"
width={1536}
height={1024}
className="w-full mb-6 bg-violet-300 rounded"
/>
</div>
export function ChatMessages({
messages,
isChatLoading,
error,
onRetry,
}: ChatMessagesProps) {
const { handleOAuthLogin } = useAuth();

const processedParts = useMemo(
() => processMessages(messages, isChatLoading),
[messages, isChatLoading],
);

// Simple error handling - just check if it's auth or not
const errorIsAuth = error ? isAuthError(error) : false;
const errorMessage = error ? getErrorMessage(error) : null;
return (
<div className="mx-6 mt-6 space-y-4 flex-1">
{/* Empty State when no messages */}
{messages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full">
<div className="max-w-md w-full space-y-6">
<div className="text-slate-400 hidden [@media(min-height:500px)]:block">
<img
src="/flow-transparent.png"
alt="Flow"
width={1536}
height={1024}
className="w-full mb-6 bg-violet-300 rounded"
/>
</div>

<div className="text-center text-slate-400">
<h2 id="chat-panel-title" className="text-lg mb-2">
Chat with your stack traces. Argue with confidence. Lose
gracefully.
</h2>
</div>
<div className="text-center text-slate-400">
<h2 id="chat-panel-title" className="text-lg mb-2">
Chat with your stack traces. Argue with confidence. Lose
gracefully.
</h2>
</div>
</div>
)}

{/* Show messages when we have any */}
{messages.length > 0 && (
<>
<h2 id="chat-panel-title" className="sr-only">
Chat Messages
</h2>
{processedParts.map((item) => (
<MessagePart
key={`${item.messageId}-part-${item.partIndex}`}
part={item.part}
messageId={item.messageId}
messageRole={item.messageRole}
partIndex={item.partIndex}
isStreaming={item.isStreaming}
/>
))}

{/* Show error or loading state */}
{error && errorMessage ? (
<div className="mr-8 p-4 bg-red-900/10 border border-red-500/30 rounded">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-red-400 mt-0.5" />
<div className="flex-1">
<p className="text-red-300">{errorMessage}</p>
{/* Simple action buttons */}
<div className="mt-3 flex gap-2">
{errorIsAuth ? (
</div>
)}

{/* Show messages when we have any */}
{messages.length > 0 && (
<>
<h2 id="chat-panel-title" className="sr-only">
Chat Messages
</h2>
{processedParts.map((item) => (
<MessagePart
key={`${item.messageId}-part-${item.partIndex}`}
part={item.part}
messageId={item.messageId}
messageRole={item.messageRole}
partIndex={item.partIndex}
isStreaming={item.isStreaming}
/>
))}

{/* Show error or loading state */}
{error && errorMessage ? (
<div className="mr-8 p-4 bg-red-900/10 border border-red-500/30 rounded">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-red-400 mt-0.5" />
<div className="flex-1">
<p className="text-red-300">{errorMessage}</p>
{/* Simple action buttons */}
<div className="mt-3 flex gap-2">
{errorIsAuth ? (
<Button
onClick={() => {
handleOAuthLogin();
}}
size="sm"
variant="secondary"
className="bg-red-900/20 hover:bg-red-900/30 text-red-300 border-red-500/30 cursor-pointer"
>
Reauthenticate
</Button>
) : (
onRetry && (
<Button
onClick={() => {
handleOAuthLogin();
}}
onClick={onRetry}
size="sm"
variant="secondary"
className="bg-red-900/20 hover:bg-red-900/30 text-red-300 border-red-500/30 cursor-pointer"
>
Reauthenticate
Try again
</Button>
) : (
onRetry && (
<Button
onClick={onRetry}
size="sm"
variant="secondary"
className="bg-red-900/20 hover:bg-red-900/30 text-red-300 border-red-500/30 cursor-pointer"
>
Try again
</Button>
)
)}
</div>
)
)}
</div>
</div>
</div>
) : isChatLoading ? (
<div className="flex items-center space-x-2 text-slate-400 mr-8">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Assistant is thinking...</span>
</div>
) : null}
</>
)}
</div>
);
},
);

ChatMessages.displayName = "ChatMessages";
</div>
) : isChatLoading ? (
<div className="flex items-center space-x-2 text-slate-400 mr-8">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Assistant is thinking...</span>
</div>
) : null}
</>
)}
</div>
);
}
86 changes: 40 additions & 46 deletions packages/mcp-cloudflare/src/client/components/chat/chat-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ import type { Message } from "ai/react";
// Constant empty function to avoid creating new instances on every render
const EMPTY_FUNCTION = () => {};

// Sample prompts for quick access
const SAMPLE_PROMPTS = [
{
label: "Get Organizations",
prompt: "What organizations do I have access to?",
},
{
label: "React SDK Usage",
prompt: "Show me how to set up the React SDK for error monitoring",
},
{
label: "Recent Issues",
prompt: "What are my most recent issues?",
},
] as const;

interface ChatUIProps {
messages: Message[];
input: string;
Expand Down Expand Up @@ -71,66 +87,44 @@ export const ChatUI = forwardRef<HTMLDivElement, ChatUIProps>(
)}
</div>

<div className="pb-34 overflow-y-auto">
{/* Chat Messages */}
{/* Chat Messages - Scrollable area */}
<div ref={ref} className="flex-1 overflow-y-auto mb-34 flex">
<ChatMessages
ref={ref}
messages={messages}
isChatLoading={isChatLoading}
error={error}
onRetry={onRetry}
/>
</div>

{/* Chat Input - Always pinned at bottom */}
<div className="py-4 px-6 bottom-0 left-0 right-0 absolute bg-slate-950/95 h-34 overflow-hidden">
{/* Sample Prompt Buttons - Always visible above input */}
{onSendPrompt && (
<div className="mb-4 flex flex-wrap gap-2 justify-center">
{/* Chat Input - Always pinned at bottom */}
<div className="py-4 px-6 bottom-0 left-0 right-0 absolute bg-slate-950/95 h-34 overflow-hidden">
{/* Sample Prompt Buttons - Always visible above input */}
{onSendPrompt && (
<div className="mb-4 flex flex-wrap gap-2 justify-center">
{SAMPLE_PROMPTS.map((samplePrompt) => (
<Button
key={samplePrompt.label}
type="button"
onClick={() =>
onSendPrompt("What organizations do I have access to?")
}
onClick={() => onSendPrompt(samplePrompt.prompt)}
size="sm"
variant="outline"
>
Get Organizations
{samplePrompt.label}
</Button>
<Button
type="button"
onClick={() =>
onSendPrompt(
"Show me how to set up the React SDK for error monitoring",
)
}
size="sm"
variant="outline"
>
React SDK Usage
</Button>
<Button
type="button"
onClick={() =>
onSendPrompt("What are my most recent issues?")
}
size="sm"
variant="outline"
>
Recent Issues
</Button>
</div>
)}
))}
</div>
)}

<ChatInput
input={input}
isLoading={isChatLoading}
isOpen={isOpen}
onInputChange={onInputChange}
onSubmit={onSubmit}
onStop={onStop || EMPTY_FUNCTION}
onSlashCommand={onSlashCommand}
/>
</div>
<ChatInput
input={input}
isLoading={isChatLoading}
isOpen={isOpen}
onInputChange={onInputChange}
onSubmit={onSubmit}
onStop={onStop || EMPTY_FUNCTION}
onSlashCommand={onSlashCommand}
/>
</div>
</div>
);
Expand Down
22 changes: 13 additions & 9 deletions packages/mcp-cloudflare/src/client/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use client";

import { useChat } from "@ai-sdk/react";
import { useEffect, useRef, useCallback } from "react";
import { Button } from "../ui/button";
import { useEffect, useRef, useCallback, useState } from "react";
import { AuthForm, ChatUI } from ".";
import { useAuth } from "../../contexts/auth-context";
import { X, Loader2 } from "lucide-react";
Expand Down Expand Up @@ -49,13 +48,10 @@ export function Chat({ isOpen, onClose, onLogout }: ChatProps) {
initialMessages,
});

// Use declarative scroll hook - scroll on new messages and during streaming
const { containerRef: messagesContainerRef } =
// Use the clean scroll hook with smart auto-scroll detection
const [messagesContainerRef, scrollToBottom, setAutoScroll] =
useScrollToBottom<HTMLDivElement>({
enabled: true,
smooth: true,
dependencies: [messages, status],
delay: status === "streaming" || status === "submitted" ? 100 : 0, // More frequent updates during loading
delay: 50, // Simple consistent delay
});

// Clear messages function - used locally for /clear command and logout
Expand Down Expand Up @@ -133,6 +129,14 @@ export function Chat({ isOpen, onClose, onLogout }: ChatProps) {
[append],
);

// Wrap form submission to ensure scrolling
const handleFormSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
handleSubmit(e);
},
[handleSubmit],
);

// Handle slash commands
const handleSlashCommand = (command: string) => {
// Always clear the input first for all commands
Expand Down Expand Up @@ -223,7 +227,7 @@ export function Chat({ isOpen, onClose, onLogout }: ChatProps) {
isOpen={isOpen}
showControls
onInputChange={handleInputChange}
onSubmit={handleSubmit}
onSubmit={handleFormSubmit}
onStop={stop}
onRetry={reload}
onClose={onClose}
Expand Down
Loading