meeting-recorder-step.tsx25.4 KBView on GitHub
'use client';

import type { ProviderId } from '../../../../server/src/services/integrations/constants';
import { MeetingIntegrationCard } from '@/modules/integrations/meeting-integration-card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2, Copy, Check, Eye, EyeOff } from 'lucide-react';
import { useTRPC } from '@/providers/query-provider';
import { useState, useEffect, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';

interface MeetingRecorderOnboardingStepProps {
  integrationsData:
    | {
        integrations: Array<{
          id: string;
          name: string;
          type: string;
          connected: boolean;
          capabilities?: unknown;
          orgConfig?: {
            type: 'api_key' | 'oauth' | 'manual' | 'service_account';
            userMustAuthenticate: boolean;
            hasOrgCredentials: boolean;
          };
          isPersonalConnection?: boolean;
        }>;
      }
    | undefined;
  session: { user?: { id: string } } | null;
  copyToClipboard: (value: string, id: string) => void;
  copiedValue: string | null;
  selectedProviderId?: string;
}

export function MeetingRecorderOnboardingStep({
  integrationsData,
  session,
  copyToClipboard,
  copiedValue,
  selectedProviderId,
}: MeetingRecorderOnboardingStepProps) {
  const trpc = useTRPC();
  const queryClient = useQueryClient();
  const [formFields, setFormFields] = useState<Record<string, Record<string, string>>>({});
  const [isConnecting, setIsConnecting] = useState<Record<string, boolean>>({});
  const [isActivating, setIsActivating] = useState<Record<string, boolean>>({});
  const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});

  const { mutateAsync: connect } = useMutation(trpc.integrations.connect.mutationOptions());
  const { mutateAsync: activateOrgIntegration } = useMutation(
    trpc.integrations.activateOrgIntegration.mutationOptions(),
  );

  // Filter to only meeting type integrations (optionally narrowed to one provider)
  const meetingProviders = useMemo(() => {
    const all = integrationsData?.integrations.filter((i) => i.type === 'meeting') || [];
    return selectedProviderId ? all.filter((i) => i.id === selectedProviderId) : all;
  }, [integrationsData?.integrations, selectedProviderId]);

  // Initialize form fields for API key providers
  useEffect(() => {
    meetingProviders.forEach((provider) => {
      const capabilities = provider.capabilities as
        | {
            connectionType?: 'oauth' | 'api_key' | 'manual';
            connectionFields?: Array<{
              key=[redacted];
              label: string;
              type: 'text' | 'password';
              placeholder: string;
            }>;
          }
        | undefined;

      if (capabilities?.connectionType === 'api_key' && capabilities.connectionFields) {
        setFormFields((prev) => {
          if (!prev[provider.id]) {
            const initialFields: Record<string, string> = {};
            capabilities.connectionFields!.forEach((field) => {
              initialFields[field.key] = '';
            });
            return { ...prev, [provider.id]: initialFields };
          }
          return prev;
        });
      }
    });
  }, [meetingProviders]);

  // Get first provider as default tab
  const defaultTab = meetingProviders[0]?.id || '';

  if (meetingProviders.length === 0) {
    return (
      <div className="flex items-center justify-center py-8">
        <p className="text-muted-foreground text-sm">No meeting providers available</p>
      </div>
    );
  }

  const handleDirectInputConnect = async (
    providerId: string,
    connectionFields: Array<{
      key=[redacted];
      label: string;
      type: 'text' | 'password';
      placeholder: string;
    }>,
  ) => {
    const providerName = providerId.charAt(0).toUpperCase() + providerId.slice(1);
    const fields = formFields[providerId] || {};

    // Validate all required fields are filled
    const missingFields = connectionFields.filter((field) => !fields[field.key]?.trim());
    if (missingFields.length > 0) {
      toast.error(
        `Please fill in all required fields: ${missingFields.map((f) => f.label).join(', ')}`,
      );
      return;
    }

    try {
      setIsConnecting((prev) => ({ ...prev, [providerId]: true }));
      await connect({
        providerId: providerId as ProviderId,
        connectionParams: fields,
        userId: session?.user?.id,
      });

      setFormFields((prev) => {
        const next = { ...prev };
        delete next[providerId];
        return next;
      });
      setIsConnecting((prev) => ({ ...prev, [providerId]: false }));
      queryClient.invalidateQueries({ queryKey=[redacted] });
    } catch (error) {
      console.error(`[MeetingRecorderOnboardingStep] Error connecting ${providerId}:`, error);
      setIsConnecting((prev) => ({ ...prev, [providerId]: false }));
      toast.error(
        `Failed to connect ${providerName}: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  };

  // Handle activation of org-provisioned integrations
  const handleActivateOrgIntegration = async (providerId: string) => {
    const providerName = providerId.charAt(0).toUpperCase() + providerId.slice(1);

    try {
      setIsActivating((prev) => ({ ...prev, [providerId]: true }));
      const result = await activateOrgIntegration({ providerId });

      if (result.alreadyConnected) {
        toast.info(`${providerName} is already connected`);
      }

      queryClient.invalidateQueries({ queryKey=[redacted] });
    } catch (error) {
      console.error(`[MeetingRecorderOnboardingStep] Error activating ${providerId}:`, error);
      toast.error(
        `Failed to activate ${providerName}: ${error instanceof Error ? error.message : String(error)}`,
      );
    } finally {
      setIsActivating((prev) => ({ ...prev, [providerId]: false }));
    }
  };

  const renderFormField = (
    providerId: string,
    field: {
      key=[redacted];
      label: string;
      type: 'text' | 'password';
      placeholder: string;
    },
  ) => {
    const isPasswordField = field.type === 'password';
    const isApiKeyField = field.key === 'apiKey';
    const fieldValue = formFields[providerId]?.[field.key] || '';

    return (
      <div key=[redacted] className="space-y-2">
        <Label htmlFor={`${providerId}-${field.key}`}>{field.label}</Label>
        <div className="relative">
          <Input
            id={`${providerId}-${field.key}`}
            type={isPasswordField && !showApiKey[providerId] ? 'password' : 'text'}
            placeholder={field.placeholder}
            value={fieldValue}
            onChange={(e) =>
              setFormFields((prev) => ({
                ...prev,
                [providerId]: { ...prev[providerId], [field.key]: e.target.value },
              }))
            }
            disabled={isConnecting[providerId]}
            className={isPasswordField ? 'pr-10' : ''}
          />
          {isPasswordField && isApiKeyField && (
            <Button
              type="button"
              variant="ghost"
              size="icon"
              className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
              onClick={() =>
                setShowApiKey((prev) => ({ ...prev, [providerId]: !prev[providerId] }))
              }
            >
              {showApiKey[providerId] ? (
                <EyeOff className="h-4 w-4 text-gray-500" />
              ) : (
                <Eye className="h-4 w-4 text-gray-500" />
              )}
            </Button>
          )}
        </div>
      </div>
    );
  };

  if (selectedProviderId) {
    const provider = meetingProviders[0];
    if (!provider) return null;
    const capabilities = provider.capabilities as
      | {
          apiKeyInstructions?: string;
          webhookSetupInstructions?: string;
          webhookScope?: 'org' | 'user' | 'none';
          connectionType?: 'oauth' | 'api_key' | 'manual';
          connectionFields?: Array<{
            key=[redacted];
            label: string;
            type: 'text' | 'password';
            placeholder: string;
          }>;
        }
      | undefined;
    const isConnected = provider.connected || false;
    const webhookUrl = `https://api.mail.cedarcopilot.com/webhooks/meeting-notes/${provider.id}`;
    return (
      <div className="space-y-4">
        <div className="space-y-2">
          <div className="flex items-center justify-between">
            <h3 className="text-lg font-medium">Connect {provider.name}</h3>
            <div className="flex items-center gap-2">
              {provider.isPersonalConnection && isConnected && (
                <span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
                  Personal
                </span>
              )}
              {isConnected && (
                <span className="flex items-center gap-2 text-sm font-medium text-green-600">
                  ✓ Connected
                </span>
              )}
            </div>
          </div>
          <p className="text-muted-foreground text-sm">
            {capabilities?.connectionType === 'api_key'
              ? "We'll sync your meetings and create a webhook to capture all your future meetings."
              : 'Add this webhook to your account to sync meeting notes.'}
          </p>
        </div>
        {capabilities?.webhookSetupInstructions &&
          capabilities?.connectionType === 'manual' &&
          capabilities?.webhookScope === 'user' && (
            <div className="bg-muted/30 space-y-4 rounded-lg border p-4">
              <div className="text-muted-foreground whitespace-pre-line text-xs">
                {capabilities.webhookSetupInstructions}
              </div>
              {provider.id === 'granola' && (
                <div className="space-y-2">
                  <Label htmlFor={`user-id-${provider.id}`}>Your User ID</Label>
                  <div className="flex items-center gap-2">
                    <Input id={`user-id-${provider.id}`} readOnly value={session?.user?.id || 'Loading...'} className="font-mono text-xs" />
                    <Button variant="outline" size="icon" disabled={!session?.user?.id} onClick={() => session?.user?.id && copyToClipboard(session.user.id, `userid-${provider.id}`)}>
                      {copiedValue === `userid-${provider.id}` ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
                    </Button>
                  </div>
                </div>
              )}
              <div className="space-y-2">
                <Label htmlFor={`webhook-url-${provider.id}`}>Webhook URL</Label>
                <div className="flex items-center gap-2">
                  <Input id={`webhook-url-${provider.id}`} readOnly value={webhookUrl} className="font-mono text-xs" />
                  <Button variant="outline" size="icon" onClick={() => copyToClipboard(webhookUrl, `webhook-${provider.id}`)}>
                    {copiedValue === `webhook-${provider.id}` ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
                  </Button>
                </div>
              </div>
            </div>
          )}
        {!isConnected && capabilities?.connectionType === 'api_key' && provider.orgConfig?.userMustAuthenticate === false && provider.orgConfig?.hasOrgCredentials && (
          <div className="space-y-4">
            <div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
              <span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">Provided by your organization</span>
              <p className="mt-2 text-sm text-blue-700 dark:text-blue-300">Your organization has configured {provider.name}. Click below to activate your account.</p>
            </div>
            <Button size="lg" className="w-full" onClick={() => handleActivateOrgIntegration(provider.id)} disabled={isActivating[provider.id]}>
              {isActivating[provider.id] ? <><Loader2 className="mr-2 h-5 w-5 animate-spin" /><span>Activating...</span></> : `Activate ${provider.name}`}
            </Button>
          </div>
        )}
        {!isConnected && capabilities?.connectionType === 'api_key' && capabilities.connectionFields && capabilities.connectionFields.length > 0 && (provider.orgConfig?.userMustAuthenticate !== false || !provider.orgConfig?.hasOrgCredentials) && (
          <div className="space-y-4">
            {capabilities.apiKeyInstructions && <div className="bg-muted/30 rounded-lg border p-4"><div className="text-muted-foreground whitespace-pre-line text-xs">{capabilities.apiKeyInstructions}</div></div>}
            {capabilities.connectionFields.map((field) => renderFormField(provider.id, field))}
            <Button size="lg" className="w-full border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50" onClick={() => handleDirectInputConnect(provider.id, capabilities.connectionFields!)} disabled={isConnecting[provider.id]}>
              {isConnecting[provider.id] ? <><Loader2 className="mr-2 h-5 w-5 animate-spin" /><span>Connecting...</span></> : `Connect ${provider.name}`}
            </Button>
          </div>
        )}
        {isConnected && capabilities?.connectionType === 'api_key' && capabilities?.webhookSetupInstructions && capabilities?.webhookScope === 'user' && (
          <div className="bg-muted/30 space-y-4 rounded-lg border p-4">
            <div className="text-muted-foreground whitespace-pre-line text-xs">{capabilities.webhookSetupInstructions}</div>
            <div className="space-y-2">
              <Label htmlFor={`webhook-url-${provider.id}`}>Webhook URL</Label>
              <div className="flex items-center gap-2">
                <Input id={`webhook-url-${provider.id}`} readOnly value={webhookUrl} className="font-mono text-xs" />
                <Button variant="outline" size="icon" onClick={() => copyToClipboard(webhookUrl, `webhook-${provider.id}`)}>
                  {copiedValue === `webhook-${provider.id}` ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
                </Button>
              </div>
            </div>
          </div>
        )}
        {(capabilities?.connectionType !== 'api_key' || isConnected) && <MeetingIntegrationCard providerId={provider.id} userId={session?.user?.id} showManager={false} />}
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <Tabs defaultValue={defaultTab} className="w-full">
        <div className="overflow-x-auto">
          <TabsList>
            {meetingProviders.map((provider) => (
              <TabsTrigger key=[redacted] value={provider.id}>
                {provider.name}
              </TabsTrigger>
            ))}
          </TabsList>
        </div>

        {meetingProviders.map((provider) => {
          const capabilities = provider.capabilities as
            | {
                apiKeyInstructions?: string;
                webhookSetupInstructions?: string;
                webhookScope?: 'org' | 'user' | 'none';
                connectionType?: 'oauth' | 'api_key' | 'manual';
                connectionFields?: Array<{
                  key=[redacted];
                  label: string;
                  type: 'text' | 'password';
                  placeholder: string;
                }>;
              }
            | undefined;
          const isConnected = provider.connected || false;
          const webhookUrl = `https://api.mail.cedarcopilot.com/webhooks/meeting-notes/${provider.id}`;

          return (
            <TabsContent key=[redacted] value={provider.id} className="space-y-4 pt-4">
              <div className="space-y-2">
                <div className="flex items-center justify-between">
                  <h3 className="text-lg font-medium">Connect {provider.name}</h3>
                  <div className="flex items-center gap-2">
                    {provider.isPersonalConnection && isConnected && (
                      <span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
                        Personal
                      </span>
                    )}
                    {isConnected && (
                      <span className="flex items-center gap-2 text-sm font-medium text-green-600">
                        ✓ Connected
                      </span>
                    )}
                  </div>
                </div>
                <p className="text-muted-foreground text-sm">
                  {capabilities?.connectionType === 'api_key'
                    ? "We'll sync your meetings and create a webhook to capture all your future meetings."
                    : 'Add this webhook to your account to sync meeting notes.'}
                </p>
              </div>

              {/* Webhook Setup Instructions (for manual providers with user-level webhooks like Granola) */}
              {capabilities?.webhookSetupInstructions &&
                capabilities?.connectionType === 'manual' &&
                capabilities?.webhookScope === 'user' && (
                  <div className="bg-muted/30 space-y-4 rounded-lg border p-4">
                    <div className="text-muted-foreground whitespace-pre-line text-xs">
                      {capabilities.webhookSetupInstructions}
                    </div>
                    {provider.id === 'granola' && (
                      <div className="space-y-2">
                        <Label htmlFor={`user-id-${provider.id}`}>Your User ID</Label>
                        <div className="flex items-center gap-2">
                          <Input
                            id={`user-id-${provider.id}`}
                            readOnly
                            value={session?.user?.id || 'Loading...'}
                            className="font-mono text-xs"
                          />
                          <Button
                            variant="outline"
                            size="icon"
                            disabled={!session?.user?.id}
                            onClick={() =>
                              session?.user?.id &&
                              copyToClipboard(session.user.id, `userid-${provider.id}`)
                            }
                          >
                            {copiedValue === `userid-${provider.id}` ? (
                              <Check className="h-4 w-4" />
                            ) : (
                              <Copy className="h-4 w-4" />
                            )}
                          </Button>
                        </div>
                      </div>
                    )}
                    {/* Webhook URL - shown with webhook instructions */}
                    <div className="space-y-2">
                      <Label htmlFor={`webhook-url-${provider.id}`}>Webhook URL</Label>
                      <div className="flex items-center gap-2">
                        <Input
                          id={`webhook-url-${provider.id}`}
                          readOnly
                          value={webhookUrl}
                          className="font-mono text-xs"
                        />
                        <Button
                          variant="outline"
                          size="icon"
                          onClick={() => copyToClipboard(webhookUrl, `webhook-${provider.id}`)}
                        >
                          {copiedValue === `webhook-${provider.id}` ? (
                            <Check className="h-4 w-4" />
                          ) : (
                            <Copy className="h-4 w-4" />
                          )}
                        </Button>
                      </div>
                    </div>
                  </div>
                )}

              {/* API Key - Org Provisioned (userMustAuthenticate === false) */}
              {!isConnected &&
                capabilities?.connectionType === 'api_key' &&
                provider.orgConfig?.userMustAuthenticate === false &&
                provider.orgConfig?.hasOrgCredentials && (
                  <div className="space-y-4">
                    <div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
                      <div className="flex items-center gap-2">
                        <span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
                          Provided by your organization
                        </span>
                      </div>
                      <p className="mt-2 text-sm text-blue-700 dark:text-blue-300">
                        Your organization has configured {provider.name}. Click below to activate
                        your account.
                      </p>
                    </div>
                    <Button
                      size="lg"
                      className="w-full"
                      onClick={() => handleActivateOrgIntegration(provider.id)}
                      disabled={isActivating[provider.id]}
                    >
                      {isActivating[provider.id] ? (
                        <>
                          <Loader2 className="mr-2 h-5 w-5 animate-spin" />
                          <span>Activating...</span>
                        </>
                      ) : (
                        `Activate ${provider.name}`
                      )}
                    </Button>
                  </div>
                )}

              {/* API Key Form - shown for API key providers when user must authenticate */}
              {!isConnected &&
                capabilities?.connectionType === 'api_key' &&
                capabilities.connectionFields &&
                capabilities.connectionFields.length > 0 &&
                (provider.orgConfig?.userMustAuthenticate !== false ||
                  !provider.orgConfig?.hasOrgCredentials) && (
                  <div className="space-y-4">
                    {capabilities.apiKeyInstructions && (
                      <div className="bg-muted/30 rounded-lg border p-4">
                        <div className="text-muted-foreground whitespace-pre-line text-xs">
                          {capabilities.apiKeyInstructions}
                        </div>
                      </div>
                    )}
                    {capabilities.connectionFields.map((field) =>
                      renderFormField(provider.id, field),
                    )}
                    <Button
                      size="lg"
                      className="w-full border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50"
                      onClick={() =>
                        handleDirectInputConnect(provider.id, capabilities.connectionFields!)
                      }
                      disabled={isConnecting[provider.id]}
                    >
                      {isConnecting[provider.id] ? (
                        <>
                          <Loader2 className="mr-2 h-5 w-5 animate-spin" />
                          <span>Connecting...</span>
                        </>
                      ) : (
                        `Connect ${provider.name}`
                      )}
                    </Button>
                  </div>
                )}

              {/* Webhook Setup Instructions for API key providers with user-level webhooks - shown after connection */}
              {isConnected &&
                capabilities?.connectionType === 'api_key' &&
                capabilities?.webhookSetupInstructions &&
                capabilities?.webhookScope === 'user' && (
                  <div className="bg-muted/30 space-y-4 rounded-lg border p-4">
                    <div className="text-muted-foreground whitespace-pre-line text-xs">
                      {capabilities.webhookSetupInstructions}
                    </div>
                    <div className="space-y-2">
                      <Label htmlFor={`webhook-url-${provider.id}`}>Webhook URL</Label>
                      <div className="flex items-center gap-2">
                        <Input
                          id={`webhook-url-${provider.id}`}
                          readOnly
                          value={webhookUrl}
                          className="font-mono text-xs"
                        />
                        <Button
                          variant="outline"
                          size="icon"
                          onClick={() => copyToClipboard(webhookUrl, `webhook-${provider.id}`)}
                        >
                          {copiedValue === `webhook-${provider.id}` ? (
                            <Check className="h-4 w-4" />
                          ) : (
                            <Copy className="h-4 w-4" />
                          )}
                        </Button>
                      </div>
                    </div>
                  </div>
                )}

              {/* Show MeetingIntegrationCard for OAuth providers or when API key provider is connected */}
              {(capabilities?.connectionType !== 'api_key' || isConnected) && (
                <MeetingIntegrationCard
                  providerId={provider.id}
                  userId={session?.user?.id}
                  showManager={false}
                />
              )}
            </TabsContent>
          );
        })}
      </Tabs>
    </div>
  );
}