- {[0, 25, 50, 75, 100].map(percent => (
-
-
-
- {percent}%
+
+ {[0, 25, 50, 75, 100].map(percent => {
+ let leftPosition = `${percent}%`;
+ if (percent === 0) leftPosition = `8px`;
+ if (percent === 100) leftPosition = `calc(100% - 8px)`;
+ return (
+
+
= percent
+ ? "bg-indigo-500 dark:bg-indigo-400"
+ : "bg-indigo-200 dark:bg-indigo-700"
+ }`}
+ >
+
= percent
+ ? "text-indigo-700 dark:text-indigo-300 font-medium"
+ : "text-indigo-400 dark:text-indigo-500"
+ }`}
+ >
+ {percent}%
+
-
- ))}
+ );
+ })}
)}
- {error &&
{error}
}
+ {error && (
+
+ {error}
+
+ )}
diff --git a/src/components/chat/tools/lifi-widget.tsx b/src/components/chat/tools/lifi-widget.tsx
index 4440dc1f..64250fcb 100644
--- a/src/components/chat/tools/lifi-widget.tsx
+++ b/src/components/chat/tools/lifi-widget.tsx
@@ -14,6 +14,7 @@ import { base } from "viem/chains";
import { useChainId } from "wagmi";
import { Chain, chainIdToName, chainNameToChain } from "@/lib/chains";
+import { isToolAborted } from "@/lib/utils";
import { useChat } from "@/contexts/chat-context";
@@ -22,10 +23,7 @@ export const BridgeCompletedCard = ({ result }: { result: any }) => {
let parsedResult;
try {
const isAborted =
- (typeof result === "object" &&
- result !== null &&
- "error" in result &&
- result.error === "Operation aborted by user") ||
+ isToolAborted(result) ||
result?.content?.[0]?.text === "Operation aborted by user";
if (isAborted) {
return (
diff --git a/src/components/chat/tools/yield-opportunities-card.tsx b/src/components/chat/tools/yield-opportunities-card.tsx
index 79e2ddc6..c8c2772b 100644
--- a/src/components/chat/tools/yield-opportunities-card.tsx
+++ b/src/components/chat/tools/yield-opportunities-card.tsx
@@ -1,3 +1,5 @@
+import React, { ChangeEvent } from "react";
+
import Image from "next/image";
import { ExternalLink, Info } from "lucide-react";
@@ -87,7 +89,7 @@ export function YieldOpportunitiesCard({
messageMode,
}: YieldOpportunitiesCardProps) {
const isMobile = useMediaQuery("(max-width: 768px)");
- const { activeMode } = useChat();
+ const { activeMode, setActiveMode, handleInputChange } = useChat();
const mode = messageMode || activeMode;
const formatFilters = () => {
@@ -159,6 +161,28 @@ export function YieldOpportunitiesCard({
);
};
+ const handleVaultClick = (opportunity: YieldOpportunity) => {
+ const vaultDetails = {
+ name: opportunity.name,
+ protocol: opportunity.protocol,
+ chain: opportunity.chain,
+ asset: opportunity.assetSymbol,
+ apy: opportunity.apy,
+ tvl: opportunity.tvlUsd,
+ vaultAddress: opportunity.vaultAddress,
+ link: opportunity.link,
+ };
+ console.log("Vault clicked - Full opportunity data:", opportunity);
+ console.log("Vault details:", vaultDetails);
+
+ const userMessage = `Deposit ${opportunity.name || opportunity.protocol} vault on ${opportunity.chain} for ${opportunity.assetSymbol}`;
+ handleInputChange({
+ target: { value: userMessage, vaultDetails: vaultDetails },
+ } as ChangeEvent
& { target: { vaultDetails: any } });
+
+ setActiveMode("sentinel");
+ };
+
return (
@@ -200,8 +224,9 @@ export function YieldOpportunitiesCard({
{yieldData.opportunities.map((opp, idx) => (
handleVaultClick(opp)}
className={cn(
- "group relative grid gap-2 py-2 text-sm border-b border-gray-100 dark:border-slate-800 hover:bg-gray-50 dark:hover:bg-slate-900/50 transition-colors",
+ "group relative grid gap-2 py-2 text-sm border-b border-gray-100 dark:border-slate-800 hover:bg-gray-50 dark:hover:bg-slate-900/50 transition-colors cursor-pointer",
isMobile ? "grid-cols-5" : "grid-cols-10"
)}
>
diff --git a/src/components/layout/left-panel-toggle-buttons.tsx b/src/components/layout/left-panel-toggle-buttons.tsx
index 3f52261d..f39a3274 100644
--- a/src/components/layout/left-panel-toggle-buttons.tsx
+++ b/src/components/layout/left-panel-toggle-buttons.tsx
@@ -73,7 +73,7 @@ export default function LeftPanelToggleButtons({
{label}
diff --git a/src/constants/tools.ts b/src/constants/tools.ts
index ac3938e7..6c6fdb46 100644
--- a/src/constants/tools.ts
+++ b/src/constants/tools.ts
@@ -125,6 +125,10 @@ export const TOOL_INFO = {
},
} as const;
-export const SIDEBAR_HIDDEN_TOOLS: string[] = ["NeoSearch", "getDesiredChain"];
+export const SIDEBAR_HIDDEN_TOOLS: string[] = [
+ "NeoSearch",
+ "getDesiredChain",
+ "getAmount",
+];
export const CHAT_HIDDEN_TOOLS: string[] = [];
diff --git a/src/contexts/chat-context.tsx b/src/contexts/chat-context.tsx
index e0e71b9f..e5fcb29f 100644
--- a/src/contexts/chat-context.tsx
+++ b/src/contexts/chat-context.tsx
@@ -319,10 +319,6 @@ export function ChatProvider({ children }: ChatProviderProps) {
// 2. OR user is not logged in at all (address is undefined)
isReadOnly =
chatIsPublic && (!address || chat.wallet_address !== address);
-
- console.log(
- `Loaded chat "${chatTitle}" with ${chatMessages.length} messages (read-only: ${isReadOnly}, public: ${chatIsPublic})`
- );
setChatNotFound(false);
} else {
setChatNotFound(true);
diff --git a/src/hooks/useMessageQuota.ts b/src/hooks/useMessageQuota.ts
new file mode 100644
index 00000000..bef7b9be
--- /dev/null
+++ b/src/hooks/useMessageQuota.ts
@@ -0,0 +1,56 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+
+import { useAccount } from "wagmi";
+
+import { supabaseReadOnly } from "@/lib/supabaseClient";
+import { UserQuota, getUserQuota } from "@/lib/userManager";
+
+import { useChat } from "@/contexts/chat-context";
+
+export function useMessageQuota() {
+ const [quota, setQuota] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [isQuotaExceeded, setIsQuotaExceeded] = useState(false);
+ const { address, isConnected } = useAccount();
+ const { messages, isLoading } = useChat();
+
+ const fetchQuota = useCallback(async () => {
+ setLoading(true);
+
+ if (!address || !isConnected) {
+ setIsQuotaExceeded(false);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const userQuota = await getUserQuota(supabaseReadOnly, address);
+ setQuota(userQuota);
+ setIsQuotaExceeded(userQuota.remaining <= 0);
+ } catch (error) {
+ console.error("Error fetching message quota:", error);
+ setIsQuotaExceeded(false);
+ } finally {
+ setLoading(false);
+ }
+ }, [address, isConnected]);
+
+ useEffect(() => {
+ fetchQuota();
+ }, [fetchQuota]);
+
+ useEffect(() => {
+ if (!isLoading) {
+ fetchQuota();
+ }
+ }, [messages.length, isLoading, fetchQuota]);
+
+ return {
+ quota,
+ loading,
+ isQuotaExceeded,
+ fetchQuota,
+ };
+}
diff --git a/src/hooks/useUnifiedChat.ts b/src/hooks/useUnifiedChat.ts
index c6fab25c..d388a8b6 100644
--- a/src/hooks/useUnifiedChat.ts
+++ b/src/hooks/useUnifiedChat.ts
@@ -10,7 +10,11 @@ import {
getErrorTypeFromStatus,
isActionableError,
} from "@/lib/errors";
-import { getCurrentModeFromStorage } from "@/lib/utils";
+import {
+ getCurrentModeFromStorage,
+ hasToolCompletedSuccessfully,
+ isToolAborted,
+} from "@/lib/utils";
import { useSplitLayout } from "@/contexts/split-layout-context";
import { useTab } from "@/contexts/tab-context";
@@ -402,7 +406,9 @@ export function useUnifiedChat({
body: {
address,
id,
- //searchType: initialSearchType,
+ searchType:
+ initialSearchType ||
+ (activeMode === "morpheus" ? "morpheus-search" : "sentinel-mode"),
},
streamProtocol: "data",
onResponse: async response => {
@@ -993,17 +999,27 @@ export function useUnifiedChat({
}
}, [messages, handleInputChange, originalHandleSubmit, clearError]);
+ const [currentVaultDetails, setCurrentVaultDetails] =
+ React.useState(null);
+
const handleInputChangeWrapper = React.useCallback(
- (value: string) => {
+ (value: any) => {
clearError();
lastActionAppendedErrorRef.current = false;
-
- handleInputChange({
- target: { value },
- } as ChangeEvent);
+ if (value.target?.vaultDetails) {
+ setCurrentVaultDetails(value.target.vaultDetails);
+ handleInputChange({
+ target: { value: value.target.value },
+ } as ChangeEvent);
+ } else {
+ handleInputChange({
+ target: { value },
+ } as ChangeEvent);
+ }
},
[handleInputChange, clearError]
);
+
const createFormEvent = () => {
return new Event("submit") as unknown as FormEvent;
};
@@ -1043,6 +1059,9 @@ export function useUnifiedChat({
| { preventDefault?: () => void },
options?: any
) => {
+ console.log(
+ `[wrappedHandleSubmit - ID: ${id}] Called. Current status: ${status}, isGenerating: ${isGenerating}, hasPendingTools: ${hasPendingTools}`
+ );
clearError();
lastActionAppendedErrorRef.current = false;
navigatedToRootRef.current = false;
@@ -1051,13 +1070,6 @@ export function useUnifiedChat({
const currentSearchType =
currentActiveMode === "morpheus" ? "morpheus-search" : "sentinel-mode";
- console.log(
- `[wrappedHandleSubmit - ID: ${id}] Active mode at submission time: ${currentActiveMode}`
- );
- console.log(
- `[wrappedHandleSubmit - ID: ${id}] Determined searchType for API call: ${currentSearchType}`
- );
-
if (currentActiveMode === "morpheus") {
console.log(
`[wrappedHandleSubmit - ID: ${id}] Setting isMorpheusSearchRef to true`
@@ -1071,22 +1083,43 @@ export function useUnifiedChat({
}
setIsGenerating(true);
-
+ console.log(
+ `[wrappedHandleSubmit - ID: ${id}] Set isGenerating to true.`
+ );
const dynamicBody = {
address: address,
id: id,
searchType: currentSearchType,
+ vaultDetails: currentVaultDetails,
+ ...(options?.body || {}),
};
- return originalHandleSubmit(event, {
+ console.log(
+ `[wrappedHandleSubmit - ID: ${id}] Calling originalHandleSubmit with body:`,
+ dynamicBody
+ );
+ const submitResult = originalHandleSubmit(event, {
...options,
- body: {
- ...(options?.body || {}),
- ...dynamicBody,
- },
+ body: dynamicBody,
});
+ setCurrentVaultDetails(null);
+
+ console.log(
+ `[wrappedHandleSubmit - ID: ${id}] Called originalHandleSubmit.`
+ );
+ return submitResult;
},
- [originalHandleSubmit, clearError]
+ [
+ originalHandleSubmit,
+ clearError,
+ activeMode,
+ address,
+ id,
+ setIsGenerating,
+ status,
+ isGenerating,
+ currentVaultDetails,
+ ]
);
const isToolPending = useCallback((toolInvocation: ToolInvocation) => {
@@ -1097,14 +1130,11 @@ export function useUnifiedChat({
return false;
}
- if (
- "result" in toolInvocation &&
- toolInvocation.result &&
- typeof toolInvocation.result === "object" &&
- "error" in toolInvocation.result &&
- (toolInvocation.result.error === "Operation aborted by user" ||
- toolInvocation.result.error === "All operations aborted by user")
- ) {
+ if (isToolAborted(toolInvocation)) {
+ return false;
+ }
+
+ if (hasToolCompletedSuccessfully(toolInvocation)) {
return false;
}
@@ -1130,7 +1160,7 @@ export function useUnifiedChat({
}, [messages, isToolPending]);
const sendMessage = useCallback(
- (message: string) => {
+ (message: string, payload?: Record) => {
if (hasPendingTools) {
return;
}
@@ -1146,7 +1176,7 @@ export function useUnifiedChat({
setIsGenerating(true);
handleInputChangeWrapper(message);
- wrappedHandleSubmit(createFormEvent());
+ wrappedHandleSubmit(createFormEvent(), { body: payload });
},
[
wrappedHandleSubmit,
@@ -1155,6 +1185,7 @@ export function useUnifiedChat({
clearError,
hasPendingTools,
setIsAborting,
+ setIsGenerating,
]
);
@@ -1189,17 +1220,6 @@ export function useUnifiedChat({
}
}
- tools.forEach(tool => {
- if (
- isToolPending(tool) &&
- !pendingTools.some(t => t.toolCallId === tool.toolCallId)
- ) {
- pendingTools.push({
- toolCallId: tool.toolCallId,
- });
- }
- });
-
return pendingTools;
}, [messages, tools, isToolPending]);
@@ -1221,13 +1241,22 @@ export function useUnifiedChat({
if (pendingTools.length > 0) {
pendingTools.forEach(tool => {
- addToolResult({
- toolCallId: tool.toolCallId,
- result: { error: "All operations aborted by user" },
- });
+ const toolInvocation = tools.find(
+ t => t.toolCallId === tool.toolCallId
+ );
+
+ const hasCompletedResult =
+ toolInvocation && hasToolCompletedSuccessfully(toolInvocation);
+
+ if (!hasCompletedResult) {
+ addToolResult({
+ toolCallId: tool.toolCallId,
+ result: { error: "All operations aborted by user" },
+ });
+ }
});
}
- }, [findPendingTools, addToolResult]);
+ }, [findPendingTools, addToolResult, tools]);
const cleanupAfterAbort = useCallback(() => {
isMorpheusSearchRef.current = false;
@@ -1260,9 +1289,11 @@ export function useUnifiedChat({
const abortStream = React.useCallback(() => {
setIsAborting(true);
- stopStream();
abortAllTools();
- setTimeout(cleanupAfterAbort, 1000);
+ setTimeout(() => {
+ stopStream();
+ setTimeout(cleanupAfterAbort, 300);
+ }, 100);
}, [setIsAborting, stopStream, abortAllTools, cleanupAfterAbort]);
const customReload = useCallback(() => {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index c23483fa..7c16129e 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -86,6 +86,38 @@ export const formatLargeNumber = (value: number | string): string => {
* @param options
* @returns
*/
+/**
+ * Check if a tool invocation was aborted by the user
+ * @param toolInvocation The tool invocation object to check
+ * @returns Boolean indicating if the tool was aborted
+ */
+export const isToolAborted = (toolInvocation: any): boolean => {
+ if (!("result" in toolInvocation)) return false;
+
+ return (
+ toolInvocation.result &&
+ typeof toolInvocation.result === "object" &&
+ "error" in toolInvocation.result &&
+ (toolInvocation.result.error === "Operation aborted by user" ||
+ toolInvocation.result.error === "All operations aborted by user")
+ );
+};
+
+/**
+ * Check if a tool invocation has a valid completed result
+ * @param toolInvocation The tool invocation object to check
+ * @returns Boolean indicating if the tool has a valid result (completed successfully)
+ */
+export const hasToolCompletedSuccessfully = (toolInvocation: any): boolean => {
+ if (!("result" in toolInvocation)) return false;
+
+ return (
+ toolInvocation.result &&
+ typeof toolInvocation.result === "object" &&
+ !("error" in toolInvocation.result)
+ );
+};
+
export const formatNumber = (
value: string | number | undefined | null,
decimals: number = 2, // Default decimals