Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface ContentProps {

interface RootProps {
children: React.ReactNode;
className?: string;
}

interface FooterProps {
Expand All @@ -17,9 +18,9 @@ interface HeaderProps {
children: React.ReactNode;
}

function Root({ children }: RootProps) {
function Root({ children, className }: RootProps) {
return (
<div className="rounded-xl bg-primary border border-secondary shadow-xs">
<div className={cx("rounded-xl bg-primary border border-secondary shadow-xs", className)}>
{children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IntegrationDetailsEvidenceChain,
InvestigationDetailsHeader,
InvestigationDetailsVerdict,
ChatWidget,
} from "./components";

function InvestigationDetails() {
Expand Down Expand Up @@ -41,17 +42,20 @@ function InvestigationDetails() {
}

return (
<div className="w-full max-w-[952px] mx-auto">
<div className="w-full max-w-[952px] mx-auto mb-50">
<InvestigationDetailsHeader />
<div className="flex gap-5 flex-row mt-5">
{/* Added a large mb so that the last card have a bottom space to breath */}
<div className="max-w-investigation-content w-full flex flex-col gap-5 mx-auto mb-50">
<InvestigationDetailsVerdict />
<div className="mt-5 flex flex-col gap-5">
<div className="flex gap-5 items-stretch">
<div className="max-w-investigation-content w-full min-w-0">
<InvestigationDetailsVerdict />
</div>
<div className="w-[340px] shrink-0">
<ChatWidget />
</div>
</div>
<div className="max-w-investigation-content w-full">
<IntegrationDetailsEvidenceChain />
</div>
{/* <div className="max-w-investigation-right-sidebar w-full">
Right sidebar components here
</div> */}
</div>
</div>
);
Expand Down
245 changes: 245 additions & 0 deletions services/dashboard/src/pages/Investigation/components/ChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { Button } from "@/components/base/buttons/button";
import Typography from "@/components/common/Typography";
import { Tooltip, TooltipTrigger } from "@/components/base/tooltip/tooltip";
import { Edit01, ClockRewind, XClose, ArrowCircleUp } from "@untitledui/icons";
import { useState, useRef, useEffect } from "react";
import logoCat from "@/assets/logo-cat.png";
import { cx } from "@/utils/cx";
import { motion, AnimatePresence } from "motion/react";

interface Message {
id: string;
role: "assistant" | "user";
content: string;
timestamp: string;
}

function getFormattedTimestamp() {
return new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
}).format(new Date());
}

const INITIAL_MESSAGES: Message[] = [
{
id: "1",
role: "assistant",
content: "How can I help you today?",
timestamp: getFormattedTimestamp(),
},
];
Comment on lines +28 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale timestamp on initial/reset messages

INITIAL_MESSAGES is evaluated once at module load time, so the timestamp of the "How can I help you today?" message will always reflect when the page was first loaded — not when the chat was opened or reset. When a user clicks "New chat" (handleNewChat), the reset messages will also carry this stale timestamp.

Consider computing the initial messages lazily, e.g.:

Suggested change
const INITIAL_MESSAGES: Message[] = [
{
id: "1",
role: "assistant",
content: "How can I help you today?",
timestamp: getFormattedTimestamp(),
},
];
const getInitialMessages = (): Message[] => [
{
id: "1",
role: "assistant",
content: "How can I help you today?",
timestamp: getFormattedTimestamp(),
},
];

Then use getInitialMessages() in both useState and handleNewChat:

const [messages, setMessages] = useState<Message[]>(getInitialMessages);
// ...
function handleNewChat() {
  setMessages(getInitialMessages());
  setInputValue("");
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/dashboard/src/pages/Investigation/components/ChatWidget.tsx
Line: 28-35

Comment:
**Stale timestamp on initial/reset messages**

`INITIAL_MESSAGES` is evaluated once at module load time, so the timestamp of the "How can I help you today?" message will always reflect when the page was first loaded — not when the chat was opened or reset. When a user clicks "New chat" (`handleNewChat`), the reset messages will also carry this stale timestamp.

Consider computing the initial messages lazily, e.g.:

```suggestion
const getInitialMessages = (): Message[] => [
  {
    id: "1",
    role: "assistant",
    content: "How can I help you today?",
    timestamp: getFormattedTimestamp(),
  },
];
```

Then use `getInitialMessages()` in both `useState` and `handleNewChat`:
```ts
const [messages, setMessages] = useState<Message[]>(getInitialMessages);
// ...
function handleNewChat() {
  setMessages(getInitialMessages());
  setInputValue("");
}
```

How can I resolve this? If you propose a fix, please make it concise.


export function ChatWidget() {
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
const [inputValue, setInputValue] = useState("");
const [isCollapsed, setIsCollapsed] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
if (!isCollapsed) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, isCollapsed]);

function handleSend() {
const trimmed = inputValue.trim();
if (!trimmed) return;

setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: "user",
content: trimmed,
timestamp: getFormattedTimestamp(),
},
]);
setInputValue("");
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
}
Comment on lines +37 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat is client-only with no backend integration

Currently handleSend adds the user's message to local state but never sends it to any API or receives a response. Messages are never persisted and will be lost on navigation or refresh. Is this intentional scaffolding for a future backend integration, or should there be an API call here?

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/dashboard/src/pages/Investigation/components/ChatWidget.tsx
Line: 37-67

Comment:
**Chat is client-only with no backend integration**

Currently `handleSend` adds the user's message to local state but never sends it to any API or receives a response. Messages are never persisted and will be lost on navigation or refresh. Is this intentional scaffolding for a future backend integration, or should there be an API call here?

How can I resolve this? If you propose a fix, please make it concise.


function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}

function handleTextareaChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
setInputValue(e.target.value);
// Auto-resize
const el = e.target;
el.style.height = "auto";
el.style.height = `${el.scrollHeight}px`;
}

function handleNewChat() {
setMessages(INITIAL_MESSAGES);
setInputValue("");
}

return (
<div className="relative h-full w-full flex flex-col items-start justify-start">
<motion.div
animate={{
width: isCollapsed ? 40 : "100%",
height: isCollapsed ? 40 : "100%",
borderRadius: 12,
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className={cx(
"bg-primary border border-secondary shadow-xs flex flex-col overflow-hidden relative",
isCollapsed ? "bg-secondary cursor-pointer hover:bg-tertiary transition-colors" : ""
)}
onClick={() => isCollapsed && setIsCollapsed(false)}
>
{/* Collapsed Logo - only visible when collapsed */}
<AnimatePresence>
{isCollapsed && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="absolute inset-0 flex items-center justify-center pointer-events-none"
>
<img src={logoCat} alt="Aster Logo" className="h-[25px] w-[25px]" />
</motion.div>
)}
</AnimatePresence>

{/* Expanded Content - Fades out when collapsed */}
<motion.div
animate={{ opacity: isCollapsed ? 0 : 1 }}
transition={{ duration: 0.2 }}
className={cx(
"flex flex-col h-full w-full",
isCollapsed ? "pointer-events-none" : ""
)}
>
{/* Header */}
<div className="relative flex flex-row items-center justify-between px-0 h-10 border-b border-secondary bg-secondary shrink-0">
<div className="flex items-center gap-0 pl-[11px] z-10">
<Tooltip title="New chat">
<Button
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleNewChat();
}}
size="md"
color="tertiary"
className="h-9 w-9 text-quaternary hover:text-tertiary"
iconLeading={<Edit01 size={14} />}
/>
</Tooltip>
<Tooltip title="Previous chats">
<Button
size="md"
color="tertiary"
className="h-9 w-9 text-quaternary hover:text-tertiary"
iconLeading={<ClockRewind size={14} />}
/>
</Tooltip>
</div>

