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(code-template): Add Code Template System and Tool Calling Infrastructure #302

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion app/components/chat/APIKeyManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const APIKeyManager: React.FC<APIKeyManagerProps> = ({
) : (
<>
<span className="flex-1 text-sm text-bolt-elements-textPrimary">
{apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'}
{apiKey ? '••••••••' : 'Not set (works via .env)'}
</span>
<IconButton onClick={() => setIsEditing(true)} title="Edit API Key">
<div className="i-ph:pencil-simple" />
Expand Down
6 changes: 5 additions & 1 deletion app/components/chat/Artifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
const artifact = artifacts[messageId];

const actions = useStore(
computed(artifact.runner.actions, (actions) => {
computed(artifact?.runner?.actions, (actions) => {
return Object.values(actions);
}),
);
Expand Down Expand Up @@ -187,6 +187,10 @@ const ActionList = memo(({ actions }: ActionListProps) => {
>
<span className="flex-1">Start Application</span>
</a>
) : type == 'tool' ? (
<div className="flex items-center w-full min-h-[28px]">
<span className="flex-1">{action.toolName}</span>
</div>
) : null}
</div>
{(type === 'shell' || type === 'start') && (
Expand Down
41 changes: 38 additions & 3 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { APIKeyManager } from './APIKeyManager';
import Cookies from 'js-cookie';

import styles from './BaseChat.module.scss';
import { ToolManager } from './ToolManager';
import type { ProviderInfo } from '~/utils/types';

const EXAMPLE_PROMPTS = [
Expand Down Expand Up @@ -85,6 +86,7 @@ interface BaseChatProps {
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
enhancePrompt?: () => void;

}

export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
Expand Down Expand Up @@ -113,8 +115,13 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
const [toolConfig, setToolConfig] = useState<IToolsConfig>({
enabled: false,
config: {}
});

const [modelList, setModelList] = useState(MODEL_LIST);

useEffect(() => {
// Load API keys from cookies on component mount
try {
Expand All @@ -136,6 +143,25 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
});
}, []);

useEffect(() => {
const config = Cookies.get('bolt.toolsConfig');
if (config) {
try {
const parsedConfig = JSON.parse(config);
setToolConfig(parsedConfig);
} catch (error) {
console.error('Error parsing tools config:', error);
// Clear invalid cookie data
Cookies.remove('bolt.toolsConfig');
}
}
else{
Cookies.set('bolt.toolsConfig', JSON.stringify(toolConfig), {
path: '/', // Accessible across the site
});
}
}, []);

const updateApiKey = (provider: string, key: string) => {
try {
const updatedApiKeys = { ...apiKeys, [provider]: key };
Expand Down Expand Up @@ -203,15 +229,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
modelList={modelList}
provider={provider}
setProvider={setProvider}
providerList={PROVIDER_LIST}

providerList={providerList}
/>
{provider && (
<div className="flex justify-between items-center">
<ToolManager toolConfig={toolConfig} onConfigChange={(config) => {
setToolConfig(config)
Cookies.set('bolt.toolsConfig', JSON.stringify(config), {
path: '/', // Accessible across the site
});
}} />
{provider && (
<APIKeyManager
provider={provider}
apiKey={apiKeys[provider.name] || ''}
setApiKey={(key) => updateApiKey(provider.name, key)}
/>
)}
</div>
<div
className={classNames(
'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
Expand Down
55 changes: 45 additions & 10 deletions app/components/chat/Chat.client.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// @ts-nocheck
// Preventing TS checks with files presented in the video for a better presentation.
import { useStore } from '@nanostores/react';
import type { Message } from 'ai';
import type { ChatRequestOptions, CreateMessage, Message } from 'ai';
import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
Expand All @@ -16,7 +14,8 @@ import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import Cookies from 'js-cookie';
import type { ProviderInfo } from '~/utils/types';
import { useWaitForLoading } from '~/lib/hooks/useWaitForLoading';
import type { IToolsConfig, ProviderInfo } from '~/utils/types';

const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
Expand Down Expand Up @@ -90,10 +89,17 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp

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

const [toolConfig, setToolConfig] = useState<IToolsConfig>({
enabled: false,
config: {}
});


const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
api: '/api/chat',
body: {
apiKeys
apiKeys,
toolConfig
},
onError: (error) => {
logger.error('Request failed\n\n', error);
Expand All @@ -104,21 +110,36 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
},
initialMessages,
});
const waitForLoading = useWaitForLoading(isLoading);

const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
const { parsedMessages, parseMessages } = useMessageParser();

const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;


useEffect(() => {
chatStore.setKey('started', initialMessages.length > 0);
let toolConfig=Cookies.get('bolt.toolsConfig');
if (toolConfig) {
try {
const parsedConfig = JSON.parse(toolConfig);
setToolConfig(parsedConfig);
} catch (error) {
console.error('Error parsing tools config:', error);
// Clear invalid cookie data
Cookies.remove('bolt.toolsConfig');
}
}
}, []);

useEffect(() => {
parseMessages(messages, isLoading);

if (messages.length > initialMessages.length) {
storeMessageHistory(messages).catch((error) => toast.error(error.message));
// filter out tool responses as it will be automatically added by the runner
let filteredMessages = messages.filter((message) => !message.annotations?.find((a) => a === 'toolResponse'));
storeMessageHistory(filteredMessages).catch((error) => toast.error(error.message));
}
}, [messages, isLoading, parseMessages]);

Expand Down Expand Up @@ -164,7 +185,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
setChatStarted(true);
};

const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
const sendMessage = async (_event: React.UIEvent, messageInput?: string,annotations?:string[]) => {
const _input = messageInput || input;

if (_input.length === 0 || isLoading) {
Expand Down Expand Up @@ -196,7 +217,8 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
* manually reset the input and we'd have to manually pass in file attachments. However, those
* aren't relevant here.
*/
append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${diff}\n\n${_input}` });

append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${diff}\n\n${_input}`,annotations });

/**
* After sending a new message we reset all modifications since the model
Expand All @@ -213,6 +235,19 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp

textareaRef.current?.blur();
};
useEffect(() => {
workbenchStore.addChatMessage = async(message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions) => {
await waitForLoading();
return await append(
{
...message,
content: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${message.content}`,
},
chatRequestOptions,
);
}
}, [append]);


const [messageRef, scrollRef] = useSnapScroll();

Expand Down Expand Up @@ -270,7 +305,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
scrollTextArea();
},
model,
provider,
provider.name,
apiKeys
);
}}
Expand Down
13 changes: 8 additions & 5 deletions app/components/chat/Messages.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
return (
<div id={id} ref={ref} className={props.className}>
{messages.length > 0
? messages.map((message, index) => {
const { role, content } = message;
const isUserMessage = role === 'user';
const isFirst = index === 0;
const isLast = index === messages.length - 1;
? messages
// filter out tool responses
.filter((message) => !(message.annotations?.find((a) => a === 'toolResponse')))
.map((message, index) => {
const { role, content } = message;
const isUserMessage = role === 'user';
const isFirst = index === 0;
const isLast = index === messages.length - 1;

return (
<div
Expand Down
31 changes: 31 additions & 0 deletions app/components/chat/ToolManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ToggleSwitch } from '../ui/ToggleSwitch';
import type { IToolsConfig } from '~/utils/types';

interface ToolManagerProps {
toolConfig: IToolsConfig;
onConfigChange?: (val: IToolsConfig) => void;
}

export function ToolManager({ toolConfig, onConfigChange }: ToolManagerProps) {
return (
<>
{toolConfig && (
<div className="grid gap-4 text-sm">
<div className="flex items-center gap-2">
<label className="text-sm text-bolt-elements-textSecondary">Tool Calling</label>
{/* <div className="block i-ph:hammer-thin text-sm text-bolt-elements-textSecondary"></div> */}
<ToggleSwitch
checked={toolConfig.enabled}
onCheckedChange={(e: boolean) => {
onConfigChange?.({
enabled: e,
config: toolConfig.config,
});
}}
/>
</div>
</div>
)}
</>
);
}
55 changes: 55 additions & 0 deletions app/components/ui/ToggleSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';

const ToggleSwitch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={[
'peer',
'inline-flex',
'h-4',
'w-9',
'shrink-0',
'cursor-pointer',
'items-center',
'rounded-full',
'border-2',
'border-transparent',
'transition-colors duration-200 bolt-ease-cubic-bezier',
// Focus styles
'focus-visible:(outline-none ring-1)',
// Disabled styles
'disabled:(cursor-not-allowed opacity-50)',
// State styles
'data-[state=checked]:bg-bolt-elements-item-contentAccent',
'data-[state=unchecked]:bg-bolt-elements-button-secondary-background',
'hover:data-[state=unchecked]:bg-bolt-elements-button-secondary-backgroundHover',
className,
]
.filter(Boolean)
.join(' ')}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={[
'pointer-events-none',
'block',
'h-3',
'w-3',
'rounded-full',
'bg-bolt-elements-textPrimary',
'shadow-lg',
'ring-0',
'transition-transform duration-200 bolt-ease-cubic-bezier',
'data-[state=checked]:translate-x-5',
'data-[state=unchecked]:translate-x-0',
].join(' ')}
/>
</SwitchPrimitives.Root>
));
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;

export { ToggleSwitch };
Loading