Add a one-click action to export all conversations. Add a self-service delete account action to the settings page

This commit is contained in:
sabaimran
2025-03-12 23:54:02 -07:00
parent 79816d2b9b
commit a3c4347c11
7 changed files with 500 additions and 201 deletions

View File

@@ -22,6 +22,10 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger
} from "@/components/ui/alert-dialog";
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
import {
@@ -54,6 +58,8 @@ import {
Brain,
EyeSlash,
Eye,
Download,
TrashSimple,
} from "@phosphor-icons/react";
import Loading from "../components/loading/loading";
@@ -63,6 +69,10 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s
import { AppSidebar } from "../components/appSidebar/appSidebar";
import { Separator } from "@/components/ui/separator";
import { KhojLogoType } from "../components/logo/khojLogo";
import { Progress } from "@/components/ui/progress";
import JSZip from "jszip";
import { saveAs } from 'file-saver';
interface DropdownComponentProps {
items: ModelOptions[];
@@ -296,6 +306,10 @@ export default function SettingsView() {
const [numberValidationState, setNumberValidationState] = useState<PhoneNumberValidationState>(
PhoneNumberValidationState.Verified,
);
const [isExporting, setIsExporting] = useState(false);
const [exportProgress, setExportProgress] = useState(0);
const [exportedConversations, setExportedConversations] = useState(0);
const [totalConversations, setTotalConversations] = useState(0);
const { toast } = useToast();
const isMobileWidth = useIsMobileWidth();
@@ -311,8 +325,8 @@ export default function SettingsView() {
initialUserConfig?.is_phone_number_verified
? PhoneNumberValidationState.Verified
: initialUserConfig?.phone_number
? PhoneNumberValidationState.SendOTP
: PhoneNumberValidationState.Setup,
? PhoneNumberValidationState.SendOTP
: PhoneNumberValidationState.Setup,
);
setName(initialUserConfig?.given_name);
setNotionToken(initialUserConfig?.notion_token ?? null);
@@ -538,6 +552,56 @@ export default function SettingsView() {
}
};
const exportChats = async () => {
try {
setIsExporting(true);
// Get total conversation count
const statsResponse = await fetch('/api/chat/stats');
const stats = await statsResponse.json();
const total = stats.num_conversations;
setTotalConversations(total);
// Create zip file
const zip = new JSZip();
const conversations = [];
// Fetch all conversations in batches of 10
for (let page = 0; page * 10 < total; page++) {
const response = await fetch(`/api/chat/export?page=${page}`);
const data = await response.json();
conversations.push(...data);
setExportedConversations((page + 1) * 10);
setExportProgress(((page + 1) * 10 / total) * 100);
}
// Add conversations to zip
zip.file("conversations.json", JSON.stringify(conversations, null, 2));
// Generate and download zip
const content = await zip.generateAsync({ type: "blob" });
saveAs(content, "khoj-conversations.zip");
toast({
title: "Export Complete",
description: `Successfully exported ${conversations.length} conversations`,
});
} catch (error) {
console.error("Error exporting chats:", error);
toast({
title: "Export Failed",
description: "Failed to export chats. Please try again.",
variant: "destructive"
});
} finally {
setIsExporting(false);
setExportProgress(0);
setExportedConversations(0);
setTotalConversations(0);
}
};
const saveNotionToken = async () => {
if (!notionToken) return;
// Save Notion API key to server
@@ -731,93 +795,93 @@ export default function SettingsView() {
)) ||
(userConfig.subscription_state ===
"subscribed" && (
<>
<p className="text-xl text-primary/80">
Futurist
</p>
<p className="text-gray-400">
Subscription <b>renews</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
</>
)) ||
(userConfig.subscription_state ===
"unsubscribed" && (
<>
<p className="text-xl">Futurist</p>
<p className="text-gray-400">
Subscription <b>ends</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
</>
)) ||
(userConfig.subscription_state ===
"expired" && (
<>
<p className="text-xl">Humanist</p>
{(userConfig.subscription_renewal_date && (
<>
<p className="text-xl text-primary/80">
Futurist
</p>
<p className="text-gray-400">
Subscription <b>expired</b>{" "}
on{" "}
Subscription <b>renews</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
)) || (
</>
)) ||
(userConfig.subscription_state ===
"unsubscribed" && (
<>
<p className="text-xl">Futurist</p>
<p className="text-gray-400">
Check{" "}
<a
href="https://khoj.dev/#pricing"
target="_blank"
>
pricing page
</a>{" "}
to compare plans.
Subscription <b>ends</b> on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
)}
</>
))}
</>
)) ||
(userConfig.subscription_state ===
"expired" && (
<>
<p className="text-xl">Humanist</p>
{(userConfig.subscription_renewal_date && (
<p className="text-gray-400">
Subscription <b>expired</b>{" "}
on{" "}
<b>
{
userConfig.subscription_renewal_date
}
</b>
</p>
)) || (
<p className="text-gray-400">
Check{" "}
<a
href="https://khoj.dev/#pricing"
target="_blank"
>
pricing page
</a>{" "}
to compare plans.
</p>
)}
</>
))}
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{(userConfig.subscription_state ==
"subscribed" && (
<Button
variant="outline"
className="hover:text-red-400"
onClick={() =>
setSubscription("cancel")
}
>
<ArrowCircleDown className="h-5 w-5 mr-2" />
Unsubscribe
</Button>
)) ||
(userConfig.subscription_state ==
"unsubscribed" && (
<Button
variant="outline"
className="text-primary/80 hover:text-primary"
className="hover:text-red-400"
onClick={() =>
setSubscription("resubscribe")
setSubscription("cancel")
}
>
<ArrowCircleUp
weight="bold"
className="h-5 w-5 mr-2"
/>
Resubscribe
<ArrowCircleDown className="h-5 w-5 mr-2" />
Unsubscribe
</Button>
)) ||
(userConfig.subscription_state ==
"unsubscribed" && (
<Button
variant="outline"
className="text-primary/80 hover:text-primary"
onClick={() =>
setSubscription("resubscribe")
}
>
<ArrowCircleUp
weight="bold"
className="h-5 w-5 mr-2"
/>
Resubscribe
</Button>
)) ||
(userConfig.subscription_enabled_trial_at && (
<Button
variant="outline"
@@ -907,16 +971,16 @@ export default function SettingsView() {
<Button variant="outline" size="sm">
{(userConfig.enabled_content_source
.github && (
<>
<Files className="h-5 w-5 inline mr-1" />
Manage
</>
)) || (
<>
<Plugs className="h-5 w-5 inline mr-1" />
Connect
</>
)}
<>
<Files className="h-5 w-5 inline mr-1" />
Manage
</>
)) || (
<>
<Plugs className="h-5 w-5 inline mr-1" />
Connect
</>
)}
</Button>
<Button
variant="outline"
@@ -958,8 +1022,8 @@ export default function SettingsView() {
{
/* Show connect to notion button if notion oauth url setup and user disconnected*/
userConfig.notion_oauth_url &&
!userConfig.enabled_content_source
.notion ? (
!userConfig.enabled_content_source
.notion ? (
<Button
variant="outline"
size="sm"
@@ -973,39 +1037,39 @@ export default function SettingsView() {
Connect
</Button>
) : /* Show sync button if user connected to notion and API key unchanged */
userConfig.enabled_content_source.notion &&
notionToken ===
userConfig.notion_token ? (
<Button
variant="outline"
size="sm"
onClick={() =>
syncContent("notion")
}
>
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
Sync
</Button>
) : /* Show set API key button notion oauth url not set setup */
!userConfig.notion_oauth_url ? (
<Button
variant="outline"
size="sm"
onClick={saveNotionToken}
disabled={
notionToken ===
userConfig.notion_token
}
>
<FloppyDisk className="h-5 w-5 inline mr-1" />
{(userConfig.enabled_content_source
.notion &&
"Update API Key") ||
"Set API Key"}
</Button>
) : (
<></>
)
userConfig.enabled_content_source.notion &&
notionToken ===
userConfig.notion_token ? (
<Button
variant="outline"
size="sm"
onClick={() =>
syncContent("notion")
}
>
<ArrowsClockwise className="h-5 w-5 inline mr-1" />
Sync
</Button>
) : /* Show set API key button notion oauth url not set setup */
!userConfig.notion_oauth_url ? (
<Button
variant="outline"
size="sm"
onClick={saveNotionToken}
disabled={
notionToken ===
userConfig.notion_token
}
>
<FloppyDisk className="h-5 w-5 inline mr-1" />
{(userConfig.enabled_content_source
.notion &&
"Update API Key") ||
"Set API Key"}
</Button>
) : (
<></>
)
}
<Button
variant="outline"
@@ -1121,18 +1185,18 @@ export default function SettingsView() {
Chat on Whatsapp
{(numberValidationState ===
PhoneNumberValidationState.Verified && (
<CheckCircle
weight="bold"
className="h-4 w-4 ml-1 text-green-400"
/>
)) ||
<CheckCircle
weight="bold"
className="h-4 w-4 ml-1 text-green-400"
/>
)) ||
(numberValidationState !==
PhoneNumberValidationState.Setup && (
<ExclamationMark
weight="bold"
className="h-4 w-4 ml-1 text-yellow-400"
/>
))}
<ExclamationMark
weight="bold"
className="h-4 w-4 ml-1 text-yellow-400"
/>
))}
</CardHeader>
<CardContent className="grid gap-4">
<p className="text-gray-400">
@@ -1161,90 +1225,192 @@ export default function SettingsView() {
/>
{numberValidationState ===
PhoneNumberValidationState.VerifyOTP && (
<>
<p>{`Enter the OTP sent to your number: ${phoneNumber}`}</p>
<InputOTP
autoFocus={true}
maxLength={6}
value={otp || ""}
onChange={setOTP}
onComplete={() =>
setNumberValidationState(
PhoneNumberValidationState.VerifyOTP,
)
}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</>
)}
<>
<p>{`Enter the OTP sent to your number: ${phoneNumber}`}</p>
<InputOTP
autoFocus={true}
maxLength={6}
value={otp || ""}
onChange={setOTP}
onComplete={() =>
setNumberValidationState(
PhoneNumberValidationState.VerifyOTP,
)
}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</>
)}
</div>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
{(numberValidationState ===
PhoneNumberValidationState.VerifyOTP && (
<Button
variant="outline"
onClick={verifyOTP}
>
Verify
</Button>
)) || (
<Button
variant="outline"
disabled={
!phoneNumber ||
(phoneNumber ===
userConfig.phone_number &&
numberValidationState ===
<Button
variant="outline"
onClick={verifyOTP}
>
Verify
</Button>
)) || (
<Button
variant="outline"
disabled={
!phoneNumber ||
(phoneNumber ===
userConfig.phone_number &&
numberValidationState ===
PhoneNumberValidationState.Verified) ||
!isValidPhoneNumber(phoneNumber)
}
onClick={sendOTP}
>
{!userConfig.phone_number ? (
<>
<Plugs className="inline mr-2" />
Setup Whatsapp
</>
) : !phoneNumber ||
(phoneNumber ===
userConfig.phone_number &&
numberValidationState ===
PhoneNumberValidationState.Verified) ||
!isValidPhoneNumber(phoneNumber) ? (
<>
<PlugsConnected className="inline mr-2 text-green-400" />
Switch Number
</>
) : (
<>
Send OTP{" "}
<ArrowRight
className="inline ml-2"
weight="bold"
/>
</>
)}
</Button>
)}
!isValidPhoneNumber(phoneNumber)
}
onClick={sendOTP}
>
{!userConfig.phone_number ? (
<>
<Plugs className="inline mr-2" />
Setup Whatsapp
</>
) : !phoneNumber ||
(phoneNumber ===
userConfig.phone_number &&
numberValidationState ===
PhoneNumberValidationState.Verified) ||
!isValidPhoneNumber(phoneNumber) ? (
<>
<PlugsConnected className="inline mr-2 text-green-400" />
Switch Number
</>
) : (
<>
Send OTP{" "}
<ArrowRight
className="inline ml-2"
weight="bold"
/>
</>
)}
</Button>
)}
{numberValidationState ===
PhoneNumberValidationState.Verified && (
<Button
variant="outline"
onClick={() => disconnectNumber()}
>
<CloudSlash className="h-5 w-5 mr-2" />
Disconnect
</Button>
<Button
variant="outline"
onClick={() => disconnectNumber()}
>
<CloudSlash className="h-5 w-5 mr-2" />
Disconnect
</Button>
)}
</CardFooter>
</Card>
</div>
</div>
<div className="section grid gap-8">
<div id="clients" className="text-2xl">
Account
</div>
<div className="cards flex flex-wrap gap-16">
<Card className={cardClassName}>
<CardHeader className="text-xl flex flex-row">
<Download className="h-7 w-7 mr-2" />
Export Data
</CardHeader>
<CardContent className="overflow-hidden">
<p className="pb-4 text-gray-400">
Download all your chat conversations
</p>
{exportProgress > 0 && (
<div className="w-full mt-4">
<Progress value={exportProgress} className="w-full" />
<p className="text-sm text-gray-500 mt-2">
Exported {exportedConversations} of {totalConversations} conversations
</p>
</div>
)}
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
<Button
variant="outline"
onClick={exportChats}
disabled={isExporting}
>
<Download className="h-5 w-5 mr-2" />
{isExporting ? "Exporting..." : "Export Chats"}
</Button>
</CardFooter>
</Card>
<Card className={cardClassName}>
<CardHeader className="text-xl flex flex-row">
<TrashSimple className="h-7 w-7 mr-2 text-red-500" />
Delete Account
</CardHeader>
<CardContent className="overflow-hidden">
<p className="pb-4 text-gray-400">
This will delete all your account data, including conversations, agents, and any assets you{"'"}ve generated. Be sure to export before you do this if you want to keep your information.
</p>
</CardContent>
<CardFooter className="flex flex-wrap gap-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
className="text-red-500 hover:text-red-600 hover:bg-red-50"
>
<TrashSimple className="h-5 w-5 mr-2" />
Delete Account
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action is irreversible. This will permanently delete your account
and remove all your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-red-500 hover:bg-red-600"
onClick={async () => {
try {
const response = await fetch('/api/self', {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete account');
toast({
title: "Account Deleted",
description: "Your account has been successfully deleted.",
});
// Redirect to home page after successful deletion
window.location.href = "/";
} catch (error) {
console.error('Error deleting account:', error);
toast({
title: "Error",
description: "Failed to delete account. Please try again or contact support.",
variant: "destructive"
});
}
}}
>
<TrashSimple className="h-5 w-5 mr-2" />
Delete Account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardFooter>
</Card>
</div>

View File

@@ -43,6 +43,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/themes": "^3.1.1",
"@types/file-saver": "^2.0.7",
"autoprefixer": "^10.4.19",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -53,9 +54,11 @@
"embla-carousel-react": "^8.5.1",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"file-saver": "^2.0.5",
"framer-motion": "^12.0.6",
"input-otp": "^1.2.4",
"intl-tel-input": "^23.8.1",
"jszip": "^3.10.1",
"katex": "^0.16.21",
"libphonenumber-js": "^1.11.4",
"lucide-react": "^0.468.0",

View File

@@ -1185,6 +1185,11 @@
dependencies:
dompurify "*"
"@types/file-saver@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
"@types/geojson@*":
version "7946.0.16"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a"
@@ -1770,6 +1775,11 @@ confbox@^0.2.1:
resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.1.tgz#ae39f2c99699afa451d00206479f15f9a1208a8b"
integrity sha512-hkT3yDPFbs95mNCy1+7qNKC6Pro+/ibzYxtM2iqEigpf0sVw+bg4Zh9/snjsBcf990vfIsg5+1U7VyiyBb3etg==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
cose-base@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a"
@@ -2739,6 +2749,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
@@ -3065,6 +3080,11 @@ ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
import-fresh@^3.2.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
@@ -3086,7 +3106,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2:
inherits@2, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -3343,6 +3363,11 @@ isarray@^2.0.5:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -3427,6 +3452,16 @@ json5@^1.0.2:
object.assign "^4.1.4"
object.values "^1.1.6"
jszip@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"
katex@^0.16.21, katex@^0.16.9:
version "0.16.21"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.21.tgz#8f63c659e931b210139691f2cc7bb35166b792a3"
@@ -3497,6 +3532,13 @@ libphonenumber-js@^1.11.4:
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.5.tgz#8e6043a67112d4beedb8627b359a613f04d88fba"
integrity sha512-DOjiaVjjSmap12ztyb4QgoFmUe/GbgnEXHu+R7iowk0lzDIjScvPAm8cK9RYTEobbRb0OPlwlZUGTTJPJg13Kw==
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lilconfig@^3.0.0, lilconfig@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
@@ -3960,6 +4002,11 @@ package-manager-detector@^0.2.8:
dependencies:
quansync "^0.2.7"
pako@~1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -4160,6 +4207,11 @@ prettier@3.3.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -4309,6 +4361,19 @@ read-cache@^1.0.0:
dependencies:
pify "^2.3.0"
readable-stream@~2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -4433,6 +4498,11 @@ safe-array-concat@^1.1.3:
has-symbols "^1.1.0"
isarray "^2.0.5"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-push-apply@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5"
@@ -4503,6 +4573,11 @@ set-proto@^1.0.0:
es-errors "^1.3.0"
es-object-atoms "^1.0.0"
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
shadcn-ui@^0.9.0:
version "0.9.5"
resolved "https://registry.yarnpkg.com/shadcn-ui/-/shadcn-ui-0.9.5.tgz#b7f35b78f2c7fe0b71651fe542ed62f01795b4a6"
@@ -4616,6 +4691,7 @@ string-argv@^0.3.2:
integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -4710,6 +4786,13 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -5043,7 +5126,7 @@ use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==
util-deprecate@^1.0.2:
util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==