Merge branch 'main' into prompt-url-params
This commit is contained in:
@@ -1,13 +1,30 @@
|
||||
import { memo } from 'react';
|
||||
import { Markdown } from './Markdown';
|
||||
import type { JSONValue } from 'ai';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
content: string;
|
||||
annotations?: JSONValue[];
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(({ content }: AssistantMessageProps) => {
|
||||
export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => {
|
||||
const filteredAnnotations = (annotations?.filter(
|
||||
(annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'),
|
||||
) || []) as { type: string; value: any }[];
|
||||
|
||||
const usage: {
|
||||
completionTokens: number;
|
||||
promptTokens: number;
|
||||
totalTokens: number;
|
||||
} = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
{usage && (
|
||||
<div className="text-sm text-bolt-elements-textSecondary mb-2">
|
||||
Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens})
|
||||
</div>
|
||||
)}
|
||||
<Markdown html>{content}</Markdown>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,8 @@ import FilePreview from './FilePreview';
|
||||
import { ModelSelector } from '~/components/chat/ModelSelector';
|
||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const TEXTAREA_MIN_HEIGHT = 76;
|
||||
|
||||
@@ -75,7 +77,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
input = '',
|
||||
enhancingPrompt,
|
||||
handleInputChange,
|
||||
promptEnhanced,
|
||||
|
||||
// promptEnhanced,
|
||||
enhancePrompt,
|
||||
sendMessage,
|
||||
handleStop,
|
||||
@@ -283,7 +286,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
<div ref={scrollRef} className="flex flex-col lg:flex-row overflow-y-auto w-full h-full">
|
||||
<div className={classNames(styles.Chat, 'flex flex-col flex-grow lg:min-w-[var(--chat-min-width)] h-full')}>
|
||||
{!chatStarted && (
|
||||
<div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center px-4 lg:px-0">
|
||||
<div id="intro" className="mt-[16vh] max-w-chat mx-auto text-center px-4 lg:px-0">
|
||||
<h1 className="text-3xl lg:text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
|
||||
Where ideas begin
|
||||
</h1>
|
||||
@@ -376,6 +379,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
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',
|
||||
@@ -384,7 +397,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={classNames(
|
||||
'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',
|
||||
'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',
|
||||
)}
|
||||
@@ -431,6 +444,11 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore if using input method engine
|
||||
if (event.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage?.(event);
|
||||
}
|
||||
}}
|
||||
@@ -473,25 +491,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
<IconButton
|
||||
title="Enhance prompt"
|
||||
disabled={input.length === 0 || enhancingPrompt}
|
||||
className={classNames(
|
||||
'transition-all',
|
||||
enhancingPrompt ? 'opacity-100' : '',
|
||||
promptEnhanced ? 'text-bolt-elements-item-contentAccent' : '',
|
||||
promptEnhanced ? 'pr-1.5' : '',
|
||||
promptEnhanced ? 'enabled:hover:bg-bolt-elements-item-backgroundAccent' : '',
|
||||
)}
|
||||
onClick={() => enhancePrompt?.()}
|
||||
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="ml-1.5">Enhancing prompt...</div>
|
||||
</>
|
||||
<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>
|
||||
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
|
||||
</>
|
||||
<div className="i-bolt:stars text-xl"></div>
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
|
||||
@@ -93,8 +93,9 @@ export const ChatImpl = memo(
|
||||
const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
|
||||
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
|
||||
const { activeProviders } = useSettings();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const files = useStore(workbenchStore.files);
|
||||
const { activeProviders, promptId } = useSettings();
|
||||
|
||||
const [model, setModel] = useState(() => {
|
||||
const savedModel = Cookies.get('selectedModel');
|
||||
@@ -115,14 +116,25 @@ export const ChatImpl = memo(
|
||||
api: '/api/chat',
|
||||
body: {
|
||||
apiKeys,
|
||||
files,
|
||||
promptId,
|
||||
},
|
||||
sendExtraMessageFields: true,
|
||||
onError: (error) => {
|
||||
logger.error('Request failed\n\n', error);
|
||||
toast.error(
|
||||
'There was an error processing your request: ' + (error.message ? error.message : 'No details were returned'),
|
||||
);
|
||||
},
|
||||
onFinish: () => {
|
||||
onFinish: (message, response) => {
|
||||
const usage = response.usage;
|
||||
|
||||
if (usage) {
|
||||
console.log('Token usage:', usage);
|
||||
|
||||
// You can now use the usage data as needed
|
||||
}
|
||||
|
||||
logger.debug('Finished streaming');
|
||||
},
|
||||
initialMessages,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ignore from 'ignore';
|
||||
import { useGit } from '~/lib/hooks/useGit';
|
||||
import type { Message } from 'ai';
|
||||
import WithTooltip from '~/components/ui/Tooltip';
|
||||
import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
|
||||
import { generateId } from '~/utils/fileUtils';
|
||||
|
||||
@@ -73,7 +72,7 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
|
||||
const filesMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Cloning the repo ${repoUrl} into ${workdir}
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
<boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
|
||||
${fileContents
|
||||
.map(
|
||||
(file) =>
|
||||
@@ -99,17 +98,13 @@ ${file.content}
|
||||
};
|
||||
|
||||
return (
|
||||
<WithTooltip tooltip="Clone A Git Repo">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
onClick(e);
|
||||
}}
|
||||
title="Clone A Git Repo"
|
||||
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
|
||||
>
|
||||
<span className="i-ph:git-branch" />
|
||||
Clone A Git Repo
|
||||
</button>
|
||||
</WithTooltip>
|
||||
<button
|
||||
onClick={onClick}
|
||||
title="Clone a Git Repo"
|
||||
className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
|
||||
>
|
||||
<span className="i-ph:git-branch" />
|
||||
Clone a Git Repo
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
|
||||
import { createChatFromFolder } from '~/utils/folderImport';
|
||||
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
|
||||
|
||||
interface ImportFolderButtonProps {
|
||||
className?: string;
|
||||
@@ -16,9 +17,15 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
||||
const allFiles = Array.from(e.target.files || []);
|
||||
|
||||
if (allFiles.length > MAX_FILES) {
|
||||
const error = new Error(`Too many files: ${allFiles.length}`);
|
||||
logStore.logError('File import failed - too many files', error, {
|
||||
fileCount: allFiles.length,
|
||||
maxFiles: MAX_FILES,
|
||||
});
|
||||
toast.error(
|
||||
`This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,7 +38,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
||||
const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
|
||||
|
||||
if (filteredFiles.length === 0) {
|
||||
const error = new Error('No valid files found');
|
||||
logStore.logError('File import failed - no valid files', error, { folderName });
|
||||
toast.error('No files found in the selected folder');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,11 +58,18 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
||||
.map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
|
||||
|
||||
if (textFiles.length === 0) {
|
||||
const error = new Error('No text files found');
|
||||
logStore.logError('File import failed - no text files', error, { folderName });
|
||||
toast.error('No text files found in the selected folder');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (binaryFilePaths.length > 0) {
|
||||
logStore.logWarning(`Skipping binary files during import`, {
|
||||
folderName,
|
||||
binaryCount: binaryFilePaths.length,
|
||||
});
|
||||
toast.info(`Skipping ${binaryFilePaths.length} binary files`);
|
||||
}
|
||||
|
||||
@@ -62,8 +79,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
|
||||
await importChat(folderName, [...messages]);
|
||||
}
|
||||
|
||||
logStore.logSystem('Folder imported successfully', {
|
||||
folderName,
|
||||
textFileCount: textFiles.length,
|
||||
binaryFileCount: binaryFilePaths.length,
|
||||
});
|
||||
toast.success('Folder imported successfully');
|
||||
} catch (error) {
|
||||
logStore.logError('Failed to import folder', error, { folderName });
|
||||
console.error('Failed to import folder:', error);
|
||||
toast.error('Failed to import folder');
|
||||
} finally {
|
||||
|
||||
@@ -65,12 +65,16 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-col-1 w-full">
|
||||
{isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
|
||||
{isUserMessage ? (
|
||||
<UserMessage content={content} />
|
||||
) : (
|
||||
<AssistantMessage content={content} annotations={message.annotations} />
|
||||
)}
|
||||
</div>
|
||||
{!isUserMessage && (
|
||||
<div className="flex gap-2 flex-col lg:flex-row">
|
||||
<WithTooltip tooltip="Revert to this message">
|
||||
{messageId && (
|
||||
{messageId && (
|
||||
<WithTooltip tooltip="Revert to this message">
|
||||
<button
|
||||
onClick={() => handleRewind(messageId)}
|
||||
key="i-ph:arrow-u-up-left"
|
||||
@@ -79,8 +83,8 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
||||
'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</WithTooltip>
|
||||
</WithTooltip>
|
||||
)}
|
||||
|
||||
<WithTooltip tooltip="Fork chat from this message">
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
model?: string;
|
||||
@@ -22,62 +21,28 @@ export const ModelSelector = ({
|
||||
providerList,
|
||||
}: ModelSelectorProps) => {
|
||||
// Load enabled providers from cookies
|
||||
const [enabledProviders, setEnabledProviders] = useState(() => {
|
||||
const savedProviders = Cookies.get('providers');
|
||||
|
||||
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 current provider is disabled, switch to first enabled provider
|
||||
if (providerList.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedProviders) {
|
||||
try {
|
||||
const parsedProviders = JSON.parse(savedProviders);
|
||||
const newEnabledProviders = providerList.filter((p) => parsedProviders[p.name]);
|
||||
setEnabledProviders(newEnabledProviders);
|
||||
if (provider && !providerList.map((p) => p.name).includes(provider.name)) {
|
||||
const firstEnabledProvider = providerList[0];
|
||||
setProvider?.(firstEnabledProvider);
|
||||
|
||||
// If current provider is disabled, switch to first enabled provider
|
||||
if (provider && !parsedProviders[provider.name] && newEnabledProviders.length > 0) {
|
||||
const firstEnabledProvider = newEnabledProviders[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]);
|
||||
|
||||
if (enabledProviders.length === 0) {
|
||||
if (providerList.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">
|
||||
@@ -93,7 +58,7 @@ export const ModelSelector = ({
|
||||
<select
|
||||
value={provider?.name ?? ''}
|
||||
onChange={(e) => {
|
||||
const newProvider = enabledProviders.find((p: ProviderInfo) => p.name === e.target.value);
|
||||
const newProvider = providerList.find((p: ProviderInfo) => p.name === e.target.value);
|
||||
|
||||
if (newProvider && setProvider) {
|
||||
setProvider(newProvider);
|
||||
@@ -107,7 +72,7 @@ export const ModelSelector = ({
|
||||
}}
|
||||
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) => (
|
||||
{providerList.map((provider: ProviderInfo) => (
|
||||
<option key={provider.name} value={provider.name}>
|
||||
{provider.name}
|
||||
</option>
|
||||
|
||||
33
app/components/chat/ScreenshotStateManager.tsx
Normal file
33
app/components/chat/ScreenshotStateManager.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
interface ScreenshotStateManagerProps {
|
||||
setUploadedFiles?: (files: File[]) => void;
|
||||
setImageDataList?: (dataList: string[]) => void;
|
||||
uploadedFiles: File[];
|
||||
imageDataList: string[];
|
||||
}
|
||||
|
||||
export const ScreenshotStateManager = ({
|
||||
setUploadedFiles,
|
||||
setImageDataList,
|
||||
uploadedFiles,
|
||||
imageDataList,
|
||||
}: ScreenshotStateManagerProps) => {
|
||||
useEffect(() => {
|
||||
if (setUploadedFiles && setImageDataList) {
|
||||
(window as any).__BOLT_SET_UPLOADED_FILES__ = setUploadedFiles;
|
||||
(window as any).__BOLT_SET_IMAGE_DATA_LIST__ = setImageDataList;
|
||||
(window as any).__BOLT_UPLOADED_FILES__ = uploadedFiles;
|
||||
(window as any).__BOLT_IMAGE_DATA_LIST__ = imageDataList;
|
||||
}
|
||||
|
||||
return () => {
|
||||
delete (window as any).__BOLT_SET_UPLOADED_FILES__;
|
||||
delete (window as any).__BOLT_SET_IMAGE_DATA_LIST__;
|
||||
delete (window as any).__BOLT_UPLOADED_FILES__;
|
||||
delete (window as any).__BOLT_IMAGE_DATA_LIST__;
|
||||
};
|
||||
}, [setUploadedFiles, setImageDataList, uploadedFiles, imageDataList]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -12,42 +12,36 @@ interface UserMessageProps {
|
||||
export function UserMessage({ content }: UserMessageProps) {
|
||||
if (Array.isArray(content)) {
|
||||
const textItem = content.find((item) => item.type === 'text');
|
||||
const textContent = sanitizeUserMessage(textItem?.text || '');
|
||||
const textContent = stripMetadata(textItem?.text || '');
|
||||
const images = content.filter((item) => item.type === 'image' && item.image);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden pt-[4px]">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<Markdown limitedMarkdown>{textContent}</Markdown>
|
||||
</div>
|
||||
{images.length > 0 && (
|
||||
<div className="flex-shrink-0 w-[160px]">
|
||||
{images.map((item, index) => (
|
||||
<div key={index} className="relative">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={`Uploaded image ${index + 1}`}
|
||||
className="w-full h-[160px] rounded-lg object-cover border border-bolt-elements-borderColor"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
{textContent && <Markdown html>{textContent}</Markdown>}
|
||||
{images.map((item, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={item.image}
|
||||
alt={`Image ${index + 1}`}
|
||||
className="max-w-full h-auto rounded-lg"
|
||||
style={{ maxHeight: '512px', objectFit: 'contain' }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const textContent = sanitizeUserMessage(content);
|
||||
const textContent = stripMetadata(content);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden pt-[4px]">
|
||||
<Markdown limitedMarkdown>{textContent}</Markdown>
|
||||
<Markdown html>{textContent}</Markdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeUserMessage(content: string) {
|
||||
function stripMetadata(content: string) {
|
||||
return content.replace(MODEL_REGEX, '').replace(PROVIDER_REGEX, '');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import React from 'react';
|
||||
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
|
||||
|
||||
export function ImportButtons(importChat: ((description: string, messages: Message[]) => Promise<void>) | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user