feat: add discuss mode and quick actions
- Implement discuss mode toggle and UI in chat box - Add quick action buttons for file, message, implement and link actions - Extend markdown parser to handle quick action elements - Update message components to support discuss mode and quick actions - Add discuss prompt for technical consulting responses - Refactor chat components to support new functionality The changes introduce a new discuss mode that allows users to switch between code implementation and technical discussion modes. Quick action buttons provide immediate interaction options like opening files, sending messages, or switching modes.
This commit is contained in:
@@ -184,7 +184,7 @@ const actionVariants = {
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
function openArtifactInWorkbench(filePath: any) {
|
||||
export function openArtifactInWorkbench(filePath: any) {
|
||||
if (workbenchStore.currentView.get() !== 'code') {
|
||||
workbenchStore.currentView.set('code');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Popover from '~/components/ui/Popover';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
import type { Message } from 'ai';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
@@ -12,6 +13,9 @@ interface AssistantMessageProps {
|
||||
messageId?: string;
|
||||
onRewind?: (messageId: string) => void;
|
||||
onFork?: (messageId: string) => void;
|
||||
append?: (message: Message) => void;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
}
|
||||
|
||||
function openArtifactInWorkbench(filePath: string) {
|
||||
@@ -38,104 +42,109 @@ function normalizedFilePath(path: string) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(({ content, annotations, messageId, onRewind, onFork }: AssistantMessageProps) => {
|
||||
const filteredAnnotations = (annotations?.filter(
|
||||
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||
) || []) as { type: string; value: any } & { [key: string]: any }[];
|
||||
export const AssistantMessage = memo(
|
||||
({ content, annotations, messageId, onRewind, onFork, append, chatMode, setChatMode }: AssistantMessageProps) => {
|
||||
const filteredAnnotations = (annotations?.filter(
|
||||
(annotation: JSONValue) =>
|
||||
annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||
) || []) as { type: string; value: any } & { [key: string]: any }[];
|
||||
|
||||
let chatSummary: string | undefined = undefined;
|
||||
let chatSummary: string | undefined = undefined;
|
||||
|
||||
if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
|
||||
chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
|
||||
}
|
||||
if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) {
|
||||
chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary;
|
||||
}
|
||||
|
||||
let codeContext: string[] | undefined = undefined;
|
||||
let codeContext: string[] | undefined = undefined;
|
||||
|
||||
if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) {
|
||||
codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files;
|
||||
}
|
||||
if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) {
|
||||
codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files;
|
||||
}
|
||||
|
||||
const usage: {
|
||||
completionTokens: number;
|
||||
promptTokens: number;
|
||||
totalTokens: number;
|
||||
} = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
|
||||
const usage: {
|
||||
completionTokens: number;
|
||||
promptTokens: number;
|
||||
totalTokens: number;
|
||||
} = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
<>
|
||||
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
|
||||
{(codeContext || chatSummary) && (
|
||||
<Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
|
||||
{chatSummary && (
|
||||
<div className="max-w-chat">
|
||||
<div className="summary max-h-96 flex flex-col">
|
||||
<h2 className="border border-bolt-elements-borderColor rounded-md p4">Summary</h2>
|
||||
<div style={{ zoom: 0.7 }} className="overflow-y-auto m4">
|
||||
<Markdown>{chatSummary}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
{codeContext && (
|
||||
<div className="code-context flex flex-col p4 border border-bolt-elements-borderColor rounded-md">
|
||||
<h2>Context</h2>
|
||||
<div className="flex gap-4 mt-4 bolt" style={{ zoom: 0.6 }}>
|
||||
{codeContext.map((x) => {
|
||||
const normalized = normalizedFilePath(x);
|
||||
return (
|
||||
<Fragment key={normalized}>
|
||||
<code
|
||||
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openArtifactInWorkbench(normalized);
|
||||
}}
|
||||
>
|
||||
{normalized}
|
||||
</code>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
<>
|
||||
<div className=" flex gap-2 items-center text-sm text-bolt-elements-textSecondary mb-2">
|
||||
{(codeContext || chatSummary) && (
|
||||
<Popover side="right" align="start" trigger={<div className="i-ph:info" />}>
|
||||
{chatSummary && (
|
||||
<div className="max-w-chat">
|
||||
<div className="summary max-h-96 flex flex-col">
|
||||
<h2 className="border border-bolt-elements-borderColor rounded-md p4">Summary</h2>
|
||||
<div style={{ zoom: 0.7 }} className="overflow-y-auto m4">
|
||||
<Markdown>{chatSummary}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
{codeContext && (
|
||||
<div className="code-context flex flex-col p4 border border-bolt-elements-borderColor rounded-md">
|
||||
<h2>Context</h2>
|
||||
<div className="flex gap-4 mt-4 bolt" style={{ zoom: 0.6 }}>
|
||||
{codeContext.map((x) => {
|
||||
const normalized = normalizedFilePath(x);
|
||||
return (
|
||||
<Fragment key={normalized}>
|
||||
<code
|
||||
className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md text-bolt-elements-item-contentAccent hover:underline cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openArtifactInWorkbench(normalized);
|
||||
}}
|
||||
>
|
||||
{normalized}
|
||||
</code>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="context"></div>
|
||||
</Popover>
|
||||
)}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{usage && (
|
||||
<div>
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
</div>
|
||||
)}
|
||||
{(onRewind || onFork) && messageId && (
|
||||
<div className="flex gap-2 flex-col lg:flex-row ml-auto">
|
||||
{onRewind && (
|
||||
<WithTooltip tooltip="Revert to this message">
|
||||
<button
|
||||
onClick={() => onRewind(messageId)}
|
||||
key="i-ph:arrow-u-up-left"
|
||||
className="i-ph:arrow-u-up-left text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
|
||||
/>
|
||||
</WithTooltip>
|
||||
)}
|
||||
{onFork && (
|
||||
<WithTooltip tooltip="Fork chat from this message">
|
||||
<button
|
||||
onClick={() => onFork(messageId)}
|
||||
key="i-ph:git-fork"
|
||||
className="i-ph:git-fork text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
|
||||
/>
|
||||
</WithTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="context"></div>
|
||||
</Popover>
|
||||
)}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
{usage && (
|
||||
<div>
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
</div>
|
||||
)}
|
||||
{(onRewind || onFork) && messageId && (
|
||||
<div className="flex gap-2 flex-col lg:flex-row ml-auto">
|
||||
{onRewind && (
|
||||
<WithTooltip tooltip="Revert to this message">
|
||||
<button
|
||||
onClick={() => onRewind(messageId)}
|
||||
key="i-ph:arrow-u-up-left"
|
||||
className="i-ph:arrow-u-up-left text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
|
||||
/>
|
||||
</WithTooltip>
|
||||
)}
|
||||
{onFork && (
|
||||
<WithTooltip tooltip="Fork chat from this message">
|
||||
<button
|
||||
onClick={() => onFork(messageId)}
|
||||
key="i-ph:git-fork"
|
||||
className="i-ph:git-fork text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
|
||||
/>
|
||||
</WithTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<Markdown html>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
</>
|
||||
<Markdown append={append} chatMode={chatMode} setChatMode={setChatMode} html>
|
||||
{content}
|
||||
</Markdown>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,28 +6,18 @@ import type { JSONValue, Message } from 'ai';
|
||||
import React, { type RefCallback, useEffect, useState } from 'react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { Menu } from '~/components/sidebar/Menu.client';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { Workbench } from '~/components/workbench/Workbench.client';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { PROVIDER_LIST } from '~/utils/constants';
|
||||
import { Messages } from './Messages.client';
|
||||
import { SendButton } from './SendButton.client';
|
||||
import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager';
|
||||
import { getApiKeysFromCookies } from './APIKeyManager';
|
||||
import Cookies from 'js-cookie';
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
import styles from './BaseChat.module.scss';
|
||||
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
|
||||
import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons';
|
||||
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
||||
import GitCloneButton from './GitCloneButton';
|
||||
|
||||
import FilePreview from './FilePreview';
|
||||
import { ModelSelector } from '~/components/chat/ModelSelector';
|
||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import StarterTemplates from './StarterTemplates';
|
||||
import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions';
|
||||
import DeployChatAlert from '~/components/deploy/DeployAlert';
|
||||
@@ -36,13 +26,11 @@ import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import ProgressCompilation from './ProgressCompilation';
|
||||
import type { ProgressAnnotation } from '~/types/context';
|
||||
import type { ActionRunner } from '~/lib/runtime/action-runner';
|
||||
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
|
||||
import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert';
|
||||
import { SupabaseConnection } from './SupabaseConnection';
|
||||
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
|
||||
import { expoUrlAtom } from '~/lib/stores/qrCodeStore';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { StickToBottom, useStickToBottomContext } from '~/lib/hooks';
|
||||
import { ChatBox } from './ChatBox';
|
||||
|
||||
const TEXTAREA_MIN_HEIGHT = 76;
|
||||
|
||||
@@ -82,6 +70,9 @@ interface BaseChatProps {
|
||||
clearDeployAlert?: () => void;
|
||||
data?: JSONValue[] | undefined;
|
||||
actionRunner?: ActionRunner;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
append?: (message: Message) => void;
|
||||
}
|
||||
|
||||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
@@ -120,6 +111,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
clearSupabaseAlert,
|
||||
data,
|
||||
actionRunner,
|
||||
chatMode,
|
||||
setChatMode,
|
||||
append,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -362,6 +356,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1"
|
||||
messages={messages}
|
||||
isStreaming={isStreaming}
|
||||
append={append}
|
||||
chatMode={chatMode}
|
||||
setChatMode={setChatMode}
|
||||
/>
|
||||
) : null;
|
||||
}}
|
||||
@@ -406,240 +403,44 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</div>
|
||||
<ScrollToBottom />
|
||||
{progressAnnotations && <ProgressCompilation data={progressAnnotations} />}
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
|
||||
|
||||
/*
|
||||
* {
|
||||
* 'sticky bottom-2': chatStarted,
|
||||
* },
|
||||
*/
|
||||
)}
|
||||
>
|
||||
<svg className={classNames(styles.PromptEffectContainer)}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="line-gradient"
|
||||
x1="20%"
|
||||
y1="0%"
|
||||
x2="-14%"
|
||||
y2="10%"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(-45)"
|
||||
>
|
||||
<stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
|
||||
<stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
|
||||
<stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
|
||||
<stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="shine-gradient">
|
||||
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
|
||||
<stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
|
||||
<stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
|
||||
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
|
||||
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
|
||||
</svg>
|
||||
<div>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<div className={isModelSettingsCollapsed ? 'hidden' : ''}>
|
||||
<ModelSelector
|
||||
key={provider?.name + ':' + modelList.length}
|
||||
model={model}
|
||||
setModel={setModel}
|
||||
modelList={modelList}
|
||||
provider={provider}
|
||||
setProvider={setProvider}
|
||||
providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
|
||||
apiKeys={apiKeys}
|
||||
modelLoading={isModelLoading}
|
||||
/>
|
||||
{(providerList || []).length > 0 &&
|
||||
provider &&
|
||||
(!LOCAL_PROVIDERS.includes(provider.name) || 'OpenAILike') && (
|
||||
<APIKeyManager
|
||||
provider={provider}
|
||||
apiKey={apiKeys[provider.name] || ''}
|
||||
setApiKey={(key) => {
|
||||
onApiKeysChange(provider.name, key);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<FilePreview
|
||||
files={uploadedFiles}
|
||||
imageDataList={imageDataList}
|
||||
onRemove={(index) => {
|
||||
setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
|
||||
setImageDataList?.(imageDataList.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<ScreenshotStateManager
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
setImageDataList={setImageDataList}
|
||||
uploadedFiles={uploadedFiles}
|
||||
imageDataList={imageDataList}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={classNames(
|
||||
'w-full pl-4 pt-4 pr-16 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',
|
||||
)}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
files.forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const base64Image = e.target?.result as string;
|
||||
setUploadedFiles?.([...uploadedFiles, file]);
|
||||
setImageDataList?.([...imageDataList, base64Image]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if using input method engine
|
||||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
value={input}
|
||||
onChange={(event) => {
|
||||
handleInputChange?.(event);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
style={{
|
||||
minHeight: TEXTAREA_MIN_HEIGHT,
|
||||
maxHeight: TEXTAREA_MAX_HEIGHT,
|
||||
}}
|
||||
placeholder="How can Bolt help you today?"
|
||||
translate="no"
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<SendButton
|
||||
show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
|
||||
isStreaming={isStreaming}
|
||||
disabled={!providerList || providerList.length === 0}
|
||||
onClick={(event) => {
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.length > 0 || uploadedFiles.length > 0) {
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
|
||||
<div className="i-ph:paperclip text-xl"></div>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title="Enhance prompt"
|
||||
disabled={input.length === 0 || enhancingPrompt}
|
||||
className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
|
||||
onClick={() => {
|
||||
enhancePrompt?.();
|
||||
toast.success('Prompt enhanced!');
|
||||
}}
|
||||
>
|
||||
{enhancingPrompt ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
|
||||
) : (
|
||||
<div className="i-bolt:stars text-xl"></div>
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<SpeechRecognitionButton
|
||||
isListening={isListening}
|
||||
onStart={startListening}
|
||||
onStop={stopListening}
|
||||
disabled={isStreaming}
|
||||
/>
|
||||
{chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
|
||||
<IconButton
|
||||
title="Model Settings"
|
||||
className={classNames('transition-all flex items-center gap-1', {
|
||||
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
|
||||
isModelSettingsCollapsed,
|
||||
'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
|
||||
!isModelSettingsCollapsed,
|
||||
})}
|
||||
onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
|
||||
disabled={!providerList || providerList.length === 0}
|
||||
>
|
||||
<div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
|
||||
{isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
|
||||
</IconButton>
|
||||
</div>
|
||||
{input.length > 3 ? (
|
||||
<div className="text-xs text-bolt-elements-textTertiary">
|
||||
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
|
||||
+ <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
|
||||
a new line
|
||||
</div>
|
||||
) : null}
|
||||
<SupabaseConnection />
|
||||
<ExpoQrModal open={qrModalOpen} onClose={() => setQrModalOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChatBox
|
||||
isModelSettingsCollapsed={isModelSettingsCollapsed}
|
||||
setIsModelSettingsCollapsed={setIsModelSettingsCollapsed}
|
||||
provider={provider}
|
||||
setProvider={setProvider}
|
||||
providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
|
||||
model={model}
|
||||
setModel={setModel}
|
||||
modelList={modelList}
|
||||
apiKeys={apiKeys}
|
||||
isModelLoading={isModelLoading}
|
||||
onApiKeysChange={onApiKeysChange}
|
||||
uploadedFiles={uploadedFiles}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
imageDataList={imageDataList}
|
||||
setImageDataList={setImageDataList}
|
||||
textareaRef={textareaRef}
|
||||
input={input}
|
||||
handleInputChange={handleInputChange}
|
||||
handlePaste={handlePaste}
|
||||
TEXTAREA_MIN_HEIGHT={TEXTAREA_MIN_HEIGHT}
|
||||
TEXTAREA_MAX_HEIGHT={TEXTAREA_MAX_HEIGHT}
|
||||
isStreaming={isStreaming}
|
||||
handleStop={handleStop}
|
||||
handleSendMessage={handleSendMessage}
|
||||
enhancingPrompt={enhancingPrompt}
|
||||
enhancePrompt={enhancePrompt}
|
||||
isListening={isListening}
|
||||
startListening={startListening}
|
||||
stopListening={stopListening}
|
||||
chatStarted={chatStarted}
|
||||
exportChat={exportChat}
|
||||
qrModalOpen={qrModalOpen}
|
||||
setQrModalOpen={setQrModalOpen}
|
||||
handleFileUpload={handleFileUpload}
|
||||
chatMode={chatMode}
|
||||
setChatMode={setChatMode}
|
||||
/>
|
||||
</div>
|
||||
</StickToBottom>
|
||||
<div className="flex flex-col justify-center">
|
||||
|
||||
@@ -148,6 +148,7 @@ export const ChatImpl = memo(
|
||||
|
||||
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||
|
||||
const [chatMode, setChatMode] = useState<'discuss' | 'build'>('build');
|
||||
const {
|
||||
messages,
|
||||
isLoading,
|
||||
@@ -168,6 +169,7 @@ export const ChatImpl = memo(
|
||||
files,
|
||||
promptId,
|
||||
contextOptimization: contextOptimizationEnabled,
|
||||
chatMode,
|
||||
supabase: {
|
||||
isConnected: supabaseConn.isConnected,
|
||||
hasSelectedProject: !!selectedProject,
|
||||
@@ -564,6 +566,9 @@ export const ChatImpl = memo(
|
||||
deployAlert={deployAlert}
|
||||
clearDeployAlert={() => workbenchStore.clearDeployAlert()}
|
||||
data={chatData}
|
||||
chatMode={chatMode}
|
||||
setChatMode={setChatMode}
|
||||
append={append}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
310
app/components/chat/ChatBox.tsx
Normal file
310
app/components/chat/ChatBox.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import React from 'react';
|
||||
import { ClientOnly } from 'remix-utils/client-only';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { PROVIDER_LIST } from '~/utils/constants';
|
||||
import { ModelSelector } from '~/components/chat/ModelSelector';
|
||||
import { APIKeyManager } from './APIKeyManager';
|
||||
import { LOCAL_PROVIDERS } from '~/lib/stores/settings';
|
||||
import FilePreview from './FilePreview';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { SendButton } from './SendButton.client';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import { toast } from 'react-toastify';
|
||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton';
|
||||
import { SupabaseConnection } from './SupabaseConnection';
|
||||
import { ExpoQrModal } from '~/components/workbench/ExpoQrModal';
|
||||
import styles from './BaseChat.module.scss';
|
||||
import type { ProviderInfo } from '~/types/model';
|
||||
|
||||
interface ChatBoxProps {
|
||||
isModelSettingsCollapsed: boolean;
|
||||
setIsModelSettingsCollapsed: (collapsed: boolean) => void;
|
||||
provider: any;
|
||||
providerList: any[];
|
||||
modelList: any[];
|
||||
apiKeys: Record<string, string>;
|
||||
isModelLoading: string | undefined;
|
||||
onApiKeysChange: (providerName: string, apiKey: string) => void;
|
||||
uploadedFiles: File[];
|
||||
imageDataList: string[];
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement> | undefined;
|
||||
input: string;
|
||||
handlePaste: (e: React.ClipboardEvent) => void;
|
||||
TEXTAREA_MIN_HEIGHT: number;
|
||||
TEXTAREA_MAX_HEIGHT: number;
|
||||
isStreaming: boolean;
|
||||
handleSendMessage: (event: React.UIEvent, messageInput?: string) => void;
|
||||
isListening: boolean;
|
||||
startListening: () => void;
|
||||
stopListening: () => void;
|
||||
chatStarted: boolean;
|
||||
exportChat?: () => void;
|
||||
qrModalOpen: boolean;
|
||||
setQrModalOpen: (open: boolean) => void;
|
||||
handleFileUpload: () => void;
|
||||
setProvider?: ((provider: ProviderInfo) => void) | undefined;
|
||||
model?: string | undefined;
|
||||
setModel?: ((model: string) => void) | undefined;
|
||||
setUploadedFiles?: ((files: File[]) => void) | undefined;
|
||||
setImageDataList?: ((dataList: string[]) => void) | undefined;
|
||||
handleInputChange?: ((event: React.ChangeEvent<HTMLTextAreaElement>) => void) | undefined;
|
||||
handleStop?: (() => void) | undefined;
|
||||
enhancingPrompt?: boolean | undefined;
|
||||
enhancePrompt?: (() => void) | undefined;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
}
|
||||
|
||||
export const ChatBox: React.FC<ChatBoxProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
|
||||
|
||||
/*
|
||||
* {
|
||||
* 'sticky bottom-2': chatStarted,
|
||||
* },
|
||||
*/
|
||||
)}
|
||||
>
|
||||
<svg className={classNames(styles.PromptEffectContainer)}>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="line-gradient"
|
||||
x1="20%"
|
||||
y1="0%"
|
||||
x2="-14%"
|
||||
y2="10%"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(-45)"
|
||||
>
|
||||
<stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
|
||||
<stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
|
||||
<stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
|
||||
<stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="shine-gradient">
|
||||
<stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
|
||||
<stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
|
||||
<stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
|
||||
<stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
|
||||
<rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
|
||||
</svg>
|
||||
<div>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<div className={props.isModelSettingsCollapsed ? 'hidden' : ''}>
|
||||
<ModelSelector
|
||||
key={props.provider?.name + ':' + props.modelList.length}
|
||||
model={props.model}
|
||||
setModel={props.setModel}
|
||||
modelList={props.modelList}
|
||||
provider={props.provider}
|
||||
setProvider={props.setProvider}
|
||||
providerList={props.providerList || (PROVIDER_LIST as ProviderInfo[])}
|
||||
apiKeys={props.apiKeys}
|
||||
modelLoading={props.isModelLoading}
|
||||
/>
|
||||
{(props.providerList || []).length > 0 &&
|
||||
props.provider &&
|
||||
(!LOCAL_PROVIDERS.includes(props.provider.name) || 'OpenAILike') && (
|
||||
<APIKeyManager
|
||||
provider={props.provider}
|
||||
apiKey={props.apiKeys[props.provider.name] || ''}
|
||||
setApiKey={(key) => {
|
||||
props.onApiKeysChange(props.provider.name, key);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<FilePreview
|
||||
files={props.uploadedFiles}
|
||||
imageDataList={props.imageDataList}
|
||||
onRemove={(index) => {
|
||||
props.setUploadedFiles?.(props.uploadedFiles.filter((_, i) => i !== index));
|
||||
props.setImageDataList?.(props.imageDataList.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<ScreenshotStateManager
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
setImageDataList={props.setImageDataList}
|
||||
uploadedFiles={props.uploadedFiles}
|
||||
imageDataList={props.imageDataList}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div
|
||||
className={classNames('relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg')}
|
||||
>
|
||||
<textarea
|
||||
ref={props.textareaRef}
|
||||
className={classNames(
|
||||
'w-full pl-4 pt-4 pr-16 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',
|
||||
)}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '2px solid #1488fc';
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
files.forEach((file) => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
const base64Image = e.target?.result as string;
|
||||
props.setUploadedFiles?.([...props.uploadedFiles, file]);
|
||||
props.setImageDataList?.([...props.imageDataList, base64Image]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (props.isStreaming) {
|
||||
props.handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if using input method engine
|
||||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
value={props.input}
|
||||
onChange={(event) => {
|
||||
props.handleInputChange?.(event);
|
||||
}}
|
||||
onPaste={props.handlePaste}
|
||||
style={{
|
||||
minHeight: props.TEXTAREA_MIN_HEIGHT,
|
||||
maxHeight: props.TEXTAREA_MAX_HEIGHT,
|
||||
}}
|
||||
placeholder={props.chatMode === 'build' ? 'How can Bolt help you today?' : 'What would you like to discuss?'}
|
||||
translate="no"
|
||||
/>
|
||||
<ClientOnly>
|
||||
{() => (
|
||||
<SendButton
|
||||
show={props.input.length > 0 || props.isStreaming || props.uploadedFiles.length > 0}
|
||||
isStreaming={props.isStreaming}
|
||||
disabled={!props.providerList || props.providerList.length === 0}
|
||||
onClick={(event) => {
|
||||
if (props.isStreaming) {
|
||||
props.handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.input.length > 0 || props.uploadedFiles.length > 0) {
|
||||
props.handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
<div className="flex justify-between items-center text-sm p-4 pt-2">
|
||||
<div className="flex gap-1 items-center">
|
||||
<IconButton title="Upload file" className="transition-all" onClick={() => props.handleFileUpload()}>
|
||||
<div className="i-ph:paperclip text-xl"></div>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
title="Enhance prompt"
|
||||
disabled={props.input.length === 0 || props.enhancingPrompt}
|
||||
className={classNames('transition-all', props.enhancingPrompt ? 'opacity-100' : '')}
|
||||
onClick={() => {
|
||||
props.enhancePrompt?.();
|
||||
toast.success('Prompt enhanced!');
|
||||
}}
|
||||
>
|
||||
{props.enhancingPrompt ? (
|
||||
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
|
||||
) : (
|
||||
<div className="i-bolt:stars text-xl"></div>
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<SpeechRecognitionButton
|
||||
isListening={props.isListening}
|
||||
onStart={props.startListening}
|
||||
onStop={props.stopListening}
|
||||
disabled={props.isStreaming}
|
||||
/>
|
||||
{props.chatStarted && (
|
||||
<IconButton
|
||||
title="Discuss"
|
||||
className={classNames(
|
||||
'transition-all flex items-center gap-1 px-1.5',
|
||||
props.chatMode === 'discuss'
|
||||
? '!bg-bolt-elements-item-backgroundAccent !text-bolt-elements-item-contentAccent'
|
||||
: 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault',
|
||||
)}
|
||||
onClick={() => {
|
||||
props.setChatMode?.(props.chatMode === 'discuss' ? 'build' : 'discuss');
|
||||
}}
|
||||
>
|
||||
<div className={`i-ph:chats text-xl`} />
|
||||
{props.chatMode === 'discuss' ? <span>Discuss</span> : <span />}
|
||||
</IconButton>
|
||||
)}
|
||||
{props.chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={props.exportChat} />}</ClientOnly>}
|
||||
<IconButton
|
||||
title="Model Settings"
|
||||
className={classNames('transition-all flex items-center gap-1', {
|
||||
'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
|
||||
props.isModelSettingsCollapsed,
|
||||
'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
|
||||
!props.isModelSettingsCollapsed,
|
||||
})}
|
||||
onClick={() => props.setIsModelSettingsCollapsed(!props.isModelSettingsCollapsed)}
|
||||
disabled={!props.providerList || props.providerList.length === 0}
|
||||
>
|
||||
<div className={`i-ph:caret-${props.isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
|
||||
{props.isModelSettingsCollapsed ? <span className="text-xs">{props.model}</span> : <span />}
|
||||
</IconButton>
|
||||
</div>
|
||||
{props.input.length > 3 ? (
|
||||
<div className="text-xs text-bolt-elements-textTertiary">
|
||||
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
|
||||
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a new line
|
||||
</div>
|
||||
) : null}
|
||||
<SupabaseConnection />
|
||||
<ExpoQrModal open={props.qrModalOpen} onClose={() => props.setQrModalOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
17
app/components/chat/DicussMode.tsx
Normal file
17
app/components/chat/DicussMode.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { IconButton } from '~/components/ui';
|
||||
|
||||
export function DiscussMode() {
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
title="Discuss"
|
||||
className={classNames(
|
||||
'transition-all flex items-center gap-1 bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent',
|
||||
)}
|
||||
>
|
||||
<div className={`i-ph:chats text-xl`} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import ReactMarkdown, { type Components } from 'react-markdown';
|
||||
import type { BundledLanguage } from 'shiki';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { rehypePlugins, remarkPlugins, allowedHTMLElements } from '~/utils/markdown';
|
||||
import { Artifact } from './Artifact';
|
||||
import { Artifact, openArtifactInWorkbench } from './Artifact';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
|
||||
import type { Message } from 'ai';
|
||||
import styles from './Markdown.module.scss';
|
||||
import ThoughtBox from './ThoughtBox';
|
||||
|
||||
@@ -15,68 +15,132 @@ interface MarkdownProps {
|
||||
children: string;
|
||||
html?: boolean;
|
||||
limitedMarkdown?: boolean;
|
||||
append?: (message: Message) => void;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
}
|
||||
|
||||
export const Markdown = memo(({ children, html = false, limitedMarkdown = false }: MarkdownProps) => {
|
||||
logger.trace('Render');
|
||||
export const Markdown = memo(
|
||||
({ children, html = false, limitedMarkdown = false, append, setChatMode }: MarkdownProps) => {
|
||||
logger.trace('Render');
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
div: ({ className, children, node, ...props }) => {
|
||||
if (className?.includes('__boltArtifact__')) {
|
||||
const messageId = node?.properties.dataMessageId as string;
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
div: ({ className, children, node, ...props }) => {
|
||||
const dataProps = node?.properties as Record<string, unknown>;
|
||||
|
||||
if (!messageId) {
|
||||
logger.error(`Invalid message id ${messageId}`);
|
||||
if (className?.includes('__boltArtifact__')) {
|
||||
const messageId = node?.properties.dataMessageId as string;
|
||||
|
||||
if (!messageId) {
|
||||
logger.error(`Invalid message id ${messageId}`);
|
||||
}
|
||||
|
||||
return <Artifact messageId={messageId} />;
|
||||
}
|
||||
|
||||
return <Artifact messageId={messageId} />;
|
||||
}
|
||||
if (className?.includes('__boltThought__')) {
|
||||
return <ThoughtBox title="Thought process">{children}</ThoughtBox>;
|
||||
}
|
||||
|
||||
if (className?.includes('__boltThought__')) {
|
||||
return <ThoughtBox title="Thought process">{children}</ThoughtBox>;
|
||||
}
|
||||
if (className?.includes('__boltQuickAction__') || dataProps?.dataBoltQuickAction) {
|
||||
return <div className="w-full grid grid-cols-2 gap-4">{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
pre: (props) => {
|
||||
const { children, node, ...rest } = props;
|
||||
return (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
pre: (props) => {
|
||||
const { children, node, ...rest } = props;
|
||||
|
||||
const [firstChild] = node?.children ?? [];
|
||||
const [firstChild] = node?.children ?? [];
|
||||
|
||||
if (
|
||||
firstChild &&
|
||||
firstChild.type === 'element' &&
|
||||
firstChild.tagName === 'code' &&
|
||||
firstChild.children[0].type === 'text'
|
||||
) {
|
||||
const { className, ...rest } = firstChild.properties;
|
||||
const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
|
||||
if (
|
||||
firstChild &&
|
||||
firstChild.type === 'element' &&
|
||||
firstChild.tagName === 'code' &&
|
||||
firstChild.children[0].type === 'text'
|
||||
) {
|
||||
const { className, ...rest } = firstChild.properties;
|
||||
const [, language = 'plaintext'] = /language-(\w+)/.exec(String(className) || '') ?? [];
|
||||
|
||||
return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
|
||||
}
|
||||
return <CodeBlock code={firstChild.children[0].value} language={language as BundledLanguage} {...rest} />;
|
||||
}
|
||||
|
||||
return <pre {...rest}>{children}</pre>;
|
||||
},
|
||||
} satisfies Components;
|
||||
}, []);
|
||||
return <pre {...rest}>{children}</pre>;
|
||||
},
|
||||
button: ({ node, children, ...props }) => {
|
||||
const dataProps = node?.properties as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
allowedElements={allowedHTMLElements}
|
||||
className={styles.MarkdownContent}
|
||||
components={components}
|
||||
remarkPlugins={remarkPlugins(limitedMarkdown)}
|
||||
rehypePlugins={rehypePlugins(html)}
|
||||
>
|
||||
{stripCodeFenceFromArtifact(children)}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
});
|
||||
if (
|
||||
dataProps?.class?.toString().includes('__boltQuickAction__') ||
|
||||
dataProps?.dataBoltQuickAction === 'true'
|
||||
) {
|
||||
const type = dataProps['data-type'] || dataProps.dataType;
|
||||
const message = dataProps['data-message'] || dataProps.dataMessage;
|
||||
const path = dataProps['data-path'] || dataProps.dataPath;
|
||||
const href = dataProps['data-href'] || dataProps.dataHref;
|
||||
|
||||
return (
|
||||
<button
|
||||
className=" p-2 rounded-md bg-bolt-elements-item-backgroundAccent hover:opacity-50 text-bolt-elements-item-contentAccent"
|
||||
data-type={type}
|
||||
data-message={message}
|
||||
data-path={path}
|
||||
data-href={href}
|
||||
onClick={() => {
|
||||
if (type === 'file') {
|
||||
openArtifactInWorkbench(path);
|
||||
} else if (type === 'message' && append) {
|
||||
append({
|
||||
id: 'random-message', // Replace with your ID generation logic
|
||||
content: message as string, // The message content from the action
|
||||
role: 'user', // Or another role as appropriate
|
||||
});
|
||||
console.log('Message appended:', message); // Log the appended message
|
||||
} else if (type === 'implement' && append && setChatMode) {
|
||||
setChatMode('build');
|
||||
append({
|
||||
id: 'implement-message', // Replace with your ID generation logic
|
||||
content: message as string, // The message content from the action
|
||||
role: 'user', // Or another role as appropriate
|
||||
});
|
||||
} else if (type === 'link' && typeof href === 'string') {
|
||||
try {
|
||||
const url = new URL(href, window.location.origin);
|
||||
window.open(url.toString(), '_blank', 'noopener,noreferrer');
|
||||
} catch (error) {
|
||||
console.error('Invalid URL:', href, error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <button {...props}>{children}</button>;
|
||||
},
|
||||
} satisfies Components;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
allowedElements={allowedHTMLElements}
|
||||
className={styles.MarkdownContent}
|
||||
components={components}
|
||||
remarkPlugins={remarkPlugins(limitedMarkdown)}
|
||||
rehypePlugins={rehypePlugins(html)}
|
||||
>
|
||||
{stripCodeFenceFromArtifact(children)}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes code fence markers (```) surrounding an artifact element while preserving the artifact content.
|
||||
|
||||
@@ -17,6 +17,9 @@ interface MessagesProps {
|
||||
className?: string;
|
||||
isStreaming?: boolean;
|
||||
messages?: Message[];
|
||||
append?: (message: Message) => void;
|
||||
chatMode?: 'discuss' | 'build';
|
||||
setChatMode?: (mode: 'discuss' | 'build') => void;
|
||||
}
|
||||
|
||||
export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
||||
@@ -94,6 +97,9 @@ export const Messages = forwardRef<HTMLDivElement, MessagesProps>(
|
||||
messageId={messageId}
|
||||
onRewind={handleRewind}
|
||||
onFork={handleFork}
|
||||
append={props.append}
|
||||
chatMode={props.chatMode}
|
||||
setChatMode={props.setChatMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user