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

added AzureOpenAI provider and fixed ModelSelector issues #675

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ HuggingFace_API_KEY=
# You only need this environment variable set if you want to use GPT models
OPENAI_API_KEY=

# Get your Azure Open AI API Key by following these instructions -
# https://docs.microsoft.com/en-us/azure/cognitive-services/openai/quickstarts
# You only need this environment variable set if you want to use Azure OpenAI models
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_API_BASE_URL=
AZURE_OPENAI_RESOURCE_NAME=
AZURE_OPENAI_API_VERSION=

# Get your Anthropic API Key in your account settings -
# https://console.anthropic.com/settings/keys
# You only need this environment variable set if you want to use Claude models
Expand Down Expand Up @@ -74,7 +82,7 @@ XAI_API_KEY=
VITE_LOG_LEVEL=debug

# Example Context Values for qwen2.5-coder:32b
#
#
# DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM
# DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM
# DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM
Expand Down
18 changes: 15 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ ENV WRANGLER_SEND_METRICS=false \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \
AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY} \
AZURE_OPENAI_API_BASE_URL=${AZURE_OPENAI_API_BASE_URL} \
AZURE_OPENAI_RESOURCE_NAME=${AZURE_OPENAI_RESOURCE_NAME} \
AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}

# Pre-configure wrangler to disable metrics
RUN mkdir -p /root/.config/.wrangler && \
Expand All @@ -56,8 +60,12 @@ FROM base AS bolt-ai-development

# Define the same environment variables for development
ARG GROQ_API_KEY
ARG HuggingFace
ARG HuggingFace
ARG OPENAI_API_KEY
ARG AZURE_OPENAI_API_KEY
ARG AZURE_OPENAI_API_BASE_URL
ARG AZURE_OPENAI_RESOURCE_NAME
ARG AZURE_OPENAI_API_VERSION
ARG ANTHROPIC_API_KEY
ARG OPEN_ROUTER_API_KEY
ARG GOOGLE_GENERATIVE_AI_API_KEY
Expand All @@ -77,7 +85,11 @@ ENV GROQ_API_KEY=${GROQ_API_KEY} \
TOGETHER_API_KEY=${TOGETHER_API_KEY} \
TOGETHER_API_BASE_URL=${TOGETHER_API_BASE_URL} \
VITE_LOG_LEVEL=${VITE_LOG_LEVEL} \
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX}
DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX} \
AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY} \
AZURE_OPENAI_API_BASE_URL=${AZURE_OPENAI_API_BASE_URL} \
AZURE_OPENAI_RESOURCE_NAME=${AZURE_OPENAI_RESOURCE_NAME} \
AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION}

RUN mkdir -p ${WORKDIR}/run
CMD pnpm run dev --host
28 changes: 25 additions & 3 deletions app/components/chat/BaseChat.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
opacity: 0;
}
}

&[data-chat-started='true'] {
--textarea-max-height: 400px;
}

&[data-chat-started='false'] {
--textarea-max-height: 200px;
}
}

