Skip to content

Commit

Permalink
feat: add Starter template menu in homepage (stackblitz-labs#884)
Browse files Browse the repository at this point in the history
* added icons and component

* updated unocss to add dynamic icons

* removed temp logs

* updated readme
  • Loading branch information
thecodacus authored Dec 24, 2024
1 parent 8185fd5 commit fc4f89f
Show file tree
Hide file tree
Showing 21 changed files with 215 additions and 53 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
- ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
- ✅ Selection tool to target changes visually (@emcconnell)
- ✅ Detect terminal Errors and ask bolt to fix it (@thecodacus)
- ✅ Add Starter Template Options (@thecodacus)
-**HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
-**HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
-**HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
Expand All @@ -71,7 +72,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
- ⬜ Voice prompting
- ⬜ Azure Open AI API Integration
- Perplexity Integration
- Perplexity Integration (@meetpateltech)
- ⬜ Vertex AI Integration

## Features
Expand Down
34 changes: 19 additions & 15 deletions app/components/chat/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}

handleSendMessage?.(event, messageInput);
})}
<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);
})}
{!chatStarted && <StarterTemplates />}
</div>
</div>
<ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
</div>
Expand Down
37 changes: 37 additions & 0 deletions app/components/chat/StarterTemplates.tsx
Original file line number Diff line number Diff line change
@@ -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;
75 changes: 38 additions & 37 deletions app/components/settings/data/DataTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -25,8 +24,6 @@ const API_KEY_PROVIDERS = [
'AzureOpenAI',
] as const;

type Provider = typeof API_KEY_PROVIDERS[number];

interface ApiKeys {
[key: string]: string;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
}
Expand All @@ -152,32 +158,37 @@ 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');
};

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++;
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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}
Expand All @@ -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'}
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -301,4 +302,4 @@ export default function DataTab() {
</div>
</div>
);
}
}
8 changes: 8 additions & 0 deletions app/types/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Template {
name: string;
label: string;
description: string;
githubRepo: string;
tags?: string[];
icon?: string;
}
Loading

0 comments on commit fc4f89f

Please sign in to comment.