From c3620b29aac6ec38cbb3f882c11eccea649eb32e Mon Sep 17 00:00:00 2001 From: lee-at-zoo-corp Date: Fri, 20 Jun 2025 16:39:18 -0400 Subject: [PATCH] wip --- src/components/HomeSearchBar.tsx | 2 + src/components/PromptCard.tsx | 38 +++++-- src/lib/singletons.ts | 24 +++- src/lib/sorting.ts | 6 +- src/machines/machineConstants.ts | 1 + src/machines/mlEphantManagerMachine.ts | 152 ++++++++++++++++++++++--- src/routes/Home.tsx | 68 ++++++++--- 7 files changed, 244 insertions(+), 47 deletions(-) diff --git a/src/components/HomeSearchBar.tsx b/src/components/HomeSearchBar.tsx index 5b1422278..4ddae749d 100644 --- a/src/components/HomeSearchBar.tsx +++ b/src/components/HomeSearchBar.tsx @@ -11,11 +11,13 @@ export type HomeItem = Project | Prompt export type HomeItems = Project[] | Prompt[] export const areHomeItemsProjects = (items: HomeItems): items is Project[] => { + if (items.length === 0) return true const item = items[0] return item !== undefined && 'path' in item } export const areHomeItemsPrompts = (items: HomeItems): items is Prompt[] => { + if (items.length === 0) return true const item = items[0] return item !== undefined && 'prompt' in item } diff --git a/src/components/PromptCard.tsx b/src/components/PromptCard.tsx index 4e18b2b0b..11e04c8f8 100644 --- a/src/components/PromptCard.tsx +++ b/src/components/PromptCard.tsx @@ -1,30 +1,40 @@ import ms from 'ms' import type { Prompt } from '@src/lib/prompt' -interface PromptCardProps extends Prompt { - onAction?: (id: Prompt['id']) => void +export interface PromptCardProps extends Prompt { + disabled?: boolean + onAction?: (id: Prompt['id'], prompt: Prompt['prompt']) => void onDelete?: (id: Prompt['id']) => void onFeedback?: (id: string, feedback: Prompt['feedback']) => void } export const PromptFeedback = (props: { id: Prompt['id'] - selected: Prompt['feedback'] + selected?: Prompt['feedback'] + disabled?: boolean onFeedback: (id: Prompt['id'], feedback: Prompt['feedback']) => void }) => { - const cssUp = 'border-green-300' - const cssDown = 'border-red-300' + const cssUp = + props.selected === undefined || props.selected === 'thumbs_up' + ? 'border-green-300' + : 'border-green-100 text-chalkboard-60' + const cssDown = + props.selected === undefined || props.selected === 'thumbs_down' + ? 'border-red-300' + : 'border-red-100 text-chalkboard-60' return (
@@ -49,15 +64,19 @@ export const PromptCardActionButton = (props: { } export const PromptCard = (props: PromptCardProps) => { + const cssCard = 'flex flex-col border rounded-md p-2 gap-2 justify-between' + return ( -
+
{props.prompt}
- + {/* TODO: */} + {/* */} props.onFeedback?.(...args)} />
@@ -65,7 +84,8 @@ export const PromptCard = (props: PromptCardProps) => {
props.onAction?.(props.id)} + disabled={props.disabled} + onClick={() => props.onAction?.(props.id, props.prompt)} />
{ms(new Date(props.created_at).getTime(), { long: true })} ago diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index 0f0d9ef5e..770334778 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -26,6 +26,10 @@ import { engineStreamContextCreate, engineStreamMachine, } from '@src/machines/engineStreamMachine' +import { + mlEphantDefaultContext, + mlEphantManagerMachine, +} from '@src/machines/mlEphantManagerMachine' import { ACTOR_IDS } from '@src/machines/machineConstants' import { settingsMachine } from '@src/machines/settingsMachine' import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop' @@ -117,13 +121,21 @@ if (typeof window !== 'undefined') { }, }) } -const { AUTH, SETTINGS, SYSTEM_IO, ENGINE_STREAM, COMMAND_BAR, BILLING } = - ACTOR_IDS +const { + AUTH, + SETTINGS, + SYSTEM_IO, + ENGINE_STREAM, + MLEPHANT_MANAGER, + COMMAND_BAR, + BILLING, +} = ACTOR_IDS const appMachineActors = { [AUTH]: authMachine, [SETTINGS]: settingsMachine, [SYSTEM_IO]: isDesktop() ? systemIOMachineDesktop : systemIOMachineWeb, [ENGINE_STREAM]: engineStreamMachine, + [MLEPHANT_MANAGER]: mlEphantManagerMachine, [COMMAND_BAR]: commandBarMachine, [BILLING]: billingMachine, } as const @@ -157,6 +169,10 @@ const appMachine = setup({ systemId: ENGINE_STREAM, input: engineStreamContextCreate(), }), + spawnChild(appMachineActors[MLEPHANT_MANAGER], { + systemId: MLEPHANT_MANAGER, + input: mlEphantDefaultContext(), + }), spawnChild(appMachineActors[SYSTEM_IO], { systemId: SYSTEM_IO, }), @@ -226,6 +242,10 @@ export const engineStreamActor = appActor.system.get( ENGINE_STREAM ) as ActorRefFrom<(typeof appMachineActors)[typeof ENGINE_STREAM]> +export const mlEphantManagerActor = appActor.system.get( + MLEPHANT_MANAGER +) as ActorRefFrom<(typeof appMachineActors)[typeof MLEPHANT_MANAGER]> + export const commandBarActor = appActor.system.get(COMMAND_BAR) as ActorRefFrom< (typeof appMachineActors)[typeof COMMAND_BAR] > diff --git a/src/lib/sorting.ts b/src/lib/sorting.ts index d7e4ec217..1cb85d8f5 100644 --- a/src/lib/sorting.ts +++ b/src/lib/sorting.ts @@ -68,8 +68,10 @@ export function getPromptSortFunction(sortBy: string) { const sortByModified = (a: Prompt, b: Prompt) => { if (a.created_at && b.created_at) { - const aDate = new Date(a.created_at) - const bDate = new Date(b.created_at) + // INTENTIONALLY REVERSED + // Will not show properly otherwise. + const aDate = new Date(b.created_at) + const bDate = new Date(a.created_at) return !sortBy || sortBy.includes('desc') ? bDate.getTime() - aDate.getTime() : aDate.getTime() - bDate.getTime() diff --git a/src/machines/machineConstants.ts b/src/machines/machineConstants.ts index b8215dfa9..b1bbfce30 100644 --- a/src/machines/machineConstants.ts +++ b/src/machines/machineConstants.ts @@ -3,6 +3,7 @@ export const ACTOR_IDS = { SETTINGS: 'settings', SYSTEM_IO: 'systemIO', ENGINE_STREAM: 'engine_stream', + MLEPHANT_MANAGER: 'mlephant_manager', COMMAND_BAR: 'command_bar', BILLING: 'billing', } as const diff --git a/src/machines/mlEphantManagerMachine.ts b/src/machines/mlEphantManagerMachine.ts index 92a71d63d..11a7599cf 100644 --- a/src/machines/mlEphantManagerMachine.ts +++ b/src/machines/mlEphantManagerMachine.ts @@ -1,7 +1,14 @@ -import { setup, fromPromise } from 'xstate' +import { assign, setup, fromPromise } from 'xstate' +import type { ActorRefFrom } from 'xstate' import type { Prompt } from '@src/lib/prompt' import { generateFakeSubmittedPrompt } from '@src/lib/prompt' +export enum MlEphantManagerStates { + Setup = 'setup', + Idle = 'idle', + Pending = 'pending', +} + export enum MlEphantManagerTransitionStates { GetPromptsThatCreatedProjects = 'get-prompts-that-created-projects', GetPromptsBelongingToProject = 'get-prompts-belonging-to-project', @@ -23,12 +30,14 @@ export type MlEphantManagerEvents = } | { type: MlEphantManagerTransitionStates.PromptCreateModel - // May or may not belong to a project. - projectId?: string + // For now we fake this, using project_name. + projectId: string + prompt: string } | { type: MlEphantManagerTransitionStates.PromptEditModel projectId: string + prompt: string } | { type: MlEphantManagerTransitionStates.PromptRate @@ -43,10 +52,8 @@ export type MlEphantManagerEvents = promptId: string } -export enum MlEphantManagerStates { - Idle = 'idle', - Pending = 'pending', -} +// Used to specify a specific event in input properties +type XSEvent = Extract export interface MlEphantManagerContext { promptsThatCreatedProjects: Map @@ -54,7 +61,7 @@ export interface MlEphantManagerContext { promptsBelongingToProject?: Map } -export const mlEphantDefaultContext = Object.freeze({ +export const mlEphantDefaultContext = () => ({ promptsThatCreatedProjects: new Map(), promptsBelongingToProject: undefined, hasPendingPrompts: false, @@ -67,37 +74,146 @@ export const mlEphantManagerMachine = setup({ }, actors: { [MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects]: - fromPromise(async function (args) { - console.log(arguments) - return { - promptsThatCreatedProjects: new Array(13) - .fill(undefined) - .map(generateFakeSubmittedPrompt), + fromPromise(async function (args: { + input: { + context: MlEphantManagerContext } + }): Promise> { + return new Promise((resolve) => { + setTimeout(() => { + const results = new Array(13) + .fill(undefined) + .map(generateFakeSubmittedPrompt) + + const promptsThatCreatedProjects = new Map( + args.input.context.promptsThatCreatedProjects + ) + results.forEach((result) => { + promptsThatCreatedProjects.set(result.id, result) + }) + resolve({ + promptsThatCreatedProjects, + }) + }, 2000) + }) }), + [MlEphantManagerTransitionStates.PromptCreateModel]: fromPromise( + async function (args: { + input: { + event: XSEvent + context: MlEphantManagerContext + } + }): Promise> { + return new Promise((resolve) => { + setTimeout(() => { + const promptsThatCreatedProjects = new Map( + args.input.context.promptsThatCreatedProjects + ) + + const result = generateFakeSubmittedPrompt() + promptsThatCreatedProjects.set(result.id, result) + + resolve({ + promptsThatCreatedProjects, + }) + }, 5000) + }) + } + ), + [MlEphantManagerTransitionStates.PromptPollStatus]: fromPromise( + async function (args: { + input: { + context: MlEphantManagerContext + } + }): Promise> { + return new Promise((resolve) => { + setTimeout(() => { + const promptsThatCreatedProjects = new Map( + args.input.context.promptsThatCreatedProjects + ) + // Do the same for prompts of a project + promptsThatCreatedProjects.values().forEach((prompt) => { + if (prompt.status !== 'pending') return + prompt.status = 'completed' + }) + resolve({ + promptsThatCreatedProjects, + }) + }, 3000) + }) + } + ), }, }).createMachine({ - initial: MlEphantManagerStates.Idle, - context: mlEphantDefaultContext, + initial: MlEphantManagerStates.Setup, + context: mlEphantDefaultContext(), states: { + [MlEphantManagerStates.Setup]: { + invoke: { + input: (args: { context: MlEphantManagerContext }) => args, + src: MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects, + onDone: { + target: MlEphantManagerStates.Idle, + actions: assign(({ event }) => event.output), + }, + onError: { target: MlEphantManagerStates.Idle }, + }, + }, [MlEphantManagerStates.Idle]: { on: { [MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects]: { - target: MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects, + target: + MlEphantManagerStates.Pending + + '.' + + MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects, }, }, }, [MlEphantManagerStates.Pending]: { + initial: MlEphantManagerStates.Idle, states: { + // Pop back out to Idle. + [MlEphantManagerStates.Idle]: { + type: 'final', + target: MlEphantManagerStates.Idle, + }, [MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects]: { invoke: { - input: (args: any) => args, + input: (args) => ({ + context: args.context, + }), src: MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects, onDone: { target: MlEphantManagerStates.Idle }, onError: { target: MlEphantManagerStates.Idle }, }, }, + [MlEphantManagerTransitionStates.PromptCreateModel]: { + invoke: { + input: (args) => ({ + event: + args.event as XSEvent, + context: args.context, + }), + src: MlEphantManagerTransitionStates.PromptCreateModel, + onDone: { target: MlEphantManagerStates.PromptPollStatus }, + onError: { target: MlEphantManagerStates.PromptPollStatus }, + }, + }, + [MlEphantManagerTransitionStates.PromptPollStatus]: { + invoke: { + input: (args) => ({ + event: + args.event as XSEvent, + context: args.context, + }), + src: MlEphantManagerTransitionStates.PromptPollStatus, + onDone: { target: MlEphantManagerStates.Idle }, + onError: { target: MlEphantManagerStates.Idle }, + }, + }, }, }, }, }) + +export type MlEphantManagerActor = ActorRefFrom diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 320279d73..5a0042ac8 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,4 +1,5 @@ import type { FormEvent, HTMLProps } from 'react' +import { useSelector } from '@xstate/react' import { useEffect, useState } from 'react' import { toast } from 'react-hot-toast' import { useHotkeys } from 'react-hotkeys-hook' @@ -29,7 +30,6 @@ import { PATHS } from '@src/lib/paths' import { markOnce } from '@src/lib/performance' import type { Project } from '@src/lib/project' import type { Prompt } from '@src/lib/prompt' -import { generateFakeSubmittedPrompt } from '@src/lib/prompt' import { getNextSearchParams, getProjectSortFunction, @@ -44,6 +44,7 @@ import { kclManager, authActor, billingActor, + mlEphantManagerActor, systemIOActor, useSettings, } from '@src/lib/singletons' @@ -73,6 +74,7 @@ import { defaultLocalStatusBarItems, defaultGlobalStatusBarItems, } from '@src/components/StatusBar/defaultStatusBarItems' +import { MlEphantManagerStates } from '@src/machines/mlEphantManagerMachine' type ReadWriteProjectState = { value: boolean @@ -217,15 +219,10 @@ const Home = () => { } ) const projects = useFolders() - const prompts: Prompt[] = [ - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - generateFakeSubmittedPrompt(), - ] + const prompts = useSelector(mlEphantManagerActor, (actor) => { + return actor.context.promptsThatCreatedProjects + }) + const [tabSelected, setTabSelected] = useState( HomeTabKeys.Projects ) @@ -236,17 +233,23 @@ const Home = () => { const onChangeTab = (key: HomeTabKeys) => { setTabSelected(key) - switch (key) { + } + + useEffect(() => { + switch (tabSelected) { case HomeTabKeys.Projects: setItems(projects) break case HomeTabKeys.Prompts: - setItems(prompts) + // Lessons hard learned: VERY important to do this here, and not within + // the useSelector. React will think it's a new value every time, and + // cause this useEffect to fire indefinitely. + setItems(Array.from(prompts.values())) break default: - const _ex: never = key + const _ex: never = tabSelected } - } + }, [tabSelected, prompts, projects]) useEffect(() => { searchAgainst(items)('') @@ -439,6 +442,7 @@ const Home = () => { searchResults={searchResults} sortBy={sortBy} query={query} + settings={settings} />
{tabs.map((el) => (
} function HomeItemsArea(props: HomeItemsAreaProps) { let grid = null + console.log('home items area', props.tabSelected, props.searchResults) + switch (props.tabSelected) { case HomeTabKeys.Projects: grid = areHomeItemsProjects(props.searchResults) ? ( @@ -640,6 +648,7 @@ function HomeItemsArea(props: HomeItemsAreaProps) { searchResults={props.searchResults} query={props.query} sortBy={props.sortBy} + settings={props.settings} /> ) : ( @@ -660,13 +669,26 @@ interface ResultGridPromptsProps { searchResults: Prompt[] query: string sortBy: string + settings: ReturnType } function ResultGridPrompts(props: ResultGridPromptsProps) { // Maybe consider lifting this higher but I see no reason at the moment - const onAction = (...args: any) => { - console.log(args) + const onAction = (_id: Prompt['id'], prompt: Prompt['prompt']) => { + commandBarActor.send({ + type: 'Find and select command', + data: { + groupId: 'application', + name: 'Text-to-CAD', + argDefaultValues: { + method: 'newProject', + prompt, + newProjectName: props.settings.projects.defaultProjectName.current, + }, + }, + }) } + // no-op for now const onDelete = (...args: any) => { console.log(args) } @@ -674,6 +696,16 @@ function ResultGridPrompts(props: ResultGridPromptsProps) { console.log(args) } + const mlEphantManagerSnapshot = mlEphantManagerActor.getSnapshot() + + if (mlEphantManagerSnapshot.matches(MlEphantManagerStates.Setup)) { + return ( +
+ Loading your prompts... +
+ ) + } + return (
{props.searchResults @@ -682,6 +714,10 @@ function ResultGridPrompts(props: ResultGridPromptsProps) {