-
Notifications
You must be signed in to change notification settings - Fork 2
chat widget #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
chat widget #36
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(), | ||
| }, | ||
| ]; | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Chat is client-only with no backend integration Currently Prompt To Fix With AIThis 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 |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| export { InvestigationDetailsHeader } from "./InvestigationDetailsHeader"; | ||
| export { InvestigationDetailsVerdict } from "./InvestigationDetailsVerdict"; | ||
| export { IntegrationDetailsEvidenceChain } from "./IntegrationDetailsEvidenceChain"; | ||
| export { ChatWidget } from "./ChatWidget"; |
There was a problem hiding this comment.
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_MESSAGESis 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.:
Then use
getInitialMessages()in bothuseStateandhandleNewChat:Prompt To Fix With AI