Skip to content
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
22 changes: 17 additions & 5 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';

const EXAMPLE_PROMPTS = [
{ text: 'Build a todo app in React using Tailwind' },
Expand Down Expand Up @@ -91,6 +92,9 @@ interface BaseChatProps {
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
enhancePrompt?: () => void;
addToolResult?: ({ toolCallId, result }: { toolCallId: string; result: any }) => void;
toolConfig?: IToolsConfig;
onToolConfigChange?: (val: IToolsConfig) => void;
}

export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
Expand All @@ -114,6 +118,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
handleInputChange,
enhancePrompt,
handleStop,
addToolResult,
toolConfig,
onToolConfigChange,
},
ref,
) => {
Expand Down Expand Up @@ -188,6 +195,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
messages={messages}
isStreaming={isStreaming}
addToolResult={addToolResult}
/>
) : null;
}}
Expand All @@ -205,11 +213,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
setProvider={setProvider}
providerList={providerList}
/>
<APIKeyManager
provider={provider}
apiKey={apiKeys[provider] || ''}
setApiKey={(key) => updateApiKey(provider, key)}
/>
<div className="flex justify-between">
<ToolManager toolConfig={toolConfig} onConfigChange={onToolConfigChange} />

<APIKeyManager
provider={provider}
apiKey={apiKeys[provider] || ''}
setApiKey={(key) => updateApiKey(provider, 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
48 changes: 37 additions & 11 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 { JSONValue, Message } from 'ai';
import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
Expand All @@ -16,6 +14,7 @@ import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';
import Cookies from 'js-cookie';
import { toolStore } from '~/lib/stores/tool';

const toastAnimation = cssTransition({
enter: 'animated fadeInRight',
Expand Down Expand Up @@ -85,15 +84,19 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp

const { showChat } = useStore(chatStore);

const toolConfig = useStore(toolStore.config);

const [animationScope, animate] = useAnimate();

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

const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
const { messages, isLoading, input, handleInputChange, setInput, stop, append, addToolResult } = useChat({
api: '/api/chat',
body: {
apiKeys
apiKeys,
toolEnabled: toolConfig.enabled,
},
maxSteps: 3,
onError: (error) => {
logger.error('Request failed\n\n', error);
toast.error('There was an error processing your request');
Expand All @@ -102,8 +105,24 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
logger.debug('Finished streaming');
},
initialMessages,
onToolCall: async ({ toolCall }) => {
if (toolCall.toolName == 'askForConfirmation') return;
logger.debug('Calling Tool:', toolCall);
try {
let result = await toolStore.handleToolCall({
toolName: toolCall.toolName,
args: toolCall.args,
toolCallId: toolCall.toolCallId,
});
logger.info('Tool Call Complete', toolCall.toolName, `${result}`.split('---')[0]);
return result;
} catch (error) {
logger.error('Error calling tool:', toolCall.toolName, error);
toast.error('There was an error processing your request');
return 'Error calling tool';
}
},
});

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

Expand Down Expand Up @@ -163,7 +182,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?: JSONValue[]) => {
const _input = messageInput || input;

if (_input.length === 0 || isLoading) {
Expand Down Expand Up @@ -195,15 +214,19 @@ 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}]\n\n${diff}\n\n${_input}` });
append({
role: 'user',
content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${diff}\n\n${_input}`,
annotations,
});

/**
* After sending a new message we reset all modifications since the model
* should now be aware of all the changes.
*/
workbenchStore.resetAllFileModifications();
} else {
append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${_input}` });
append({ role: 'user', content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n${_input}`, annotations });
}

setInput('');
Expand Down Expand Up @@ -251,6 +274,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
scrollRef={scrollRef}
handleInputChange={handleInputChange}
handleStop={abort}
addToolResult={addToolResult}
toolConfig={toolConfig}
onToolConfigChange={(config) => toolStore.setConfig(config)}
messages={messages.map((message, i) => {
if (message.role === 'user') {
return message;
Expand All @@ -263,14 +289,14 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
})}
enhancePrompt={() => {
enhancePrompt(
input,
input,
(input) => {
setInput(input);
scrollTextArea();
},
model,
provider,
apiKeys
apiKeys,
);
}}
/>
Expand Down
45 changes: 40 additions & 5 deletions app/components/chat/Messages.client.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { Message } from 'ai';
import React from 'react';
import type { JSONValue, Message, ToolInvocation } from 'ai';
import React, { Fragment } from 'react';
import { classNames } from '~/utils/classNames';
import { AssistantMessage } from './AssistantMessage';
import { UserMessage } from './UserMessage';
import { ToolMessage } from './ToolMessage';

interface MessagesProps {
id?: string;
className?: string;
isStreaming?: boolean;
messages?: Message[];
addToolResult?: ({ toolCallId, result }: { toolCallId: string; result: any }) => void;
}

export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props: MessagesProps, ref) => {
const { id, isStreaming = false, messages = [] } = props;
const { id, isStreaming = false, messages = [], addToolResult } = props;

return (
<div id={id} ref={ref} className={props.className}>
Expand All @@ -23,6 +25,23 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
const isFirst = index === 0;
const isLast = index === messages.length - 1;

// checking for annotated message marked as "hidden"
if (message.annotations) {
let isHidden = message.annotations.find((annotation: JSONValue) => {
if (typeof annotation !== 'object' || typeof annotation?.length === 'number') return false;
let object = annotation as any;
return object.visibility === 'hidden';
});
if (isHidden) return <Fragment key={index}></Fragment>;
}
//hide confirmation message that has been answered
if (
message.toolInvocations?.length == 1 &&
message.toolInvocations[0].toolName === 'askForConfirmation' &&
(message.toolInvocations[0] as any).result
) {
return <Fragment key={index}></Fragment>;
}
return (
<div
key={index}
Expand All @@ -33,13 +52,29 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
'mt-4': !isFirst,
})}
>
{isUserMessage && (
{role === 'user' && (
<div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
<div className="i-ph:user-fill text-xl"></div>
</div>
)}
<div className="grid grid-col-1 w-full">
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
{role === 'user' ? (
<UserMessage content={content} />
) : (
<>
{message.toolInvocations?.map((toolInvocation: ToolInvocation) => {
return (
<ToolMessage
key={toolInvocation.toolCallId}
content={toolInvocation}
data={message.data}
addToolResult={addToolResult}
/>
);
})}
<AssistantMessage content={content} />
</>
)}
</div>
</div>
);
Expand Down
30 changes: 30 additions & 0 deletions app/components/chat/ToolManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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>
<ToggleSwitch
checked={toolConfig.enabled}
onCheckedChange={(e: boolean) => {
onConfigChange?.({
enabled: e,
config: toolConfig.config,
});
}}
/>
</div>
</div>
)}
</>
);
}
Loading