chore: wip

This commit is contained in:
Lluis Agusti
2025-12-16 15:51:10 +01:00
parent d726db6488
commit 17cef05b8b
23 changed files with 289 additions and 308 deletions

View File

@@ -141,6 +141,15 @@
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;

View File

@@ -92,7 +92,7 @@ export function Input({
className={cn(
baseStyles,
errorStyles,
"-mb-1 h-auto min-h-[2.875rem] rounded-medium",
"-mb-1 h-auto min-h-[2.875rem] rounded-full",
// Size variants for textarea
size === "small" && [
"min-h-[2.25rem]", // 36px minimum
@@ -107,6 +107,11 @@ export function Input({
)}
placeholder={placeholder || label}
onChange={handleTextareaChange}
onKeyDown={
props.onKeyDown as
| React.KeyboardEventHandler<HTMLTextAreaElement>
| undefined
}
rows={props.rows || 3}
{...(hideLabel ? { "aria-label": label } : {})}
id={props.id}

View File

@@ -13,7 +13,11 @@ import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState
import { useChat } from "./useChat";
import { useChatDrawer } from "./useChatDrawer";
export function ChatDrawer() {
interface ChatDrawerProps {
blurBackground?: boolean;
}
export function ChatDrawer({ blurBackground = true }: ChatDrawerProps) {
const isChatEnabled = useGetFlag(Flag.CHAT);
const { isOpen, close } = useChatDrawer();
const {
@@ -49,14 +53,23 @@ export function ChatDrawer() {
modal={false}
>
<Drawer.Portal>
{blurBackground && isOpen && (
<div
onClick={close}
className="fixed inset-0 z-[45] cursor-pointer bg-black/10 backdrop-blur-sm animate-in fade-in-0"
style={{ pointerEvents: "auto" }}
/>
)}
<Drawer.Content
onClick={(e) => e.stopPropagation()}
onInteractOutside={blurBackground ? close : undefined}
className={cn(
"fixed right-0 top-0 z-50 flex h-full w-1/2 flex-col border-l border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900",
"fixed right-0 top-0 z-50 flex h-full w-1/2 flex-col border-l border-zinc-200 bg-white",
scrollbarStyles,
)}
>
{/* Header */}
<header className="shrink-0 border-b border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900">
<header className="shrink-0 border-b border-zinc-200 bg-white p-4">
<div className="flex items-center justify-between">
<Drawer.Title className="text-xl font-semibold">
Chat
@@ -64,12 +77,12 @@ export function ChatDrawer() {
<div className="flex items-center gap-4">
{sessionId && (
<>
<span className="text-sm text-zinc-600 dark:text-zinc-400">
<span className="text-sm text-zinc-600">
Session: {sessionId.slice(0, 8)}...
</span>
<button
onClick={clearSession}
className="text-sm text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
className="text-sm text-zinc-600 hover:text-zinc-900"
>
New Chat
</button>

View File

@@ -1,9 +1,8 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import { Card } from "@/components/atoms/Card/Card";
import { List, Robot, ArrowRight } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { ArrowRight, List, Robot } from "@phosphor-icons/react";
export interface Agent {
id: string;
@@ -30,7 +29,7 @@ export function AgentCarouselMessage({
return (
<div
className={cn(
"mx-4 my-2 flex flex-col gap-4 rounded-lg border border-purple-200 bg-purple-50 p-6 dark:border-purple-900 dark:bg-purple-950",
"mx-4 my-2 flex flex-col gap-4 rounded-lg border border-purple-200 bg-purple-50 p-6",
className,
)}
>
@@ -40,13 +39,10 @@ export function AgentCarouselMessage({
<List size={24} weight="bold" className="text-white" />
</div>
<div>
<Text variant="h3" className="text-purple-900 dark:text-purple-100">
<Text variant="h3" className="text-purple-900">
Found {displayCount} {displayCount === 1 ? "Agent" : "Agents"}
</Text>
<Text
variant="small"
className="text-purple-700 dark:text-purple-300"
>
<Text variant="small" className="text-purple-700">
Select an agent to view details or run it
</Text>
</div>
@@ -57,40 +53,34 @@ export function AgentCarouselMessage({
{agents.map((agent) => (
<Card
key={agent.id}
className="border border-purple-200 bg-white p-4 dark:border-purple-800 dark:bg-purple-900"
className="border border-purple-200 bg-white p-4"
>
<div className="flex gap-3">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-800">
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100">
<Robot size={20} weight="bold" className="text-purple-600" />
</div>
<div className="flex-1 space-y-2">
<div>
<Text
variant="body"
className="font-semibold text-purple-900 dark:text-purple-100"
className="font-semibold text-purple-900"
>
{agent.name}
</Text>
{agent.version && (
<Text
variant="small"
className="text-purple-600 dark:text-purple-400"
>
<Text variant="small" className="text-purple-600">
v{agent.version}
</Text>
)}
</div>
<Text
variant="small"
className="line-clamp-2 text-purple-700 dark:text-purple-300"
>
<Text variant="small" className="line-clamp-2 text-purple-700">
{agent.description}
</Text>
{onSelectAgent && (
<Button
onClick={() => onSelectAgent(agent.id)}
variant="ghost"
className="mt-2 flex items-center gap-1 p-0 text-sm text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200"
className="mt-2 flex items-center gap-1 p-0 text-sm text-purple-600 hover:text-purple-800"
>
View details
<ArrowRight size={16} weight="bold" />
@@ -103,10 +93,7 @@ export function AgentCarouselMessage({
</div>
{totalCount && totalCount > agents.length && (
<Text
variant="small"
className="text-center text-purple-600 dark:text-purple-400"
>
<Text variant="small" className="text-center text-purple-600">
Showing {agents.length} of {totalCount} results
</Text>
)}

View File

@@ -1,10 +1,9 @@
"use client";
import React from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/atoms/Button/Button";
import { SignInIcon, UserPlusIcon, ShieldIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { ShieldIcon, SignInIcon, UserPlusIcon } from "@phosphor-icons/react";
import { useRouter } from "next/navigation";
export interface AuthPromptWidgetProps {
message: string;
@@ -54,8 +53,8 @@ export function AuthPromptWidget({
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",
"my-4 overflow-hidden rounded-lg border border-violet-200",
"bg-gradient-to-br from-violet-50 to-purple-50",
"duration-500 animate-in fade-in-50 slide-in-from-bottom-2",
className,
)}
@@ -66,21 +65,19 @@ export function AuthPromptWidget({
<ShieldIcon size={20} weight="fill" className="text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
<h3 className="text-lg font-semibold text-neutral-900">
Authentication Required
</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
<p className="text-sm text-neutral-600">
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>
<div className="mb-5 rounded-md bg-white/50 p-4">
<p className="text-sm text-neutral-700">{message}</p>
{agentInfo && (
<div className="mt-3 text-xs text-neutral-600 dark:text-neutral-400">
<div className="mt-3 text-xs text-neutral-600">
<p>
Ready to set up:{" "}
<span className="font-medium">{agentInfo.name}</span>
@@ -114,7 +111,7 @@ export function AuthPromptWidget({
</Button>
</div>
<div className="mt-4 text-center text-xs text-neutral-500 dark:text-neutral-500">
<div className="mt-4 text-center text-xs text-neutral-500">
Your chat session will be preserved after signing in
</div>
</div>

View File

@@ -54,7 +54,7 @@ export function ChatContainer({
)}
{/* Input - Always visible */}
<div className="border-t border-zinc-200 p-4 dark:border-zinc-800">
<div className="border-t border-zinc-200 p-4">
<ChatInput
onSend={sendMessage}
disabled={isStreaming || !sessionId}

View File

@@ -8,6 +8,7 @@ export interface HandlerDependencies {
setStreamingChunks: Dispatch<SetStateAction<string[]>>;
streamingChunksRef: MutableRefObject<string[]>;
setMessages: Dispatch<SetStateAction<ChatMessageData[]>>;
setIsStreamingInitiated: Dispatch<SetStateAction<boolean>>;
sessionId: string;
}
@@ -39,6 +40,7 @@ export function handleTextEnded(
deps.setStreamingChunks([]);
deps.streamingChunksRef.current = [];
deps.setHasTextChunks(false);
deps.setIsStreamingInitiated(false);
}
export function handleToolCallStart(
@@ -197,10 +199,15 @@ export function handleStreamEnd(
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) {
export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
const errorMessage = chunk.message || chunk.content || "An error occurred";
console.error("Stream error:", errorMessage);
deps.setIsStreamingInitiated(false);
deps.setHasTextChunks(false);
deps.setStreamingChunks([]);
deps.streamingChunksRef.current = [];
}

View File

@@ -25,9 +25,10 @@ export function useChatContainer({
const [messages, setMessages] = useState<ChatMessageData[]>([]);
const [streamingChunks, setStreamingChunks] = useState<string[]>([]);
const [hasTextChunks, setHasTextChunks] = useState(false);
const [isStreamingInitiated, setIsStreamingInitiated] = useState(false);
const streamingChunksRef = useRef<string[]>([]);
const { error, sendMessage: sendStreamMessage } = useChatStream();
const isStreaming = hasTextChunks;
const isStreaming = isStreamingInitiated || hasTextChunks;
const allMessages = useMemo(() => {
const processedInitialMessages = initialMessages
@@ -99,17 +100,20 @@ export function useChatContainer({
setStreamingChunks([]);
streamingChunksRef.current = [];
setHasTextChunks(false);
setIsStreamingInitiated(true);
const dispatcher = createStreamEventDispatcher({
setHasTextChunks,
setStreamingChunks,
streamingChunksRef,
setMessages,
sessionId,
setIsStreamingInitiated,
});
try {
await sendStreamMessage(sessionId, content, dispatcher, isUserMessage);
} catch (err) {
console.error("Failed to send message:", err);
setIsStreamingInitiated(false);
const errorMessage =
err instanceof Error ? err.message : "Failed to send message";
toast.error("Failed to send message", {

View File

@@ -75,7 +75,7 @@ export function ChatCredentialsSetup({
return (
<Card
className={cn(
"mx-4 my-2 overflow-hidden border-orange-200 bg-orange-50 dark:border-orange-900 dark:bg-orange-950",
"mx-4 my-2 overflow-hidden border-orange-200 bg-orange-50",
className,
)}
>
@@ -84,16 +84,10 @@ export function ChatCredentialsSetup({
<KeyIcon size={24} weight="bold" className="text-white" />
</div>
<div className="flex-1">
<Text
variant="h3"
className="mb-2 text-orange-900 dark:text-orange-100"
>
<Text variant="h3" className="mb-2 text-orange-900">
Credentials Required
</Text>
<Text
variant="body"
className="mb-4 text-orange-700 dark:text-orange-300"
>
<Text variant="body" className="mb-4 text-orange-700">
{message}
</Text>
@@ -106,9 +100,8 @@ export function ChatCredentialsSetup({
<div
key={`${cred.provider}-${index}`}
className={cn(
"relative rounded-lg border border-orange-200 bg-white p-4 dark:border-orange-800 dark:bg-orange-900/20",
isSelected &&
"border-green-500 bg-green-50 dark:border-green-700 dark:bg-green-950/30",
"relative rounded-lg border border-orange-200 bg-white p-4",
isSelected && "border-green-500 bg-green-50",
)}
>
<div className="mb-2 flex items-center justify-between">
@@ -128,7 +121,7 @@ export function ChatCredentialsSetup({
)}
<Text
variant="body"
className="font-semibold text-orange-900 dark:text-orange-100"
className="font-semibold text-orange-900"
>
{cred.providerName}
</Text>

View File

@@ -1,6 +1,6 @@
import { Input } from "@/components/atoms/Input/Input";
import { cn } from "@/lib/utils";
import { PaperPlaneRightIcon } from "@phosphor-icons/react";
import { Button } from "@/components/atoms/Button/Button";
import { ArrowUpIcon } from "@phosphor-icons/react";
import { useChatInput } from "./useChatInput";
export interface ChatInputProps {
@@ -16,48 +16,49 @@ export function ChatInput({
placeholder = "Type your message...",
className,
}: ChatInputProps) {
const { value, setValue, handleKeyDown, handleSend, textareaRef } =
useChatInput({
onSend,
disabled,
maxRows: 5,
});
const inputId = "chat-input";
const { value, setValue, handleKeyDown, handleSend } = useChatInput({
onSend,
disabled,
maxRows: 5,
inputId,
});
return (
<div className={cn("flex gap-2", className)}>
<textarea
ref={textareaRef}
<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}
autoComplete="off"
aria-label="Chat message input"
aria-describedby="chat-input-hint"
className={cn(
"flex-1 resize-none rounded-lg border border-neutral-200 bg-white px-4 py-2 text-sm",
"placeholder:text-neutral-400",
"focus:border-violet-600 focus:outline-none focus:ring-2 focus:ring-violet-600/20",
"dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500",
"disabled:cursor-not-allowed disabled:opacity-50",
)}
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>
<Button
variant="primary"
size="small"
<button
onClick={handleSend}
disabled={disabled || !value.trim()}
className="self-end"
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"
>
<PaperPlaneRightIcon className="h-4 w-4" weight="fill" />
</Button>
<ArrowUpIcon className="h-3 w-3" weight="bold" />
</button>
</div>
);
}

View File

@@ -1,21 +1,22 @@
import { KeyboardEvent, useCallback, useState, useRef, useEffect } from "react";
import { KeyboardEvent, useCallback, useEffect, useState } from "react";
interface UseChatInputArgs {
onSend: (message: string) => void;
disabled?: boolean;
maxRows?: number;
inputId?: string;
}
export function useChatInput({
onSend,
disabled = false,
maxRows = 5,
inputId = "chat-input",
}: UseChatInputArgs) {
const [value, setValue] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const textarea = textareaRef.current;
const textarea = document.getElementById(inputId) as HTMLTextAreaElement;
if (!textarea) return;
textarea.style.height = "auto";
const lineHeight = parseInt(
@@ -27,23 +28,25 @@ export function useChatInput({
textarea.style.height = `${newHeight}px`;
textarea.style.overflowY =
textarea.scrollHeight > maxHeight ? "auto" : "hidden";
}, [value, maxRows]);
}, [value, maxRows, inputId]);
const handleSend = useCallback(() => {
if (disabled || !value.trim()) return;
onSend(value.trim());
setValue("");
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
const textarea = document.getElementById(inputId) as HTMLTextAreaElement;
if (textarea) {
textarea.style.height = "auto";
}
}, [value, onSend, disabled]);
}, [value, onSend, disabled, inputId]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLTextAreaElement>) => {
(event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
handleSend();
}
// Shift+Enter allows default behavior (new line) - no need to handle explicitly
},
[handleSend],
);
@@ -53,6 +56,5 @@ export function useChatInput({
setValue,
handleKeyDown,
handleSend,
textareaRef,
};
}

View File

@@ -1,7 +1,6 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { ArrowClockwiseIcon } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { ArrowClockwiseIcon } from "@phosphor-icons/react";
export interface ChatLoadingStateProps {
message?: string;
@@ -22,7 +21,7 @@ export function ChatLoadingState({
weight="bold"
className="animate-spin text-purple-500"
/>
<Text variant="body" className="text-zinc-600 dark:text-zinc-400">
<Text variant="body" className="text-zinc-600">
{message}
</Text>
</div>

View File

@@ -1,8 +1,13 @@
"use client";
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import Avatar, {
AvatarFallback,
AvatarImage,
} from "@/components/atoms/Avatar/Avatar";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils";
import { CheckCircleIcon, RobotIcon, UserIcon } from "@phosphor-icons/react";
import { CheckCircleIcon, RobotIcon } from "@phosphor-icons/react";
import { useCallback } from "react";
import { getToolActionPhrase } from "../../helpers";
import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
@@ -28,15 +33,21 @@ export function ChatMessage({
}: ChatMessageProps) {
const { user } = useSupabase();
const {
formattedTimestamp,
isUser,
isAssistant,
isToolCall,
isToolResponse,
isLoginNeeded,
isCredentialsNeeded,
} = useChatMessage(message);
const { data: profile } = useGetV2GetUserProfile({
query: {
select: (res) => (res.status === 200 ? res.data : null),
enabled: isUser && !!user,
queryKey: ["/api/store/profile", user?.id],
},
});
const handleAllCredentialsComplete = useCallback(
function handleAllCredentialsComplete() {
// Send a user message that explicitly asks to retry the setup
@@ -81,7 +92,7 @@ export function ChatMessage({
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="my-4 overflow-hidden rounded-lg border border-green-200 bg-gradient-to-br from-green-50 to-emerald-50">
<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">
@@ -92,10 +103,10 @@ export function ChatMessage({
/>
</div>
<div>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
<h3 className="text-lg font-semibold text-neutral-900">
Successfully Authenticated
</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
<p className="text-sm text-neutral-600">
You&apos;re now signed in and ready to continue
</p>
</div>
@@ -146,43 +157,44 @@ export function ChatMessage({
return (
<div
className={cn(
"flex gap-3 px-4 py-4",
isUser && "flex-row-reverse",
"group relative flex w-full gap-3 px-4 py-3",
isUser ? "justify-end" : "justify-start",
className,
)}
>
{/* Avatar */}
<div className="flex-shrink-0">
<div className="flex w-full max-w-3xl gap-3">
{!isUser && (
<div className="flex-shrink-0">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-neutral-200">
<RobotIcon className="h-4 w-4 text-neutral-600" />
</div>
</div>
)}
<div
className={cn(
"flex h-8 w-8 items-center justify-center rounded-full",
isUser && "bg-zinc-200 dark:bg-zinc-700",
isAssistant && "bg-purple-600 dark:bg-purple-500",
"flex min-w-0 flex-1 flex-col",
isUser && "items-end",
)}
>
{isUser ? (
<UserIcon className="h-5 w-5 text-zinc-700 dark:text-zinc-200" />
) : (
<RobotIcon className="h-5 w-5 text-white" />
)}
<MessageBubble variant={isUser ? "user" : "assistant"}>
<MarkdownContent content={message.content} />
</MessageBubble>
</div>
</div>
{/* Message Content */}
<div className={cn("flex max-w-[70%] flex-col", isUser && "items-end")}>
<MessageBubble variant={isUser ? "user" : "assistant"}>
<MarkdownContent content={message.content} />
</MessageBubble>
{/* Timestamp */}
<span
className={cn(
"mt-1 text-xs text-zinc-500 dark:text-zinc-400",
isUser && "text-right",
)}
>
{formattedTimestamp}
</span>
{isUser && (
<div className="flex-shrink-0">
<Avatar className="h-7 w-7">
<AvatarImage
src={profile?.avatar_url ?? ""}
alt={profile?.username ?? "User"}
/>
<AvatarFallback className="rounded-lg bg-neutral-200 text-neutral-600">
{profile?.username?.charAt(0)?.toUpperCase() || "U"}
</AvatarFallback>
</Avatar>
</div>
)}
</div>
</div>
);

View File

@@ -1,8 +1,7 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { Button } from "@/components/atoms/Button/Button";
import { CheckCircle, Play, ArrowSquareOut } from "@phosphor-icons/react";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { ArrowSquareOut, CheckCircle, Play } from "@phosphor-icons/react";
export interface ExecutionStartedMessageProps {
executionId: string;
@@ -22,7 +21,7 @@ export function ExecutionStartedMessage({
return (
<div
className={cn(
"mx-4 my-2 flex flex-col gap-4 rounded-lg border border-green-200 bg-green-50 p-6 dark:border-green-900 dark:bg-green-950",
"mx-4 my-2 flex flex-col gap-4 rounded-lg border border-green-200 bg-green-50 p-6",
className,
)}
>
@@ -32,48 +31,33 @@ export function ExecutionStartedMessage({
<CheckCircle size={24} weight="bold" className="text-white" />
</div>
<div className="flex-1">
<Text
variant="h3"
className="mb-1 text-green-900 dark:text-green-100"
>
<Text variant="h3" className="mb-1 text-green-900">
Execution Started
</Text>
<Text variant="body" className="text-green-700 dark:text-green-300">
<Text variant="body" className="text-green-700">
{message}
</Text>
</div>
</div>
{/* Details */}
<div className="rounded-md bg-green-100 p-4 dark:bg-green-900">
<div className="rounded-md bg-green-100 p-4">
<div className="space-y-2">
{agentName && (
<div className="flex items-center justify-between">
<Text
variant="small"
className="font-semibold text-green-900 dark:text-green-100"
>
<Text variant="small" className="font-semibold text-green-900">
Agent:
</Text>
<Text
variant="body"
className="text-green-800 dark:text-green-200"
>
<Text variant="body" className="text-green-800">
{agentName}
</Text>
</div>
)}
<div className="flex items-center justify-between">
<Text
variant="small"
className="font-semibold text-green-900 dark:text-green-100"
>
<Text variant="small" className="font-semibold text-green-900">
Execution ID:
</Text>
<Text
variant="small"
className="font-mono text-green-800 dark:text-green-200"
>
<Text variant="small" className="font-mono text-green-800">
{executionId.slice(0, 16)}...
</Text>
</div>
@@ -94,7 +78,7 @@ export function ExecutionStartedMessage({
</div>
)}
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<div className="flex items-center gap-2 text-green-600">
<Play size={16} weight="fill" />
<Text variant="small">
Your agent is now running. You can monitor its progress in the monitor

View File

@@ -1,9 +1,9 @@
"use client";
import { cn } from "@/lib/utils";
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils";
interface MarkdownContentProps {
content: string;
@@ -41,7 +41,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
if (isInline) {
return (
<code
className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-sm text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
className="rounded bg-zinc-100 px-1.5 py-0.5 font-mono text-sm text-zinc-800"
{...props}
>
{children}
@@ -49,17 +49,14 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
);
}
return (
<code
className="font-mono text-sm text-zinc-100 dark:text-zinc-200"
{...props}
>
<code className="font-mono text-sm text-zinc-100" {...props}>
{children}
</code>
);
},
pre: ({ children, ...props }) => (
<pre
className="my-2 overflow-x-auto rounded-md bg-zinc-900 p-3 dark:bg-zinc-950"
className="my-2 overflow-x-auto rounded-md bg-zinc-900 p-3"
{...props}
>
{children}
@@ -70,7 +67,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 underline decoration-1 underline-offset-2 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300"
className="text-purple-600 underline decoration-1 underline-offset-2 hover:text-purple-700"
{...props}
>
{children}
@@ -126,7 +123,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
return (
<input
type="checkbox"
className="mr-2 h-4 w-4 rounded border-zinc-300 text-purple-600 focus:ring-purple-500 disabled:cursor-not-allowed disabled:opacity-70 dark:border-zinc-600"
className="mr-2 h-4 w-4 rounded border-zinc-300 text-purple-600 focus:ring-purple-500 disabled:cursor-not-allowed disabled:opacity-70"
disabled
{...props}
/>
@@ -136,57 +133,42 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
},
blockquote: ({ children, ...props }) => (
<blockquote
className="my-2 border-l-4 border-zinc-300 pl-3 italic text-zinc-700 dark:border-zinc-600 dark:text-zinc-300"
className="my-2 border-l-4 border-zinc-300 pl-3 italic text-zinc-700"
{...props}
>
{children}
</blockquote>
),
h1: ({ children, ...props }) => (
<h1
className="my-2 text-xl font-bold text-zinc-900 dark:text-zinc-100"
{...props}
>
<h1 className="my-2 text-xl font-bold text-zinc-900" {...props}>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2
className="my-2 text-lg font-semibold text-zinc-800 dark:text-zinc-200"
{...props}
>
<h2 className="my-2 text-lg font-semibold text-zinc-800" {...props}>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3
className="my-1 text-base font-semibold text-zinc-800 dark:text-zinc-200"
className="my-1 text-base font-semibold text-zinc-800"
{...props}
>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4
className="my-1 text-sm font-medium text-zinc-700 dark:text-zinc-300"
{...props}
>
<h4 className="my-1 text-sm font-medium text-zinc-700" {...props}>
{children}
</h4>
),
h5: ({ children, ...props }) => (
<h5
className="my-1 text-sm font-medium text-zinc-700 dark:text-zinc-300"
{...props}
>
<h5 className="my-1 text-sm font-medium text-zinc-700" {...props}>
{children}
</h5>
),
h6: ({ children, ...props }) => (
<h6
className="my-1 text-xs font-medium text-zinc-600 dark:text-zinc-400"
{...props}
>
<h6 className="my-1 text-xs font-medium text-zinc-600" {...props}>
{children}
</h6>
),
@@ -196,15 +178,12 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
</p>
),
hr: ({ ...props }) => (
<hr
className="my-3 border-zinc-300 dark:border-zinc-700"
{...props}
/>
<hr className="my-3 border-zinc-300" {...props} />
),
table: ({ children, ...props }) => (
<div className="my-2 overflow-x-auto">
<table
className="min-w-full divide-y divide-zinc-200 rounded border border-zinc-200 dark:divide-zinc-700 dark:border-zinc-700"
className="min-w-full divide-y divide-zinc-200 rounded border border-zinc-200"
{...props}
>
{children}
@@ -213,7 +192,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
th: ({ children, ...props }) => (
<th
className="bg-zinc-50 px-3 py-2 text-left text-xs font-semibold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
className="bg-zinc-50 px-3 py-2 text-left text-xs font-semibold text-zinc-700"
{...props}
>
{children}
@@ -221,7 +200,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
),
td: ({ children, ...props }) => (
<td
className="border-t border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700"
className="border-t border-zinc-200 px-3 py-2 text-sm"
{...props}
>
{children}

View File

@@ -15,10 +15,9 @@ export function MessageBubble({
return (
<div
className={cn(
"rounded-lg px-4 py-3 text-sm",
variant === "user" && "bg-violet-600 text-white dark:bg-violet-500",
variant === "assistant" &&
"border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100",
"min-w-20 rounded-[20px] px-6 py-2.5 text-sm leading-relaxed",
variant === "user" && "bg-zinc-700 text-right text-neutral-50",
variant === "assistant" && "bg-zinc-100 text-left text-neutral-900",
className,
)}
>

View File

@@ -2,6 +2,7 @@ import { cn } from "@/lib/utils";
import { ChatMessage } from "../ChatMessage/ChatMessage";
import type { ChatMessageData } from "../ChatMessage/useChatMessage";
import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
import { useMessageList } from "./useMessageList";
export interface MessageListProps {
@@ -30,12 +31,12 @@ export function MessageList({
<div
ref={messagesContainerRef}
className={cn(
"flex-1 overflow-y-auto",
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700",
"flex-1 overflow-y-auto bg-white",
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300",
className,
)}
>
<div className="space-y-0">
<div className="mx-auto flex max-w-3xl flex-col py-4">
{/* Render all persisted messages */}
{messages.map((message, index) => (
<ChatMessage
@@ -45,6 +46,9 @@ export function MessageList({
/>
))}
{/* Render thinking message when streaming but no chunks yet */}
{isStreaming && streamingChunks.length === 0 && <ThinkingMessage />}
{/* Render streaming message if active */}
{isStreaming && streamingChunks.length > 0 && (
<StreamingMessage

View File

@@ -1,7 +1,6 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { MagnifyingGlass, X } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import { MagnifyingGlass, X } from "@phosphor-icons/react";
export interface NoResultsMessageProps {
message: string;
@@ -17,26 +16,26 @@ export function NoResultsMessage({
return (
<div
className={cn(
"mx-4 my-2 flex flex-col items-center gap-4 rounded-lg border border-gray-200 bg-gray-50 p-6 dark:border-gray-800 dark:bg-gray-900",
"mx-4 my-2 flex flex-col items-center gap-4 rounded-lg border border-gray-200 bg-gray-50 p-6",
className,
)}
>
{/* Icon */}
<div className="relative flex h-16 w-16 items-center justify-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200 dark:bg-gray-700">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gray-200">
<MagnifyingGlass size={32} weight="bold" className="text-gray-500" />
</div>
<div className="absolute -right-1 -top-1 flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 dark:bg-gray-600">
<div className="absolute -right-1 -top-1 flex h-8 w-8 items-center justify-center rounded-full bg-gray-400">
<X size={20} weight="bold" className="text-white" />
</div>
</div>
{/* Content */}
<div className="text-center">
<Text variant="h3" className="mb-2 text-gray-900 dark:text-gray-100">
<Text variant="h3" className="mb-2 text-gray-900">
No Results Found
</Text>
<Text variant="body" className="text-gray-700 dark:text-gray-300">
<Text variant="body" className="text-gray-700">
{message}
</Text>
</div>
@@ -44,17 +43,14 @@ export function NoResultsMessage({
{/* Suggestions */}
{suggestions.length > 0 && (
<div className="w-full space-y-2">
<Text
variant="small"
className="font-semibold text-gray-900 dark:text-gray-100"
>
<Text variant="small" className="font-semibold text-gray-900">
Try these suggestions:
</Text>
<ul className="space-y-1 rounded-md bg-gray-100 p-4 dark:bg-gray-800">
<ul className="space-y-1 rounded-md bg-gray-100 p-4">
{suggestions.map((suggestion, index) => (
<li
key={index}
className="flex items-start gap-2 text-sm text-gray-700 dark:text-gray-300"
className="flex items-start gap-2 text-sm text-gray-700"
>
<span className="mt-1 text-gray-500"></span>
<span>{suggestion}</span>

View File

@@ -1,4 +1,3 @@
import React from "react";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
@@ -24,13 +23,10 @@ export function QuickActionsWelcome({
className={cn("flex flex-1 items-center justify-center p-4", className)}
>
<div className="max-w-2xl text-center">
<Text
variant="h2"
className="mb-4 text-3xl font-bold text-zinc-900 dark:text-zinc-100"
>
<Text variant="h2" className="mb-4 text-3xl font-bold text-zinc-900">
{title}
</Text>
<Text variant="body" className="mb-8 text-zinc-600 dark:text-zinc-400">
<Text variant="body" className="mb-8 text-zinc-600">
{description}
</Text>
<div className="grid gap-2 sm:grid-cols-2">
@@ -39,7 +35,7 @@ export function QuickActionsWelcome({
key={action}
onClick={() => onActionClick(action)}
disabled={disabled}
className="rounded-lg border border-zinc-200 bg-white p-4 text-left text-sm hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:bg-zinc-800"
className="rounded-lg border border-zinc-200 bg-white p-4 text-left text-sm hover:bg-zinc-50 disabled:cursor-not-allowed disabled:opacity-50"
>
{action}
</button>

View File

@@ -18,24 +18,24 @@ export function StreamingMessage({
const { displayText } = useStreamingMessage({ chunks, onComplete });
return (
<div className={cn("flex gap-3 px-4 py-4", className)}>
{/* Avatar */}
<div className="flex-shrink-0">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-purple-600 dark:bg-purple-500">
<Robot className="h-5 w-5 text-white" />
<div
className={cn(
"group relative flex w-full justify-start gap-3 px-4 py-3",
className,
)}
>
<div className="flex w-full max-w-3xl gap-3">
<div className="flex-shrink-0">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-neutral-200">
<Robot className="h-4 w-4 text-neutral-600" />
</div>
</div>
</div>
{/* Message Content */}
<div className="flex max-w-[70%] flex-col">
<MessageBubble variant="assistant">
<MarkdownContent content={displayText} />
</MessageBubble>
{/* Timestamp */}
<span className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
Typing...
</span>
<div className="flex min-w-0 flex-1 flex-col">
<MessageBubble variant="assistant">
<MarkdownContent content={displayText} />
</MessageBubble>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,40 @@
import { cn } from "@/lib/utils";
import { Robot } from "@phosphor-icons/react";
import { MessageBubble } from "../MessageBubble/MessageBubble";
export interface ThinkingMessageProps {
className?: string;
}
export function ThinkingMessage({ className }: ThinkingMessageProps) {
return (
<div
className={cn(
"group relative flex w-full justify-start gap-3 px-4 py-3",
className,
)}
>
<div className="flex w-full max-w-3xl gap-3">
<div className="flex-shrink-0">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-neutral-200">
<Robot className="h-4 w-4 text-neutral-600" />
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<MessageBubble variant="assistant">
<span
className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-clip-text text-transparent"
style={{
backgroundSize: "200% 100%",
animation: "shimmer 2s ease-in-out infinite",
}}
>
Thinking...
</span>
</MessageBubble>
</div>
</div>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { WrenchIcon } from "@phosphor-icons/react";
import { getToolActionPhrase } from "../../helpers";
@@ -9,40 +10,15 @@ export interface ToolCallMessageProps {
export function ToolCallMessage({ toolName, className }: ToolCallMessageProps) {
return (
<div
className={cn(
"mx-10 max-w-[70%] overflow-hidden rounded-lg border transition-all duration-200",
"border-neutral-200 dark:border-neutral-700",
"bg-white dark:bg-neutral-900",
"animate-in fade-in-50 slide-in-from-top-1",
className,
)}
>
{/* Header */}
<div
className={cn(
"flex items-center justify-between px-3 py-2",
"bg-gradient-to-r from-neutral-50 to-neutral-100 dark:from-neutral-800/20 dark:to-neutral-700/20",
)}
>
<div className="flex items-center gap-2 overflow-hidden">
<WrenchIcon
size={16}
weight="bold"
className="flex-shrink-0 text-neutral-500 dark:text-neutral-400"
/>
<span className="relative inline-block overflow-hidden text-sm font-medium text-neutral-700 dark:text-neutral-300">
{getToolActionPhrase(toolName)}...
<span
className={cn(
"absolute inset-0 bg-gradient-to-r from-transparent via-white/50 to-transparent",
"dark:via-white/20",
"animate-shimmer",
)}
/>
</span>
</div>
</div>
<div className={cn("flex items-center justify-center gap-2", className)}>
<WrenchIcon
size={14}
weight="bold"
className="flex-shrink-0 text-neutral-500"
/>
<Text variant="small" className="text-neutral-500">
{getToolActionPhrase(toolName)}...
</Text>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { WrenchIcon } from "@phosphor-icons/react";
import { getToolActionPhrase } from "../../helpers";
@@ -10,42 +11,19 @@ export interface ToolResponseMessageProps {
export function ToolResponseMessage({
toolName,
success = true,
success: _success = true,
className,
}: ToolResponseMessageProps) {
return (
<div
className={cn(
"mx-10 max-w-[70%] overflow-hidden rounded-lg border transition-all duration-200",
success
? "border-neutral-200 dark:border-neutral-700"
: "border-red-200 dark:border-red-800",
"bg-white dark:bg-neutral-900",
"animate-in fade-in-50 slide-in-from-top-1",
className,
)}
>
{/* Header */}
<div
className={cn(
"flex items-center justify-between px-3 py-2",
"bg-gradient-to-r",
success
? "from-neutral-50 to-neutral-100 dark:from-neutral-800/20 dark:to-neutral-700/20"
: "from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20",
)}
>
<div className="flex items-center gap-2">
<WrenchIcon
size={16}
weight="bold"
className="text-neutral-500 dark:text-neutral-400"
/>
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{getToolActionPhrase(toolName)}...
</span>
</div>
</div>
<div className={cn("flex items-center justify-center gap-2", className)}>
<WrenchIcon
size={14}
weight="bold"
className="flex-shrink-0 text-neutral-500"
/>
<Text variant="small" className="text-neutral-500">
{getToolActionPhrase(toolName)}...
</Text>
</div>
);
}