Skip to content
Closed
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
14 changes: 14 additions & 0 deletions components/VercelChat/ToolComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ import {
} from "./tools/files/UpdateFileResult";
import GoogleSheetsLoginResult from "./tools/googleSheets/GoogleSheetsLoginResult";
import GoogleSheetsLoginLoading from "./tools/googleSheets/GoogleSheetsLoginLoading";
import GoogleDriveLoginResult from "./tools/googleDrive/GoogleDriveLoginResult";
import GoogleDriveLoginLoading from "./tools/googleDrive/GoogleDriveLoginLoading";
import { TextContent } from "@modelcontextprotocol/sdk/types.js";

type CallToolResult = {
Expand Down Expand Up @@ -283,6 +285,12 @@ export function getToolCallComponent(part: ToolUIPart) {
<GoogleSheetsLoginLoading />
</div>
);
} else if (toolName === "googleDriveLoginTool") {
return (
<div key={toolCallId}>
<GoogleDriveLoginLoading />
</div>
);
}

// Default for other tools
Expand Down Expand Up @@ -548,6 +556,12 @@ export function getToolResultComponent(part: ToolUIPart | DynamicToolUIPart) {
<GoogleSheetsLoginResult />
</div>
);
} else if (toolName === "googleDriveLoginTool") {
return (
<div key={toolCallId}>
<GoogleDriveLoginResult />
</div>
);
}

// Default generic result for other tools
Expand Down
18 changes: 18 additions & 0 deletions components/VercelChat/tools/composio/ComposioLoginLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Loader } from "lucide-react";

interface ComposioLoginLoadingProps {
toolkitName: string;
}

export function ComposioLoginLoading({
toolkitName,
}: ComposioLoginLoadingProps) {
return (
<div className="flex items-center gap-1 py-1 px-2 bg-muted/50 rounded-sm border border-border w-fit text-xs text-muted-foreground">
<Loader className="h-3 w-3 animate-spin text-foreground" />
<span>Initializing {toolkitName} login...</span>
</div>
);
}

export default ComposioLoginLoading;
68 changes: 68 additions & 0 deletions components/VercelChat/tools/composio/ComposioLoginResult.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY principle - This component should already exist from google sheets login.

  • why are you recreating existing components?

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import React from "react";
import { LucideIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUserProvider } from "@/providers/UserProvder";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find provider files with similar names
fd -i "userprov" providers/

Repository: Recoupable-com/Recoup-Chat

Length of output: 95


🏁 Script executed:

# Also check the full path for the file in question
fd -t f "ComposioLoginResult.tsx"

Repository: Recoupable-com/Recoup-Chat

Length of output: 130


🏁 Script executed:

# Search for UserProvider/UserProvder in providers directory
rg "UserProv" providers/ -t ts -t tsx

Repository: Recoupable-com/Recoup-Chat

Length of output: 97


🏁 Script executed:

# Let's read the ComposioLoginResult.tsx file to verify other issues
cat -n components/VercelChat/tools/composio/ComposioLoginResult.tsx

Repository: Recoupable-com/Recoup-Chat

Length of output: 2411


🏁 Script executed:

# Also verify the content of the provider file
cat -n providers/UserProvder.tsx

Repository: Recoupable-com/Recoup-Chat

Length of output: 950


Remove hardcoded "Google" text and use cn() for class merging.

The notice at line 60-62 assumes all toolkits redirect to Google, which breaks reusability for non-Google Composio integrations. Make the redirect service dynamic via props or pass it through context.

Additionally, line 46 uses string concatenation for className instead of the cn() utility per coding guidelines. Replace className={w-full ${buttonColor} text-white} with proper class merging using cn().

Note: The import path UserProvder correctly matches the actual provider filename, though the provider file itself has a typo in its name (UserProvder.tsx should be UserProvider.tsx).

🤖 Prompt for AI Agents
In @components/VercelChat/tools/composio/ComposioLoginResult.tsx at line 6, The
ComposioLoginResult component hardcodes "Google" and uses string-concatenated
classNames; make the redirect service dynamic (add a prop like serviceName or
redirectService or read it from useUserProvider()) and replace the literal
"Google" text with that prop/value throughout the component so non-Google
integrations work; also replace the className usage className={`w-full
${buttonColor} text-white`} with the cn() utility (e.g., cn('w-full',
buttonColor, 'text-white')) to merge classes properly while keeping the existing
import of useUserProvider (whose filename currently has the typo UserProvder)
unchanged.

import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay";

interface ComposioLoginResultProps {
toolkitName: string;
description: string;
icon: LucideIcon;
buttonColor: string;
isLoading: boolean;
onLogin: () => void;
}

export function ComposioLoginResult({
toolkitName,
description,
icon: Icon,
buttonColor,
isLoading,
onLogin,
}: ComposioLoginResultProps) {
const { userData } = useUserProvider();
const accountId = userData?.account_id;

return (
<div className="flex flex-col space-y-3 p-4 rounded-lg bg-muted border border-border my-2 max-w-md">
<div className="flex items-center space-x-2">
<Icon className="h-5 w-5 text-muted-foreground" />
<span className="font-medium text-foreground">
{toolkitName} Access Required
</span>
</div>

<p className="text-sm text-muted-foreground">
Connect your {toolkitName} account to enable {description}.
</p>

{accountId && <AccountIdDisplay accountId={accountId} label="Account" />}

<Button
onClick={onLogin}
className={`w-full ${buttonColor} text-white`}
size="sm"
disabled={isLoading || !accountId}
>
{isLoading ? (
<span>Connecting...</span>
) : (
<>
<Icon className="h-4 w-4 mr-2" />
Connect {toolkitName}
</>
)}
</Button>

<p className="text-xs text-muted-foreground text-center">
You&apos;ll be redirected to Google to authorize access to your{" "}
{toolkitName} for this account.
</p>
</div>
);
}

export default ComposioLoginResult;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ComposioLoginLoading } from "../composio/ComposioLoginLoading";

export function GoogleDriveLoginLoading() {
return <ComposioLoginLoading toolkitName="Google Drive" />;
}

export default GoogleDriveLoginLoading;
22 changes: 22 additions & 0 deletions components/VercelChat/tools/googleDrive/GoogleDriveLoginResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { HardDrive } from "lucide-react";
import { useGoogleDriveLogin } from "@/hooks/useGoogleDriveLogin";
import { ComposioLoginResult } from "../composio/ComposioLoginResult";

export function GoogleDriveLoginResult() {
const { isLoading, handleLogin } = useGoogleDriveLogin();

return (
<ComposioLoginResult
toolkitName="Google Drive"
description="uploading, downloading, and managing files"
icon={HardDrive}
buttonColor="bg-blue-600 hover:bg-blue-700"
isLoading={isLoading}
onLogin={handleLogin}
/>
);
}

export default GoogleDriveLoginResult;
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { Loader } from "lucide-react";
import { ComposioLoginLoading } from "../composio/ComposioLoginLoading";

export function GoogleSheetsLoginLoading() {
return (
<div className="flex items-center gap-1 py-1 px-2 bg-muted/50 rounded-sm border border-border w-fit text-xs text-muted-foreground">
<Loader className="h-3 w-3 animate-spin text-foreground" />
<span>Initializing Google Sheets login...</span>
</div>
);
return <ComposioLoginLoading toolkitName="Google Sheets" />;
}

export default GoogleSheetsLoginLoading;
Original file line number Diff line number Diff line change
@@ -1,56 +1,21 @@
"use client";

import React from "react";
import { FileSpreadsheet } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useUserProvider } from "@/providers/UserProvder";
import AccountIdDisplay from "@/components/ArtistSetting/AccountIdDisplay";
import { useGoogleSheetsLogin } from "@/hooks/useGoogleSheetsLogin";
import { ComposioLoginResult } from "../composio/ComposioLoginResult";

export function GoogleSheetsLoginResult() {
const { userData } = useUserProvider();
const accountId = userData?.account_id;
const { isLoading, handleLogin } = useGoogleSheetsLogin();

return (
<div className="flex flex-col space-y-3 p-4 rounded-lg bg-muted border border-border my-2 max-w-md">
<div className="flex items-center space-x-2">
<FileSpreadsheet className="h-5 w-5 text-muted-foreground" />
<span className="font-medium text-foreground">
Google Sheets Access Required
</span>
</div>

<p className="text-sm text-muted-foreground">
Connect your Google Sheets account to enable reading and writing to
spreadsheets.
</p>

{accountId && <AccountIdDisplay accountId={accountId} label="Account" />}

<Button
onClick={handleLogin}
className="w-full bg-green-600 hover:bg-green-700 text-white"
size="sm"
disabled={isLoading || !accountId}
>
{isLoading ? (
<>
<span className="mr-2">Connecting...</span>
</>
) : (
<>
<FileSpreadsheet className="h-4 w-4 mr-2" />
Connect Google Sheets
</>
)}
</Button>

<p className="text-xs text-muted-foreground text-center">
You&apos;ll be redirected to Google to authorize access to your Google
Sheets for this account.
</p>
</div>
<ComposioLoginResult
toolkitName="Google Sheets"
description="reading and writing to spreadsheets"
icon={FileSpreadsheet}
buttonColor="bg-green-600 hover:bg-green-700"
isLoading={isLoading}
onLogin={handleLogin}
/>
);
}

Expand Down
59 changes: 59 additions & 0 deletions hooks/useComposioLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from "react";
import { useUserProvider } from "@/providers/UserProvder";
import { useVercelChatContext } from "@/providers/VercelChatProvider";
import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText";
import { fetchConnectedAccountsRefresh } from "@/lib/composio/fetchConnectedAccountsRefresh";
import {
ComposioToolkitKey,
getToolkitConfig,
} from "@/lib/composio/toolkits";
import { toast } from "sonner";

