diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml new file mode 100644 index 0000000..2f194cd --- /dev/null +++ b/.github/workflows/commit.yaml @@ -0,0 +1,32 @@ +name: Update Commit Hash File + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + update-commit: + runs-on: ubuntu-latest + + steps: + - name: Checkout the code + uses: actions/checkout@v3 + + - name: Get the latest commit hash + run: echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV + + - name: Update commit file + run: | + echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json + + - name: Commit and push the update + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add app/commit.json + git commit -m "chore: update commit hash to $COMMIT_HASH" + git push \ No newline at end of file diff --git a/app/commit.json b/app/commit.json new file mode 100644 index 0000000..8b96fc7 --- /dev/null +++ b/app/commit.json @@ -0,0 +1 @@ +{ "commit": "228cf1f34fd64b6960460f84c9db47bd7ef03150" } diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index a0a87f8..5a525d8 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -88,13 +88,65 @@ export const BaseChat = React.forwardRef( ref, ) => { const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; - const [apiKeys, setApiKeys] = useState>({}); + const [apiKeys, setApiKeys] = useState>(() => { + const savedKeys = Cookies.get('apiKeys'); + + if (savedKeys) { + try { + return JSON.parse(savedKeys); + } catch (error) { + console.error('Failed to parse API keys from cookies:', error); + return {}; + } + } + + return {}; + }); const [modelList, setModelList] = useState(MODEL_LIST); const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false); const [isListening, setIsListening] = useState(false); const [recognition, setRecognition] = useState(null); const [transcript, setTranscript] = useState(''); + // Load enabled providers from cookies + const [enabledProviders, setEnabledProviders] = useState(() => { + const savedProviders = Cookies.get('providers'); + + if (savedProviders) { + try { + const parsedProviders = JSON.parse(savedProviders); + return PROVIDER_LIST.filter((p) => parsedProviders[p.name]); + } catch (error) { + console.error('Failed to parse providers from cookies:', error); + return PROVIDER_LIST; + } + } + + return PROVIDER_LIST; + }); + + // Update enabled providers when cookies change + useEffect(() => { + const updateProvidersFromCookies = () => { + const savedProviders = Cookies.get('providers'); + + if (savedProviders) { + try { + const parsedProviders = JSON.parse(savedProviders); + setEnabledProviders(PROVIDER_LIST.filter((p) => parsedProviders[p.name])); + } catch (error) { + console.error('Failed to parse providers from cookies:', error); + } + } + }; + + updateProvidersFromCookies(); + + const interval = setInterval(updateProvidersFromCookies, 1000); + + return () => clearInterval(interval); + }, [PROVIDER_LIST]); + console.log(transcript); useEffect(() => { // Load API keys from cookies on component mount @@ -184,23 +236,6 @@ export const BaseChat = React.forwardRef( } }; - const updateApiKey = (provider: string, key: string) => { - try { - const updatedApiKeys = { ...apiKeys, [provider]: key }; - setApiKeys(updatedApiKeys); - - // Save updated API keys to cookies with 30 day expiry and secure settings - Cookies.set('apiKeys', JSON.stringify(updatedApiKeys), { - expires: 30, // 30 days - secure: true, // Only send over HTTPS - sameSite: 'strict', // Protect against CSRF - path: '/', // Accessible across the site - }); - } catch (error) { - console.error('Error saving API keys to cookies:', error); - } - }; - const handleFileUpload = () => { const input = document.createElement('input'); input.type = 'file'; @@ -360,11 +395,15 @@ export const BaseChat = React.forwardRef( providerList={PROVIDER_LIST} apiKeys={apiKeys} /> - {provider && ( + {enabledProviders.length > 0 && provider && ( updateApiKey(provider.name, key)} + setApiKey={(key) => { + const newApiKeys = { ...apiKeys, [provider.name]: key }; + setApiKeys(newApiKeys); + Cookies.set('apiKeys', JSON.stringify(newApiKeys)); + }} /> )} diff --git a/app/components/chat/ModelSelector.tsx b/app/components/chat/ModelSelector.tsx index 1bc7a66..435f4ba 100644 --- a/app/components/chat/ModelSelector.tsx +++ b/app/components/chat/ModelSelector.tsx @@ -1,5 +1,7 @@ import type { ProviderInfo } from '~/types/model'; import type { ModelInfo } from '~/utils/types'; +import { useEffect, useState } from 'react'; +import Cookies from 'js-cookie'; interface ModelSelectorProps { model?: string; @@ -19,12 +21,79 @@ export const ModelSelector = ({ modelList, providerList, }: ModelSelectorProps) => { + // Load enabled providers from cookies + const [enabledProviders, setEnabledProviders] = useState(() => { + const savedProviders = Cookies.get('providers'); + + if (savedProviders) { + try { + const parsedProviders = JSON.parse(savedProviders); + return providerList.filter((p) => parsedProviders[p.name]); + } catch (error) { + console.error('Failed to parse providers from cookies:', error); + return providerList; + } + } + + return providerList; + }); + + // Update enabled providers when cookies change + useEffect(() => { + // Function to update providers from cookies + const updateProvidersFromCookies = () => { + const savedProviders = Cookies.get('providers'); + + if (savedProviders) { + try { + const parsedProviders = JSON.parse(savedProviders); + const newEnabledProviders = providerList.filter((p) => parsedProviders[p.name]); + setEnabledProviders(newEnabledProviders); + + // If current provider is disabled, switch to first enabled provider + if (provider && !parsedProviders[provider.name] && newEnabledProviders.length > 0) { + const firstEnabledProvider = newEnabledProviders[0]; + setProvider?.(firstEnabledProvider); + + // Also update the model to the first available one for the new provider + const firstModel = modelList.find((m) => m.provider === firstEnabledProvider.name); + + if (firstModel) { + setModel?.(firstModel.name); + } + } + } catch (error) { + console.error('Failed to parse providers from cookies:', error); + } + } + }; + + // Initial update + updateProvidersFromCookies(); + + // Set up an interval to check for cookie changes + const interval = setInterval(updateProvidersFromCookies, 1000); + + return () => clearInterval(interval); + }, [providerList, provider, setProvider, modelList, setModel]); + + if (enabledProviders.length === 0) { + return ( +
+

