mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 02:06:36 +00:00
UI updates for AppSummary messages (#146)
This commit is contained in:
parent
17684daf02
commit
d00e1a605d
@ -3,8 +3,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { type BuildAppResult, type BuildAppSummary, getAppById, getRecentApps } from '~/lib/persistence/apps';
|
import { type BuildAppResult, type BuildAppSummary, getAppById, getRecentApps } from '~/lib/persistence/apps';
|
||||||
import styles from './ExampleLibraryApps.module.scss';
|
import styles from './ExampleLibraryApps.module.scss';
|
||||||
import { getMessagesRepositoryId, parseTestResultsMessage, TEST_RESULTS_CATEGORY } from '~/lib/persistence/message';
|
import { getMessagesRepositoryId } from '~/lib/persistence/message';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
|
import { APP_SUMMARY_CATEGORY } from '~/lib/persistence/messageAppSummary';
|
||||||
|
import { parseAppSummaryMessage } from '~/lib/persistence/messageAppSummary';
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
return new Intl.DateTimeFormat('en-US', {
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
@ -139,9 +141,9 @@ export const ExampleLibraryApps = ({ filterText }: ExampleLibraryAppsProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTestResults = (appContents: BuildAppResult) => {
|
const getAppSummary = (appContents: BuildAppResult) => {
|
||||||
const message = appContents.messages.findLast((message) => message.category === TEST_RESULTS_CATEGORY);
|
const message = appContents.messages.findLast((message) => message.category === APP_SUMMARY_CATEGORY);
|
||||||
return message ? parseTestResultsMessage(message) : [];
|
return message ? parseAppSummaryMessage(message) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAppDetails = (appId: string, appContents: BuildAppResult | null) => {
|
const renderAppDetails = (appId: string, appContents: BuildAppResult | null) => {
|
||||||
@ -150,7 +152,7 @@ export const ExampleLibraryApps = ({ filterText }: ExampleLibraryAppsProps) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testResults = appContents ? getTestResults(appContents) : null;
|
const appSummary = appContents ? getAppSummary(appContents) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.detailView}>
|
<div className={styles.detailView}>
|
||||||
@ -197,34 +199,34 @@ export const ExampleLibraryApps = ({ filterText }: ExampleLibraryAppsProps) => {
|
|||||||
<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 text-bolt-elements-textPrimary">Test Results</div>
|
<div className="text-lg font-semibold mb-2 text-bolt-elements-textPrimary">Test Results</div>
|
||||||
{testResults && (
|
{appSummary?.tests.length && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{testResults.map((result) => (
|
{appSummary.tests.map((test) => (
|
||||||
<div key={result.title} className="flex items-center gap-2">
|
<div key={test.title} className="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className={classNames('w-3 h-3 rounded-full border border-bolt-elements-borderColor', {
|
className={classNames('w-3 h-3 rounded-full border border-bolt-elements-borderColor', {
|
||||||
'bg-green-500': result.status === 'Pass',
|
'bg-green-500': test.status === 'Pass',
|
||||||
'bg-red-500': result.status === 'Fail',
|
'bg-red-500': test.status === 'Fail',
|
||||||
'bg-gray-300': result.status === 'NotRun',
|
'bg-gray-300': test.status === 'NotRun',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{result.recordingId ? (
|
{test.recordingId ? (
|
||||||
<a
|
<a
|
||||||
href={`https://app.replay.io/recording/${result.recordingId}`}
|
href={`https://app.replay.io/recording/${test.recordingId}`}
|
||||||
className="text-bolt-elements-textPrimary hover:text-bolt-elements-textSecondary"
|
className="text-bolt-elements-textPrimary hover:text-bolt-elements-textSecondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{result.title}
|
{test.title}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-bolt-elements-textPrimary">{result.title}</div>
|
<div className="text-bolt-elements-textPrimary">{test.title}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!testResults && <div className="text-bolt-elements-textPrimary">Loading...</div>}
|
{!appSummary && <div className="text-bolt-elements-textPrimary">Loading...</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,10 @@
|
|||||||
import React, { Suspense, useState, useEffect, useRef, useCallback } from 'react';
|
import React, { Suspense, useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import WithTooltip from '~/components/ui/Tooltip';
|
import WithTooltip from '~/components/ui/Tooltip';
|
||||||
import {
|
import { type Message, USER_RESPONSE_CATEGORY } from '~/lib/persistence/message';
|
||||||
parseTestResultsMessage,
|
|
||||||
type Message,
|
|
||||||
TEST_RESULTS_CATEGORY,
|
|
||||||
DESCRIBE_APP_CATEGORY,
|
|
||||||
parseDescribeAppMessage,
|
|
||||||
SEARCH_ARBORETUM_CATEGORY,
|
|
||||||
type AppDescription,
|
|
||||||
parseSearchArboretumResult,
|
|
||||||
FEATURE_DONE_CATEGORY,
|
|
||||||
parseFeatureDoneMessage,
|
|
||||||
USER_RESPONSE_CATEGORY,
|
|
||||||
} from '~/lib/persistence/message';
|
|
||||||
import { MessageContents } from './components/MessageContents';
|
import { MessageContents } from './components/MessageContents';
|
||||||
import { JumpToBottom } from './components/JumpToBottom';
|
import { JumpToBottom } from './components/JumpToBottom';
|
||||||
|
import { APP_SUMMARY_CATEGORY, parseAppSummaryMessage } from '~/lib/persistence/messageAppSummary';
|
||||||
|
|
||||||
interface MessagesProps {
|
interface MessagesProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -25,40 +14,13 @@ interface MessagesProps {
|
|||||||
messages?: Message[];
|
messages?: Message[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAppFeatures(allMessages: Message[], message: Message, index: number) {
|
function renderAppSummary(message: Message, index: number) {
|
||||||
let arboretumDescription: AppDescription | undefined;
|
const appSummary = parseAppSummaryMessage(message);
|
||||||
let appDescription: AppDescription | undefined;
|
|
||||||
switch (message.category) {
|
|
||||||
case DESCRIBE_APP_CATEGORY:
|
|
||||||
appDescription = parseDescribeAppMessage(message);
|
|
||||||
break;
|
|
||||||
case SEARCH_ARBORETUM_CATEGORY: {
|
|
||||||
const result = parseSearchArboretumResult(message);
|
|
||||||
if (result) {
|
|
||||||
arboretumDescription = result.arboretumDescription;
|
|
||||||
appDescription = result.revisedDescription;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!appDescription) {
|
if (!appSummary) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishedFeatures = new Set<string>();
|
|
||||||
for (let i = index; i < allMessages.length; i++) {
|
|
||||||
if (allMessages[i].category == USER_RESPONSE_CATEGORY) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (allMessages[i].category == FEATURE_DONE_CATEGORY) {
|
|
||||||
const result = parseFeatureDoneMessage(allMessages[i]);
|
|
||||||
if (result) {
|
|
||||||
finishedFeatures.add(result.featureDescription);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="message"
|
data-testid="message"
|
||||||
@ -69,64 +31,40 @@ function renderAppFeatures(allMessages: Message[], message: Message, index: numb
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="text-lg font-semibold mb-2">Development Plan</div>
|
<div className="text-lg font-semibold mb-2">Development Plan</div>
|
||||||
<div>{appDescription.description}</div>
|
<div>{appSummary.description}</div>
|
||||||
{arboretumDescription && (
|
|
||||||
<>
|
|
||||||
<div className="text-lg font-semibold mb-2">Prebuilt App</div>
|
|
||||||
<div>I found a prebuilt app that will be a good starting point:</div>
|
|
||||||
<div>{arboretumDescription.description}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="text-lg font-semibold mb-2">Features</div>
|
<div className="text-lg font-semibold mb-2">Features</div>
|
||||||
{appDescription.features.map((feature) => (
|
{appSummary.features.map((feature) => (
|
||||||
<div key={feature} className="flex items-center gap-2">
|
<div key={feature.id} 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-black', {
|
||||||
'bg-gray-300': !finishedFeatures.has(feature),
|
'bg-gray-300': !feature.done,
|
||||||
'bg-green-500': finishedFeatures.has(feature),
|
'bg-green-500': feature.done,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<div>{feature}</div>
|
<div>{feature.description}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
{appSummary.tests.length > 0 && <div className="text-lg font-semibold mb-2">Test Results</div>}
|
||||||
</div>
|
{appSummary.tests.map((test) => (
|
||||||
);
|
<div key={test.title} className="flex items-center gap-2">
|
||||||
}
|
|
||||||
|
|
||||||
function renderTestResults(message: Message, index: number) {
|
|
||||||
const testResults = parseTestResultsMessage(message);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="message"
|
|
||||||
key={index}
|
|
||||||
className={classNames(
|
|
||||||
'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="text-lg font-semibold mb-2">Test Results</div>
|
|
||||||
{testResults.map((result) => (
|
|
||||||
<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-black', {
|
||||||
'bg-green-500': result.status === 'Pass',
|
'bg-green-500': test.status === 'Pass',
|
||||||
'bg-red-500': result.status === 'Fail',
|
'bg-red-500': test.status === 'Fail',
|
||||||
'bg-gray-300': result.status === 'NotRun',
|
'bg-gray-300': test.status === 'NotRun',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{result.recordingId ? (
|
{test.recordingId ? (
|
||||||
<a
|
<a
|
||||||
href={`https://app.replay.io/recording/${result.recordingId}`}
|
href={`https://app.replay.io/recording/${test.recordingId}`}
|
||||||
className="underline hover:text-blue-600"
|
className="underline hover:text-blue-600"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{result.title}
|
{test.title}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div>{result.title}</div>
|
<div>{test.title}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -135,27 +73,6 @@ function renderTestResults(message: Message, index: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFeatureDone(message: Message, index: number) {
|
|
||||||
const result = parseFeatureDoneMessage(message);
|
|
||||||
if (!result) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-testid="message"
|
|
||||||
key={index}
|
|
||||||
className={classNames(
|
|
||||||
'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="text-lg font-semibold mb-2">Feature Done</div>
|
|
||||||
<div>{result.featureDescription}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>(
|
export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>(
|
||||||
({ messages = [], hasPendingMessage = false, pendingMessageStatus = '' }, ref) => {
|
({ messages = [], hasPendingMessage = false, pendingMessageStatus = '' }, ref) => {
|
||||||
const [showDetailMessageIds, setShowDetailMessageIds] = useState<string[]>([]);
|
const [showDetailMessageIds, setShowDetailMessageIds] = useState<string[]>([]);
|
||||||
@ -226,36 +143,21 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>(
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Return whether the test results at index are the last for the associated user response.
|
// Return whether the app summary at index is the last for the associated user response.
|
||||||
const isLastTestResults = (index: number) => {
|
const isLastAppSummary = (index: number) => {
|
||||||
let lastIndex = -1;
|
let lastIndex = -1;
|
||||||
for (let i = index; i < messages.length; i++) {
|
for (let i = index; i < messages.length; i++) {
|
||||||
const { category } = messages[i];
|
const { category } = messages[i];
|
||||||
if (category === USER_RESPONSE_CATEGORY) {
|
if (category === USER_RESPONSE_CATEGORY) {
|
||||||
return lastIndex === index;
|
return lastIndex === index;
|
||||||
}
|
}
|
||||||
if (category === TEST_RESULTS_CATEGORY) {
|
if (category === APP_SUMMARY_CATEGORY) {
|
||||||
lastIndex = i;
|
lastIndex = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lastIndex === index;
|
return lastIndex === index;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasLaterSearchArboretumMessage = (index: number) => {
|
|
||||||
for (let i = index + 1; i < messages.length; i++) {
|
|
||||||
const { category } = messages[i];
|
|
||||||
if (category === USER_RESPONSE_CATEGORY) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Only return on successful searches. Failed searches do not have
|
|
||||||
// a valid result.
|
|
||||||
if (category === SEARCH_ARBORETUM_CATEGORY && parseSearchArboretumResult(messages[i])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMessage = (message: Message, index: number) => {
|
const renderMessage = (message: Message, index: number) => {
|
||||||
const { role, repositoryId } = message;
|
const { role, repositoryId } = message;
|
||||||
const isUserMessage = role === 'user';
|
const isUserMessage = role === 'user';
|
||||||
@ -266,29 +168,12 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>(
|
|||||||
const lastUserResponse = getLastUserResponse(index);
|
const lastUserResponse = getLastUserResponse(index);
|
||||||
const showDetails = !lastUserResponse || showDetailMessageIds.includes(lastUserResponse.id);
|
const showDetails = !lastUserResponse || showDetailMessageIds.includes(lastUserResponse.id);
|
||||||
|
|
||||||
if (message.category === DESCRIBE_APP_CATEGORY) {
|
if (message.category === APP_SUMMARY_CATEGORY) {
|
||||||
// We only render the DescribeApp if there is no later arboretum match,
|
// The default view only shows the last app summary for each user response.
|
||||||
// which will be rendered instead.
|
if (!isLastAppSummary(index) && !showDetails) {
|
||||||
if (hasLaterSearchArboretumMessage(index) && !showDetails) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return renderAppFeatures(messages, message, index);
|
return renderAppSummary(message, index);
|
||||||
}
|
|
||||||
|
|
||||||
if (message.category === TEST_RESULTS_CATEGORY) {
|
|
||||||
// The default view only shows the last test results for each user response.
|
|
||||||
if (!isLastTestResults(index) && !showDetails) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return renderTestResults(message, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.category === SEARCH_ARBORETUM_CATEGORY) {
|
|
||||||
return renderAppFeatures(messages, message, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.category === FEATURE_DONE_CATEGORY && showDetails) {
|
|
||||||
return renderFeatureDone(message, index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showDetails) {
|
if (!showDetails) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Client messages match the format used by the Nut protocol.
|
// Client messages match the format used by the Nut protocol.
|
||||||
|
|
||||||
import { generateId } from '~/utils/fileUtils';
|
import { generateId } from '~/utils/fileUtils';
|
||||||
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
|
||||||
|
|
||||||
type MessageRole = 'user' | 'assistant';
|
type MessageRole = 'user' | 'assistant';
|
||||||
|
|
||||||
@ -31,22 +30,12 @@ export interface MessageImage extends MessageBase {
|
|||||||
|
|
||||||
export type Message = MessageText | MessageImage;
|
export type Message = MessageText | MessageImage;
|
||||||
|
|
||||||
function ignoreMessageRepositoryId(message: Message) {
|
|
||||||
if (message.category === SEARCH_ARBORETUM_CATEGORY) {
|
|
||||||
// Repositories associated with Arboretum search results have details abstracted
|
|
||||||
// and shouldn't be displayed in the UI. We should get a new message shortly
|
|
||||||
// afterwards with the repository instantiated for details in this request.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the repositoryId before any changes in the message at the given index.
|
// Get the repositoryId before any changes in the message at the given index.
|
||||||
export function getPreviousRepositoryId(messages: Message[], index: number): string | undefined {
|
export function getPreviousRepositoryId(messages: Message[], index: number): string | undefined {
|
||||||
for (let i = index - 1; i >= 0; i--) {
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
const message = messages[i];
|
const message = messages[i];
|
||||||
|
|
||||||
if (message.repositoryId && !ignoreMessageRepositoryId(message)) {
|
if (message.repositoryId) {
|
||||||
return message.repositoryId;
|
return message.repositoryId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,109 +74,3 @@ export function createMessagesForRepository(title: string, repositoryId: string)
|
|||||||
// Category for the initial response made to every user message.
|
// Category for the initial response made to every user message.
|
||||||
// All messages up to the next UserResponse are responding to this message.
|
// All messages up to the next UserResponse are responding to this message.
|
||||||
export const USER_RESPONSE_CATEGORY = 'UserResponse';
|
export const USER_RESPONSE_CATEGORY = 'UserResponse';
|
||||||
|
|
||||||
export enum PlaywrightTestStatus {
|
|
||||||
Pass = 'Pass',
|
|
||||||
Fail = 'Fail',
|
|
||||||
NotRun = 'NotRun',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlaywrightTestResult {
|
|
||||||
title: string;
|
|
||||||
status: PlaywrightTestStatus;
|
|
||||||
recordingId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message sent whenever tests have been run.
|
|
||||||
export const TEST_RESULTS_CATEGORY = 'TestResults';
|
|
||||||
|
|
||||||
export function parseTestResultsMessage(message: Message): PlaywrightTestResult[] {
|
|
||||||
if (message.type !== 'text') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: PlaywrightTestResult[] = [];
|
|
||||||
const lines = message.content.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
const match = line.match(/TestResult (.*?) (.*?) (.*)/);
|
|
||||||
if (!match) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const [status, recordingId, title] = match.slice(1);
|
|
||||||
results.push({
|
|
||||||
status: status as PlaywrightTestStatus,
|
|
||||||
title,
|
|
||||||
recordingId: recordingId == 'NoRecording' ? undefined : recordingId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message sent after the initial user response to describe the app's features.
|
|
||||||
// Contents are a JSON-stringified AppDescription.
|
|
||||||
export const DESCRIBE_APP_CATEGORY = 'DescribeApp';
|
|
||||||
|
|
||||||
export interface AppDescription {
|
|
||||||
// Short description of the app's overall purpose.
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
// Short descriptions of each feature of the app, in the order they should be implemented.
|
|
||||||
features: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseDescribeAppMessage(message: Message): AppDescription | undefined {
|
|
||||||
try {
|
|
||||||
assert(message.type === 'text', 'Message is not a text message');
|
|
||||||
const appDescription = JSON.parse(message.content) as AppDescription;
|
|
||||||
assert(appDescription.description, 'Missing description');
|
|
||||||
assert(appDescription.features, 'Missing features');
|
|
||||||
return appDescription;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse describe app message', e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message sent when a match was found in the arboretum.
|
|
||||||
// Contents are a JSON-stringified ArboretumMatch.
|
|
||||||
export const SEARCH_ARBORETUM_CATEGORY = 'SearchArboretum';
|
|
||||||
|
|
||||||
export interface BestAppFeatureResult {
|
|
||||||
arboretumRepositoryId: string;
|
|
||||||
arboretumDescription: AppDescription;
|
|
||||||
revisedDescription: AppDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseSearchArboretumResult(message: Message): BestAppFeatureResult | undefined {
|
|
||||||
try {
|
|
||||||
assert(message.type === 'text', 'Message is not a text message');
|
|
||||||
const bestAppFeatureResult = JSON.parse(message.content) as BestAppFeatureResult;
|
|
||||||
assert(bestAppFeatureResult.arboretumRepositoryId, 'Missing arboretum repository id');
|
|
||||||
assert(bestAppFeatureResult.arboretumDescription, 'Missing arboretum description');
|
|
||||||
assert(bestAppFeatureResult.revisedDescription, 'Missing revised description');
|
|
||||||
return bestAppFeatureResult;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse best app feature result message', e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message sent when a feature has finished being implemented.
|
|
||||||
export const FEATURE_DONE_CATEGORY = 'FeatureDone';
|
|
||||||
|
|
||||||
export interface FeatureDoneResult {
|
|
||||||
implementedFeatureIndex: number;
|
|
||||||
featureDescription: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseFeatureDoneMessage(message: Message): FeatureDoneResult | undefined {
|
|
||||||
try {
|
|
||||||
assert(message.type === 'text', 'Message is not a text message');
|
|
||||||
const featureDoneResult = JSON.parse(message.content) as FeatureDoneResult;
|
|
||||||
assert(featureDoneResult.featureDescription, 'Missing feature description');
|
|
||||||
return featureDoneResult;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse feature done message', e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
60
app/lib/persistence/messageAppSummary.ts
Normal file
60
app/lib/persistence/messageAppSummary.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Routines for parsing the current state of the app from backend messages.
|
||||||
|
|
||||||
|
import { assert } from '~/lib/replay/ReplayProtocolClient';
|
||||||
|
import type { Message } from './message';
|
||||||
|
|
||||||
|
// Message sent whenever the app summary is updated.
|
||||||
|
export const APP_SUMMARY_CATEGORY = 'AppSummary';
|
||||||
|
|
||||||
|
export interface AppFeature {
|
||||||
|
id: number;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
// Set when the feature has been implemented and all tests pass.
|
||||||
|
done: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlaywrightTestStatus {
|
||||||
|
Pass = 'Pass',
|
||||||
|
Fail = 'Fail',
|
||||||
|
NotRun = 'NotRun',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppTest {
|
||||||
|
title: string;
|
||||||
|
featureId?: number;
|
||||||
|
status: PlaywrightTestStatus;
|
||||||
|
recordingId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppAbstraction {
|
||||||
|
// Name of the abstraction as referred to in the abstracted description.
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// Value in the original client messages which this abstraction represents.
|
||||||
|
representation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSummary {
|
||||||
|
description: string;
|
||||||
|
abstractions: AppAbstraction[];
|
||||||
|
features: AppFeature[];
|
||||||
|
tests: AppTest[];
|
||||||
|
|
||||||
|
// Any planned feature for which initial code changes have been made but not
|
||||||
|
// all tests are passing yet.
|
||||||
|
inProgressFeatureId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAppSummaryMessage(message: Message): AppSummary | undefined {
|
||||||
|
try {
|
||||||
|
assert(message.category === APP_SUMMARY_CATEGORY, 'Message is not an app summary message');
|
||||||
|
assert(message.type === 'text', 'Message is not a text message');
|
||||||
|
const appSummary = JSON.parse(message.content) as AppSummary;
|
||||||
|
assert(appSummary.description, 'Missing app description');
|
||||||
|
return appSummary;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse feature done message', e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user