mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
233 lines
9.2 KiB
TypeScript
233 lines
9.2 KiB
TypeScript
import type { Message } from 'ai';
|
||
import React, { type RefCallback, 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 { Messages } from './Messages.client';
|
||
import { SendButton } from './SendButton.client';
|
||
import { TemplateSelector } from '~/components/workbench/TemplateSelector';
|
||
import { type TemplateName } from '~/utils/templates';
|
||
|
||
import styles from './BaseChat.module.scss';
|
||
|
||
interface BaseChatProps {
|
||
textareaRef?: React.RefObject<HTMLTextAreaElement> | undefined;
|
||
messageRef?: RefCallback<HTMLDivElement> | undefined;
|
||
scrollRef?: RefCallback<HTMLDivElement> | undefined;
|
||
showChat?: boolean;
|
||
chatStarted?: boolean;
|
||
isStreaming?: boolean;
|
||
messages?: Message[];
|
||
enhancingPrompt?: boolean;
|
||
promptEnhanced?: boolean;
|
||
input?: string;
|
||
handleStop?: () => void;
|
||
sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
|
||
handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||
enhancePrompt?: () => void;
|
||
}
|
||
|
||
const EXAMPLE_PROMPTS = [
|
||
{ text: '使用 React 和 Tailwind 构建一个待办事项应用' },
|
||
{ text: '使用 Astro 构建一个简单的博客' },
|
||
{ text: '使用 Material UI 创建一个 cookie 同意表单' },
|
||
{ text: '制作一个太空入侵者游戏' },
|
||
{ text: '如何让一个 div 居中?' },
|
||
];
|
||
|
||
const TEXTAREA_MIN_HEIGHT = 76;
|
||
|
||
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||
(
|
||
{
|
||
textareaRef,
|
||
messageRef,
|
||
scrollRef,
|
||
showChat = true,
|
||
chatStarted = false,
|
||
isStreaming = false,
|
||
enhancingPrompt = false,
|
||
promptEnhanced = false,
|
||
messages,
|
||
input = '',
|
||
sendMessage,
|
||
handleInputChange,
|
||
enhancePrompt,
|
||
handleStop,
|
||
},
|
||
ref,
|
||
) => {
|
||
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
|
||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateName>('basic');
|
||
|
||
const handleTemplateChange = async (templateName: TemplateName) => {
|
||
setSelectedTemplate(templateName);
|
||
try {
|
||
console.log('templateName', templateName);
|
||
// await workbenchStore.changeTemplate(templateName);
|
||
} catch (error) {
|
||
console.error('Failed to change template:', error);
|
||
// 可以在这里添加错误处理,比如显示一个错误提示
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className={classNames(
|
||
styles.BaseChat,
|
||
'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
|
||
)}
|
||
data-chat-visible={showChat}
|
||
>
|
||
<ClientOnly>{() => <Menu />}</ClientOnly>
|
||
<div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
|
||
<div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
|
||
{!chatStarted && (
|
||
<div id="intro" className="mt-[26vh] max-w-chat mx-auto">
|
||
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
|
||
创意的起点
|
||
</h1>
|
||
<p className="mb-4 text-center text-bolt-elements-textSecondary">
|
||
在几秒钟内将想法变为现实,或获取现有项目的帮助。
|
||
</p>
|
||
{/* <TemplateSelector
|
||
className="w-full mb-4"
|
||
value={selectedTemplate}
|
||
onChange={handleTemplateChange}
|
||
/> */}
|
||
</div>
|
||
)}
|
||
<div
|
||
className={classNames('pt-6 px-6', {
|
||
'h-full flex flex-col': chatStarted,
|
||
})}
|
||
>
|
||
<ClientOnly>
|
||
{() => {
|
||
return chatStarted ? (
|
||
<Messages
|
||
ref={messageRef}
|
||
className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
|
||
messages={messages}
|
||
isStreaming={isStreaming}
|
||
/>
|
||
) : null;
|
||
}}
|
||
</ClientOnly>
|
||
<div
|
||
className={classNames('relative w-full max-w-chat mx-auto z-prompt', {
|
||
'sticky bottom-0': chatStarted,
|
||
})}
|
||
>
|
||
<div
|
||
className={classNames(
|
||
'shadow-sm border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden',
|
||
)}
|
||
>
|
||
<textarea
|
||
ref={textareaRef}
|
||
className={`w-full pl-4 pt-4 pr-16 focus:outline-none resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent`}
|
||
onKeyDown={(event) => {
|
||
if (event.key === 'Enter') {
|
||
if (event.shiftKey) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
|
||
sendMessage?.(event);
|
||
}
|
||
}}
|
||
value={input}
|
||
onChange={(event) => {
|
||
handleInputChange?.(event);
|
||
}}
|
||
style={{
|
||
minHeight: TEXTAREA_MIN_HEIGHT,
|
||
maxHeight: TEXTAREA_MAX_HEIGHT,
|
||
}}
|
||
placeholder="多八多今天能为您做些什么?"
|
||
translate="no"
|
||
/>
|
||
<ClientOnly>
|
||
{() => (
|
||
<SendButton
|
||
show={input.length > 0 || isStreaming}
|
||
isStreaming={isStreaming}
|
||
onClick={(event) => {
|
||
if (isStreaming) {
|
||
handleStop?.();
|
||
return;
|
||
}
|
||
|
||
sendMessage?.(event);
|
||
}}
|
||
/>
|
||
)}
|
||
</ClientOnly>
|
||
<div className="flex justify-between text-sm p-4 pt-2">
|
||
<div className="flex gap-1 items-center">
|
||
<IconButton
|
||
title="增强提示"
|
||
disabled={input.length === 0 || enhancingPrompt}
|
||
className={classNames({
|
||
'opacity-100!': enhancingPrompt,
|
||
'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
|
||
promptEnhanced,
|
||
})}
|
||
onClick={() => enhancePrompt?.()}
|
||
>
|
||
{enhancingPrompt ? (
|
||
<>
|
||
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
|
||
<div className="ml-1.5">正在增强提示...</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div className="i-bolt:stars text-xl"></div>
|
||
{promptEnhanced && <div className="ml-1.5">提示已增强</div>}
|
||
</>
|
||
)}
|
||
</IconButton>
|
||
</div>
|
||
{input.length > 3 ? (
|
||
<div className="text-xs text-bolt-elements-textTertiary">
|
||
使用 <kbd className="kdb">Shift</kbd> + <kbd className="kdb">回车</kbd> 换行
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<div className="bg-bolt-elements-background-depth-1 pb-6">{/* 幽灵元素 */}</div>
|
||
</div>
|
||
</div>
|
||
{!chatStarted && (
|
||
<div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
|
||
<div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
|
||
{EXAMPLE_PROMPTS.map((examplePrompt, index) => {
|
||
return (
|
||
<button
|
||
key={index}
|
||
onClick={(event) => {
|
||
sendMessage?.(event, examplePrompt.text);
|
||
}}
|
||
className="group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme"
|
||
>
|
||
{examplePrompt.text}
|
||
<div className="i-ph:arrow-bend-down-left" />
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
);
|