Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added debug tab #562

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/components/chat/APIKeyManager.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { IconButton } from '~/components/ui/IconButton';
import type { ProviderInfo } from '~/types/model';
import { apiSettingsStore } from '~/lib/stores/settings';
import { useStore } from '@nanostores/react';

interface APIKeyManagerProps {
provider: ProviderInfo;
Expand All @@ -14,6 +16,17 @@ interface APIKeyManagerProps {
export const APIKeyManager: React.FC<APIKeyManagerProps> = ({ provider, apiKey, setApiKey }) => {
const [isEditing, setIsEditing] = useState(false);
const [tempKey, setTempKey] = useState(apiKey);
const storedSettings = useStore(apiSettingsStore);

useEffect(() => {
// Update the API key if it exists in the store
const storedKey = storedSettings.apiKeys[provider.name];

if (storedKey) {
setApiKey(storedKey);
setTempKey(storedKey);
}
}, [storedSettings, provider.name, setApiKey]);

const handleSave = () => {
setApiKey(tempKey);
Expand Down
64 changes: 11 additions & 53 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { classNames } from '~/utils/classNames';
import { MODEL_LIST, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client';
import { APIKeyManager } from './APIKeyManager';
import Cookies from 'js-cookie';
import * as Tooltip from '@radix-ui/react-tooltip';

import styles from './BaseChat.module.scss';
Expand Down Expand Up @@ -54,6 +52,7 @@ interface BaseChatProps {
setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[];
setImageDataList?: (dataList: string[]) => void;
availableProviders?: ProviderInfo[];
}

export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
Expand Down Expand Up @@ -83,11 +82,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
imageDataList = [],
setImageDataList,
messages,
availableProviders = PROVIDER_LIST,
},
ref,
) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const [modelList, setModelList] = useState(MODEL_LIST);
const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false);
const [isListening, setIsListening] = useState(false);
Expand All @@ -96,24 +95,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(

console.log(transcript);
useEffect(() => {
// Load API keys from cookies on component mount
try {
const storedApiKeys = Cookies.get('apiKeys');

if (storedApiKeys) {
const parsedKeys = JSON.parse(storedApiKeys);

if (typeof parsedKeys === 'object' && parsedKeys !== null) {
setApiKeys(parsedKeys);
}
}
} catch (error) {
console.error('Error loading API keys from cookies:', error);

// Clear invalid cookie data
Cookies.remove('apiKeys');
}

initializeModelList().then((modelList) => {
setModelList(modelList);
});
Expand Down Expand Up @@ -183,23 +164,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}
};

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';
Expand Down Expand Up @@ -349,23 +313,17 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div>

<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
<ModelSelector
key={provider?.name + ':' + modelList.length}
model={model}
setModel={setModel}
modelList={modelList}
provider={provider}
setProvider={setProvider}
providerList={PROVIDER_LIST}
apiKeys={apiKeys}
/>
{provider && (
<APIKeyManager
<div className="flex gap-2">
<ModelSelector
key={provider?.name + ':' + modelList.length}
model={model}
setModel={setModel}
modelList={modelList}
provider={provider}
apiKey={apiKeys[provider.name] || ''}
setApiKey={(key) => updateApiKey(provider.name, key)}
setProvider={setProvider}
providerList={availableProviders}
/>
)}
</div>
</div>
</div>
<FilePreview
Expand Down
42 changes: 33 additions & 9 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,25 @@ export const ChatImpl = memo(
});
const [provider, setProvider] = useState(() => {
const savedProvider = Cookies.get('selectedProvider');
return PROVIDER_LIST.find((p) => p.name === savedProvider) || DEFAULT_PROVIDER;
const savedActiveProviders = Cookies.get('activeProviders');
const activeProviders = savedActiveProviders ? JSON.parse(savedActiveProviders) : {};

// Filter PROVIDER_LIST to only include active providers and Ollama (which is always available)
const availableProviders = PROVIDER_LIST.filter(
(p) =>
p.name === 'Ollama' || // Ollama is always available
activeProviders[p.name], // Provider is active in settings
);

// If no providers are available, default to Ollama
if (availableProviders.length === 0) {
return PROVIDER_LIST.find((p) => p.name === 'Ollama') || DEFAULT_PROVIDER;
}

// Try to find the saved provider in available providers
const savedProviderObj = availableProviders.find((p) => p.name === savedProvider);

return savedProviderObj || availableProviders[0] || DEFAULT_PROVIDER;
});

const { showChat } = useStore(chatStore);
Expand All @@ -107,6 +125,15 @@ export const ChatImpl = memo(

const [apiKeys, setApiKeys] = useState<Record<string, string>>({});

// Load API keys from cookies when component mounts
useEffect(() => {
const savedApiKeys = Cookies.get('apiKeys');

if (savedApiKeys) {
setApiKeys(JSON.parse(savedApiKeys));
}
}, []);

const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
api: '/api/chat',
body: {
Expand Down Expand Up @@ -283,14 +310,6 @@ export const ChatImpl = memo(

const [messageRef, scrollRef] = useSnapScroll();

useEffect(() => {
const storedApiKeys = Cookies.get('apiKeys');

if (storedApiKeys) {
setApiKeys(JSON.parse(storedApiKeys));
}
}, []);

const handleModelChange = (newModel: string) => {
setModel(newModel);
Cookies.set('selectedModel', newModel, { expires: 30 });
Expand Down Expand Up @@ -352,6 +371,11 @@ export const ChatImpl = memo(
setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList}
setImageDataList={setImageDataList}
availableProviders={PROVIDER_LIST.filter(
(p) =>
p.name === 'Ollama' || // Ollama is always available
JSON.parse(Cookies.get('activeProviders') || '{}')[p.name], // Provider is active in settings
)}
/>
);
},
Expand Down
85 changes: 51 additions & 34 deletions app/components/chat/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ProviderInfo } from '~/types/model';
import type { ModelInfo } from '~/utils/types';
import { classNames } from '~/utils/classNames';

interface ModelSelectorProps {
model?: string;
Expand All @@ -8,7 +9,6 @@ interface ModelSelectorProps {
setProvider?: (provider: ProviderInfo) => void;
modelList: ModelInfo[];
providerList: ProviderInfo[];
apiKeys: Record<string, string>;
}

export const ModelSelector = ({
Expand All @@ -20,44 +20,61 @@ export const ModelSelector = ({
providerList,
}: ModelSelectorProps) => {
return (
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
<select
value={provider?.name ?? ''}
onChange={(e) => {
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
<div className="mb-4 flex gap-4 flex-row w-full">
<div className="space-y-2 flex-1">
<label className="text-sm font-medium leading-none text-bolt-elements-textPrimary">Provider</label>
<select
value={provider?.name ?? ''}
onChange={(e) => {
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);

if (newProvider && setProvider) {
setProvider(newProvider);
}
if (newProvider && setProvider) {
setProvider(newProvider);
}

const firstModel = [...modelList].find((m) => m.provider === e.target.value);
const firstModel = [...modelList].find((m) => m.provider === e.target.value);

if (firstModel && setModel) {
setModel(firstModel.name);
}
}}
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"
>
{providerList.map((provider: ProviderInfo) => (
<option key={provider.name} value={provider.name}>
{provider.name}
</option>
))}
</select>
<select
key={provider?.name}
value={model}
onChange={(e) => setModel?.(e.target.value)}
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 lg:max-w-[70%]"
>
{[...modelList]
.filter((e) => e.provider == provider?.name && e.name)
.map((modelOption) => (
<option key={modelOption.name} value={modelOption.name}>
{modelOption.label}
if (firstModel && setModel) {
setModel(firstModel.name);
}
}}
className={classNames(
'flex h-10 w-full rounded-md border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1',
'px-3 py-2 text-sm text-bolt-elements-textPrimary ring-offset-background',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
>
{providerList.map((provider: ProviderInfo) => (
<option key={provider.name} value={provider.name}>
{provider.name}
</option>
))}
</select>
</select>
</div>

<div className="space-y-2 flex-1">
<label className="text-sm font-medium leading-none text-bolt-elements-textPrimary">Model</label>
<select
key={provider?.name}
value={model}
onChange={(e) => setModel?.(e.target.value)}
className={classNames(
'flex h-10 w-full rounded-md border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1',
'px-3 py-2 text-sm text-bolt-elements-textPrimary ring-offset-background',
'focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
>
{[...modelList]
.filter((e) => e.provider == provider?.name && e.name)
.map((modelOption) => (
<option key={modelOption.name} value={modelOption.name}>
{modelOption.label}
</option>
))}
</select>
</div>
</div>
);
};
4 changes: 2 additions & 2 deletions app/components/sidebar/Menu.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { motion, type Variants } from 'framer-motion';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
import { ThemeAndSettings } from '~/components/ui/ThemeAndSettings';
import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
import { cubicEasingFn } from '~/utils/easings';
import { logger } from '~/utils/logger';
Expand Down Expand Up @@ -201,7 +201,7 @@ export const Menu = () => {
</DialogRoot>
</div>
<div className="flex items-center border-t border-bolt-elements-borderColor p-4">
<ThemeSwitch className="ml-auto" />
<ThemeAndSettings className="ml-auto" />
</div>
</div>
</motion.div>
Expand Down
Loading
Loading