333 lines
14 KiB
TypeScript
333 lines
14 KiB
TypeScript
import { checkIfUserHasApiKey, registerDevice, signOutAction } from "@/app/actions";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Check, Cog, LogOut, RefreshCw } from "lucide-react";
|
|
import AuthTokenModal from "../AuthTokenModal";
|
|
import DoctorForm from "./DoctorForm";
|
|
import GeneralUserForm from "./UserForm";
|
|
import { Slider } from "@/components/ui/slider";
|
|
import { updateUser } from "@/db/users";
|
|
import _ from "lodash";
|
|
import { createClient } from "@/utils/supabase/client";
|
|
import React, { useCallback } from "react";
|
|
import { doesUserHaveADevice, updateDevice } from "@/db/devices";
|
|
import { useToast } from "@/components/ui/use-toast";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
TooltipProvider,
|
|
} from "@/components/ui/tooltip";
|
|
|
|
interface AppSettingsProps {
|
|
selectedUser: IUser;
|
|
heading: React.ReactNode;
|
|
}
|
|
|
|
const AppSettings: React.FC<AppSettingsProps> = ({
|
|
selectedUser,
|
|
heading,
|
|
}) => {
|
|
const supabase = createClient();
|
|
const { toast } = useToast();
|
|
const [isConnected, setIsConnected] = React.useState(false);
|
|
const doctorFormRef = React.useRef<{ submitForm: () => void } | null>(null);
|
|
const userFormRef = React.useRef<{ submitForm: () => void } | null>(null);
|
|
const [deviceCode, setDeviceCode] = React.useState("");
|
|
const [error, setError] = React.useState("");
|
|
const [hasApiKey, setHasApiKey] = React.useState<boolean>(false);
|
|
|
|
const userHasApiKey = useCallback(async () => {
|
|
const hasApiKey = await checkIfUserHasApiKey(selectedUser.user_id);
|
|
setHasApiKey(hasApiKey);
|
|
}, [selectedUser.user_id]);
|
|
|
|
// ... existing code ...
|
|
|
|
const handleSave = () => {
|
|
if (selectedUser.user_info.user_type === "doctor") {
|
|
doctorFormRef.current?.submitForm();
|
|
} else {
|
|
userFormRef.current?.submitForm();
|
|
}
|
|
};
|
|
|
|
const checkIfUserHasDevice = useCallback(async () => {
|
|
setIsConnected(
|
|
await doesUserHaveADevice(supabase, selectedUser.user_id)
|
|
);
|
|
}, [selectedUser.user_id, supabase]);
|
|
|
|
React.useEffect(() => {
|
|
checkIfUserHasDevice();
|
|
userHasApiKey();
|
|
}, [checkIfUserHasDevice, userHasApiKey]);
|
|
|
|
const [volume, setVolume] = React.useState([
|
|
selectedUser.device?.volume ?? 50,
|
|
]);
|
|
const [isReset, setIsReset] = React.useState(selectedUser.device?.is_reset ?? false);
|
|
const [isOta, setIsOta] = React.useState(selectedUser.device?.is_ota ?? false);
|
|
|
|
const debouncedUpdateVolume = _.debounce(async () => {
|
|
if (selectedUser.device?.device_id) {
|
|
await updateDevice(
|
|
supabase,
|
|
{ volume: volume[0] },
|
|
selectedUser.device.device_id
|
|
);
|
|
}
|
|
}, 1000); // Adjust the debounce delay as needed
|
|
|
|
const updateVolume = (value: number[]) => {
|
|
setVolume(value);
|
|
debouncedUpdateVolume();
|
|
};
|
|
|
|
const onSave = async (values: any, userType: "doctor" | "user") => {
|
|
console.log("onSave", values, userType);
|
|
if (userType === "doctor") {
|
|
await updateUser(
|
|
supabase,
|
|
{
|
|
user_info: {
|
|
user_type: userType,
|
|
user_metadata: values,
|
|
},
|
|
},
|
|
selectedUser!.user_id);
|
|
} else {
|
|
await updateUser(
|
|
supabase,
|
|
{
|
|
supervisee_age: values.supervisee_age,
|
|
supervisee_name: values.supervisee_name,
|
|
supervisee_persona: values.supervisee_persona,
|
|
user_info: {
|
|
user_type: userType,
|
|
user_metadata: values,
|
|
},
|
|
},
|
|
selectedUser!.user_id);
|
|
}
|
|
toast({
|
|
description: "Your prefereces have been saved!",
|
|
});
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{selectedUser.user_info.user_type === "doctor" ? (
|
|
<DoctorForm
|
|
selectedUser={selectedUser}
|
|
heading={heading}
|
|
onSave={onSave}
|
|
onClickCallback={() => handleSave()}
|
|
/>
|
|
) : (
|
|
<GeneralUserForm
|
|
selectedUser={selectedUser}
|
|
heading={heading}
|
|
onSave={onSave}
|
|
onClickCallback={() => handleSave()}
|
|
/>
|
|
)}
|
|
<section className="space-y-4 max-w-screen-sm mt-12">
|
|
<h2 className="text-lg font-semibold border-b border-gray-200 pb-2">
|
|
Device settings
|
|
</h2>
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex flex-row items-center gap-2">
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Set your OpenAI API Key
|
|
</Label>
|
|
<div
|
|
className={`rounded-full flex-shrink-0 h-2 w-2 ${
|
|
hasApiKey ? 'bg-green-500' : 'bg-amber-500'
|
|
}`}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-row items-center gap-2 mt-2">
|
|
<AuthTokenModal user={selectedUser} userHasApiKey={userHasApiKey} hasApiKey={hasApiKey} setHasApiKey={setHasApiKey} />
|
|
</div>
|
|
<p className="text-xs text-gray-400">
|
|
Your keys are E2E encrypted and never stored on our servers as plain text.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex flex-row items-center gap-2">
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Register your device
|
|
</Label>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger className="inline-flex items-center justify-center h-4 w-4 rounded-full bg-gray-200 text-gray-600 text-xs hover:bg-gray-300">
|
|
?
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>For simplicity, you can register your ESP32 MAC address without the colons</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
<div
|
|
className={`rounded-full flex-shrink-0 h-2 w-2 ${
|
|
isConnected ? 'bg-green-500' : 'bg-amber-500'
|
|
}`}
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="flex flex-row items-center gap-2 mt-2">
|
|
<Input
|
|
value={deviceCode}
|
|
disabled={isConnected}
|
|
onChange={(e) => setDeviceCode(e.target.value)}
|
|
placeholder={isConnected ? "**********" : "Enter your device code"}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isConnected}
|
|
onClick={async () => {
|
|
const result = await registerDevice(selectedUser.user_id, deviceCode);
|
|
if (result.error) {
|
|
setError(result.error);
|
|
}
|
|
checkIfUserHasDevice();
|
|
}}
|
|
>
|
|
Register
|
|
</Button>
|
|
</div>
|
|
<p className="text-xs text-gray-400">
|
|
{isConnected ? <span className="font-medium text-gray-800">Registered!</span> :
|
|
error ? <span className="text-red-500">{error}.</span> :
|
|
"Add your device code to your account to register it."
|
|
}
|
|
</p>
|
|
</div>
|
|
{/* <div className="flex flex-col gap-4 flex-nowrap">
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Over-the-air (OTA) updates
|
|
</Label>
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="font-normal flex flex-row items-center gap-2 w-fit"
|
|
onClick={async () => {
|
|
setIsOta(true);
|
|
await setDeviceOta(
|
|
selectedUser.user_id
|
|
);
|
|
}}
|
|
disabled={isOta}
|
|
>
|
|
<RefreshCw size={16} />
|
|
<span>Update</span>
|
|
</Button>
|
|
{isOta ? (
|
|
<p className="text-xs text-gray-400 inline">
|
|
<Check
|
|
size={16}
|
|
className="inline-block mr-1"
|
|
/>
|
|
Your device will be updated on next
|
|
start
|
|
</p>
|
|
) : (
|
|
<p className="text-xs text-gray-400">
|
|
This will update your device software to
|
|
the latest version.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Factory reset
|
|
</Label>
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="font-normal flex flex-row items-center gap-2 w-fit"
|
|
onClick={async () => {
|
|
setIsReset(true);
|
|
await setDeviceReset(
|
|
selectedUser.user_id
|
|
);
|
|
}}
|
|
disabled={isReset}
|
|
>
|
|
<Cog size={16} />
|
|
<span>Factory reset</span>
|
|
</Button>
|
|
{isReset ? (
|
|
<p className="text-xs text-gray-400 inline">
|
|
<Check
|
|
size={16}
|
|
className="inline-block mr-1"
|
|
/>
|
|
Your device will be factory reset on next start
|
|
</p>
|
|
) : (
|
|
<p className="text-xs text-gray-400">
|
|
Caution: This will reset your wifi and authentication details on your device.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div> */}
|
|
|
|
<div className="flex flex-col gap-2 mt-2">
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Logged in as
|
|
</Label>
|
|
<Input
|
|
// autoFocus
|
|
disabled
|
|
value={selectedUser?.email}
|
|
className="max-w-screen-sm h-10 bg-white"
|
|
autoComplete="on"
|
|
style={{
|
|
fontSize: 16,
|
|
}}
|
|
/>
|
|
</div>
|
|
{isConnected && <div className="flex flex-col gap-4 mt-6">
|
|
<Label className="text-sm font-medium text-gray-700">
|
|
Device volume
|
|
</Label>
|
|
<div className="flex flex-row gap-2 items-center flex-nowrap">
|
|
<Slider
|
|
value={volume}
|
|
onValueChange={updateVolume}
|
|
className="sm:w-1/2"
|
|
defaultValue={[50]}
|
|
max={100}
|
|
min={1}
|
|
step={1}
|
|
/>
|
|
<p className="text-gray-500 text-sm">{volume}%</p>
|
|
</div>
|
|
</div>}
|
|
<form
|
|
action={signOutAction}
|
|
className="flex flex-row justify-between mt-4"
|
|
>
|
|
<Button
|
|
variant="destructive_outline"
|
|
size="sm"
|
|
className="font-medium flex flex-row items-center rounded-full gap-2 "
|
|
>
|
|
<LogOut size={18} strokeWidth={2} />
|
|
<span>Logout</span>
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default AppSettings;
|