UI fixes
This commit is contained in:
@@ -35,6 +35,7 @@ interface GitHubStats {
|
||||
interface GitHubConnection {
|
||||
user: GitHubUserResponse | null;
|
||||
token: string;
|
||||
tokenType: 'classic' | 'fine-grained';
|
||||
stats?: GitHubStats;
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ export default function ConnectionsTab() {
|
||||
const [connection, setConnection] = useState<GitHubConnection>({
|
||||
user: null,
|
||||
token: '',
|
||||
tokenType: 'classic',
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
@@ -53,7 +55,14 @@ export default function ConnectionsTab() {
|
||||
|
||||
if (savedConnection) {
|
||||
const parsed = JSON.parse(savedConnection);
|
||||
|
||||
// Ensure backward compatibility with existing connections
|
||||
if (!parsed.tokenType) {
|
||||
parsed.tokenType = 'classic';
|
||||
}
|
||||
|
||||
setConnection(parsed);
|
||||
|
||||
if (parsed.user && parsed.token) {
|
||||
fetchGitHubStats(parsed.token);
|
||||
}
|
||||
@@ -73,7 +82,9 @@ export default function ConnectionsTab() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!reposResponse.ok) throw new Error('Failed to fetch repositories');
|
||||
if (!reposResponse.ok) {
|
||||
throw new Error('Failed to fetch repositories');
|
||||
}
|
||||
|
||||
const repos = (await reposResponse.json()) as GitHubRepoInfo[];
|
||||
|
||||
@@ -107,10 +118,16 @@ export default function ConnectionsTab() {
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Invalid token or unauthorized');
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid token or unauthorized');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GitHubUserResponse;
|
||||
const newConnection = { user: data, token };
|
||||
const newConnection: GitHubConnection = {
|
||||
user: data,
|
||||
token,
|
||||
tokenType: connection.tokenType,
|
||||
};
|
||||
|
||||
// Save connection
|
||||
localStorage.setItem('github_connection', JSON.stringify(newConnection));
|
||||
@@ -123,7 +140,7 @@ export default function ConnectionsTab() {
|
||||
} catch (error) {
|
||||
logStore.logError('Failed to authenticate with GitHub', { error });
|
||||
toast.error('Failed to connect to GitHub');
|
||||
setConnection({ user: null, token: '' });
|
||||
setConnection({ user: null, token: '', tokenType: 'classic' });
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
@@ -136,11 +153,13 @@ export default function ConnectionsTab() {
|
||||
|
||||
const handleDisconnect = () => {
|
||||
localStorage.removeItem('github_connection');
|
||||
setConnection({ user: null, token: '' });
|
||||
setConnection({ user: null, token: '', tokenType: 'classic' });
|
||||
toast.success('Disconnected from GitHub');
|
||||
};
|
||||
|
||||
if (isLoading) return <LoadingSpinner />;
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -174,31 +193,37 @@ export default function ConnectionsTab() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-bolt-elements-textSecondary mb-2">GitHub Username</label>
|
||||
<input
|
||||
type="text"
|
||||
value={connection.user?.login || ''}
|
||||
disabled={true}
|
||||
placeholder="Not connected"
|
||||
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Token Type</label>
|
||||
<select
|
||||
value={connection.tokenType}
|
||||
onChange={(e) =>
|
||||
setConnection((prev) => ({ ...prev, tokenType: e.target.value as 'classic' | 'fine-grained' }))
|
||||
}
|
||||
disabled={isConnecting || !!connection.user}
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
||||
'disabled:opacity-50',
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<option value="classic">Personal Access Token (Classic)</option>
|
||||
<option value="fine-grained">Fine-grained Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label>
|
||||
<label className="block text-sm text-bolt-elements-textSecondary mb-2">
|
||||
{connection.tokenType === 'classic' ? 'Personal Access Token' : 'Fine-grained Token'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={connection.token}
|
||||
onChange={(e) => setConnection((prev) => ({ ...prev, token: e.target.value }))}
|
||||
disabled={isConnecting || !!connection.user}
|
||||
placeholder="Enter your GitHub token"
|
||||
placeholder={`Enter your GitHub ${connection.tokenType === 'classic' ? 'personal access token' : 'fine-grained token'}`}
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||
@@ -257,69 +282,49 @@ export default function ConnectionsTab() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{connection.user && connection.stats && (
|
||||
<div className="mt-6 border-t border-[#E5E5E5] dark:border-[#1A1A1A] pt-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
{connection.user && (
|
||||
<div className="p-4 bg-[#F8F8F8] dark:bg-[#1A1A1A] rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={connection.user.avatar_url}
|
||||
alt={connection.user.login}
|
||||
className="w-16 h-16 rounded-full"
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
{connection.user.name || connection.user.login}
|
||||
</h3>
|
||||
{connection.user.bio && (
|
||||
<p className="text-sm text-bolt-elements-textSecondary">{connection.user.bio}</p>
|
||||
)}
|
||||
<div className="flex gap-4 mt-2 text-sm text-bolt-elements-textSecondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:users w-4 h-4" />
|
||||
{connection.user.followers} followers
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:star w-4 h-4" />
|
||||
{connection.stats.totalStars} stars
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:git-fork w-4 h-4" />
|
||||
{connection.stats.totalForks} forks
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">{connection.user.name}</h4>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">@{connection.user.login}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-3">Recent Repositories</h4>
|
||||
<div className="space-y-3">
|
||||
{connection.stats.repos.map((repo) => (
|
||||
<a
|
||||
key={repo.full_name}
|
||||
href={repo.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#1A1A1A] hover:bg-[#F0F0F0] dark:hover:bg-[#252525] transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-bolt-elements-textPrimary">{repo.name}</h5>
|
||||
{repo.description && (
|
||||
<p className="text-xs text-bolt-elements-textSecondary mt-1">{repo.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:star w-3 h-3" />
|
||||
{repo.stargazers_count}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="i-ph:git-fork w-3 h-3" />
|
||||
{repo.forks_count}
|
||||
</span>
|
||||
</div>
|
||||
{isFetchingStats ? (
|
||||
<div className="mt-4 flex items-center gap-2 text-sm text-bolt-elements-textSecondary">
|
||||
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
||||
Fetching GitHub stats...
|
||||
</div>
|
||||
) : (
|
||||
connection.stats && (
|
||||
<div className="mt-4 grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">Public Repos</p>
|
||||
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
{connection.user.public_repos}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">Total Stars</p>
|
||||
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
{connection.stats.totalStars}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">Total Forks</p>
|
||||
<p className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
{connection.stats.totalForks}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub';
|
||||
import Cookies from 'js-cookie';
|
||||
import { getLocalStorage } from '~/utils/localStorage';
|
||||
|
||||
const GITHUB_TOKEN_KEY = 'github_token';
|
||||
|
||||
interface ConnectionFormProps {
|
||||
authState: GitHubAuthState;
|
||||
setAuthState: React.Dispatch<React.SetStateAction<GitHubAuthState>>;
|
||||
onSave: (e: React.FormEvent) => void;
|
||||
onDisconnect: () => void;
|
||||
}
|
||||
|
||||
export function ConnectionForm({ authState, setAuthState, onSave, onDisconnect }: ConnectionFormProps) {
|
||||
// Check for saved token on mount
|
||||
useEffect(() => {
|
||||
const savedToken = Cookies.get(GITHUB_TOKEN_KEY) || getLocalStorage(GITHUB_TOKEN_KEY);
|
||||
|
||||
if (savedToken && !authState.tokenInfo?.token) {
|
||||
setAuthState((prev: GitHubAuthState) => ({
|
||||
...prev,
|
||||
tokenInfo: {
|
||||
token: savedToken,
|
||||
scope: [],
|
||||
avatar_url: '',
|
||||
name: null,
|
||||
created_at: new Date().toISOString(),
|
||||
followers: 0,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] overflow-hidden">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="i-ph:plug-fill text-bolt-elements-textTertiary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Connection Settings</h3>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">Configure your GitHub connection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSave} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-bolt-elements-textSecondary mb-2">
|
||||
GitHub Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={authState.username}
|
||||
onChange={(e) => setAuthState((prev: GitHubAuthState) => ({ ...prev, username: e.target.value }))}
|
||||
className={classNames(
|
||||
'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-[#1A1A1A] border rounded-lg',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary text-base',
|
||||
'border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
placeholder="e.g., octocat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor="token" className="block text-sm font-medium text-bolt-elements-textSecondary">
|
||||
Personal Access Token
|
||||
</label>
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?scopes=repo,user,read:org,workflow,delete_repo,write:packages,read:packages"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classNames(
|
||||
'inline-flex items-center gap-1.5 text-xs',
|
||||
'text-purple-500 hover:text-purple-600 dark:text-purple-400 dark:hover:text-purple-300',
|
||||
'transition-colors duration-200',
|
||||
)}
|
||||
>
|
||||
<span>Generate new token</span>
|
||||
<div className="i-ph:plus-circle" />
|
||||
</a>
|
||||
</div>
|
||||
<input
|
||||
id="token"
|
||||
type="password"
|
||||
value={authState.tokenInfo?.token || ''}
|
||||
onChange={(e) =>
|
||||
setAuthState((prev: GitHubAuthState) => ({
|
||||
...prev,
|
||||
tokenInfo: {
|
||||
token: e.target.value,
|
||||
scope: [],
|
||||
avatar_url: '',
|
||||
name: null,
|
||||
created_at: new Date().toISOString(),
|
||||
followers: 0,
|
||||
},
|
||||
username: '',
|
||||
isConnected: false,
|
||||
isVerifying: false,
|
||||
isLoadingRepos: false,
|
||||
}))
|
||||
}
|
||||
className={classNames(
|
||||
'w-full px-4 py-2.5 bg-[#F5F5F5] dark:bg-[#1A1A1A] border rounded-lg',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary text-base',
|
||||
'border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
placeholder="ghp_xxxxxxxxxxxx"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="flex items-center gap-4">
|
||||
{!authState.isConnected ? (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={authState.isVerifying || !authState.username || !authState.tokenInfo?.token}
|
||||
className={classNames(
|
||||
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||
'bg-purple-500 hover:bg-purple-600',
|
||||
'text-white',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
{authState.isVerifying ? (
|
||||
<>
|
||||
<div className="i-ph:spinner animate-spin" />
|
||||
<span>Verifying...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-ph:plug-fill" />
|
||||
<span>Connect</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className={classNames(
|
||||
'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||
'bg-[#F5F5F5] hover:bg-red-500/10 hover:text-red-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-red-500/20 dark:hover:text-red-500',
|
||||
'text-bolt-elements-textPrimary',
|
||||
)}
|
||||
>
|
||||
<div className="i-ph:plug-fill" />
|
||||
<span>Disconnect</span>
|
||||
</button>
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 text-sm text-green-600 dark:text-green-400 bg-green-500/5 rounded-lg border border-green-500/20">
|
||||
<div className="i-ph:check-circle-fill" />
|
||||
<span>Connected</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{authState.rateLimits && (
|
||||
<div className="flex items-center gap-2 text-sm text-bolt-elements-textTertiary">
|
||||
<div className="i-ph:clock-countdown opacity-60" />
|
||||
<span>Rate limit resets at {authState.rateLimits.reset.toLocaleTimeString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useState } from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import type { GitHubRepoInfo } from '~/components/settings/connections/types/GitHub';
|
||||
import { GitBranch } from '@phosphor-icons/react';
|
||||
|
||||
interface GitHubBranch {
|
||||
name: string;
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
interface CreateBranchDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (branchName: string, sourceBranch: string) => void;
|
||||
repository: GitHubRepoInfo;
|
||||
branches?: GitHubBranch[];
|
||||
}
|
||||
|
||||
export function CreateBranchDialog({ isOpen, onClose, onConfirm, repository, branches }: CreateBranchDialogProps) {
|
||||
const [branchName, setBranchName] = useState('');
|
||||
const [sourceBranch, setSourceBranch] = useState(branches?.find((b) => b.default)?.name || 'main');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onConfirm(branchName, sourceBranch);
|
||||
setBranchName('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={onClose}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 dark:bg-black/80" />
|
||||
<Dialog.Content
|
||||
className={classNames(
|
||||
'fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'w-full max-w-md p-6 rounded-xl shadow-lg',
|
||||
'bg-white dark:bg-[#0A0A0A]',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
)}
|
||||
>
|
||||
<Dialog.Title className="text-lg font-medium text-bolt-elements-textPrimary mb-4">
|
||||
Create New Branch
|
||||
</Dialog.Title>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="branchName" className="block text-sm font-medium text-bolt-elements-textSecondary mb-2">
|
||||
Branch Name
|
||||
</label>
|
||||
<input
|
||||
id="branchName"
|
||||
type="text"
|
||||
value={branchName}
|
||||
onChange={(e) => setBranchName(e.target.value)}
|
||||
placeholder="feature/my-new-branch"
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg',
|
||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'text-bolt-elements-textPrimary placeholder:text-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/50',
|
||||
)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="sourceBranch"
|
||||
className="block text-sm font-medium text-bolt-elements-textSecondary mb-2"
|
||||
>
|
||||
Source Branch
|
||||
</label>
|
||||
<select
|
||||
id="sourceBranch"
|
||||
value={sourceBranch}
|
||||
onChange={(e) => setSourceBranch(e.target.value)}
|
||||
className={classNames(
|
||||
'w-full px-3 py-2 rounded-lg',
|
||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/50',
|
||||
)}
|
||||
>
|
||||
{branches?.map((branch) => (
|
||||
<option key={branch.name} value={branch.name}>
|
||||
{branch.name} {branch.default ? '(default)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg">
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textSecondary mb-2">Branch Overview</h4>
|
||||
<ul className="space-y-2 text-sm text-bolt-elements-textSecondary">
|
||||
<li className="flex items-center gap-2">
|
||||
<GitBranch className="text-lg" />
|
||||
Repository: {repository.name}
|
||||
</li>
|
||||
{branchName && (
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="i-ph:check-circle text-green-500" />
|
||||
New branch will be created as: {branchName}
|
||||
</li>
|
||||
)}
|
||||
<li className="flex items-center gap-2">
|
||||
<div className="i-ph:check-circle text-green-500" />
|
||||
Based on: {sourceBranch}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={classNames(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
|
||||
'transition-colors',
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={classNames(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium',
|
||||
'text-white bg-purple-500',
|
||||
'hover:bg-purple-600',
|
||||
'transition-colors',
|
||||
)}
|
||||
>
|
||||
Create Branch
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
59
app/components/settings/connections/types/GitHub.ts
Normal file
59
app/components/settings/connections/types/GitHub.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
export interface GitHubUserResponse {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
name: string;
|
||||
bio: string;
|
||||
public_repos: number;
|
||||
followers: number;
|
||||
following: number;
|
||||
}
|
||||
|
||||
export interface GitHubRepoInfo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
default_branch: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface GitHubStats {
|
||||
repos: GitHubRepoInfo[];
|
||||
totalStars: number;
|
||||
totalForks: number;
|
||||
}
|
||||
|
||||
export interface GitHubConnection {
|
||||
user: GitHubUserResponse | null;
|
||||
token: string;
|
||||
tokenType: 'classic' | 'fine-grained';
|
||||
stats?: GitHubStats;
|
||||
}
|
||||
|
||||
export interface GitHubTokenInfo {
|
||||
token: string;
|
||||
scope: string[];
|
||||
avatar_url: string;
|
||||
name: string | null;
|
||||
created_at: string;
|
||||
followers: number;
|
||||
}
|
||||
|
||||
export interface GitHubRateLimits {
|
||||
limit: number;
|
||||
remaining: number;
|
||||
reset: Date;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export interface GitHubAuthState {
|
||||
username: string;
|
||||
tokenInfo: GitHubTokenInfo | null;
|
||||
isConnected: boolean;
|
||||
isVerifying: boolean;
|
||||
isLoadingRepos: boolean;
|
||||
rateLimits?: GitHubRateLimits;
|
||||
}
|
||||
@@ -305,9 +305,9 @@ export default function DataTab() {
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="i-ph:chat-circle-duotone w-5 h-5 text-purple-500" />
|
||||
<h3 className="text-lg font-medium">Chat History</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Chat History</h3>
|
||||
</div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">Export or delete all your chat history.</p>
|
||||
<div className="flex gap-4">
|
||||
<motion.button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
|
||||
@@ -339,9 +339,9 @@ export default function DataTab() {
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="i-ph:gear-duotone w-5 h-5 text-purple-500" />
|
||||
<h3 className="text-lg font-medium">Settings Backup</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Settings Backup</h3>
|
||||
</div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Export your settings to a JSON file or import settings from a previously exported file.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
@@ -364,7 +364,7 @@ export default function DataTab() {
|
||||
Import Settings
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-50 text-yellow-600 text-sm hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-50 text-yellow-600 text-sm hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20 dark:text-yellow-500"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setShowResetInlineConfirm(true)}
|
||||
@@ -384,9 +384,9 @@ export default function DataTab() {
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="i-ph:key-duotone w-5 h-5 text-purple-500" />
|
||||
<h3 className="text-lg font-medium">API Keys Management</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">API Keys Management</h3>
|
||||
</div>
|
||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Import API keys from a JSON file or download a template to fill in your keys.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
@@ -405,7 +405,7 @@ export default function DataTab() {
|
||||
disabled={isDownloadingTemplate}
|
||||
>
|
||||
{isDownloadingTemplate ? (
|
||||
<div className="i-ph:spinner-gap-bold animate-spin" />
|
||||
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
||||
) : (
|
||||
<div className="i-ph:download-simple w-4 h-4" />
|
||||
)}
|
||||
|
||||
@@ -92,10 +92,12 @@ interface WebAppInfo {
|
||||
nodeVersion: string;
|
||||
dependencies: { [key: string]: string };
|
||||
devDependencies: { [key: string]: string };
|
||||
|
||||
// Build Info
|
||||
buildTime?: string;
|
||||
buildNumber?: string;
|
||||
environment?: string;
|
||||
|
||||
// Git Info
|
||||
gitInfo?: {
|
||||
branch: string;
|
||||
@@ -104,6 +106,7 @@ interface WebAppInfo {
|
||||
author: string;
|
||||
remoteUrl: string;
|
||||
};
|
||||
|
||||
// GitHub Repository Info
|
||||
repoInfo?: {
|
||||
name: string;
|
||||
@@ -121,6 +124,39 @@ interface WebAppInfo {
|
||||
};
|
||||
}
|
||||
|
||||
interface GitInfo {
|
||||
branch: string;
|
||||
commit: string;
|
||||
commitTime: string;
|
||||
author: string;
|
||||
remoteUrl: string;
|
||||
}
|
||||
|
||||
interface RepoData {
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
open_issues_count: number;
|
||||
default_branch: string;
|
||||
updated_at: string;
|
||||
owner: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppData {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
license: string;
|
||||
nodeVersion: string;
|
||||
dependencies: { [key: string]: string };
|
||||
devDependencies: { [key: string]: string };
|
||||
}
|
||||
|
||||
export default function DebugTab() {
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
||||
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
|
||||
@@ -328,23 +364,27 @@ export default function DebugTab() {
|
||||
|
||||
// Fetch local app info
|
||||
const appInfoResponse = await fetch('/api/system/app-info');
|
||||
|
||||
if (!appInfoResponse.ok) {
|
||||
throw new Error('Failed to fetch webapp info');
|
||||
}
|
||||
const appData = await appInfoResponse.json();
|
||||
|
||||
const appData = (await appInfoResponse.json()) as AppData;
|
||||
|
||||
// Fetch git info
|
||||
const gitInfoResponse = await fetch('/api/system/git-info');
|
||||
let gitInfo = null;
|
||||
let gitInfo: GitInfo | undefined;
|
||||
|
||||
if (gitInfoResponse.ok) {
|
||||
gitInfo = await gitInfoResponse.json();
|
||||
gitInfo = (await gitInfoResponse.json()) as GitInfo;
|
||||
}
|
||||
|
||||
// Fetch GitHub repository info
|
||||
const repoInfoResponse = await fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy');
|
||||
let repoInfo = null;
|
||||
let repoInfo: WebAppInfo['repoInfo'] | undefined;
|
||||
|
||||
if (repoInfoResponse.ok) {
|
||||
const repoData = await repoInfoResponse.json();
|
||||
const repoData = (await repoInfoResponse.json()) as RepoData;
|
||||
repoInfo = {
|
||||
name: repoData.name,
|
||||
fullName: repoData.full_name,
|
||||
@@ -396,21 +436,6 @@ export default function DebugTab() {
|
||||
return `${Math.round(size)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const handleLogSystemInfo = () => {
|
||||
if (!systemInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
logStore.logSystem('System Information', {
|
||||
os: systemInfo.os,
|
||||
arch: systemInfo.arch,
|
||||
cpus: systemInfo.cpus,
|
||||
memory: systemInfo.memory,
|
||||
node: systemInfo.node,
|
||||
});
|
||||
toast.success('System information logged');
|
||||
};
|
||||
|
||||
const handleLogPerformance = () => {
|
||||
try {
|
||||
setLoading((prev) => ({ ...prev, performance: true }));
|
||||
@@ -625,6 +650,26 @@ export default function DebugTab() {
|
||||
Check Errors
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={getWebAppInfo}
|
||||
disabled={loading.webAppInfo}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
||||
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
||||
)}
|
||||
>
|
||||
{loading.webAppInfo ? (
|
||||
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<div className="i-ph:info w-4 h-4" />
|
||||
)}
|
||||
Fetch WebApp Info
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={exportDebugInfo}
|
||||
className={classNames(
|
||||
@@ -640,67 +685,11 @@ export default function DebugTab() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Log Display */}
|
||||
{errorLog.errors.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Error Log</h3>
|
||||
<div className="bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">
|
||||
{errorLog.errors.map((error, index) => (
|
||||
<div key={index} className="mb-4 last:mb-0 p-3 bg-white rounded border border-red-200">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<span className="font-medium">Type:</span> {error.type}
|
||||
<span className="font-medium ml-4">Time:</span>
|
||||
{new Date(error.timestamp).toLocaleString()}
|
||||
</div>
|
||||
<div className="mt-2 text-red-600">{error.message}</div>
|
||||
{error.filename && (
|
||||
<div className="mt-1 text-sm text-gray-500">
|
||||
File: {error.filename} (Line: {error.lineNumber}, Column: {error.columnNumber})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Information */}
|
||||
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="i-ph:cpu text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleLogSystemInfo}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'transition-colors duration-200',
|
||||
)}
|
||||
>
|
||||
<div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
|
||||
Log
|
||||
</button>
|
||||
<button
|
||||
onClick={getSystemInfo}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'transition-colors duration-200',
|
||||
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
|
||||
)}
|
||||
disabled={loading.systemInfo}
|
||||
>
|
||||
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="i-ph:cpu text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
|
||||
</div>
|
||||
{systemInfo ? (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
@@ -826,26 +815,9 @@ export default function DebugTab() {
|
||||
|
||||
{/* Performance Metrics */}
|
||||
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="i-ph:chart-line text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogPerformance}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
||||
{ 'opacity-50 cursor-not-allowed': loading.performance },
|
||||
)}
|
||||
disabled={loading.performance}
|
||||
>
|
||||
<div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
|
||||
Log Performance
|
||||
</button>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="i-ph:chart-line text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
|
||||
</div>
|
||||
{systemInfo && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -914,26 +886,9 @@ export default function DebugTab() {
|
||||
|
||||
{/* WebApp Information */}
|
||||
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="i-ph:info text-blue-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={getWebAppInfo}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
||||
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
|
||||
)}
|
||||
disabled={loading.webAppInfo}
|
||||
>
|
||||
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.webAppInfo ? 'animate-spin' : '')} />
|
||||
Refresh
|
||||
</button>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="i-ph:info text-blue-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
|
||||
</div>
|
||||
{webAppInfo ? (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -1008,18 +963,12 @@ export default function DebugTab() {
|
||||
<span className="text-bolt-elements-textSecondary">Git Info:</span>
|
||||
</div>
|
||||
<div className="pl-6 space-y-1">
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Branch: {webAppInfo.gitInfo.branch}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Commit: {webAppInfo.gitInfo.commit}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Branch: {webAppInfo.gitInfo.branch}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Commit: {webAppInfo.gitInfo.commit}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Commit Time: {webAppInfo.gitInfo.commitTime}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Author: {webAppInfo.gitInfo.author}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Author: {webAppInfo.gitInfo.author}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Remote URL: {webAppInfo.gitInfo.remoteUrl}
|
||||
</div>
|
||||
@@ -1033,21 +982,15 @@ export default function DebugTab() {
|
||||
<span className="text-bolt-elements-textSecondary">GitHub Repository:</span>
|
||||
</div>
|
||||
<div className="pl-6 space-y-1">
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Name: {webAppInfo.repoInfo.name}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Name: {webAppInfo.repoInfo.name}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Full Name: {webAppInfo.repoInfo.fullName}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Description: {webAppInfo.repoInfo.description}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Stars: {webAppInfo.repoInfo.stars}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Forks: {webAppInfo.repoInfo.forks}
|
||||
</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Stars: {webAppInfo.repoInfo.stars}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">Forks: {webAppInfo.repoInfo.forks}</div>
|
||||
<div className="text-xs text-bolt-elements-textPrimary">
|
||||
Open Issues: {webAppInfo.repoInfo.openIssues}
|
||||
</div>
|
||||
@@ -1077,26 +1020,9 @@ export default function DebugTab() {
|
||||
|
||||
{/* Error Check */}
|
||||
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="i-ph:warning text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={checkErrors}
|
||||
className={classNames(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
||||
'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
|
||||
'text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
|
||||
{ 'opacity-50 cursor-not-allowed': loading.errors },
|
||||
)}
|
||||
disabled={loading.errors}
|
||||
>
|
||||
<div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
|
||||
Check for Errors
|
||||
</button>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="i-ph:warning text-purple-500 w-5 h-5" />
|
||||
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-bolt-elements-textSecondary">
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { TabManagement } from './TabManagement';
|
||||
import { TabTile } from '~/components/settings/shared/TabTile';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
||||
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
||||
import {
|
||||
tabConfigurationStore,
|
||||
resetTabConfiguration,
|
||||
updateTabConfiguration,
|
||||
developerModeStore,
|
||||
setDeveloperMode,
|
||||
} from '~/lib/stores/settings';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@@ -24,6 +30,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
|
||||
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
|
||||
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
|
||||
import { Switch } from '~/components/ui/Switch';
|
||||
|
||||
interface DraggableTabTileProps {
|
||||
tab: TabVisibilityConfig;
|
||||
@@ -83,8 +90,14 @@ const DraggableTabTile = ({
|
||||
},
|
||||
});
|
||||
|
||||
const dragDropRef = (node: HTMLDivElement | null) => {
|
||||
if (node) {
|
||||
drag(drop(node));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
||||
<div ref={dragDropRef} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
||||
<TabTile
|
||||
tab={tab}
|
||||
onClick={onClick}
|
||||
@@ -104,15 +117,32 @@ interface DeveloperWindowProps {
|
||||
}
|
||||
|
||||
export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const [showTabManagement, setShowTabManagement] = useState(false);
|
||||
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const [showTabManagement, setShowTabManagement] = useState(false);
|
||||
const developerMode = useStore(developerModeStore);
|
||||
const [profile, setProfile] = useState(() => {
|
||||
const saved = localStorage.getItem('bolt_user_profile');
|
||||
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
|
||||
});
|
||||
|
||||
// Handle developer mode change
|
||||
const handleDeveloperModeChange = (checked: boolean) => {
|
||||
setDeveloperMode(checked);
|
||||
|
||||
if (!checked) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure developer mode is true when window is opened
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDeveloperMode(true);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Listen for profile changes
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
@@ -134,6 +164,38 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||
|
||||
// Ensure tab configuration is properly initialized
|
||||
useEffect(() => {
|
||||
if (!tabConfiguration || !tabConfiguration.userTabs || !tabConfiguration.developerTabs) {
|
||||
console.warn('Tab configuration is invalid in DeveloperWindow, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
} else {
|
||||
// Validate tab configuration structure
|
||||
const isValid =
|
||||
tabConfiguration.userTabs.every(
|
||||
(tab) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
) &&
|
||||
tabConfiguration.developerTabs.every(
|
||||
(tab) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn('Tab configuration is malformed in DeveloperWindow, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
}
|
||||
}
|
||||
}, [tabConfiguration]);
|
||||
|
||||
const handleBack = () => {
|
||||
if (showTabManagement) {
|
||||
setShowTabManagement(false);
|
||||
@@ -143,21 +205,55 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
};
|
||||
|
||||
// Only show tabs that are assigned to the developer window AND are visible
|
||||
const visibleDeveloperTabs = tabConfiguration.developerTabs
|
||||
.filter((tab) => {
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
return false;
|
||||
}
|
||||
const visibleDeveloperTabs = useMemo(() => {
|
||||
console.log('Filtering developer tabs with configuration:', tabConfiguration);
|
||||
|
||||
return tab.visible;
|
||||
})
|
||||
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
||||
if (!tabConfiguration?.developerTabs || !Array.isArray(tabConfiguration.developerTabs)) {
|
||||
console.warn('Invalid tab configuration, using empty array');
|
||||
return [];
|
||||
}
|
||||
|
||||
return tabConfiguration.developerTabs
|
||||
.filter((tab) => {
|
||||
if (!tab || typeof tab.id !== 'string') {
|
||||
console.warn('Invalid tab entry:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
console.log('Hiding notifications tab due to disabled notifications');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the tab has the required properties
|
||||
if (typeof tab.visible !== 'boolean' || typeof tab.window !== 'string' || typeof tab.order !== 'number') {
|
||||
console.warn('Tab missing required properties:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show tabs that are explicitly visible and assigned to the developer window
|
||||
const isVisible = tab.visible && tab.window === 'developer';
|
||||
console.log(`Tab ${tab.id} visibility:`, isVisible);
|
||||
|
||||
return isVisible;
|
||||
})
|
||||
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => {
|
||||
const orderA = typeof a.order === 'number' ? a.order : 0;
|
||||
const orderB = typeof b.order === 'number' ? b.order : 0;
|
||||
|
||||
return orderA - orderB;
|
||||
});
|
||||
}, [tabConfiguration, profile.notifications]);
|
||||
|
||||
console.log('Filtered visible developer tabs:', visibleDeveloperTabs);
|
||||
|
||||
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
||||
const draggedTab = visibleDeveloperTabs[dragIndex];
|
||||
const targetTab = visibleDeveloperTabs[hoverIndex];
|
||||
|
||||
console.log('Moving developer tab:', { draggedTab, targetTab });
|
||||
|
||||
// Update the order of the dragged and target tabs
|
||||
const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
|
||||
const updatedTargetTab = { ...targetTab, order: draggedTab.order };
|
||||
@@ -278,7 +374,10 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[60]">
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center z-[60]"
|
||||
style={{ opacity: developerMode ? 1 : 0, transition: 'opacity 0.2s ease-in-out' }}
|
||||
>
|
||||
<RadixDialog.Overlay className="fixed inset-0">
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
@@ -299,7 +398,7 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
'flex flex-col overflow-hidden',
|
||||
)}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
animate={{ opacity: developerMode ? 1 : 0, scale: developerMode ? 1 : 0.95, y: developerMode ? 0 : 20 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
@@ -346,6 +445,16 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={developerMode}
|
||||
onCheckedChange={handleDeveloperModeChange}
|
||||
className="data-[state=checked]:bg-purple-500"
|
||||
aria-label="Toggle developer mode"
|
||||
/>
|
||||
<label className="text-sm text-gray-500 dark:text-gray-400">Switch to User Mode</label>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
|
||||
@@ -37,7 +37,7 @@ const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }
|
||||
const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
|
||||
return (
|
||||
<div className="mb-8 rounded-xl bg-white/5 p-6 backdrop-blur-sm dark:bg-gray-800/30">
|
||||
<div className="mb-8 rounded-xl bg-white/5 p-6 dark:bg-gray-800/30">
|
||||
<div className="mb-6">
|
||||
<h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
|
||||
<span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
|
||||
|
||||
@@ -10,8 +10,6 @@ import { settingsStyles } from '~/components/settings/settings.styles';
|
||||
import { toast } from 'react-toastify';
|
||||
import { BsBox, BsCodeSquare, BsRobot } from 'react-icons/bs';
|
||||
import type { IconType } from 'react-icons';
|
||||
import OllamaModelUpdater from './OllamaModelUpdater';
|
||||
import { DialogRoot, Dialog } from '~/components/ui/Dialog';
|
||||
import { BiChip } from 'react-icons/bi';
|
||||
import { TbBrandOpenai } from 'react-icons/tb';
|
||||
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
||||
@@ -33,12 +31,33 @@ const PROVIDER_DESCRIPTIONS: Record<ProviderName, string> = {
|
||||
OpenAILike: 'Connect to OpenAI-compatible API endpoints',
|
||||
};
|
||||
|
||||
interface OllamaModel {
|
||||
name: string;
|
||||
digest: string;
|
||||
size: number;
|
||||
modified_at: string;
|
||||
details?: {
|
||||
family: string;
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
};
|
||||
status?: 'idle' | 'updating' | 'updated' | 'error' | 'checking';
|
||||
error?: string;
|
||||
newDigest?: string;
|
||||
progress?: {
|
||||
current: number;
|
||||
total: number;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
const LocalProvidersTab = () => {
|
||||
const settings = useSettings();
|
||||
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
||||
const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
|
||||
const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
||||
const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
|
||||
// Effect to filter and sort providers
|
||||
useEffect(() => {
|
||||
@@ -46,9 +65,32 @@ const LocalProvidersTab = () => {
|
||||
.filter(([key]) => [...LOCAL_PROVIDERS, 'OpenAILike'].includes(key))
|
||||
.map(([key, value]) => {
|
||||
const provider = value as IProviderConfig;
|
||||
const envKey = providerBaseUrlEnvKeys[key]?.baseUrlKey;
|
||||
|
||||
// Get environment URL safely
|
||||
const envUrl = envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
|
||||
|
||||
console.log(`Checking env URL for ${key}:`, {
|
||||
envKey,
|
||||
envUrl,
|
||||
currentBaseUrl: provider.settings.baseUrl,
|
||||
});
|
||||
|
||||
// If there's an environment URL and no base URL set, update it
|
||||
if (envUrl && !provider.settings.baseUrl) {
|
||||
console.log(`Setting base URL for ${key} from env:`, envUrl);
|
||||
settings.updateProviderSettings(key, {
|
||||
...provider.settings,
|
||||
baseUrl: envUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: key,
|
||||
settings: provider.settings,
|
||||
settings: {
|
||||
...provider.settings,
|
||||
baseUrl: provider.settings.baseUrl || envUrl,
|
||||
},
|
||||
staticModels: provider.staticModels || [],
|
||||
getDynamicModels: provider.getDynamicModels,
|
||||
getApiKeyLink: provider.getApiKeyLink,
|
||||
@@ -57,16 +99,135 @@ const LocalProvidersTab = () => {
|
||||
} as IProviderConfig;
|
||||
});
|
||||
|
||||
const sorted = newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
|
||||
// Custom sort function to ensure LMStudio appears before OpenAILike
|
||||
const sorted = newFilteredProviders.sort((a, b) => {
|
||||
if (a.name === 'LMStudio') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.name === 'LMStudio') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.name === 'OpenAILike') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.name === 'OpenAILike') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setFilteredProviders(sorted);
|
||||
}, [settings.providers]);
|
||||
|
||||
// Helper function to safely get environment URL
|
||||
const getEnvUrl = (provider: IProviderConfig): string | undefined => {
|
||||
const envKey = providerBaseUrlEnvKeys[provider.name]?.baseUrlKey;
|
||||
return envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
|
||||
};
|
||||
|
||||
// Add effect to update category toggle state based on provider states
|
||||
useEffect(() => {
|
||||
const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
|
||||
setCategoryEnabled(newCategoryState);
|
||||
}, [filteredProviders]);
|
||||
|
||||
// Fetch Ollama models when enabled
|
||||
useEffect(() => {
|
||||
const ollamaProvider = filteredProviders.find((p) => p.name === 'Ollama');
|
||||
|
||||
if (ollamaProvider?.settings.enabled) {
|
||||
fetchOllamaModels();
|
||||
}
|
||||
}, [filteredProviders]);
|
||||
|
||||
const fetchOllamaModels = async () => {
|
||||
try {
|
||||
setIsLoadingModels(true);
|
||||
|
||||
const response = await fetch('http://127.0.0.1:11434/api/tags');
|
||||
const data = (await response.json()) as { models: OllamaModel[] };
|
||||
|
||||
setOllamaModels(
|
||||
data.models.map((model) => ({
|
||||
...model,
|
||||
status: 'idle' as const,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Ollama models:', error);
|
||||
} finally {
|
||||
setIsLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateOllamaModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:11434/api/pull', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: modelName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update ${modelName}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
throw new Error('No response reader available');
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
const text = new TextDecoder().decode(value);
|
||||
const lines = text.split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
const data = JSON.parse(line) as {
|
||||
status: string;
|
||||
completed?: number;
|
||||
total?: number;
|
||||
digest?: string;
|
||||
};
|
||||
|
||||
setOllamaModels((current) =>
|
||||
current.map((m) =>
|
||||
m.name === modelName
|
||||
? {
|
||||
...m,
|
||||
progress: {
|
||||
current: data.completed || 0,
|
||||
total: data.total || 0,
|
||||
status: data.status,
|
||||
},
|
||||
newDigest: data.digest,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedResponse = await fetch('http://127.0.0.1:11434/api/tags');
|
||||
const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
|
||||
const updatedModel = updatedData.models.find((m) => m.name === modelName);
|
||||
|
||||
return { success: true, newDigest: updatedModel?.digest };
|
||||
} catch (error) {
|
||||
console.error(`Error updating ${modelName}:`, error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleCategory = useCallback(
|
||||
(enabled: boolean) => {
|
||||
setCategoryEnabled(enabled);
|
||||
@@ -106,6 +267,31 @@ const LocalProvidersTab = () => {
|
||||
setEditingProvider(null);
|
||||
};
|
||||
|
||||
const handleUpdateOllamaModel = async (modelName: string) => {
|
||||
setOllamaModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m)));
|
||||
|
||||
const { success, newDigest } = await updateOllamaModel(modelName);
|
||||
|
||||
setOllamaModels((current) =>
|
||||
current.map((m) =>
|
||||
m.name === modelName
|
||||
? {
|
||||
...m,
|
||||
status: success ? 'updated' : 'error',
|
||||
error: success ? undefined : 'Update failed',
|
||||
newDigest,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
);
|
||||
|
||||
if (success) {
|
||||
toast.success(`Updated ${modelName}`);
|
||||
} else {
|
||||
toast.error(`Failed to update ${modelName}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
@@ -139,7 +325,7 @@ const LocalProvidersTab = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{filteredProviders.map((provider, index) => (
|
||||
<motion.div
|
||||
key={provider.name}
|
||||
@@ -150,6 +336,12 @@ const LocalProvidersTab = () => {
|
||||
'transition-all duration-200',
|
||||
'relative overflow-hidden group',
|
||||
'flex flex-col',
|
||||
|
||||
// Make Ollama span 2 rows
|
||||
provider.name === 'Ollama' ? 'row-span-2' : '',
|
||||
|
||||
// Place Ollama in the second column
|
||||
provider.name === 'Ollama' ? 'col-start-2' : 'col-start-1',
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -253,21 +445,109 @@ const LocalProvidersTab = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
||||
<div className="mt-2 text-xs text-green-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="i-ph:info" />
|
||||
<span>Environment URL set in .env file</span>
|
||||
{providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
|
||||
<div className="mt-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className={
|
||||
getEnvUrl(provider)
|
||||
? 'i-ph:check-circle text-green-500'
|
||||
: 'i-ph:warning-circle text-yellow-500'
|
||||
}
|
||||
/>
|
||||
<span className={getEnvUrl(provider) ? 'text-green-500' : 'text-yellow-500'}>
|
||||
{getEnvUrl(provider)
|
||||
? 'Environment URL set in .env.local'
|
||||
: 'Environment URL not set in .env.local'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{provider.name === 'Ollama' && provider.settings.enabled && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="i-ph:cube-duotone text-purple-500" />
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary">Installed Models</span>
|
||||
</div>
|
||||
{isLoadingModels ? (
|
||||
<div className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary">
|
||||
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
|
||||
Loading models...
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-bolt-elements-textSecondary">
|
||||
{ollamaModels.length} models available
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{ollamaModels.map((model) => (
|
||||
<div
|
||||
key={model.name}
|
||||
className="flex items-center justify-between p-2 rounded-lg bg-bolt-elements-background-depth-3"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
|
||||
{model.status === 'updating' && (
|
||||
<div className="i-ph:spinner-gap-bold animate-spin w-4 h-4 text-purple-500" />
|
||||
)}
|
||||
{model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
|
||||
{model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
|
||||
<span>Version: {model.digest.substring(0, 7)}</span>
|
||||
{model.status === 'updated' && model.newDigest && (
|
||||
<>
|
||||
<div className="i-ph:arrow-right w-3 h-3" />
|
||||
<span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
|
||||
</>
|
||||
)}
|
||||
{model.progress && (
|
||||
<span className="ml-2">
|
||||
{model.progress.status}{' '}
|
||||
{model.progress.total > 0 && (
|
||||
<>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{model.details && (
|
||||
<span className="ml-2">
|
||||
({model.details.parameter_size}, {model.details.quantization_level})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
onClick={() => handleUpdateOllamaModel(model.name)}
|
||||
disabled={model.status === 'updating'}
|
||||
className={classNames(
|
||||
settingsStyles.button.base,
|
||||
settingsStyles.button.secondary,
|
||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="i-ph:arrows-clockwise" />
|
||||
Update
|
||||
</motion.button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
|
||||
animate={{
|
||||
@@ -276,36 +556,10 @@ const LocalProvidersTab = () => {
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
|
||||
{provider.name === 'Ollama' && provider.settings.enabled && (
|
||||
<motion.button
|
||||
onClick={() => setShowOllamaUpdater(true)}
|
||||
className={classNames(
|
||||
settingsStyles.button.base,
|
||||
settingsStyles.button.secondary,
|
||||
'ml-2',
|
||||
'hover:bg-purple-500/10 hover:text-purple-500',
|
||||
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
|
||||
)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="i-ph:arrows-clockwise" />
|
||||
Update Models
|
||||
</motion.button>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
|
||||
<Dialog>
|
||||
<div className="p-6">
|
||||
<OllamaModelUpdater />
|
||||
</div>
|
||||
</Dialog>
|
||||
</DialogRoot>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Switch } from '~/components/ui/Switch';
|
||||
import { themeStore, kTheme } from '~/lib/stores/theme';
|
||||
import type { UserProfile } from '~/components/settings/settings.types';
|
||||
import { settingsStyles } from '~/components/settings/settings.styles';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { shortcutsStore } from '~/lib/stores/settings';
|
||||
|
||||
export default function SettingsTab() {
|
||||
const [currentTimezone, setCurrentTimezone] = useState('');
|
||||
@@ -212,6 +214,39 @@ export default function SettingsTab() {
|
||||
</select>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Keyboard Shortcuts */}
|
||||
<motion.div
|
||||
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="i-ph:keyboard-fill w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary">Keyboard Shortcuts</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
|
||||
<div key={name} className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A]">
|
||||
<span className="text-sm text-bolt-elements-textPrimary capitalize">
|
||||
{name.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{shortcut.ctrlOrMetaKey && (
|
||||
<kbd className="kdb">{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}</kbd>
|
||||
)}
|
||||
{shortcut.ctrlKey && <kbd className="kdb">Ctrl</kbd>}
|
||||
{shortcut.metaKey && <kbd className="kdb">⌘</kbd>}
|
||||
{shortcut.shiftKey && <kbd className="kdb">⇧</kbd>}
|
||||
{shortcut.altKey && <kbd className="kdb">⌥</kbd>}
|
||||
<kbd className="kdb">{shortcut.key.toUpperCase()}</kbd>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const DraggableTabItem = ({
|
||||
onWindowChange,
|
||||
onVisibilityChange,
|
||||
}: DraggableTabItemProps) => {
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
const [{ isDragging }, dragRef] = useDrag({
|
||||
type: 'tab',
|
||||
item: { type: 'tab', index, id: tab.id },
|
||||
collect: (monitor) => ({
|
||||
@@ -44,7 +44,7 @@ const DraggableTabItem = ({
|
||||
}),
|
||||
});
|
||||
|
||||
const [, drop] = useDrop({
|
||||
const [, dropRef] = useDrop({
|
||||
accept: 'tab',
|
||||
hover: (item: DragItem, monitor) => {
|
||||
if (!monitor.isOver({ shallow: true })) {
|
||||
@@ -64,9 +64,14 @@ const DraggableTabItem = ({
|
||||
},
|
||||
});
|
||||
|
||||
const ref = (node: HTMLDivElement | null) => {
|
||||
dragRef(node);
|
||||
dropRef(node);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={(node) => drag(drop(node))}
|
||||
ref={ref}
|
||||
initial={false}
|
||||
animate={{
|
||||
scale: isDragging ? 1.02 : 1,
|
||||
|
||||
@@ -55,7 +55,7 @@ export const TabTile = ({
|
||||
'border border-[#E5E5E5]/50 dark:border-[#333333]/50',
|
||||
|
||||
// Shadow and glass effect
|
||||
'shadow-sm backdrop-blur-sm',
|
||||
'shadow-sm',
|
||||
'dark:shadow-[0_0_15px_rgba(0,0,0,0.1)]',
|
||||
'dark:bg-opacity-50',
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
import { Switch } from '~/components/ui/Switch';
|
||||
@@ -9,7 +9,6 @@ import type { TabType, TabVisibilityConfig } from '~/components/settings/setting
|
||||
import { TAB_LABELS } from '~/components/settings/settings.types';
|
||||
import { DeveloperWindow } from '~/components/settings/developer/DeveloperWindow';
|
||||
import { TabTile } from '~/components/settings/shared/TabTile';
|
||||
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@@ -30,6 +29,13 @@ import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
|
||||
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
|
||||
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
|
||||
import {
|
||||
tabConfigurationStore,
|
||||
resetTabConfiguration,
|
||||
updateTabConfiguration,
|
||||
developerModeStore,
|
||||
setDeveloperMode,
|
||||
} from '~/lib/stores/settings';
|
||||
|
||||
interface DraggableTabTileProps {
|
||||
tab: TabVisibilityConfig;
|
||||
@@ -89,8 +95,14 @@ const DraggableTabTile = ({
|
||||
},
|
||||
});
|
||||
|
||||
const dragDropRef = (node: HTMLDivElement | null) => {
|
||||
if (node) {
|
||||
drag(drop(node));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
||||
<div ref={dragDropRef} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
||||
<TabTile
|
||||
tab={tab}
|
||||
onClick={onClick}
|
||||
@@ -110,10 +122,15 @@ interface UsersWindowProps {
|
||||
}
|
||||
|
||||
export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
const [developerMode, setDeveloperMode] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const developerMode = useStore(developerModeStore);
|
||||
const [showDeveloperWindow, setShowDeveloperWindow] = useState(false);
|
||||
const [profile, setProfile] = useState(() => {
|
||||
const saved = localStorage.getItem('bolt_user_profile');
|
||||
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
|
||||
});
|
||||
|
||||
// Status hooks
|
||||
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
||||
@@ -122,11 +139,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||
|
||||
const [profile, setProfile] = useState(() => {
|
||||
const saved = localStorage.getItem('bolt_user_profile');
|
||||
return saved ? JSON.parse(saved) : { avatar: null, notifications: true };
|
||||
});
|
||||
|
||||
// Listen for profile changes
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'bolt_user_profile') {
|
||||
@@ -140,8 +153,66 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, []);
|
||||
|
||||
// Listen for settings toggle event
|
||||
useEffect(() => {
|
||||
const handleToggleSettings = () => {
|
||||
if (!open) {
|
||||
// Open settings panel
|
||||
setActiveTab('settings');
|
||||
onClose(); // Close any other open panels
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('toggle-settings', handleToggleSettings);
|
||||
|
||||
return () => document.removeEventListener('toggle-settings', handleToggleSettings);
|
||||
}, [open, onClose]);
|
||||
|
||||
// Ensure tab configuration is properly initialized
|
||||
useEffect(() => {
|
||||
if (!tabConfiguration || !tabConfiguration.userTabs || !tabConfiguration.developerTabs) {
|
||||
console.warn('Tab configuration is invalid, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
} else {
|
||||
// Validate tab configuration structure
|
||||
const isValid =
|
||||
tabConfiguration.userTabs.every(
|
||||
(tab) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
) &&
|
||||
tabConfiguration.developerTabs.every(
|
||||
(tab) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn('Tab configuration is malformed, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
}
|
||||
}
|
||||
}, [tabConfiguration]);
|
||||
|
||||
// Handle developer mode changes
|
||||
const handleDeveloperModeChange = (checked: boolean) => {
|
||||
setDeveloperMode(checked);
|
||||
|
||||
if (checked) {
|
||||
setShowDeveloperWindow(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle developer window close
|
||||
const handleDeveloperWindowClose = () => {
|
||||
setShowDeveloperWindow(false);
|
||||
setDeveloperMode(false);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
@@ -149,21 +220,55 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
};
|
||||
|
||||
// Only show tabs that are assigned to the user window AND are visible
|
||||
const visibleUserTabs = tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
return false;
|
||||
}
|
||||
const visibleUserTabs = useMemo(() => {
|
||||
console.log('Filtering user tabs with configuration:', tabConfiguration);
|
||||
|
||||
return tab.visible;
|
||||
})
|
||||
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||
console.warn('Invalid tab configuration, using empty array');
|
||||
return [];
|
||||
}
|
||||
|
||||
return tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
if (!tab || typeof tab.id !== 'string') {
|
||||
console.warn('Invalid tab entry:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
console.log('Hiding notifications tab due to disabled notifications');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the tab has the required properties
|
||||
if (typeof tab.visible !== 'boolean' || typeof tab.window !== 'string' || typeof tab.order !== 'number') {
|
||||
console.warn('Tab missing required properties:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show tabs that are explicitly visible and assigned to the user window
|
||||
const isVisible = tab.visible && tab.window === 'user';
|
||||
console.log(`Tab ${tab.id} visibility:`, isVisible);
|
||||
|
||||
return isVisible;
|
||||
})
|
||||
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => {
|
||||
const orderA = typeof a.order === 'number' ? a.order : 0;
|
||||
const orderB = typeof b.order === 'number' ? b.order : 0;
|
||||
|
||||
return orderA - orderB;
|
||||
});
|
||||
}, [tabConfiguration, profile.notifications]);
|
||||
|
||||
console.log('Filtered visible user tabs:', visibleUserTabs);
|
||||
|
||||
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
||||
const draggedTab = visibleUserTabs[dragIndex];
|
||||
const targetTab = visibleUserTabs[hoverIndex];
|
||||
|
||||
console.log('Moving tab:', { draggedTab, targetTab });
|
||||
|
||||
// Update the order of the dragged and target tabs
|
||||
const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
|
||||
const updatedTargetTab = { ...targetTab, order: draggedTab.order };
|
||||
@@ -310,7 +415,7 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
className="data-[state=checked]:bg-purple-500"
|
||||
aria-label="Toggle developer mode"
|
||||
/>
|
||||
<label className="text-sm text-gray-500 dark:text-gray-400">Developer Mode</label>
|
||||
<label className="text-sm text-gray-500 dark:text-gray-400">Switch to Developer Mode</label>
|
||||
</div>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
@@ -412,9 +517,9 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
|
||||
<DeveloperWindow open={showDeveloperWindow} onClose={handleDeveloperWindowClose} />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Root open={open && !showDeveloperWindow}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[50]">
|
||||
<RadixDialog.Overlay asChild>
|
||||
|
||||
@@ -87,7 +87,9 @@ export const FileBreadcrumb = memo<FileBreadcrumbProps>(({ files, pathSegments =
|
||||
<DropdownMenu.Root open={isActive} modal={false}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<span
|
||||
ref={(ref) => (segmentRefs.current[index] = ref)}
|
||||
ref={(ref) => {
|
||||
segmentRefs.current[index] = ref;
|
||||
}}
|
||||
className={classNames('flex items-center gap-1.5 cursor-pointer shrink-0', {
|
||||
'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary': !isActive,
|
||||
'text-bolt-elements-textPrimary underline': isActive,
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { IProviderConfig } from '~/types/model';
|
||||
import type { TabVisibilityConfig, TabWindowConfig } from '~/components/settings/settings.types';
|
||||
import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
|
||||
import Cookies from 'js-cookie';
|
||||
import { toggleTheme } from './theme';
|
||||
import { chatStore } from './chat';
|
||||
|
||||
export interface Shortcut {
|
||||
key: string;
|
||||
@@ -18,6 +20,9 @@ export interface Shortcut {
|
||||
|
||||
export interface Shortcuts {
|
||||
toggleTerminal: Shortcut;
|
||||
toggleTheme: Shortcut;
|
||||
toggleChat: Shortcut;
|
||||
toggleSettings: Shortcut;
|
||||
}
|
||||
|
||||
export const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
|
||||
@@ -31,6 +36,25 @@ export const shortcutsStore = map<Shortcuts>({
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => workbenchStore.toggleTerminal(),
|
||||
},
|
||||
toggleTheme: {
|
||||
key: 't',
|
||||
ctrlOrMetaKey: true,
|
||||
shiftKey: true,
|
||||
action: () => toggleTheme(),
|
||||
},
|
||||
toggleChat: {
|
||||
key: '/',
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => chatStore.setKey('showChat', !chatStore.get().showChat),
|
||||
},
|
||||
toggleSettings: {
|
||||
key: ',',
|
||||
ctrlOrMetaKey: true,
|
||||
action: () => {
|
||||
// This will be connected to the settings panel toggle
|
||||
document.dispatchEvent(new CustomEvent('toggle-settings'));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initialProviderSettings: ProviderSetting = {};
|
||||
@@ -70,18 +94,69 @@ export const enableContextOptimizationStore = atom(false);
|
||||
|
||||
// Initialize tab configuration from cookie or default
|
||||
const savedTabConfig = Cookies.get('tabConfiguration');
|
||||
const initialTabConfig: TabWindowConfig = savedTabConfig
|
||||
? JSON.parse(savedTabConfig)
|
||||
: {
|
||||
console.log('Saved tab configuration:', savedTabConfig);
|
||||
|
||||
let initialTabConfig: TabWindowConfig;
|
||||
|
||||
try {
|
||||
if (savedTabConfig) {
|
||||
const parsedConfig = JSON.parse(savedTabConfig);
|
||||
|
||||
// Validate the parsed configuration
|
||||
if (
|
||||
parsedConfig &&
|
||||
Array.isArray(parsedConfig.userTabs) &&
|
||||
Array.isArray(parsedConfig.developerTabs) &&
|
||||
parsedConfig.userTabs.every(
|
||||
(tab: any) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
) &&
|
||||
parsedConfig.developerTabs.every(
|
||||
(tab: any) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
)
|
||||
) {
|
||||
initialTabConfig = parsedConfig;
|
||||
console.log('Using saved tab configuration');
|
||||
} else {
|
||||
console.warn('Invalid saved tab configuration, using defaults');
|
||||
initialTabConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log('No saved tab configuration found, using defaults');
|
||||
initialTabConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading tab configuration:', error);
|
||||
initialTabConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Initial tab configuration:', initialTabConfig);
|
||||
|
||||
export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
|
||||
|
||||
// Helper function to update tab configuration
|
||||
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
|
||||
const currentConfig = tabConfigurationStore.get();
|
||||
console.log('Current tab configuration before update:', currentConfig);
|
||||
|
||||
const isUserTab = config.window === 'user';
|
||||
const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
|
||||
|
||||
@@ -99,16 +174,38 @@ export const updateTabConfiguration = (config: TabVisibilityConfig) => {
|
||||
[targetArray]: updatedTabs,
|
||||
};
|
||||
|
||||
console.log('New tab configuration after update:', newConfig);
|
||||
|
||||
tabConfigurationStore.set(newConfig);
|
||||
Cookies.set('tabConfiguration', JSON.stringify(newConfig));
|
||||
Cookies.set('tabConfiguration', JSON.stringify(newConfig), {
|
||||
expires: 365, // Set cookie to expire in 1 year
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to reset tab configuration
|
||||
export const resetTabConfiguration = () => {
|
||||
console.log('Resetting tab configuration to defaults');
|
||||
|
||||
const defaultConfig: TabWindowConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
|
||||
console.log('Default tab configuration:', defaultConfig);
|
||||
|
||||
tabConfigurationStore.set(defaultConfig);
|
||||
Cookies.set('tabConfiguration', JSON.stringify(defaultConfig));
|
||||
Cookies.set('tabConfiguration', JSON.stringify(defaultConfig), {
|
||||
expires: 365, // Set cookie to expire in 1 year
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
};
|
||||
|
||||
// Developer mode store
|
||||
export const developerModeStore = atom<boolean>(false);
|
||||
|
||||
export const setDeveloperMode = (value: boolean) => {
|
||||
developerModeStore.set(value);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { json } from '@remix-run/node';
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function loader({ request: _request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
||||
const commit = execSync('git rev-parse --short HEAD').toString().trim();
|
||||
|
||||
Reference in New Issue
Block a user