LlamaFinetuneGGUF commited on
Commit
5dd9c92
·
unverified ·
2 Parent(s): bcb6628 0a9f04f

Merge branch 'coleam00:main' into ui/model-dropdown

Browse files
.github/workflows/commit.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Update Commit Hash File
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ update-commit:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Checkout the code
17
+ uses: actions/checkout@v3
18
+
19
+ - name: Get the latest commit hash
20
+ run: echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
21
+
22
+ - name: Update commit file
23
+ run: |
24
+ echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json
25
+
26
+ - name: Commit and push the update
27
+ run: |
28
+ git config --global user.name "github-actions[bot]"
29
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
30
+ git add app/commit.json
31
+ git commit -m "chore: update commit hash to $COMMIT_HASH"
32
+ git push
.husky/pre-commit CHANGED
@@ -5,15 +5,21 @@ echo "🔍 Running pre-commit hook to check the code looks good... 🔍"
5
  export NVM_DIR="$HOME/.nvm"
6
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i
7
 
 
 
 
8
  if ! pnpm typecheck; then
9
- echo "❌ Type checking failed! Please review TypeScript types."
10
- echo "Once you're done, don't forget to add your changes to the commit! 🚀"
11
- exit 1
 
12
  fi
13
 
 
14
  if ! pnpm lint; then
15
  echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
16
  echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
 
17
  exit 1
18
  fi
19
 
 
5
  export NVM_DIR="$HOME/.nvm"
