From 48548684c0c05f3db7363da1c5936e0b52c5cad1 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 25 Jul 2024 15:55:33 +0530 Subject: [PATCH] Add card to connect Whatsapp to Khoj on settings page --- src/interface/web/app/settings/page.tsx | 103 +++++++++++++++++- src/interface/web/components/ui/input-otp.tsx | 71 ++++++++++++ src/interface/web/package.json | 2 + src/interface/web/tailwind.config.ts | 5 + src/interface/web/yarn.lock | 10 ++ 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/interface/web/components/ui/input-otp.tsx diff --git a/src/interface/web/app/settings/page.tsx b/src/interface/web/app/settings/page.tsx index f8c6611e..e249a77a 100644 --- a/src/interface/web/app/settings/page.tsx +++ b/src/interface/web/app/settings/page.tsx @@ -6,6 +6,9 @@ import { Suspense, useEffect, useState } from "react"; import { useToast } from "@/components/ui/use-toast" import { useUserConfig, ModelOptions, UserConfig } from "../common/auth"; + +import { isValidPhoneNumber } from 'libphonenumber-js'; + import { Button } from "@/components/ui/button"; import { Card, @@ -44,13 +47,17 @@ import { CheckCircle, NotionLogo, GithubLogo, - Files + Files, + WhatsappLogo, + ExclamationMark } from "@phosphor-icons/react"; import NavMenu from "../components/navMenu/navMenu"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; import Loading from "../components/loading/loading"; import { ExternalLinkIcon } from "lucide-react"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp"; +import { Input } from "@/components/ui/input"; interface DropdownComponentProps { @@ -158,6 +165,12 @@ export const useApiKeys = () => { }; }; +enum PhoneNumberValidationState { + Setup = "setup", + SendOTP = "otp", + VerifyOTP = "verify", + Verified = "verified", +} export default function SettingsView() { const [title, setTitle] = useState("Settings"); @@ -165,10 +178,24 @@ export default function SettingsView() { const { apiKeys, generateAPIKey, copyAPIKey, deleteAPIKey } = useApiKeys(); const initialUserConfig = useUserConfig(true); const [userConfig, setUserConfig] = useState(null); + const [number, setNumber] = useState(undefined); + const [otp, setOTP] = useState(""); + const [isValidNumber, setIsValidNumber] = useState(false); + const [numberValidationState, setNumberValidationState] = useState(PhoneNumberValidationState.Verified); const { toast } = useToast(); const cardClassName = "w-1/3 grid grid-flow-column border border-gray-300 shadow-md rounded-lg"; - useEffect(() => setUserConfig(initialUserConfig), [initialUserConfig]); + useEffect(() => { + setUserConfig(initialUserConfig); + setNumber(initialUserConfig?.phone_number); + setNumberValidationState( + initialUserConfig?.is_phone_number_verified + ? PhoneNumberValidationState.Verified + : initialUserConfig?.phone_number + ? PhoneNumberValidationState.SendOTP + : PhoneNumberValidationState.Setup + ); + }, [initialUserConfig]); useEffect(() => { setIsMobileWidth(window.innerWidth < 786); @@ -387,6 +414,78 @@ export default function SettingsView() { + + + + Chat on Whatsapp + {numberValidationState === PhoneNumberValidationState.Verified && ( + + ) || numberValidationState !== PhoneNumberValidationState.Setup && ( + + )} + + +

+ Connect your number to chat with Khoj on WhatsApp. Learn more about the integration here. +

+
+ {numberValidationState === PhoneNumberValidationState.VerifyOTP && ( +
+

{`Enter the OTP sent to your WhatsApp number: ${number}`}

+ setNumberValidationState(PhoneNumberValidationState.Verified)} + > + + + + + + + + + +
+ ) || ( +
+ setNumber(e.target.value)} + value={number} + placeholder="Enter your WhatsApp number" + className="w-full border border-gray-300 rounded-lg p-4" + /> +
+ )} +
+
+ + {numberValidationState === PhoneNumberValidationState.VerifyOTP && ( + + ) || ( + + )} + +
diff --git a/src/interface/web/components/ui/input-otp.tsx b/src/interface/web/components/ui/input-otp.tsx new file mode 100644 index 00000000..f66fcfa0 --- /dev/null +++ b/src/interface/web/components/ui/input-otp.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { OTPInput, OTPInputContext } from "input-otp" +import { Dot } from "lucide-react" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index 9141072b..9ad26062 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -45,7 +45,9 @@ "dompurify": "^3.1.6", "eslint": "^8", "eslint-config-next": "14.2.3", + "input-otp": "^1.2.4", "katex": "^0.16.10", + "libphonenumber-js": "^1.11.4", "lucide-react": "^0.397.0", "markdown-it": "^14.1.0", "markdown-it-highlightjs": "^4.1.0", diff --git a/src/interface/web/tailwind.config.ts b/src/interface/web/tailwind.config.ts index f41977ad..853b7486 100644 --- a/src/interface/web/tailwind.config.ts +++ b/src/interface/web/tailwind.config.ts @@ -89,10 +89,15 @@ const config = { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, + "caret-blink": { + "0%,70%,100%": { opacity: "1" }, + "20%,50%": { opacity: "0" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "caret-blink": "caret-blink 1.25s ease-out infinite", }, }, }, diff --git a/src/interface/web/yarn.lock b/src/interface/web/yarn.lock index 7f1ca408..7f526bdb 100644 --- a/src/interface/web/yarn.lock +++ b/src/interface/web/yarn.lock @@ -2968,6 +2968,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +input-otp@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.2.4.tgz#9834af8675ac72c7f1b7c010f181b3b4ffdd0f72" + integrity sha512-md6rhmD+zmMnUh5crQNSQxq3keBRYvE3odbr4Qb9g2NWzQv9azi+t1a3X4TBTbh98fsGHgEEJlzbe1q860uGCA== + internal-slot@^1.0.4, internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -3357,6 +3362,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libphonenumber-js@^1.11.4: + version "1.11.4" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz#e63fe553f45661b30bb10bb8c82c9cf2b22ec32a" + integrity sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q== + lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"