few more bro

This commit is contained in:
Mahesh Sanikommmu 2025-12-14 19:56:07 -08:00
parent 5e4afbd317
commit 84fad23e71
6 changed files with 215 additions and 175 deletions

View file

@ -82,7 +82,6 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) {
}, [conversations])
useEffect(() => {
console.log("searchParams", searchParams.get("mcp"))
const mcpParam = searchParams.get("mcp")
if (mcpParam === "manual") {
setMcpInitialClient("mcp-url")

View file

@ -50,7 +50,7 @@ function ChatEmptyStatePlaceholder() {
<Button
key={suggestion}
variant="default"
className="rounded-full text-base gap-1 h-10! border-[#2261CA33] bg-[#041127] border w-fit py-[4px] pl-[8px] pr-[12px]"
className="rounded-full text-base gap-1 h-10! border-[#2261CA33] bg-[#041127] border w-fit py-[4px] pl-[8px] pr-[12px] hover:bg-[#0A1A3A] hover:[&_span]:text-white hover:[&_svg]:text-white transition-colors"
>
<SearchIcon className="size-4 text-[#267BF1]" />
<span className="text-[#267BF1] text-[12px]">{suggestion}</span>
@ -78,6 +78,7 @@ export function ChatSidebar() {
const [loadingFollowUps, setLoadingFollowUps] = useState<
Record<string, boolean>
>({})
const [isInputExpanded, setIsInputExpanded] = useState(false)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const { selectedProject } = useProject()
const { setCurrentChatId } = usePersistentChat()
@ -199,8 +200,6 @@ export function ChatSidebar() {
}
}, [messages, followUpQuestions, loadingFollowUps, status])
console.log(messages)
const handleSend = () => {
if (!input.trim() || status === "submitted" || status === "streaming")
return
@ -284,7 +283,7 @@ export function ChatSidebar() {
>
<motion.button
onClick={toggleChat}
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border-[1px] border-[#17181A] text-white cursor-pointer"
className="flex items-center gap-2 rounded-full px-3 py-1.5 text-xs font-medium border border-[#17181A] text-white cursor-pointer"
style={{
background: "linear-gradient(180deg, #0A0E14 0%, #05070A 100%)",
}}
@ -297,7 +296,7 @@ export function ChatSidebar() {
<motion.div
key="open"
className={cn(
"w-[450px] h-[calc(100vh-95px)] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 border border-[#17181AB2] relative",
"w-[450px] h-[calc(100vh-95px)] bg-[#05070A] backdrop-blur-md flex flex-col rounded-2xl m-4 mt-2 border border-[#17181AB2] relative",
dmSansClassName(),
)}
initial={{ x: "100px", opacity: 0 }}
@ -354,6 +353,12 @@ export function ChatSidebar() {
dmSansClassName(),
)}
>
{isInputExpanded && (
<div
className="absolute inset-0 z-10! rounded-2xl pointer-events-none"
style={{ backgroundColor: "#000000E5" }}
/>
)}
{messages.length === 0 && <ChatEmptyStatePlaceholder />}
<div
className={cn(
@ -429,24 +434,12 @@ export function ChatSidebar() {
? "Structuring response..."
: "Waiting for input..."
}
chainOfThoughtComponent={(() => {
const lastUserMessage = [...messages]
.reverse()
.find((msg) => msg.role === "user")
const lastAgentMessage = [...messages]
.reverse()
.find((msg) => msg.role === "assistant")
const userMessageText =
lastUserMessage?.parts.find((part) => part.type === "text")
?.text ?? ""
return userMessageText ? (
<ChainOfThought
userMessage={userMessageText}
lastAgentMessage={lastAgentMessage}
/>
onExpandedChange={setIsInputExpanded}
chainOfThoughtComponent={
messages.length > 0 ? (
<ChainOfThought messages={messages} />
) : null
})()}
}
/>
</motion.div>
)}

View file

@ -18,169 +18,198 @@ interface ReasoningStep {
message: string
}
export function ChainOfThought({
userMessage,
lastAgentMessage,
}: {
userMessage: string
lastAgentMessage: UIMessage | undefined
}) {
export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
const { user } = useAuth()
const reasoningSteps: ReasoningStep[] = []
if (lastAgentMessage) {
lastAgentMessage.parts.forEach((part) => {
if (part.type === "tool-searchMemories") {
if (
part.state === "input-available" ||
part.state === "input-streaming"
) {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Searching memories...",
})
} else if (part.state === "output-available") {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Found relevant memories",
})
} else if (part.state === "output-error") {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Error searching memories",
})
}
}
})
}
// Group messages into user-assistant pairs
const messagePairs: Array<{
userMessage: UIMessage
agentMessage?: UIMessage
}> = []
const memoryResults: MemoryResult[] = []
if (lastAgentMessage) {
lastAgentMessage.parts.forEach((part) => {
if (
part.type === "tool-searchMemories" &&
part.state === "output-available"
) {
const output = part.output as { results?: MemoryResult[] } | undefined
const results = Array.isArray(output?.results) ? output.results : []
memoryResults.push(...results)
}
})
for (let i = 0; i < messages.length; i++) {
const message = messages[i]
if (message.role === "user") {
// Find the next assistant message after this user message
const agentMessage = messages
.slice(i + 1)
.find((msg) => msg.role === "assistant")
messagePairs.push({
userMessage: message,
agentMessage,
})
}
}
return (
<div className="m-3 mb-0 space-y-3 relative z-10">
<div className="absolute left-[11px] top-0 bottom-0 w-[1px] bg-[#151F31] self-stretch mb-0 z-[-10]" />
<div className="absolute left-[11px] top-0 bottom-0 w-px bg-[#151F31] self-stretch mb-0 -z-10" />
<div className="flex items-start gap-3 text-[#525D6E] py-1 bg-[#01173C]">
{user && (
<Avatar className="size-[21px]">
<AvatarImage src={user?.image ?? ""} />
<AvatarFallback className="text-[10px]">
{user?.name?.charAt(0)}
</AvatarFallback>
</Avatar>
)}
<p>{userMessage}</p>
</div>
{messagePairs.map((pair, pairIdx) => {
const userMessageText =
pair.userMessage.parts.find((part) => part.type === "text")?.text ??
""
{(reasoningSteps.length > 0 || memoryResults.length > 0) && (
<div className="flex gap-3">
<div className="flex flex-col items-center mx-2 bg-[#01173C] py-1 h-fit">
<div className="size-[7px] rounded-full bg-[#151F31] shrink-0" />
</div>
const reasoningSteps: ReasoningStep[] = []
if (pair.agentMessage) {
pair.agentMessage.parts.forEach((part) => {
if (part.type === "tool-searchMemories") {
if (
part.state === "input-available" ||
part.state === "input-streaming"
) {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Searching memories...",
})
} else if (part.state === "output-available") {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Found relevant memories",
})
} else if (part.state === "output-error") {
reasoningSteps.push({
type: part.type,
state: part.state,
message: "Error searching memories",
})
}
}
})
}
<div className="flex-1 space-y-1">
{reasoningSteps.length > 0 && (
<div className="space-y-1.5">
{reasoningSteps.map((step, idx) => (
<div
key={`${step.type}-${step.state}-${idx}`}
className="text-[#525D6E]"
>
{step.message}
</div>
))}
</div>
)}
const memoryResults: MemoryResult[] = []
if (pair.agentMessage) {
pair.agentMessage.parts.forEach((part) => {
if (
part.type === "tool-searchMemories" &&
part.state === "output-available"
) {
const output = part.output as
| { results?: MemoryResult[] }
| undefined
const results = Array.isArray(output?.results)
? output.results
: []
memoryResults.push(...results)
}
})
}
{memoryResults.length > 0 && (
<div className="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto">
{memoryResults.map((result, idx) => {
const isClickable =
result.url &&
(result.url.startsWith("http://") ||
result.url.startsWith("https://"))
return (
<div key={pair.userMessage.id || pairIdx} className="space-y-3">
<div className="flex items-start gap-3 text-[#525D6E] py-1">
{user && (
<Avatar className="size-[21px]">
<AvatarImage src={user?.image ?? ""} />
<AvatarFallback className="text-[10px]">
{user?.name?.charAt(0)}
</AvatarFallback>
</Avatar>
)}
<p>{userMessageText}</p>
</div>
const content = (
<div className="">
<div className="bg-[#060D17] p-2 px-[10px] rounded-xl m-[2px]">
{result.title && (
<div className="text-xs text-[#525D6E] line-clamp-2">
{result.title}
</div>
)}
{result.content && (
<div className="text-xs text-[#525D6E]/80 line-clamp-2 mt-1">
{result.content}
</div>
)}
{result.url && (
<div className="text-xs text-[#525D6E] mt-1 truncate">
{result.url}
</div>
)}
</div>
{result.score && (
<div className="flex justify-center p-1">
<div
className={cn(
"text-[10px] inline-block bg-clip-text text-transparent font-medium",
dmSansClassName(),
)}
style={{
backgroundImage:
"var(--grad-1, linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%))",
}}
>
Relevancy score: {(result.score * 100).toFixed(1)}%
</div>
{(reasoningSteps.length > 0 || memoryResults.length > 0) && (
<div className="flex gap-3">
<div className="flex flex-col items-center mx-2 py-1 h-fit">
<div className="size-[7px] rounded-full bg-[#151F31] shrink-0" />
</div>
<div className="flex-1 space-y-1">
{reasoningSteps.length > 0 && (
<div className="space-y-1.5">
{reasoningSteps.map((step, idx) => (
<div
key={`${step.type}-${step.state}-${idx}`}
className="text-[#525D6E]"
>
{step.message}
</div>
)}
))}
</div>
)
)}
if (isClickable) {
return (
<a
className="block p-2 bg-[#0C1829]/50 rounded-md border border-[#525D6E]/20 hover:bg-[#0C1829]/70 transition-colors cursor-pointer"
href={result.url}
key={result.documentId || idx}
rel="noopener noreferrer"
target="_blank"
>
{content}
</a>
)
}
{memoryResults.length > 0 && (
<div className="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto">
{memoryResults.map((result, idx) => {
const isClickable =
result.url &&
(result.url.startsWith("http://") ||
result.url.startsWith("https://"))
return (
<div
className={cn("bg-[#0C1829] rounded-xl")}
key={result.documentId || idx}
>
{content}
const content = (
<div className="">
<div className="bg-[#060D17] p-2 px-[10px] rounded-xl m-[2px]">
{result.title && (
<div className="text-xs text-[#525D6E] line-clamp-2">
{result.title}
</div>
)}
{result.content && (
<div className="text-xs text-[#525D6E]/80 line-clamp-2 mt-1">
{result.content}
</div>
)}
{result.url && (
<div className="text-xs text-[#525D6E] mt-1 truncate">
{result.url}
</div>
)}
</div>
{result.score && (
<div className="flex justify-center p-1">
<div
className={cn(
"text-[10px] inline-block bg-clip-text text-transparent font-medium",
dmSansClassName(),
)}
style={{
backgroundImage:
"var(--grad-1, linear-gradient(94deg, #369BFD 4.8%, #36FDFD 77.04%, #36FDB5 143.99%))",
}}
>
Relevancy score:{" "}
{(result.score * 100).toFixed(1)}%
</div>
</div>
)}
</div>
)
if (isClickable) {
return (
<a
className="block p-2 bg-[#0C1829]/50 rounded-md border border-[#525D6E]/20 hover:bg-[#0C1829]/70 transition-colors cursor-pointer"
href={result.url}
key={result.documentId || idx}
rel="noopener noreferrer"
target="_blank"
>
{content}
</a>
)
}
return (
<div
className={cn("bg-[#0C1829] rounded-xl")}
key={result.documentId || idx}
>
{content}
</div>
)
})}
</div>
)
})}
)}
</div>
</div>
)}
</div>
</div>
)}
)
})}
</div>
)
}

