chore: refinements frontend

This commit is contained in:
Lluis Agusti
2025-12-16 16:06:26 +01:00
parent 17cef05b8b
commit d9d6a66608
7 changed files with 210 additions and 104 deletions

View File

@@ -0,0 +1,104 @@
"use client";
import { cn } from "@/lib/utils";
import React from "react";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState";
import { useChat } from "./useChat";
export interface ChatProps {
className?: string;
headerTitle?: React.ReactNode;
showHeader?: boolean;
showSessionInfo?: boolean;
showNewChatButton?: boolean;
onNewChat?: () => void;
headerActions?: React.ReactNode;
}
export function Chat({
className,
headerTitle = "Chat",
showHeader = true,
showSessionInfo = true,
showNewChatButton = true,
onNewChat,
headerActions,
}: ChatProps) {
const {
messages,
isLoading,
isCreating,
error,
sessionId,
createSession,
clearSession,
refreshSession,
} = useChat();
const handleNewChat = () => {
clearSession();
onNewChat?.();
};
return (
<div className={cn("flex h-full flex-col", className)}>
{/* Header */}
{showHeader && (
<header className="shrink-0 border-b border-zinc-200 bg-white p-4">
<div className="flex items-center justify-between">
{typeof headerTitle === "string" ? (
<h2 className="text-xl font-semibold">{headerTitle}</h2>
) : (
headerTitle
)}
<div className="flex items-center gap-4">
{showSessionInfo && sessionId && (
<>
<span className="text-sm text-zinc-600">
Session: {sessionId.slice(0, 8)}...
</span>
{showNewChatButton && (
<button
onClick={handleNewChat}
className="text-sm text-zinc-600 hover:text-zinc-900"
>
New Chat
</button>
)}
</>
)}
{headerActions}
</div>
</div>
</header>
)}
{/* Main Content */}
<main className="flex min-h-0 flex-1 flex-col overflow-hidden">
{/* Loading State - show when explicitly loading/creating OR when we don't have a session yet and no error */}
{(isLoading || isCreating || (!sessionId && !error)) && (
<ChatLoadingState
message={isCreating ? "Creating session..." : "Loading..."}
/>
)}
{/* Error State */}
{error && !isLoading && (
<ChatErrorState error={error} onRetry={createSession} />
)}
{/* Session Content */}
{sessionId && !isLoading && !error && (
<ChatContainer
sessionId={sessionId}
initialMessages={messages}
onRefreshSession={refreshSession}
className="flex-1"
/>
)}
</main>
</div>
);
}

View File

