From 5e3a7b69d75eaaabeb37e4a24baa83a96da38121 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:07:07 +0300 Subject: [PATCH] Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402) --- package-lock.json | 13 + package.json | 1 + src/components/ApiKeysSettings.jsx | 373 ---- src/components/CodeEditor.jsx | 875 -------- src/components/CommandMenu.jsx | 367 --- src/components/CredentialsSettings.jsx | 421 ---- src/components/DarkModeToggle.jsx | 35 - src/components/DarkModeToggle.tsx | 48 + src/components/ErrorBoundary.jsx | 140 +- src/components/FileTree.jsx | 729 ------ src/components/GitPanel.jsx | 1426 ------------ src/components/GitSettings.jsx | 131 -- src/components/LoginModal.jsx | 2 +- src/components/MicButton.jsx | 272 --- src/components/NextTaskBanner.jsx | 2 +- src/components/Onboarding.jsx | 10 +- src/components/Settings.jsx | 1977 ----------------- src/components/Shell.jsx | 692 ------ src/components/StandaloneShell.jsx | 105 - src/components/TaskDetail.jsx | 3 +- src/components/TaskList.jsx | 76 +- src/components/TaskMasterSetupWizard.jsx | 3 +- src/components/TasksSettings.jsx | 110 +- .../chat/hooks/useChatComposerState.ts | 6 + .../chat/hooks/useChatRealtimeHandlers.ts | 20 +- .../chat/tools/components/OneLineDisplay.tsx | 49 +- src/components/chat/view/ChatInterface.tsx | 8 +- .../AssistantThinkingIndicator.tsx | 2 +- .../chat/view/subcomponents/ChatComposer.tsx | 9 +- .../view/subcomponents/ClaudeStatus.tsx} | 89 +- .../chat/view/subcomponents/CommandMenu.tsx | 224 ++ .../chat/view/subcomponents/Markdown.tsx | 52 +- .../view/subcomponents/MessageComponent.tsx | 2 +- .../ProviderSelectionEmptyState.tsx | 2 +- .../code-editor/constants/settings.ts | 17 + .../hooks/useCodeEditorDocument.ts | 126 ++ .../hooks/useCodeEditorSettings.ts | 85 + .../hooks/useEditorKeyboardShortcuts.ts | 37 + .../hooks/useEditorSidebar.ts | 18 +- src/components/code-editor/types/types.ts | 21 + .../code-editor/utils/editorExtensions.ts | 141 ++ .../code-editor/utils/editorStyles.ts | 79 + .../code-editor/utils/editorToolbarPanel.ts | 212 ++ .../code-editor/view/CodeEditor.tsx | 234 ++ .../view}/EditorSidebar.tsx | 31 +- .../view/subcomponents/CodeEditorFooter.tsx | 28 + .../view/subcomponents/CodeEditorHeader.tsx | 143 ++ .../subcomponents/CodeEditorLoadingState.tsx | 36 + .../view/subcomponents/CodeEditorSurface.tsx | 62 + .../markdown/MarkdownCodeBlock.tsx | 72 + .../markdown/MarkdownPreview.tsx | 52 + .../file-tree/constants/constants.ts | 18 + .../file-tree/constants/fileIcons.ts | 224 ++ .../file-tree/hooks/useExpandedDirectories.ts | 44 + .../file-tree/hooks/useFileTreeData.ts | 76 + .../file-tree/hooks/useFileTreeSearch.ts | 42 + .../file-tree/hooks/useFileTreeViewMode.ts | 43 + src/components/file-tree/types/types.ts | 30 + .../file-tree/utils/fileTreeUtils.ts | 83 + src/components/file-tree/view/FileTree.tsx | 103 + .../file-tree/view/FileTreeBody.tsx | 62 + .../view/FileTreeDetailedColumns.tsx | 17 + .../file-tree/view/FileTreeEmptyState.tsx | 20 + .../file-tree/view/FileTreeHeader.tsx | 81 + .../file-tree/view/FileTreeList.tsx | 42 + .../file-tree/view/FileTreeLoadingState.tsx | 12 + .../file-tree/view/FileTreeNode.tsx | 141 ++ .../view/ImageViewer.tsx} | 47 +- .../git-panel/constants/constants.ts | 70 + .../git-panel/hooks/useGitPanelController.ts | 710 ++++++ .../git-panel/hooks/useSelectedProvider.ts | 20 + src/components/git-panel/types/types.ts | 135 ++ .../git-panel/utils/gitPanelUtils.ts | 26 + src/components/git-panel/view/GitPanel.tsx | 150 ++ .../git-panel/view/GitPanelHeader.tsx | 263 +++ .../view/GitRepositoryErrorState.tsx | 27 + src/components/git-panel/view/GitViewTabs.tsx | 45 + .../git-panel/view/changes/ChangesView.tsx | 213 ++ .../git-panel/view/changes/CommitComposer.tsx | 162 ++ .../git-panel/view/changes/FileChangeItem.tsx | 138 ++ .../git-panel/view/changes/FileChangeList.tsx | 55 + .../view/changes/FileSelectionControls.tsx | 44 + .../view/changes/FileStatusLegend.tsx | 52 + .../view/history/CommitHistoryItem.tsx | 71 + .../git-panel/view/history/HistoryView.tsx | 77 + .../view/modals/ConfirmActionModal.tsx | 97 + .../git-panel/view/modals/NewBranchModal.tsx | 124 ++ .../ClaudeLogo.tsx} | 6 +- .../CodexLogo.tsx} | 8 +- .../CursorLogo.tsx} | 8 +- .../SessionProviderLogo.tsx | 2 +- src/components/main-content/types/types.ts | 61 +- .../main-content/view/MainContent.tsx | 21 +- .../view/subcomponents/MainContentTitle.tsx | 2 +- .../mic-button/constants/constants.ts | 45 + src/components/mic-button/data/whisper.ts | 52 + .../hooks/useMicButtonController.ts | 204 ++ src/components/mic-button/types/types.ts | 2 + src/components/mic-button/view/MicButton.tsx | 32 + .../mic-button/view/MicButtonView.tsx | 86 + src/components/settings/McpServersContent.jsx | 319 --- .../settings/constants/constants.ts | 94 + .../settings/hooks/useCredentialsSettings.ts | 273 +++ .../settings/hooks/useGitSettings.ts | 96 + .../settings/hooks/useSettingsController.ts | 841 +++++++ src/components/settings/types/types.ts | 134 ++ src/components/settings/view/Settings.tsx | 249 +++ .../settings/view/SettingsMainTabs.tsx | 54 + .../view/modals/ClaudeMcpFormModal.tsx | 479 ++++ .../view/modals/CodexMcpFormModal.tsx | 178 ++ .../view/tabs/AppearanceSettingsTab.tsx | 193 ++ .../tabs/agents-settings/AgentListItem.tsx} | 38 +- .../agents-settings/AgentsSettingsTab.tsx | 101 + .../sections/AgentCategoryContentSection.tsx | 125 ++ .../sections/AgentCategoryTabsSection.tsx | 36 + .../sections/AgentSelectorSection.tsx | 44 + .../sections/content/AccountContent.tsx} | 52 +- .../sections/content/McpServersContent.tsx | 382 ++++ .../sections/content/PermissionsContent.tsx} | 349 ++- .../view/tabs/agents-settings/types.ts | 82 + .../api-settings/CredentialsSettingsTab.tsx | 100 + .../api-settings/sections/ApiKeysSection.tsx | 109 + .../sections/GithubCredentialsSection.tsx | 142 ++ .../api-settings/sections/NewApiKeyAlert.tsx | 42 + .../sections/VersionInfoSection.tsx | 46 + .../settings/view/tabs/api-settings/types.ts | 36 + .../view/tabs/git-settings/GitSettingsTab.tsx | 82 + .../tabs/tasks-settings/TasksSettingsTab.tsx | 106 + src/components/shell/constants/constants.ts | 63 + .../shell/hooks/useShellConnection.ts | 229 ++ src/components/shell/hooks/useShellRuntime.ts | 162 ++ .../shell/hooks/useShellTerminal.ts | 245 ++ src/components/shell/types/types.ts | 73 + src/components/shell/utils/auth.ts | 24 + src/components/shell/utils/socket.ts | 32 + src/components/shell/utils/terminalStyles.ts | 29 + src/components/shell/view/Shell.tsx | 162 ++ .../subcomponents/ShellConnectionOverlay.tsx | 59 + .../view/subcomponents/ShellEmptyState.tsx | 25 + .../shell/view/subcomponents/ShellHeader.tsx | 87 + .../view/subcomponents/ShellMinimalView.tsx | 113 + .../view}/modals/VersionUpgradeModal.tsx | 13 +- .../view/subcomponents/SidebarModals.tsx | 4 +- .../view/subcomponents/SidebarSessionItem.tsx | 2 +- .../standalone-shell/view/StandaloneShell.tsx | 74 + .../StandaloneShellEmptyState.tsx | 24 + .../subcomponents/StandaloneShellHeader.tsx | 30 + src/utils/clipboard.ts | 50 + src/utils/whisper.js | 37 - 149 files changed, 11627 insertions(+), 8453 deletions(-) delete mode 100644 src/components/ApiKeysSettings.jsx delete mode 100644 src/components/CodeEditor.jsx delete mode 100644 src/components/CommandMenu.jsx delete mode 100644 src/components/CredentialsSettings.jsx delete mode 100644 src/components/DarkModeToggle.jsx create mode 100644 src/components/DarkModeToggle.tsx delete mode 100644 src/components/FileTree.jsx delete mode 100644 src/components/GitPanel.jsx delete mode 100644 src/components/GitSettings.jsx delete mode 100644 src/components/MicButton.jsx delete mode 100644 src/components/Settings.jsx delete mode 100644 src/components/Shell.jsx delete mode 100644 src/components/StandaloneShell.jsx rename src/components/{ClaudeStatus.jsx => chat/view/subcomponents/ClaudeStatus.tsx} (61%) create mode 100644 src/components/chat/view/subcomponents/CommandMenu.tsx create mode 100644 src/components/code-editor/constants/settings.ts create mode 100644 src/components/code-editor/hooks/useCodeEditorDocument.ts create mode 100644 src/components/code-editor/hooks/useCodeEditorSettings.ts create mode 100644 src/components/code-editor/hooks/useEditorKeyboardShortcuts.ts rename src/components/{main-content => code-editor}/hooks/useEditorSidebar.ts (83%) create mode 100644 src/components/code-editor/types/types.ts create mode 100644 src/components/code-editor/utils/editorExtensions.ts create mode 100644 src/components/code-editor/utils/editorStyles.ts create mode 100644 src/components/code-editor/utils/editorToolbarPanel.ts create mode 100644 src/components/code-editor/view/CodeEditor.tsx rename src/components/{main-content/view/subcomponents => code-editor/view}/EditorSidebar.tsx (62%) create mode 100644 src/components/code-editor/view/subcomponents/CodeEditorFooter.tsx create mode 100644 src/components/code-editor/view/subcomponents/CodeEditorHeader.tsx create mode 100644 src/components/code-editor/view/subcomponents/CodeEditorLoadingState.tsx create mode 100644 src/components/code-editor/view/subcomponents/CodeEditorSurface.tsx create mode 100644 src/components/code-editor/view/subcomponents/markdown/MarkdownCodeBlock.tsx create mode 100644 src/components/code-editor/view/subcomponents/markdown/MarkdownPreview.tsx create mode 100644 src/components/file-tree/constants/constants.ts create mode 100644 src/components/file-tree/constants/fileIcons.ts create mode 100644 src/components/file-tree/hooks/useExpandedDirectories.ts create mode 100644 src/components/file-tree/hooks/useFileTreeData.ts create mode 100644 src/components/file-tree/hooks/useFileTreeSearch.ts create mode 100644 src/components/file-tree/hooks/useFileTreeViewMode.ts create mode 100644 src/components/file-tree/types/types.ts create mode 100644 src/components/file-tree/utils/fileTreeUtils.ts create mode 100644 src/components/file-tree/view/FileTree.tsx create mode 100644 src/components/file-tree/view/FileTreeBody.tsx create mode 100644 src/components/file-tree/view/FileTreeDetailedColumns.tsx create mode 100644 src/components/file-tree/view/FileTreeEmptyState.tsx create mode 100644 src/components/file-tree/view/FileTreeHeader.tsx create mode 100644 src/components/file-tree/view/FileTreeList.tsx create mode 100644 src/components/file-tree/view/FileTreeLoadingState.tsx create mode 100644 src/components/file-tree/view/FileTreeNode.tsx rename src/components/{ImageViewer.jsx => file-tree/view/ImageViewer.tsx} (72%) create mode 100644 src/components/git-panel/constants/constants.ts create mode 100644 src/components/git-panel/hooks/useGitPanelController.ts create mode 100644 src/components/git-panel/hooks/useSelectedProvider.ts create mode 100644 src/components/git-panel/types/types.ts create mode 100644 src/components/git-panel/utils/gitPanelUtils.ts create mode 100644 src/components/git-panel/view/GitPanel.tsx create mode 100644 src/components/git-panel/view/GitPanelHeader.tsx create mode 100644 src/components/git-panel/view/GitRepositoryErrorState.tsx create mode 100644 src/components/git-panel/view/GitViewTabs.tsx create mode 100644 src/components/git-panel/view/changes/ChangesView.tsx create mode 100644 src/components/git-panel/view/changes/CommitComposer.tsx create mode 100644 src/components/git-panel/view/changes/FileChangeItem.tsx create mode 100644 src/components/git-panel/view/changes/FileChangeList.tsx create mode 100644 src/components/git-panel/view/changes/FileSelectionControls.tsx create mode 100644 src/components/git-panel/view/changes/FileStatusLegend.tsx create mode 100644 src/components/git-panel/view/history/CommitHistoryItem.tsx create mode 100644 src/components/git-panel/view/history/HistoryView.tsx create mode 100644 src/components/git-panel/view/modals/ConfirmActionModal.tsx create mode 100644 src/components/git-panel/view/modals/NewBranchModal.tsx rename src/components/{ClaudeLogo.jsx => llm-logo-provider/ClaudeLogo.tsx} (56%) rename src/components/{CodexLogo.jsx => llm-logo-provider/CodexLogo.tsx} (58%) rename src/components/{CursorLogo.jsx => llm-logo-provider/CursorLogo.tsx} (58%) rename src/components/{ => llm-logo-provider}/SessionProviderLogo.tsx (90%) create mode 100644 src/components/mic-button/constants/constants.ts create mode 100644 src/components/mic-button/data/whisper.ts create mode 100644 src/components/mic-button/hooks/useMicButtonController.ts create mode 100644 src/components/mic-button/types/types.ts create mode 100644 src/components/mic-button/view/MicButton.tsx create mode 100644 src/components/mic-button/view/MicButtonView.tsx delete mode 100644 src/components/settings/McpServersContent.jsx create mode 100644 src/components/settings/constants/constants.ts create mode 100644 src/components/settings/hooks/useCredentialsSettings.ts create mode 100644 src/components/settings/hooks/useGitSettings.ts create mode 100644 src/components/settings/hooks/useSettingsController.ts create mode 100644 src/components/settings/types/types.ts create mode 100644 src/components/settings/view/Settings.tsx create mode 100644 src/components/settings/view/SettingsMainTabs.tsx create mode 100644 src/components/settings/view/modals/ClaudeMcpFormModal.tsx create mode 100644 src/components/settings/view/modals/CodexMcpFormModal.tsx create mode 100644 src/components/settings/view/tabs/AppearanceSettingsTab.tsx rename src/components/settings/{AgentListItem.jsx => view/tabs/agents-settings/AgentListItem.tsx} (79%) create mode 100644 src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx create mode 100644 src/components/settings/view/tabs/agents-settings/sections/AgentCategoryContentSection.tsx create mode 100644 src/components/settings/view/tabs/agents-settings/sections/AgentCategoryTabsSection.tsx create mode 100644 src/components/settings/view/tabs/agents-settings/sections/AgentSelectorSection.tsx rename src/components/settings/{AccountContent.jsx => view/tabs/agents-settings/sections/content/AccountContent.tsx} (73%) create mode 100644 src/components/settings/view/tabs/agents-settings/sections/content/McpServersContent.tsx rename src/components/settings/{PermissionsContent.jsx => view/tabs/agents-settings/sections/content/PermissionsContent.tsx} (66%) create mode 100644 src/components/settings/view/tabs/agents-settings/types.ts create mode 100644 src/components/settings/view/tabs/api-settings/CredentialsSettingsTab.tsx create mode 100644 src/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsx create mode 100644 src/components/settings/view/tabs/api-settings/sections/GithubCredentialsSection.tsx create mode 100644 src/components/settings/view/tabs/api-settings/sections/NewApiKeyAlert.tsx create mode 100644 src/components/settings/view/tabs/api-settings/sections/VersionInfoSection.tsx create mode 100644 src/components/settings/view/tabs/api-settings/types.ts create mode 100644 src/components/settings/view/tabs/git-settings/GitSettingsTab.tsx create mode 100644 src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx create mode 100644 src/components/shell/constants/constants.ts create mode 100644 src/components/shell/hooks/useShellConnection.ts create mode 100644 src/components/shell/hooks/useShellRuntime.ts create mode 100644 src/components/shell/hooks/useShellTerminal.ts create mode 100644 src/components/shell/types/types.ts create mode 100644 src/components/shell/utils/auth.ts create mode 100644 src/components/shell/utils/socket.ts create mode 100644 src/components/shell/utils/terminalStyles.ts create mode 100644 src/components/shell/view/Shell.tsx create mode 100644 src/components/shell/view/subcomponents/ShellConnectionOverlay.tsx create mode 100644 src/components/shell/view/subcomponents/ShellEmptyState.tsx create mode 100644 src/components/shell/view/subcomponents/ShellHeader.tsx create mode 100644 src/components/shell/view/subcomponents/ShellMinimalView.tsx rename src/components/{ => sidebar/view}/modals/VersionUpgradeModal.tsx (97%) create mode 100644 src/components/standalone-shell/view/StandaloneShell.tsx create mode 100644 src/components/standalone-shell/view/subcomponents/StandaloneShellEmptyState.tsx create mode 100644 src/components/standalone-shell/view/subcomponents/StandaloneShellHeader.tsx create mode 100644 src/utils/clipboard.ts delete mode 100755 src/utils/whisper.js diff --git a/package-lock.json b/package-lock.json index d2ea10b..4b54657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.1.2", "react-i18next": "^16.5.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", @@ -9834,6 +9835,18 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-i18next": { "version": "16.5.3", "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.3.tgz", diff --git a/package.json b/package.json index 8d54477..82dd5a8 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.1.2", "react-i18next": "^16.5.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", diff --git a/src/components/ApiKeysSettings.jsx b/src/components/ApiKeysSettings.jsx deleted file mode 100644 index 1ab27df..0000000 --- a/src/components/ApiKeysSettings.jsx +++ /dev/null @@ -1,373 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github } from 'lucide-react'; -import { authenticatedFetch } from '../utils/api'; -import { useTranslation } from 'react-i18next'; - -function ApiKeysSettings() { - const { t } = useTranslation('settings'); - const [apiKeys, setApiKeys] = useState([]); - const [githubTokens, setGithubTokens] = useState([]); - const [loading, setLoading] = useState(true); - const [showNewKeyForm, setShowNewKeyForm] = useState(false); - const [showNewTokenForm, setShowNewTokenForm] = useState(false); - const [newKeyName, setNewKeyName] = useState(''); - const [newTokenName, setNewTokenName] = useState(''); - const [newGithubToken, setNewGithubToken] = useState(''); - const [showToken, setShowToken] = useState({}); - const [copiedKey, setCopiedKey] = useState(null); - const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - try { - setLoading(true); - - // Fetch API keys - const apiKeysRes = await authenticatedFetch('/api/settings/api-keys'); - const apiKeysData = await apiKeysRes.json(); - setApiKeys(apiKeysData.apiKeys || []); - - // Fetch GitHub tokens - const githubRes = await authenticatedFetch('/api/settings/credentials?type=github_token'); - const githubData = await githubRes.json(); - setGithubTokens(githubData.credentials || []); - } catch (error) { - console.error('Error fetching settings:', error); - } finally { - setLoading(false); - } - }; - - const createApiKey = async () => { - if (!newKeyName.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/api-keys', { - method: 'POST', - body: JSON.stringify({ keyName: newKeyName }) - }); - - const data = await res.json(); - if (data.success) { - setNewlyCreatedKey(data.apiKey); - setNewKeyName(''); - setShowNewKeyForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating API key:', error); - } - }; - - const deleteApiKey = async (keyId) => { - if (!confirm(t('apiKeys.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting API key:', error); - } - }; - - const toggleApiKey = async (keyId, isActive) => { - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling API key:', error); - } - }; - - const createGithubToken = async () => { - if (!newTokenName.trim() || !newGithubToken.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/credentials', { - method: 'POST', - body: JSON.stringify({ - credentialName: newTokenName, - credentialType: 'github_token', - credentialValue: newGithubToken - }) - }); - - const data = await res.json(); - if (data.success) { - setNewTokenName(''); - setNewGithubToken(''); - setShowNewTokenForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating GitHub token:', error); - } - }; - - const deleteGithubToken = async (tokenId) => { - if (!confirm(t('apiKeys.github.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/credentials/${tokenId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting GitHub token:', error); - } - }; - - const toggleGithubToken = async (tokenId, isActive) => { - try { - await authenticatedFetch(`/api/settings/credentials/${tokenId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling GitHub token:', error); - } - }; - - const copyToClipboard = (text, id) => { - navigator.clipboard.writeText(text); - setCopiedKey(id); - setTimeout(() => setCopiedKey(null), 2000); - }; - - if (loading) { - return
{t('apiKeys.loading')}
; - } - - return ( -
- {/* New API Key Alert */} - {newlyCreatedKey && ( -
-

{t('apiKeys.newKey.alertTitle')}

-

- {t('apiKeys.newKey.alertMessage')} -

-
- - {newlyCreatedKey.apiKey} - - -
- -
- )} - - {/* API Keys Section */} -
-
-
- -

{t('apiKeys.title')}

-
- -
- -

- {t('apiKeys.description')} -

- - {showNewKeyForm && ( -
- setNewKeyName(e.target.value)} - className="mb-2" - /> -
- - -
-
- )} - -
- {apiKeys.length === 0 ? ( -

{t('apiKeys.empty')}

- ) : ( - apiKeys.map((key) => ( -
-
-
{key.key_name}
- {key.api_key} -
- {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} - {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`} -
-
-
- - -
-
- )) - )} -
-
- - {/* GitHub Tokens Section */} -
-
-
- -

{t('apiKeys.github.title')}

-
- -
- -

- {t('apiKeys.github.description')} -

- - {showNewTokenForm && ( -
- setNewTokenName(e.target.value)} - className="mb-2" - /> -
- setNewGithubToken(e.target.value)} - className="mb-2 pr-10" - /> - -
-
- - -
-
- )} - -
- {githubTokens.length === 0 ? ( -

{t('apiKeys.github.empty')}

- ) : ( - githubTokens.map((token) => ( -
-
-
{token.credential_name}
-
- {t('apiKeys.github.added')} {new Date(token.created_at).toLocaleDateString()} -
-
-
- - -
-
- )) - )} -
-
- - {/* Documentation Link */} -
-

{t('apiKeys.documentation.title')}

-

- {t('apiKeys.documentation.description')} -

- - {t('apiKeys.documentation.viewLink')} - -
-
- ); -} - -export default ApiKeysSettings; diff --git a/src/components/CodeEditor.jsx b/src/components/CodeEditor.jsx deleted file mode 100644 index 20eff13..0000000 --- a/src/components/CodeEditor.jsx +++ /dev/null @@ -1,875 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; -import CodeMirror from '@uiw/react-codemirror'; -import { javascript } from '@codemirror/lang-javascript'; -import { python } from '@codemirror/lang-python'; -import { html } from '@codemirror/lang-html'; -import { css } from '@codemirror/lang-css'; -import { json } from '@codemirror/lang-json'; -import { markdown } from '@codemirror/lang-markdown'; -import { oneDark } from '@codemirror/theme-one-dark'; -import { StreamLanguage } from '@codemirror/language'; -import { EditorView, showPanel, ViewPlugin } from '@codemirror/view'; -import { unifiedMergeView, getChunks } from '@codemirror/merge'; -import { showMinimap } from '@replit/codemirror-minimap'; -import { X, Save, Download, Maximize2, Minimize2, Settings as SettingsIcon } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; -import rehypeRaw from 'rehype-raw'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark as prismOneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { api } from '../utils/api'; -import { useTranslation } from 'react-i18next'; -import { Eye, Code2 } from 'lucide-react'; - -// Custom .env file syntax highlighting -const envLanguage = StreamLanguage.define({ - token(stream) { - // Comments - if (stream.match(/^#.*/)) return 'comment'; - // Key (before =) - if (stream.sol() && stream.match(/^[A-Za-z_][A-Za-z0-9_.]*(?==)/)) return 'variableName.definition'; - // Equals sign - if (stream.match(/^=/)) return 'operator'; - // Double-quoted string - if (stream.match(/^"(?:[^"\\]|\\.)*"?/)) return 'string'; - // Single-quoted string - if (stream.match(/^'(?:[^'\\]|\\.)*'?/)) return 'string'; - // Variable interpolation ${...} - if (stream.match(/^\$\{[^}]*\}?/)) return 'variableName.special'; - // Variable reference $VAR - if (stream.match(/^\$[A-Za-z_][A-Za-z0-9_]*/)) return 'variableName.special'; - // Numbers - if (stream.match(/^\d+/)) return 'number'; - // Skip other characters - stream.next(); - return null; - }, -}); - -function MarkdownCodeBlock({ inline, className, children, ...props }) { - const [copied, setCopied] = useState(false); - const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); - const looksMultiline = /[\r\n]/.test(raw); - const shouldInline = inline || !looksMultiline; - - if (shouldInline) { - return ( - - {children} - - ); - } - - const match = /language-(\w+)/.exec(className || ''); - const language = match ? match[1] : 'text'; - - return ( -
- {language && language !== 'text' && ( -
{language}
- )} - - - {raw} - -
- ); -} - -const markdownPreviewComponents = { - code: MarkdownCodeBlock, - blockquote: ({ children }) => ( -
- {children} -
- ), - a: ({ href, children }) => ( - - {children} - - ), - table: ({ children }) => ( -
- {children}
-
- ), - thead: ({ children }) => {children}, - th: ({ children }) => ( - {children} - ), - td: ({ children }) => ( - {children} - ), -}; - -function MarkdownPreview({ content }) { - const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []); - const rehypePlugins = useMemo(() => [rehypeRaw, rehypeKatex], []); - - return ( - - {content} - - ); -} - -function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null, onPopOut = null }) { - const { t } = useTranslation('codeEditor'); - const [content, setContent] = useState(''); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [isDarkMode, setIsDarkMode] = useState(() => { - const savedTheme = localStorage.getItem('codeEditorTheme'); - return savedTheme ? savedTheme === 'dark' : true; - }); - const [saveSuccess, setSaveSuccess] = useState(false); - const [showDiff, setShowDiff] = useState(!!file.diffInfo); - const [wordWrap, setWordWrap] = useState(() => { - return localStorage.getItem('codeEditorWordWrap') === 'true'; - }); - const [minimapEnabled, setMinimapEnabled] = useState(() => { - return localStorage.getItem('codeEditorShowMinimap') !== 'false'; - }); - const [showLineNumbers, setShowLineNumbers] = useState(() => { - return localStorage.getItem('codeEditorLineNumbers') !== 'false'; - }); - const [fontSize, setFontSize] = useState(() => { - return localStorage.getItem('codeEditorFontSize') || '12'; - }); - const [markdownPreview, setMarkdownPreview] = useState(false); - const editorRef = useRef(null); - - // Check if file is markdown - const isMarkdownFile = useMemo(() => { - const ext = file.name.split('.').pop()?.toLowerCase(); - return ext === 'md' || ext === 'markdown'; - }, [file.name]); - - // Create minimap extension with chunk-based gutters - const minimapExtension = useMemo(() => { - if (!file.diffInfo || !showDiff || !minimapEnabled) return []; - - const gutters = {}; - - return [ - showMinimap.compute(['doc'], (state) => { - // Get actual chunks from merge view - const chunksData = getChunks(state); - const chunks = chunksData?.chunks || []; - - // Clear previous gutters - Object.keys(gutters).forEach(key => delete gutters[key]); - - // Mark lines that are part of chunks - chunks.forEach(chunk => { - // Mark the lines in the B side (current document) - const fromLine = state.doc.lineAt(chunk.fromB).number; - const toLine = state.doc.lineAt(Math.min(chunk.toB, state.doc.length)).number; - - for (let lineNum = fromLine; lineNum <= toLine; lineNum++) { - gutters[lineNum] = isDarkMode ? 'rgba(34, 197, 94, 0.8)' : 'rgba(34, 197, 94, 1)'; - } - }); - - return { - create: () => ({ dom: document.createElement('div') }), - displayText: 'blocks', - showOverlay: 'always', - gutters: [gutters] - }; - }) - ]; - }, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]); - - // Create extension to scroll to first chunk on mount - const scrollToFirstChunkExtension = useMemo(() => { - if (!file.diffInfo || !showDiff) return []; - - return [ - ViewPlugin.fromClass(class { - constructor(view) { - // Delay to ensure merge view is fully initialized - setTimeout(() => { - const chunksData = getChunks(view.state); - const chunks = chunksData?.chunks || []; - - if (chunks.length > 0) { - const firstChunk = chunks[0]; - - // Scroll to the first chunk - view.dispatch({ - effects: EditorView.scrollIntoView(firstChunk.fromB, { y: 'center' }) - }); - } - }, 100); - } - - update() {} - destroy() {} - }) - ]; - }, [file.diffInfo, showDiff]); - - // Whether toolbar has any buttons worth showing - const hasToolbarButtons = !!(file.diffInfo || (isSidebar && onPopOut) || (isSidebar && onToggleExpand)); - - // Create editor toolbar panel - only when there are buttons to show - const editorToolbarPanel = useMemo(() => { - if (!hasToolbarButtons) return []; - - const createPanel = (view) => { - const dom = document.createElement('div'); - dom.className = 'cm-editor-toolbar-panel'; - - let currentIndex = 0; - - const updatePanel = () => { - // Check if we have diff info and it's enabled - const hasDiff = file.diffInfo && showDiff; - const chunksData = hasDiff ? getChunks(view.state) : null; - const chunks = chunksData?.chunks || []; - const chunkCount = chunks.length; - - // Build the toolbar HTML - let toolbarHTML = '
'; - - // Left side - diff navigation (if applicable) - toolbarHTML += '
'; - if (hasDiff) { - toolbarHTML += ` - ${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} ${t('toolbar.changes')} - - - `; - } - toolbarHTML += '
'; - - // Right side - action buttons - toolbarHTML += '
'; - - // Show/hide diff button (only if there's diff info) - if (file.diffInfo) { - toolbarHTML += ` - - `; - } - - // Pop out button (only in sidebar mode with onPopOut) - if (isSidebar && onPopOut) { - toolbarHTML += ` - - `; - } - - // Expand button (only in sidebar mode) - if (isSidebar && onToggleExpand) { - toolbarHTML += ` - - `; - } - - toolbarHTML += '
'; - toolbarHTML += '
'; - - dom.innerHTML = toolbarHTML; - - if (hasDiff) { - const prevBtn = dom.querySelector('.cm-diff-nav-prev'); - const nextBtn = dom.querySelector('.cm-diff-nav-next'); - - prevBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; - - const chunk = chunks[currentIndex]; - if (chunk) { - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); - } - updatePanel(); - }); - - nextBtn?.addEventListener('click', () => { - if (chunks.length === 0) return; - currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; - - const chunk = chunks[currentIndex]; - if (chunk) { - view.dispatch({ - effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) - }); - } - updatePanel(); - }); - } - - if (file.diffInfo) { - const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn'); - toggleDiffBtn?.addEventListener('click', () => { - setShowDiff(!showDiff); - }); - } - - if (isSidebar && onPopOut) { - const popoutBtn = dom.querySelector('.cm-popout-btn'); - popoutBtn?.addEventListener('click', () => { - onPopOut(); - }); - } - - if (isSidebar && onToggleExpand) { - const expandBtn = dom.querySelector('.cm-expand-btn'); - expandBtn?.addEventListener('click', () => { - onToggleExpand(); - }); - } - }; - - updatePanel(); - - return { - top: true, - dom, - update: updatePanel - }; - }; - - return [showPanel.of(createPanel)]; - }, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand, onPopOut]); - - // Get language extension based on file extension - const getLanguageExtension = (filename) => { - const lowerName = filename.toLowerCase(); - // Handle dotfiles like .env, .env.local, .env.production, etc. - if (lowerName === '.env' || lowerName.startsWith('.env.')) { - return [envLanguage]; - } - const ext = filename.split('.').pop()?.toLowerCase(); - switch (ext) { - case 'js': - case 'jsx': - case 'ts': - case 'tsx': - return [javascript({ jsx: true, typescript: ext.includes('ts') })]; - case 'py': - return [python()]; - case 'html': - case 'htm': - return [html()]; - case 'css': - case 'scss': - case 'less': - return [css()]; - case 'json': - return [json()]; - case 'md': - case 'markdown': - return [markdown()]; - case 'env': - return [envLanguage]; - default: - return []; - } - }; - - // Load file content - useEffect(() => { - const loadFileContent = async () => { - try { - setLoading(true); - - // If we have diffInfo with both old and new content, we can show the diff directly - // This handles both GitPanel (full content) and ChatInterface (full content from API) - if (file.diffInfo && file.diffInfo.new_string !== undefined && file.diffInfo.old_string !== undefined) { - // Use the new_string as the content to display - // The unifiedMergeView will compare it against old_string - setContent(file.diffInfo.new_string); - setLoading(false); - return; - } - - // Otherwise, load from disk - const response = await api.readFile(file.projectName, file.path); - - if (!response.ok) { - throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - setContent(data.content); - } catch (error) { - console.error('Error loading file:', error); - setContent(`// Error loading file: ${error.message}\n// File: ${file.name}\n// Path: ${file.path}`); - } finally { - setLoading(false); - } - }; - - loadFileContent(); - }, [file, projectPath]); - - const handleSave = async () => { - setSaving(true); - try { - console.log('Saving file:', { - projectName: file.projectName, - path: file.path, - contentLength: content?.length - }); - - const response = await api.saveFile(file.projectName, file.path, content); - - console.log('Save response:', { - status: response.status, - ok: response.ok, - contentType: response.headers.get('content-type') - }); - - if (!response.ok) { - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - const errorData = await response.json(); - throw new Error(errorData.error || `Save failed: ${response.status}`); - } else { - const textError = await response.text(); - console.error('Non-JSON error response:', textError); - throw new Error(`Save failed: ${response.status} ${response.statusText}`); - } - } - - const result = await response.json(); - console.log('Save successful:', result); - - setSaveSuccess(true); - setTimeout(() => setSaveSuccess(false), 2000); - - } catch (error) { - console.error('Error saving file:', error); - alert(`Error saving file: ${error.message}`); - } finally { - setSaving(false); - } - }; - - const handleDownload = () => { - const blob = new Blob([content], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = file.name; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const toggleFullscreen = () => { - setIsFullscreen(!isFullscreen); - }; - - // Save theme preference to localStorage - useEffect(() => { - localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light'); - }, [isDarkMode]); - - // Save word wrap preference to localStorage - useEffect(() => { - localStorage.setItem('codeEditorWordWrap', wordWrap.toString()); - }, [wordWrap]); - - // Listen for settings changes from the Settings modal - useEffect(() => { - const handleStorageChange = () => { - const newTheme = localStorage.getItem('codeEditorTheme'); - if (newTheme) { - setIsDarkMode(newTheme === 'dark'); - } - - const newWordWrap = localStorage.getItem('codeEditorWordWrap'); - if (newWordWrap !== null) { - setWordWrap(newWordWrap === 'true'); - } - - const newShowMinimap = localStorage.getItem('codeEditorShowMinimap'); - if (newShowMinimap !== null) { - setMinimapEnabled(newShowMinimap !== 'false'); - } - - const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers'); - if (newShowLineNumbers !== null) { - setShowLineNumbers(newShowLineNumbers !== 'false'); - } - - const newFontSize = localStorage.getItem('codeEditorFontSize'); - if (newFontSize) { - setFontSize(newFontSize); - } - }; - - // Listen for storage events (changes from other tabs/windows) - window.addEventListener('storage', handleStorageChange); - - // Custom event for same-window updates - window.addEventListener('codeEditorSettingsChanged', handleStorageChange); - - return () => { - window.removeEventListener('storage', handleStorageChange); - window.removeEventListener('codeEditorSettingsChanged', handleStorageChange); - }; - }, []); - - // Handle keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e) => { - if (e.ctrlKey || e.metaKey) { - if (e.key === 's') { - e.preventDefault(); - handleSave(); - } else if (e.key === 'Escape') { - e.preventDefault(); - onClose(); - } - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [content]); - - if (loading) { - return ( - <> - - {isSidebar ? ( -
-
-
- {t('loading', { fileName: file.name })} -
-
- ) : ( -
-
-
-
- {t('loading', { fileName: file.name })} -
-
-
- )} - - ); - } - - return ( - <> - -
-
- {/* Header */} -
-
-
-
-

{file.name}

- {file.diffInfo && ( - - {t('header.showingChanges')} - - )} -
-

{file.path}

-
-
- -
- {isMarkdownFile && ( - - )} - - - - - - - - {!isSidebar && ( - - )} - - -
-
- - {/* Editor / Markdown Preview */} -
- {markdownPreview && isMarkdownFile ? ( -
-
- -
-
- ) : ( - - )} -
- - {/* Footer */} -
-
- {t('footer.lines')} {content.split('\n').length} - {t('footer.characters')} {content.length} -
- -
- {t('footer.shortcuts')} -
-
-
-
- - ); -} - -export default CodeEditor; diff --git a/src/components/CommandMenu.jsx b/src/components/CommandMenu.jsx deleted file mode 100644 index d8f344d..0000000 --- a/src/components/CommandMenu.jsx +++ /dev/null @@ -1,367 +0,0 @@ -import React, { useEffect, useRef } from 'react'; - -/** - * CommandMenu - Autocomplete dropdown for slash commands - * - * @param {Array} commands - Array of command objects to display - * @param {number} selectedIndex - Currently selected command index (index in `commands`) - * @param {Function} onSelect - Callback when a command is selected - * @param {Function} onClose - Callback when menu should close - * @param {Object} position - Position object { top, left } for absolute positioning - * @param {boolean} isOpen - Whether the menu is open - * @param {Array} frequentCommands - Array of frequently used command objects - */ -const CommandMenu = ({ - commands = [], - selectedIndex = -1, - onSelect, - onClose, - position = { top: 0, left: 0 }, - isOpen = false, - frequentCommands = [], -}) => { - const menuRef = useRef(null); - const selectedItemRef = useRef(null); - - // Calculate responsive menu positioning. - // Mobile: dock above chat input. Desktop: clamp to viewport. - const getMenuPosition = () => { - const isMobile = window.innerWidth < 640; - const viewportHeight = window.innerHeight; - - if (isMobile) { - // On mobile, calculate bottom position dynamically to appear above the input. - // Use the bottom value calculated as: window.innerHeight - textarea.top + spacing. - const inputBottom = position.bottom || 90; - - return { - position: 'fixed', - bottom: `${inputBottom}px`, // Position above the input with spacing already included. - left: '16px', - right: '16px', - width: 'auto', - maxWidth: 'calc(100vw - 32px)', - maxHeight: 'min(50vh, 300px)', // Limit to smaller of 50vh or 300px. - }; - } - - // On desktop, use provided position but ensure it stays on screen. - return { - position: 'fixed', - top: `${Math.max(16, Math.min(position.top, viewportHeight - 316))}px`, - left: `${position.left}px`, - width: 'min(400px, calc(100vw - 32px))', - maxWidth: 'calc(100vw - 32px)', - maxHeight: '300px', - }; - }; - - const menuPosition = getMenuPosition(); - - // Close menu when clicking outside. - useEffect(() => { - const handleClickOutside = (event) => { - if (menuRef.current && !menuRef.current.contains(event.target) && isOpen) { - onClose(); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - } - - return undefined; - }, [isOpen, onClose]); - - // Keep selected keyboard item visible while navigating. - useEffect(() => { - if (selectedItemRef.current && menuRef.current) { - const menuRect = menuRef.current.getBoundingClientRect(); - const itemRect = selectedItemRef.current.getBoundingClientRect(); - - if (itemRect.bottom > menuRect.bottom) { - selectedItemRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } else if (itemRect.top < menuRect.top) { - selectedItemRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - } - }, [selectedIndex]); - - if (!isOpen) { - return null; - } - - // Show a message if no commands are available. - if (commands.length === 0) { - return ( -
- No commands available -
- ); - } - - // Add frequent commands as a special group if provided. - const hasFrequentCommands = frequentCommands.length > 0; - - const getCommandKey = (command) => - `${command.name}::${command.namespace || command.type || 'other'}::${command.path || ''}`; - const frequentCommandKeys = new Set(frequentCommands.map(getCommandKey)); - - // Group commands by namespace for section rendering. - // When frequent commands are shown, avoid duplicate rows in other sections. - const groupedCommands = commands.reduce((groups, command) => { - if (hasFrequentCommands && frequentCommandKeys.has(getCommandKey(command))) { - return groups; - } - - const namespace = command.namespace || command.type || 'other'; - if (!groups[namespace]) { - groups[namespace] = []; - } - groups[namespace].push(command); - return groups; - }, {}); - - // Add frequent commands as a separate group. - if (hasFrequentCommands) { - groupedCommands.frequent = frequentCommands; - } - - // Order: frequent, builtin, project, user, other. - const namespaceOrder = hasFrequentCommands - ? ['frequent', 'builtin', 'project', 'user', 'other'] - : ['builtin', 'project', 'user', 'other']; - const orderedNamespaces = namespaceOrder.filter((ns) => groupedCommands[ns]); - - const namespaceLabels = { - frequent: '\u2B50 Frequently Used', - builtin: 'Built-in Commands', - project: 'Project Commands', - user: 'User Commands', - other: 'Other Commands', - }; - - // Keep all selection indices aligned to `commands` (filteredCommands from the hook). - // This prevents mismatches between mouse selection (rendered list) and keyboard selection. - const commandIndexByKey = new Map(); - commands.forEach((command, index) => { - const key = getCommandKey(command); - if (!commandIndexByKey.has(key)) { - commandIndexByKey.set(key, index); - } - }); - - return ( -
- {orderedNamespaces.map((namespace) => ( -
- {orderedNamespaces.length > 1 && ( -
- {namespaceLabels[namespace] || namespace} -
- )} - - {groupedCommands[namespace].map((command) => { - const commandKey = getCommandKey(command); - const commandIndex = commandIndexByKey.get(commandKey) ?? -1; - const isSelected = commandIndex === selectedIndex; - - return ( -
{ - if (onSelect && commandIndex >= 0) { - onSelect(command, commandIndex, true); - } - }} - onClick={() => { - if (onSelect) { - onSelect(command, commandIndex, false); - } - }} - style={{ - display: 'flex', - alignItems: 'flex-start', - padding: '10px 12px', - borderRadius: '6px', - cursor: 'pointer', - backgroundColor: isSelected ? '#eff6ff' : 'transparent', - transition: 'background-color 100ms ease-in-out', - marginBottom: '2px', - }} - // Prevent textarea blur when clicking a menu item. - onMouseDown={(e) => e.preventDefault()} - > -
-
- {/* Command icon based on namespace */} - - {namespace === 'builtin' && '\u26A1'} - {namespace === 'project' && '\uD83D\uDCC1'} - {namespace === 'user' && '\uD83D\uDC64'} - {namespace === 'other' && '\uD83D\uDCDD'} - {namespace === 'frequent' && '\u2B50'} - - - {/* Command name */} - - {command.name} - - - {/* Command metadata badge */} - {command.metadata?.type && ( - - {command.metadata.type} - - )} -
- - {/* Command description */} - {command.description && ( -
- {command.description} -
- )} -
- - {/* Selection indicator */} - {isSelected && ( - - {'\u21B5'} - - )} -
- ); - })} -
- ))} - - {/* Default light mode styles */} - -
- ); -}; - -export default CommandMenu; diff --git a/src/components/CredentialsSettings.jsx b/src/components/CredentialsSettings.jsx deleted file mode 100644 index cc7d424..0000000 --- a/src/components/CredentialsSettings.jsx +++ /dev/null @@ -1,421 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Key, Plus, Trash2, Eye, EyeOff, Copy, Check, Github, ExternalLink } from 'lucide-react'; -import { useVersionCheck } from '../hooks/useVersionCheck'; -import { version } from '../../package.json'; -import { authenticatedFetch } from '../utils/api'; -import { useTranslation } from 'react-i18next'; - -function CredentialsSettings() { - const { t } = useTranslation('settings'); - const [apiKeys, setApiKeys] = useState([]); - const [githubCredentials, setGithubCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [showNewKeyForm, setShowNewKeyForm] = useState(false); - const [showNewGithubForm, setShowNewGithubForm] = useState(false); - const [newKeyName, setNewKeyName] = useState(''); - const [newGithubName, setNewGithubName] = useState(''); - const [newGithubToken, setNewGithubToken] = useState(''); - const [newGithubDescription, setNewGithubDescription] = useState(''); - const [showToken, setShowToken] = useState({}); - const [copiedKey, setCopiedKey] = useState(null); - const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); - - // Version check hook - const { updateAvailable, latestVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); - - useEffect(() => { - fetchData(); - }, []); - - const fetchData = async () => { - try { - setLoading(true); - - // Fetch API keys - const apiKeysRes = await authenticatedFetch('/api/settings/api-keys'); - const apiKeysData = await apiKeysRes.json(); - setApiKeys(apiKeysData.apiKeys || []); - - // Fetch GitHub credentials only - const credentialsRes = await authenticatedFetch('/api/settings/credentials?type=github_token'); - const credentialsData = await credentialsRes.json(); - setGithubCredentials(credentialsData.credentials || []); - } catch (error) { - console.error('Error fetching settings:', error); - } finally { - setLoading(false); - } - }; - - const createApiKey = async () => { - if (!newKeyName.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/api-keys', { - method: 'POST', - body: JSON.stringify({ keyName: newKeyName }) - }); - - const data = await res.json(); - if (data.success) { - setNewlyCreatedKey(data.apiKey); - setNewKeyName(''); - setShowNewKeyForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating API key:', error); - } - }; - - const deleteApiKey = async (keyId) => { - if (!confirm(t('apiKeys.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting API key:', error); - } - }; - - const toggleApiKey = async (keyId, isActive) => { - try { - await authenticatedFetch(`/api/settings/api-keys/${keyId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling API key:', error); - } - }; - - const createGithubCredential = async () => { - if (!newGithubName.trim() || !newGithubToken.trim()) return; - - try { - const res = await authenticatedFetch('/api/settings/credentials', { - method: 'POST', - body: JSON.stringify({ - credentialName: newGithubName, - credentialType: 'github_token', - credentialValue: newGithubToken, - description: newGithubDescription - }) - }); - - const data = await res.json(); - if (data.success) { - setNewGithubName(''); - setNewGithubToken(''); - setNewGithubDescription(''); - setShowNewGithubForm(false); - fetchData(); - } - } catch (error) { - console.error('Error creating GitHub credential:', error); - } - }; - - const deleteGithubCredential = async (credentialId) => { - if (!confirm(t('apiKeys.github.confirmDelete'))) return; - - try { - await authenticatedFetch(`/api/settings/credentials/${credentialId}`, { - method: 'DELETE' - }); - fetchData(); - } catch (error) { - console.error('Error deleting GitHub credential:', error); - } - }; - - const toggleGithubCredential = async (credentialId, isActive) => { - try { - await authenticatedFetch(`/api/settings/credentials/${credentialId}/toggle`, { - method: 'PATCH', - body: JSON.stringify({ isActive: !isActive }) - }); - fetchData(); - } catch (error) { - console.error('Error toggling GitHub credential:', error); - } - }; - - const copyToClipboard = (text, id) => { - navigator.clipboard.writeText(text); - setCopiedKey(id); - setTimeout(() => setCopiedKey(null), 2000); - }; - - if (loading) { - return
{t('apiKeys.loading')}
; - } - - return ( -
- {/* New API Key Alert */} - {newlyCreatedKey && ( -
-

{t('apiKeys.newKey.alertTitle')}

-

- {t('apiKeys.newKey.alertMessage')} -

-
- - {newlyCreatedKey.apiKey} - - -
- -
- )} - - {/* API Keys Section */} -
-
-
- -

{t('apiKeys.title')}

-
- -
- -
-

- {t('apiKeys.description')} -

- - {t('apiKeys.apiDocsLink')} - - -
- - {showNewKeyForm && ( -
- setNewKeyName(e.target.value)} - className="mb-2" - /> -
- - -
-
- )} - -
- {apiKeys.length === 0 ? ( -

{t('apiKeys.empty')}

- ) : ( - apiKeys.map((key) => ( -
-
-
{key.key_name}
- {key.api_key} -
- {t('apiKeys.list.created')} {new Date(key.created_at).toLocaleDateString()} - {key.last_used && ` • ${t('apiKeys.list.lastUsed')} ${new Date(key.last_used).toLocaleDateString()}`} -
-
-
- - -
-
- )) - )} -
-
- - {/* GitHub Credentials Section */} -
-
-
- -

