mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Support filtering apps (#116)
This commit is contained in:
parent
83380c2f5c
commit
d82183b9cd
@ -16,7 +16,11 @@ const formatDate = (date: Date) => {
|
|||||||
}).format(date);
|
}).format(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExampleLibraryApps = () => {
|
interface ExampleLibraryAppsProps {
|
||||||
|
filterText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExampleLibraryApps = ({ filterText }: ExampleLibraryAppsProps) => {
|
||||||
const [numApps, setNumApps] = useState<number>(6);
|
const [numApps, setNumApps] = useState<number>(6);
|
||||||
const [apps, setApps] = useState<BuildAppSummary[]>([]);
|
const [apps, setApps] = useState<BuildAppSummary[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -53,11 +57,16 @@ export const ExampleLibraryApps = () => {
|
|||||||
})();
|
})();
|
||||||
}, [selectedAppId]);
|
}, [selectedAppId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setApps([]);
|
||||||
|
setNumApps(6);
|
||||||
|
}, [filterText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchRecentApps() {
|
async function fetchRecentApps() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const recentApps = await getRecentApps(numApps);
|
const recentApps = await getRecentApps(numApps, filterText);
|
||||||
setApps(recentApps);
|
setApps(recentApps);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -68,10 +77,8 @@ export const ExampleLibraryApps = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apps.length < numApps) {
|
fetchRecentApps();
|
||||||
fetchRecentApps();
|
}, [numApps, filterText]);
|
||||||
}
|
|
||||||
}, [numApps]);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div className={styles.error}>{error}</div>;
|
return <div className={styles.error}>{error}</div>;
|
||||||
@ -148,7 +155,7 @@ export const ExampleLibraryApps = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.detailView}>
|
<div className={styles.detailView}>
|
||||||
<div className={styles.detailHeader}>
|
<div className={styles.detailHeader}>
|
||||||
<h3 className={styles.detailTitle}>{app.title}</h3>
|
<h3 className={`${styles.detailTitle} text-bolt-elements-textPrimary`}>{app.title}</h3>
|
||||||
<div className={styles.detailActions}>
|
<div className={styles.detailActions}>
|
||||||
<button
|
<button
|
||||||
className={styles.actionButton}
|
className={styles.actionButton}
|
||||||
@ -172,7 +179,7 @@ export const ExampleLibraryApps = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.appDetails}>
|
<div className={`${styles.appDetails} text-bolt-elements-textPrimary`}>
|
||||||
<div className={styles.detailRow}>
|
<div className={styles.detailRow}>
|
||||||
<span className={styles.detailLabel}>Created:</span>
|
<span className={styles.detailLabel}>Created:</span>
|
||||||
<span className={styles.detailValue}>{new Date(app.createdAt).toLocaleString()}</span>
|
<span className={styles.detailValue}>{new Date(app.createdAt).toLocaleString()}</span>
|
||||||
@ -189,13 +196,13 @@ export const ExampleLibraryApps = () => {
|
|||||||
<span className={styles.detailLabel}>Database:</span>
|
<span className={styles.detailLabel}>Database:</span>
|
||||||
<span className={styles.detailValue}>{app.outcome.hasDatabase ? 'Present' : 'None'}</span>
|
<span className={styles.detailValue}>{app.outcome.hasDatabase ? 'Present' : 'None'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-semibold mb-2">Test Results</div>
|
<div className="text-lg font-semibold mb-2 text-bolt-elements-textPrimary">Test Results</div>
|
||||||
{testResults && (
|
{testResults && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{testResults.map((result) => (
|
{testResults.map((result) => (
|
||||||
<div key={result.title} className="flex items-center gap-2">
|
<div key={result.title} className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={classNames('w-3 h-3 rounded-full border border-black', {
|
className={classNames('w-3 h-3 rounded-full border border-bolt-elements-borderColor', {
|
||||||
'bg-green-500': result.status === 'Pass',
|
'bg-green-500': result.status === 'Pass',
|
||||||
'bg-red-500': result.status === 'Fail',
|
'bg-red-500': result.status === 'Fail',
|
||||||
'bg-gray-300': result.status === 'NotRun',
|
'bg-gray-300': result.status === 'NotRun',
|
||||||
@ -204,20 +211,20 @@ export const ExampleLibraryApps = () => {
|
|||||||
{result.recordingId ? (
|
{result.recordingId ? (
|
||||||
<a
|
<a
|
||||||
href={`https://app.replay.io/recording/${result.recordingId}`}
|
href={`https://app.replay.io/recording/${result.recordingId}`}
|
||||||
className="underline hover:text-blue-600"
|
className="text-bolt-elements-textPrimary hover:text-bolt-elements-textSecondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{result.title}
|
{result.title}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div>{result.title}</div>
|
<div className="text-bolt-elements-textPrimary">{result.title}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!testResults && <div>Loading...</div>}
|
{!testResults && <div className="text-bolt-elements-textPrimary">Loading...</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -45,3 +45,20 @@
|
|||||||
fill: url(#shine-gradient);
|
fill: url(#shine-gradient);
|
||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filterInput {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -78,6 +78,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
const [recognition, setRecognition] = useState<SpeechRecognition | null>(null);
|
||||||
const [transcript, setTranscript] = useState('');
|
const [transcript, setTranscript] = useState('');
|
||||||
const [rejectFormOpen, setRejectFormOpen] = useState(false);
|
const [rejectFormOpen, setRejectFormOpen] = useState(false);
|
||||||
|
const [pendingFilterText, setPendingFilterText] = useState('');
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(transcript);
|
console.log(transcript);
|
||||||
@ -455,7 +457,33 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
<div className="text-bolt-elements-textSecondary text-center max-w-chat mx-auto">
|
<div className="text-bolt-elements-textSecondary text-center max-w-chat mx-auto">
|
||||||
Browse these auto-generated apps for a place to start
|
Browse these auto-generated apps for a place to start
|
||||||
</div>
|
</div>
|
||||||
<ExampleLibraryApps />
|
<div
|
||||||
|
className="placeholder-bolt-elements-textTertiary"
|
||||||
|
style={{ display: 'flex', justifyContent: 'center', marginBottom: '1rem' }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
setFilterText(pendingFilterText);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(event) => {
|
||||||
|
setPendingFilterText(event.target.value);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '200px',
|
||||||
|
padding: '0.5rem',
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ExampleLibraryApps filterText={filterText} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
|
|||||||
data-testid="message"
|
data-testid="message"
|
||||||
key={index}
|
key={index}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)] mt-4 bg-bolt-elements-messages-background',
|
'flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)] mt-4 bg-bolt-elements-messages-background text-bolt-elements-textPrimary',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { getSupabase } from '~/lib/supabase/client';
|
import { getSupabase } from '~/lib/supabase/client';
|
||||||
import type { Message } from './message';
|
import type { Message } from './message';
|
||||||
|
import { pingTelemetry } from '~/lib/hooks/pingTelemetry';
|
||||||
|
|
||||||
export interface BuildAppOutcome {
|
export interface BuildAppOutcome {
|
||||||
testsPassed?: boolean;
|
testsPassed?: boolean;
|
||||||
@ -85,12 +86,23 @@ function databaseRowToBuildAppResult(row: any): BuildAppResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appMatchesFilter(app: BuildAppSummary, filterText: string): boolean {
|
||||||
|
// Always filter out apps that didn't get up and running.
|
||||||
|
if (!app.title || !app.imageDataURL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = `${app.title} ${app.prompt}`.toLowerCase();
|
||||||
|
const words = filterText.toLowerCase().split(' ');
|
||||||
|
return words.every((word) => text.includes(word));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all apps created within the last X hours
|
* Get all apps created within the last X hours
|
||||||
* @param hours Number of hours to look back
|
* @param hours Number of hours to look back
|
||||||
* @returns Array of BuildAppResult objects
|
* @returns Array of BuildAppResult objects
|
||||||
*/
|
*/
|
||||||
async function getAppsCreatedInLastXHours(hours: number): Promise<BuildAppSummary[]> {
|
async function getAppsCreatedInLastXHours(hours: number, filterText: string): Promise<BuildAppSummary[]> {
|
||||||
try {
|
try {
|
||||||
// Calculate the timestamp for X hours ago
|
// Calculate the timestamp for X hours ago
|
||||||
const hoursAgo = new Date();
|
const hoursAgo = new Date();
|
||||||
@ -109,19 +121,19 @@ async function getAppsCreatedInLastXHours(hours: number): Promise<BuildAppSummar
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ignore apps that don't have a title or image.
|
// Ignore apps that don't have a title or image.
|
||||||
return data.map(databaseRowToBuildAppSummary).filter((app) => app.title && app.imageDataURL);
|
return data.map(databaseRowToBuildAppSummary).filter((app) => appMatchesFilter(app, filterText));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get recent apps:', error);
|
console.error('Failed to get recent apps:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOUR_RANGES = [1, 2, 3, 6, 12, 24];
|
const HOUR_RANGES = [1, 2, 3, 6, 12, 24, 72];
|
||||||
|
|
||||||
export async function getRecentApps(numApps: number): Promise<BuildAppSummary[]> {
|
export async function getRecentApps(numApps: number, filterText: string): Promise<BuildAppSummary[]> {
|
||||||
let apps: BuildAppSummary[] = [];
|
let apps: BuildAppSummary[] = [];
|
||||||
for (const range of HOUR_RANGES) {
|
for (const range of HOUR_RANGES) {
|
||||||
apps = await getAppsCreatedInLastXHours(range);
|
apps = await getAppsCreatedInLastXHours(range, filterText);
|
||||||
if (apps.length >= numApps) {
|
if (apps.length >= numApps) {
|
||||||
return apps.slice(0, numApps);
|
return apps.slice(0, numApps);
|
||||||
}
|
}
|
||||||
@ -131,7 +143,16 @@ export async function getRecentApps(numApps: number): Promise<BuildAppSummary[]>
|
|||||||
|
|
||||||
export async function getAppById(id: string): Promise<BuildAppResult> {
|
export async function getAppById(id: string): Promise<BuildAppResult> {
|
||||||
console.log('GetAppByIdStart', id);
|
console.log('GetAppByIdStart', id);
|
||||||
|
|
||||||
|
// In local testing we've seen problems where this query hangs.
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
pingTelemetry('GetAppByIdTimeout', {});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
const { data, error } = await getSupabase().from('apps').select('*').eq('id', id).single();
|
const { data, error } = await getSupabase().from('apps').select('*').eq('id', id).single();
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
console.log('GetAppByIdDone', id);
|
console.log('GetAppByIdDone', id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { logStore } from './logs';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { isAuthenticated } from '~/lib/supabase/client';
|
import { isAuthenticated } from '~/lib/supabase/client';
|
||||||
import { database } from '~/lib/persistence/chats';
|
import { database } from '~/lib/persistence/chats';
|
||||||
|
import { pingTelemetry } from '~/lib/hooks/pingTelemetry';
|
||||||
|
|
||||||
export const userStore = atom<User | null>(null);
|
export const userStore = atom<User | null>(null);
|
||||||
export const sessionStore = atom<Session | null>(null);
|
export const sessionStore = atom<Session | null>(null);
|
||||||
@ -36,6 +37,7 @@ export async function initializeAuth() {
|
|||||||
// Handle this by using a timeout to ensure we don't wait indefinitely.
|
// Handle this by using a timeout to ensure we don't wait indefinitely.
|
||||||
const timeoutPromise = new Promise<{ data: { session: Session | null }; error?: AuthError }>((resolve) => {
|
const timeoutPromise = new Promise<{ data: { session: Session | null }; error?: AuthError }>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
pingTelemetry('AuthTimeout', {});
|
||||||
resolve({
|
resolve({
|
||||||
data: { session: null },
|
data: { session: null },
|
||||||
error: new AuthError('Timed out initializing auth'),
|
error: new AuthError('Timed out initializing auth'),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user