View file

@ -17,6 +17,7 @@ interface ChatInputProps {
isResponding?: boolean
activeStatus?: string
chainOfThoughtComponent?: React.ReactNode
onExpandedChange?: (expanded: boolean) => void
}
export default function ChatInput({
@ -28,6 +29,7 @@ export default function ChatInput({
isResponding = false,
activeStatus,
chainOfThoughtComponent,
onExpandedChange,
}: ChatInputProps) {
const [isMultiline, setIsMultiline] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
@ -48,11 +50,12 @@ export default function ChatInput({
return (
<motion.div
className={cn("bg-[#01173C] relative")}
className={cn("relative z-20!")}
animate={{
padding: isExpanded ? "0.5rem" : "0",
padding: isExpanded ? "16px" : "0",
margin: isExpanded ? "0" : "16px",
borderRadius: isExpanded ? "0 0 12px 12px" : "12px",
backgroundColor: isExpanded ? "#000B1B" : "#01173C",
}}
transition={{
duration: 0.3,
@ -61,18 +64,15 @@ export default function ChatInput({
>
<div
className={cn(
"absolute bottom-full left-0 right-0 overflow-hidden transition-all duration-300 ease-out bg-[#01173C]",
"absolute bottom-full left-0 right-0 overflow-hidden transition-all duration-300 ease-out bg-[#000B1B]",
isExpanded
? "max-h-[80vh] opacity-100 overflow-y-auto pt-1.5 pb-2 rounded-t-xl px-2"
? "max-h-[80vh] opacity-100 overflow-y-auto pt-1.5 pb-2 rounded-t-xl px-4"
: "max-h-0 opacity-0",
)}
style={{
zIndex: isExpanded ? 50 : 0,
}}
>
{isExpanded && (
<div className="absolute top-0 left-0 right-0 h-10 bg-linear-to-b from-[#01173C] via-[#01173C]/50 to-transparent pointer-events-none z-10" />
)}
{chainOfThoughtComponent}
</div>
<button
@ -82,8 +82,11 @@ export default function ChatInput({
!chainOfThoughtComponent && "disabled:cursor-not-allowed",
)}
onClick={() => {
setIsExpanded(!isExpanded)
const newExpanded = !isExpanded
setIsExpanded(newExpanded)
onExpandedChange?.(newExpanded)
}}
disabled={!chainOfThoughtComponent}
>
<div className="flex items-center gap-3">
<NovaOrb size={24} className="blur-[1px]! z-10" />

View file

@ -62,7 +62,7 @@ export function AgentMessage({
return (
<div
key={`${message.id}-${partIndex}`}
className="text-sm text-white/90"
className="text-sm text-white/90 chat-markdown-content"
>
<Streamdown>{part.text}</Streamdown>
</div>

View file

@ -66,3 +66,19 @@
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* css to fix for ordered list numbers being cut off in chat agent messages */
.chat-markdown-content ol {
padding-left: 1.5rem;
margin-left: 0;
}
.chat-markdown-content ul {
padding-left: 1.5rem;
margin-left: 0;
}
.chat-markdown-content li {
margin-left: 0;
padding-left: 0.25rem;
}