<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="flex items-center gap-1 pointer-events-auto">
<img src={logoCat} alt="Aster Logo" className="h-[18px] w-[18px]" />
<Typography variant="xs/semibold" className="text-quaternary uppercase">
CHAT
</Typography>
</div>
</div>

<div className="pr-[11px] z-10">
<Tooltip title="Collapse chat">
<Button
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setIsCollapsed(true);
}}
size="md"
color="tertiary"
className="h-9 w-9 text-quaternary hover:text-tertiary"
iconLeading={<XClose size={14} />}
/>
</Tooltip>
</div>
</div>

{/* Messages */}
<div className="flex-1 overflow-y-auto px-4 py-4 flex flex-col gap-3 min-h-0">
{messages.map((msg) => (
<ChatMessage key={msg.id} message={msg} />
))}
<div ref={messagesEndRef} />
</div>

{/* Input area */}
<div className="shrink-0 px-5 pb-4 pt-4 border-t border-secondary">
<div className="flex items-center gap-2 rounded-[12px] border border-secondary bg-primary pl-3 pr-2 py-2 min-h-[40px] hover:border-brand-500 focus-within:border-brand-500 focus-within:ring-1 focus-within:ring-brand-500 transition-all">
<textarea
ref={textareaRef}
rows={1}
placeholder="Ask Aster a follow-up question..."
value={inputValue}
onChange={handleTextareaChange}
onKeyDown={handleKeyDown}
className={cx(
"flex-1 resize-none bg-transparent text-sm text-primary placeholder:text-placeholder outline-none min-h-[24px] max-h-32 leading-6 py-0",
)}
/>
<button
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
handleSend();
}}
disabled={!inputValue.trim()}
className={cx(
"shrink-0 flex items-center justify-center h-6 w-6 rounded-lg transition-colors cursor-pointer",
inputValue.trim()
? "text-brand-600 hover:text-brand-700 hover:bg-brand-50"
: "text-quaternary cursor-not-allowed",
)}
title="Send"
>
<ArrowCircleUp size={20} />
</button>
</div>
</div>
</motion.div>
</motion.div>
</div>
);
}

function ChatMessage({ message }: { message: Message }) {
if (message.role === "assistant") {
return (
<div className="flex flex-row gap-2 items-start">
<Tooltip title={message.timestamp}>
<TooltipTrigger className="max-w-[85%] rounded-lg rounded-tl-none bg-secondary border border-secondary px-3 py-2 text-sm text-primary text-left">
{message.content}
</TooltipTrigger>
</Tooltip>
</div>
);
}

return (
<div className="flex flex-row-reverse gap-2 items-end">
<Tooltip title={message.timestamp}>
<TooltipTrigger className="max-w-[85%] rounded-lg rounded-br-none bg-brand-600 px-3 py-2 text-sm text-white text-left">
{message.content}
</TooltipTrigger>
</Tooltip>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ const CONFIDENCE_LEVEL_MAP = {
},
};

export function InvestigationDetailsVerdict() {
export function InvestigationDetailsVerdict({ className }: { className?: string }) {
const { id } = useParams();
const { data: investigation } = useInvestigation(id || "");

const { hypothesis, rootCause, recommendedFix, confidenceLevel } =
investigation;

return (
<div className="">
<ContentContainerCard>
<div className={cx("h-full", className)}>
<ContentContainerCard className="h-full flex flex-col">
<ContentContainerCard.Header>
ASTER'S VERDICT
</ContentContainerCard.Header>
<ContentContainerCard.Content>
<ContentContainerCard.Content className="flex-1">
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-3">
<div className="flex items-center justify-center min-w-9 min-h-9 rounded-lg bg-brand-50">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { InvestigationDetailsHeader } from "./InvestigationDetailsHeader";
export { InvestigationDetailsVerdict } from "./InvestigationDetailsVerdict";
export { IntegrationDetailsEvidenceChain } from "./IntegrationDetailsEvidenceChain";
export { ChatWidget } from "./ChatWidget";