.Chat {
Expand All @@ -31,9 +39,10 @@
.PromptEffectLine {
width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width));
x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2);
rx: calc(8px - var(--prompt-line-stroke-width));
transform: translate(
calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2),
calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2)
);
fill: transparent;
stroke-width: var(--prompt-line-stroke-width);
stroke: url(#line-gradient);
Expand All @@ -45,3 +54,16 @@
fill: url(#shine-gradient);
mix-blend-mode: overlay;
}

.chatTextarea {
min-height: 76px;
max-height: var(--textarea-max-height, 400px);

&[data-drag-active="true"] {
border: 2px solid #1488fc;
}

&[data-drag-active="false"] {
border: 1px solid var(--bolt-elements-borderColor);
}
}
25 changes: 13 additions & 12 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ import { ModelSelector } from '~/components/chat/ModelSelector';
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import type { IProviderSetting, ProviderInfo } from '~/types/model';

const TEXTAREA_MIN_HEIGHT = 76;

interface BaseChatProps {
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
messageRef?: RefCallback<HTMLDivElement> | undefined;
Expand Down Expand Up @@ -89,7 +87,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
},
ref,
) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>(() => {
const savedKeys = Cookies.get('apiKeys');

Expand Down Expand Up @@ -278,6 +275,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
ref={ref}
className={classNames(styles.BaseChat, 'relative flex h-full w-full overflow-hidden')}
data-chat-visible={showChat}
data-chat-started={chatStarted}
>
<ClientOnly>{() => <Menu />}</ClientOnly>
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
Expand Down Expand Up @@ -340,7 +338,12 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
</linearGradient>
</defs>
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
<rect
className={classNames(styles.PromptEffectLine)}
pathLength="100"
strokeLinecap="round"
rx={8}
></rect>
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
</svg>
<div>
Expand Down Expand Up @@ -387,22 +390,23 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
'w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
'transition-all duration-200',
'hover:border-bolt-elements-focus',
styles.chatTextarea,
)}
onDragEnter={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
e.currentTarget.dataset.dragActive = 'true';
}}
onDragOver={(e) => {
e.preventDefault();
e.currentTarget.style.border = '2px solid #1488fc';
e.currentTarget.dataset.dragActive = 'true';
}}
onDragLeave={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
e.currentTarget.dataset.dragActive = 'false';
}}
onDrop={(e) => {
e.preventDefault();
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
e.currentTarget.dataset.dragActive = 'false';

const files = Array.from(e.dataTransfer.files);
files.forEach((file) => {
Expand All @@ -418,6 +422,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}
});
}}
data-drag-active="false"
onKeyDown={(event) => {
if (event.key === 'Enter') {
if (event.shiftKey) {
Expand All @@ -439,10 +444,6 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleInputChange?.(event);
}}
onPaste={handlePaste}
style={{
minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT,
}}
placeholder="How can Bolt help you today?"
translate="no"
/>
Expand Down
7 changes: 6 additions & 1 deletion app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export function Chat() {
<ToastContainer
closeButton={({ closeToast }) => {
return (
<button className="Toastify__close-button" onClick={closeToast}>
<button
className="Toastify__close-button"
onClick={closeToast}
title="Close"
aria-label="Close notification"
>
<div className="i-ph:x text-lg" />
</button>
);
Expand Down
80 changes: 23 additions & 57 deletions app/components/chat/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ProviderInfo } from '~/types/model';
import type { ModelInfo } from '~/utils/types';
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { useEffect } from 'react';
import { useSettings } from '~/lib/hooks/useSettings';

interface ModelSelectorProps {
model?: string;
Expand All @@ -19,65 +19,29 @@ export const ModelSelector = ({
provider,
setProvider,
modelList,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
providerList,
}: ModelSelectorProps) => {
// Load enabled providers from cookies
const [enabledProviders, setEnabledProviders] = useState(() => {
const savedProviders = Cookies.get('providers');
const { activeProviders } = useSettings();

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);
// If current provider is disabled or not in active providers, switch to first active provider
if ((provider && !activeProviders.find((p) => p.name === provider.name)) || !provider) {
if (activeProviders.length > 0) {
const firstEnabledProvider = activeProviders[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);
// 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);
if (firstModel) {
setModel?.(firstModel.name);
}
}
};

// 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]);
}
}, [activeProviders, provider, setProvider, modelList, setModel]);

if (enabledProviders.length === 0) {
if (activeProviders.length === 0) {
return (
<div className="mb-2 p-4 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary">
<p className="text-center">
Expand All @@ -91,36 +55,38 @@ export const ModelSelector = ({
return (
<div className="mb-2 flex gap-2 flex-col sm:flex-row">
<select
aria-label="Select AI Provider"
value={provider?.name ?? ''}
onChange={(e) => {
const newProvider = enabledProviders.find((p: ProviderInfo) => p.name === e.target.value);
const newProvider = activeProviders.find((p: ProviderInfo) => p.name === e.target.value);

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"
>
{enabledProviders.map((provider: ProviderInfo) => (
{activeProviders.map((provider: ProviderInfo) => (
<option key={provider.name} value={provider.name}>
{provider.name}
</option>
))}
</select>
<select
aria-label="Select AI Model"
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)
{modelList
.filter((m) => m.provider === provider?.name)
.map((modelOption, index) => (
<option key={index} value={modelOption.name}>
{modelOption.label}
Expand Down
7 changes: 7 additions & 0 deletions app/components/workbench/FileTree.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.nodeButton {
padding-left: calc(6px + var(--node-depth) * 16px);
}

:global([data-node-depth]) {
--node-depth: attr(data-node-depth number);
}
6 changes: 4 additions & 2 deletions app/components/workbench/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react';
import type { FileMap } from '~/lib/stores/files';
import { classNames } from '~/utils/classNames';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import styles from './FileTree.module.scss';

const logger = createScopedLogger('FileTree');

const NODE_PADDING_LEFT = 8;
const DEFAULT_HIDDEN_FILES = [/\/node_modules\//, /\/\.next/, /\/\.astro/];

interface Props {
Expand Down Expand Up @@ -222,11 +222,13 @@ interface ButtonProps {
function NodeButton({ depth, iconClasses, onClick, className, children }: ButtonProps) {
return (
<button
type="button"
className={classNames(
'flex items-center gap-1.5 w-full pr-2 border-2 border-transparent text-faded py-0.5',
styles.nodeButton,
className,
)}
style={{ paddingLeft: `${6 + depth * NODE_PADDING_LEFT}px` }}
data-node-depth={depth}
onClick={() => onClick?.()}
>
<div className={classNames('scale-120 shrink-0', iconClasses)}></div>
Expand Down
Loading