mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat(chat): 更新聊天组件,支持模板选择和更本地化的体验
This commit is contained in:
parent
753889851a
commit
19cf6debf8
@ -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 && (
|
||||||
|
|||||||
39
app/components/ui/Select.tsx
Normal file
39
app/components/ui/Select.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
21
app/components/workbench/TemplateSelector.tsx
Normal file
21
app/components/workbench/TemplateSelector.tsx
Normal 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="选择模板"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -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
40
app/utils/templates.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user