@@ -7,10 +7,7 @@ import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { X } from "@phosphor-icons/react";
import { useEffect } from "react";
import { Drawer } from "vaul";
import { ChatContainer } from "./components/ChatContainer/ChatContainer";
import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
import { ChatLoadingState } from "./components/ChatLoadingState/ChatLoadingState";
import { useChat } from "./useChat";
import { Chat } from "./Chat";
import { useChatDrawer } from "./useChatDrawer";
interface ChatDrawerProps {
@@ -20,16 +17,6 @@ interface ChatDrawerProps {
export function ChatDrawer({ blurBackground = true }: ChatDrawerProps) {
const isChatEnabled = useGetFlag(Flag.CHAT);
const { isOpen, close } = useChatDrawer();
const {
messages,
isLoading,
isCreating,
error,
sessionId,
createSession,
clearSession,
refreshSession,
} = useChat();
useEffect(() => {
if (isChatEnabled === false && isOpen) {
@@ -68,62 +55,23 @@ export function ChatDrawer({ blurBackground = true }: ChatDrawerProps) {
scrollbarStyles,
)}
>
{/* Header */}
<header className="shrink-0 border-b border-zinc-200 bg-white p-4">
<div className="flex items-center justify-between">
<Chat
headerTitle={
<Drawer.Title className="text-xl font-semibold">
Chat
</Drawer.Title>
<div className="flex items-center gap-4">
{sessionId && (
<>
<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"
>
New Chat
</button>
</>
)}
<Button
variant="link"
aria-label="Close"
onClick={close}
className="!focus-visible:ring-0 p-0"
>
<X width="1.5rem" />
</Button>
</div>
</div>
</header>
{/* Main Content */}
<main className="flex min-h-0 flex-1 flex-col overflow-hidden">
{/* Loading State - show when explicitly loading/creating OR when we don't have a session yet and no error */}
{(isLoading || isCreating || (!sessionId && !error)) && (
<ChatLoadingState
message={isCreating ? "Creating session..." : "Loading..."}
/>
)}
{/* Error State */}
{error && !isLoading && (
<ChatErrorState error={error} onRetry={createSession} />
)}
{/* Session Content */}
{sessionId && !isLoading && !error && (
<ChatContainer
sessionId={sessionId}
initialMessages={messages}
onRefreshSession={refreshSession}
className="flex-1"
/>
)}
</main>
}
headerActions={
<Button
variant="link"
aria-label="Close"
onClick={close}
className="!focus-visible:ring-0 p-0"
>
<X width="1.5rem" />
</Button>
}
/>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>

View File

@@ -3,12 +3,14 @@ import { Card } from "@/components/atoms/Card/Card";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { ArrowRight, List, Robot } from "@phosphor-icons/react";
import Image from "next/image";
export interface Agent {
id: string;
name: string;
description: string;
version?: number;
image_url?: string;
}
export interface AgentCarouselMessageProps {
@@ -56,8 +58,23 @@ export function AgentCarouselMessage({
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">
<Robot size={20} weight="bold" className="text-purple-600" />
<div className="relative h-10 w-10 flex-shrink-0 overflow-hidden rounded-lg bg-purple-100">
{agent.image_url ? (
<Image
src={agent.image_url}
alt={`${agent.name} preview image`}
fill
className="object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<Robot
size={20}
weight="bold"
className="text-purple-600"
/>
</div>
)}
</div>
<div className="flex-1 space-y-2">
<div>

View File

@@ -63,6 +63,7 @@ export function isAgentArray(value: unknown): value is Array<{
name: string;
description: string;
version?: number;
image_url?: string;
}> {
if (!Array.isArray(value)) {
return false;
@@ -77,7 +78,8 @@ export function isAgentArray(value: unknown): value is Array<{
typeof item.name === "string" &&
"description" in item &&
typeof item.description === "string" &&
(!("version" in item) || typeof item.version === "number"),
(!("version" in item) || typeof item.version === "number") &&
(!("image_url" in item) || typeof item.image_url === "string"),
);
}

View File

@@ -1,29 +1,18 @@
import { Text } from "@/components/atoms/Text/Text";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { cn } from "@/lib/utils";
import { ArrowClockwiseIcon } from "@phosphor-icons/react";
export interface ChatLoadingStateProps {
message?: string;
className?: string;
}
export function ChatLoadingState({
message = "Loading...",
className,
}: ChatLoadingStateProps) {
export function ChatLoadingState({ className }: ChatLoadingStateProps) {
return (
<div
className={cn("flex flex-1 items-center justify-center p-6", className)}
>
<div className="flex flex-col items-center gap-4 text-center">
<ArrowClockwiseIcon
size={32}
weight="bold"
className="animate-spin text-purple-500"
/>
<Text variant="body" className="text-zinc-600">
{message}
</Text>
<LoadingSpinner />
</div>
</div>
);

View File

@@ -1,5 +1,5 @@
import { formatDistanceToNow } from "date-fns";
import type { ToolArguments, ToolResult } from "@/types/chat";
import { formatDistanceToNow } from "date-fns";
export type ChatMessageData =
| {
@@ -65,6 +65,7 @@ export type ChatMessageData =
name: string;
description: string;
version?: number;
image_url?: string;
}>;
totalCount?: number;
timestamp?: string | Date;

View File

@@ -20,26 +20,71 @@ export function QuickActionsWelcome({
}: QuickActionsWelcomeProps) {
return (
<div
className={cn("flex flex-1 items-center justify-center p-4", className)}
className={cn("flex flex-1 items-center justify-center p-8", className)}
>
<div className="max-w-2xl text-center">
<Text variant="h2" className="mb-4 text-3xl font-bold text-zinc-900">
{title}
</Text>
<Text variant="body" className="mb-8 text-zinc-600">
{description}
</Text>
<div className="grid gap-2 sm:grid-cols-2">
{actions.map((action) => (
<button
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"
>
{action}
</button>
))}
<div className="w-full max-w-3xl">
<div className="mb-12 text-center">
<Text
variant="h2"
className="mb-3 text-2xl font-semibold text-zinc-900"
>
{title}
</Text>
<Text variant="body" className="text-zinc-500">
{description}
</Text>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{actions.map((action) => {
// Use slate theme for all cards
const theme = {
bg: "bg-slate-50/30",
border: "border-slate-200",
hoverBg: "hover:bg-slate-100",
hoverBorder: "hover:border-slate-200",
gradient: "from-slate-300/20 via-slate-400/10 to-transparent",
text: "text-slate-900",
hoverText: "group-hover:text-slate-900",
};
return (
<button
key={action}
onClick={() => onActionClick(action)}
disabled={disabled}
className={cn(
"group relative overflow-hidden rounded-xl border p-5 text-left backdrop-blur-xl",
"transition-all duration-200",
theme.bg,
theme.border,
theme.hoverBg,
theme.hoverBorder,
"hover:shadow-sm",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:shadow-none",
)}
>
{/* Gradient flare background */}
<div
className={cn(
"absolute inset-0 bg-gradient-to-br",
theme.gradient,
)}
/>
<Text
variant="body"
className={cn(
"relative z-10 font-medium",
theme.text,
theme.hoverText,
)}
>
{action}
</Text>
</button>
);
})}
</div>
</div>
</div>