feat: add Starter template menu in homepage (#884)
* added icons and component * updated unocss to add dynamic icons * removed temp logs * updated readme
This commit is contained in:
@@ -28,6 +28,7 @@ import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import { ScreenshotStateManager } from './ScreenshotStateManager';
|
||||
import { toast } from 'react-toastify';
|
||||
import StarterTemplates from './StarterTemplates';
|
||||
import type { ActionAlert } from '~/types/actions';
|
||||
import ChatAlert from './ChatAlert';
|
||||
|
||||
@@ -569,21 +570,24 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!chatStarted && (
|
||||
<div className="flex justify-center gap-2">
|
||||
{ImportButtons(importChat)}
|
||||
<GitCloneButton importChat={importChat} />
|
||||
</div>
|
||||
)}
|
||||
{!chatStarted &&
|
||||
ExamplePrompts((event, messageInput) => {
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
<div className="flex flex-col justify-center gap-5">
|
||||
{!chatStarted && (
|
||||
<div className="flex justify-center gap-2">
|
||||
{ImportButtons(importChat)}
|
||||
<GitCloneButton importChat={importChat} />
|
||||
</div>
|
||||
)}
|
||||
{!chatStarted &&
|
||||
ExamplePrompts((event, messageInput) => {
|
||||
if (isStreaming) {
|
||||
handleStop?.();
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage?.(event, messageInput);
|
||||
})}
|
||||
handleSendMessage?.(event, messageInput);
|
||||
})}
|
||||
{!chatStarted && <StarterTemplates />}
|
||||
</div>
|
||||
</div>
|
||||
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
|
||||
</div>
|
||||
|
||||
37
app/components/chat/StarterTemplates.tsx
Normal file
37
app/components/chat/StarterTemplates.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import type { Template } from '~/types/template';
|
||||
import { STARTER_TEMPLATES } from '~/utils/constants';
|
||||
|
||||
interface FrameworkLinkProps {
|
||||
template: Template;
|
||||
}
|
||||
|
||||
const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
|
||||
<a
|
||||
href={`/git?url=https://github.com/${template.githubRepo}.git`}
|
||||
data-state="closed"
|
||||
data-discover="true"
|
||||
className="items-center justify-center "
|
||||
>
|
||||
<div
|
||||
className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-75 transition-all`}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
const StarterTemplates: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>
|
||||
<div className="flex justify-center">
|
||||
<div className="flex w-70 flex-wrap items-center justify-center gap-4">
|
||||
{STARTER_TEMPLATES.map((template) => (
|
||||
<FrameworkLink key={template.name} template={template} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StarterTemplates;
|
||||
@@ -5,7 +5,6 @@ import { toast } from 'react-toastify';
|
||||
import { db, deleteById, getAll } from '~/lib/persistence';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import styles from '~/components/settings/Settings.module.scss';
|
||||
|
||||
// List of supported providers that can have API keys
|
||||
const API_KEY_PROVIDERS = [
|
||||
@@ -25,8 +24,6 @@ const API_KEY_PROVIDERS = [
|
||||
'AzureOpenAI',
|
||||
] as const;
|
||||
|
||||
type Provider = typeof API_KEY_PROVIDERS[number];
|
||||
|
||||
interface ApiKeys {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -52,6 +49,7 @@ export default function DataTab() {
|
||||
const error = new Error('Database is not available');
|
||||
logStore.logError('Failed to export chats - DB unavailable', error);
|
||||
toast.error('Database is not available');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,11 +81,13 @@ export default function DataTab() {
|
||||
const error = new Error('Database is not available');
|
||||
logStore.logError('Failed to delete chats - DB unavailable', error);
|
||||
toast.error('Database is not available');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
|
||||
const allChats = await getAll(db);
|
||||
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
|
||||
logStore.logSystem('All chats deleted successfully', { count: allChats.length });
|
||||
@@ -125,16 +125,22 @@ export default function DataTab() {
|
||||
|
||||
const handleImportSettings = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const settings = JSON.parse(e.target?.result as string);
|
||||
|
||||
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
if (key === 'bolt_theme') {
|
||||
if (value) localStorage.setItem(key, value as string);
|
||||
if (value) {
|
||||
localStorage.setItem(key, value as string);
|
||||
}
|
||||
} else if (value) {
|
||||
Cookies.set(key, value as string);
|
||||
}
|
||||
@@ -152,14 +158,14 @@ export default function DataTab() {
|
||||
|
||||
const handleExportApiKeyTemplate = () => {
|
||||
const template: ApiKeys = {};
|
||||
API_KEY_PROVIDERS.forEach(provider => {
|
||||
API_KEY_PROVIDERS.forEach((provider) => {
|
||||
template[`${provider}_API_KEY`] = '';
|
||||
});
|
||||
|
||||
template['OPENAI_LIKE_API_BASE_URL'] = '';
|
||||
template['LMSTUDIO_API_BASE_URL'] = '';
|
||||
template['OLLAMA_API_BASE_URL'] = '';
|
||||
template['TOGETHER_API_BASE_URL'] = '';
|
||||
template.OPENAI_LIKE_API_BASE_URL = '';
|
||||
template.LMSTUDIO_API_BASE_URL = '';
|
||||
template.OLLAMA_API_BASE_URL = '';
|
||||
template.TOGETHER_API_BASE_URL = '';
|
||||
|
||||
downloadAsJson(template, 'api-keys-template.json');
|
||||
toast.success('API keys template exported successfully');
|
||||
@@ -167,17 +173,22 @@ export default function DataTab() {
|
||||
|
||||
const handleImportApiKeys = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const apiKeys = JSON.parse(e.target?.result as string);
|
||||
let importedCount = 0;
|
||||
const consolidatedKeys: Record<string, string> = {};
|
||||
|
||||
API_KEY_PROVIDERS.forEach(provider => {
|
||||
API_KEY_PROVIDERS.forEach((provider) => {
|
||||
const keyName = `${provider}_API_KEY`;
|
||||
|
||||
if (apiKeys[keyName]) {
|
||||
consolidatedKeys[provider] = apiKeys[keyName];
|
||||
importedCount++;
|
||||
@@ -187,13 +198,14 @@ export default function DataTab() {
|
||||
if (importedCount > 0) {
|
||||
// Store all API keys in a single cookie as JSON
|
||||
Cookies.set('apiKeys', JSON.stringify(consolidatedKeys));
|
||||
|
||||
|
||||
// Also set individual cookies for backward compatibility
|
||||
Object.entries(consolidatedKeys).forEach(([provider, key]) => {
|
||||
Cookies.set(`${provider}_API_KEY`, key);
|
||||
});
|
||||
|
||||
toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`);
|
||||
|
||||
// Reload the page after a short delay to allow the toast to be seen
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
@@ -203,12 +215,13 @@ export default function DataTab() {
|
||||
}
|
||||
|
||||
// Set base URLs if they exist
|
||||
['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(baseUrl => {
|
||||
if (apiKeys[baseUrl]) {
|
||||
Cookies.set(baseUrl, apiKeys[baseUrl]);
|
||||
}
|
||||
});
|
||||
|
||||
['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(
|
||||
(baseUrl) => {
|
||||
if (apiKeys[baseUrl]) {
|
||||
Cookies.set(baseUrl, apiKeys[baseUrl]);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error('Failed to import API keys. Make sure the file is a valid JSON file.');
|
||||
console.error('Failed to import API keys:', error);
|
||||
@@ -226,9 +239,7 @@ export default function DataTab() {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h4 className="text-bolt-elements-textPrimary mb-2">Chat History</h4>
|
||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">
|
||||
Export or delete all your chat history.
|
||||
</p>
|
||||
<p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleExportAllChats}
|
||||
@@ -241,7 +252,7 @@ export default function DataTab() {
|
||||
disabled={isDeleting}
|
||||
className={classNames(
|
||||
'px-4 py-2 bg-bolt-elements-button-danger-background hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text rounded-lg transition-colors',
|
||||
isDeleting ? 'opacity-50 cursor-not-allowed' : ''
|
||||
isDeleting ? 'opacity-50 cursor-not-allowed' : '',
|
||||
)}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete All Chats'}
|
||||
@@ -263,12 +274,7 @@ export default function DataTab() {
|
||||
</button>
|
||||
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
|
||||
Import Settings
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImportSettings}
|
||||
className="hidden"
|
||||
/>
|
||||
<input type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,12 +293,7 @@ export default function DataTab() {
|
||||
</button>
|
||||
<label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
|
||||
Import API Keys
|
||||
<input
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImportApiKeys}
|
||||
className="hidden"
|
||||
/>
|
||||
<input type="file" accept=".json" onChange={handleImportApiKeys} className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,4 +302,4 @@ export default function DataTab() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
8
app/types/template.ts
Normal file
8
app/types/template.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface Template {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
githubRepo: string;
|
||||
tags?: string[];
|
||||
icon?: string;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { IProviderSetting } from '~/types/model';
|
||||
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { Template } from '~/types/template';
|
||||
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||
@@ -359,3 +360,96 @@ async function initializeModelList(options: {
|
||||
|
||||
// initializeModelList({})
|
||||
export { initializeModelList, providerBaseUrlEnvKeys, MODEL_LIST };
|
||||
|
||||
// starter Templates
|
||||
|
||||
export const STARTER_TEMPLATES: Template[] = [
|
||||
{
|
||||
name: 'bolt-astro-basic',
|
||||
label: 'Astro Basic',
|
||||
description: 'Lightweight Astro starter template for building fast static websites',
|
||||
githubRepo: 'thecodacus/bolt-astro-basic-template',
|
||||
tags: ['astro', 'blog', 'performance'],
|
||||
icon: 'i-bolt:astro',
|
||||
},
|
||||
{
|
||||
name: 'bolt-nextjs-shadcn',
|
||||
label: 'Next.js with shadcn/ui',
|
||||
description: 'Next.js starter fullstack template integrated with shadcn/ui components and styling system',
|
||||
githubRepo: 'thecodacus/bolt-nextjs-shadcn-template',
|
||||
tags: ['nextjs', 'react', 'typescript', 'shadcn', 'tailwind'],
|
||||
icon: 'i-bolt:nextjs',
|
||||
},
|
||||
{
|
||||
name: 'bolt-qwik-ts',
|
||||
label: 'Qwik TypeScript',
|
||||
description: 'Qwik framework starter with TypeScript for building resumable applications',
|
||||
githubRepo: 'thecodacus/bolt-qwik-ts-template',
|
||||
tags: ['qwik', 'typescript', 'performance', 'resumable'],
|
||||
icon: 'i-bolt:qwik',
|
||||
},
|
||||
{
|
||||
name: 'bolt-remix-ts',
|
||||
label: 'Remix TypeScript',
|
||||
description: 'Remix framework starter with TypeScript for full-stack web applications',
|
||||
githubRepo: 'thecodacus/bolt-remix-ts-template',
|
||||
tags: ['remix', 'typescript', 'fullstack', 'react'],
|
||||
icon: 'i-bolt:remix',
|
||||
},
|
||||
{
|
||||
name: 'bolt-slidev',
|
||||
label: 'Slidev Presentation',
|
||||
description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
|
||||
githubRepo: 'thecodacus/bolt-slidev-template',
|
||||
tags: ['slidev', 'presentation', 'markdown'],
|
||||
icon: 'i-bolt:slidev',
|
||||
},
|
||||
{
|
||||
name: 'bolt-sveltekit',
|
||||
label: 'SvelteKit',
|
||||
description: 'SvelteKit starter template for building fast, efficient web applications',
|
||||
githubRepo: 'bolt-sveltekit-template',
|
||||
tags: ['svelte', 'sveltekit', 'typescript'],
|
||||
icon: 'i-bolt:svelte',
|
||||
},
|
||||
{
|
||||
name: 'vanilla-vite',
|
||||
label: 'Vanilla + Vite',
|
||||
description: 'Minimal Vite starter template for vanilla JavaScript projects',
|
||||
githubRepo: 'thecodacus/vanilla-vite-template',
|
||||
tags: ['vite', 'vanilla-js', 'minimal'],
|
||||
icon: 'i-bolt:vite',
|
||||
},
|
||||
{
|
||||
name: 'bolt-vite-react',
|
||||
label: 'React + Vite + typescript',
|
||||
description: 'React starter template powered by Vite for fast development experience',
|
||||
githubRepo: 'thecodacus/bolt-vite-react-ts-template',
|
||||
tags: ['react', 'vite', 'frontend'],
|
||||
icon: 'i-bolt:react',
|
||||
},
|
||||
{
|
||||
name: 'bolt-vite-ts',
|
||||
label: 'Vite + TypeScript',
|
||||
description: 'Vite starter template with TypeScript configuration for type-safe development',
|
||||
githubRepo: 'thecodacus/bolt-vite-ts-template',
|
||||
tags: ['vite', 'typescript', 'minimal'],
|
||||
icon: 'i-bolt:typescript',
|
||||
},
|
||||
{
|
||||
name: 'bolt-vue',
|
||||
label: 'Vue.js',
|
||||
description: 'Vue.js starter template with modern tooling and best practices',
|
||||
githubRepo: 'thecodacus/bolt-vue-template',
|
||||
tags: ['vue', 'typescript', 'frontend'],
|
||||
icon: 'i-bolt:vue',
|
||||
},
|
||||
{
|
||||
name: 'bolt-angular',
|
||||
label: 'Angular Starter',
|
||||
description: 'A modern Angular starter template with TypeScript support and best practices configuration',
|
||||
githubRepo: 'thecodacus/bolt-angular-template',
|
||||
tags: ['angular', 'typescript', 'frontend', 'spa'],
|
||||
icon: 'i-bolt:angular',
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user