{t('apiKeys.github.title')}

-
- -
- -

- {t('apiKeys.github.descriptionAlt')} -

- - {showNewGithubForm && ( -
- setNewGithubName(e.target.value)} - /> - -
- setNewGithubToken(e.target.value)} - className="pr-10" - /> - -
- - setNewGithubDescription(e.target.value)} - /> - -
- - -
- - - {t('apiKeys.github.form.howToCreate')} - -
- )} - -
- {githubCredentials.length === 0 ? ( -

{t('apiKeys.github.empty')}

- ) : ( - githubCredentials.map((credential) => ( -
-
-
{credential.credential_name}
- {credential.description && ( -
{credential.description}
- )} -
- {t('apiKeys.github.added')} {new Date(credential.created_at).toLocaleDateString()} -
-
-
- - -
-
- )) - )} -
-
- - {/* Version Information */} -
-
- - v{version} - - {updateAvailable && latestVersion && ( - - {t('apiKeys.version.updateAvailable', { version: latestVersion })} - - - )} -
-
-
- ); -} - -export default CredentialsSettings; diff --git a/src/components/DarkModeToggle.jsx b/src/components/DarkModeToggle.jsx deleted file mode 100644 index b2b3ca0..0000000 --- a/src/components/DarkModeToggle.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { useTheme } from '../contexts/ThemeContext'; - -function DarkModeToggle() { - const { isDarkMode, toggleDarkMode } = useTheme(); - - return ( - - ); -} - -export default DarkModeToggle; \ No newline at end of file diff --git a/src/components/DarkModeToggle.tsx b/src/components/DarkModeToggle.tsx new file mode 100644 index 0000000..7f12b9b --- /dev/null +++ b/src/components/DarkModeToggle.tsx @@ -0,0 +1,48 @@ +import { Moon, Sun } from 'lucide-react'; +import { useTheme } from '../contexts/ThemeContext'; + +type DarkModeToggleProps = { + checked?: boolean; + onToggle?: (nextValue: boolean) => void; + ariaLabel?: string; +}; + +function DarkModeToggle({ checked, onToggle, ariaLabel = 'Toggle dark mode' }: DarkModeToggleProps) { + const { isDarkMode, toggleDarkMode } = useTheme(); + const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function'; + const isEnabled = isControlled ? checked : isDarkMode; + + const handleToggle = () => { + if (isControlled) { + onToggle(!isEnabled); + return; + } + + toggleDarkMode(); + }; + + return ( + + ); +} + +export default DarkModeToggle; diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx index 5ebe7ca..b20c028 100644 --- a/src/components/ErrorBoundary.jsx +++ b/src/components/ErrorBoundary.jsx @@ -1,73 +1,77 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; +import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false, error: null, errorInfo: null }; - } - - static getDerivedStateFromError(error) { - // Update state so the next render will show the fallback UI - return { hasError: true }; - } - - componentDidCatch(error, errorInfo) { - // Log the error details - console.error('ErrorBoundary caught an error:', error, errorInfo); - - // You can also log the error to an error reporting service here - this.setState({ - error: error, - errorInfo: errorInfo - }); - } - - render() { - if (this.state.hasError) { - // Fallback UI - return ( -
-
-
-
- - - -
-

- Something went wrong -

-
-
-

An error occurred while loading the chat interface.

- {this.props.showDetails && this.state.error && ( -
- Error Details -
-                    {this.state.error.toString()}
-                    {this.state.errorInfo && this.state.errorInfo.componentStack}
-                  
-
- )} -
-
- -
+function ErrorFallback({ error, resetErrorBoundary, showDetails, componentStack }) { + return ( +
+
+
+
+ + +
+

+ Something went wrong +

- ); - } - - return this.props.children; - } +
+

An error occurred while loading the chat interface.

+ {showDetails && error && ( +
+ Error Details +
+                {error.toString()}
+                {componentStack}
+              
+
+ )} +
+
+ +
+
+
+ ); } -export default ErrorBoundary; \ No newline at end of file +function ErrorBoundary({ children, showDetails = false, onRetry = undefined, resetKeys = undefined }) { + const [componentStack, setComponentStack] = useState(null); + + const handleError = useCallback((error, errorInfo) => { + console.error('ErrorBoundary caught an error:', error, errorInfo); + setComponentStack(errorInfo?.componentStack || null); + }, []); + + const handleReset = useCallback(() => { + setComponentStack(null); + onRetry?.(); + }, [onRetry]); + + const renderFallback = useCallback(({ error, resetErrorBoundary }) => ( + + ), [showDetails, componentStack]); + + return ( + + {children} + + ); +} + +export default ErrorBoundary; diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx deleted file mode 100644 index cf3d1e1..0000000 --- a/src/components/FileTree.jsx +++ /dev/null @@ -1,729 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ScrollArea } from './ui/scroll-area'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { - Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X, - ChevronRight, - FileJson, FileType, FileSpreadsheet, FileArchive, - Hash, Braces, Terminal, Database, Globe, Palette, Music2, Video, Archive, - Lock, Shield, Settings, Image, BookOpen, Cpu, Box, Gem, Coffee, - Flame, Hexagon, FileCode2, Code2, Cog, FileWarning, Binary, SquareFunction, - Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks -} from 'lucide-react'; -import { cn } from '../lib/utils'; -import ImageViewer from './ImageViewer'; -import { api } from '../utils/api'; - -// ─── File Icon Registry ────────────────────────────────────────────── -// Maps file extensions (and special filenames) to { icon, colorClass } pairs. -// Uses lucide-react icons mapped semantically to file types. - -const ICON_SIZE = 'w-4 h-4 flex-shrink-0'; - -const FILE_ICON_MAP = { - // ── JavaScript / TypeScript ── - js: { icon: FileCode, color: 'text-yellow-500' }, - jsx: { icon: FileCode, color: 'text-yellow-500' }, - mjs: { icon: FileCode, color: 'text-yellow-500' }, - cjs: { icon: FileCode, color: 'text-yellow-500' }, - ts: { icon: FileCode2, color: 'text-blue-500' }, - tsx: { icon: FileCode2, color: 'text-blue-500' }, - mts: { icon: FileCode2, color: 'text-blue-500' }, - - // ── Python ── - py: { icon: Code2, color: 'text-emerald-500' }, - pyw: { icon: Code2, color: 'text-emerald-500' }, - pyi: { icon: Code2, color: 'text-emerald-400' }, - ipynb:{ icon: NotebookPen, color: 'text-orange-500' }, - - // ── Rust ── - rs: { icon: Cog, color: 'text-orange-600' }, - toml: { icon: Settings, color: 'text-gray-500' }, - - // ── Go ── - go: { icon: Hexagon, color: 'text-cyan-500' }, - - // ── Ruby ── - rb: { icon: Gem, color: 'text-red-500' }, - erb: { icon: Gem, color: 'text-red-400' }, - - // ── PHP ── - php: { icon: Blocks, color: 'text-violet-500' }, - - // ── Java / Kotlin ── - java: { icon: Coffee, color: 'text-red-600' }, - jar: { icon: Coffee, color: 'text-red-500' }, - kt: { icon: Hexagon, color: 'text-violet-500' }, - kts: { icon: Hexagon, color: 'text-violet-400' }, - - // ── C / C++ ── - c: { icon: Cpu, color: 'text-blue-600' }, - h: { icon: Cpu, color: 'text-blue-400' }, - cpp: { icon: Cpu, color: 'text-blue-700' }, - hpp: { icon: Cpu, color: 'text-blue-500' }, - cc: { icon: Cpu, color: 'text-blue-700' }, - - // ── C# ── - cs: { icon: Hexagon, color: 'text-purple-600' }, - - // ── Swift ── - swift:{ icon: Flame, color: 'text-orange-500' }, - - // ── Lua ── - lua: { icon: SquareFunction, color: 'text-blue-500' }, - - // ── R ── - r: { icon: FlaskConical, color: 'text-blue-600' }, - - // ── Web ── - html: { icon: Globe, color: 'text-orange-600' }, - htm: { icon: Globe, color: 'text-orange-600' }, - css: { icon: Hash, color: 'text-blue-500' }, - scss: { icon: Hash, color: 'text-pink-500' }, - sass: { icon: Hash, color: 'text-pink-400' }, - less: { icon: Hash, color: 'text-indigo-500' }, - vue: { icon: FileCode2, color: 'text-emerald-500' }, - svelte:{ icon: FileCode2, color: 'text-orange-500' }, - - // ── Data / Config ── - json: { icon: Braces, color: 'text-yellow-600' }, - jsonc:{ icon: Braces, color: 'text-yellow-500' }, - json5:{ icon: Braces, color: 'text-yellow-500' }, - yaml: { icon: Settings, color: 'text-purple-400' }, - yml: { icon: Settings, color: 'text-purple-400' }, - xml: { icon: FileCode, color: 'text-orange-500' }, - csv: { icon: FileSpreadsheet, color: 'text-green-600' }, - tsv: { icon: FileSpreadsheet, color: 'text-green-500' }, - sql: { icon: Database, color: 'text-blue-500' }, - graphql:{ icon: Workflow, color: 'text-pink-500' }, - gql: { icon: Workflow, color: 'text-pink-500' }, - proto:{ icon: Box, color: 'text-green-500' }, - env: { icon: Shield, color: 'text-yellow-600' }, - - // ── Documents ── - md: { icon: BookOpen, color: 'text-blue-500' }, - mdx: { icon: BookOpen, color: 'text-blue-400' }, - txt: { icon: FileText, color: 'text-gray-500' }, - doc: { icon: FileText, color: 'text-blue-600' }, - docx: { icon: FileText, color: 'text-blue-600' }, - pdf: { icon: FileCheck, color: 'text-red-600' }, - rtf: { icon: FileText, color: 'text-gray-500' }, - tex: { icon: Scroll, color: 'text-teal-600' }, - rst: { icon: FileText, color: 'text-gray-400' }, - - // ── Shell / Scripts ── - sh: { icon: Terminal, color: 'text-green-500' }, - bash: { icon: Terminal, color: 'text-green-500' }, - zsh: { icon: Terminal, color: 'text-green-400' }, - fish: { icon: Terminal, color: 'text-green-400' }, - ps1: { icon: Terminal, color: 'text-blue-400' }, - bat: { icon: Terminal, color: 'text-gray-500' }, - cmd: { icon: Terminal, color: 'text-gray-500' }, - - // ── Images ── - png: { icon: Image, color: 'text-purple-500' }, - jpg: { icon: Image, color: 'text-purple-500' }, - jpeg: { icon: Image, color: 'text-purple-500' }, - gif: { icon: Image, color: 'text-purple-400' }, - webp: { icon: Image, color: 'text-purple-400' }, - ico: { icon: Image, color: 'text-purple-400' }, - bmp: { icon: Image, color: 'text-purple-400' }, - tiff: { icon: Image, color: 'text-purple-400' }, - svg: { icon: Palette, color: 'text-amber-500' }, - - // ── Audio ── - mp3: { icon: Music2, color: 'text-pink-500' }, - wav: { icon: Music2, color: 'text-pink-500' }, - ogg: { icon: Music2, color: 'text-pink-400' }, - flac: { icon: Music2, color: 'text-pink-400' }, - aac: { icon: Music2, color: 'text-pink-400' }, - m4a: { icon: Music2, color: 'text-pink-400' }, - - // ── Video ── - mp4: { icon: Video, color: 'text-rose-500' }, - mov: { icon: Video, color: 'text-rose-500' }, - avi: { icon: Video, color: 'text-rose-500' }, - webm: { icon: Video, color: 'text-rose-400' }, - mkv: { icon: Video, color: 'text-rose-400' }, - - // ── Fonts ── - ttf: { icon: FileType, color: 'text-red-500' }, - otf: { icon: FileType, color: 'text-red-500' }, - woff: { icon: FileType, color: 'text-red-400' }, - woff2:{ icon: FileType, color: 'text-red-400' }, - eot: { icon: FileType, color: 'text-red-400' }, - - // ── Archives ── - zip: { icon: Archive, color: 'text-amber-600' }, - tar: { icon: Archive, color: 'text-amber-600' }, - gz: { icon: Archive, color: 'text-amber-600' }, - bz2: { icon: Archive, color: 'text-amber-600' }, - rar: { icon: Archive, color: 'text-amber-500' }, - '7z': { icon: Archive, color: 'text-amber-500' }, - - // ── Lock files ── - lock: { icon: Lock, color: 'text-gray-500' }, - - // ── Binary / Executable ── - exe: { icon: Binary, color: 'text-gray-500' }, - bin: { icon: Binary, color: 'text-gray-500' }, - dll: { icon: Binary, color: 'text-gray-400' }, - so: { icon: Binary, color: 'text-gray-400' }, - dylib:{ icon: Binary, color: 'text-gray-400' }, - wasm: { icon: Binary, color: 'text-purple-500' }, - - // ── Misc config ── - ini: { icon: Settings, color: 'text-gray-500' }, - cfg: { icon: Settings, color: 'text-gray-500' }, - conf: { icon: Settings, color: 'text-gray-500' }, - log: { icon: Scroll, color: 'text-gray-400' }, - map: { icon: File, color: 'text-gray-400' }, -}; - -// Special full-filename matches (highest priority) -const FILENAME_ICON_MAP = { - 'Dockerfile': { icon: Box, color: 'text-blue-500' }, - 'docker-compose.yml': { icon: Box, color: 'text-blue-500' }, - 'docker-compose.yaml': { icon: Box, color: 'text-blue-500' }, - '.dockerignore': { icon: Box, color: 'text-gray-500' }, - '.gitignore': { icon: Settings, color: 'text-gray-500' }, - '.gitmodules': { icon: Settings, color: 'text-gray-500' }, - '.gitattributes': { icon: Settings, color: 'text-gray-500' }, - '.editorconfig': { icon: Settings, color: 'text-gray-500' }, - '.prettierrc': { icon: Settings, color: 'text-pink-400' }, - '.prettierignore': { icon: Settings, color: 'text-gray-500' }, - '.eslintrc': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.js': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.json': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.cjs': { icon: Settings, color: 'text-violet-500' }, - 'eslint.config.js': { icon: Settings, color: 'text-violet-500' }, - 'eslint.config.mjs':{ icon: Settings, color: 'text-violet-500' }, - '.env': { icon: Shield, color: 'text-yellow-600' }, - '.env.local': { icon: Shield, color: 'text-yellow-600' }, - '.env.development': { icon: Shield, color: 'text-yellow-500' }, - '.env.production': { icon: Shield, color: 'text-yellow-600' }, - '.env.example': { icon: Shield, color: 'text-yellow-400' }, - 'package.json': { icon: Braces, color: 'text-green-500' }, - 'package-lock.json':{ icon: Lock, color: 'text-gray-500' }, - 'yarn.lock': { icon: Lock, color: 'text-blue-400' }, - 'pnpm-lock.yaml': { icon: Lock, color: 'text-orange-400' }, - 'bun.lockb': { icon: Lock, color: 'text-gray-400' }, - 'Cargo.toml': { icon: Cog, color: 'text-orange-600' }, - 'Cargo.lock': { icon: Lock, color: 'text-orange-400' }, - 'Gemfile': { icon: Gem, color: 'text-red-500' }, - 'Gemfile.lock': { icon: Lock, color: 'text-red-400' }, - 'Makefile': { icon: Terminal, color: 'text-gray-500' }, - 'CMakeLists.txt': { icon: Cog, color: 'text-blue-500' }, - 'tsconfig.json': { icon: Braces, color: 'text-blue-500' }, - 'jsconfig.json': { icon: Braces, color: 'text-yellow-500' }, - 'vite.config.ts': { icon: Flame, color: 'text-purple-500' }, - 'vite.config.js': { icon: Flame, color: 'text-purple-500' }, - 'webpack.config.js':{ icon: Cog, color: 'text-blue-500' }, - 'tailwind.config.js':{ icon: Hash, color: 'text-cyan-500' }, - 'tailwind.config.ts':{ icon: Hash, color: 'text-cyan-500' }, - 'postcss.config.js':{ icon: Cog, color: 'text-red-400' }, - 'babel.config.js': { icon: Settings, color: 'text-yellow-500' }, - '.babelrc': { icon: Settings, color: 'text-yellow-500' }, - 'README.md': { icon: BookOpen, color: 'text-blue-500' }, - 'LICENSE': { icon: FileCheck, color: 'text-gray-500' }, - 'LICENSE.md': { icon: FileCheck, color: 'text-gray-500' }, - 'CHANGELOG.md': { icon: Scroll, color: 'text-blue-400' }, - 'requirements.txt': { icon: FileText, color: 'text-emerald-400' }, - 'go.mod': { icon: Hexagon, color: 'text-cyan-500' }, - 'go.sum': { icon: Lock, color: 'text-cyan-400' }, -}; - -function getFileIconData(filename) { - // 1. Exact filename match - if (FILENAME_ICON_MAP[filename]) { - return FILENAME_ICON_MAP[filename]; - } - - // 2. Check for .env prefix pattern - if (filename.startsWith('.env')) { - return { icon: Shield, color: 'text-yellow-600' }; - } - - // 3. Extension-based lookup - const ext = filename.split('.').pop()?.toLowerCase(); - if (ext && FILE_ICON_MAP[ext]) { - return FILE_ICON_MAP[ext]; - } - - // 4. Fallback - return { icon: File, color: 'text-muted-foreground' }; -} - - -// ─── Component ─────────────────────────────────────────────────────── - -function FileTree({ selectedProject, onFileOpen }) { - const { t } = useTranslation(); - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(false); - const [expandedDirs, setExpandedDirs] = useState(new Set()); - const [selectedImage, setSelectedImage] = useState(null); - const [viewMode, setViewMode] = useState('detailed'); - const [searchQuery, setSearchQuery] = useState(''); - const [filteredFiles, setFilteredFiles] = useState([]); - - useEffect(() => { - if (selectedProject) { - fetchFiles(); - } - }, [selectedProject]); - - useEffect(() => { - const savedViewMode = localStorage.getItem('file-tree-view-mode'); - if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) { - setViewMode(savedViewMode); - } - }, []); - - useEffect(() => { - if (!searchQuery.trim()) { - setFilteredFiles(files); - } else { - const filtered = filterFiles(files, searchQuery.toLowerCase()); - setFilteredFiles(filtered); - - const expandMatches = (items) => { - items.forEach(item => { - if (item.type === 'directory' && item.children && item.children.length > 0) { - setExpandedDirs(prev => new Set(prev.add(item.path))); - expandMatches(item.children); - } - }); - }; - expandMatches(filtered); - } - }, [files, searchQuery]); - - const filterFiles = (items, query) => { - return items.reduce((filtered, item) => { - const matchesName = item.name.toLowerCase().includes(query); - let filteredChildren = []; - - if (item.type === 'directory' && item.children) { - filteredChildren = filterFiles(item.children, query); - } - - if (matchesName || filteredChildren.length > 0) { - filtered.push({ - ...item, - children: filteredChildren - }); - } - - return filtered; - }, []); - }; - - const fetchFiles = async () => { - setLoading(true); - try { - const response = await api.getFiles(selectedProject.name); - - if (!response.ok) { - const errorText = await response.text(); - console.error('❌ File fetch failed:', response.status, errorText); - setFiles([]); - return; - } - - const data = await response.json(); - setFiles(data); - } catch (error) { - console.error('❌ Error fetching files:', error); - setFiles([]); - } finally { - setLoading(false); - } - }; - - const toggleDirectory = (path) => { - const newExpanded = new Set(expandedDirs); - if (newExpanded.has(path)) { - newExpanded.delete(path); - } else { - newExpanded.add(path); - } - setExpandedDirs(newExpanded); - }; - - const changeViewMode = (mode) => { - setViewMode(mode); - localStorage.setItem('file-tree-view-mode', mode); - }; - - const formatFileSize = (bytes) => { - if (!bytes || bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - }; - - const formatRelativeTime = (date) => { - if (!date) return '-'; - const now = new Date(); - const past = new Date(date); - const diffInSeconds = Math.floor((now - past) / 1000); - - if (diffInSeconds < 60) return t('fileTree.justNow'); - if (diffInSeconds < 3600) return t('fileTree.minAgo', { count: Math.floor(diffInSeconds / 60) }); - if (diffInSeconds < 86400) return t('fileTree.hoursAgo', { count: Math.floor(diffInSeconds / 3600) }); - if (diffInSeconds < 2592000) return t('fileTree.daysAgo', { count: Math.floor(diffInSeconds / 86400) }); - return past.toLocaleDateString(); - }; - - const isImageFile = (filename) => { - const ext = filename.split('.').pop()?.toLowerCase(); - const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp']; - return imageExtensions.includes(ext); - }; - - const getFileIcon = (filename) => { - const { icon: Icon, color } = getFileIconData(filename); - return ; - }; - - // ── Click handler shared across all view modes ── - const handleItemClick = (item) => { - if (item.type === 'directory') { - toggleDirectory(item.path); - } else if (isImageFile(item.name)) { - setSelectedImage({ - name: item.name, - path: item.path, - projectPath: selectedProject.path, - projectName: selectedProject.name - }); - } else if (onFileOpen) { - onFileOpen(item.path); - } - }; - - // ── Indent guide + folder/file icon rendering ── - const renderIndentGuides = (level) => { - if (level === 0) return null; - return ( -