/**
* Hook for handling Composio toolkit login.
*
* @param toolkitKey - The toolkit to login to (e.g., "GOOGLE_SHEETS", "GOOGLE_DRIVE")
*/
export function useComposioLogin(toolkitKey: ComposioToolkitKey) {
const [isLoading, setIsLoading] = useState(false);
const { userData } = useUserProvider();
const { messages } = useVercelChatContext();
const accountId = userData?.account_id as string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add null safety for accountId.

The accountId is cast to string without checking if userData?.account_id is defined. This could lead to "undefined" being passed as a string to API calls, causing cryptic errors.

🔒 Proposed fix with early validation
  const { userData } = useUserProvider();
  const { messages } = useVercelChatContext();
- const accountId = userData?.account_id as string;
+ const accountId = userData?.account_id;
  const config = getToolkitConfig(toolkitKey);

  const handleLogin = async () => {
+   if (!accountId) {
+     toast.error("Account information is missing. Please refresh the page.");
+     return;
+   }
+
    const latestUserMessageText = getLatestUserMessageText(messages);

Additionally, consider reflecting this state in the consuming components by including accountId in the return value so buttons can be properly disabled when the account is not available.

🤖 Prompt for AI Agents
In @hooks/useComposioLogin.ts at line 21, The hook useComposioLogin currently
force-casts userData?.account_id to a string via accountId =
userData?.account_id as string; — change this to null-safe handling: validate
that userData and userData.account_id exist (e.g., derive accountId as string |
null or undefined instead of forcing a cast), return that accountId from the
hook, and update any API call paths inside the hook (and consuming components)
to early-return or disable operations when accountId is missing; ensure the
returned shape includes accountId so callers can disable buttons when no account
is available.

const config = getToolkitConfig(toolkitKey);

const handleLogin = async () => {
const latestUserMessageText = getLatestUserMessageText(messages);
// Use current origin so it works in both local dev and production
const baseUrl = typeof window !== "undefined" ? window.location.origin : "https://chat.recoupable.com";
Comment on lines +26 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS principle - why not just always use https://chat.recoupable.com?

const redirectUrl = `${baseUrl}?q=${encodeURIComponent(
latestUserMessageText
)}`;

setIsLoading(true);

try {
const data = await fetchConnectedAccountsRefresh(toolkitKey, {
accountId,
redirectUrl,
});

if (data.redirect_url) {
// Use location.href instead of window.open to avoid popup blocker
// since this is called after an async operation
window.location.href = data.redirect_url;
}
} catch (error) {
toast.error(
`Failed to initiate ${config.name} login. Please try again.`
);
} finally {
setIsLoading(false);
}
};

return {
isLoading,
handleLogin,
toolkitName: config.name,
};
}
8 changes: 8 additions & 0 deletions hooks/useGoogleDriveLogin.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS principle - is this hook really necessary?

  • why not just directly import useComposioLogin wherever you're using useGoogleDriveLogin?

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useComposioLogin } from "./useComposioLogin";

/**
* Hook for Google Drive login.
*/
export function useGoogleDriveLogin() {
return useComposioLogin("GOOGLE_DRIVE");
}
45 changes: 5 additions & 40 deletions hooks/useGoogleSheetsLogin.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS principle - is this hook really necessary?

  • why not just directly import useComposioLogin wherever you're using useGoogleSheetsLogin?

Original file line number Diff line number Diff line change
@@ -1,43 +1,8 @@
import { useState } from "react";
import { useUserProvider } from "@/providers/UserProvder";
import { useVercelChatContext } from "@/providers/VercelChatProvider";
import getLatestUserMessageText from "@/lib/messages/getLatestUserMessageText";
import { fetchConnectedAccountsRefresh } from "@/lib/composio/googleSheets/fetchConnectedAccountsRefresh";
import { toast } from "sonner";
import { useComposioLogin } from "./useComposioLogin";

/**
* Hook for Google Sheets login.
*/
export function useGoogleSheetsLogin() {
const [isLoading, setIsLoading] = useState(false);
const { userData } = useUserProvider();
const { messages } = useVercelChatContext();
const accountId = userData?.account_id as string;

const handleLogin = async () => {
const latestUserMessageText = getLatestUserMessageText(messages);
const redirectUrl = `https://chat.recoupable.com?q=${encodeURIComponent(
latestUserMessageText
)}`;

setIsLoading(true);

try {
const data = await fetchConnectedAccountsRefresh({
accountId,
redirectUrl,
});

if (data.redirect_url) {
window.open(data.redirect_url, "_blank", "noopener,noreferrer");
}
} catch (error) {
console.error("Error initiating Google Sheets login:", error);
toast.error("Failed to initiate Google Sheets login. Please try again.");
} finally {
setIsLoading(false);
}
};

return {
isLoading,
handleLogin,
};
return useComposioLogin("GOOGLE_SHEETS");
}
Loading
Loading