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

Closed
Closed
Show file tree
Hide file tree
Changes from 12 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
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)'}
thecodacus marked this conversation as resolved.
Show resolved Hide resolved
</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;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some formatting artifacts. Please lint:fix the source to keep the diff small as possible

}

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>({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making it an interface 👍

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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about moving this whole piece of logic into a separate file in components/tools and to have only a single-line-modification here?

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">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another candidate for a separate file

<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
52 changes: 43 additions & 9 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,20 +110,34 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
},
initialMessages,
});
const waitForLoading = useWaitForLoading(isLoading);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also actually independent from this PR, isn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, this is needed cause, once the tool call is done I need a way to push the result of the tool call into the conversation to provide it as context for the llm ( its hidden in the UI ).

but you can only push anything once the loading is done. so this was implemented for that reason


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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another candidate for a separate file

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) {
// filter out tool responses as it will be automatically added by the runner
storeMessageHistory(messages).catch((error) => toast.error(error.message));
}
}, [messages, isLoading, parseMessages]);
Expand Down Expand Up @@ -164,7 +184,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 +216,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 +234,19 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp

textareaRef.current?.blur();
};
useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also actually independent from this PR, isn't it?

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 +304,7 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
scrollTextArea();
},
model,
provider,
provider.name,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also actually independent from this PR, isn't it?

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')))
thecodacus marked this conversation as resolved.
Show resolved Hide resolved
.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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think this enhancement is big enough to justify an own folder components/tools

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