Skip to content
Merged
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
125 changes: 3 additions & 122 deletions apps/web/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,6 @@ export function ChatSidebar({
Record<string, "like" | "dislike" | null>
>({})
const [expandedMemories, setExpandedMemories] = useState<string | null>(null)
const [followUpQuestions, setFollowUpQuestions] = useState<
Record<string, string[]>
>({})
const [loadingFollowUps, setLoadingFollowUps] = useState<
Record<string, boolean>
>({})
const [isInputExpanded, setIsInputExpanded] = useState(false)
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true)
const [heightOffset, setHeightOffset] = useState(95)
Expand All @@ -136,25 +130,24 @@ export function ChatSidebar({
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(
null,
)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const messagesContainerRef = useRef<HTMLDivElement>(null)
const sentQueuedMessageRef = useRef<string | null>(null)
const { selectedProject } = useProject()
const { viewMode } = useViewMode()
const { user } = useAuth()
const { user: _user } = useAuth()
const [threadId, setThreadId] = useQueryState("thread", threadParam)
const [fallbackChatId, setFallbackChatId] = useState(() => generateId())
const currentChatId = threadId ?? fallbackChatId
const chatIdRef = useRef(currentChatId)
chatIdRef.current = currentChatId
const setCurrentChatId = useCallback(
const _setCurrentChatId = useCallback(
(id: string) => setThreadId(id),
[setThreadId],
)
const chatTransport = useMemo(
() =>
new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"}/chat/v2`,
api: `${process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"}/chat`,
credentials: "include",
prepareSendMessagesRequest: ({ messages }) => ({
body: {
Expand Down Expand Up @@ -196,15 +189,6 @@ export function ChatSidebar({
const { messages, sendMessage, status, setMessages, stop } = useChat({
id: currentChatId ?? undefined,
transport: chatTransport,
onFinish: async (result) => {
if (result.message.role !== "assistant") return

// Mark this message as needing follow-up generation
// We'll generate it after the message is fully in the messages array
if (result.message.id) {
pendingFollowUpGenerations.current.add(result.message.id)
}
},
})

useEffect(() => {
Expand All @@ -214,100 +198,6 @@ export function ChatSidebar({
}
}, [currentChatId, pendingThreadLoad, setMessages])

// Generate follow-up questions after assistant messages are complete
useEffect(() => {
const generateFollowUps = async () => {
// Find assistant messages that need follow-up generation
const messagesToProcess = messages.filter(
(msg) =>
msg.role === "assistant" &&
pendingFollowUpGenerations.current.has(msg.id) &&
!followUpQuestions[msg.id] &&
!loadingFollowUps[msg.id],
)

for (const message of messagesToProcess) {
// Get complete text from the message
const assistantText = message.parts
.filter((p) => p.type === "text")
.map((p) => p.text)
.join(" ")
.trim()

// Only generate if we have substantial text (at least 50 chars)
// This ensures the message is complete, not just the first chunk
// Also check if status is idle to ensure streaming is complete
if (
assistantText.length < 50 ||
status === "streaming" ||
status === "submitted"
) {
continue
}

// Mark as processing
pendingFollowUpGenerations.current.delete(message.id)
setLoadingFollowUps((prev) => ({
...prev,
[message.id]: true,
}))

try {
// Get recent messages for context
const recentMessages = messages.slice(-5).map((msg) => ({
role: msg.role,
content: msg.parts
.filter((p) => p.type === "text")
.map((p) => p.text)
.join(" "),
}))

const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/follow-ups`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
messages: recentMessages,
assistantResponse: assistantText,
}),
},
)

if (response.ok) {
const data = await response.json()
if (data.questions && Array.isArray(data.questions)) {
setFollowUpQuestions((prev) => ({
...prev,
[message.id]: data.questions,
}))
}
}
} catch (error) {
console.error("Failed to generate follow-up questions:", error)
} finally {
setLoadingFollowUps((prev) => ({
...prev,
[message.id]: false,
}))
}
}
}

// Only generate if not currently streaming or submitted
// Small delay to ensure message is fully processed
if (status !== "streaming" && status !== "submitted") {
const timeoutId = setTimeout(() => {
generateFollowUps()
}, 300)

return () => clearTimeout(timeoutId)
}
}, [messages, followUpQuestions, loadingFollowUps, status])

const checkIfScrolledToBottom = useCallback(() => {
if (!messagesContainerRef.current) return
const container = messagesContainerRef.current
Expand Down Expand Up @@ -879,19 +769,10 @@ export function ChatSidebar({
copiedMessageId={copiedMessageId}
messageFeedback={messageFeedback}
expandedMemories={expandedMemories}
followUpQuestions={followUpQuestions[message.id] || []}
isLoadingFollowUps={loadingFollowUps[message.id] || false}
onCopy={handleCopyMessage}
onLike={handleLikeMessage}
onDislike={handleDislikeMessage}
onToggleMemories={handleToggleMemories}
onQuestionClick={(question) => {
analytics.chatFollowUpClicked({
thread_id: currentChatId || undefined,
})
analytics.chatMessageSent({ source: "follow_up" })
setInput(question)
}}
/>
)}
</div>
Expand Down
168 changes: 80 additions & 88 deletions apps/web/components/chat/input/chain-of-thought.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { useAuth } from "@lib/auth-context"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import type { UIMessage } from "@ai-sdk/react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"

interface MemoryResult {
documentId?: string
title?: string
content?: string
url?: string
score?: number
}
import {
type ChatMemoryCard,
memoryResultsFromSearchToolOutput,
} from "@/lib/chat-search-memory-results"
import { isWebSearchToolName } from "@/lib/chat-web-search-tools"
import { MemorySearchResultCard } from "../message/memory-search-result-card"

interface ReasoningStep {
type: string
Expand All @@ -28,7 +24,8 @@ export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
}> = []

for (let i = 0; i < messages.length; i++) {
const message = messages[i]!
const message = messages[i]
if (!message) continue
if (message.role === "user") {
// Find the next assistant message after this user message
const agentMessage = messages
Expand Down Expand Up @@ -76,24 +73,80 @@ export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
message: "Error searching memories",
})
}
return
}

const webSearchPart =
part.type === "dynamic-tool" &&
isWebSearchToolName(
(part as { toolName?: string }).toolName ?? "",
)
? (part as {
type: "dynamic-tool"
toolName: string
state: string
})
: part.type === "tool-web_search" ||
part.type === "tool-google_search"
? (part as { type: string; state: string })
: null

if (webSearchPart) {
if (
webSearchPart.state === "input-available" ||
webSearchPart.state === "input-streaming"
) {
reasoningSteps.push({
type: "web-search",
state: webSearchPart.state,
message: "Searching the web...",
})
} else if (webSearchPart.state === "output-available") {
reasoningSteps.push({
type: "web-search",
state: webSearchPart.state,
message: "Explored the web",
})
} else if (webSearchPart.state === "output-error") {
reasoningSteps.push({
type: "web-search",
state: webSearchPart.state,
message: "Web search failed",
})
}
}
})

const webSourceCount = pair.agentMessage.parts.filter(
(p) => p.type === "source-url",
).length
if (webSourceCount > 0) {
const hasToolWebDone = reasoningSteps.some(
(s) => s.type === "web-search" && s.state === "output-available",
)
if (!hasToolWebDone) {
reasoningSteps.push({
type: "web-sources",
state: "done",
message:
webSourceCount === 1
? "Found 1 web source"
: `Found ${webSourceCount} web sources`,
})
}
}
}

const memoryResults: MemoryResult[] = []
const memoryResults: ChatMemoryCard[] = []
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.push(
...memoryResultsFromSearchToolOutput(part.output),
)
}
})
}
Expand Down Expand Up @@ -133,75 +186,14 @@ export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
)}

{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://"))

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 className="grid grid-cols-2 gap-2 max-h-64 overflow-y-auto items-stretch">
{memoryResults.map((result, idx) => (
<MemorySearchResultCard
key={result.documentId ?? idx}
result={result}
tone="input"
/>
))}
</div>
)}
</div>
Expand Down
Loading
Loading