feat(chat): 更新聊天组件,支持模板选择和更本地化的体验

This commit is contained in:
zyh 2024-10-19 02:02:39 +00:00
parent 753889851a
commit 19cf6debf8
6 changed files with 138 additions and 14 deletions

View File

@ -1,5 +1,5 @@
import type { Message } from 'ai'; import type { Message } from 'ai';
import React, { type RefCallback } from 'react'; import React, { type RefCallback, useState } from 'react';
import { ClientOnly } from 'remix-utils/client-only'; import { ClientOnly } from 'remix-utils/client-only';
import { Menu } from '~/components/sidebar/Menu.client'; import { Menu } from '~/components/sidebar/Menu.client';
import { IconButton } from '~/components/ui/IconButton'; import { IconButton } from '~/components/ui/IconButton';
@ -7,6 +7,8 @@ import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
import { Messages } from './Messages.client'; import { Messages } from './Messages.client';
import { SendButton } from './SendButton.client'; import { SendButton } from './SendButton.client';
import { TemplateSelector } from '~/components/workbench/TemplateSelector';
import { type TemplateName } from '~/utils/templates';
import styles from './BaseChat.module.scss'; import styles from './BaseChat.module.scss';
@ -28,11 +30,11 @@ interface BaseChatProps {
} }
const EXAMPLE_PROMPTS = [ const EXAMPLE_PROMPTS = [
{ text: 'Build a todo app in React using Tailwind' }, { text: '使用 React 和 Tailwind 构建一个待办事项应用' },
{ text: 'Build a simple blog using Astro' }, { text: '使用 Astro 构建一个简单的博客' },
{ text: 'Create a cookie consent form using Material UI' }, { text: '使用 Material UI 创建一个 cookie 同意表单' },
{ text: 'Make a space invaders game' }, { text: '制作一个太空入侵者游戏' },
{ text: 'How do I center a div?' }, { text: '如何让一个 div 居中?' },
]; ];
const TEXTAREA_MIN_HEIGHT = 76; const TEXTAREA_MIN_HEIGHT = 76;
@ -58,6 +60,18 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
ref, ref,
) => { ) => {
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; 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 ( return (
<div <div
@ -74,11 +88,17 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{!chatStarted && ( {!chatStarted && (
<div id="intro" className="mt-[26vh] max-w-chat mx-auto"> <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 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
Where ideas begin
</h1> </h1>
<p className="mb-4 text-center text-bolt-elements-textSecondary"> <p className="mb-4 text-center text-bolt-elements-textSecondary">
Bring ideas to life in seconds or get help on existing projects.
</p> </p>
<TemplateSelector
className="w-full mb-4"
value={selectedTemplate}
onChange={handleTemplateChange}
/>
<ClientOnly>{() => <div>123</div>}</ClientOnly>
</div> </div>
)} )}
<div <div
@ -130,7 +150,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
minHeight: TEXTAREA_MIN_HEIGHT, minHeight: TEXTAREA_MIN_HEIGHT,
maxHeight: TEXTAREA_MAX_HEIGHT, maxHeight: TEXTAREA_MAX_HEIGHT,
}} }}
placeholder="How can Bolt help you today?" placeholder="Bolt 今天能为您做些什么?"
translate="no" translate="no"
/> />
<ClientOnly> <ClientOnly>
@ -152,7 +172,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
<div className="flex justify-between text-sm p-4 pt-2"> <div className="flex justify-between text-sm p-4 pt-2">
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<IconButton <IconButton
title="Enhance prompt" title="增强提示"
disabled={input.length === 0 || enhancingPrompt} disabled={input.length === 0 || enhancingPrompt}
className={classNames({ className={classNames({
'opacity-100!': enhancingPrompt, 'opacity-100!': enhancingPrompt,
@ -164,24 +184,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
{enhancingPrompt ? ( {enhancingPrompt ? (
<> <>
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div> <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
<div className="ml-1.5">Enhancing prompt...</div> <div className="ml-1.5">...</div>
</> </>
) : ( ) : (
<> <>
<div className="i-bolt:stars text-xl"></div> <div className="i-bolt:stars text-xl"></div>
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>} {promptEnhanced && <div className="ml-1.5"></div>}
</> </>
)} )}
</IconButton> </IconButton>
</div> </div>
{input.length > 3 ? ( {input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary"> <div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line 使 <kbd className="kdb">Shift</kbd> + <kbd className="kdb"></kbd>
</div> </div>
) : null} ) : null}
</div> </div>
</div> </div>
<div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div> <div className="bg-bolt-elements-background-depth-1 pb-6">{/* 幽灵元素 */}</div>
</div> </div>
</div> </div>
{!chatStarted && ( {!chatStarted && (

View File

@ -0,0 +1,39 @@
import React from 'react';
import { classNames } from '~/utils/classNames';
interface SelectOption {
value: string;
label: string;
}
interface SelectProps {
options: SelectOption[];
value: string;
onChange: (value: string) => void;
className?: string;
placeholder?: string;
}
export const Select: React.FC<SelectProps> = ({ options, value, onChange, className, placeholder }) => {
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={classNames(
'px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor focus:border-transparent',
className
)}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};

View File

@ -0,0 +1,21 @@
import { memo } from 'react';
import { templates, type TemplateName } from '~/utils/templates';
import { Select } from '~/components/ui/Select';
interface TemplateSelectorProps {
className?: string;
value: TemplateName;
onChange: (templateName: TemplateName) => void;
}
export const TemplateSelector = memo(({ className, value, onChange }: TemplateSelectorProps) => {
return (
<Select
options={Object.entries(templates).map(([key, value]) => ({ value: key, label: value.name }))}
value={value}
onChange={(newValue) => onChange(newValue as TemplateName)}
className={className}
placeholder="选择模板"
/>
);
});

View File

@ -1,3 +1,4 @@
export const WORK_DIR_NAME = 'project'; export const WORK_DIR_NAME = 'project';
export const WORK_DIR = `/home/${WORK_DIR_NAME}`; export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications'; export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications';
export const DEFAULT_TEMPLATE = 'basic'; // 可以添加这样的常量来指定默认模板

40
app/utils/templates.ts Normal file
View File

@ -0,0 +1,40 @@
import type { FileSystemTree } from '@webcontainer/api';
export const templates = {
basic: {
name: "Basic",
template: {
'index.js': {
file: {
contents: `console.log('Hello, WebContainer!');`,
},
},
'package.json': {
file: {
contents: `{
"name": "webcontainer-project",
"version": "1.0.0",
"description": "A basic WebContainer project",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}`,
},
},
},
},
react: {
name: "React",
template: {
// 定义 React 项目模板
},
},
// 添加更多模板...
};
export type TemplateName = keyof typeof templates;
export async function getInitialTemplate(templateName: TemplateName = 'basic'): Promise<FileSystemTree> {
return templates[templateName].template;
}

View File

@ -7,6 +7,9 @@ import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig((config) => { export default defineConfig((config) => {
return { return {
define: {
'module': {},
},
build: { build: {
target: 'esnext', target: 'esnext',
}, },