+ No providers are currently enabled. Please enable at least one provider in the settings to start using the + chat. +

+
+ ); + } + return (
setSearchTerm(e.target.value)} + className="mb-4 p-2 rounded border border-gray-300 w-full" + /> + {filteredProviders.map((provider) => ( +
+
+ {provider.name} + +
+ + {/* Base URL input for configurable providers */} + {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && provider.isEnabled && ( +
+ + handleBaseUrlChange(provider.name, e.target.value)} + placeholder={`Enter ${provider.name} base URL`} + className="w-full p-2 rounded border border-gray-600 bg-gray-700 text-white text-sm" + /> +
+ )} +
+ ))} +
+ )} + {activeTab === 'features' && ( +
+
+ Debug Info + +
+
{/* Your feature content here */}
+
+ )} + {activeTab === 'debug' && isDebugEnabled && ( +
+

Debug Tab

+ + +

System Information

+

OS: {navigator.platform}

+

Browser: {navigator.userAgent}

+ +

Active Features

+
    + {providers + .filter((provider) => provider.isEnabled) + .map((provider) => ( +
  • + {provider.name} +
  • + ))} +
+ +

Base URLs

+
    +
  • Ollama: {process.env.REACT_APP_OLLAMA_URL}
  • +
  • OpenAI: {process.env.REACT_APP_OPENAI_URL}
  • +
  • LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}
  • +
+ +

Version Information

+

Version Hash: {versionHash}

+
+ )} + + + + + + + + + + + ); +}; diff --git a/app/components/ui/SettingsButton.tsx b/app/components/ui/SettingsButton.tsx new file mode 100644 index 0000000..906fe52 --- /dev/null +++ b/app/components/ui/SettingsButton.tsx @@ -0,0 +1,18 @@ +import { memo } from 'react'; +import { IconButton } from './IconButton'; + +interface SettingsButtonProps { + onClick: () => void; +} + +export const SettingsButton = memo(({ onClick }: SettingsButtonProps) => { + return ( + + ); +}); diff --git a/app/components/ui/SettingsSlider.tsx b/app/components/ui/SettingsSlider.tsx new file mode 100644 index 0000000..1a5d410 --- /dev/null +++ b/app/components/ui/SettingsSlider.tsx @@ -0,0 +1,62 @@ +import { motion } from 'framer-motion'; +import { memo } from 'react'; +import { classNames } from '~/utils/classNames'; + +interface SliderOption { + value: T; + text: string; +} + +export interface SliderOptions { + left: SliderOption; + right: SliderOption; +} + +interface SettingsSliderProps { + selected: T; + options: SliderOptions; + setSelected?: (selected: T) => void; +} + +export const SettingsSlider = memo(({ selected, options, setSelected }: SettingsSliderProps) => { + const isLeftSelected = selected === options.left.value; + + return ( +
+ + + +
+ ); +}); diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index 5e48bfe..7106cfb 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -15,10 +15,33 @@ export interface Shortcuts { toggleTerminal: Shortcut; } +export interface Provider { + name: string; + isEnabled: boolean; +} + export interface Settings { shortcuts: Shortcuts; + providers: Provider[]; } +export const providersList: Provider[] = [ + { name: 'Groq', isEnabled: false }, + { name: 'HuggingFace', isEnabled: false }, + { name: 'OpenAI', isEnabled: false }, + { name: 'Anthropic', isEnabled: false }, + { name: 'OpenRouter', isEnabled: false }, + { name: 'Google', isEnabled: false }, + { name: 'Ollama', isEnabled: false }, + { name: 'OpenAILike', isEnabled: false }, + { name: 'Together', isEnabled: false }, + { name: 'Deepseek', isEnabled: false }, + { name: 'Mistral', isEnabled: false }, + { name: 'Cohere', isEnabled: false }, + { name: 'LMStudio', isEnabled: false }, + { name: 'xAI', isEnabled: false }, +]; + export const shortcutsStore = map({ toggleTerminal: { key: 'j', @@ -29,6 +52,7 @@ export const shortcutsStore = map({ export const settingsStore = map({ shortcuts: shortcutsStore.get(), + providers: providersList, }); shortcutsStore.subscribe((shortcuts) => { diff --git a/app/types/model.ts b/app/types/model.ts index 29bff2e..c6c58d7 100644 --- a/app/types/model.ts +++ b/app/types/model.ts @@ -7,4 +7,5 @@ export type ProviderInfo = { getApiKeyLink?: string; labelForGetApiKey?: string; icon?: string; + isEnabled?: boolean; };