6
  [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i
7
 
8
+ echo "Running typecheck..."
9
+ which pnpm
10
+
11
  if ! pnpm typecheck; then
12
+ echo "❌ Type checking failed! Please review TypeScript types."
13
+ echo "Once you're done, don't forget to add your changes to the commit! 🚀"
14
+ echo "Typecheck exit code: $?"
15
+ exit 1
16
  fi
17
 
18
+ echo "Running lint..."
19
  if ! pnpm lint; then
20
  echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones."
21
  echo "Once you're done, don't forget to add your beautification to the commit! 🤩"
22
+ echo "lint exit code: $?"
23
  exit 1
24
  fi
25
 
app/commit.json ADDED
@@ -0,0 +1 @@
 
 
1
+ { "commit": "31e7b48e057d12008a9790810433179bf88b9a32" }
app/components/chat/Artifact.tsx CHANGED
@@ -52,7 +52,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
52
  if (actions.length !== 0 && artifact.type === 'bundled') {
53
  const finished = !actions.find((action) => action.status !== 'complete');
54
 
55
- if (finished != allActionFinished) {
56
  setAllActionFinished(finished);
57
  }
58
  }
 
52
  if (actions.length !== 0 && artifact.type === 'bundled') {
53
  const finished = !actions.find((action) => action.status !== 'complete');
54
 
55
+ if (allActionFinished !== finished) {
56
  setAllActionFinished(finished);
57
  }
58
  }
app/components/chat/BaseChat.tsx CHANGED
@@ -21,6 +21,7 @@ import type { ProviderInfo } from '~/utils/types';
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
 
24
 
25
  import FilePreview from './FilePreview';
26
  import { ModelSelector } from '~/components/chat/ModelSelector';
@@ -87,13 +88,65 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
87
  ref,
88
  ) => {
89
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
90
- const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  const [modelList, setModelList] = useState(MODEL_LIST);
92
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
93
  const [isListening, setIsListening] = useState(false);
94
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
95
  const [transcript, setTranscript] = useState('');
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  console.log(transcript);
98
  useEffect(() => {
99
  // Load API keys from cookies on component mount
@@ -183,23 +236,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
183
  }
184
  };
185
 
186
- const updateApiKey = (provider: string, key: string) => {
187
- try {
188
- const updatedApiKeys = { ...apiKeys, [provider]: key };
189
- setApiKeys(updatedApiKeys);
190
-
191
- // Save updated API keys to cookies with 30 day expiry and secure settings
192
- Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), {
193
- expires: 30, // 30 days
194
- secure: true, // Only send over HTTPS
195
- sameSite: 'strict', // Protect against CSRF
196
- path: '/', // Accessible across the site
197
- });
198
- } catch (error) {
199
- console.error('Error saving API keys to cookies:', error);
200
- }
201
- };
202
-
203
  const handleFileUpload = () => {
204
  const input = document.createElement('input');
205
  input.type = 'file';
@@ -344,11 +380,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
344
  providerList={PROVIDER_LIST}
345
  apiKeys={apiKeys}
346
  />
347
- {provider && (
348
  <APIKeyManager
349
  provider={provider}
350
  apiKey={apiKeys[provider.name] || ''}
351
- setApiKey={(key) => updateApiKey(provider.name, key)}
 
 
 
 
352
  />
353
  )}
354
  </div>
@@ -511,7 +551,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
511
  </div>
512
  </div>
513
  </div>
514
- {!chatStarted && ImportButtons(importChat)}
 
 
 
 
 
515
  {!chatStarted &&
516
  ExamplePrompts((event, messageInput) => {
517
  if (isStreaming) {
 
21
  import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
22
  import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
23
  import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
24
+ import GitCloneButton from './GitCloneButton';
25
 
26
  import FilePreview from './FilePreview';
27
  import { ModelSelector } from '~/components/chat/ModelSelector';
 
88
  ref,
89
  ) => {
90
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
91
+ const [apiKeys, setApiKeys] = useState<Record<string, string>>(() => {
92
+ const savedKeys = Cookies.get('apiKeys');
93
+
94
+ if (savedKeys) {
95
+ try {
96
+ return JSON.parse(savedKeys);
97
+ } catch (error) {
98
+ console.error('Failed to parse API keys from cookies:', error);
99
+ return {};
100
+ }
101
+ }
102
+
103
+ return {};
104
+ });
105
  const [modelList, setModelList] = useState(MODEL_LIST);
106
  const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
107
  const [isListening, setIsListening] = useState(false);
108
  const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
109
  const [transcript, setTranscript] = useState('');
110
 
111
+ // Load enabled providers from cookies
112
+ const [enabledProviders, setEnabledProviders] = useState(() => {
113
+ const savedProviders = Cookies.get('providers');
114
+
115
+ if (savedProviders) {
116
+ try {
117
+ const parsedProviders = JSON.parse(savedProviders);
118
+ return PROVIDER_LIST.filter((p) => parsedProviders[p.name]);
119
+ } catch (error) {
120
+ console.error('Failed to parse providers from cookies:', error);
121
+ return PROVIDER_LIST;
122
+ }
123
+ }
124
+
125
+ return PROVIDER_LIST;
126
+ });
127
+
128
+ // Update enabled providers when cookies change
129
+ useEffect(() => {
130
+ const updateProvidersFromCookies = () => {
131
+ const savedProviders = Cookies.get('providers');
132
+
133
+ if (savedProviders) {
134
+ try {
135
+ const parsedProviders = JSON.parse(savedProviders);
136
+ setEnabledProviders(PROVIDER_LIST.filter((p) => parsedProviders[p.name]));
137
+ } catch (error) {
138
+ console.error('Failed to parse providers from cookies:', error);
139
+ }
140
+ }
141
+ };
142
+
143
+ updateProvidersFromCookies();
144
+
145
+ const interval = setInterval(updateProvidersFromCookies, 1000);
146
+
147
+ return () => clearInterval(interval);
148
+ }, [PROVIDER_LIST]);
149
+
150
  console.log(transcript);
151
  useEffect(() => {
152
  // Load API keys from cookies on component mount
 
236
  }
237
  };
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  const handleFileUpload = () => {
240
  const input = document.createElement('input');
241
  input.type = 'file';
 
380
  providerList={PROVIDER_LIST}
381
  apiKeys={apiKeys}
382
  />
383
+ {enabledProviders.length > 0 && provider && (
384
  <APIKeyManager
385
  provider={provider}
386
  apiKey={apiKeys[provider.name] || ''}
387
+ setApiKey={(key) => {
388
+ const newApiKeys = { ...apiKeys, [provider.name]: key };
389
+ setApiKeys(newApiKeys);
390
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
391
+ }}
392
  />
393
  )}
394
  </div>
 
551
  </div>
552
  </div>
553
  </div>
554
+ {!chatStarted && (
555
+ <div className="flex justify-center gap-2">
556
+ {ImportButtons(importChat)}
557
+ <GitCloneButton importChat={importChat} />
558
+ </div>
559
+ )}
560
  {!chatStarted &&
561
  ExamplePrompts((event, messageInput) => {
562
  if (isStreaming) {
app/components/chat/GitCloneButton.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ignore from 'ignore';
2
+ import { useGit } from '~/lib/hooks/useGit';
3
+ import type { Message } from 'ai';
4
+ import WithTooltip from '~/components/ui/Tooltip';
5
+
6
+ const IGNORE_PATTERNS = [
7
+ 'node_modules/**',
8
+ '.git/**',
9
+ '.github/**',
10
+ '.vscode/**',
11
+ '**/*.jpg',
12
+ '**/*.jpeg',
13
+ '**/*.png',
14
+ 'dist/**',
15
+ 'build/**',
16
+ '.next/**',
17
+ 'coverage/**',
18
+ '.cache/**',
19
+ '.vscode/**',
20
+ '.idea/**',
21
+ '**/*.log',
22
+ '**/.DS_Store',
23
+ '**/npm-debug.log*',
24
+ '**/yarn-debug.log*',
25
+ '**/yarn-error.log*',
26
+ '**/*lock.json',
27
+ '**/*lock.yaml',
28
+ ];
29
+
30
+ const ig = ignore().add(IGNORE_PATTERNS);
31
+ const generateId = () => Math.random().toString(36).substring(2, 15);
32
+
33
+ interface GitCloneButtonProps {
34
+ className?: string;
35
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
36
+ }
37
+
38
+ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
39
+ const { ready, gitClone } = useGit();
40
+ const onClick = async (_e: any) => {
41
+ if (!ready) {
42
+ return;
43
+ }
44
+
45
+ const repoUrl = prompt('Enter the Git url');
46
+
47
+ if (repoUrl) {
48
+ const { workdir, data } = await gitClone(repoUrl);
49
+
50
+ if (importChat) {
51
+ const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath));
52
+ console.log(filePaths);
53
+
54
+ const textDecoder = new TextDecoder('utf-8');
55
+ const message: Message = {
56
+ role: 'assistant',
57
+ content: `Cloning the repo ${repoUrl} into ${workdir}
58
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled" >
59
+ ${filePaths
60
+ .map((filePath) => {
61
+ const { data: content, encoding } = data[filePath];
62
+
63
+ if (encoding === 'utf8') {
64
+ return `<boltAction type="file" filePath="${filePath}">
65
+ ${content}
66
+ </boltAction>`;
67
+ } else if (content instanceof Uint8Array) {
68
+ return `<boltAction type="file" filePath="${filePath}">
69
+ ${textDecoder.decode(content)}
70
+ </boltAction>`;
71
+ } else {
72
+ return '';
73
+ }
74
+ })
75
+ .join('\n')}
76
+ </boltArtifact>`,
77
+ id: generateId(),
78
+ createdAt: new Date(),
79
+ };
80
+ console.log(JSON.stringify(message));
81
+
82
+ importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, [message]);
83
+
84
+ // console.log(files);
85
+ }
86
+ }
87
+ };
88
+
89
+ return (
90
+ <WithTooltip tooltip="Clone A Git Repo">
91
+ <button
92
+ onClick={(e) => {
93
+ onClick(e);
94
+ }}
95
+ title="Clone A Git Repo"
96
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
97
+ >
98
+ <span className="i-ph:git-branch" />
99
+ Clone A Git Repo
100
+ </button>
101
+ </WithTooltip>
102
+ );
103
+ }
app/components/chat/ModelSelector.tsx CHANGED
@@ -1,5 +1,7 @@
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
 
 
3
 
4
  interface ModelSelectorProps {
5
  model?: string;
@@ -19,12 +21,79 @@ export const ModelSelector = ({
19
  modelList,
20
  providerList,
21
  }: ModelSelectorProps) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  return (
23
  <div className="mb-2 flex gap-2 flex-col sm:flex-row">
24
  <select
25
  value={provider?.name ?? ''}
26
  onChange={(e) => {
27
- const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
28
 
29
  if (newProvider && setProvider) {
30
  setProvider(newProvider);
@@ -38,7 +107,7 @@ export const ModelSelector = ({
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
40
  >
41
- {providerList.map((provider: ProviderInfo) => (
42
  <option key={provider.name} value={provider.name}>
43
  {provider.name}
44
  </option>
 
1
  import type { ProviderInfo } from '~/types/model';
2
  import type { ModelInfo } from '~/utils/types';
3
+ import { useEffect, useState } from 'react';
4
+ import Cookies from 'js-cookie';
5
 
6
  interface ModelSelectorProps {
7
  model?: string;
 
21
  modelList,
22
  providerList,
23
  }: ModelSelectorProps) => {
24
+ // Load enabled providers from cookies
25
+ const [enabledProviders, setEnabledProviders] = useState(() => {
26
+ const savedProviders = Cookies.get('providers');
27
+
28
+ if (savedProviders) {
29
+ try {
30
+ const parsedProviders = JSON.parse(savedProviders);
31
+ return providerList.filter((p) => parsedProviders[p.name]);
32
+ } catch (error) {
33
+ console.error('Failed to parse providers from cookies:', error);
34
+ return providerList;
35
+ }
36
+ }
37
+
38
+ return providerList;
39
+ });
40
+
41
+ // Update enabled providers when cookies change
42
+ useEffect(() => {
43
+ // Function to update providers from cookies
44
+ const updateProvidersFromCookies = () => {
45
+ const savedProviders = Cookies.get('providers');
46
+
47
+ if (savedProviders) {
48
+ try {
49
+ const parsedProviders = JSON.parse(savedProviders);
50
+ const newEnabledProviders = providerList.filter((p) => parsedProviders[p.name]);
51
+ setEnabledProviders(newEnabledProviders);
52
+
53
+ // If current provider is disabled, switch to first enabled provider
54
+ if (provider && !parsedProviders[provider.name] && newEnabledProviders.length > 0) {
55
+ const firstEnabledProvider = newEnabledProviders[0];
56
+ setProvider?.(firstEnabledProvider);
57
+
58
+ // Also update the model to the first available one for the new provider
59
+ const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name);
60
+
61
+ if (firstModel) {
62
+ setModel?.(firstModel.name);
63
+ }
64
+ }
65
+ } catch (error) {
66
+ console.error('Failed to parse providers from cookies:', error);
67
+ }
68
+ }
69
+ };
70
+
71
+ // Initial update
72
+ updateProvidersFromCookies();
73
+
74
+ // Set up an interval to check for cookie changes
75
+ const interval = setInterval(updateProvidersFromCookies, 1000);
76
+
77
+ return () => clearInterval(interval);
78
+ }, [providerList, provider, setProvider, modelList, setModel]);
79
+
80
+ if (enabledProviders.length === 0) {
81
+ return (
82
+ <div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
83
+ <p className="text-center">
84
+ No providers are currently enabled. Please enable at least one provider in the settings to start using the
85
+ chat.
86
+ </p>
87
+ </div>
88
+ );
89
+ }
90
+
91
  return (
92
  <div className="mb-2 flex gap-2 flex-col sm:flex-row">
93
  <select
94
  value={provider?.name ?? ''}
95
  onChange={(e) => {
96
+ const newProvider = enabledProviders.find((p: ProviderInfo) => p.name === e.target.value);
97
 
98
  if (newProvider && setProvider) {
99
  setProvider(newProvider);
 
107
  }}
108
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
109
  >
110
+ {enabledProviders.map((provider: ProviderInfo) => (
111
  <option key={provider.name} value={provider.name}>
112
  {provider.name}
113
  </option>
app/components/chat/chatExportAndImport/ImportButtons.tsx CHANGED
@@ -5,7 +5,7 @@ import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
5
 
6
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
7
  return (
8
- <div className="flex flex-col items-center justify-center flex-1 p-4">
9
  <input
10
  type="file"
11
  id="chat-import"
 
5
 
6
  export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
7
  return (
8
+ <div className="flex flex-col items-center justify-center w-auto">
9
  <input
10
  type="file"
11
  id="chat-import"
app/components/sidebar/Menu.client.tsx CHANGED
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
 
 
6
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
7
  import { cubicEasingFn } from '~/utils/easings';
8
  import { logger } from '~/utils/logger';
@@ -39,6 +41,7 @@ export const Menu = () => {
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
41
  const [dialogContent, setDialogContent] = useState<DialogContent>(null);
 
42
 
43
  const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
44
  items: list,
@@ -200,10 +203,12 @@ export const Menu = () => {
200
  </Dialog>
201
  </DialogRoot>
202
  </div>
203
- <div className="flex items-center border-t border-bolt-elements-borderColor p-4">
204
- <ThemeSwitch className="ml-auto" />
 
205
  </div>
206
  </div>
 
207
  </motion.div>
208
  );
209
  };
 
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
+ import { Settings } from '~/components/ui/Settings';
7
+ import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
10
  import { logger } from '~/utils/logger';
 
41
  const [list, setList] = useState<ChatHistoryItem[]>([]);
42
  const [open, setOpen] = useState(false);
43
  const [dialogContent, setDialogContent] = useState<DialogContent>(null);
44
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
45
 
46
  const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
47
  items: list,
 
203
  </Dialog>
204
  </DialogRoot>
205
  </div>
206
+ <div className="flex items-center justify-between border-t border-bolt-elements-borderColor p-4">
207
+ <SettingsButton onClick={() => setIsSettingsOpen(true)} />
208
+ <ThemeSwitch />
209
  </div>
210
  </div>
211
+ <Settings open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
212
  </motion.div>
213
  );
214
  };
app/components/ui/Settings.tsx ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion } from 'framer-motion';
3
+ import { useState } from 'react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { DialogTitle, dialogVariants, dialogBackdropVariants } from './Dialog';
6
+ import { IconButton } from './IconButton';
7
+ import { providersList } from '~/lib/stores/settings';
8
+ import { db, getAll, deleteById } from '~/lib/persistence';
9
+ import { toast } from 'react-toastify';
10
+ import { useNavigate } from '@remix-run/react';
11
+ import commit from '~/commit.json';
12
+ import Cookies from 'js-cookie';
13
+
14
+ interface SettingsProps {
15
+ open: boolean;
16
+ onClose: () => void;
17
+ }
18
+
19
+ type TabType = 'chat-history' | 'providers' | 'features' | 'debug';
20
+
21
+ // Providers that support base URL configuration
22
+ const URL_CONFIGURABLE_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
23
+
24
+ export const Settings = ({ open, onClose }: SettingsProps) => {
25
+ const navigate = useNavigate();
26
+ const [activeTab, setActiveTab] = useState<TabType>('chat-history');
27
+ const [isDebugEnabled, setIsDebugEnabled] = useState(false);
28
+ const [searchTerm, setSearchTerm] = useState('');
29
+ const [isDeleting, setIsDeleting] = useState(false);
30
+
31
+ // Load base URLs from cookies
32
+ const [baseUrls, setBaseUrls] = useState(() => {
33
+ const savedUrls = Cookies.get('providerBaseUrls');
34
+
35
+ if (savedUrls) {
36
+ try {
37
+ return JSON.parse(savedUrls);
38
+ } catch (error) {
39
+ console.error('Failed to parse base URLs from cookies:', error);
40
+ return {
41
+ Ollama: 'http://localhost:11434',
42
+ LMStudio: 'http://localhost:1234',
43
+ OpenAILike: '',
44
+ };
45
+ }
46
+ }
47
+
48
+ return {
49
+ Ollama: 'http://localhost:11434',
50
+ LMStudio: 'http://localhost:1234',
51
+ OpenAILike: '',
52
+ };
53
+ });
54
+
55
+ const handleBaseUrlChange = (provider: string, url: string) => {
56
+ setBaseUrls((prev: Record<string, string>) => {
57
+ const newUrls = { ...prev, [provider]: url };
58
+ Cookies.set('providerBaseUrls', JSON.stringify(newUrls));
59
+
60
+ return newUrls;
61
+ });
62
+ };
63
+
64
+ const tabs: { id: TabType; label: string; icon: string }[] = [
65
+ { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book' },
66
+ { id: 'providers', label: 'Providers', icon: 'i-ph:key' },
67
+ { id: 'features', label: 'Features', icon: 'i-ph:star' },
68
+ ...(isDebugEnabled ? [{ id: 'debug' as TabType, label: 'Debug Tab', icon: 'i-ph:bug' }] : []),
69
+ ];
70
+
71
+ // Load providers from cookies on mount
72
+ const [providers, setProviders] = useState(() => {
73
+ const savedProviders = Cookies.get('providers');
74
+
75
+ if (savedProviders) {
76
+ try {
77
+ const parsedProviders = JSON.parse(savedProviders);
78
+
79
+ // Merge saved enabled states with the base provider list
80
+ return providersList.map((provider) => ({
81
+ ...provider,
82
+ isEnabled: parsedProviders[provider.name] || false,
83
+ }));
84
+ } catch (error) {
85
+ console.error('Failed to parse providers from cookies:', error);
86
+ }
87
+ }
88
+
89
+ return providersList;
90
+ });
91
+
92
+ const handleToggleProvider = (providerName: string) => {
93
+ setProviders((prevProviders) => {
94
+ const newProviders = prevProviders.map((provider) =>
95
+ provider.name === providerName ? { ...provider, isEnabled: !provider.isEnabled } : provider,
96
+ );
97
+
98
+ // Save to cookies
99
+ const enabledStates = newProviders.reduce(
100
+ (acc, provider) => ({
101
+ ...acc,
102
+ [provider.name]: provider.isEnabled,
103
+ }),
104
+ {},
105
+ );
106
+ Cookies.set('providers', JSON.stringify(enabledStates));
107
+
108
+ return newProviders;
109
+ });
110
+ };
111
+
112
+ const filteredProviders = providers
113
+ .filter((provider) => provider.name.toLowerCase().includes(searchTerm.toLowerCase()))
114
+ .sort((a, b) => a.name.localeCompare(b.name));
115
+
116
+ const handleCopyToClipboard = () => {
117
+ const debugInfo = {
118
+ OS: navigator.platform,
119
+ Browser: navigator.userAgent,
120
+ ActiveFeatures: providers.filter((provider) => provider.isEnabled).map((provider) => provider.name),
121
+ BaseURLs: {
122
+ Ollama: process.env.REACT_APP_OLLAMA_URL,
123
+ OpenAI: process.env.REACT_APP_OPENAI_URL,
124
+ LMStudio: process.env.REACT_APP_LM_STUDIO_URL,
125
+ },
126
+ Version: versionHash,
127
+ };
128
+ navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
129
+ alert('Debug information copied to clipboard!');
130
+ });
131
+ };
132
+
133
+ const downloadAsJson = (data: any, filename: string) => {
134
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
135
+ const url = URL.createObjectURL(blob);
136
+ const link = document.createElement('a');
137
+ link.href = url;
138
+ link.download = filename;
139
+ document.body.appendChild(link);
140
+ link.click();
141
+ document.body.removeChild(link);
142
+ URL.revokeObjectURL(url);
143
+ };
144
+
145
+ const handleDeleteAllChats = async () => {
146
+ if (!db) {
147
+ toast.error('Database is not available');
148
+ return;
149
+ }
150
+
151
+ try {
152
+ setIsDeleting(true);
153
+
154
+ const allChats = await getAll(db);
155
+
156
+ // Delete all chats one by one
157
+ await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
158
+
159
+ toast.success('All chats deleted successfully');
160
+ navigate('/', { replace: true });
161
+ } catch (error) {
162
+ toast.error('Failed to delete chats');
163
+ console.error(error);
164
+ } finally {
165
+ setIsDeleting(false);
166
+ }
167
+ };
168
+
169
+ const handleExportAllChats = async () => {
170
+ if (!db) {
171
+ toast.error('Database is not available');
172
+ return;
173
+ }
174
+
175
+ try {
176
+ const allChats = await getAll(db);
177
+ const exportData = {
178
+ chats: allChats,
179
+ exportDate: new Date().toISOString(),
180
+ };
181
+
182
+ downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
183
+ toast.success('Chats exported successfully');
184
+ } catch (error) {
185
+ toast.error('Failed to export chats');
186
+ console.error(error);
187
+ }
188
+ };
189
+
190
+ const versionHash = commit.commit; // Get the version hash from commit.json
191
+
192
+ return (
193
+ <RadixDialog.Root open={open}>
194
+ <RadixDialog.Portal>
195
+ <RadixDialog.Overlay asChild>
196
+ <motion.div
197
+ className="bg-black/50 fixed inset-0 z-max"
198
+ initial="closed"
199
+ animate="open"
200
+ exit="closed"
201
+ variants={dialogBackdropVariants}
202
+ />
203
+ </RadixDialog.Overlay>
204
+ <RadixDialog.Content asChild>
205
+ <motion.div
206
+ className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg bg-gray-800 shadow-lg focus:outline-none overflow-hidden"
207
+ initial="closed"
208
+ animate="open"
209
+ exit="closed"
210
+ variants={dialogVariants}
211
+ >
212
+ <div className="flex h-full">
213
+ <div className="w-48 border-r border-bolt-elements-borderColor bg-gray-700 p-4 flex flex-col justify-between">
214
+ {tabs.map((tab) => (
215
+ <button
216
+ key={tab.id}
217
+ onClick={() => setActiveTab(tab.id)}
218
+ className={classNames(
219
+ 'w-full flex items-center gap-2 px-4 py-3 rounded-lg text-left text-sm transition-all mb-2',
220
+ activeTab === tab.id ? 'bg-blue-600 text-white' : 'bg-gray-600 text-gray-200 hover:bg-blue-500',
221
+ )}
222
+ >
223
+ <div className={tab.icon} />
224
+ {tab.label}
225
+ </button>
226
+ ))}
227
+ <div className="mt-auto flex flex-col gap-2">
228
+ <a
229
+ href="https://github.com/coleam00/bolt.new-any-llm"
230
+ target="_blank"
231
+ rel="noopener noreferrer"
232
+ className="flex items-center justify-center bg-blue-600 text-white rounded-lg py-2 hover:bg-blue-500 transition-colors duration-200"
233
+ >
234
+ GitHub
235
+ </a>
236
+ <a
237
+ href="https://coleam00.github.io/bolt.new-any-llm"
238
+ target="_blank"
239
+ rel="noopener noreferrer"
240
+ className="flex items-center justify-center bg-blue-600 text-white rounded-lg py-2 hover:bg-blue-500 transition-colors duration-200"
241
+ >
242
+ Docs
243
+ </a>
244
+ </div>
245
+ </div>
246
+
247
+ <div className="flex-1 flex flex-col p-8">
248
+ <DialogTitle className="flex-shrink-0 text-lg font-semibold text-white">Settings</DialogTitle>
249
+ <div className="flex-1 overflow-y-auto">
250
+ {activeTab === 'chat-history' && (
251
+ <div className="p-4">
252
+ <h3 className="text-lg font-medium text-white mb-4">Chat History</h3>
253
+ <button
254
+ onClick={handleExportAllChats}
255
+ className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
256
+ >
257
+ Export All Chats
258
+ </button>
259
+
260
+ <div className="bg-red-500 text-white rounded-lg p-4 mb-4">
261
+ <h4 className="font-semibold">Danger Area</h4>
262
+ <p className="mb-2">This action cannot be undone!</p>
263
+ <button
264
+ onClick={handleDeleteAllChats}
265
+ disabled={isDeleting}
266
+ className={classNames(
267
+ 'bg-red-700 text-white rounded-lg px-4 py-2 transition-colors duration-200',
268
+ isDeleting ? 'opacity-50 cursor-not-allowed' : 'hover:bg-red-800',
269
+ )}
270
+ >
271
+ {isDeleting ? 'Deleting...' : 'Delete All Chats'}
272
+ </button>
273
+ </div>
274
+ </div>
275
+ )}
276
+ {activeTab === 'providers' && (
277
+ <div className="p-4">
278
+ <h3 className="text-lg font-medium text-white mb-4">Providers</h3>
279
+ <input
280
+ type="text"
281
+ placeholder="Search providers..."
282
+ value={searchTerm}
283
+ onChange={(e) => setSearchTerm(e.target.value)}
284
+ className="mb-4 p-2 rounded border border-gray-300 w-full"
285
+ />
286
+ {filteredProviders.map((provider) => (
287
+ <div
288
+ key={provider.name}
289
+ className="flex flex-col mb-6 provider-item hover:bg-gray-600 p-4 rounded-lg"
290
+ >
291
+ <div className="flex items-center justify-between mb-2">
292
+ <span className="text-white">{provider.name}</span>
293
+ <label className="relative inline-flex items-center cursor-pointer">
294
+ <input
295
+ type="checkbox"
296
+ className="sr-only"
297
+ checked={provider.isEnabled}
298
+ onChange={() => handleToggleProvider(provider.name)}
299
+ />
300
+ <div className="w-11 h-6 bg-gray-300 rounded-full shadow-inner"></div>
301
+ <div
302
+ className={`absolute left-0 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
303
+ provider.isEnabled ? 'transform translate-x-full bg-green-500' : ''
304
+ }`}
305
+ ></div>
306
+ </label>
307
+ </div>
308
+
309
+ {/* Base URL input for configurable providers */}
310
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.isEnabled && (
311
+ <div className="mt-2">
312
+ <label className="block text-sm text-gray-300 mb-1">Base URL:</label>
313
+ <input
314
+ type="text"
315
+ value={baseUrls[provider.name]}
316
+ onChange={(e) => handleBaseUrlChange(provider.name, e.target.value)}
317
+ placeholder={`Enter ${provider.name} base URL`}
318
+ className="w-full p-2 rounded border border-gray-600 bg-gray-700 text-white text-sm"
319
+ />
320
+ </div>
321
+ )}
322
+ </div>
323
+ ))}
324
+ </div>
325
+ )}
326
+ {activeTab === 'features' && (
327
+ <div className="p-4">
328
+ <div className="flex items-center justify-between mb-2">
329
+ <span className="text-white">Debug Info</span>
330
+ <label className="relative inline-flex items-center cursor-pointer">
331
+ <input
332
+ type="checkbox"
333
+ className="sr-only"
334
+ checked={isDebugEnabled}
335
+ onChange={() => setIsDebugEnabled(!isDebugEnabled)}
336
+ />
337
+ <div className="w-11 h-6 bg-gray-300 rounded-full shadow-inner"></div>
338
+ <div
339
+ className={`absolute left-0 w-6 h-6 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${
340
+ isDebugEnabled ? 'transform translate-x-full bg-green-500' : ''
341
+ }`}
342
+ ></div>
343
+ </label>
344
+ </div>
345
+ <div className="feature-row">{/* Your feature content here */}</div>
346
+ </div>
347
+ )}
348
+ {activeTab === 'debug' && isDebugEnabled && (
349
+ <div className="p-4">
350
+ <h3 className="text-lg font-medium text-white mb-4">Debug Tab</h3>
351
+ <button
352
+ onClick={handleCopyToClipboard}
353
+ className="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600 mb-4 transition-colors duration-200"
354
+ >
355
+ Copy to Clipboard
356
+ </button>
357
+
358
+ <h4 className="text-md font-medium text-white">System Information</h4>
359
+ <p className="text-white">OS: {navigator.platform}</p>
360
+ <p className="text-white">Browser: {navigator.userAgent}</p>
361
+
362
+ <h4 className="text-md font-medium text-white mt-4">Active Features</h4>
363
+ <ul>
364
+ {providers
365
+ .filter((provider) => provider.isEnabled)
366
+ .map((provider) => (
367
+ <li key={provider.name} className="text-white">
368
+ {provider.name}
369
+ </li>
370
+ ))}
371
+ </ul>
372
+
373
+ <h4 className="text-md font-medium text-white mt-4">Base URLs</h4>
374
+ <ul>
375
+ <li className="text-white">Ollama: {process.env.REACT_APP_OLLAMA_URL}</li>
376
+ <li className="text-white">OpenAI: {process.env.REACT_APP_OPENAI_URL}</li>
377
+ <li className="text-white">LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}</li>
378
+ </ul>
379
+
380
+ <h4 className="text-md font-medium text-white mt-4">Version Information</h4>
381
+ <p className="text-white">Version Hash: {versionHash}</p>
382
+ </div>
383
+ )}
384
+ </div>
385
+ </div>
386
+ </div>
387
+ <RadixDialog.Close asChild onClick={onClose}>
388
+ <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
389
+ </RadixDialog.Close>
390
+ </motion.div>
391
+ </RadixDialog.Content>
392
+ </RadixDialog.Portal>
393
+ </RadixDialog.Root>
394
+ );
395
+ };
app/components/ui/SettingsButton.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo } from 'react';
2
+ import { IconButton } from './IconButton';
3
+
4
+ interface SettingsButtonProps {
5
+ onClick: () => void;
6
+ }
7
+
8
+ export const SettingsButton = memo(({ onClick }: SettingsButtonProps) => {
9
+ return (
10
+ <IconButton
11
+ onClick={onClick}
12
+ icon="i-ph:gear"
13
+ size="xl"
14
+ title="Settings"
15
+ className="text-[#666] hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive/10 transition-colors"
16
+ />
17
+ );
18
+ });
app/components/ui/SettingsSlider.tsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion';
2
+ import { memo } from 'react';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface SliderOption<T> {
6
+ value: T;
7
+ text: string;
8
+ }
9
+
10
+ export interface SliderOptions<T> {
11
+ left: SliderOption<T>;
12
+ right: SliderOption<T>;
13
+ }
14
+
15
+ interface SettingsSliderProps<T> {
16
+ selected: T;
17
+ options: SliderOptions<T>;
18
+ setSelected?: (selected: T) => void;
19
+ }
20
+
21
+ export const SettingsSlider = memo(<T,>({ selected, options, setSelected }: SettingsSliderProps<T>) => {
22
+ const isLeftSelected = selected === options.left.value;
23
+
24
+ return (
25
+ <div className="relative flex items-center bg-bolt-elements-prompt-background rounded-lg">
26
+ <motion.div
27
+ className={classNames(
28
+ 'absolute h-full bg-green-500 transition-all duration-300 rounded-lg',
29
+ isLeftSelected ? 'left-0 w-1/2' : 'right-0 w-1/2',
30
+ )}
31
+ initial={false}
32
+ animate={{
33
+ x: isLeftSelected ? 0 : '100%',
34
+ opacity: 0.2,
35
+ }}
36
+ transition={{
37
+ type: 'spring',
38
+ stiffness: 300,
39
+ damping: 30,
40
+ }}
41
+ />
42
+ <button
43
+ onClick={() => setSelected?.(options.left.value)}
44
+ className={classNames(
45
+ 'relative z-10 flex-1 p-2 rounded-lg text-sm transition-colors duration-200',
46
+ isLeftSelected ? 'text-white' : 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary',
47
+ )}
48
+ >
49
+ {options.left.text}
50
+ </button>
51
+ <button
52
+ onClick={() => setSelected?.(options.right.value)}
53
+ className={classNames(
54
+ 'relative z-10 flex-1 p-2 rounded-lg text-sm transition-colors duration-200',
55
+ !isLeftSelected ? 'text-white' : 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary',
56
+ )}
57
+ >
58
+ {options.right.text}
59
+ </button>
60
+ </div>
61
+ );
62
+ });
app/lib/hooks/useGit.ts ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer } from '@webcontainer/api';
2
+ import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react';
3
+ import { webcontainer as webcontainerPromise } from '~/lib/webcontainer';
4
+ import git, { type GitAuth, type PromiseFsClient } from 'isomorphic-git';
5
+ import http from 'isomorphic-git/http/web';
6
+ import Cookies from 'js-cookie';
7
+ import { toast } from 'react-toastify';
8
+
9
+ const lookupSavedPassword = (url: string) => {
10
+ const domain = url.split('/')[2];
11
+ const gitCreds = Cookies.get(`git:${domain}`);
12
+
13
+ if (!gitCreds) {
14
+ return null;
15
+ }
16
+
17
+ try {
18
+ const { username, password } = JSON.parse(gitCreds || '{}');
19
+ return { username, password };
20
+ } catch (error) {
21
+ console.log(`Failed to parse Git Cookie ${error}`);
22
+ return null;
23
+ }
24
+ };
25
+
26
+ const saveGitAuth = (url: string, auth: GitAuth) => {
27
+ const domain = url.split('/')[2];
28
+ Cookies.set(`git:${domain}`, JSON.stringify(auth));
29
+ };
30
+
31
+ export function useGit() {
32
+ const [ready, setReady] = useState(false);
33
+ const [webcontainer, setWebcontainer] = useState<WebContainer>();
34
+ const [fs, setFs] = useState<PromiseFsClient>();
35
+ const fileData = useRef<Record<string, { data: any; encoding?: string }>>({});
36
+ useEffect(() => {
37
+ webcontainerPromise.then((container) => {
38
+ fileData.current = {};
39
+ setWebcontainer(container);
40
+ setFs(getFs(container, fileData));
41
+ setReady(true);
42
+ });
43
+ }, []);
44
+
45
+ const gitClone = useCallback(
46
+ async (url: string) => {
47
+ if (!webcontainer || !fs || !ready) {
48
+ throw 'Webcontainer not initialized';
49
+ }
50
+
51
+ fileData.current = {};
52
+ await git.clone({
53
+ fs,
54
+ http,
55
+ dir: webcontainer.workdir,
56
+ url,
57
+ depth: 1,
58
+ singleBranch: true,
59
+ corsProxy: 'https://cors.isomorphic-git.org',
60
+ onAuth: (url) => {
61
+ // let domain=url.split("/")[2]
62
+
63
+ let auth = lookupSavedPassword(url);
64
+
65
+ if (auth) {
66
+ return auth;
67
+ }
68
+
69
+ if (confirm('This repo is password protected. Ready to enter a username & password?')) {
70
+ auth = {
71
+ username: prompt('Enter username'),
72
+ password: prompt('Enter password'),
73
+ };
74
+ return auth;
75
+ } else {
76
+ return { cancel: true };
77
+ }
78
+ },
79
+ onAuthFailure: (url, _auth) => {
80
+ toast.error(`Error Authenticating with ${url.split('/')[2]}`);
81
+ },
82
+ onAuthSuccess: (url, auth) => {
83
+ saveGitAuth(url, auth);
84
+ },
85
+ });
86
+
87
+ const data: Record<string, { data: any; encoding?: string }> = {};
88
+
89
+ for (const [key, value] of Object.entries(fileData.current)) {
90
+ data[key] = value;
91
+ }
92
+
93
+ return { workdir: webcontainer.workdir, data };
94
+ },
95
+ [webcontainer],
96
+ );
97
+
98
+ return { ready, gitClone };
99
+ }
100
+
101
+ const getFs = (
102
+ webcontainer: WebContainer,
103
+ record: MutableRefObject<Record<string, { data: any; encoding?: string }>>,
104
+ ) => ({
105
+ promises: {
106
+ readFile: async (path: string, options: any) => {
107
+ const encoding = options.encoding;
108
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
109
+ console.log('readFile', relativePath, encoding);
110
+
111
+ return await webcontainer.fs.readFile(relativePath, encoding);
112
+ },
113
+ writeFile: async (path: string, data: any, options: any) => {
114
+ const encoding = options.encoding;
115
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
116
+ console.log('writeFile', { relativePath, data, encoding });
117
+
118
+ if (record.current) {
119
+ record.current[relativePath] = { data, encoding };
120
+ }
121
+
122
+ return await webcontainer.fs.writeFile(relativePath, data, { ...options, encoding });
123
+ },
124
+ mkdir: async (path: string, options: any) => {
125
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
126
+ console.log('mkdir', relativePath, options);
127
+
128
+ return await webcontainer.fs.mkdir(relativePath, { ...options, recursive: true });
129
+ },
130
+ readdir: async (path: string, options: any) => {
131
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
132
+ console.log('readdir', relativePath, options);
133
+
134
+ return await webcontainer.fs.readdir(relativePath, options);
135
+ },
136
+ rm: async (path: string, options: any) => {
137
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
138
+ console.log('rm', relativePath, options);
139
+
140
+ return await webcontainer.fs.rm(relativePath, { ...(options || {}) });
141
+ },
142
+ rmdir: async (path: string, options: any) => {
143
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
144
+ console.log('rmdir', relativePath, options);
145
+
146
+ return await webcontainer.fs.rm(relativePath, { recursive: true, ...options });
147
+ },
148
+
149
+ // Mock implementations for missing functions
150
+ unlink: async (path: string) => {
151
+ // unlink is just removing a single file
152
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
153
+ return await webcontainer.fs.rm(relativePath, { recursive: false });
154
+ },
155
+
156
+ stat: async (path: string) => {
157
+ try {
158
+ const relativePath = pathUtils.relative(webcontainer.workdir, path);
159
+ const resp = await webcontainer.fs.readdir(pathUtils.dirname(relativePath), { withFileTypes: true });
160
+ const name = pathUtils.basename(relativePath);
161
+ const fileInfo = resp.find((x) => x.name == name);
162
+
163
+ if (!fileInfo) {
164
+ throw new Error(`ENOENT: no such file or directory, stat '${path}'`);
165
+ }
166
+
167
+ return {
168
+ isFile: () => fileInfo.isFile(),
169
+ isDirectory: () => fileInfo.isDirectory(),
170
+ isSymbolicLink: () => false,
171
+ size: 1,
172
+ mode: 0o666, // Default permissions
173
+ mtimeMs: Date.now(),
174
+ uid: 1000,
175
+ gid: 1000,
176
+ };
177
+ } catch (error: any) {
178
+ console.log(error?.message);
179
+
180
+ const err = new Error(`ENOENT: no such file or directory, stat '${path}'`) as NodeJS.ErrnoException;
181
+ err.code = 'ENOENT';
182
+ err.errno = -2;
183
+ err.syscall = 'stat';
184
+ err.path = path;
185
+ throw err;
186
+ }
187
+ },
188
+
189
+ lstat: async (path: string) => {
190
+ /*
191
+ * For basic usage, lstat can return the same as stat
192
+ * since we're not handling symbolic links
193
+ */
194
+ return await getFs(webcontainer, record).promises.stat(path);
195
+ },
196
+
197
+ readlink: async (path: string) => {
198
+ /*
199
+ * Since WebContainer doesn't support symlinks,
200
+ * we'll throw a "not a symbolic link" error
201
+ */
202
+ throw new Error(`EINVAL: invalid argument, readlink '${path}'`);
203
+ },
204
+
205
+ symlink: async (target: string, path: string) => {
206
+ /*
207
+ * Since WebContainer doesn't support symlinks,
208
+ * we'll throw a "operation not supported" error
209
+ */
210
+ throw new Error(`EPERM: operation not permitted, symlink '${target}' -> '${path}'`);
211
+ },
212
+
213
+ chmod: async (_path: string, _mode: number) => {
214
+ /*
215
+ * WebContainer doesn't support changing permissions,
216
+ * but we can pretend it succeeded for compatibility
217
+ */
218
+ return await Promise.resolve();
219
+ },
220
+ },
221
+ });
222
+
223
+ const pathUtils = {
224
+ dirname: (path: string) => {
225
+ // Handle empty or just filename cases
226
+ if (!path || !path.includes('/')) {
227
+ return '.';
228
+ }
229
+
230
+ // Remove trailing slashes
231
+ path = path.replace(/\/+$/, '');
232
+
233
+ // Get directory part
234
+ return path.split('/').slice(0, -1).join('/') || '/';
235
+ },
236
+
237
+ basename: (path: string, ext?: string) => {
238
+ // Remove trailing slashes
239
+ path = path.replace(/\/+$/, '');
240
+
241
+ // Get the last part of the path
242
+ const base = path.split('/').pop() || '';
243
+
244
+ // If extension is provided, remove it from the result
245
+ if (ext && base.endsWith(ext)) {
246
+ return base.slice(0, -ext.length);
247
+ }
248
+
249
+ return base;
250
+ },
251
+ relative: (from: string, to: string): string => {
252
+ // Handle empty inputs
253
+ if (!from || !to) {
254
+ return '.';
255
+ }
256
+
257
+ // Normalize paths by removing trailing slashes and splitting
258
+ const normalizePathParts = (p: string) => p.replace(/\/+$/, '').split('/').filter(Boolean);
259
+
260
+ const fromParts = normalizePathParts(from);
261
+ const toParts = normalizePathParts(to);
262
+
263
+ // Find common parts at the start of both paths
264
+ let commonLength = 0;
265
+ const minLength = Math.min(fromParts.length, toParts.length);
266
+
267
+ for (let i = 0; i < minLength; i++) {
268
+ if (fromParts[i] !== toParts[i]) {
269
+ break;
270
+ }
271
+
272
+ commonLength++;
273
+ }
274
+
275
+ // Calculate the number of "../" needed
276
+ const upCount = fromParts.length - commonLength;
277
+
278
+ // Get the remaining path parts we need to append
279
+ const remainingPath = toParts.slice(commonLength);
280
+
281
+ // Construct the relative path
282
+ const relativeParts = [...Array(upCount).fill('..'), ...remainingPath];
283
+
284
+ // Handle empty result case
285
+ return relativeParts.length === 0 ? '.' : relativeParts.join('/');
286
+ },
287
+ };
app/lib/stores/settings.ts CHANGED
@@ -15,10 +15,33 @@ export interface Shortcuts {
15
  toggleTerminal: Shortcut;
16
  }
17
 
 
 
 
 
 
18
  export interface Settings {
19
  shortcuts: Shortcuts;
 
20
  }
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  export const shortcutsStore = map<Shortcuts>({
23
  toggleTerminal: {
24
  key: 'j',
@@ -29,6 +52,7 @@ export const shortcutsStore = map<Shortcuts>({
29
 
30
  export const settingsStore = map<Settings>({
31
  shortcuts: shortcutsStore.get(),
 
32
  });
33
 
34
  shortcutsStore.subscribe((shortcuts) => {
 
15
  toggleTerminal: Shortcut;
16
  }
17
 
18
+ export interface Provider {
19
+ name: string;
20
+ isEnabled: boolean;
21
+ }
22
+
23
  export interface Settings {
24
  shortcuts: Shortcuts;
25
+ providers: Provider[];
26
  }
27
 
28
+ export const providersList: Provider[] = [
29
+ { name: 'Groq', isEnabled: false },
30
+ { name: 'HuggingFace', isEnabled: false },
31
+ { name: 'OpenAI', isEnabled: false },
32
+ { name: 'Anthropic', isEnabled: false },
33
+ { name: 'OpenRouter', isEnabled: false },
34
+ { name: 'Google', isEnabled: false },
35
+ { name: 'Ollama', isEnabled: false },
36
+ { name: 'OpenAILike', isEnabled: false },
37
+ { name: 'Together', isEnabled: false },
38
+ { name: 'Deepseek', isEnabled: false },
39
+ { name: 'Mistral', isEnabled: false },
40
+ { name: 'Cohere', isEnabled: false },
41
+ { name: 'LMStudio', isEnabled: false },
42
+ { name: 'xAI', isEnabled: false },
43
+ ];
44
+
45
  export const shortcutsStore = map<Shortcuts>({
46
  toggleTerminal: {
47
  key: 'j',
 
52
 
53
  export const settingsStore = map<Settings>({
54
  shortcuts: shortcutsStore.get(),
55
+ providers: providersList,
56
  });
57
 
58
  shortcutsStore.subscribe((shortcuts) => {
app/types/model.ts CHANGED
@@ -7,4 +7,5 @@ export type ProviderInfo = {
7
  getApiKeyLink?: string;
8
  labelForGetApiKey?: string;
9
  icon?: string;
 
10
  };
 
7
  getApiKeyLink?: string;
8
  labelForGetApiKey?: string;
9
  icon?: string;
10
+ isEnabled?: boolean;
11
  };
package.json CHANGED
@@ -58,6 +58,7 @@
58
  "@openrouter/ai-sdk-provider": "^0.0.5",
59
  "@radix-ui/react-dialog": "^1.1.2",
60
  "@radix-ui/react-dropdown-menu": "^2.1.2",
 
61
  "@radix-ui/react-tooltip": "^1.1.4",
62
  "@remix-run/cloudflare": "^2.15.0",
63
  "@remix-run/cloudflare-pages": "^2.15.0",
@@ -75,13 +76,13 @@
75
  "framer-motion": "^11.12.0",
76
  "ignore": "^6.0.2",
77
  "isbot": "^4.4.0",
 
78
  "istextorbinary": "^9.5.0",
79
  "jose": "^5.9.6",
80
  "js-cookie": "^3.0.5",
81
  "jszip": "^3.10.1",
82
  "nanostores": "^0.10.3",
83
  "ollama-ai-provider": "^0.15.2",
84
- "pnpm": "^9.14.4",
85
  "react": "^18.3.1",
86
  "react-dom": "^18.3.1",
87
  "react-hotkeys-hook": "^4.6.1",
@@ -110,6 +111,7 @@
110
  "husky": "9.1.7",
111
  "is-ci": "^3.0.1",
112
  "node-fetch": "^3.3.2",
 
113
  "prettier": "^3.4.1",
114
  "sass-embedded": "^1.81.0",
115
  "typescript": "^5.7.2",
 
58
  "@openrouter/ai-sdk-provider": "^0.0.5",
59
  "@radix-ui/react-dialog": "^1.1.2",
60
  "@radix-ui/react-dropdown-menu": "^2.1.2",
61
+ "@radix-ui/react-separator": "^1.1.0",
62
  "@radix-ui/react-tooltip": "^1.1.4",
63
  "@remix-run/cloudflare": "^2.15.0",
64
  "@remix-run/cloudflare-pages": "^2.15.0",
 
76
  "framer-motion": "^11.12.0",
77
  "ignore": "^6.0.2",
78
  "isbot": "^4.4.0",
79
+ "isomorphic-git": "^1.27.2",
80
  "istextorbinary": "^9.5.0",
81
  "jose": "^5.9.6",
82
  "js-cookie": "^3.0.5",
83
  "jszip": "^3.10.1",
84
  "nanostores": "^0.10.3",
85
  "ollama-ai-provider": "^0.15.2",
 
86
  "react": "^18.3.1",
87
  "react-dom": "^18.3.1",
88
  "react-hotkeys-hook": "^4.6.1",
 
111
  "husky": "9.1.7",
112
  "is-ci": "^3.0.1",
113
  "node-fetch": "^3.3.2",
114
+ "pnpm": "^9.14.4",
115
  "prettier": "^3.4.1",
116
  "sass-embedded": "^1.81.0",
117
  "typescript": "^5.7.2",
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff