Skip to content

Commit 54daa4b

Browse files
committed
Implement chat feature with streaming support and UI enhancements
1 parent 6db8b4d commit 54daa4b

7 files changed

Lines changed: 796 additions & 4 deletions

File tree

apps/web/app/chat/page.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
"use client";
2+
3+
import { useState, useRef, useEffect, useCallback } from "react";
4+
import Link from "next/link";
5+
import { ArrowLeft, Trash2, MessageSquare } from "lucide-react";
6+
import { Button } from "@/components/ui/button";
7+
import { ChatMessageItem, ChatInput } from "@/components/chat";
8+
import { ChatService } from "@/services/chat.service";
9+
import type { AgentType, ChatMessage, ContextItem } from "@/types/chat";
10+
import { AGENTS } from "@/types/chat";
11+
12+
export default function ChatPage() {
13+
const [messages, setMessages] = useState<ChatMessage[]>([]);
14+
const [selectedAgent, setSelectedAgent] = useState<AgentType>("pathfinder");
15+
const [isLoading, setIsLoading] = useState(false);
16+
const messagesEndRef = useRef<HTMLDivElement>(null);
17+
18+
const scrollToBottom = useCallback(() => {
19+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
20+
}, []);
21+
22+
useEffect(() => {
23+
scrollToBottom();
24+
}, [messages, scrollToBottom]);
25+
26+
const handleSendMessage = async (content: string) => {
27+
const userMessage: ChatMessage = {
28+
id: crypto.randomUUID(),
29+
role: "user",
30+
content,
31+
timestamp: new Date(),
32+
agentType: selectedAgent,
33+
};
34+
35+
setMessages((prev) => [...prev, userMessage]);
36+
setIsLoading(true);
37+
38+
const assistantMessageId = crypto.randomUUID();
39+
const assistantMessage: ChatMessage = {
40+
id: assistantMessageId,
41+
role: "assistant",
42+
content: "",
43+
timestamp: new Date(),
44+
agentType: selectedAgent,
45+
isStreaming: true,
46+
};
47+
48+
setMessages((prev) => [...prev, assistantMessage]);
49+
50+
try {
51+
let fullContent = "";
52+
let context: ContextItem[] = [];
53+
54+
for await (const chunk of ChatService.streamChat(content, selectedAgent, {
55+
searchType: "hybrid",
56+
contextLimit: 5,
57+
})) {
58+
if (chunk.type === "error") {
59+
throw new Error(chunk.error);
60+
}
61+
62+
if (chunk.type === "context" && chunk.context) {
63+
context = chunk.context;
64+
}
65+
66+
if (chunk.type === "answer_chunk" && chunk.content) {
67+
fullContent += chunk.content;
68+
69+
setMessages((prev) =>
70+
prev.map((msg) =>
71+
msg.id === assistantMessageId
72+
? { ...msg, content: fullContent, context }
73+
: msg
74+
)
75+
);
76+
}
77+
}
78+
79+
setMessages((prev) =>
80+
prev.map((msg) =>
81+
msg.id === assistantMessageId
82+
? { ...msg, isStreaming: false, context }
83+
: msg
84+
)
85+
);
86+
} catch (error) {
87+
console.error("Chat error:", error);
88+
89+
setMessages((prev) =>
90+
prev.map((msg) =>
91+
msg.id === assistantMessageId
92+
? {
93+
...msg,
94+
content:
95+
error instanceof Error
96+
? `Error: ${error.message}`
97+
: "An error occurred while processing your request.",
98+
isStreaming: false,
99+
}
100+
: msg
101+
)
102+
);
103+
} finally {
104+
setIsLoading(false);
105+
}
106+
};
107+
108+
const handleClearChat = () => {
109+
setMessages([]);
110+
};
111+
112+
const currentAgent = AGENTS.find((a) => a.type === selectedAgent);
113+
114+
return (
115+
<div className="flex flex-col h-screen bg-background">
116+
{/* Header */}
117+
<header className="flex items-center justify-between px-4 py-3 border-b bg-card">
118+
<div className="flex items-center gap-3">
119+
<Link href="/">
120+
<Button variant="ghost" size="icon">
121+
<ArrowLeft className="w-4 h-4" />
122+
</Button>
123+
</Link>
124+
<div className="flex items-center gap-2">
125+
<MessageSquare className="w-5 h-5 text-primary" />
126+
<h1 className="text-lg font-semibold">Chat</h1>
127+
</div>
128+
</div>
129+
<Button
130+
variant="ghost"
131+
size="icon"
132+
onClick={handleClearChat}
133+
disabled={messages.length === 0}
134+
title="Clear chat"
135+
>
136+
<Trash2 className="w-4 h-4" />
137+
</Button>
138+
</header>
139+
140+
{/* Messages Area */}
141+
<div className="flex-1 overflow-y-auto">
142+
{messages.length === 0 ? (
143+
<div className="flex flex-col items-center justify-center h-full text-center p-8">
144+
{currentAgent && (
145+
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-4">
146+
<currentAgent.Icon className="w-8 h-8 text-primary" />
147+
</div>
148+
)}
149+
<h2 className="text-2xl font-bold mb-2">
150+
Ask anything about your codebase
151+
</h2>
152+
<p className="text-muted-foreground max-w-md mb-8">
153+
Use the dropdown in the input below to select an agent. Each agent
154+
specializes in different aspects of code analysis.
155+
</p>
156+
157+
{/* Suggestion Cards */}
158+
<div className="w-full max-w-2xl">
159+
<p className="text-sm text-muted-foreground mb-3">Try asking:</p>
160+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
161+
<SuggestionCard
162+
text="How is the authentication flow structured?"
163+
onClick={() =>
164+
handleSendMessage(
165+
"How is the authentication flow structured?"
166+
)
167+
}
168+
disabled={isLoading}
169+
/>
170+
<SuggestionCard
171+
text="What are the main API endpoints?"
172+
onClick={() =>
173+
handleSendMessage("What are the main API endpoints?")
174+
}
175+
disabled={isLoading}
176+
/>
177+
<SuggestionCard
178+
text="Explain the database schema"
179+
onClick={() =>
180+
handleSendMessage("Explain the database schema")
181+
}
182+
disabled={isLoading}
183+
/>
184+
<SuggestionCard
185+
text="Find potential security issues"
186+
onClick={() =>
187+
handleSendMessage("Find potential security issues")
188+
}
189+
disabled={isLoading}
190+
/>
191+
</div>
192+
</div>
193+
</div>
194+
) : (
195+
<div className="max-w-4xl mx-auto py-4 px-4 space-y-4">
196+
{messages.map((message) => (
197+
<ChatMessageItem key={message.id} message={message} />
198+
))}
199+
<div ref={messagesEndRef} />
200+
</div>
201+
)}
202+
</div>
203+
204+
{/* Input Area */}
205+
<div className="border-t bg-card p-4">
206+
<div className="max-w-4xl mx-auto">
207+
<ChatInput
208+
onSend={handleSendMessage}
209+
selectedAgent={selectedAgent}
210+
onAgentChange={setSelectedAgent}
211+
isLoading={isLoading}
212+
/>
213+
</div>
214+
</div>
215+
</div>
216+
);
217+
}
218+
219+
interface SuggestionCardProps {
220+
text: string;
221+
onClick: () => void;
222+
disabled?: boolean;
223+
}
224+
225+
function SuggestionCard({ text, onClick, disabled }: SuggestionCardProps) {
226+
return (
227+
<button
228+
onClick={onClick}
229+
disabled={disabled}
230+
className="p-3 text-left text-sm border rounded-lg hover:bg-accent hover:border-primary/50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
231+
>
232+
{text}
233+
</button>
234+
);
235+
}

apps/web/app/page.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import Link from "next/link";
2-
import { ArrowRight, Network, Puzzle } from "lucide-react";
2+
import { ArrowRight, Network, Puzzle, MessageSquare } from "lucide-react";
33

44
export default function Home() {
55
return (
66
<div className="flex min-h-screen flex-col items-center justify-center p-8 bg-background text-foreground">
7-
<div className="max-w-2xl w-full space-y-8 text-center">
7+
<div className="max-w-3xl w-full space-y-8 text-center">
88
<div className="space-y-4">
99
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
1010
Welcome to Secrin
1111
</h1>
1212
<p className="text-lg text-muted-foreground">
1313
Your security dashboard is currently under construction. In the
14-
meantime, explore our graph visualization and integration settings.
14+
meantime, explore our graph visualization, chat with your codebase, and manage integrations.
1515
</p>
1616
</div>
1717

18-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 mt-8">
18+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 mt-8">
1919
<Link
2020
href="/graph"
2121
className="group relative flex flex-col items-center justify-center rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm transition-all hover:shadow-md hover:border-primary/50"
@@ -33,6 +33,23 @@ export default function Home() {
3333
</span>
3434
</Link>
3535

36+
<Link
37+
href="/chat"
38+
className="group relative flex flex-col items-center justify-center rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm transition-all hover:shadow-md hover:border-primary/50"
39+
>
40+
<div className="mb-4 rounded-full bg-primary/10 p-3 text-primary group-hover:bg-primary/20">
41+
<MessageSquare className="h-6 w-6" />
42+
</div>
43+
<h3 className="mb-2 text-xl font-semibold">Chat</h3>
44+
<p className="text-sm text-muted-foreground text-center mb-4">
45+
Ask questions about your codebase with specialized AI agents.
46+
</p>
47+
<span className="flex items-center text-sm font-medium text-primary">
48+
Start Chatting{" "}
49+
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
50+
</span>
51+
</Link>
52+
3653
<Link
3754
href="/integrations"
3855
className="group relative flex flex-col items-center justify-center rounded-xl border border-border bg-card p-6 text-card-foreground shadow-sm transition-all hover:shadow-md hover:border-primary/50"

0 commit comments

Comments
 (0)