mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Show app building results (#107)
This commit is contained in:
parent
f5cd0fd9f1
commit
5be90e492f
109
app/components/app-library/ExampleLibraryApps.module.scss
Normal file
109
app/components/app-library/ExampleLibraryApps.module.scss
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appItemError {
|
||||||
|
border: 2px solid #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewImage {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholderImage {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg-subtle);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appTitle {
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error,
|
||||||
|
.empty {
|
||||||
|
width: 100%;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--text-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreButton {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: #22c55e;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #16a34a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
92
app/components/app-library/ExampleLibraryApps.tsx
Normal file
92
app/components/app-library/ExampleLibraryApps.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { type BuildAppResult, getRecentApps } from '~/lib/persistence/apps';
|
||||||
|
import styles from './ExampleLibraryApps.module.scss';
|
||||||
|
import { importChat } from '~/lib/persistence/useChatHistory';
|
||||||
|
|
||||||
|
export const ExampleLibraryApps = () => {
|
||||||
|
const [numApps, setNumApps] = useState<number>(6);
|
||||||
|
const [apps, setApps] = useState<BuildAppResult[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchRecentApps() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const recentApps = await getRecentApps(numApps);
|
||||||
|
setApps(recentApps);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch recent apps:', err);
|
||||||
|
setError('Failed to load recent apps');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apps.length < numApps) {
|
||||||
|
fetchRecentApps();
|
||||||
|
}
|
||||||
|
}, [numApps]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className={styles.error}>{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
if (loading) {
|
||||||
|
return <div className={styles.loading}>Loading recent apps...</div>;
|
||||||
|
}
|
||||||
|
return <div className={styles.empty}>No recent apps found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayApps = apps.slice(0, numApps);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{displayApps.map((app) => (
|
||||||
|
<div
|
||||||
|
key={app.appId}
|
||||||
|
className={`${styles.appItem} ${app.outcome !== 'success' ? styles.appItemError : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
importChat(
|
||||||
|
app.title ?? 'Untitled App',
|
||||||
|
app.messages.filter((msg) => {
|
||||||
|
// Workaround an issue where the messages in the database include images
|
||||||
|
// (used to generate the screenshots).
|
||||||
|
if (msg.role == 'assistant' && msg.type == 'image') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{app.imageDataURL ? (
|
||||||
|
<img src={app.imageDataURL} alt={app.title || 'App preview'} className={styles.previewImage} />
|
||||||
|
) : (
|
||||||
|
<div className={styles.placeholderImage}>{app.title || 'No preview'}</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.appTitle}>{app.title || 'Untitled App'}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{loading && <div className={styles.loading}>Loading recent apps...</div>}
|
||||||
|
{!loading && (
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<button
|
||||||
|
className={styles.loadMoreButton}
|
||||||
|
onClick={() => {
|
||||||
|
setNumApps((prev) => prev + 12);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -15,6 +15,7 @@ import * as Tooltip from '@radix-ui/react-tooltip';
|
|||||||
|
|
||||||
import styles from './BaseChat.module.scss';
|
import styles from './BaseChat.module.scss';
|
||||||
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
import { ExamplePrompts } from '~/components/chat/ExamplePrompts';
|
||||||
|
import { ExampleLibraryApps } from '~/components/app-library/ExampleLibraryApps';
|
||||||
|
|
||||||
import FilePreview from './FilePreview';
|
import FilePreview from './FilePreview';
|
||||||
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
|
||||||
@ -358,7 +359,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames('pt-6 px-2 sm:px-6', {
|
className={classNames('px-2 sm:px-6', {
|
||||||
'h-full flex flex-col': chatStarted,
|
'h-full flex flex-col': chatStarted,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@ -377,7 +378,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
|
'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
|
||||||
{
|
{
|
||||||
'sticky bottom-2': chatStarted,
|
'sticky bottom-2': chatStarted,
|
||||||
},
|
},
|
||||||
@ -438,8 +439,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
{!rejectFormOpen && messageInput}
|
{!rejectFormOpen && messageInput}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!chatStarted &&
|
{!chatStarted && (
|
||||||
ExamplePrompts((event, messageInput) => {
|
<>
|
||||||
|
{ExamplePrompts((event: React.UIEvent, messageInput?: string) => {
|
||||||
if (hasPendingMessage) {
|
if (hasPendingMessage) {
|
||||||
handleStop?.();
|
handleStop?.();
|
||||||
return;
|
return;
|
||||||
@ -447,6 +449,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
|
|||||||
|
|
||||||
handleSendMessage?.(event, messageInput);
|
handleSendMessage?.(event, messageInput);
|
||||||
})}
|
})}
|
||||||
|
<div className="text-2xl lg:text-4xl font-bold text-bolt-elements-textPrimary mt-8 mb-4 animate-fade-in text-center max-w-chat mx-auto">
|
||||||
|
Library
|
||||||
|
</div>
|
||||||
|
<div className="text-bolt-elements-textSecondary text-center max-w-chat mx-auto">
|
||||||
|
Browse these auto-generated apps for a place to start
|
||||||
|
</div>
|
||||||
|
<ExampleLibraryApps />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ClientOnly>{() => <Workbench chatStarted={chatStarted} />}</ClientOnly>
|
<ClientOnly>{() => <Workbench chatStarted={chatStarted} />}</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,8 @@ import { useAnimate } from 'framer-motion';
|
|||||||
import { memo, useEffect, useRef, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
import { cssTransition, toast, ToastContainer } from 'react-toastify';
|
||||||
import { useSnapScroll } from '~/lib/hooks';
|
import { useSnapScroll } from '~/lib/hooks';
|
||||||
import { database, handleChatTitleUpdate, useChatHistory, type ResumeChatInfo } from '~/lib/persistence';
|
import { handleChatTitleUpdate, useChatHistory, type ResumeChatInfo } from '~/lib/persistence';
|
||||||
|
import { database } from '~/lib/persistence/chats';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { renderLogger } from '~/utils/logger';
|
import { renderLogger } from '~/utils/logger';
|
||||||
|
@ -6,13 +6,12 @@ const EXAMPLE_PROMPTS = [
|
|||||||
full: 'build an app to get turn by turn directions using the OpenStreetMap API. the directions should be in a clean and easy to read format showing a small map of the turn next to each step. do not show any complete map for the entire route. make sure the directions work on real locations, e.g. getting from santa cruz to san francisco should take about an hour and a half',
|
full: 'build an app to get turn by turn directions using the OpenStreetMap API. the directions should be in a clean and easy to read format showing a small map of the turn next to each step. do not show any complete map for the entire route. make sure the directions work on real locations, e.g. getting from santa cruz to san francisco should take about an hour and a half',
|
||||||
},
|
},
|
||||||
{ text: 'Build a todo app' },
|
{ text: 'Build a todo app' },
|
||||||
{ text: 'Build a simple blog' },
|
|
||||||
{ text: 'Make a space invaders game' },
|
{ text: 'Make a space invaders game' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) {
|
export function ExamplePrompts(sendMessage?: { (event: React.UIEvent, messageInput?: string): void | undefined }) {
|
||||||
return (
|
return (
|
||||||
<div id="examples" className="relative flex flex-col gap-9 w-full max-w-3xl mx-auto flex justify-center mt-6">
|
<div id="examples" className="relative flex flex-col gap-9 w-full max-w-3xl mx-auto flex justify-center mt-4">
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap justify-center gap-2"
|
className="flex flex-wrap justify-center gap-2"
|
||||||
style={{
|
style={{
|
||||||
|
@ -5,7 +5,7 @@ import type { DeploySettingsDatabase } from '~/lib/replay/Deploy';
|
|||||||
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
import { generateRandomId } from '~/lib/replay/ReplayProtocolClient';
|
||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { database } from '~/lib/persistence/db';
|
import { database } from '~/lib/persistence/chats';
|
||||||
import { deployRepository } from '~/lib/replay/Deploy';
|
import { deployRepository } from '~/lib/replay/Deploy';
|
||||||
|
|
||||||
ReactModal.setAppElement('#root');
|
ReactModal.setAppElement('#root');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useParams } from '@remix-run/react';
|
import { useParams } from '@remix-run/react';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { type ChatContents } from '~/lib/persistence/db';
|
import { type ChatContents } from '~/lib/persistence/chats';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
|
import { useEditChatTitle } from '~/lib/hooks/useEditChatDescription';
|
||||||
import { forwardRef, type ForwardedRef } from 'react';
|
import { forwardRef, type ForwardedRef } from 'react';
|
||||||
|
@ -5,7 +5,7 @@ import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from
|
|||||||
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
||||||
import { SettingsWindow } from '~/components/settings/SettingsWindow';
|
import { SettingsWindow } from '~/components/settings/SettingsWindow';
|
||||||
import { SettingsButton } from '~/components/ui/SettingsButton';
|
import { SettingsButton } from '~/components/ui/SettingsButton';
|
||||||
import { database, type ChatContents } from '~/lib/persistence/db';
|
import { database, type ChatContents } from '~/lib/persistence/chats';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { cubicEasingFn } from '~/utils/easings';
|
import { cubicEasingFn } from '~/utils/easings';
|
||||||
import { logger } from '~/utils/logger';
|
import { logger } from '~/utils/logger';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns';
|
import { format, isAfter, isThisWeek, isThisYear, isToday, isYesterday, subDays } from 'date-fns';
|
||||||
import type { ChatContents } from '~/lib/persistence/db';
|
import type { ChatContents } from '~/lib/persistence/chats';
|
||||||
|
|
||||||
type Bin = { category: string; items: ChatContents[] };
|
type Bin = { category: string; items: ChatContents[] };
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { database } from '~/lib/persistence/db';
|
import { database } from '~/lib/persistence/chats';
|
||||||
import { handleChatTitleUpdate } from '~/lib/persistence/useChatHistory';
|
import { handleChatTitleUpdate } from '~/lib/persistence/useChatHistory';
|
||||||
|
|
||||||
interface EditChatDescriptionOptions {
|
interface EditChatDescriptionOptions {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
import type { ChatContents } from '~/lib/persistence/db';
|
import type { ChatContents } from '~/lib/persistence/chats';
|
||||||
|
|
||||||
interface UseSearchFilterOptions {
|
interface UseSearchFilterOptions {
|
||||||
items: ChatContents[];
|
items: ChatContents[];
|
||||||
|
83
app/lib/persistence/apps.ts
Normal file
83
app/lib/persistence/apps.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// Functions for accessing the apps table in the database
|
||||||
|
|
||||||
|
import { getSupabase } from '~/lib/supabase/client';
|
||||||
|
import type { Message } from './message';
|
||||||
|
|
||||||
|
export enum BuildAppOutcome {
|
||||||
|
Success = 'success',
|
||||||
|
Error = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildAppResult {
|
||||||
|
title: string | undefined;
|
||||||
|
elapsedMinutes: number;
|
||||||
|
totalPeanuts: number;
|
||||||
|
imageDataURL: string | undefined;
|
||||||
|
messages: Message[];
|
||||||
|
protocolChatId: string;
|
||||||
|
outcome: BuildAppOutcome;
|
||||||
|
appId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function databaseRowToBuildAppResult(row: any): BuildAppResult {
|
||||||
|
// Determine the outcome based on the result field
|
||||||
|
let outcome = BuildAppOutcome.Error;
|
||||||
|
if (row.outcome === 'success') {
|
||||||
|
outcome = BuildAppOutcome.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: row.title,
|
||||||
|
elapsedMinutes: row.elapsed_minutes || 0,
|
||||||
|
totalPeanuts: row.total_peanuts || 0,
|
||||||
|
imageDataURL: row.image_url,
|
||||||
|
messages: row.messages || [],
|
||||||
|
protocolChatId: row.protocol_chat_id,
|
||||||
|
outcome,
|
||||||
|
appId: row.app_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all apps created within the last X hours
|
||||||
|
* @param hours Number of hours to look back
|
||||||
|
* @returns Array of BuildAppResult objects
|
||||||
|
*/
|
||||||
|
async function getAppsCreatedInLastXHours(hours: number): Promise<BuildAppResult[]> {
|
||||||
|
try {
|
||||||
|
// Calculate the timestamp for X hours ago
|
||||||
|
const hoursAgo = new Date();
|
||||||
|
hoursAgo.setHours(hoursAgo.getHours() - hours);
|
||||||
|
|
||||||
|
const { data, error } = await getSupabase()
|
||||||
|
.from('apps')
|
||||||
|
.select('*')
|
||||||
|
.eq('deleted', false)
|
||||||
|
.gte('created_at', hoursAgo.toISOString())
|
||||||
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching recent apps:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore apps that don't have a title or image.
|
||||||
|
return data.map(databaseRowToBuildAppResult).filter((app) => app.title && app.imageDataURL);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get recent apps:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HOUR_RANGES = [1, 2, 3, 6, 12, 24];
|
||||||
|
|
||||||
|
export async function getRecentApps(numApps: number): Promise<BuildAppResult[]> {
|
||||||
|
let apps: BuildAppResult[] = [];
|
||||||
|
for (const range of HOUR_RANGES) {
|
||||||
|
apps = await getAppsCreatedInLastXHours(range);
|
||||||
|
if (apps.length >= numApps) {
|
||||||
|
return apps.slice(0, numApps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
export * from './db';
|
export * from './chats';
|
||||||
export * from './useChatHistory';
|
export * from './useChatHistory';
|
||||||
|
export * from './apps';
|
||||||
|
@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { database } from './db';
|
import { database } from './chats';
|
||||||
import { createMessagesForRepository, type Message } from './message';
|
import { createMessagesForRepository, type Message } from './message';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
@ -12,14 +12,7 @@ export interface ResumeChatInfo {
|
|||||||
protocolChatResponseId: string;
|
protocolChatResponseId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatHistory() {
|
export async function importChat(title: string, messages: Message[]) {
|
||||||
const { id: mixedId, repositoryId } = useLoaderData<{ id?: string; repositoryId?: string }>() ?? {};
|
|
||||||
|
|
||||||
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
|
||||||
const [resumeChat, setResumeChat] = useState<ResumeChatInfo | undefined>(undefined);
|
|
||||||
const [ready, setReady] = useState<boolean>(!mixedId && !repositoryId);
|
|
||||||
|
|
||||||
const importChat = async (title: string, messages: Message[]) => {
|
|
||||||
try {
|
try {
|
||||||
const chat = await database.createChat(title, messages);
|
const chat = await database.createChat(title, messages);
|
||||||
window.location.href = `/chat/${chat.id}`;
|
window.location.href = `/chat/${chat.id}`;
|
||||||
@ -31,7 +24,14 @@ export function useChatHistory() {
|
|||||||
toast.error('Failed to import chat');
|
toast.error('Failed to import chat');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export function useChatHistory() {
|
||||||
|
const { id: mixedId, repositoryId } = useLoaderData<{ id?: string; repositoryId?: string }>() ?? {};
|
||||||
|
|
||||||
|
const [initialMessages, setInitialMessages] = useState<Message[]>([]);
|
||||||
|
const [resumeChat, setResumeChat] = useState<ResumeChatInfo | undefined>(undefined);
|
||||||
|
const [ready, setReady] = useState<boolean>(!mixedId && !repositoryId);
|
||||||
|
|
||||||
const loadRepository = async (repositoryId: string) => {
|
const loadRepository = async (repositoryId: string) => {
|
||||||
const messages = createMessagesForRepository(`Repository: ${repositoryId}`, repositoryId);
|
const messages = createMessagesForRepository(`Repository: ${repositoryId}`, repositoryId);
|
||||||
|
@ -7,7 +7,7 @@ import { simulationDataVersion } from './SimulationData';
|
|||||||
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
import { assert, generateRandomId, ProtocolClient } from './ReplayProtocolClient';
|
||||||
import { updateDevelopmentServer } from './DevelopmentServer';
|
import { updateDevelopmentServer } from './DevelopmentServer';
|
||||||
import type { Message } from '~/lib/persistence/message';
|
import type { Message } from '~/lib/persistence/message';
|
||||||
import { database } from '~/lib/persistence/db';
|
import { database } from '~/lib/persistence/chats';
|
||||||
import { chatStore } from '~/lib/stores/chat';
|
import { chatStore } from '~/lib/stores/chat';
|
||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
import { getSupabase } from '~/lib/supabase/client';
|
import { getSupabase } from '~/lib/supabase/client';
|
||||||
|
@ -4,7 +4,7 @@ import type { User, Session } from '@supabase/supabase-js';
|
|||||||
import { logStore } from './logs';
|
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/db';
|
import { database } from '~/lib/persistence/chats';
|
||||||
|
|
||||||
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);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import type { ChatContents } from '~/lib/persistence/db';
|
import type { ChatContents } from '~/lib/persistence/chats';
|
||||||
|
|
||||||
export class ChatStore {
|
export class ChatStore {
|
||||||
currentChat = atom<ChatContents | undefined>(undefined);
|
currentChat = atom<ChatContents | undefined>(undefined);
|
||||||
|
35
supabase/migrations/20250425000001_create_apps_table.sql
Normal file
35
supabase/migrations/20250425000001_create_apps_table.sql
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
-- These tables aren't needed anymore.
|
||||||
|
DROP TABLE IF EXISTS public.problems;
|
||||||
|
DROP TABLE IF EXISTS public.problem_comments;
|
||||||
|
|
||||||
|
-- Create apps table
|
||||||
|
CREATE TABLE IF NOT EXISTS public.apps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
title TEXT,
|
||||||
|
elapsed_minutes FLOAT,
|
||||||
|
total_peanuts INTEGER,
|
||||||
|
image_url TEXT,
|
||||||
|
messages JSONB,
|
||||||
|
protocol_chat_id UUID,
|
||||||
|
result TEXT,
|
||||||
|
app_id TEXT,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create updated_at trigger for apps table
|
||||||
|
CREATE TRIGGER update_apps_updated_at
|
||||||
|
BEFORE UPDATE ON public.apps
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Enable Row Level Security
|
||||||
|
ALTER TABLE public.apps ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Create policy to allow all users full access
|
||||||
|
CREATE POLICY "Allow full access to all users" ON public.apps
|
||||||
|
FOR ALL
|
||||||
|
TO public
|
||||||
|
USING (true)
|
||||||
|
WITH CHECK (true);
|
Loading…
Reference in New Issue
Block a user