Stijnus
commited on
Commit
·
78d4e1b
1
Parent(s):
436a8e5
ui fix
Browse files- app/components/settings/debug/DebugTab.tsx +151 -46
- app/components/settings/developer/DeveloperWindow.tsx +167 -61
- app/components/settings/features/FeaturesTab.tsx +200 -182
- app/components/settings/notifications/NotificationsTab.tsx +58 -44
- app/components/settings/settings.types.ts +1 -0
- app/components/settings/update/UpdateTab.tsx +629 -83
- app/components/settings/user/UsersWindow.tsx +165 -60
- app/lib/api/notifications.ts +81 -27
- app/lib/hooks/useNotifications.ts +9 -35
- app/lib/stores/logs.ts +52 -0
- package.json +1 -0
- pnpm-lock.yaml +435 -0
- scripts/update.sh +52 -0
app/components/settings/debug/DebugTab.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|
| 2 |
import { toast } from 'react-toastify';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { logStore } from '~/lib/stores/logs';
|
|
|
|
| 5 |
|
| 6 |
interface ProviderStatus {
|
| 7 |
id: string;
|
|
@@ -472,51 +473,147 @@ export default function DebugTab() {
|
|
| 472 |
}
|
| 473 |
};
|
| 474 |
|
| 475 |
-
const
|
| 476 |
try {
|
| 477 |
setLoading((prev) => ({ ...prev, errors: true }));
|
| 478 |
|
| 479 |
-
// Get
|
| 480 |
-
const
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
}))
|
| 491 |
-
|
| 492 |
-
// Combine collected errors with resource errors
|
| 493 |
-
const allErrors = [...errorLog.errors, ...resourceErrors];
|
| 494 |
-
|
| 495 |
-
if (allErrors.length > 0) {
|
| 496 |
-
logStore.logError('JavaScript Errors Found', {
|
| 497 |
-
errors: allErrors,
|
| 498 |
-
timestamp: new Date().toISOString(),
|
| 499 |
-
});
|
| 500 |
-
toast.error(`Found ${allErrors.length} error(s)`);
|
| 501 |
-
} else {
|
| 502 |
-
toast.success('No errors found');
|
| 503 |
-
}
|
| 504 |
|
| 505 |
-
// Update error log
|
| 506 |
setErrorLog({
|
| 507 |
errors: allErrors,
|
| 508 |
lastCheck: new Date().toISOString(),
|
| 509 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
} catch (error) {
|
| 511 |
-
toast.error('Failed to check
|
| 512 |
-
console.error('Failed to check
|
| 513 |
} finally {
|
| 514 |
setLoading((prev) => ({ ...prev, errors: false }));
|
| 515 |
}
|
| 516 |
};
|
| 517 |
|
| 518 |
return (
|
| 519 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
{/* System Information */}
|
| 521 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 522 |
<div className="flex items-center justify-between mb-4">
|
|
@@ -529,9 +626,9 @@ export default function DebugTab() {
|
|
| 529 |
onClick={handleLogSystemInfo}
|
| 530 |
className={classNames(
|
| 531 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 532 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 533 |
-
'
|
| 534 |
-
'transition-colors',
|
| 535 |
)}
|
| 536 |
>
|
| 537 |
<div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
|
|
@@ -541,10 +638,12 @@ export default function DebugTab() {
|
|
| 541 |
onClick={getSystemInfo}
|
| 542 |
className={classNames(
|
| 543 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 544 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 545 |
-
'
|
| 546 |
-
'transition-colors',
|
|
|
|
| 547 |
)}
|
|
|
|
| 548 |
>
|
| 549 |
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
|
| 550 |
Refresh
|
|
@@ -684,10 +783,12 @@ export default function DebugTab() {
|
|
| 684 |
onClick={checkProviderStatus}
|
| 685 |
className={classNames(
|
| 686 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 687 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 688 |
-
'
|
| 689 |
-
'transition-colors',
|
|
|
|
| 690 |
)}
|
|
|
|
| 691 |
>
|
| 692 |
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
|
| 693 |
Refresh
|
|
@@ -729,10 +830,12 @@ export default function DebugTab() {
|
|
| 729 |
onClick={handleLogPerformance}
|
| 730 |
className={classNames(
|
| 731 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 732 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 733 |
-
'
|
| 734 |
-
'transition-colors',
|
|
|
|
| 735 |
)}
|
|
|
|
| 736 |
>
|
| 737 |
<div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
|
| 738 |
Log Performance
|
|
@@ -811,13 +914,15 @@ export default function DebugTab() {
|
|
| 811 |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
| 812 |
</div>
|
| 813 |
<button
|
| 814 |
-
onClick={
|
| 815 |
className={classNames(
|
| 816 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 817 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 818 |
-
'
|
| 819 |
-
'transition-colors',
|
|
|
|
| 820 |
)}
|
|
|
|
| 821 |
>
|
| 822 |
<div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
|
| 823 |
Check for Errors
|
|
|
|
| 2 |
import { toast } from 'react-toastify';
|
| 3 |
import { classNames } from '~/utils/classNames';
|
| 4 |
import { logStore } from '~/lib/stores/logs';
|
| 5 |
+
import type { LogEntry } from '~/lib/stores/logs';
|
| 6 |
|
| 7 |
interface ProviderStatus {
|
| 8 |
id: string;
|
|
|
|
| 473 |
}
|
| 474 |
};
|
| 475 |
|
| 476 |
+
const checkErrors = async () => {
|
| 477 |
try {
|
| 478 |
setLoading((prev) => ({ ...prev, errors: true }));
|
| 479 |
|
| 480 |
+
// Get errors from log store
|
| 481 |
+
const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error');
|
| 482 |
+
|
| 483 |
+
// Combine with runtime errors
|
| 484 |
+
const allErrors = [
|
| 485 |
+
...errorLog.errors,
|
| 486 |
+
...storedErrors.map((error) => ({
|
| 487 |
+
type: 'stored',
|
| 488 |
+
message: error.message,
|
| 489 |
+
timestamp: error.timestamp,
|
| 490 |
+
details: error.details || {},
|
| 491 |
+
})),
|
| 492 |
+
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
|
|
|
|
| 494 |
setErrorLog({
|
| 495 |
errors: allErrors,
|
| 496 |
lastCheck: new Date().toISOString(),
|
| 497 |
});
|
| 498 |
+
|
| 499 |
+
if (allErrors.length === 0) {
|
| 500 |
+
toast.success('No errors found');
|
| 501 |
+
} else {
|
| 502 |
+
toast.warning(`Found ${allErrors.length} error(s)`);
|
| 503 |
+
}
|
| 504 |
} catch (error) {
|
| 505 |
+
toast.error('Failed to check errors');
|
| 506 |
+
console.error('Failed to check errors:', error);
|
| 507 |
} finally {
|
| 508 |
setLoading((prev) => ({ ...prev, errors: false }));
|
| 509 |
}
|
| 510 |
};
|
| 511 |
|
| 512 |
return (
|
| 513 |
+
<div className="flex flex-col gap-6">
|
| 514 |
+
{/* Action Buttons */}
|
| 515 |
+
<div className="flex flex-wrap gap-4">
|
| 516 |
+
<button
|
| 517 |
+
onClick={checkProviderStatus}
|
| 518 |
+
disabled={loading.providers}
|
| 519 |
+
className={classNames(
|
| 520 |
+
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 521 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 522 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 523 |
+
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
| 524 |
+
{ 'opacity-50 cursor-not-allowed': loading.providers },
|
| 525 |
+
)}
|
| 526 |
+
>
|
| 527 |
+
{loading.providers ? (
|
| 528 |
+
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
| 529 |
+
) : (
|
| 530 |
+
<div className="i-ph:plug w-4 h-4" />
|
| 531 |
+
)}
|
| 532 |
+
Check Providers
|
| 533 |
+
</button>
|
| 534 |
+
|
| 535 |
+
<button
|
| 536 |
+
onClick={getSystemInfo}
|
| 537 |
+
disabled={loading.systemInfo}
|
| 538 |
+
className={classNames(
|
| 539 |
+
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 540 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 541 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 542 |
+
'focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2',
|
| 543 |
+
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
| 544 |
+
)}
|
| 545 |
+
>
|
| 546 |
+
{loading.systemInfo ? (
|
| 547 |
+
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
| 548 |
+
) : (
|
| 549 |
+
<div className="i-ph:gear w-4 h-4" />
|
| 550 |
+
)}
|
| 551 |
+
Update System Info
|
| 552 |
+
</button>
|
| 553 |
+
|
| 554 |
+
<button
|
| 555 |
+
onClick={handleLogPerformance}
|
| 556 |
+
disabled={loading.performance}
|
| 557 |
+
className={classNames(
|
| 558 |
+
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 559 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 560 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 561 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2',
|
| 562 |
+
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
| 563 |
+
)}
|
| 564 |
+
>
|
| 565 |
+
{loading.performance ? (
|
| 566 |
+
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
| 567 |
+
) : (
|
| 568 |
+
<div className="i-ph:chart-bar w-4 h-4" />
|
| 569 |
+
)}
|
| 570 |
+
Log Performance
|
| 571 |
+
</button>
|
| 572 |
+
|
| 573 |
+
<button
|
| 574 |
+
onClick={checkErrors}
|
| 575 |
+
disabled={loading.errors}
|
| 576 |
+
className={classNames(
|
| 577 |
+
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
| 578 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 579 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 580 |
+
'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2',
|
| 581 |
+
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
| 582 |
+
)}
|
| 583 |
+
>
|
| 584 |
+
{loading.errors ? (
|
| 585 |
+
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
| 586 |
+
) : (
|
| 587 |
+
<div className="i-ph:warning w-4 h-4" />
|
| 588 |
+
)}
|
| 589 |
+
Check Errors
|
| 590 |
+
</button>
|
| 591 |
+
</div>
|
| 592 |
+
|
| 593 |
+
{/* Error Log Display */}
|
| 594 |
+
{errorLog.errors.length > 0 && (
|
| 595 |
+
<div className="mt-4">
|
| 596 |
+
<h3 className="text-lg font-semibold mb-2">Error Log</h3>
|
| 597 |
+
<div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
|
| 598 |
+
{errorLog.errors.map((error, index) => (
|
| 599 |
+
<div key={index} className="mb-4 last:mb-0 p-3 bg-white rounded border border-red-200">
|
| 600 |
+
<div className="flex items-center gap-2 text-sm text-gray-600">
|
| 601 |
+
<span className="font-medium">Type:</span> {error.type}
|
| 602 |
+
<span className="font-medium ml-4">Time:</span>
|
| 603 |
+
{new Date(error.timestamp).toLocaleString()}
|
| 604 |
+
</div>
|
| 605 |
+
<div className="mt-2 text-red-600">{error.message}</div>
|
| 606 |
+
{error.filename && (
|
| 607 |
+
<div className="mt-1 text-sm text-gray-500">
|
| 608 |
+
File: {error.filename} (Line: {error.lineNumber}, Column: {error.columnNumber})
|
| 609 |
+
</div>
|
| 610 |
+
)}
|
| 611 |
+
</div>
|
| 612 |
+
))}
|
| 613 |
+
</div>
|
| 614 |
+
</div>
|
| 615 |
+
)}
|
| 616 |
+
|
| 617 |
{/* System Information */}
|
| 618 |
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
| 619 |
<div className="flex items-center justify-between mb-4">
|
|
|
|
| 626 |
onClick={handleLogSystemInfo}
|
| 627 |
className={classNames(
|
| 628 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 629 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 630 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 631 |
+
'transition-colors duration-200',
|
| 632 |
)}
|
| 633 |
>
|
| 634 |
<div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
|
|
|
|
| 638 |
onClick={getSystemInfo}
|
| 639 |
className={classNames(
|
| 640 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 641 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 642 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 643 |
+
'transition-colors duration-200',
|
| 644 |
+
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
| 645 |
)}
|
| 646 |
+
disabled={loading.systemInfo}
|
| 647 |
>
|
| 648 |
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
|
| 649 |
Refresh
|
|
|
|
| 783 |
onClick={checkProviderStatus}
|
| 784 |
className={classNames(
|
| 785 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 786 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 787 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 788 |
+
'transition-colors duration-200',
|
| 789 |
+
{ 'opacity-50 cursor-not-allowed': loading.providers },
|
| 790 |
)}
|
| 791 |
+
disabled={loading.providers}
|
| 792 |
>
|
| 793 |
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
|
| 794 |
Refresh
|
|
|
|
| 830 |
onClick={handleLogPerformance}
|
| 831 |
className={classNames(
|
| 832 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 833 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 834 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 835 |
+
'transition-colors duration-200',
|
| 836 |
+
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
| 837 |
)}
|
| 838 |
+
disabled={loading.performance}
|
| 839 |
>
|
| 840 |
<div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
|
| 841 |
Log Performance
|
|
|
|
| 914 |
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
| 915 |
</div>
|
| 916 |
<button
|
| 917 |
+
onClick={checkErrors}
|
| 918 |
className={classNames(
|
| 919 |
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 920 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 921 |
+
'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
|
| 922 |
+
'transition-colors duration-200',
|
| 923 |
+
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
| 924 |
)}
|
| 925 |
+
disabled={loading.errors}
|
| 926 |
>
|
| 927 |
<div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
|
| 928 |
Check for Errors
|
app/components/settings/developer/DeveloperWindow.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
-
import { useState } from 'react';
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { TabManagement } from './TabManagement';
|
| 6 |
import { TabTile } from '~/components/settings/shared/TabTile';
|
|
|
|
| 7 |
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
| 8 |
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
| 9 |
import { useStore } from '@nanostores/react';
|
|
@@ -20,6 +21,7 @@ import SettingsTab from '~/components/settings/settings/SettingsTab';
|
|
| 20 |
import ProfileTab from '~/components/settings/profile/ProfileTab';
|
| 21 |
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
| 22 |
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
|
|
|
|
| 23 |
|
| 24 |
interface DraggableTabTileProps {
|
| 25 |
tab: TabVisibilityConfig;
|
|
@@ -102,6 +104,24 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 102 |
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
| 103 |
const [showTabManagement, setShowTabManagement] = useState(false);
|
| 104 |
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
// Status hooks
|
| 107 |
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
@@ -120,7 +140,14 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 120 |
|
| 121 |
// Only show tabs that are assigned to the developer window AND are visible
|
| 122 |
const visibleDeveloperTabs = tabConfiguration.developerTabs
|
| 123 |
-
.filter((tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
| 125 |
|
| 126 |
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
|
@@ -136,32 +163,38 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 136 |
updateTabConfiguration(updatedTargetTab);
|
| 137 |
};
|
| 138 |
|
| 139 |
-
const handleTabClick =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
setLoadingTab(tabId);
|
| 141 |
setActiveTab(tabId);
|
| 142 |
|
| 143 |
// Acknowledge the status based on tab type
|
| 144 |
switch (tabId) {
|
| 145 |
case 'update':
|
| 146 |
-
|
| 147 |
break;
|
| 148 |
case 'features':
|
| 149 |
-
|
| 150 |
break;
|
| 151 |
case 'notifications':
|
| 152 |
-
|
| 153 |
break;
|
| 154 |
case 'connection':
|
| 155 |
acknowledgeIssue();
|
| 156 |
break;
|
| 157 |
case 'debug':
|
| 158 |
-
|
| 159 |
break;
|
| 160 |
}
|
| 161 |
|
| 162 |
-
//
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
};
|
| 166 |
|
| 167 |
const getTabComponent = () => {
|
|
@@ -238,7 +271,7 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 238 |
<RadixDialog.Root open={open}>
|
| 239 |
<RadixDialog.Portal>
|
| 240 |
<div className="fixed inset-0 flex items-center justify-center z-[60]">
|
| 241 |
-
<RadixDialog.Overlay
|
| 242 |
<motion.div
|
| 243 |
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
| 244 |
initial={{ opacity: 0 }}
|
|
@@ -247,16 +280,15 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 247 |
transition={{ duration: 0.2 }}
|
| 248 |
/>
|
| 249 |
</RadixDialog.Overlay>
|
| 250 |
-
|
|
|
|
| 251 |
<motion.div
|
| 252 |
className={classNames(
|
| 253 |
-
'relative',
|
| 254 |
'w-[1200px] h-[90vh]',
|
| 255 |
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 256 |
'rounded-2xl shadow-2xl',
|
| 257 |
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 258 |
'flex flex-col overflow-hidden',
|
| 259 |
-
'z-[61]',
|
| 260 |
)}
|
| 261 |
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 262 |
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
@@ -264,68 +296,142 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
|
| 264 |
transition={{ duration: 0.2 }}
|
| 265 |
>
|
| 266 |
{/* Header */}
|
| 267 |
-
<div className="flex
|
| 268 |
-
<div className="flex items-center
|
| 269 |
-
{
|
| 270 |
-
<
|
| 271 |
onClick={handleBack}
|
| 272 |
-
className=
|
| 273 |
-
'flex items-center justify-center w-8 h-8 rounded-lg',
|
| 274 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 275 |
-
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 276 |
-
'group transition-all duration-200',
|
| 277 |
-
)}
|
| 278 |
-
whileHover={{ scale: 1.05 }}
|
| 279 |
-
whileTap={{ scale: 0.95 }}
|
| 280 |
>
|
| 281 |
-
<div className="i-ph:arrow-left w-4 h-4 text-
|
| 282 |
-
</
|
| 283 |
-
)
|
| 284 |
-
<div className="flex items-center gap-3">
|
| 285 |
<motion.div
|
| 286 |
-
className="i-ph:
|
| 287 |
-
initial={{ rotate:
|
| 288 |
-
animate={{ rotate:
|
| 289 |
transition={{
|
| 290 |
repeat: Infinity,
|
| 291 |
-
|
| 292 |
-
|
|
|
|
| 293 |
}}
|
| 294 |
/>
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
</
|
| 299 |
</div>
|
| 300 |
-
|
| 301 |
-
|
|
|
|
| 302 |
<motion.button
|
| 303 |
onClick={() => setShowTabManagement(true)}
|
| 304 |
-
className=
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
'hover:bg-purple-500/20',
|
| 308 |
-
'transition-colors duration-200',
|
| 309 |
-
)}
|
| 310 |
-
whileHover={{ scale: 1.02 }}
|
| 311 |
-
whileTap={{ scale: 0.98 }}
|
| 312 |
>
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
| 314 |
</motion.button>
|
| 315 |
)}
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
onClick={onClose}
|
| 318 |
-
className=
|
| 319 |
-
'flex items-center justify-center w-8 h-8 rounded-lg',
|
| 320 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 321 |
-
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 322 |
-
'group transition-all duration-200',
|
| 323 |
-
)}
|
| 324 |
-
whileHover={{ scale: 1.05 }}
|
| 325 |
-
whileTap={{ scale: 0.95 }}
|
| 326 |
>
|
| 327 |
-
<div className="i-ph:x w-4 h-4 text-
|
| 328 |
-
</
|
| 329 |
</div>
|
| 330 |
</div>
|
| 331 |
|
|
|
|
| 1 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
+
import { useState, useEffect } from 'react';
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { TabManagement } from './TabManagement';
|
| 6 |
import { TabTile } from '~/components/settings/shared/TabTile';
|
| 7 |
+
import { DialogTitle } from '~/components/ui/Dialog';
|
| 8 |
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
| 9 |
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
| 10 |
import { useStore } from '@nanostores/react';
|
|
|
|
| 21 |
import ProfileTab from '~/components/settings/profile/ProfileTab';
|
| 22 |
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
| 23 |
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
|
| 24 |
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
| 25 |
|
| 26 |
interface DraggableTabTileProps {
|
| 27 |
tab: TabVisibilityConfig;
|
|
|
|
| 104 |
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
| 105 |
const [showTabManagement, setShowTabManagement] = useState(false);
|
| 106 |
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
| 107 |
+
const [profile, setProfile] = useState(() => {
|
| 108 |
+
const saved = localStorage.getItem('bolt_user_profile');
|
| 109 |
+
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
// Listen for profile changes
|
| 113 |
+
useEffect(() => {
|
| 114 |
+
const handleStorageChange = (e: StorageEvent) => {
|
| 115 |
+
if (e.key === 'bolt_user_profile') {
|
| 116 |
+
const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
|
| 117 |
+
setProfile(newProfile);
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
window.addEventListener('storage', handleStorageChange);
|
| 122 |
+
|
| 123 |
+
return () => window.removeEventListener('storage', handleStorageChange);
|
| 124 |
+
}, []);
|
| 125 |
|
| 126 |
// Status hooks
|
| 127 |
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
|
|
|
| 140 |
|
| 141 |
// Only show tabs that are assigned to the developer window AND are visible
|
| 142 |
const visibleDeveloperTabs = tabConfiguration.developerTabs
|
| 143 |
+
.filter((tab) => {
|
| 144 |
+
// Hide notifications tab if notifications are disabled
|
| 145 |
+
if (tab.id === 'notifications' && !profile.notifications) {
|
| 146 |
+
return false;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
return tab.visible;
|
| 150 |
+
})
|
| 151 |
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
| 152 |
|
| 153 |
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
|
|
|
| 163 |
updateTabConfiguration(updatedTargetTab);
|
| 164 |
};
|
| 165 |
|
| 166 |
+
const handleTabClick = (tabId: TabType) => {
|
| 167 |
+
// Don't allow clicking notifications tab if disabled
|
| 168 |
+
if (tabId === 'notifications' && !profile.notifications) {
|
| 169 |
+
return;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
setLoadingTab(tabId);
|
| 173 |
setActiveTab(tabId);
|
| 174 |
|
| 175 |
// Acknowledge the status based on tab type
|
| 176 |
switch (tabId) {
|
| 177 |
case 'update':
|
| 178 |
+
acknowledgeUpdate();
|
| 179 |
break;
|
| 180 |
case 'features':
|
| 181 |
+
acknowledgeAllFeatures();
|
| 182 |
break;
|
| 183 |
case 'notifications':
|
| 184 |
+
markAllAsRead();
|
| 185 |
break;
|
| 186 |
case 'connection':
|
| 187 |
acknowledgeIssue();
|
| 188 |
break;
|
| 189 |
case 'debug':
|
| 190 |
+
acknowledgeAllIssues();
|
| 191 |
break;
|
| 192 |
}
|
| 193 |
|
| 194 |
+
// Clear loading state after a short delay
|
| 195 |
+
setTimeout(() => {
|
| 196 |
+
setLoadingTab(null);
|
| 197 |
+
}, 500);
|
| 198 |
};
|
| 199 |
|
| 200 |
const getTabComponent = () => {
|
|
|
|
| 271 |
<RadixDialog.Root open={open}>
|
| 272 |
<RadixDialog.Portal>
|
| 273 |
<div className="fixed inset-0 flex items-center justify-center z-[60]">
|
| 274 |
+
<RadixDialog.Overlay className="fixed inset-0">
|
| 275 |
<motion.div
|
| 276 |
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
| 277 |
initial={{ opacity: 0 }}
|
|
|
|
| 280 |
transition={{ duration: 0.2 }}
|
| 281 |
/>
|
| 282 |
</RadixDialog.Overlay>
|
| 283 |
+
|
| 284 |
+
<RadixDialog.Content aria-describedby={undefined} className="relative z-[61]">
|
| 285 |
<motion.div
|
| 286 |
className={classNames(
|
|
|
|
| 287 |
'w-[1200px] h-[90vh]',
|
| 288 |
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
| 289 |
'rounded-2xl shadow-2xl',
|
| 290 |
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 291 |
'flex flex-col overflow-hidden',
|
|
|
|
| 292 |
)}
|
| 293 |
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 294 |
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
|
|
| 296 |
transition={{ duration: 0.2 }}
|
| 297 |
>
|
| 298 |
{/* Header */}
|
| 299 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
| 300 |
+
<div className="flex items-center space-x-4">
|
| 301 |
+
{activeTab || showTabManagement ? (
|
| 302 |
+
<button
|
| 303 |
onClick={handleBack}
|
| 304 |
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
>
|
| 306 |
+
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 307 |
+
</button>
|
| 308 |
+
) : (
|
|
|
|
| 309 |
<motion.div
|
| 310 |
+
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
|
| 311 |
+
initial={{ rotate: -10 }}
|
| 312 |
+
animate={{ rotate: 10 }}
|
| 313 |
transition={{
|
| 314 |
repeat: Infinity,
|
| 315 |
+
repeatType: 'reverse',
|
| 316 |
+
duration: 2,
|
| 317 |
+
ease: 'easeInOut',
|
| 318 |
}}
|
| 319 |
/>
|
| 320 |
+
)}
|
| 321 |
+
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
| 322 |
+
{showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Settings'}
|
| 323 |
+
</DialogTitle>
|
| 324 |
</div>
|
| 325 |
+
|
| 326 |
+
<div className="flex items-center space-x-4">
|
| 327 |
+
{!activeTab && !showTabManagement && (
|
| 328 |
<motion.button
|
| 329 |
onClick={() => setShowTabManagement(true)}
|
| 330 |
+
className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
| 331 |
+
whileHover={{ scale: 1.05 }}
|
| 332 |
+
whileTap={{ scale: 0.95 }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
>
|
| 334 |
+
<div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 335 |
+
<span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
|
| 336 |
+
Manage Tabs
|
| 337 |
+
</span>
|
| 338 |
</motion.button>
|
| 339 |
)}
|
| 340 |
+
|
| 341 |
+
<div className="relative">
|
| 342 |
+
<DropdownMenu.Root>
|
| 343 |
+
<DropdownMenu.Trigger asChild>
|
| 344 |
+
<button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
|
| 345 |
+
{profile.avatar ? (
|
| 346 |
+
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
|
| 347 |
+
) : (
|
| 348 |
+
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
| 349 |
+
<svg
|
| 350 |
+
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
| 351 |
+
fill="none"
|
| 352 |
+
stroke="currentColor"
|
| 353 |
+
viewBox="0 0 24 24"
|
| 354 |
+
>
|
| 355 |
+
<path
|
| 356 |
+
strokeLinecap="round"
|
| 357 |
+
strokeLinejoin="round"
|
| 358 |
+
strokeWidth={2}
|
| 359 |
+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
| 360 |
+
/>
|
| 361 |
+
</svg>
|
| 362 |
+
</div>
|
| 363 |
+
)}
|
| 364 |
+
</button>
|
| 365 |
+
</DropdownMenu.Trigger>
|
| 366 |
+
|
| 367 |
+
<DropdownMenu.Portal>
|
| 368 |
+
<DropdownMenu.Content
|
| 369 |
+
className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[200]"
|
| 370 |
+
sideOffset={5}
|
| 371 |
+
align="end"
|
| 372 |
+
>
|
| 373 |
+
<DropdownMenu.Item
|
| 374 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 375 |
+
onSelect={() => handleTabClick('profile')}
|
| 376 |
+
>
|
| 377 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 378 |
+
<div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 379 |
+
</div>
|
| 380 |
+
<span className="group-hover:text-purple-500 transition-colors">Profile</span>
|
| 381 |
+
</DropdownMenu.Item>
|
| 382 |
+
|
| 383 |
+
<DropdownMenu.Item
|
| 384 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 385 |
+
onSelect={() => handleTabClick('settings')}
|
| 386 |
+
>
|
| 387 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 388 |
+
<div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 389 |
+
</div>
|
| 390 |
+
<span className="group-hover:text-purple-500 transition-colors">Settings</span>
|
| 391 |
+
</DropdownMenu.Item>
|
| 392 |
+
|
| 393 |
+
{profile.notifications && (
|
| 394 |
+
<>
|
| 395 |
+
<DropdownMenu.Item
|
| 396 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 397 |
+
onSelect={() => handleTabClick('notifications')}
|
| 398 |
+
>
|
| 399 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 400 |
+
<div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 401 |
+
</div>
|
| 402 |
+
<span className="group-hover:text-purple-500 transition-colors">
|
| 403 |
+
Notifications
|
| 404 |
+
{hasUnreadNotifications && (
|
| 405 |
+
<span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
|
| 406 |
+
{unreadNotifications.length}
|
| 407 |
+
</span>
|
| 408 |
+
)}
|
| 409 |
+
</span>
|
| 410 |
+
</DropdownMenu.Item>
|
| 411 |
+
|
| 412 |
+
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
| 413 |
+
</>
|
| 414 |
+
)}
|
| 415 |
+
<DropdownMenu.Item
|
| 416 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 417 |
+
onSelect={onClose}
|
| 418 |
+
>
|
| 419 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 420 |
+
<div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 421 |
+
</div>
|
| 422 |
+
<span className="group-hover:text-purple-500 transition-colors">Close</span>
|
| 423 |
+
</DropdownMenu.Item>
|
| 424 |
+
</DropdownMenu.Content>
|
| 425 |
+
</DropdownMenu.Portal>
|
| 426 |
+
</DropdownMenu.Root>
|
| 427 |
+
</div>
|
| 428 |
+
|
| 429 |
+
<button
|
| 430 |
onClick={onClose}
|
| 431 |
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
>
|
| 433 |
+
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 434 |
+
</button>
|
| 435 |
</div>
|
| 436 |
</div>
|
| 437 |
|
app/components/settings/features/FeaturesTab.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import React from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { Switch } from '~/components/ui/Switch';
|
| 4 |
import { useSettings } from '~/lib/hooks/useSettings';
|
|
@@ -19,6 +19,93 @@ interface FeatureToggle {
|
|
| 19 |
tooltip?: string;
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
export default function FeaturesTab() {
|
| 23 |
const {
|
| 24 |
setEventLogs,
|
|
@@ -36,50 +123,56 @@ export default function FeaturesTab() {
|
|
| 36 |
|
| 37 |
const eventLogs = useStore(isEventLogsEnabled);
|
| 38 |
|
| 39 |
-
const features: FeatureToggle[] =
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
const handleToggleFeature = (featureId: string, enabled: boolean) => {
|
| 85 |
switch (featureId) {
|
|
@@ -107,163 +200,88 @@ export default function FeaturesTab() {
|
|
| 107 |
};
|
| 108 |
|
| 109 |
return (
|
| 110 |
-
<div className="flex flex-col gap-
|
| 111 |
-
<
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
<div>
|
| 119 |
-
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
|
| 120 |
-
<p className="text-sm text-bolt-elements-textSecondary">
|
| 121 |
-
Customize your Bolt experience with experimental features
|
| 122 |
-
</p>
|
| 123 |
-
</div>
|
| 124 |
-
</motion.div>
|
| 125 |
-
|
| 126 |
-
<motion.div
|
| 127 |
-
className="grid grid-cols-1 md:grid-cols-2 gap-4"
|
| 128 |
-
initial={{ opacity: 0, y: 20 }}
|
| 129 |
-
animate={{ opacity: 1, y: 0 }}
|
| 130 |
-
transition={{ duration: 0.3 }}
|
| 131 |
-
>
|
| 132 |
-
{features.map((feature, index) => (
|
| 133 |
-
<motion.div
|
| 134 |
-
key={feature.id}
|
| 135 |
-
className={classNames(
|
| 136 |
-
'relative group cursor-pointer',
|
| 137 |
-
'bg-bolt-elements-background-depth-2',
|
| 138 |
-
'hover:bg-bolt-elements-background-depth-3',
|
| 139 |
-
'transition-colors duration-200',
|
| 140 |
-
'rounded-lg overflow-hidden',
|
| 141 |
-
)}
|
| 142 |
-
initial={{ opacity: 0, y: 20 }}
|
| 143 |
-
animate={{ opacity: 1, y: 0 }}
|
| 144 |
-
transition={{ delay: index * 0.1 }}
|
| 145 |
-
>
|
| 146 |
-
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
| 147 |
-
{feature.beta && (
|
| 148 |
-
<motion.span
|
| 149 |
-
className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium"
|
| 150 |
-
whileHover={{ scale: 1.05 }}
|
| 151 |
-
whileTap={{ scale: 0.95 }}
|
| 152 |
-
>
|
| 153 |
-
Beta
|
| 154 |
-
</motion.span>
|
| 155 |
-
)}
|
| 156 |
-
{feature.experimental && (
|
| 157 |
-
<motion.span
|
| 158 |
-
className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
|
| 159 |
-
whileHover={{ scale: 1.05 }}
|
| 160 |
-
whileTap={{ scale: 0.95 }}
|
| 161 |
-
>
|
| 162 |
-
Experimental
|
| 163 |
-
</motion.span>
|
| 164 |
-
)}
|
| 165 |
-
</div>
|
| 166 |
-
|
| 167 |
-
<div className="flex items-start gap-4 p-4">
|
| 168 |
-
<motion.div
|
| 169 |
-
className={classNames(
|
| 170 |
-
'p-2 rounded-lg text-xl',
|
| 171 |
-
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
| 172 |
-
'transition-colors duration-200',
|
| 173 |
-
)}
|
| 174 |
-
whileHover={{ scale: 1.1 }}
|
| 175 |
-
whileTap={{ scale: 0.9 }}
|
| 176 |
-
>
|
| 177 |
-
<div className={classNames(feature.icon, 'text-purple-500')} />
|
| 178 |
-
</motion.div>
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
checked={feature.enabled}
|
| 190 |
-
onCheckedChange={(checked) => handleToggleFeature(feature.id, checked)}
|
| 191 |
-
/>
|
| 192 |
-
</div>
|
| 193 |
-
</div>
|
| 194 |
-
</div>
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
))}
|
| 206 |
-
</motion.div>
|
| 207 |
|
| 208 |
<motion.div
|
|
|
|
| 209 |
className={classNames(
|
| 210 |
'bg-bolt-elements-background-depth-2',
|
| 211 |
'hover:bg-bolt-elements-background-depth-3',
|
| 212 |
'transition-all duration-200',
|
| 213 |
-
'rounded-lg',
|
| 214 |
'group',
|
| 215 |
)}
|
| 216 |
initial={{ opacity: 0, y: 20 }}
|
| 217 |
animate={{ opacity: 1, y: 0 }}
|
| 218 |
-
transition={{ delay: 0.
|
| 219 |
-
whileHover={{ scale: 1.01 }}
|
| 220 |
>
|
| 221 |
-
<div className="flex items-
|
| 222 |
-
<
|
| 223 |
className={classNames(
|
| 224 |
'p-2 rounded-lg text-xl',
|
| 225 |
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
| 226 |
'transition-colors duration-200',
|
|
|
|
| 227 |
)}
|
| 228 |
-
whileHover={{ scale: 1.1 }}
|
| 229 |
-
whileTap={{ scale: 0.9 }}
|
| 230 |
>
|
| 231 |
-
<div className="i-ph:book
|
| 232 |
-
</
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
| 241 |
-
Choose a prompt from the library to use as the system prompt
|
| 242 |
-
</p>
|
| 243 |
-
</div>
|
| 244 |
-
<select
|
| 245 |
-
value={promptId}
|
| 246 |
-
onChange={(e) => {
|
| 247 |
-
setPromptId(e.target.value);
|
| 248 |
-
toast.success('Prompt template updated');
|
| 249 |
-
}}
|
| 250 |
-
className={classNames(
|
| 251 |
-
'p-2 rounded-lg text-sm min-w-[200px]',
|
| 252 |
-
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
| 253 |
-
'text-bolt-elements-textPrimary',
|
| 254 |
-
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 255 |
-
'group-hover:border-purple-500/30',
|
| 256 |
-
'transition-all duration-200',
|
| 257 |
-
)}
|
| 258 |
-
>
|
| 259 |
-
{PromptLibrary.getList().map((x) => (
|
| 260 |
-
<option key={x.id} value={x.id}>
|
| 261 |
-
{x.label}
|
| 262 |
-
</option>
|
| 263 |
-
))}
|
| 264 |
-
</select>
|
| 265 |
-
</div>
|
| 266 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
</div>
|
| 268 |
</motion.div>
|
| 269 |
</div>
|
|
|
|
| 1 |
+
import React, { memo } from 'react';
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
import { Switch } from '~/components/ui/Switch';
|
| 4 |
import { useSettings } from '~/lib/hooks/useSettings';
|
|
|
|
| 19 |
tooltip?: string;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
const FeatureCard = memo(
|
| 23 |
+
({
|
| 24 |
+
feature,
|
| 25 |
+
index,
|
| 26 |
+
onToggle,
|
| 27 |
+
}: {
|
| 28 |
+
feature: FeatureToggle;
|
| 29 |
+
index: number;
|
| 30 |
+
onToggle: (id: string, enabled: boolean) => void;
|
| 31 |
+
}) => (
|
| 32 |
+
<motion.div
|
| 33 |
+
key={feature.id}
|
| 34 |
+
layoutId={feature.id}
|
| 35 |
+
className={classNames(
|
| 36 |
+
'relative group cursor-pointer',
|
| 37 |
+
'bg-bolt-elements-background-depth-2',
|
| 38 |
+
'hover:bg-bolt-elements-background-depth-3',
|
| 39 |
+
'transition-colors duration-200',
|
| 40 |
+
'rounded-lg overflow-hidden',
|
| 41 |
+
)}
|
| 42 |
+
initial={{ opacity: 0, y: 20 }}
|
| 43 |
+
animate={{ opacity: 1, y: 0 }}
|
| 44 |
+
transition={{ delay: index * 0.1 }}
|
| 45 |
+
>
|
| 46 |
+
<div className="p-4">
|
| 47 |
+
<div className="flex items-center justify-between">
|
| 48 |
+
<div className="flex items-center gap-3">
|
| 49 |
+
<div className={classNames(feature.icon, 'w-5 h-5 text-bolt-elements-textSecondary')} />
|
| 50 |
+
<div className="flex items-center gap-2">
|
| 51 |
+
<h4 className="font-medium text-bolt-elements-textPrimary">{feature.title}</h4>
|
| 52 |
+
{feature.beta && (
|
| 53 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">Beta</span>
|
| 54 |
+
)}
|
| 55 |
+
{feature.experimental && (
|
| 56 |
+
<span className="px-2 py-0.5 text-xs rounded-full bg-orange-500/10 text-orange-500 font-medium">
|
| 57 |
+
Experimental
|
| 58 |
+
</span>
|
| 59 |
+
)}
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
<Switch checked={feature.enabled} onCheckedChange={(checked) => onToggle(feature.id, checked)} />
|
| 63 |
+
</div>
|
| 64 |
+
<p className="mt-2 text-sm text-bolt-elements-textSecondary">{feature.description}</p>
|
| 65 |
+
{feature.tooltip && <p className="mt-1 text-xs text-bolt-elements-textTertiary">{feature.tooltip}</p>}
|
| 66 |
+
</div>
|
| 67 |
+
</motion.div>
|
| 68 |
+
),
|
| 69 |
+
);
|
| 70 |
+
|
| 71 |
+
const FeatureSection = memo(
|
| 72 |
+
({
|
| 73 |
+
title,
|
| 74 |
+
features,
|
| 75 |
+
icon,
|
| 76 |
+
description,
|
| 77 |
+
onToggleFeature,
|
| 78 |
+
}: {
|
| 79 |
+
title: string;
|
| 80 |
+
features: FeatureToggle[];
|
| 81 |
+
icon: string;
|
| 82 |
+
description: string;
|
| 83 |
+
onToggleFeature: (id: string, enabled: boolean) => void;
|
| 84 |
+
}) => (
|
| 85 |
+
<motion.div
|
| 86 |
+
layout
|
| 87 |
+
className="flex flex-col gap-4"
|
| 88 |
+
initial={{ opacity: 0, y: 20 }}
|
| 89 |
+
animate={{ opacity: 1, y: 0 }}
|
| 90 |
+
transition={{ duration: 0.3 }}
|
| 91 |
+
>
|
| 92 |
+
<div className="flex items-center gap-3">
|
| 93 |
+
<div className={classNames(icon, 'text-xl text-purple-500')} />
|
| 94 |
+
<div>
|
| 95 |
+
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">{title}</h3>
|
| 96 |
+
<p className="text-sm text-bolt-elements-textSecondary">{description}</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 101 |
+
{features.map((feature, index) => (
|
| 102 |
+
<FeatureCard key={feature.id} feature={feature} index={index} onToggle={onToggleFeature} />
|
| 103 |
+
))}
|
| 104 |
+
</div>
|
| 105 |
+
</motion.div>
|
| 106 |
+
),
|
| 107 |
+
);
|
| 108 |
+
|
| 109 |
export default function FeaturesTab() {
|
| 110 |
const {
|
| 111 |
setEventLogs,
|
|
|
|
| 123 |
|
| 124 |
const eventLogs = useStore(isEventLogsEnabled);
|
| 125 |
|
| 126 |
+
const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
|
| 127 |
+
stable: [
|
| 128 |
+
{
|
| 129 |
+
id: 'autoTemplate',
|
| 130 |
+
title: 'Auto Select Code Template',
|
| 131 |
+
description: 'Let Bolt select the best starter template for your project',
|
| 132 |
+
icon: 'i-ph:magic-wand',
|
| 133 |
+
enabled: autoSelectTemplate,
|
| 134 |
+
tooltip: 'Automatically choose the most suitable template based on your project type',
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
id: 'contextOptimization',
|
| 138 |
+
title: 'Context Optimization',
|
| 139 |
+
description: 'Optimize chat context by redacting file contents and using system prompts',
|
| 140 |
+
icon: 'i-ph:arrows-in',
|
| 141 |
+
enabled: contextOptimizationEnabled,
|
| 142 |
+
tooltip: 'Improve AI responses by optimizing the context window and system prompts',
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
id: 'eventLogs',
|
| 146 |
+
title: 'Event Logging',
|
| 147 |
+
description: 'Enable detailed event logging and history',
|
| 148 |
+
icon: 'i-ph:list-bullets',
|
| 149 |
+
enabled: eventLogs,
|
| 150 |
+
tooltip: 'Record detailed logs of system events and user actions',
|
| 151 |
+
},
|
| 152 |
+
],
|
| 153 |
+
beta: [
|
| 154 |
+
{
|
| 155 |
+
id: 'latestBranch',
|
| 156 |
+
title: 'Use Main Branch',
|
| 157 |
+
description: 'Check for updates against the main branch instead of stable',
|
| 158 |
+
icon: 'i-ph:git-branch',
|
| 159 |
+
enabled: isLatestBranch,
|
| 160 |
+
beta: true,
|
| 161 |
+
tooltip: 'Get the latest features and improvements before they are officially released',
|
| 162 |
+
},
|
| 163 |
+
],
|
| 164 |
+
experimental: [
|
| 165 |
+
{
|
| 166 |
+
id: 'experimentalProviders',
|
| 167 |
+
title: 'Experimental Providers',
|
| 168 |
+
description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
|
| 169 |
+
icon: 'i-ph:robot',
|
| 170 |
+
enabled: isLocalModel,
|
| 171 |
+
experimental: true,
|
| 172 |
+
tooltip: 'Try out new AI providers and models in development',
|
| 173 |
+
},
|
| 174 |
+
],
|
| 175 |
+
};
|
| 176 |
|
| 177 |
const handleToggleFeature = (featureId: string, enabled: boolean) => {
|
| 178 |
switch (featureId) {
|
|
|
|
| 200 |
};
|
| 201 |
|
| 202 |
return (
|
| 203 |
+
<div className="flex flex-col gap-8">
|
| 204 |
+
<FeatureSection
|
| 205 |
+
title="Stable Features"
|
| 206 |
+
features={features.stable}
|
| 207 |
+
icon="i-ph:check-circle"
|
| 208 |
+
description="Production-ready features that have been thoroughly tested"
|
| 209 |
+
onToggleFeature={handleToggleFeature}
|
| 210 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
+
{features.beta.length > 0 && (
|
| 213 |
+
<FeatureSection
|
| 214 |
+
title="Beta Features"
|
| 215 |
+
features={features.beta}
|
| 216 |
+
icon="i-ph:test-tube"
|
| 217 |
+
description="New features that are ready for testing but may have some rough edges"
|
| 218 |
+
onToggleFeature={handleToggleFeature}
|
| 219 |
+
/>
|
| 220 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
+
{features.experimental.length > 0 && (
|
| 223 |
+
<FeatureSection
|
| 224 |
+
title="Experimental Features"
|
| 225 |
+
features={features.experimental}
|
| 226 |
+
icon="i-ph:flask"
|
| 227 |
+
description="Features in early development that may be unstable or require additional setup"
|
| 228 |
+
onToggleFeature={handleToggleFeature}
|
| 229 |
+
/>
|
| 230 |
+
)}
|
|
|
|
|
|
|
| 231 |
|
| 232 |
<motion.div
|
| 233 |
+
layout
|
| 234 |
className={classNames(
|
| 235 |
'bg-bolt-elements-background-depth-2',
|
| 236 |
'hover:bg-bolt-elements-background-depth-3',
|
| 237 |
'transition-all duration-200',
|
| 238 |
+
'rounded-lg p-4',
|
| 239 |
'group',
|
| 240 |
)}
|
| 241 |
initial={{ opacity: 0, y: 20 }}
|
| 242 |
animate={{ opacity: 1, y: 0 }}
|
| 243 |
+
transition={{ delay: 0.3 }}
|
|
|
|
| 244 |
>
|
| 245 |
+
<div className="flex items-center gap-4">
|
| 246 |
+
<div
|
| 247 |
className={classNames(
|
| 248 |
'p-2 rounded-lg text-xl',
|
| 249 |
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
|
| 250 |
'transition-colors duration-200',
|
| 251 |
+
'text-purple-500',
|
| 252 |
)}
|
|
|
|
|
|
|
| 253 |
>
|
| 254 |
+
<div className="i-ph:book" />
|
| 255 |
+
</div>
|
| 256 |
+
<div className="flex-1">
|
| 257 |
+
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
|
| 258 |
+
Prompt Library
|
| 259 |
+
</h4>
|
| 260 |
+
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
|
| 261 |
+
Choose a prompt from the library to use as the system prompt
|
| 262 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
</div>
|
| 264 |
+
<select
|
| 265 |
+
value={promptId}
|
| 266 |
+
onChange={(e) => {
|
| 267 |
+
setPromptId(e.target.value);
|
| 268 |
+
toast.success('Prompt template updated');
|
| 269 |
+
}}
|
| 270 |
+
className={classNames(
|
| 271 |
+
'p-2 rounded-lg text-sm min-w-[200px]',
|
| 272 |
+
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
| 273 |
+
'text-bolt-elements-textPrimary',
|
| 274 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 275 |
+
'group-hover:border-purple-500/30',
|
| 276 |
+
'transition-all duration-200',
|
| 277 |
+
)}
|
| 278 |
+
>
|
| 279 |
+
{PromptLibrary.getList().map((x) => (
|
| 280 |
+
<option key={x.id} value={x.id}>
|
| 281 |
+
{x.label}
|
| 282 |
+
</option>
|
| 283 |
+
))}
|
| 284 |
+
</select>
|
| 285 |
</div>
|
| 286 |
</motion.div>
|
| 287 |
</div>
|
app/components/settings/notifications/NotificationsTab.tsx
CHANGED
|
@@ -15,7 +15,7 @@ interface NotificationDetails {
|
|
| 15 |
}
|
| 16 |
|
| 17 |
const NotificationsTab = () => {
|
| 18 |
-
const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all');
|
| 19 |
const logs = useStore(logStore.logs);
|
| 20 |
|
| 21 |
const handleClearNotifications = () => {
|
|
@@ -29,13 +29,44 @@ const NotificationsTab = () => {
|
|
| 29 |
const filteredLogs = Object.values(logs)
|
| 30 |
.filter((log) => {
|
| 31 |
if (filter === 'all') {
|
| 32 |
-
return log.level === 'error' || log.level === 'warning';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
return log.level === filter;
|
| 36 |
})
|
| 37 |
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
const renderNotificationDetails = (details: NotificationDetails) => {
|
| 40 |
if (details.type === 'update') {
|
| 41 |
return (
|
|
@@ -48,7 +79,7 @@ const NotificationsTab = () => {
|
|
| 48 |
</div>
|
| 49 |
<button
|
| 50 |
onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
|
| 51 |
-
className="mt-2 inline-flex items-center gap-2 rounded-md bg-
|
| 52 |
>
|
| 53 |
<span className="i-ph:git-branch text-lg" />
|
| 54 |
View Changes
|
|
@@ -66,10 +97,11 @@ const NotificationsTab = () => {
|
|
| 66 |
<div className="flex items-center gap-2">
|
| 67 |
<select
|
| 68 |
value={filter}
|
| 69 |
-
onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning')}
|
| 70 |
className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
| 71 |
>
|
| 72 |
<option value="all">All Notifications</option>
|
|
|
|
| 73 |
<option value="error">Errors</option>
|
| 74 |
<option value="warning">Warnings</option>
|
| 75 |
</select>
|
|
@@ -92,48 +124,30 @@ const NotificationsTab = () => {
|
|
| 92 |
</div>
|
| 93 |
</div>
|
| 94 |
) : (
|
| 95 |
-
filteredLogs.map((log) =>
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
'text-lg',
|
| 112 |
-
log.level === 'error'
|
| 113 |
-
? 'i-ph:warning-circle text-red-600 dark:text-red-400'
|
| 114 |
-
: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
|
| 115 |
-
)}
|
| 116 |
-
/>
|
| 117 |
-
<div>
|
| 118 |
-
<h3
|
| 119 |
-
className={classNames(
|
| 120 |
-
'text-sm font-medium',
|
| 121 |
-
log.level === 'error'
|
| 122 |
-
? 'text-red-900 dark:text-red-300'
|
| 123 |
-
: 'text-yellow-900 dark:text-yellow-300',
|
| 124 |
-
)}
|
| 125 |
-
>
|
| 126 |
-
{log.message}
|
| 127 |
-
</h3>
|
| 128 |
-
{log.details && renderNotificationDetails(log.details as NotificationDetails)}
|
| 129 |
</div>
|
|
|
|
|
|
|
|
|
|
| 130 |
</div>
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
</div>
|
| 135 |
-
</motion.div>
|
| 136 |
-
))
|
| 137 |
)}
|
| 138 |
</div>
|
| 139 |
</div>
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
const NotificationsTab = () => {
|
| 18 |
+
const [filter, setFilter] = useState<'all' | 'error' | 'warning' | 'update'>('all');
|
| 19 |
const logs = useStore(logStore.logs);
|
| 20 |
|
| 21 |
const handleClearNotifications = () => {
|
|
|
|
| 29 |
const filteredLogs = Object.values(logs)
|
| 30 |
.filter((log) => {
|
| 31 |
if (filter === 'all') {
|
| 32 |
+
return log.level === 'error' || log.level === 'warning' || log.details?.type === 'update';
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
if (filter === 'update') {
|
| 36 |
+
return log.details?.type === 'update';
|
| 37 |
}
|
| 38 |
|
| 39 |
return log.level === filter;
|
| 40 |
})
|
| 41 |
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
| 42 |
|
| 43 |
+
const getNotificationStyle = (log: (typeof filteredLogs)[0]) => {
|
| 44 |
+
if (log.details?.type === 'update') {
|
| 45 |
+
return {
|
| 46 |
+
border: 'border-purple-200 dark:border-purple-900/50',
|
| 47 |
+
bg: 'bg-purple-50 dark:bg-purple-900/20',
|
| 48 |
+
icon: 'i-ph:arrow-circle-up text-purple-600 dark:text-purple-400',
|
| 49 |
+
text: 'text-purple-900 dark:text-purple-300',
|
| 50 |
+
};
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if (log.level === 'error') {
|
| 54 |
+
return {
|
| 55 |
+
border: 'border-red-200 dark:border-red-900/50',
|
| 56 |
+
bg: 'bg-red-50 dark:bg-red-900/20',
|
| 57 |
+
icon: 'i-ph:warning-circle text-red-600 dark:text-red-400',
|
| 58 |
+
text: 'text-red-900 dark:text-red-300',
|
| 59 |
+
};
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return {
|
| 63 |
+
border: 'border-yellow-200 dark:border-yellow-900/50',
|
| 64 |
+
bg: 'bg-yellow-50 dark:bg-yellow-900/20',
|
| 65 |
+
icon: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
|
| 66 |
+
text: 'text-yellow-900 dark:text-yellow-300',
|
| 67 |
+
};
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
const renderNotificationDetails = (details: NotificationDetails) => {
|
| 71 |
if (details.type === 'update') {
|
| 72 |
return (
|
|
|
|
| 79 |
</div>
|
| 80 |
<button
|
| 81 |
onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
|
| 82 |
+
className="mt-2 inline-flex items-center gap-2 rounded-md bg-purple-50 px-3 py-1.5 text-sm font-medium text-purple-600 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/30"
|
| 83 |
>
|
| 84 |
<span className="i-ph:git-branch text-lg" />
|
| 85 |
View Changes
|
|
|
|
| 97 |
<div className="flex items-center gap-2">
|
| 98 |
<select
|
| 99 |
value={filter}
|
| 100 |
+
onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning' | 'update')}
|
| 101 |
className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
| 102 |
>
|
| 103 |
<option value="all">All Notifications</option>
|
| 104 |
+
<option value="update">Updates</option>
|
| 105 |
<option value="error">Errors</option>
|
| 106 |
<option value="warning">Warnings</option>
|
| 107 |
</select>
|
|
|
|
| 124 |
</div>
|
| 125 |
</div>
|
| 126 |
) : (
|
| 127 |
+
filteredLogs.map((log) => {
|
| 128 |
+
const style = getNotificationStyle(log);
|
| 129 |
+
return (
|
| 130 |
+
<motion.div
|
| 131 |
+
key={log.id}
|
| 132 |
+
initial={{ opacity: 0, y: 20 }}
|
| 133 |
+
animate={{ opacity: 1, y: 0 }}
|
| 134 |
+
className={classNames('flex flex-col gap-2 rounded-lg border p-4', style.border, style.bg)}
|
| 135 |
+
>
|
| 136 |
+
<div className="flex items-start justify-between gap-4">
|
| 137 |
+
<div className="flex items-center gap-3">
|
| 138 |
+
<span className={classNames('text-lg', style.icon)} />
|
| 139 |
+
<div>
|
| 140 |
+
<h3 className={classNames('text-sm font-medium', style.text)}>{log.message}</h3>
|
| 141 |
+
{log.details && renderNotificationDetails(log.details as NotificationDetails)}
|
| 142 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
+
<time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
|
| 145 |
+
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
| 146 |
+
</time>
|
| 147 |
</div>
|
| 148 |
+
</motion.div>
|
| 149 |
+
);
|
| 150 |
+
})
|
|
|
|
|
|
|
|
|
|
| 151 |
)}
|
| 152 |
</div>
|
| 153 |
</div>
|
app/components/settings/settings.types.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type TabType =
|
|
| 17 |
export type WindowType = 'user' | 'developer';
|
| 18 |
|
| 19 |
export interface UserProfile {
|
|
|
|
| 20 |
name: string;
|
| 21 |
email: string;
|
| 22 |
avatar?: string;
|
|
|
|
| 17 |
export type WindowType = 'user' | 'developer';
|
| 18 |
|
| 19 |
export interface UserProfile {
|
| 20 |
+
nickname: any;
|
| 21 |
name: string;
|
| 22 |
email: string;
|
| 23 |
avatar?: string;
|
app/components/settings/update/UpdateTab.tsx
CHANGED
|
@@ -1,11 +1,24 @@
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
-
import { motion } from 'framer-motion';
|
| 3 |
import { useSettings } from '~/lib/hooks/useSettings';
|
| 4 |
import { logStore } from '~/lib/stores/logs';
|
| 5 |
import { classNames } from '~/utils/classNames';
|
|
|
|
| 6 |
|
| 7 |
interface GitHubCommitResponse {
|
| 8 |
sha: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
interface UpdateInfo {
|
|
@@ -13,26 +26,136 @@ interface UpdateInfo {
|
|
| 13 |
latestVersion: string;
|
| 14 |
branch: string;
|
| 15 |
hasUpdate: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const GITHUB_URLS = {
|
| 19 |
-
commitJson: async (branch: string): Promise<UpdateInfo> => {
|
| 20 |
try {
|
| 21 |
-
const
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
const
|
| 25 |
-
const remoteCommitHash = data.sha.slice(0, 7);
|
| 26 |
|
| 27 |
return {
|
| 28 |
-
currentVersion:
|
| 29 |
-
latestVersion:
|
| 30 |
branch,
|
| 31 |
-
hasUpdate:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
};
|
| 33 |
} catch (error) {
|
| 34 |
-
console.error('
|
| 35 |
-
throw
|
| 36 |
}
|
| 37 |
},
|
| 38 |
};
|
|
@@ -41,19 +164,71 @@ const UpdateTab = () => {
|
|
| 41 |
const { isLatestBranch } = useSettings();
|
| 42 |
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
| 43 |
const [isChecking, setIsChecking] = useState(false);
|
|
|
|
| 44 |
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
const checkForUpdates = async () => {
|
| 47 |
setIsChecking(true);
|
| 48 |
setError(null);
|
| 49 |
|
| 50 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
| 52 |
-
const info = await GITHUB_URLS.commitJson(branchToCheck);
|
| 53 |
setUpdateInfo(info);
|
| 54 |
|
| 55 |
if (info.hasUpdate) {
|
| 56 |
-
// Add update notification only if it doesn't already exist
|
| 57 |
const existingLogs = Object.values(logStore.logs.get());
|
| 58 |
const hasUpdateNotification = existingLogs.some(
|
| 59 |
(log) =>
|
|
@@ -62,7 +237,7 @@ const UpdateTab = () => {
|
|
| 62 |
log.details.latestVersion === info.latestVersion,
|
| 63 |
);
|
| 64 |
|
| 65 |
-
if (!hasUpdateNotification) {
|
| 66 |
logStore.logWarning('Update Available', {
|
| 67 |
currentVersion: info.currentVersion,
|
| 68 |
latestVersion: info.latestVersion,
|
|
@@ -71,29 +246,123 @@ const UpdateTab = () => {
|
|
| 71 |
message: `A new version is available on the ${branchToCheck} branch`,
|
| 72 |
updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
|
| 73 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
| 76 |
} catch (err) {
|
| 77 |
setError('Failed to check for updates. Please try again later.');
|
| 78 |
console.error('Update check failed:', err);
|
|
|
|
| 79 |
} finally {
|
| 80 |
setIsChecking(false);
|
| 81 |
}
|
| 82 |
};
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
useEffect(() => {
|
| 85 |
checkForUpdates();
|
| 86 |
}, [isLatestBranch]);
|
| 87 |
|
| 88 |
-
const handleViewChanges = () => {
|
| 89 |
-
if (updateInfo) {
|
| 90 |
-
window.open(
|
| 91 |
-
`https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`,
|
| 92 |
-
'_blank',
|
| 93 |
-
);
|
| 94 |
-
}
|
| 95 |
-
};
|
| 96 |
-
|
| 97 |
return (
|
| 98 |
<div className="flex flex-col gap-6">
|
| 99 |
<motion.div
|
|
@@ -109,43 +378,130 @@ const UpdateTab = () => {
|
|
| 109 |
</div>
|
| 110 |
</motion.div>
|
| 111 |
|
|
|
|
| 112 |
<motion.div
|
| 113 |
-
className="
|
| 114 |
initial={{ opacity: 0, y: 20 }}
|
| 115 |
animate={{ opacity: 1, y: 0 }}
|
| 116 |
transition={{ duration: 0.3, delay: 0.1 }}
|
| 117 |
>
|
| 118 |
-
<div className="flex items-center
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
<div className="flex items-center gap-4">
|
| 120 |
<span className="text-sm text-bolt-elements-textSecondary">
|
| 121 |
Currently on {isLatestBranch ? 'main' : 'stable'} branch
|
| 122 |
</span>
|
| 123 |
{updateInfo && (
|
| 124 |
-
<span className="text-xs text-bolt-elements-textTertiary">
|
|
|
|
|
|
|
| 125 |
)}
|
| 126 |
</div>
|
| 127 |
<button
|
| 128 |
-
onClick={
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
className={classNames(
|
| 131 |
-
'px-3 py-2 rounded-lg text-sm',
|
| 132 |
-
'bg-
|
|
|
|
| 133 |
'text-bolt-elements-textPrimary',
|
| 134 |
-
'
|
| 135 |
-
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
| 136 |
-
'transition-all duration-200',
|
| 137 |
'disabled:opacity-50 disabled:cursor-not-allowed',
|
| 138 |
)}
|
| 139 |
>
|
| 140 |
-
<div className=
|
| 141 |
-
|
| 142 |
-
{isChecking ? 'Checking...' : 'Check for Updates'}
|
| 143 |
-
</div>
|
| 144 |
</button>
|
| 145 |
</div>
|
| 146 |
|
| 147 |
{error && (
|
| 148 |
-
<div className="p-4 rounded-lg bg-red-
|
| 149 |
<div className="flex items-center gap-2">
|
| 150 |
<div className="i-ph:warning-circle" />
|
| 151 |
{error}
|
|
@@ -156,60 +512,250 @@ const UpdateTab = () => {
|
|
| 156 |
{updateInfo && (
|
| 157 |
<div
|
| 158 |
className={classNames(
|
| 159 |
-
'p-4 rounded-lg
|
| 160 |
updateInfo.hasUpdate
|
| 161 |
-
? 'bg-
|
| 162 |
-
: 'bg-green-
|
| 163 |
)}
|
| 164 |
>
|
| 165 |
-
<div className="flex items-
|
| 166 |
-
<
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
: 'text-green-900 dark:text-green-300',
|
| 182 |
-
)}
|
| 183 |
-
>
|
| 184 |
-
{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
|
| 185 |
-
</h3>
|
| 186 |
-
<p className="text-sm text-bolt-elements-textSecondary mt-1">
|
| 187 |
-
{updateInfo.hasUpdate
|
| 188 |
-
? `A new version is available on the ${updateInfo.branch} branch`
|
| 189 |
-
: 'You are running the latest version'}
|
| 190 |
-
</p>
|
| 191 |
-
{updateInfo.hasUpdate && (
|
| 192 |
-
<div className="mt-2 flex flex-col gap-1 text-xs text-bolt-elements-textTertiary">
|
| 193 |
-
<p>Current Version: {updateInfo.currentVersion}</p>
|
| 194 |
-
<p>Latest Version: {updateInfo.latestVersion}</p>
|
| 195 |
-
<p>Branch: {updateInfo.branch}</p>
|
| 196 |
-
</div>
|
| 197 |
-
)}
|
| 198 |
-
</div>
|
| 199 |
</div>
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
<button
|
| 202 |
-
onClick={
|
| 203 |
-
className="
|
| 204 |
>
|
| 205 |
-
<
|
| 206 |
-
View
|
| 207 |
</button>
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
</div>
|
|
|
|
| 210 |
</div>
|
| 211 |
-
|
| 212 |
-
|
| 213 |
</div>
|
| 214 |
);
|
| 215 |
};
|
|
|
|
| 1 |
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import { useSettings } from '~/lib/hooks/useSettings';
|
| 4 |
import { logStore } from '~/lib/stores/logs';
|
| 5 |
import { classNames } from '~/utils/classNames';
|
| 6 |
+
import { toast } from 'react-toastify';
|
| 7 |
|
| 8 |
interface GitHubCommitResponse {
|
| 9 |
sha: string;
|
| 10 |
+
commit: {
|
| 11 |
+
message: string;
|
| 12 |
+
};
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
interface GitHubReleaseResponse {
|
| 16 |
+
tag_name: string;
|
| 17 |
+
body: string;
|
| 18 |
+
assets: Array<{
|
| 19 |
+
size: number;
|
| 20 |
+
browser_download_url: string;
|
| 21 |
+
}>;
|
| 22 |
}
|
| 23 |
|
| 24 |
interface UpdateInfo {
|
|
|
|
| 26 |
latestVersion: string;
|
| 27 |
branch: string;
|
| 28 |
hasUpdate: boolean;
|
| 29 |
+
releaseNotes?: string;
|
| 30 |
+
downloadSize?: string;
|
| 31 |
+
changelog?: string[];
|
| 32 |
+
currentCommit?: string;
|
| 33 |
+
latestCommit?: string;
|
| 34 |
+
downloadProgress?: number;
|
| 35 |
+
installProgress?: number;
|
| 36 |
+
estimatedTimeRemaining?: number;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
interface UpdateSettings {
|
| 40 |
+
autoUpdate: boolean;
|
| 41 |
+
notifyInApp: boolean;
|
| 42 |
+
checkInterval: number;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
interface UpdateResponse {
|
| 46 |
+
success: boolean;
|
| 47 |
+
error?: string;
|
| 48 |
+
progress?: {
|
| 49 |
+
downloaded: number;
|
| 50 |
+
total: number;
|
| 51 |
+
stage: 'download' | 'install' | 'complete';
|
| 52 |
+
};
|
| 53 |
}
|
| 54 |
|
| 55 |
+
const categorizeChangelog = (messages: string[]) => {
|
| 56 |
+
const categories = new Map<string, string[]>();
|
| 57 |
+
|
| 58 |
+
messages.forEach((message) => {
|
| 59 |
+
let category = 'Other';
|
| 60 |
+
|
| 61 |
+
if (message.startsWith('feat:')) {
|
| 62 |
+
category = 'Features';
|
| 63 |
+
} else if (message.startsWith('fix:')) {
|
| 64 |
+
category = 'Bug Fixes';
|
| 65 |
+
} else if (message.startsWith('docs:')) {
|
| 66 |
+
category = 'Documentation';
|
| 67 |
+
} else if (message.startsWith('ci:')) {
|
| 68 |
+
category = 'CI Improvements';
|
| 69 |
+
} else if (message.startsWith('refactor:')) {
|
| 70 |
+
category = 'Refactoring';
|
| 71 |
+
} else if (message.startsWith('test:')) {
|
| 72 |
+
category = 'Testing';
|
| 73 |
+
} else if (message.startsWith('style:')) {
|
| 74 |
+
category = 'Styling';
|
| 75 |
+
} else if (message.startsWith('perf:')) {
|
| 76 |
+
category = 'Performance';
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
if (!categories.has(category)) {
|
| 80 |
+
categories.set(category, []);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
categories.get(category)!.push(message);
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
const order = [
|
| 87 |
+
'Features',
|
| 88 |
+
'Bug Fixes',
|
| 89 |
+
'Documentation',
|
| 90 |
+
'CI Improvements',
|
| 91 |
+
'Refactoring',
|
| 92 |
+
'Performance',
|
| 93 |
+
'Testing',
|
| 94 |
+
'Styling',
|
| 95 |
+
'Other',
|
| 96 |
+
];
|
| 97 |
+
|
| 98 |
+
return Array.from(categories.entries())
|
| 99 |
+
.sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))
|
| 100 |
+
.filter(([_, messages]) => messages.length > 0);
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const parseCommitMessage = (message: string) => {
|
| 104 |
+
const prMatch = message.match(/#(\d+)/);
|
| 105 |
+
const prNumber = prMatch ? prMatch[1] : null;
|
| 106 |
+
|
| 107 |
+
let cleanMessage = message.replace(/^[a-z]+:\s*/i, '');
|
| 108 |
+
cleanMessage = cleanMessage.replace(/#\d+/g, '').trim();
|
| 109 |
+
|
| 110 |
+
const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/);
|
| 111 |
+
const title = parts[0].trim();
|
| 112 |
+
const description = parts
|
| 113 |
+
.slice(1)
|
| 114 |
+
.map((p) => p.trim())
|
| 115 |
+
.filter((p) => p && !p.includes('Co-authored-by:'))
|
| 116 |
+
.join('\n');
|
| 117 |
+
|
| 118 |
+
return { title, description, prNumber };
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
const GITHUB_URLS = {
|
| 122 |
+
commitJson: async (branch: string, headers: HeadersInit = {}): Promise<UpdateInfo> => {
|
| 123 |
try {
|
| 124 |
+
const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([
|
| 125 |
+
fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }),
|
| 126 |
+
fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }),
|
| 127 |
+
fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }),
|
| 128 |
+
]);
|
| 129 |
+
|
| 130 |
+
if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) {
|
| 131 |
+
throw new Error(
|
| 132 |
+
`GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`,
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const commitData = (await commitResponse.json()) as GitHubCommitResponse;
|
| 137 |
+
const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse;
|
| 138 |
+
const commits = (await changelogResponse.json()) as GitHubCommitResponse[];
|
| 139 |
+
|
| 140 |
+
const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0;
|
| 141 |
+
const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB';
|
| 142 |
|
| 143 |
+
const changelog = commits.map((commit) => commit.commit.message);
|
|
|
|
| 144 |
|
| 145 |
return {
|
| 146 |
+
currentVersion: process.env.APP_VERSION || 'unknown',
|
| 147 |
+
latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7),
|
| 148 |
branch,
|
| 149 |
+
hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT,
|
| 150 |
+
releaseNotes: releaseData.body || '',
|
| 151 |
+
downloadSize,
|
| 152 |
+
changelog,
|
| 153 |
+
currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7),
|
| 154 |
+
latestCommit: commitData.sha.substring(0, 7),
|
| 155 |
};
|
| 156 |
} catch (error) {
|
| 157 |
+
console.error('Error fetching update info:', error);
|
| 158 |
+
throw error;
|
| 159 |
}
|
| 160 |
},
|
| 161 |
};
|
|
|
|
| 164 |
const { isLatestBranch } = useSettings();
|
| 165 |
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
| 166 |
const [isChecking, setIsChecking] = useState(false);
|
| 167 |
+
const [isUpdating, setIsUpdating] = useState(false);
|
| 168 |
const [error, setError] = useState<string | null>(null);
|
| 169 |
+
const [retryCount, setRetryCount] = useState(0);
|
| 170 |
+
const [showChangelog, setShowChangelog] = useState(false);
|
| 171 |
+
const [showManualInstructions, setShowManualInstructions] = useState(false);
|
| 172 |
+
const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
|
| 173 |
+
const [updateFailed, setUpdateFailed] = useState(false);
|
| 174 |
+
const [updateSettings, setUpdateSettings] = useState<UpdateSettings>(() => {
|
| 175 |
+
const stored = localStorage.getItem('update_settings');
|
| 176 |
+
return stored
|
| 177 |
+
? JSON.parse(stored)
|
| 178 |
+
: {
|
| 179 |
+
autoUpdate: false,
|
| 180 |
+
notifyInApp: true,
|
| 181 |
+
checkInterval: 24,
|
| 182 |
+
};
|
| 183 |
+
});
|
| 184 |
+
|
| 185 |
+
useEffect(() => {
|
| 186 |
+
localStorage.setItem('update_settings', JSON.stringify(updateSettings));
|
| 187 |
+
}, [updateSettings]);
|
| 188 |
+
|
| 189 |
+
const handleUpdateProgress = async (response: Response): Promise<void> => {
|
| 190 |
+
const reader = response.body?.getReader();
|
| 191 |
+
|
| 192 |
+
if (!reader) {
|
| 193 |
+
return;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
const contentLength = +(response.headers.get('Content-Length') ?? 0);
|
| 197 |
+
let receivedLength = 0;
|
| 198 |
+
|
| 199 |
+
while (true) {
|
| 200 |
+
const { done, value } = await reader.read();
|
| 201 |
+
|
| 202 |
+
if (done) {
|
| 203 |
+
break;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
receivedLength += value.length;
|
| 207 |
+
|
| 208 |
+
const progress = (receivedLength / contentLength) * 100;
|
| 209 |
+
|
| 210 |
+
setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev));
|
| 211 |
+
}
|
| 212 |
+
};
|
| 213 |
|
| 214 |
const checkForUpdates = async () => {
|
| 215 |
setIsChecking(true);
|
| 216 |
setError(null);
|
| 217 |
|
| 218 |
try {
|
| 219 |
+
const githubToken = localStorage.getItem('github_connection');
|
| 220 |
+
const headers: HeadersInit = {};
|
| 221 |
+
|
| 222 |
+
if (githubToken) {
|
| 223 |
+
const { token } = JSON.parse(githubToken);
|
| 224 |
+
headers.Authorization = `Bearer ${token}`;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
| 228 |
+
const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
|
| 229 |
setUpdateInfo(info);
|
| 230 |
|
| 231 |
if (info.hasUpdate) {
|
|
|
|
| 232 |
const existingLogs = Object.values(logStore.logs.get());
|
| 233 |
const hasUpdateNotification = existingLogs.some(
|
| 234 |
(log) =>
|
|
|
|
| 237 |
log.details.latestVersion === info.latestVersion,
|
| 238 |
);
|
| 239 |
|
| 240 |
+
if (!hasUpdateNotification && updateSettings.notifyInApp) {
|
| 241 |
logStore.logWarning('Update Available', {
|
| 242 |
currentVersion: info.currentVersion,
|
| 243 |
latestVersion: info.latestVersion,
|
|
|
|
| 246 |
message: `A new version is available on the ${branchToCheck} branch`,
|
| 247 |
updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
|
| 248 |
});
|
| 249 |
+
|
| 250 |
+
if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
|
| 251 |
+
const changelogText = info.changelog?.join('\n') || 'No changelog available';
|
| 252 |
+
const userWantsUpdate = confirm(
|
| 253 |
+
`An update is available.\n\nChangelog:\n${changelogText}\n\nDo you want to update now?`,
|
| 254 |
+
);
|
| 255 |
+
setHasUserRespondedToUpdate(true);
|
| 256 |
+
|
| 257 |
+
if (userWantsUpdate) {
|
| 258 |
+
await initiateUpdate();
|
| 259 |
+
} else {
|
| 260 |
+
logStore.logSystem('Update cancelled by user');
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
}
|
| 264 |
}
|
| 265 |
} catch (err) {
|
| 266 |
setError('Failed to check for updates. Please try again later.');
|
| 267 |
console.error('Update check failed:', err);
|
| 268 |
+
setUpdateFailed(true);
|
| 269 |
} finally {
|
| 270 |
setIsChecking(false);
|
| 271 |
}
|
| 272 |
};
|
| 273 |
|
| 274 |
+
const initiateUpdate = async () => {
|
| 275 |
+
setIsUpdating(true);
|
| 276 |
+
setError(null);
|
| 277 |
+
|
| 278 |
+
let currentRetry = 0;
|
| 279 |
+
const maxRetries = 3;
|
| 280 |
+
|
| 281 |
+
const attemptUpdate = async (): Promise<void> => {
|
| 282 |
+
try {
|
| 283 |
+
const platform = process.platform;
|
| 284 |
+
|
| 285 |
+
if (platform === 'darwin' || platform === 'linux') {
|
| 286 |
+
const response = await fetch('/api/update', {
|
| 287 |
+
method: 'POST',
|
| 288 |
+
headers: {
|
| 289 |
+
'Content-Type': 'application/json',
|
| 290 |
+
},
|
| 291 |
+
body: JSON.stringify({
|
| 292 |
+
branch: isLatestBranch ? 'main' : 'stable',
|
| 293 |
+
settings: updateSettings,
|
| 294 |
+
}),
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
if (!response.ok) {
|
| 298 |
+
throw new Error('Failed to initiate update');
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
await handleUpdateProgress(response);
|
| 302 |
+
|
| 303 |
+
const result = (await response.json()) as UpdateResponse;
|
| 304 |
+
|
| 305 |
+
if (result.success) {
|
| 306 |
+
logStore.logSuccess('Update downloaded successfully', {
|
| 307 |
+
type: 'update',
|
| 308 |
+
message: 'Update completed successfully.',
|
| 309 |
+
});
|
| 310 |
+
toast.success('Update completed successfully!');
|
| 311 |
+
setUpdateFailed(false);
|
| 312 |
+
|
| 313 |
+
return;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
throw new Error(result.error || 'Update failed');
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
window.open('https://github.com/stackblitz-labs/bolt.diy/releases/latest', '_blank');
|
| 320 |
+
logStore.logInfo('Manual update required', {
|
| 321 |
+
type: 'update',
|
| 322 |
+
message: 'Please download and install the latest version from the GitHub releases page.',
|
| 323 |
+
});
|
| 324 |
+
|
| 325 |
+
return;
|
| 326 |
+
} catch (err) {
|
| 327 |
+
currentRetry++;
|
| 328 |
+
|
| 329 |
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
| 330 |
+
|
| 331 |
+
if (currentRetry < maxRetries) {
|
| 332 |
+
toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 });
|
| 333 |
+
setRetryCount(currentRetry);
|
| 334 |
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
| 335 |
+
await attemptUpdate();
|
| 336 |
+
|
| 337 |
+
return;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
setError('Failed to initiate update. Please try again or update manually.');
|
| 341 |
+
console.error('Update failed:', err);
|
| 342 |
+
logStore.logSystem('Update failed: ' + errorMessage);
|
| 343 |
+
toast.error('Update failed: ' + errorMessage);
|
| 344 |
+
setUpdateFailed(true);
|
| 345 |
+
|
| 346 |
+
return;
|
| 347 |
+
}
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
await attemptUpdate();
|
| 351 |
+
setIsUpdating(false);
|
| 352 |
+
setRetryCount(0);
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
+
useEffect(() => {
|
| 356 |
+
const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
|
| 357 |
+
const intervalId = setInterval(checkForUpdates, checkInterval);
|
| 358 |
+
|
| 359 |
+
return () => clearInterval(intervalId);
|
| 360 |
+
}, [updateSettings.checkInterval, isLatestBranch]);
|
| 361 |
+
|
| 362 |
useEffect(() => {
|
| 363 |
checkForUpdates();
|
| 364 |
}, [isLatestBranch]);
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
return (
|
| 367 |
<div className="flex flex-col gap-6">
|
| 368 |
<motion.div
|
|
|
|
| 378 |
</div>
|
| 379 |
</motion.div>
|
| 380 |
|
| 381 |
+
{/* Update Settings Card */}
|
| 382 |
<motion.div
|
| 383 |
+
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 384 |
initial={{ opacity: 0, y: 20 }}
|
| 385 |
animate={{ opacity: 1, y: 0 }}
|
| 386 |
transition={{ duration: 0.3, delay: 0.1 }}
|
| 387 |
>
|
| 388 |
+
<div className="flex items-center gap-3 mb-6">
|
| 389 |
+
<div className="i-ph:gear text-purple-500 w-5 h-5" />
|
| 390 |
+
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Settings</h3>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
+
<div className="space-y-4">
|
| 394 |
+
<div className="flex items-center justify-between">
|
| 395 |
+
<div>
|
| 396 |
+
<span className="text-sm text-bolt-elements-textPrimary">Automatic Updates</span>
|
| 397 |
+
<p className="text-xs text-bolt-elements-textSecondary">
|
| 398 |
+
Automatically check and apply updates when available
|
| 399 |
+
</p>
|
| 400 |
+
</div>
|
| 401 |
+
<button
|
| 402 |
+
onClick={() => setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
|
| 403 |
+
className={classNames(
|
| 404 |
+
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
| 405 |
+
updateSettings.autoUpdate ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
|
| 406 |
+
)}
|
| 407 |
+
>
|
| 408 |
+
<span
|
| 409 |
+
className={classNames(
|
| 410 |
+
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
| 411 |
+
updateSettings.autoUpdate ? 'translate-x-6' : 'translate-x-1',
|
| 412 |
+
)}
|
| 413 |
+
/>
|
| 414 |
+
</button>
|
| 415 |
+
</div>
|
| 416 |
+
|
| 417 |
+
<div className="flex items-center justify-between">
|
| 418 |
+
<div>
|
| 419 |
+
<span className="text-sm text-bolt-elements-textPrimary">In-App Notifications</span>
|
| 420 |
+
<p className="text-xs text-bolt-elements-textSecondary">Show notifications when updates are available</p>
|
| 421 |
+
</div>
|
| 422 |
+
<button
|
| 423 |
+
onClick={() => setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
|
| 424 |
+
className={classNames(
|
| 425 |
+
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
|
| 426 |
+
updateSettings.notifyInApp ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
|
| 427 |
+
)}
|
| 428 |
+
>
|
| 429 |
+
<span
|
| 430 |
+
className={classNames(
|
| 431 |
+
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
|
| 432 |
+
updateSettings.notifyInApp ? 'translate-x-6' : 'translate-x-1',
|
| 433 |
+
)}
|
| 434 |
+
/>
|
| 435 |
+
</button>
|
| 436 |
+
</div>
|
| 437 |
+
|
| 438 |
+
<div className="flex items-center justify-between">
|
| 439 |
+
<div>
|
| 440 |
+
<span className="text-sm text-bolt-elements-textPrimary">Check Interval</span>
|
| 441 |
+
<p className="text-xs text-bolt-elements-textSecondary">How often to check for updates</p>
|
| 442 |
+
</div>
|
| 443 |
+
<select
|
| 444 |
+
value={updateSettings.checkInterval}
|
| 445 |
+
onChange={(e) => setUpdateSettings((prev) => ({ ...prev, checkInterval: Number(e.target.value) }))}
|
| 446 |
+
className={classNames(
|
| 447 |
+
'px-3 py-2 rounded-lg text-sm',
|
| 448 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 449 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
| 450 |
+
'text-bolt-elements-textPrimary',
|
| 451 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 452 |
+
'transition-colors duration-200',
|
| 453 |
+
)}
|
| 454 |
+
>
|
| 455 |
+
<option value="6">6 hours</option>
|
| 456 |
+
<option value="12">12 hours</option>
|
| 457 |
+
<option value="24">24 hours</option>
|
| 458 |
+
<option value="48">48 hours</option>
|
| 459 |
+
</select>
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
</motion.div>
|
| 463 |
+
|
| 464 |
+
{/* Update Status Card */}
|
| 465 |
+
<motion.div
|
| 466 |
+
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 467 |
+
initial={{ opacity: 0, y: 20 }}
|
| 468 |
+
animate={{ opacity: 1, y: 0 }}
|
| 469 |
+
transition={{ duration: 0.3, delay: 0.2 }}
|
| 470 |
+
>
|
| 471 |
+
<div className="flex items-center justify-between mb-4">
|
| 472 |
<div className="flex items-center gap-4">
|
| 473 |
<span className="text-sm text-bolt-elements-textSecondary">
|
| 474 |
Currently on {isLatestBranch ? 'main' : 'stable'} branch
|
| 475 |
</span>
|
| 476 |
{updateInfo && (
|
| 477 |
+
<span className="text-xs text-bolt-elements-textTertiary">
|
| 478 |
+
Version: {updateInfo.currentVersion} ({updateInfo.currentCommit})
|
| 479 |
+
</span>
|
| 480 |
)}
|
| 481 |
</div>
|
| 482 |
<button
|
| 483 |
+
onClick={() => {
|
| 484 |
+
setHasUserRespondedToUpdate(false);
|
| 485 |
+
setUpdateFailed(false);
|
| 486 |
+
checkForUpdates();
|
| 487 |
+
}}
|
| 488 |
+
disabled={isChecking || (updateFailed && !hasUserRespondedToUpdate)}
|
| 489 |
className={classNames(
|
| 490 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
| 491 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 492 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 493 |
'text-bolt-elements-textPrimary',
|
| 494 |
+
'transition-colors duration-200',
|
|
|
|
|
|
|
| 495 |
'disabled:opacity-50 disabled:cursor-not-allowed',
|
| 496 |
)}
|
| 497 |
>
|
| 498 |
+
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', isChecking ? 'animate-spin' : '')} />
|
| 499 |
+
{isChecking ? 'Checking...' : 'Check for Updates'}
|
|
|
|
|
|
|
| 500 |
</button>
|
| 501 |
</div>
|
| 502 |
|
| 503 |
{error && (
|
| 504 |
+
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
|
| 505 |
<div className="flex items-center gap-2">
|
| 506 |
<div className="i-ph:warning-circle" />
|
| 507 |
{error}
|
|
|
|
| 512 |
{updateInfo && (
|
| 513 |
<div
|
| 514 |
className={classNames(
|
| 515 |
+
'p-4 rounded-lg',
|
| 516 |
updateInfo.hasUpdate
|
| 517 |
+
? 'bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20'
|
| 518 |
+
: 'bg-green-500/5 dark:bg-green-500/10 border border-green-500/20',
|
| 519 |
)}
|
| 520 |
>
|
| 521 |
+
<div className="flex items-center gap-3">
|
| 522 |
+
<span
|
| 523 |
+
className={classNames(
|
| 524 |
+
'text-lg',
|
| 525 |
+
updateInfo.hasUpdate ? 'i-ph:warning text-purple-500' : 'i-ph:check-circle text-green-500',
|
| 526 |
+
)}
|
| 527 |
+
/>
|
| 528 |
+
<div>
|
| 529 |
+
<h4 className="font-medium text-bolt-elements-textPrimary">
|
| 530 |
+
{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
|
| 531 |
+
</h4>
|
| 532 |
+
<p className="text-sm text-bolt-elements-textSecondary">
|
| 533 |
+
{updateInfo.hasUpdate
|
| 534 |
+
? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available`
|
| 535 |
+
: 'You are running the latest version'}
|
| 536 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
)}
|
| 541 |
+
</motion.div>
|
| 542 |
+
|
| 543 |
+
{/* Update Details Card */}
|
| 544 |
+
{updateInfo && updateInfo.hasUpdate && (
|
| 545 |
+
<motion.div
|
| 546 |
+
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 547 |
+
initial={{ opacity: 0, y: 20 }}
|
| 548 |
+
animate={{ opacity: 1, y: 0 }}
|
| 549 |
+
transition={{ duration: 0.3, delay: 0.2 }}
|
| 550 |
+
>
|
| 551 |
+
<div className="flex items-center justify-between mb-6">
|
| 552 |
+
<div className="flex items-center gap-3">
|
| 553 |
+
<div className="i-ph:arrow-circle-up text-purple-500 w-5 h-5" />
|
| 554 |
+
<span className="text-sm font-medium text-bolt-elements-textPrimary">
|
| 555 |
+
Version {updateInfo.latestVersion}
|
| 556 |
+
</span>
|
| 557 |
+
</div>
|
| 558 |
+
<span className="text-xs px-3 py-1 rounded-full bg-purple-500/10 text-purple-500">
|
| 559 |
+
{updateInfo.downloadSize}
|
| 560 |
+
</span>
|
| 561 |
+
</div>
|
| 562 |
+
|
| 563 |
+
{/* Update Options */}
|
| 564 |
+
<div className="flex flex-col gap-4">
|
| 565 |
+
<div className="flex items-center gap-3">
|
| 566 |
+
<button
|
| 567 |
+
onClick={initiateUpdate}
|
| 568 |
+
disabled={isUpdating || updateFailed}
|
| 569 |
+
className={classNames(
|
| 570 |
+
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
| 571 |
+
'bg-purple-500 hover:bg-purple-600',
|
| 572 |
+
'text-white',
|
| 573 |
+
'transition-all duration-200',
|
| 574 |
+
'hover:shadow-lg hover:shadow-purple-500/20',
|
| 575 |
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
| 576 |
+
)}
|
| 577 |
+
>
|
| 578 |
+
<div className={classNames('i-ph:arrow-circle-up w-4 h-4', isUpdating ? 'animate-spin' : '')} />
|
| 579 |
+
{isUpdating ? 'Updating...' : 'Auto Update'}
|
| 580 |
+
</button>
|
| 581 |
+
<button
|
| 582 |
+
onClick={() => setShowManualInstructions(!showManualInstructions)}
|
| 583 |
+
className={classNames(
|
| 584 |
+
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
|
| 585 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 586 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
|
| 587 |
+
'text-bolt-elements-textPrimary',
|
| 588 |
+
'transition-all duration-200',
|
| 589 |
+
)}
|
| 590 |
+
>
|
| 591 |
+
<div className="i-ph:book-open w-4 h-4" />
|
| 592 |
+
{showManualInstructions ? 'Hide Instructions' : 'Manual Update'}
|
| 593 |
+
</button>
|
| 594 |
+
</div>
|
| 595 |
+
|
| 596 |
+
{/* Manual Update Instructions */}
|
| 597 |
+
<AnimatePresence>
|
| 598 |
+
{showManualInstructions && (
|
| 599 |
+
<motion.div
|
| 600 |
+
initial={{ opacity: 0, height: 0 }}
|
| 601 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 602 |
+
exit={{ opacity: 0, height: 0 }}
|
| 603 |
+
className="space-y-6 text-bolt-elements-textSecondary"
|
| 604 |
+
>
|
| 605 |
+
<div className="p-4 rounded-lg bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20">
|
| 606 |
+
<p className="font-medium text-purple-500">
|
| 607 |
+
Update available from {isLatestBranch ? 'main' : 'stable'} branch!
|
| 608 |
+
</p>
|
| 609 |
+
<div className="mt-2 space-y-1">
|
| 610 |
+
<p>
|
| 611 |
+
Current: {updateInfo.currentVersion} ({updateInfo.currentCommit})
|
| 612 |
+
</p>
|
| 613 |
+
<p>
|
| 614 |
+
Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit})
|
| 615 |
+
</p>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
|
| 619 |
+
<div>
|
| 620 |
+
<h4 className="text-base font-medium text-bolt-elements-textPrimary mb-3">To update:</h4>
|
| 621 |
+
<ol className="space-y-4">
|
| 622 |
+
<li className="flex items-start gap-3">
|
| 623 |
+
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
|
| 624 |
+
1
|
| 625 |
+
</div>
|
| 626 |
+
<div>
|
| 627 |
+
<p className="font-medium text-bolt-elements-textPrimary">Pull the latest changes:</p>
|
| 628 |
+
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
|
| 629 |
+
git pull upstream {isLatestBranch ? 'main' : 'stable'}
|
| 630 |
+
</code>
|
| 631 |
+
</div>
|
| 632 |
+
</li>
|
| 633 |
+
<li className="flex items-start gap-3">
|
| 634 |
+
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
|
| 635 |
+
2
|
| 636 |
+
</div>
|
| 637 |
+
<div>
|
| 638 |
+
<p className="font-medium text-bolt-elements-textPrimary">Install dependencies:</p>
|
| 639 |
+
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
|
| 640 |
+
pnpm install
|
| 641 |
+
</code>
|
| 642 |
+
</div>
|
| 643 |
+
</li>
|
| 644 |
+
<li className="flex items-start gap-3">
|
| 645 |
+
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
|
| 646 |
+
3
|
| 647 |
+
</div>
|
| 648 |
+
<div>
|
| 649 |
+
<p className="font-medium text-bolt-elements-textPrimary">Build the application:</p>
|
| 650 |
+
<code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
|
| 651 |
+
pnpm build
|
| 652 |
+
</code>
|
| 653 |
+
</div>
|
| 654 |
+
</li>
|
| 655 |
+
<li className="flex items-start gap-3">
|
| 656 |
+
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
|
| 657 |
+
4
|
| 658 |
+
</div>
|
| 659 |
+
<p className="font-medium text-bolt-elements-textPrimary">Restart the application</p>
|
| 660 |
+
</li>
|
| 661 |
+
</ol>
|
| 662 |
+
</div>
|
| 663 |
+
</motion.div>
|
| 664 |
+
)}
|
| 665 |
+
</AnimatePresence>
|
| 666 |
+
|
| 667 |
+
{/* Changelog */}
|
| 668 |
+
{updateInfo.changelog && updateInfo.changelog.length > 0 && (
|
| 669 |
+
<div className="mt-4">
|
| 670 |
<button
|
| 671 |
+
onClick={() => setShowChangelog(!showChangelog)}
|
| 672 |
+
className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary hover:text-purple-500 transition-colors"
|
| 673 |
>
|
| 674 |
+
<div className={`i-ph:${showChangelog ? 'caret-up' : 'caret-down'} w-4 h-4`} />
|
| 675 |
+
{showChangelog ? 'Hide Changelog' : 'View Changelog'}
|
| 676 |
</button>
|
| 677 |
+
|
| 678 |
+
<AnimatePresence>
|
| 679 |
+
{showChangelog && (
|
| 680 |
+
<motion.div
|
| 681 |
+
initial={{ opacity: 0, height: 0 }}
|
| 682 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 683 |
+
exit={{ opacity: 0, height: 0 }}
|
| 684 |
+
className="mt-4 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 685 |
+
>
|
| 686 |
+
<div className="max-h-[400px] overflow-y-auto">
|
| 687 |
+
{categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
|
| 688 |
+
<div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
|
| 689 |
+
<div className="p-3 bg-bolt-elements-bg-depth-4">
|
| 690 |
+
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">
|
| 691 |
+
{category}
|
| 692 |
+
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
|
| 693 |
+
({messages.length})
|
| 694 |
+
</span>
|
| 695 |
+
</h5>
|
| 696 |
+
</div>
|
| 697 |
+
<div className="divide-y divide-bolt-elements-borderColor">
|
| 698 |
+
{messages.map((message, index) => {
|
| 699 |
+
const { title, description, prNumber } = parseCommitMessage(message);
|
| 700 |
+
return (
|
| 701 |
+
<div key={index} className="p-3 hover:bg-bolt-elements-bg-depth-4 transition-colors">
|
| 702 |
+
<div className="flex items-start gap-3">
|
| 703 |
+
<div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-bolt-elements-textSecondary" />
|
| 704 |
+
<div className="space-y-1 flex-1">
|
| 705 |
+
<p className="text-sm font-medium text-bolt-elements-textPrimary">
|
| 706 |
+
{title}
|
| 707 |
+
{prNumber && (
|
| 708 |
+
<span className="ml-2 text-xs text-bolt-elements-textSecondary">
|
| 709 |
+
#{prNumber}
|
| 710 |
+
</span>
|
| 711 |
+
)}
|
| 712 |
+
</p>
|
| 713 |
+
{description && (
|
| 714 |
+
<p className="text-xs text-bolt-elements-textSecondary">{description}</p>
|
| 715 |
+
)}
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
);
|
| 720 |
+
})}
|
| 721 |
+
</div>
|
| 722 |
+
</div>
|
| 723 |
+
))}
|
| 724 |
+
</div>
|
| 725 |
+
</motion.div>
|
| 726 |
+
)}
|
| 727 |
+
</AnimatePresence>
|
| 728 |
+
</div>
|
| 729 |
+
)}
|
| 730 |
+
</div>
|
| 731 |
+
</motion.div>
|
| 732 |
+
)}
|
| 733 |
+
|
| 734 |
+
{/* Update Progress */}
|
| 735 |
+
{isUpdating && updateInfo?.downloadProgress !== undefined && (
|
| 736 |
+
<motion.div
|
| 737 |
+
className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
|
| 738 |
+
initial={{ opacity: 0, y: 20 }}
|
| 739 |
+
animate={{ opacity: 1, y: 0 }}
|
| 740 |
+
transition={{ duration: 0.3 }}
|
| 741 |
+
>
|
| 742 |
+
<div className="space-y-4">
|
| 743 |
+
<div className="flex items-center justify-between">
|
| 744 |
+
<span className="text-sm text-bolt-elements-textPrimary">Downloading Update</span>
|
| 745 |
+
<span className="text-sm text-bolt-elements-textSecondary">
|
| 746 |
+
{Math.round(updateInfo.downloadProgress)}%
|
| 747 |
+
</span>
|
| 748 |
+
</div>
|
| 749 |
+
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
| 750 |
+
<div
|
| 751 |
+
className="h-full bg-purple-500 transition-all duration-300"
|
| 752 |
+
style={{ width: `${updateInfo.downloadProgress}%` }}
|
| 753 |
+
/>
|
| 754 |
</div>
|
| 755 |
+
{retryCount > 0 && <p className="text-sm text-yellow-500">Retry attempt {retryCount}/3...</p>}
|
| 756 |
</div>
|
| 757 |
+
</motion.div>
|
| 758 |
+
)}
|
| 759 |
</div>
|
| 760 |
);
|
| 761 |
};
|
app/components/settings/user/UsersWindow.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
|
|
|
| 2 |
import { motion } from 'framer-motion';
|
| 3 |
-
import { useState } from 'react';
|
| 4 |
import { classNames } from '~/utils/classNames';
|
| 5 |
import { DialogTitle } from '~/components/ui/Dialog';
|
| 6 |
import { Switch } from '~/components/ui/Switch';
|
|
@@ -117,6 +118,24 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|
| 117 |
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
| 118 |
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
const handleDeveloperModeChange = (checked: boolean) => {
|
| 121 |
setDeveloperMode(checked);
|
| 122 |
};
|
|
@@ -127,7 +146,14 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|
| 127 |
|
| 128 |
// Only show tabs that are assigned to the user window AND are visible
|
| 129 |
const visibleUserTabs = tabConfiguration.userTabs
|
| 130 |
-
.filter((tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
| 132 |
|
| 133 |
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
|
@@ -240,6 +266,142 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|
| 240 |
}
|
| 241 |
};
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
return (
|
| 244 |
<>
|
| 245 |
<DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
|
|
@@ -273,64 +435,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
|
| 273 |
transition={{ duration: 0.2 }}
|
| 274 |
>
|
| 275 |
{/* Header */}
|
| 276 |
-
|
| 277 |
-
<div className="flex items-center gap-3">
|
| 278 |
-
{activeTab ? (
|
| 279 |
-
<motion.button
|
| 280 |
-
onClick={handleBack}
|
| 281 |
-
className={classNames(
|
| 282 |
-
'flex items-center justify-center w-8 h-8 rounded-lg',
|
| 283 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 284 |
-
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 285 |
-
'group transition-all duration-200',
|
| 286 |
-
)}
|
| 287 |
-
whileHover={{ scale: 1.05 }}
|
| 288 |
-
whileTap={{ scale: 0.95 }}
|
| 289 |
-
>
|
| 290 |
-
<div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
| 291 |
-
</motion.button>
|
| 292 |
-
) : (
|
| 293 |
-
<motion.div
|
| 294 |
-
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
|
| 295 |
-
initial={{ rotate: -10 }}
|
| 296 |
-
animate={{ rotate: 10 }}
|
| 297 |
-
transition={{
|
| 298 |
-
repeat: Infinity,
|
| 299 |
-
repeatType: 'reverse',
|
| 300 |
-
duration: 2,
|
| 301 |
-
ease: 'easeInOut',
|
| 302 |
-
}}
|
| 303 |
-
/>
|
| 304 |
-
)}
|
| 305 |
-
<DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
|
| 306 |
-
{activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
|
| 307 |
-
</DialogTitle>
|
| 308 |
-
</div>
|
| 309 |
-
<div className="flex items-center gap-3">
|
| 310 |
-
<div className="flex items-center gap-2">
|
| 311 |
-
<Switch
|
| 312 |
-
checked={developerMode}
|
| 313 |
-
onCheckedChange={handleDeveloperModeChange}
|
| 314 |
-
className="data-[state=checked]:bg-purple-500"
|
| 315 |
-
aria-label="Toggle developer mode"
|
| 316 |
-
/>
|
| 317 |
-
<label className="text-sm text-bolt-elements-textSecondary">Developer Mode</label>
|
| 318 |
-
</div>
|
| 319 |
-
<motion.button
|
| 320 |
-
onClick={onClose}
|
| 321 |
-
className={classNames(
|
| 322 |
-
'flex items-center justify-center w-8 h-8 rounded-lg',
|
| 323 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
| 324 |
-
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
| 325 |
-
'group transition-all duration-200',
|
| 326 |
-
)}
|
| 327 |
-
whileHover={{ scale: 1.05 }}
|
| 328 |
-
whileTap={{ scale: 0.95 }}
|
| 329 |
-
>
|
| 330 |
-
<div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
| 331 |
-
</motion.button>
|
| 332 |
-
</div>
|
| 333 |
-
</div>
|
| 334 |
|
| 335 |
{/* Content */}
|
| 336 |
<div
|
|
|
|
| 1 |
import * as RadixDialog from '@radix-ui/react-dialog';
|
| 2 |
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
| 3 |
import { motion } from 'framer-motion';
|
| 4 |
+
import { useState, useEffect } from 'react';
|
| 5 |
import { classNames } from '~/utils/classNames';
|
| 6 |
import { DialogTitle } from '~/components/ui/Dialog';
|
| 7 |
import { Switch } from '~/components/ui/Switch';
|
|
|
|
| 118 |
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
| 119 |
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
| 120 |
|
| 121 |
+
const [profile, setProfile] = useState(() => {
|
| 122 |
+
const saved = localStorage.getItem('bolt_user_profile');
|
| 123 |
+
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
useEffect(() => {
|
| 127 |
+
const handleStorageChange = (e: StorageEvent) => {
|
| 128 |
+
if (e.key === 'bolt_user_profile') {
|
| 129 |
+
const newProfile = e.newValue ? JSON.parse(e.newValue) : { avatar: null, notifications: true };
|
| 130 |
+
setProfile(newProfile);
|
| 131 |
+
}
|
| 132 |
+
};
|
| 133 |
+
|
| 134 |
+
window.addEventListener('storage', handleStorageChange);
|
| 135 |
+
|
| 136 |
+
return () => window.removeEventListener('storage', handleStorageChange);
|
| 137 |
+
}, []);
|
| 138 |
+
|
| 139 |
const handleDeveloperModeChange = (checked: boolean) => {
|
| 140 |
setDeveloperMode(checked);
|
| 141 |
};
|
|
|
|
| 146 |
|
| 147 |
// Only show tabs that are assigned to the user window AND are visible
|
| 148 |
const visibleUserTabs = tabConfiguration.userTabs
|
| 149 |
+
.filter((tab) => {
|
| 150 |
+
// Hide notifications tab if notifications are disabled
|
| 151 |
+
if (tab.id === 'notifications' && !profile.notifications) {
|
| 152 |
+
return false;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
return tab.visible;
|
| 156 |
+
})
|
| 157 |
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
| 158 |
|
| 159 |
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
|
|
|
| 266 |
}
|
| 267 |
};
|
| 268 |
|
| 269 |
+
const renderHeader = () => (
|
| 270 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
| 271 |
+
<div className="flex items-center space-x-4">
|
| 272 |
+
{activeTab ? (
|
| 273 |
+
<button
|
| 274 |
+
onClick={handleBack}
|
| 275 |
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
| 276 |
+
>
|
| 277 |
+
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 278 |
+
</button>
|
| 279 |
+
) : (
|
| 280 |
+
<motion.div
|
| 281 |
+
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
|
| 282 |
+
initial={{ rotate: -10 }}
|
| 283 |
+
animate={{ rotate: 10 }}
|
| 284 |
+
transition={{
|
| 285 |
+
repeat: Infinity,
|
| 286 |
+
repeatType: 'reverse',
|
| 287 |
+
duration: 2,
|
| 288 |
+
ease: 'easeInOut',
|
| 289 |
+
}}
|
| 290 |
+
/>
|
| 291 |
+
)}
|
| 292 |
+
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
| 293 |
+
{activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
|
| 294 |
+
</DialogTitle>
|
| 295 |
+
</div>
|
| 296 |
+
|
| 297 |
+
<div className="flex items-center space-x-4">
|
| 298 |
+
<div className="flex items-center gap-2">
|
| 299 |
+
<Switch
|
| 300 |
+
checked={developerMode}
|
| 301 |
+
onCheckedChange={handleDeveloperModeChange}
|
| 302 |
+
className="data-[state=checked]:bg-purple-500"
|
| 303 |
+
aria-label="Toggle developer mode"
|
| 304 |
+
/>
|
| 305 |
+
<label className="text-sm text-gray-500 dark:text-gray-400">Developer Mode</label>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<DropdownMenu.Root>
|
| 309 |
+
<DropdownMenu.Trigger asChild>
|
| 310 |
+
<button className="flex items-center justify-center w-8 h-8 rounded-full overflow-hidden hover:ring-2 ring-gray-300 dark:ring-gray-600 transition-all">
|
| 311 |
+
{profile.avatar ? (
|
| 312 |
+
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
|
| 313 |
+
) : (
|
| 314 |
+
<div className="w-full h-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
| 315 |
+
<svg
|
| 316 |
+
className="w-5 h-5 text-gray-500 dark:text-gray-400"
|
| 317 |
+
fill="none"
|
| 318 |
+
stroke="currentColor"
|
| 319 |
+
viewBox="0 0 24 24"
|
| 320 |
+
>
|
| 321 |
+
<path
|
| 322 |
+
strokeLinecap="round"
|
| 323 |
+
strokeLinejoin="round"
|
| 324 |
+
strokeWidth={2}
|
| 325 |
+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
| 326 |
+
/>
|
| 327 |
+
</svg>
|
| 328 |
+
</div>
|
| 329 |
+
)}
|
| 330 |
+
</button>
|
| 331 |
+
</DropdownMenu.Trigger>
|
| 332 |
+
|
| 333 |
+
<DropdownMenu.Portal>
|
| 334 |
+
<DropdownMenu.Content
|
| 335 |
+
className="min-w-[220px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-50 animate-in fade-in-0 zoom-in-95"
|
| 336 |
+
sideOffset={5}
|
| 337 |
+
align="end"
|
| 338 |
+
>
|
| 339 |
+
<DropdownMenu.Item
|
| 340 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 341 |
+
onSelect={() => handleTabClick('profile')}
|
| 342 |
+
>
|
| 343 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 344 |
+
<div className="i-ph:user-circle w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 345 |
+
</div>
|
| 346 |
+
<span className="group-hover:text-purple-500 transition-colors">Profile</span>
|
| 347 |
+
</DropdownMenu.Item>
|
| 348 |
+
|
| 349 |
+
<DropdownMenu.Item
|
| 350 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 351 |
+
onSelect={() => handleTabClick('settings')}
|
| 352 |
+
>
|
| 353 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 354 |
+
<div className="i-ph:gear w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 355 |
+
</div>
|
| 356 |
+
<span className="group-hover:text-purple-500 transition-colors">Settings</span>
|
| 357 |
+
</DropdownMenu.Item>
|
| 358 |
+
|
| 359 |
+
{profile.notifications && (
|
| 360 |
+
<>
|
| 361 |
+
<DropdownMenu.Item
|
| 362 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 363 |
+
onSelect={() => handleTabClick('notifications')}
|
| 364 |
+
>
|
| 365 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 366 |
+
<div className="i-ph:bell w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 367 |
+
</div>
|
| 368 |
+
<span className="group-hover:text-purple-500 transition-colors">
|
| 369 |
+
Notifications
|
| 370 |
+
{hasUnreadNotifications && (
|
| 371 |
+
<span className="ml-2 px-1.5 py-0.5 text-xs bg-purple-500 text-white rounded-full">
|
| 372 |
+
{unreadNotifications.length}
|
| 373 |
+
</span>
|
| 374 |
+
)}
|
| 375 |
+
</span>
|
| 376 |
+
</DropdownMenu.Item>
|
| 377 |
+
|
| 378 |
+
<DropdownMenu.Separator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
| 379 |
+
</>
|
| 380 |
+
)}
|
| 381 |
+
|
| 382 |
+
<DropdownMenu.Item
|
| 383 |
+
className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
|
| 384 |
+
onSelect={onClose}
|
| 385 |
+
>
|
| 386 |
+
<div className="mr-3 flex h-5 w-5 items-center justify-center">
|
| 387 |
+
<div className="i-ph:sign-out w-[18px] h-[18px] text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 388 |
+
</div>
|
| 389 |
+
<span className="group-hover:text-purple-500 transition-colors">Close</span>
|
| 390 |
+
</DropdownMenu.Item>
|
| 391 |
+
</DropdownMenu.Content>
|
| 392 |
+
</DropdownMenu.Portal>
|
| 393 |
+
</DropdownMenu.Root>
|
| 394 |
+
|
| 395 |
+
<button
|
| 396 |
+
onClick={onClose}
|
| 397 |
+
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
| 398 |
+
>
|
| 399 |
+
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
| 400 |
+
</button>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
);
|
| 404 |
+
|
| 405 |
return (
|
| 406 |
<>
|
| 407 |
<DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
|
|
|
|
| 435 |
transition={{ duration: 0.2 }}
|
| 436 |
>
|
| 437 |
{/* Header */}
|
| 438 |
+
{renderHeader()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
{/* Content */}
|
| 441 |
<div
|
app/lib/api/notifications.ts
CHANGED
|
@@ -1,40 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
export interface Notification {
|
| 2 |
id: string;
|
| 3 |
title: string;
|
| 4 |
message: string;
|
| 5 |
-
type:
|
| 6 |
read: boolean;
|
| 7 |
timestamp: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
export const getNotifications = async (): Promise<Notification[]> => {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
},
|
| 24 |
-
{
|
| 25 |
-
id: 'notif-2',
|
| 26 |
-
title: 'New Update Available',
|
| 27 |
-
message: 'Version 1.0.1 is now available',
|
| 28 |
-
type: 'info',
|
| 29 |
-
read: false,
|
| 30 |
-
timestamp: new Date().toISOString(),
|
| 31 |
-
},
|
| 32 |
-
];
|
| 33 |
};
|
| 34 |
|
| 35 |
export const markNotificationRead = async (notificationId: string): Promise<void> => {
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
};
|
|
|
|
| 1 |
+
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
| 2 |
+
|
| 3 |
+
export type NotificationType = 'info' | 'warning' | 'error' | 'success' | 'update';
|
| 4 |
+
|
| 5 |
+
export interface NotificationDetails {
|
| 6 |
+
type?: string;
|
| 7 |
+
message?: string;
|
| 8 |
+
currentVersion?: string;
|
| 9 |
+
latestVersion?: string;
|
| 10 |
+
branch?: string;
|
| 11 |
+
updateUrl?: string;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
export interface Notification {
|
| 15 |
id: string;
|
| 16 |
title: string;
|
| 17 |
message: string;
|
| 18 |
+
type: NotificationType;
|
| 19 |
read: boolean;
|
| 20 |
timestamp: string;
|
| 21 |
+
details?: NotificationDetails;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
interface LogEntryWithRead extends LogEntry {
|
| 25 |
+
read?: boolean;
|
| 26 |
}
|
| 27 |
|
| 28 |
+
const mapLogToNotification = (log: LogEntryWithRead): Notification => {
|
| 29 |
+
const type: NotificationType =
|
| 30 |
+
log.details?.type === 'update'
|
| 31 |
+
? 'update'
|
| 32 |
+
: log.level === 'error'
|
| 33 |
+
? 'error'
|
| 34 |
+
: log.level === 'warning'
|
| 35 |
+
? 'warning'
|
| 36 |
+
: 'info';
|
| 37 |
+
|
| 38 |
+
const baseNotification: Notification = {
|
| 39 |
+
id: log.id,
|
| 40 |
+
title: log.category.charAt(0).toUpperCase() + log.category.slice(1),
|
| 41 |
+
message: log.message,
|
| 42 |
+
type,
|
| 43 |
+
read: log.read || false,
|
| 44 |
+
timestamp: log.timestamp,
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
if (log.details) {
|
| 48 |
+
return {
|
| 49 |
+
...baseNotification,
|
| 50 |
+
details: log.details as NotificationDetails,
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return baseNotification;
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
export const getNotifications = async (): Promise<Notification[]> => {
|
| 58 |
+
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
|
| 59 |
+
|
| 60 |
+
return logs
|
| 61 |
+
.filter((log) => {
|
| 62 |
+
if (log.details?.type === 'update') {
|
| 63 |
+
return true;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return log.level === 'error' || log.level === 'warning';
|
| 67 |
+
})
|
| 68 |
+
.map(mapLogToNotification)
|
| 69 |
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
};
|
| 71 |
|
| 72 |
export const markNotificationRead = async (notificationId: string): Promise<void> => {
|
| 73 |
+
logStore.markAsRead(notificationId);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
export const clearNotifications = async (): Promise<void> => {
|
| 77 |
+
logStore.clearLogs();
|
| 78 |
+
};
|
| 79 |
+
|
| 80 |
+
export const getUnreadCount = (): number => {
|
| 81 |
+
const logs = Object.values(logStore.logs.get()) as LogEntryWithRead[];
|
| 82 |
+
|
| 83 |
+
return logs.filter((log) => {
|
| 84 |
+
if (!log.read) {
|
| 85 |
+
if (log.details?.type === 'update') {
|
| 86 |
+
return true;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return log.level === 'error' || log.level === 'warning';
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
return false;
|
| 93 |
+
}).length;
|
| 94 |
};
|
app/lib/hooks/useNotifications.ts
CHANGED
|
@@ -1,34 +1,17 @@
|
|
| 1 |
import { useState, useEffect } from 'react';
|
| 2 |
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
const getReadNotifications = (): string[] => {
|
| 7 |
-
try {
|
| 8 |
-
const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
|
| 9 |
-
return stored ? JSON.parse(stored) : [];
|
| 10 |
-
} catch {
|
| 11 |
-
return [];
|
| 12 |
-
}
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
const setReadNotifications = (notificationIds: string[]) => {
|
| 16 |
-
try {
|
| 17 |
-
localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
|
| 18 |
-
} catch (error) {
|
| 19 |
-
console.error('Failed to persist read notifications:', error);
|
| 20 |
-
}
|
| 21 |
-
};
|
| 22 |
|
| 23 |
export const useNotifications = () => {
|
| 24 |
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
|
| 25 |
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
|
| 26 |
-
const
|
| 27 |
|
| 28 |
const checkNotifications = async () => {
|
| 29 |
try {
|
| 30 |
const notifications = await getNotifications();
|
| 31 |
-
const unread = notifications.filter((n) => !
|
| 32 |
setUnreadNotifications(unread);
|
| 33 |
setHasUnreadNotifications(unread.length > 0);
|
| 34 |
} catch (error) {
|
|
@@ -43,17 +26,12 @@ export const useNotifications = () => {
|
|
| 43 |
const interval = setInterval(checkNotifications, 60 * 1000);
|
| 44 |
|
| 45 |
return () => clearInterval(interval);
|
| 46 |
-
}, [
|
| 47 |
|
| 48 |
const markAsRead = async (notificationId: string) => {
|
| 49 |
try {
|
| 50 |
await markNotificationRead(notificationId);
|
| 51 |
-
|
| 52 |
-
const newReadIds = [...readNotificationIds, notificationId];
|
| 53 |
-
setReadNotificationIds(newReadIds);
|
| 54 |
-
setReadNotifications(newReadIds);
|
| 55 |
-
setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
|
| 56 |
-
setHasUnreadNotifications(unreadNotifications.length > 1);
|
| 57 |
} catch (error) {
|
| 58 |
console.error('Failed to mark notification as read:', error);
|
| 59 |
}
|
|
@@ -61,13 +39,9 @@ export const useNotifications = () => {
|
|
| 61 |
|
| 62 |
const markAllAsRead = async () => {
|
| 63 |
try {
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
setReadNotificationIds(newReadIds);
|
| 68 |
-
setReadNotifications(newReadIds);
|
| 69 |
-
setUnreadNotifications([]);
|
| 70 |
-
setHasUnreadNotifications(false);
|
| 71 |
} catch (error) {
|
| 72 |
console.error('Failed to mark all notifications as read:', error);
|
| 73 |
}
|
|
|
|
| 1 |
import { useState, useEffect } from 'react';
|
| 2 |
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
|
| 3 |
+
import { logStore } from '~/lib/stores/logs';
|
| 4 |
+
import { useStore } from '@nanostores/react';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
export const useNotifications = () => {
|
| 7 |
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
|
| 8 |
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
|
| 9 |
+
const logs = useStore(logStore.logs);
|
| 10 |
|
| 11 |
const checkNotifications = async () => {
|
| 12 |
try {
|
| 13 |
const notifications = await getNotifications();
|
| 14 |
+
const unread = notifications.filter((n) => !logStore.isRead(n.id));
|
| 15 |
setUnreadNotifications(unread);
|
| 16 |
setHasUnreadNotifications(unread.length > 0);
|
| 17 |
} catch (error) {
|
|
|
|
| 26 |
const interval = setInterval(checkNotifications, 60 * 1000);
|
| 27 |
|
| 28 |
return () => clearInterval(interval);
|
| 29 |
+
}, [logs]); // Re-run when logs change
|
| 30 |
|
| 31 |
const markAsRead = async (notificationId: string) => {
|
| 32 |
try {
|
| 33 |
await markNotificationRead(notificationId);
|
| 34 |
+
await checkNotifications();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
} catch (error) {
|
| 36 |
console.error('Failed to mark notification as read:', error);
|
| 37 |
}
|
|
|
|
| 39 |
|
| 40 |
const markAllAsRead = async () => {
|
| 41 |
try {
|
| 42 |
+
const notifications = await getNotifications();
|
| 43 |
+
await Promise.all(notifications.map((n) => markNotificationRead(n.id)));
|
| 44 |
+
await checkNotifications();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
} catch (error) {
|
| 46 |
console.error('Failed to mark all notifications as read:', error);
|
| 47 |
}
|
app/lib/stores/logs.ts
CHANGED
|
@@ -19,12 +19,25 @@ export interface LogEntry {
|
|
| 19 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
| 20 |
|
| 21 |
class LogStore {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
private _logs = map<Record<string, LogEntry>>({});
|
| 23 |
showLogs = atom(true);
|
|
|
|
| 24 |
|
| 25 |
constructor() {
|
| 26 |
// Load saved logs from cookies on initialization
|
| 27 |
this._loadLogs();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
// Expose the logs store for subscription
|
|
@@ -45,11 +58,36 @@ class LogStore {
|
|
| 45 |
}
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
private _saveLogs() {
|
| 49 |
const currentLogs = this._logs.get();
|
| 50 |
Cookies.set('eventLogs', JSON.stringify(currentLogs));
|
| 51 |
}
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
private _generateId(): string {
|
| 54 |
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 55 |
}
|
|
@@ -210,6 +248,20 @@ class LogStore {
|
|
| 210 |
return matchesLevel && matchesCategory && matchesSearch;
|
| 211 |
});
|
| 212 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
export const logStore = new LogStore();
|
|
|
|
| 19 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
| 20 |
|
| 21 |
class LogStore {
|
| 22 |
+
logInfo(message: string, details: { type: string; message: string }) {
|
| 23 |
+
return this.addLog(message, 'info', 'system', details);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
logSuccess(message: string, details: { type: string; message: string }) {
|
| 27 |
+
return this.addLog(message, 'info', 'system', { ...details, success: true });
|
| 28 |
+
}
|
| 29 |
private _logs = map<Record<string, LogEntry>>({});
|
| 30 |
showLogs = atom(true);
|
| 31 |
+
private _readLogs = new Set<string>();
|
| 32 |
|
| 33 |
constructor() {
|
| 34 |
// Load saved logs from cookies on initialization
|
| 35 |
this._loadLogs();
|
| 36 |
+
|
| 37 |
+
// Only load read logs in browser environment
|
| 38 |
+
if (typeof window !== 'undefined') {
|
| 39 |
+
this._loadReadLogs();
|
| 40 |
+
}
|
| 41 |
}
|
| 42 |
|
| 43 |
// Expose the logs store for subscription
|
|
|
|
| 58 |
}
|
| 59 |
}
|
| 60 |
|
| 61 |
+
private _loadReadLogs() {
|
| 62 |
+
if (typeof window === 'undefined') {
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const savedReadLogs = localStorage.getItem('bolt_read_logs');
|
| 67 |
+
|
| 68 |
+
if (savedReadLogs) {
|
| 69 |
+
try {
|
| 70 |
+
const parsedReadLogs = JSON.parse(savedReadLogs);
|
| 71 |
+
this._readLogs = new Set(parsedReadLogs);
|
| 72 |
+
} catch (error) {
|
| 73 |
+
logger.error('Failed to parse read logs:', error);
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
private _saveLogs() {
|
| 79 |
const currentLogs = this._logs.get();
|
| 80 |
Cookies.set('eventLogs', JSON.stringify(currentLogs));
|
| 81 |
}
|
| 82 |
|
| 83 |
+
private _saveReadLogs() {
|
| 84 |
+
if (typeof window === 'undefined') {
|
| 85 |
+
return;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs)));
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
private _generateId(): string {
|
| 92 |
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
| 93 |
}
|
|
|
|
| 248 |
return matchesLevel && matchesCategory && matchesSearch;
|
| 249 |
});
|
| 250 |
}
|
| 251 |
+
|
| 252 |
+
markAsRead(logId: string) {
|
| 253 |
+
this._readLogs.add(logId);
|
| 254 |
+
this._saveReadLogs();
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
isRead(logId: string): boolean {
|
| 258 |
+
return this._readLogs.has(logId);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
clearReadLogs() {
|
| 262 |
+
this._readLogs.clear();
|
| 263 |
+
this._saveReadLogs();
|
| 264 |
+
}
|
| 265 |
}
|
| 266 |
|
| 267 |
export const logStore = new LogStore();
|
package.json
CHANGED
|
@@ -90,6 +90,7 @@
|
|
| 90 |
"js-cookie": "^3.0.5",
|
| 91 |
"jszip": "^3.10.1",
|
| 92 |
"nanostores": "^0.10.3",
|
|
|
|
| 93 |
"ollama-ai-provider": "^0.15.2",
|
| 94 |
"react": "^18.3.1",
|
| 95 |
"react-dnd": "^16.0.1",
|
|
|
|
| 90 |
"js-cookie": "^3.0.5",
|
| 91 |
"jszip": "^3.10.1",
|
| 92 |
"nanostores": "^0.10.3",
|
| 93 |
+
"next": "^15.1.5",
|
| 94 |
"ollama-ai-provider": "^0.15.2",
|
| 95 |
"react": "^18.3.1",
|
| 96 |
"react-dnd": "^16.0.1",
|
pnpm-lock.yaml
CHANGED
|
@@ -191,6 +191,9 @@ importers:
|
|
| 191 |
nanostores:
|
| 192 |
specifier: ^0.10.3
|
| 193 |
version: 0.10.3
|
|
|
|
|
|
|
|
|
|
| 194 |
ollama-ai-provider:
|
| 195 |
specifier: ^0.15.2
|
| 196 |
version: 0.15.2([email protected])
|
|
@@ -849,6 +852,9 @@ packages:
|
|
| 849 |
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
| 850 |
engines: {node: '>=12'}
|
| 851 |
|
|
|
|
|
|
|
|
|
|
| 852 |
'@emotion/[email protected]':
|
| 853 |
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
|
| 854 |
|
|
@@ -1506,6 +1512,111 @@ packages:
|
|
| 1506 |
'@iconify/[email protected]':
|
| 1507 |
resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
|
| 1508 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1509 |
'@isaacs/[email protected]':
|
| 1510 |
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
| 1511 |
engines: {node: '>=12'}
|
|
@@ -1577,6 +1688,57 @@ packages:
|
|
| 1577 |
nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0
|
| 1578 |
react: '>=18.0.0'
|
| 1579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1580 |
'@nodelib/[email protected]':
|
| 1581 |
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
| 1582 |
engines: {node: '>= 8'}
|
|
@@ -2475,6 +2637,9 @@ packages:
|
|
| 2475 |
peerDependencies:
|
| 2476 |
eslint: '>=8.40.0'
|
| 2477 |
|
|
|
|
|
|
|
|
|
|
| 2478 |
'@swc/[email protected]':
|
| 2479 |
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
| 2480 |
|
|
@@ -2991,6 +3156,10 @@ packages:
|
|
| 2991 |
peerDependencies:
|
| 2992 |
esbuild: '>=0.18'
|
| 2993 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2994 | |
| 2995 |
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
| 2996 |
engines: {node: '>= 0.8'}
|
|
@@ -3104,6 +3273,13 @@ packages:
|
|
| 3104 | |
| 3105 |
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
| 3106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3107 | |
| 3108 |
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
| 3109 |
|
|
@@ -3305,6 +3481,10 @@ packages:
|
|
| 3305 |
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
| 3306 |
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
| 3307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3308 | |
| 3309 |
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
| 3310 |
|
|
@@ -3961,6 +4141,9 @@ packages:
|
|
| 3961 |
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
|
| 3962 |
engines: {node: '>= 0.4'}
|
| 3963 |
|
|
|
|
|
|
|
|
|
|
| 3964 | |
| 3965 |
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
| 3966 |
engines: {node: '>=8'}
|
|
@@ -4650,6 +4833,27 @@ packages:
|
|
| 4650 |
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
| 4651 |
engines: {node: '>= 0.6'}
|
| 4652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4653 | |
| 4654 |
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
| 4655 |
engines: {node: '>=10.5.0'}
|
|
@@ -4942,6 +5146,10 @@ packages:
|
|
| 4942 | |
| 4943 |
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
| 4944 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4945 | |
| 4946 |
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
| 4947 |
engines: {node: ^10 || ^12 || >=14}
|
|
@@ -5496,6 +5704,10 @@ packages:
|
|
| 5496 |
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
| 5497 |
hasBin: true
|
| 5498 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5499 | |
| 5500 |
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
| 5501 |
engines: {node: '>=8'}
|
|
@@ -5527,6 +5739,9 @@ packages:
|
|
| 5527 | |
| 5528 |
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
| 5529 |
|
|
|
|
|
|
|
|
|
|
| 5530 | |
| 5531 |
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
| 5532 |
engines: {node: '>= 10'}
|
|
@@ -5598,6 +5813,10 @@ packages:
|
|
| 5598 | |
| 5599 |
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
|
| 5600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5601 | |
| 5602 |
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
|
| 5603 |
|
|
@@ -5650,6 +5869,19 @@ packages:
|
|
| 5650 | |
| 5651 |
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
| 5652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5653 | |
| 5654 |
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
| 5655 |
engines: {node: '>=8'}
|
|
@@ -7161,6 +7393,11 @@ snapshots:
|
|
| 7161 |
dependencies:
|
| 7162 |
'@jridgewell/trace-mapping': 0.3.9
|
| 7163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7164 |
'@emotion/[email protected]': {}
|
| 7165 |
|
| 7166 |
'@esbuild-plugins/[email protected]([email protected])':
|
|
@@ -7556,6 +7793,81 @@ snapshots:
|
|
| 7556 |
transitivePeerDependencies:
|
| 7557 |
- supports-color
|
| 7558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7559 |
'@isaacs/[email protected]':
|
| 7560 |
dependencies:
|
| 7561 |
string-width: 5.1.2
|
|
@@ -7673,6 +7985,32 @@ snapshots:
|
|
| 7673 |
nanostores: 0.10.3
|
| 7674 |
react: 18.3.1
|
| 7675 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7676 |
'@nodelib/[email protected]':
|
| 7677 |
dependencies:
|
| 7678 |
'@nodelib/fs.stat': 2.0.5
|
|
@@ -8739,6 +9077,8 @@ snapshots:
|
|
| 8739 |
- supports-color
|
| 8740 |
- typescript
|
| 8741 |
|
|
|
|
|
|
|
| 8742 |
'@swc/[email protected]':
|
| 8743 |
dependencies:
|
| 8744 |
tslib: 2.8.1
|
|
@@ -9432,6 +9772,10 @@ snapshots:
|
|
| 9432 |
esbuild: 0.23.1
|
| 9433 |
load-tsconfig: 0.2.5
|
| 9434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9435 | |
| 9436 |
|
| 9437 | |
|
@@ -9546,6 +9890,18 @@ snapshots:
|
|
| 9546 |
|
| 9547 | |
| 9548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9549 | |
| 9550 |
|
| 9551 | |
|
@@ -9711,6 +10067,9 @@ snapshots:
|
|
| 9711 |
|
| 9712 | |
| 9713 |
|
|
|
|
|
|
|
|
|
|
| 9714 | |
| 9715 |
|
| 9716 | |
|
@@ -10569,6 +10928,9 @@ snapshots:
|
|
| 10569 |
call-bind: 1.0.7
|
| 10570 |
has-tostringtag: 1.0.2
|
| 10571 |
|
|
|
|
|
|
|
|
|
|
| 10572 | |
| 10573 |
dependencies:
|
| 10574 |
binary-extensions: 2.3.0
|
|
@@ -11605,6 +11967,32 @@ snapshots:
|
|
| 11605 |
|
| 11606 | |
| 11607 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11608 | |
| 11609 |
|
| 11610 | |
|
@@ -11929,6 +12317,12 @@ snapshots:
|
|
| 11929 |
|
| 11930 | |
| 11931 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11932 | |
| 11933 |
dependencies:
|
| 11934 |
nanoid: 3.3.8
|
|
@@ -12512,6 +12906,33 @@ snapshots:
|
|
| 12512 |
inherits: 2.0.4
|
| 12513 |
safe-buffer: 5.2.1
|
| 12514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12515 | |
| 12516 |
dependencies:
|
| 12517 |
shebang-regex: 3.0.0
|
|
@@ -12548,6 +12969,11 @@ snapshots:
|
|
| 12548 |
once: 1.4.0
|
| 12549 |
simple-concat: 1.0.1
|
| 12550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12551 | |
| 12552 |
dependencies:
|
| 12553 |
'@polka/url': 1.0.0-next.28
|
|
@@ -12616,6 +13042,8 @@ snapshots:
|
|
| 12616 |
|
| 12617 | |
| 12618 |
|
|
|
|
|
|
|
| 12619 | |
| 12620 |
|
| 12621 | |
|
@@ -12669,6 +13097,13 @@ snapshots:
|
|
| 12669 |
dependencies:
|
| 12670 |
inline-style-parser: 0.2.4
|
| 12671 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12672 | |
| 12673 |
dependencies:
|
| 12674 |
has-flag: 4.0.0
|
|
|
|
| 191 |
nanostores:
|
| 192 |
specifier: ^0.10.3
|
| 193 |
version: 0.10.3
|
| 194 |
+
next:
|
| 195 |
+
specifier: ^15.1.5
|
| 196 |
+
version: 15.1.5(@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
|
| 197 |
ollama-ai-provider:
|
| 198 |
specifier: ^0.15.2
|
| 199 |
version: 0.15.2([email protected])
|
|
|
|
| 852 |
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
| 853 |
engines: {node: '>=12'}
|
| 854 |
|
| 855 |
+
'@emnapi/[email protected]':
|
| 856 |
+
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
|
| 857 |
+
|
| 858 |
'@emotion/[email protected]':
|
| 859 |
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
|
| 860 |
|
|
|
|
| 1512 |
'@iconify/[email protected]':
|
| 1513 |
resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
|
| 1514 |
|
| 1515 |
+
'@img/[email protected]':
|
| 1516 |
+
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
| 1517 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1518 |
+
cpu: [arm64]
|
| 1519 |
+
os: [darwin]
|
| 1520 |
+
|
| 1521 |
+
'@img/[email protected]':
|
| 1522 |
+
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
| 1523 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1524 |
+
cpu: [x64]
|
| 1525 |
+
os: [darwin]
|
| 1526 |
+
|
| 1527 |
+
'@img/[email protected]':
|
| 1528 |
+
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
| 1529 |
+
cpu: [arm64]
|
| 1530 |
+
os: [darwin]
|
| 1531 |
+
|
| 1532 |
+
'@img/[email protected]':
|
| 1533 |
+
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
| 1534 |
+
cpu: [x64]
|
| 1535 |
+
os: [darwin]
|
| 1536 |
+
|
| 1537 |
+
'@img/[email protected]':
|
| 1538 |
+
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
| 1539 |
+
cpu: [arm64]
|
| 1540 |
+
os: [linux]
|
| 1541 |
+
|
| 1542 |
+
'@img/[email protected]':
|
| 1543 |
+
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
| 1544 |
+
cpu: [arm]
|
| 1545 |
+
os: [linux]
|
| 1546 |
+
|
| 1547 |
+
'@img/[email protected]':
|
| 1548 |
+
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
| 1549 |
+
cpu: [s390x]
|
| 1550 |
+
os: [linux]
|
| 1551 |
+
|
| 1552 |
+
'@img/[email protected]':
|
| 1553 |
+
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
| 1554 |
+
cpu: [x64]
|
| 1555 |
+
os: [linux]
|
| 1556 |
+
|
| 1557 |
+
'@img/[email protected]':
|
| 1558 |
+
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
| 1559 |
+
cpu: [arm64]
|
| 1560 |
+
os: [linux]
|
| 1561 |
+
|
| 1562 |
+
'@img/[email protected]':
|
| 1563 |
+
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
| 1564 |
+
cpu: [x64]
|
| 1565 |
+
os: [linux]
|
| 1566 |
+
|
| 1567 |
+
'@img/[email protected]':
|
| 1568 |
+
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
| 1569 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1570 |
+
cpu: [arm64]
|
| 1571 |
+
os: [linux]
|
| 1572 |
+
|
| 1573 |
+
'@img/[email protected]':
|
| 1574 |
+
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
| 1575 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1576 |
+
cpu: [arm]
|
| 1577 |
+
os: [linux]
|
| 1578 |
+
|
| 1579 |
+
'@img/[email protected]':
|
| 1580 |
+
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
| 1581 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1582 |
+
cpu: [s390x]
|
| 1583 |
+
os: [linux]
|
| 1584 |
+
|
| 1585 |
+
'@img/[email protected]':
|
| 1586 |
+
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
| 1587 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1588 |
+
cpu: [x64]
|
| 1589 |
+
os: [linux]
|
| 1590 |
+
|
| 1591 |
+
'@img/[email protected]':
|
| 1592 |
+
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
| 1593 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1594 |
+
cpu: [arm64]
|
| 1595 |
+
os: [linux]
|
| 1596 |
+
|
| 1597 |
+
'@img/[email protected]':
|
| 1598 |
+
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
| 1599 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1600 |
+
cpu: [x64]
|
| 1601 |
+
os: [linux]
|
| 1602 |
+
|
| 1603 |
+
'@img/[email protected]':
|
| 1604 |
+
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
| 1605 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1606 |
+
cpu: [wasm32]
|
| 1607 |
+
|
| 1608 |
+
'@img/[email protected]':
|
| 1609 |
+
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
|
| 1610 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1611 |
+
cpu: [ia32]
|
| 1612 |
+
os: [win32]
|
| 1613 |
+
|
| 1614 |
+
'@img/[email protected]':
|
| 1615 |
+
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
| 1616 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 1617 |
+
cpu: [x64]
|
| 1618 |
+
os: [win32]
|
| 1619 |
+
|
| 1620 |
'@isaacs/[email protected]':
|
| 1621 |
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
| 1622 |
engines: {node: '>=12'}
|
|
|
|
| 1688 |
nanostores: ^0.9.0 || ^0.10.0 || ^0.11.0
|
| 1689 |
react: '>=18.0.0'
|
| 1690 |
|
| 1691 |
+
'@next/[email protected]':
|
| 1692 |
+
resolution: {integrity: sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==}
|
| 1693 |
+
|
| 1694 |
+
'@next/[email protected]':
|
| 1695 |
+
resolution: {integrity: sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==}
|
| 1696 |
+
engines: {node: '>= 10'}
|
| 1697 |
+
cpu: [arm64]
|
| 1698 |
+
os: [darwin]
|
| 1699 |
+
|
| 1700 |
+
'@next/[email protected]':
|
| 1701 |
+
resolution: {integrity: sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==}
|
| 1702 |
+
engines: {node: '>= 10'}
|
| 1703 |
+
cpu: [x64]
|
| 1704 |
+
os: [darwin]
|
| 1705 |
+
|
| 1706 |
+
'@next/[email protected]':
|
| 1707 |
+
resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
|
| 1708 |
+
engines: {node: '>= 10'}
|
| 1709 |
+
cpu: [arm64]
|
| 1710 |
+
os: [linux]
|
| 1711 |
+
|
| 1712 |
+
'@next/[email protected]':
|
| 1713 |
+
resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
|
| 1714 |
+
engines: {node: '>= 10'}
|
| 1715 |
+
cpu: [arm64]
|
| 1716 |
+
os: [linux]
|
| 1717 |
+
|
| 1718 |
+
'@next/[email protected]':
|
| 1719 |
+
resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
|
| 1720 |
+
engines: {node: '>= 10'}
|
| 1721 |
+
cpu: [x64]
|
| 1722 |
+
os: [linux]
|
| 1723 |
+
|
| 1724 |
+
'@next/[email protected]':
|
| 1725 |
+
resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
|
| 1726 |
+
engines: {node: '>= 10'}
|
| 1727 |
+
cpu: [x64]
|
| 1728 |
+
os: [linux]
|
| 1729 |
+
|
| 1730 |
+
'@next/[email protected]':
|
| 1731 |
+
resolution: {integrity: sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==}
|
| 1732 |
+
engines: {node: '>= 10'}
|
| 1733 |
+
cpu: [arm64]
|
| 1734 |
+
os: [win32]
|
| 1735 |
+
|
| 1736 |
+
'@next/[email protected]':
|
| 1737 |
+
resolution: {integrity: sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==}
|
| 1738 |
+
engines: {node: '>= 10'}
|
| 1739 |
+
cpu: [x64]
|
| 1740 |
+
os: [win32]
|
| 1741 |
+
|
| 1742 |
'@nodelib/[email protected]':
|
| 1743 |
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
| 1744 |
engines: {node: '>= 8'}
|
|
|
|
| 2637 |
peerDependencies:
|
| 2638 |
eslint: '>=8.40.0'
|
| 2639 |
|
| 2640 |
+
'@swc/[email protected]':
|
| 2641 |
+
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
| 2642 |
+
|
| 2643 |
'@swc/[email protected]':
|
| 2644 |
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
| 2645 |
|
|
|
|
| 3156 |
peerDependencies:
|
| 3157 |
esbuild: '>=0.18'
|
| 3158 |
|
| 3159 | |
| 3160 |
+
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
| 3161 |
+
engines: {node: '>=10.16.0'}
|
| 3162 |
+
|
| 3163 | |
| 3164 |
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
| 3165 |
engines: {node: '>= 0.8'}
|
|
|
|
| 3273 | |
| 3274 |
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
| 3275 |
|
| 3276 | |
| 3277 |
+
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
| 3278 |
+
|
| 3279 | |
| 3280 |
+
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
| 3281 |
+
engines: {node: '>=12.5.0'}
|
| 3282 |
+
|
| 3283 | |
| 3284 |
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
| 3285 |
|
|
|
|
| 3481 |
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
| 3482 |
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
| 3483 |
|
| 3484 | |
| 3485 |
+
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
| 3486 |
+
engines: {node: '>=8'}
|
| 3487 |
+
|
| 3488 | |
| 3489 |
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
| 3490 |
|
|
|
|
| 4141 |
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
|
| 4142 |
engines: {node: '>= 0.4'}
|
| 4143 |
|
| 4144 | |
| 4145 |
+
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
| 4146 |
+
|
| 4147 | |
| 4148 |
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
| 4149 |
engines: {node: '>=8'}
|
|
|
|
| 4833 |
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
| 4834 |
engines: {node: '>= 0.6'}
|
| 4835 |
|
| 4836 | |
| 4837 |
+
resolution: {integrity: sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==}
|
| 4838 |
+
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
| 4839 |
+
hasBin: true
|
| 4840 |
+
peerDependencies:
|
| 4841 |
+
'@opentelemetry/api': ^1.1.0
|
| 4842 |
+
'@playwright/test': ^1.41.2
|
| 4843 |
+
babel-plugin-react-compiler: '*'
|
| 4844 |
+
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
| 4845 |
+
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
| 4846 |
+
sass: ^1.3.0
|
| 4847 |
+
peerDependenciesMeta:
|
| 4848 |
+
'@opentelemetry/api':
|
| 4849 |
+
optional: true
|
| 4850 |
+
'@playwright/test':
|
| 4851 |
+
optional: true
|
| 4852 |
+
babel-plugin-react-compiler:
|
| 4853 |
+
optional: true
|
| 4854 |
+
sass:
|
| 4855 |
+
optional: true
|
| 4856 |
+
|
| 4857 | |
| 4858 |
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
| 4859 |
engines: {node: '>=10.5.0'}
|
|
|
|
| 5146 | |
| 5147 |
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
| 5148 |
|
| 5149 | |
| 5150 |
+
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
| 5151 |
+
engines: {node: ^10 || ^12 || >=14}
|
| 5152 |
+
|
| 5153 | |
| 5154 |
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
|
| 5155 |
engines: {node: ^10 || ^12 || >=14}
|
|
|
|
| 5704 |
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
| 5705 |
hasBin: true
|
| 5706 |
|
| 5707 | |
| 5708 |
+
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
| 5709 |
+
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
| 5710 |
+
|
| 5711 | |
| 5712 |
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
| 5713 |
engines: {node: '>=8'}
|
|
|
|
| 5739 | |
| 5740 |
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
| 5741 |
|
| 5742 | |
| 5743 |
+
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
| 5744 |
+
|
| 5745 | |
| 5746 |
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
| 5747 |
engines: {node: '>= 10'}
|
|
|
|
| 5813 | |
| 5814 |
resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==}
|
| 5815 |
|
| 5816 | |
| 5817 |
+
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
| 5818 |
+
engines: {node: '>=10.0.0'}
|
| 5819 |
+
|
| 5820 | |
| 5821 |
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
|
| 5822 |
|
|
|
|
| 5869 | |
| 5870 |
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
| 5871 |
|
| 5872 | |
| 5873 |
+
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
| 5874 |
+
engines: {node: '>= 12.0.0'}
|
| 5875 |
+
peerDependencies:
|
| 5876 |
+
'@babel/core': '*'
|
| 5877 |
+
babel-plugin-macros: '*'
|
| 5878 |
+
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
|
| 5879 |
+
peerDependenciesMeta:
|
| 5880 |
+
'@babel/core':
|
| 5881 |
+
optional: true
|
| 5882 |
+
babel-plugin-macros:
|
| 5883 |
+
optional: true
|
| 5884 |
+
|
| 5885 | |
| 5886 |
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
| 5887 |
engines: {node: '>=8'}
|
|
|
|
| 7393 |
dependencies:
|
| 7394 |
'@jridgewell/trace-mapping': 0.3.9
|
| 7395 |
|
| 7396 |
+
'@emnapi/[email protected]':
|
| 7397 |
+
dependencies:
|
| 7398 |
+
tslib: 2.8.1
|
| 7399 |
+
optional: true
|
| 7400 |
+
|
| 7401 |
'@emotion/[email protected]': {}
|
| 7402 |
|
| 7403 |
'@esbuild-plugins/[email protected]([email protected])':
|
|
|
|
| 7793 |
transitivePeerDependencies:
|
| 7794 |
- supports-color
|
| 7795 |
|
| 7796 |
+
'@img/[email protected]':
|
| 7797 |
+
optionalDependencies:
|
| 7798 |
+
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
| 7799 |
+
optional: true
|
| 7800 |
+
|
| 7801 |
+
'@img/[email protected]':
|
| 7802 |
+
optionalDependencies:
|
| 7803 |
+
'@img/sharp-libvips-darwin-x64': 1.0.4
|
| 7804 |
+
optional: true
|
| 7805 |
+
|
| 7806 |
+
'@img/[email protected]':
|
| 7807 |
+
optional: true
|
| 7808 |
+
|
| 7809 |
+
'@img/[email protected]':
|
| 7810 |
+
optional: true
|
| 7811 |
+
|
| 7812 |
+
'@img/[email protected]':
|
| 7813 |
+
optional: true
|
| 7814 |
+
|
| 7815 |
+
'@img/[email protected]':
|
| 7816 |
+
optional: true
|
| 7817 |
+
|
| 7818 |
+
'@img/[email protected]':
|
| 7819 |
+
optional: true
|
| 7820 |
+
|
| 7821 |
+
'@img/[email protected]':
|
| 7822 |
+
optional: true
|
| 7823 |
+
|
| 7824 |
+
'@img/[email protected]':
|
| 7825 |
+
optional: true
|
| 7826 |
+
|
| 7827 |
+
'@img/[email protected]':
|
| 7828 |
+
optional: true
|
| 7829 |
+
|
| 7830 |
+
'@img/[email protected]':
|
| 7831 |
+
optionalDependencies:
|
| 7832 |
+
'@img/sharp-libvips-linux-arm64': 1.0.4
|
| 7833 |
+
optional: true
|
| 7834 |
+
|
| 7835 |
+
'@img/[email protected]':
|
| 7836 |
+
optionalDependencies:
|
| 7837 |
+
'@img/sharp-libvips-linux-arm': 1.0.5
|
| 7838 |
+
optional: true
|
| 7839 |
+
|
| 7840 |
+
'@img/[email protected]':
|
| 7841 |
+
optionalDependencies:
|
| 7842 |
+
'@img/sharp-libvips-linux-s390x': 1.0.4
|
| 7843 |
+
optional: true
|
| 7844 |
+
|
| 7845 |
+
'@img/[email protected]':
|
| 7846 |
+
optionalDependencies:
|
| 7847 |
+
'@img/sharp-libvips-linux-x64': 1.0.4
|
| 7848 |
+
optional: true
|
| 7849 |
+
|
| 7850 |
+
'@img/[email protected]':
|
| 7851 |
+
optionalDependencies:
|
| 7852 |
+
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
| 7853 |
+
optional: true
|
| 7854 |
+
|
| 7855 |
+
'@img/[email protected]':
|
| 7856 |
+
optionalDependencies:
|
| 7857 |
+
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
| 7858 |
+
optional: true
|
| 7859 |
+
|
| 7860 |
+
'@img/[email protected]':
|
| 7861 |
+
dependencies:
|
| 7862 |
+
'@emnapi/runtime': 1.3.1
|
| 7863 |
+
optional: true
|
| 7864 |
+
|
| 7865 |
+
'@img/[email protected]':
|
| 7866 |
+
optional: true
|
| 7867 |
+
|
| 7868 |
+
'@img/[email protected]':
|
| 7869 |
+
optional: true
|
| 7870 |
+
|
| 7871 |
'@isaacs/[email protected]':
|
| 7872 |
dependencies:
|
| 7873 |
string-width: 5.1.2
|
|
|
|
| 7985 |
nanostores: 0.10.3
|
| 7986 |
react: 18.3.1
|
| 7987 |
|
| 7988 |
+
'@next/[email protected]': {}
|
| 7989 |
+
|
| 7990 |
+
'@next/[email protected]':
|
| 7991 |
+
optional: true
|
| 7992 |
+
|
| 7993 |
+
'@next/[email protected]':
|
| 7994 |
+
optional: true
|
| 7995 |
+
|
| 7996 |
+
'@next/[email protected]':
|
| 7997 |
+
optional: true
|
| 7998 |
+
|
| 7999 |
+
'@next/[email protected]':
|
| 8000 |
+
optional: true
|
| 8001 |
+
|
| 8002 |
+
'@next/[email protected]':
|
| 8003 |
+
optional: true
|
| 8004 |
+
|
| 8005 |
+
'@next/[email protected]':
|
| 8006 |
+
optional: true
|
| 8007 |
+
|
| 8008 |
+
'@next/[email protected]':
|
| 8009 |
+
optional: true
|
| 8010 |
+
|
| 8011 |
+
'@next/[email protected]':
|
| 8012 |
+
optional: true
|
| 8013 |
+
|
| 8014 |
'@nodelib/[email protected]':
|
| 8015 |
dependencies:
|
| 8016 |
'@nodelib/fs.stat': 2.0.5
|
|
|
|
| 9077 |
- supports-color
|
| 9078 |
- typescript
|
| 9079 |
|
| 9080 |
+
'@swc/[email protected]': {}
|
| 9081 |
+
|
| 9082 |
'@swc/[email protected]':
|
| 9083 |
dependencies:
|
| 9084 |
tslib: 2.8.1
|
|
|
|
| 9772 |
esbuild: 0.23.1
|
| 9773 |
load-tsconfig: 0.2.5
|
| 9774 |
|
| 9775 | |
| 9776 |
+
dependencies:
|
| 9777 |
+
streamsearch: 1.1.0
|
| 9778 |
+
|
| 9779 | |
| 9780 |
|
| 9781 | |
|
|
|
| 9890 |
|
| 9891 | |
| 9892 |
|
| 9893 | |
| 9894 |
+
dependencies:
|
| 9895 |
+
color-name: 1.1.4
|
| 9896 |
+
simple-swizzle: 0.2.2
|
| 9897 |
+
optional: true
|
| 9898 |
+
|
| 9899 | |
| 9900 |
+
dependencies:
|
| 9901 |
+
color-convert: 2.0.1
|
| 9902 |
+
color-string: 1.9.1
|
| 9903 |
+
optional: true
|
| 9904 |
+
|
| 9905 | |
| 9906 |
|
| 9907 | |
|
|
|
| 10067 |
|
| 10068 | |
| 10069 |
|
| 10070 | |
| 10071 |
+
optional: true
|
| 10072 |
+
|
| 10073 | |
| 10074 |
|
| 10075 | |
|
|
|
| 10928 |
call-bind: 1.0.7
|
| 10929 |
has-tostringtag: 1.0.2
|
| 10930 |
|
| 10931 | |
| 10932 |
+
optional: true
|
| 10933 |
+
|
| 10934 | |
| 10935 |
dependencies:
|
| 10936 |
binary-extensions: 2.3.0
|
|
|
|
| 11967 |
|
| 11968 | |
| 11969 |
|
| 11970 |
+
[email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected]):
|
| 11971 |
+
dependencies:
|
| 11972 |
+
'@next/env': 15.1.5
|
| 11973 |
+
'@swc/counter': 0.1.3
|
| 11974 |
+
'@swc/helpers': 0.5.15
|
| 11975 |
+
busboy: 1.6.0
|
| 11976 |
+
caniuse-lite: 1.0.30001685
|
| 11977 |
+
postcss: 8.4.31
|
| 11978 |
+
react: 18.3.1
|
| 11979 |
+
react-dom: 18.3.1([email protected])
|
| 11980 |
+
styled-jsx: 5.1.6(@babel/[email protected])([email protected])
|
| 11981 |
+
optionalDependencies:
|
| 11982 |
+
'@next/swc-darwin-arm64': 15.1.5
|
| 11983 |
+
'@next/swc-darwin-x64': 15.1.5
|
| 11984 |
+
'@next/swc-linux-arm64-gnu': 15.1.5
|
| 11985 |
+
'@next/swc-linux-arm64-musl': 15.1.5
|
| 11986 |
+
'@next/swc-linux-x64-gnu': 15.1.5
|
| 11987 |
+
'@next/swc-linux-x64-musl': 15.1.5
|
| 11988 |
+
'@next/swc-win32-arm64-msvc': 15.1.5
|
| 11989 |
+
'@next/swc-win32-x64-msvc': 15.1.5
|
| 11990 |
+
'@opentelemetry/api': 1.9.0
|
| 11991 |
+
sharp: 0.33.5
|
| 11992 |
+
transitivePeerDependencies:
|
| 11993 |
+
- '@babel/core'
|
| 11994 |
+
- babel-plugin-macros
|
| 11995 |
+
|
| 11996 | |
| 11997 |
|
| 11998 | |
|
|
|
| 12317 |
|
| 12318 | |
| 12319 |
|
| 12320 | |
| 12321 |
+
dependencies:
|
| 12322 |
+
nanoid: 3.3.8
|
| 12323 |
+
picocolors: 1.1.1
|
| 12324 |
+
source-map-js: 1.2.1
|
| 12325 |
+
|
| 12326 | |
| 12327 |
dependencies:
|
| 12328 |
nanoid: 3.3.8
|
|
|
|
| 12906 |
inherits: 2.0.4
|
| 12907 |
safe-buffer: 5.2.1
|
| 12908 |
|
| 12909 | |
| 12910 |
+
dependencies:
|
| 12911 |
+
color: 4.2.3
|
| 12912 |
+
detect-libc: 2.0.3
|
| 12913 |
+
semver: 7.6.3
|
| 12914 |
+
optionalDependencies:
|
| 12915 |
+
'@img/sharp-darwin-arm64': 0.33.5
|
| 12916 |
+
'@img/sharp-darwin-x64': 0.33.5
|
| 12917 |
+
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
| 12918 |
+
'@img/sharp-libvips-darwin-x64': 1.0.4
|
| 12919 |
+
'@img/sharp-libvips-linux-arm': 1.0.5
|
| 12920 |
+
'@img/sharp-libvips-linux-arm64': 1.0.4
|
| 12921 |
+
'@img/sharp-libvips-linux-s390x': 1.0.4
|
| 12922 |
+
'@img/sharp-libvips-linux-x64': 1.0.4
|
| 12923 |
+
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
| 12924 |
+
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
| 12925 |
+
'@img/sharp-linux-arm': 0.33.5
|
| 12926 |
+
'@img/sharp-linux-arm64': 0.33.5
|
| 12927 |
+
'@img/sharp-linux-s390x': 0.33.5
|
| 12928 |
+
'@img/sharp-linux-x64': 0.33.5
|
| 12929 |
+
'@img/sharp-linuxmusl-arm64': 0.33.5
|
| 12930 |
+
'@img/sharp-linuxmusl-x64': 0.33.5
|
| 12931 |
+
'@img/sharp-wasm32': 0.33.5
|
| 12932 |
+
'@img/sharp-win32-ia32': 0.33.5
|
| 12933 |
+
'@img/sharp-win32-x64': 0.33.5
|
| 12934 |
+
optional: true
|
| 12935 |
+
|
| 12936 | |
| 12937 |
dependencies:
|
| 12938 |
shebang-regex: 3.0.0
|
|
|
|
| 12969 |
once: 1.4.0
|
| 12970 |
simple-concat: 1.0.1
|
| 12971 |
|
| 12972 | |
| 12973 |
+
dependencies:
|
| 12974 |
+
is-arrayish: 0.3.2
|
| 12975 |
+
optional: true
|
| 12976 |
+
|
| 12977 | |
| 12978 |
dependencies:
|
| 12979 |
'@polka/url': 1.0.0-next.28
|
|
|
|
| 13042 |
|
| 13043 | |
| 13044 |
|
| 13045 |
+
[email protected]: {}
|
| 13046 |
+
|
| 13047 | |
| 13048 |
|
| 13049 | |
|
|
|
| 13097 |
dependencies:
|
| 13098 |
inline-style-parser: 0.2.4
|
| 13099 |
|
| 13100 |
+
[email protected](@babel/[email protected])([email protected]):
|
| 13101 |
+
dependencies:
|
| 13102 |
+
client-only: 0.0.1
|
| 13103 |
+
react: 18.3.1
|
| 13104 |
+
optionalDependencies:
|
| 13105 |
+
'@babel/core': 7.26.0
|
| 13106 |
+
|
| 13107 | |
| 13108 |
dependencies:
|
| 13109 |
has-flag: 4.0.0
|
scripts/update.sh
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Exit on any error
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo "Starting Bolt.DIY update process..."
|
| 7 |
+
|
| 8 |
+
# Get the current directory
|
| 9 |
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
| 10 |
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
| 11 |
+
|
| 12 |
+
# Store current version
|
| 13 |
+
CURRENT_VERSION=$(cat "$PROJECT_ROOT/package.json" | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[[:space:]]')
|
| 14 |
+
|
| 15 |
+
echo "Current version: $CURRENT_VERSION"
|
| 16 |
+
echo "Fetching latest version..."
|
| 17 |
+
|
| 18 |
+
# Create temp directory
|
| 19 |
+
TMP_DIR=$(mktemp -d)
|
| 20 |
+
cd "$TMP_DIR"
|
| 21 |
+
|
| 22 |
+
# Download latest release
|
| 23 |
+
LATEST_RELEASE_URL=$(curl -s https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest | grep "browser_download_url.*zip" | cut -d : -f 2,3 | tr -d \")
|
| 24 |
+
if [ -z "$LATEST_RELEASE_URL" ]; then
|
| 25 |
+
echo "Error: Could not find latest release download URL"
|
| 26 |
+
exit 1
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
echo "Downloading latest release..."
|
| 30 |
+
curl -L -o latest.zip "$LATEST_RELEASE_URL"
|
| 31 |
+
|
| 32 |
+
echo "Extracting update..."
|
| 33 |
+
unzip -q latest.zip
|
| 34 |
+
|
| 35 |
+
# Backup current installation
|
| 36 |
+
echo "Creating backup..."
|
| 37 |
+
BACKUP_DIR="$PROJECT_ROOT/backup_$(date +%Y%m%d_%H%M%S)"
|
| 38 |
+
mkdir -p "$BACKUP_DIR"
|
| 39 |
+
cp -r "$PROJECT_ROOT"/* "$BACKUP_DIR/"
|
| 40 |
+
|
| 41 |
+
# Install update
|
| 42 |
+
echo "Installing update..."
|
| 43 |
+
cp -r ./* "$PROJECT_ROOT/"
|
| 44 |
+
|
| 45 |
+
# Clean up
|
| 46 |
+
cd "$PROJECT_ROOT"
|
| 47 |
+
rm -rf "$TMP_DIR"
|
| 48 |
+
|
| 49 |
+
echo "Update completed successfully!"
|
| 50 |
+
echo "Please restart the application to apply the changes."
|
| 51 |
+
|
| 52 |
+
exit 0
|