Compare commits
7 Commits
jtran/plus
...
lee-at-zoo
Author | SHA1 | Date | |
---|---|---|---|
c3620b29aa | |||
eaa52a9b73 | |||
b0ff9d636c | |||
fa8d07d02c | |||
4a46a246fc | |||
503359093e | |||
3f16204517 |
@ -1343,7 +1343,7 @@ test(
|
|||||||
|
|
||||||
await test.step('should be shorted by modified initially', async () => {
|
await test.step('should be shorted by modified initially', async () => {
|
||||||
const lastModifiedButton = page.getByRole('button', {
|
const lastModifiedButton = page.getByRole('button', {
|
||||||
name: 'Last Modified',
|
name: 'Age',
|
||||||
})
|
})
|
||||||
await expect(lastModifiedButton).toBeVisible()
|
await expect(lastModifiedButton).toBeVisible()
|
||||||
await expect(lastModifiedButton.getByLabel('arrow down')).toBeVisible()
|
await expect(lastModifiedButton.getByLabel('arrow down')).toBeVisible()
|
||||||
@ -1364,7 +1364,7 @@ test(
|
|||||||
|
|
||||||
await test.step('Reverse modified order', async () => {
|
await test.step('Reverse modified order', async () => {
|
||||||
const lastModifiedButton = page.getByRole('button', {
|
const lastModifiedButton = page.getByRole('button', {
|
||||||
name: 'Last Modified',
|
name: 'Age',
|
||||||
})
|
})
|
||||||
await lastModifiedButton.click()
|
await lastModifiedButton.click()
|
||||||
await expect(lastModifiedButton).toBeVisible()
|
await expect(lastModifiedButton).toBeVisible()
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -53,6 +53,7 @@
|
|||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
|
"ms": "^2.1.3",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"re-resizable": "^6.11.2",
|
"re-resizable": "^6.11.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@ -2495,6 +2496,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
"extraneous": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
"json-rpc-2.0": "^1.6.0",
|
"json-rpc-2.0": "^1.6.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
|
"ms": "^2.1.3",
|
||||||
"openid-client": "^5.6.5",
|
"openid-client": "^5.6.5",
|
||||||
"re-resizable": "^6.11.2",
|
"re-resizable": "^6.11.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
95
src/components/HomeSearchBar.tsx
Normal file
95
src/components/HomeSearchBar.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import Fuse from 'fuse.js'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
|
|
||||||
|
import type { Project } from '@src/lib/project'
|
||||||
|
import type { Prompt } from '@src/lib/prompt'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useHomeSearch(initialSearchResults: HomeItems) {
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [searchResults, setSearchResults] =
|
||||||
|
useState<HomeItems>(initialSearchResults)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchResults(initialSearchResults)
|
||||||
|
}, [initialSearchResults])
|
||||||
|
|
||||||
|
const searchAgainst = (items: HomeItems) => (queryRequested: string) => {
|
||||||
|
const nameKeyToMatchAgainst = areHomeItemsProjects(items)
|
||||||
|
? 'name'
|
||||||
|
: 'prompt'
|
||||||
|
|
||||||
|
// Fuse is not happy with HomeItems
|
||||||
|
// @ts-expect-error
|
||||||
|
const fuse = new Fuse(items, {
|
||||||
|
keys: [{ name: nameKeyToMatchAgainst, weight: 0.7 }],
|
||||||
|
includeScore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = fuse.search(queryRequested).map((result) => result.item)
|
||||||
|
|
||||||
|
// On an empty query, we consider that matching all items.
|
||||||
|
setSearchResults(queryRequested.length > 0 ? results : items)
|
||||||
|
setQuery(queryRequested)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchAgainst,
|
||||||
|
searchResults,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomeSearchBar({
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
onChange: (query: string) => void
|
||||||
|
}) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
useHotkeys(
|
||||||
|
'Ctrl+.',
|
||||||
|
(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
inputRef.current?.focus()
|
||||||
|
},
|
||||||
|
{ enableOnFormTags: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="flex items-center gap-2 py-0.5 pl-0.5 pr-2 rounded border-solid border border-primary/10 dark:border-chalkboard-80 focus-within:border-primary dark:focus-within:border-chalkboard-30">
|
||||||
|
<CustomIcon
|
||||||
|
name="search"
|
||||||
|
className="w-5 h-5 rounded-sm bg-primary/10 dark:bg-transparent text-primary dark:text-chalkboard-10 group-focus-within:bg-primary group-focus-within:text-chalkboard-10"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
className="w-full text-sm bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
|
||||||
|
placeholder="Search (Ctrl+.)"
|
||||||
|
autoCapitalize="off"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,64 +0,0 @@
|
|||||||
import Fuse from 'fuse.js'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
|
||||||
|
|
||||||
import { CustomIcon } from '@src/components/CustomIcon'
|
|
||||||
import type { Project } from '@src/lib/project'
|
|
||||||
|
|
||||||
export function useProjectSearch(projects: Project[]) {
|
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const [searchResults, setSearchResults] = useState(projects)
|
|
||||||
|
|
||||||
const fuse = new Fuse(projects, {
|
|
||||||
keys: [{ name: 'name', weight: 0.7 }],
|
|
||||||
includeScore: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
|
||||||
setSearchResults(query.length > 0 ? results : projects)
|
|
||||||
}, [query, projects])
|
|
||||||
|
|
||||||
return {
|
|
||||||
searchResults,
|
|
||||||
query,
|
|
||||||
setQuery,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProjectSearchBar({
|
|
||||||
setQuery,
|
|
||||||
}: {
|
|
||||||
setQuery: (query: string) => void
|
|
||||||
}) {
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
|
||||||
useHotkeys(
|
|
||||||
'Ctrl+.',
|
|
||||||
(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
inputRef.current?.focus()
|
|
||||||
},
|
|
||||||
{ enableOnFormTags: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative group">
|
|
||||||
<div className="flex items-center gap-2 py-0.5 pl-0.5 pr-2 rounded border-solid border border-primary/10 dark:border-chalkboard-80 focus-within:border-primary dark:focus-within:border-chalkboard-30">
|
|
||||||
<CustomIcon
|
|
||||||
name="search"
|
|
||||||
className="w-5 h-5 rounded-sm bg-primary/10 dark:bg-transparent text-primary dark:text-chalkboard-10 group-focus-within:bg-primary group-focus-within:text-chalkboard-10"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
|
||||||
className="w-full text-sm bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
|
|
||||||
placeholder="Search projects (Ctrl+.)"
|
|
||||||
autoCapitalize="off"
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
96
src/components/PromptCard.tsx
Normal file
96
src/components/PromptCard.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import ms from 'ms'
|
||||||
|
import type { Prompt } from '@src/lib/prompt'
|
||||||
|
|
||||||
|
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']
|
||||||
|
disabled?: boolean
|
||||||
|
onFeedback: (id: Prompt['id'], feedback: Prompt['feedback']) => void
|
||||||
|
}) => {
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => props.onFeedback(props.id, 'thumbs_up')}
|
||||||
|
disabled={props.disabled}
|
||||||
|
className={cssUp}
|
||||||
|
>
|
||||||
|
Good
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => props.onFeedback(props.id, 'thumbs_down')}
|
||||||
|
disabled={props.disabled}
|
||||||
|
className={cssDown}
|
||||||
|
>
|
||||||
|
Bad
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptCardActionButton = (props: {
|
||||||
|
status: Prompt['status']
|
||||||
|
disabled?: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-gray-100"
|
||||||
|
onClick={props.onClick}
|
||||||
|
disabled={
|
||||||
|
props.disabled ||
|
||||||
|
props.status === 'queued' ||
|
||||||
|
props.status === 'in_progress'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.status === 'completed' ? 'Create' : 'Pending'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptCard = (props: PromptCardProps) => {
|
||||||
|
const cssCard = 'flex flex-col border rounded-md p-2 gap-2 justify-between'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${cssCard} ${props.disabled ? 'text-chalkboard-60' : ''}`}>
|
||||||
|
<div className="flex flex-row justify-between gap-2">
|
||||||
|
<div>{props.prompt}</div>
|
||||||
|
<div className="w-fit flex flex-col items-end">
|
||||||
|
{/* TODO: */}
|
||||||
|
{/* <button disabled={props.disabled} onClick={() => props.onDelete?.(props.id)}>Delete</button> */}
|
||||||
|
<PromptFeedback
|
||||||
|
id={props.id}
|
||||||
|
selected={props.feedback}
|
||||||
|
disabled={props.disabled}
|
||||||
|
onFeedback={(...args) => props.onFeedback?.(...args)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<PromptCardActionButton
|
||||||
|
status={props.status}
|
||||||
|
disabled={props.disabled}
|
||||||
|
onClick={() => props.onAction?.(props.id, props.prompt)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{ms(new Date(props.created_at).getTime(), { long: true })} ago
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
121
src/lib/prompt.ts
Normal file
121
src/lib/prompt.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import type { Models } from '@kittycad/lib'
|
||||||
|
|
||||||
|
export type Prompt = Models['TextToCad_type']
|
||||||
|
|
||||||
|
// export interface TextToCad_type {
|
||||||
|
// code?: string;
|
||||||
|
// completed_at?: string;
|
||||||
|
// created_at: string;
|
||||||
|
// error?: string;
|
||||||
|
// feedback?: MlFeedback_type;
|
||||||
|
// id: Uuid_type;
|
||||||
|
// kcl_version?: string;
|
||||||
|
// model: TextToCadModel_type;
|
||||||
|
// model_version: string;
|
||||||
|
// output_format: FileExportFormat_type;
|
||||||
|
// outputs: {
|
||||||
|
// [key: string]: string;
|
||||||
|
// };
|
||||||
|
// prompt: string;
|
||||||
|
// started_at?: string;
|
||||||
|
// status: ApiCallStatus_type;
|
||||||
|
// updated_at: string;
|
||||||
|
// user_id: Uuid_type;
|
||||||
|
// }
|
||||||
|
// export interface TextToCadCreateBody_type {
|
||||||
|
// kcl_version?: string;
|
||||||
|
// project_name?: string;
|
||||||
|
// prompt: string;
|
||||||
|
// }
|
||||||
|
// export interface TextToCadIteration_type {
|
||||||
|
// code: string;
|
||||||
|
// completed_at?: string;
|
||||||
|
// created_at: string;
|
||||||
|
// error?: string;
|
||||||
|
// feedback?: MlFeedback_type;
|
||||||
|
// id: Uuid_type;
|
||||||
|
// model: TextToCadModel_type;
|
||||||
|
// model_version: string;
|
||||||
|
// original_source_code: string;
|
||||||
|
// prompt?: string;
|
||||||
|
// source_ranges: SourceRangePrompt_type[];
|
||||||
|
// started_at?: string;
|
||||||
|
// status: ApiCallStatus_type;
|
||||||
|
// updated_at: string;
|
||||||
|
// user_id: Uuid_type;
|
||||||
|
// }
|
||||||
|
// export interface TextToCadIterationBody_type {
|
||||||
|
// kcl_version?: string;
|
||||||
|
// original_source_code: string;
|
||||||
|
// project_name?: string;
|
||||||
|
// prompt?: string;
|
||||||
|
// source_ranges: SourceRangePrompt_type[];
|
||||||
|
// }
|
||||||
|
// export interface TextToCadMultiFileIteration_type {
|
||||||
|
// completed_at?: string;
|
||||||
|
// created_at: string;
|
||||||
|
// error?: string;
|
||||||
|
// feedback?: MlFeedback_type;
|
||||||
|
// id: Uuid_type;
|
||||||
|
// kcl_version?: string;
|
||||||
|
// model: TextToCadModel_type;
|
||||||
|
// model_version: string;
|
||||||
|
// outputs: {
|
||||||
|
// [key: string]: string;
|
||||||
|
// };
|
||||||
|
// project_name?: string;
|
||||||
|
// prompt?: string;
|
||||||
|
// source_ranges: SourceRangePrompt_type[];
|
||||||
|
// started_at?: string;
|
||||||
|
// status: ApiCallStatus_type;
|
||||||
|
// updated_at: string;
|
||||||
|
// user_id: Uuid_type;
|
||||||
|
// }
|
||||||
|
// export interface TextToCadMultiFileIterationBody_type {
|
||||||
|
// kcl_version?: string;
|
||||||
|
// project_name?: string;
|
||||||
|
// prompt?: string;
|
||||||
|
// source_ranges: SourceRangePrompt_type[];
|
||||||
|
// }
|
||||||
|
// export interface TextToCadResultsPage_type {
|
||||||
|
// items: TextToCad_type[];
|
||||||
|
// next_page?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const PROMPTS = [
|
||||||
|
'Generate a step-by-step guide to design a parametric gear with adjustable tooth count.',
|
||||||
|
'Explain how to model a hollow cylinder with internal threads using 3D modeling principles.',
|
||||||
|
'Create a script to generate a customizable box with finger joints using parametric design.',
|
||||||
|
'Suggest best practices for modeling an ergonomic handheld object in CAD.',
|
||||||
|
'Convert this verbal sketch description into structured CAD modeling steps.',
|
||||||
|
'Define geometric constraints for modeling a modular rail profile used in assembly systems.',
|
||||||
|
'How do I design a 3D-printable snap-fit enclosure with proper tolerances?',
|
||||||
|
'Generate geometry instructions for creating a ball-and-socket joint.',
|
||||||
|
'Model a heat-dissipating structure suitable for passive cooling in electronic assemblies.',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const generateFakeSubmittedPrompt = () => ({
|
||||||
|
code: Math.random().toString(),
|
||||||
|
completed_at: Math.random().toString(),
|
||||||
|
created_at: new Date(Math.random() * 100000000).toISOString(),
|
||||||
|
error: Math.random().toString(),
|
||||||
|
// declare type MlFeedback_type = 'thumbs_up' | 'thumbs_down' | 'accepted' | 'rejected';
|
||||||
|
feedback: 'thumbs_up' as Prompt['feedback'],
|
||||||
|
id: Math.random().toString(),
|
||||||
|
kcl_version: Math.random().toString(),
|
||||||
|
model_version: Math.random().toString(),
|
||||||
|
// export declare type TextToCadModel_type = 'cad' | 'kcl' | 'kcl_iteration'; model : 'kcl',
|
||||||
|
model: 'kcl' as Prompt['model'],
|
||||||
|
// export declare type FileExportFormat_type = 'fbx' | 'glb' | 'gltf' | 'obj' | 'ply' | 'step' | 'stl';
|
||||||
|
output_format: 'glb' as Prompt['output_format'],
|
||||||
|
outputs: {
|
||||||
|
[Math.random().toString()]: Math.random().toString(),
|
||||||
|
},
|
||||||
|
prompt: PROMPTS[parseInt((Math.random() * 10).toString()[0])],
|
||||||
|
started_at: new Date(Math.random()).toISOString(),
|
||||||
|
// declare type ApiCallStatus_type = 'queued' | 'uploaded' | 'in_progress' | 'completed' | 'failed';
|
||||||
|
status: 'completed' as Prompt['status'],
|
||||||
|
updated_at: Math.random().toString(),
|
||||||
|
// declare type ApiTokenUuid_type = string;
|
||||||
|
user_id: Math.random().toString(),
|
||||||
|
})
|
@ -26,6 +26,10 @@ import {
|
|||||||
engineStreamContextCreate,
|
engineStreamContextCreate,
|
||||||
engineStreamMachine,
|
engineStreamMachine,
|
||||||
} from '@src/machines/engineStreamMachine'
|
} from '@src/machines/engineStreamMachine'
|
||||||
|
import {
|
||||||
|
mlEphantDefaultContext,
|
||||||
|
mlEphantManagerMachine,
|
||||||
|
} from '@src/machines/mlEphantManagerMachine'
|
||||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||||
import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop'
|
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 } =
|
const {
|
||||||
ACTOR_IDS
|
AUTH,
|
||||||
|
SETTINGS,
|
||||||
|
SYSTEM_IO,
|
||||||
|
ENGINE_STREAM,
|
||||||
|
MLEPHANT_MANAGER,
|
||||||
|
COMMAND_BAR,
|
||||||
|
BILLING,
|
||||||
|
} = ACTOR_IDS
|
||||||
const appMachineActors = {
|
const appMachineActors = {
|
||||||
[AUTH]: authMachine,
|
[AUTH]: authMachine,
|
||||||
[SETTINGS]: settingsMachine,
|
[SETTINGS]: settingsMachine,
|
||||||
[SYSTEM_IO]: isDesktop() ? systemIOMachineDesktop : systemIOMachineWeb,
|
[SYSTEM_IO]: isDesktop() ? systemIOMachineDesktop : systemIOMachineWeb,
|
||||||
[ENGINE_STREAM]: engineStreamMachine,
|
[ENGINE_STREAM]: engineStreamMachine,
|
||||||
|
[MLEPHANT_MANAGER]: mlEphantManagerMachine,
|
||||||
[COMMAND_BAR]: commandBarMachine,
|
[COMMAND_BAR]: commandBarMachine,
|
||||||
[BILLING]: billingMachine,
|
[BILLING]: billingMachine,
|
||||||
} as const
|
} as const
|
||||||
@ -157,6 +169,10 @@ const appMachine = setup({
|
|||||||
systemId: ENGINE_STREAM,
|
systemId: ENGINE_STREAM,
|
||||||
input: engineStreamContextCreate(),
|
input: engineStreamContextCreate(),
|
||||||
}),
|
}),
|
||||||
|
spawnChild(appMachineActors[MLEPHANT_MANAGER], {
|
||||||
|
systemId: MLEPHANT_MANAGER,
|
||||||
|
input: mlEphantDefaultContext(),
|
||||||
|
}),
|
||||||
spawnChild(appMachineActors[SYSTEM_IO], {
|
spawnChild(appMachineActors[SYSTEM_IO], {
|
||||||
systemId: SYSTEM_IO,
|
systemId: SYSTEM_IO,
|
||||||
}),
|
}),
|
||||||
@ -226,6 +242,10 @@ export const engineStreamActor = appActor.system.get(
|
|||||||
ENGINE_STREAM
|
ENGINE_STREAM
|
||||||
) as ActorRefFrom<(typeof appMachineActors)[typeof 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<
|
export const commandBarActor = appActor.system.get(COMMAND_BAR) as ActorRefFrom<
|
||||||
(typeof appMachineActors)[typeof COMMAND_BAR]
|
(typeof appMachineActors)[typeof COMMAND_BAR]
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||||
import type { Project } from '@src/lib/project'
|
import type { Project } from '@src/lib/project'
|
||||||
|
import type { Prompt } from '@src/lib/prompt'
|
||||||
|
|
||||||
const DESC = ':desc'
|
const DESC = ':desc'
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSortFunction(sortBy: string) {
|
export function getProjectSortFunction(sortBy: string) {
|
||||||
const sortByName = (a: Project, b: Project) => {
|
const sortByName = (a: Project, b: Project) => {
|
||||||
if (a.name && b.name) {
|
if (a.name && b.name) {
|
||||||
return sortBy.includes('desc')
|
return sortBy.includes('desc')
|
||||||
@ -52,3 +53,35 @@ export function getSortFunction(sortBy: string) {
|
|||||||
return sortByModified
|
return sortByModified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Below is to keep the same behavior as above but for prompts.
|
||||||
|
// Do NOT take it as actually "sort by modified" but more like "sort by time".
|
||||||
|
export function getPromptSortFunction(sortBy: string) {
|
||||||
|
const sortByName = (a: Prompt, b: Prompt) => {
|
||||||
|
if (a.prompt && b.prompt) {
|
||||||
|
return sortBy.includes('desc')
|
||||||
|
? a.prompt.localeCompare(b.prompt)
|
||||||
|
: b.prompt.localeCompare(a.prompt)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortByModified = (a: Prompt, b: Prompt) => {
|
||||||
|
if (a.created_at && 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()
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy?.includes('name')) {
|
||||||
|
return sortByName
|
||||||
|
} else {
|
||||||
|
return sortByModified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ export const ACTOR_IDS = {
|
|||||||
SETTINGS: 'settings',
|
SETTINGS: 'settings',
|
||||||
SYSTEM_IO: 'systemIO',
|
SYSTEM_IO: 'systemIO',
|
||||||
ENGINE_STREAM: 'engine_stream',
|
ENGINE_STREAM: 'engine_stream',
|
||||||
|
MLEPHANT_MANAGER: 'mlephant_manager',
|
||||||
COMMAND_BAR: 'command_bar',
|
COMMAND_BAR: 'command_bar',
|
||||||
BILLING: 'billing',
|
BILLING: 'billing',
|
||||||
} as const
|
} as const
|
||||||
|
219
src/machines/mlEphantManagerMachine.ts
Normal file
219
src/machines/mlEphantManagerMachine.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
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',
|
||||||
|
PromptEditModel = 'prompt-edit-model',
|
||||||
|
PromptCreateModel = 'prompt-create-model',
|
||||||
|
PromptRate = 'prompt-rate',
|
||||||
|
// Note, technically hiding.
|
||||||
|
PromptDelete = 'prompt-delete',
|
||||||
|
PromptPollStatus = 'prompt-poll-status',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MlEphantManagerEvents =
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.GetPromptsBelongingToProject
|
||||||
|
projectId: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.PromptCreateModel
|
||||||
|
// For now we fake this, using project_name.
|
||||||
|
projectId: string
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.PromptEditModel
|
||||||
|
projectId: string
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.PromptRate
|
||||||
|
promptId: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.PromptDelete
|
||||||
|
promptId: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: MlEphantManagerTransitionStates.PromptPollStatus
|
||||||
|
promptId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to specify a specific event in input properties
|
||||||
|
type XSEvent<T> = Extract<MlEphantManagerEvents, { type: T }>
|
||||||
|
|
||||||
|
export interface MlEphantManagerContext {
|
||||||
|
promptsThatCreatedProjects: Map<Prompt['id'], Prompt>
|
||||||
|
// If no project is selected: undefined.
|
||||||
|
promptsBelongingToProject?: Map<Prompt['id'], Prompt>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mlEphantDefaultContext = () => ({
|
||||||
|
promptsThatCreatedProjects: new Map(),
|
||||||
|
promptsBelongingToProject: undefined,
|
||||||
|
hasPendingPrompts: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mlEphantManagerMachine = setup({
|
||||||
|
types: {
|
||||||
|
context: {} as MlEphantManagerContext,
|
||||||
|
events: {} as MlEphantManagerEvents,
|
||||||
|
},
|
||||||
|
actors: {
|
||||||
|
[MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects]:
|
||||||
|
fromPromise(async function (args: {
|
||||||
|
input: {
|
||||||
|
context: MlEphantManagerContext
|
||||||
|
}
|
||||||
|
}): Promise<Partial<MlEphantManagerContext>> {
|
||||||
|
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<MlEphantManagerTransitionStates.PromptCreateModel>
|
||||||
|
context: MlEphantManagerContext
|
||||||
|
}
|
||||||
|
}): Promise<Partial<MlEphantManagerContext>> {
|
||||||
|
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<Partial<MlEphantManagerContext>> {
|
||||||
|
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.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:
|
||||||
|
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) => ({
|
||||||
|
context: args.context,
|
||||||
|
}),
|
||||||
|
src: MlEphantManagerTransitionStates.GetPromptsThatCreatedProjects,
|
||||||
|
onDone: { target: MlEphantManagerStates.Idle },
|
||||||
|
onError: { target: MlEphantManagerStates.Idle },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[MlEphantManagerTransitionStates.PromptCreateModel]: {
|
||||||
|
invoke: {
|
||||||
|
input: (args) => ({
|
||||||
|
event:
|
||||||
|
args.event as XSEvent<MlEphantManagerTransitionStates.PromptCreateModel>,
|
||||||
|
context: args.context,
|
||||||
|
}),
|
||||||
|
src: MlEphantManagerTransitionStates.PromptCreateModel,
|
||||||
|
onDone: { target: MlEphantManagerStates.PromptPollStatus },
|
||||||
|
onError: { target: MlEphantManagerStates.PromptPollStatus },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[MlEphantManagerTransitionStates.PromptPollStatus]: {
|
||||||
|
invoke: {
|
||||||
|
input: (args) => ({
|
||||||
|
event:
|
||||||
|
args.event as XSEvent<MlEphantManagerTransitionStates.PromptPollStatus>,
|
||||||
|
context: args.context,
|
||||||
|
}),
|
||||||
|
src: MlEphantManagerTransitionStates.PromptPollStatus,
|
||||||
|
onDone: { target: MlEphantManagerStates.Idle },
|
||||||
|
onError: { target: MlEphantManagerStates.Idle },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type MlEphantManagerActor = ActorRefFrom<typeof mlEphantManagerMachine>
|
@ -1,4 +1,5 @@
|
|||||||
import type { FormEvent, HTMLProps } from 'react'
|
import type { FormEvent, HTMLProps } from 'react'
|
||||||
|
import { useSelector } from '@xstate/react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -13,10 +14,14 @@ import { ActionButton } from '@src/components/ActionButton'
|
|||||||
import { AppHeader } from '@src/components/AppHeader'
|
import { AppHeader } from '@src/components/AppHeader'
|
||||||
import Loading from '@src/components/Loading'
|
import Loading from '@src/components/Loading'
|
||||||
import ProjectCard from '@src/components/ProjectCard/ProjectCard'
|
import ProjectCard from '@src/components/ProjectCard/ProjectCard'
|
||||||
|
import { PromptCard } from '@src/components/PromptCard'
|
||||||
import {
|
import {
|
||||||
ProjectSearchBar,
|
HomeSearchBar,
|
||||||
useProjectSearch,
|
useHomeSearch,
|
||||||
} from '@src/components/ProjectSearchBar'
|
areHomeItemsProjects,
|
||||||
|
areHomeItemsPrompts,
|
||||||
|
} from '@src/components/HomeSearchBar'
|
||||||
|
import type { HomeItems } from '@src/components/HomeSearchBar'
|
||||||
import { BillingDialog } from '@src/components/BillingDialog'
|
import { BillingDialog } from '@src/components/BillingDialog'
|
||||||
import { useQueryParamEffects } from '@src/hooks/useQueryParamEffects'
|
import { useQueryParamEffects } from '@src/hooks/useQueryParamEffects'
|
||||||
import { useMenuListener } from '@src/hooks/useMenu'
|
import { useMenuListener } from '@src/hooks/useMenu'
|
||||||
@ -24,9 +29,11 @@ import { isDesktop } from '@src/lib/isDesktop'
|
|||||||
import { PATHS } from '@src/lib/paths'
|
import { PATHS } from '@src/lib/paths'
|
||||||
import { markOnce } from '@src/lib/performance'
|
import { markOnce } from '@src/lib/performance'
|
||||||
import type { Project } from '@src/lib/project'
|
import type { Project } from '@src/lib/project'
|
||||||
|
import type { Prompt } from '@src/lib/prompt'
|
||||||
import {
|
import {
|
||||||
getNextSearchParams,
|
getNextSearchParams,
|
||||||
getSortFunction,
|
getProjectSortFunction,
|
||||||
|
getPromptSortFunction,
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
} from '@src/lib/sorting'
|
} from '@src/lib/sorting'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
@ -37,6 +44,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
authActor,
|
authActor,
|
||||||
billingActor,
|
billingActor,
|
||||||
|
mlEphantManagerActor,
|
||||||
systemIOActor,
|
systemIOActor,
|
||||||
useSettings,
|
useSettings,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
@ -66,6 +74,7 @@ import {
|
|||||||
defaultLocalStatusBarItems,
|
defaultLocalStatusBarItems,
|
||||||
defaultGlobalStatusBarItems,
|
defaultGlobalStatusBarItems,
|
||||||
} from '@src/components/StatusBar/defaultStatusBarItems'
|
} from '@src/components/StatusBar/defaultStatusBarItems'
|
||||||
|
import { MlEphantManagerStates } from '@src/machines/mlEphantManagerMachine'
|
||||||
|
|
||||||
type ReadWriteProjectState = {
|
type ReadWriteProjectState = {
|
||||||
value: boolean
|
value: boolean
|
||||||
@ -210,9 +219,42 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
const projects = useFolders()
|
const projects = useFolders()
|
||||||
|
const prompts = useSelector(mlEphantManagerActor, (actor) => {
|
||||||
|
return actor.context.promptsThatCreatedProjects
|
||||||
|
})
|
||||||
|
|
||||||
|
const [tabSelected, setTabSelected] = useState<HomeTabKeys>(
|
||||||
|
HomeTabKeys.Projects
|
||||||
|
)
|
||||||
|
const [items, setItems] = useState<HomeItems>(projects)
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const { searchResults, query, setQuery } = useProjectSearch(projects)
|
const { searchResults, query, searchAgainst } = useHomeSearch(projects)
|
||||||
const sort = searchParams.get('sort_by') ?? 'modified:desc'
|
const sortBy = searchParams.get('sort_by') ?? 'modified:desc'
|
||||||
|
|
||||||
|
const onChangeTab = (key: HomeTabKeys) => {
|
||||||
|
setTabSelected(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (tabSelected) {
|
||||||
|
case HomeTabKeys.Projects:
|
||||||
|
setItems(projects)
|
||||||
|
break
|
||||||
|
case HomeTabKeys.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 = tabSelected
|
||||||
|
}
|
||||||
|
}, [tabSelected, prompts, projects])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
searchAgainst(items)('')
|
||||||
|
}, [items])
|
||||||
|
|
||||||
const sidebarButtonClasses =
|
const sidebarButtonClasses =
|
||||||
'flex items-center p-2 gap-2 leading-tight border-transparent dark:border-transparent enabled:dark:border-transparent enabled:hover:border-primary/50 enabled:dark:hover:border-inherit active:border-primary dark:bg-transparent hover:bg-transparent'
|
'flex items-center p-2 gap-2 leading-tight border-transparent dark:border-transparent enabled:dark:border-transparent enabled:hover:border-primary/50 enabled:dark:hover:border-inherit active:border-primary dark:bg-transparent hover:bg-transparent'
|
||||||
|
|
||||||
@ -224,8 +266,10 @@ const Home = () => {
|
|||||||
/>
|
/>
|
||||||
<div className="overflow-hidden self-stretch w-full flex-1 home-layout max-w-4xl lg:max-w-5xl xl:max-w-7xl px-4 mx-auto mt-8 lg:mt-24 lg:px-0">
|
<div className="overflow-hidden self-stretch w-full flex-1 home-layout max-w-4xl lg:max-w-5xl xl:max-w-7xl px-4 mx-auto mt-8 lg:mt-24 lg:px-0">
|
||||||
<HomeHeader
|
<HomeHeader
|
||||||
setQuery={setQuery}
|
tabSelected={tabSelected}
|
||||||
sort={sort}
|
onChangeHomeSearchBar={searchAgainst(items)}
|
||||||
|
onChangeTab={onChangeTab}
|
||||||
|
sortBy={sortBy}
|
||||||
setSearchParams={setSearchParams}
|
setSearchParams={setSearchParams}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
readWriteProjectDir={readWriteProjectDir}
|
readWriteProjectDir={readWriteProjectDir}
|
||||||
@ -393,12 +437,12 @@ const Home = () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
<ProjectGrid
|
<HomeItemsArea
|
||||||
|
tabSelected={tabSelected}
|
||||||
searchResults={searchResults}
|
searchResults={searchResults}
|
||||||
projects={projects}
|
sortBy={sortBy}
|
||||||
query={query}
|
query={query}
|
||||||
sort={sort}
|
settings={settings}
|
||||||
className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
@ -412,47 +456,98 @@ const Home = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HomeTabKeys {
|
||||||
|
Projects,
|
||||||
|
Prompts,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HomeTabProps {
|
||||||
|
onChange: (key: HomeTabKeys) => void
|
||||||
|
selected: HomeTabKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeTab(props: HomeTabProps) {
|
||||||
|
const [selected, setSelected] = useState(props.selected)
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: 'Projects', key: HomeTabKeys.Projects },
|
||||||
|
{ name: 'Prompts', key: HomeTabKeys.Prompts },
|
||||||
|
]
|
||||||
|
|
||||||
|
const cssTab = 'cursor-pointer p-4'
|
||||||
|
const cssActive = `${cssTab} border rounded bg-green`
|
||||||
|
const cssInactive = `${cssTab}`
|
||||||
|
|
||||||
|
const onClickTab = (key: HomeTabKeys) => () => {
|
||||||
|
setSelected(key)
|
||||||
|
props.onChange(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{tabs.map((el) => (
|
||||||
|
<div
|
||||||
|
key={el.key}
|
||||||
|
className={el.key === selected ? cssActive : cssInactive}
|
||||||
|
onClick={onClickTab(el.key)}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{el.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface HomeHeaderProps extends HTMLProps<HTMLDivElement> {
|
interface HomeHeaderProps extends HTMLProps<HTMLDivElement> {
|
||||||
setQuery: (query: string) => void
|
tabSelected: HomeTabKeys
|
||||||
sort: string
|
onChangeHomeSearchBar: (query: string) => void
|
||||||
|
onChangeTab: (key: HomeTabKeys) => void
|
||||||
|
sortBy: string
|
||||||
setSearchParams: (params: Record<string, string>) => void
|
setSearchParams: (params: Record<string, string>) => void
|
||||||
settings: ReturnType<typeof useSettings>
|
settings: ReturnType<typeof useSettings>
|
||||||
readWriteProjectDir: ReadWriteProjectState
|
readWriteProjectDir: ReadWriteProjectState
|
||||||
}
|
}
|
||||||
|
|
||||||
function HomeHeader({
|
function HomeHeader({
|
||||||
setQuery,
|
tabSelected,
|
||||||
sort,
|
onChangeHomeSearchBar,
|
||||||
|
onChangeTab,
|
||||||
|
sortBy,
|
||||||
setSearchParams,
|
setSearchParams,
|
||||||
settings,
|
settings,
|
||||||
readWriteProjectDir,
|
readWriteProjectDir,
|
||||||
...rest
|
...rest
|
||||||
}: HomeHeaderProps) {
|
}: HomeHeaderProps) {
|
||||||
const isSortByModified = sort?.includes('modified') || !sort || sort === null
|
const isSortByModified =
|
||||||
|
sortBy?.includes('modified') || !sortBy || sortBy === null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section {...rest}>
|
<section {...rest}>
|
||||||
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center select-none">
|
<div className="flex flex-col md:flex-row gap-4 justify-between md:items-center select-none">
|
||||||
<div className="flex gap-8 items-center">
|
<div className="flex gap-8 items-center">
|
||||||
<h1 className="text-3xl font-bold">Projects</h1>
|
<HomeTab onChange={onChangeTab} selected={tabSelected} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
|
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
|
||||||
<ProjectSearchBar setQuery={setQuery} />
|
<HomeSearchBar onChange={onChangeHomeSearchBar} />
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<small>Sort by</small>
|
<small>Sort by</small>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
data-testid="home-sort-by-name"
|
data-testid="home-sort-by-name"
|
||||||
className={`text-xs border-primary/10 ${
|
className={`text-xs border-primary/10 ${
|
||||||
!sort.includes('name')
|
!sortBy.includes('name')
|
||||||
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
? 'text-chalkboard-80 dark:text-chalkboard-40'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSearchParams(getNextSearchParams(sort, 'name'))}
|
onClick={() =>
|
||||||
|
setSearchParams(getNextSearchParams(sortBy, 'name'))
|
||||||
|
}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: getSortIcon(sort, 'name'),
|
icon: getSortIcon(sortBy, 'name'),
|
||||||
bgClassName: 'bg-transparent',
|
bgClassName: 'bg-transparent',
|
||||||
iconClassName: !sort.includes('name')
|
iconClassName: !sortBy.includes('name')
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
||||||
: '',
|
: '',
|
||||||
}}
|
}}
|
||||||
@ -468,17 +563,17 @@ function HomeHeader({
|
|||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSearchParams(getNextSearchParams(sort, 'modified'))
|
setSearchParams(getNextSearchParams(sortBy, 'modified'))
|
||||||
}
|
}
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: sort ? getSortIcon(sort, 'modified') : 'arrowDown',
|
icon: sortBy ? getSortIcon(sortBy, 'modified') : 'arrowDown',
|
||||||
bgClassName: 'bg-transparent',
|
bgClassName: 'bg-transparent',
|
||||||
iconClassName: !isSortByModified
|
iconClassName: !isSortByModified
|
||||||
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
? '!text-chalkboard-90 dark:!text-chalkboard-30'
|
||||||
: '',
|
: '',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Last Modified
|
Age
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -514,34 +609,147 @@ function HomeHeader({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectGridProps extends HTMLProps<HTMLDivElement> {
|
function NoResults() {
|
||||||
searchResults: Project[]
|
return (
|
||||||
projects: Project[]
|
<div className="col-start-2 -col-end-1 w-full flex flex-col justify-center items-center">
|
||||||
query: string
|
No results.
|
||||||
sort: string
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectGrid({
|
interface HomeItemsAreaProps {
|
||||||
searchResults,
|
tabSelected: HomeTabKeys
|
||||||
projects,
|
searchResults: HomeItems
|
||||||
query,
|
sortBy: string
|
||||||
sort,
|
query: string
|
||||||
...rest
|
settings: ReturnType<typeof useSettings>
|
||||||
}: ProjectGridProps) {
|
}
|
||||||
|
|
||||||
|
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) ? (
|
||||||
|
<ResultGridProjects
|
||||||
|
searchResults={props.searchResults}
|
||||||
|
query={props.query}
|
||||||
|
sortBy={props.sortBy}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<NoResults />
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case HomeTabKeys.Prompts:
|
||||||
|
grid = areHomeItemsPrompts(props.searchResults) ? (
|
||||||
|
<ResultGridPrompts
|
||||||
|
searchResults={props.searchResults}
|
||||||
|
query={props.query}
|
||||||
|
sortBy={props.sortBy}
|
||||||
|
settings={props.settings}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<NoResults />
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
const _ex: never = props.tabSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 col-start-2 -col-end-1 overflow-y-auto pr-2 pb-24">
|
||||||
|
{grid}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultGridPromptsProps {
|
||||||
|
searchResults: Prompt[]
|
||||||
|
query: string
|
||||||
|
sortBy: string
|
||||||
|
settings: ReturnType<typeof useSettings>
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResultGridPrompts(props: ResultGridPromptsProps) {
|
||||||
|
// Maybe consider lifting this higher but I see no reason at the moment
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
const onFeedback = (...args: any) => {
|
||||||
|
console.log(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mlEphantManagerSnapshot = mlEphantManagerActor.getSnapshot()
|
||||||
|
|
||||||
|
if (mlEphantManagerSnapshot.matches(MlEphantManagerStates.Setup)) {
|
||||||
|
return (
|
||||||
|
<div className="col-start-2 -col-end-1 w-full flex flex-col justify-center items-center">
|
||||||
|
<Loading isDummy={true}>Loading your prompts...</Loading>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid w-full sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-4">
|
||||||
|
{props.searchResults
|
||||||
|
.sort(getPromptSortFunction(props.sortBy))
|
||||||
|
.map((prompt: Prompt) => (
|
||||||
|
<PromptCard
|
||||||
|
key={prompt.id}
|
||||||
|
{...prompt}
|
||||||
|
disabled={
|
||||||
|
mlEphantManagerSnapshot.matches(MlEphantManagerStates.Pending) ||
|
||||||
|
prompt.status !== 'completed'
|
||||||
|
}
|
||||||
|
onAction={onAction}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onFeedback={onFeedback}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResultGridProjectsProps extends HTMLProps<HTMLDivElement> {
|
||||||
|
searchResults: Project[]
|
||||||
|
query: string
|
||||||
|
sortBy: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResultGridProjects(props: ResultGridProjectsProps) {
|
||||||
const state = useSystemIOState()
|
const state = useSystemIOState()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section data-testid="home-section" {...rest}>
|
<section data-testid="home-section" className={props.className}>
|
||||||
{state.matches(SystemIOMachineStates.readingFolders) ? (
|
{state.matches(SystemIOMachineStates.readingFolders) ? (
|
||||||
<Loading isDummy={true}>Loading your Projects...</Loading>
|
<Loading isDummy={true}>Loading your Projects...</Loading>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{searchResults.length > 0 ? (
|
{props.searchResults.length > 0 ? (
|
||||||
<ul className="grid w-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
<ul className="grid w-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
{searchResults.sort(getSortFunction(sort)).map((project) => (
|
{props.searchResults
|
||||||
|
.sort(getProjectSortFunction(props.sortBy))
|
||||||
|
.map((item) => (
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
key={project.name}
|
key={item.name}
|
||||||
project={project}
|
project={item}
|
||||||
handleRenameProject={handleRenameProject}
|
handleRenameProject={handleRenameProject}
|
||||||
handleDeleteProject={handleDeleteProject}
|
handleDeleteProject={handleDeleteProject}
|
||||||
/>
|
/>
|
||||||
@ -549,10 +757,10 @@ function ProjectGrid({
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p className="p-4 my-8 border border-dashed rounded border-chalkboard-30 dark:border-chalkboard-70">
|
<p className="p-4 my-8 border border-dashed rounded border-chalkboard-30 dark:border-chalkboard-70">
|
||||||
No projects found
|
No results found
|
||||||
{projects.length === 0
|
{props.searchResults.length === 0
|
||||||
? ', ready to make your first one?'
|
? ', ready to make your first one?'
|
||||||
: ` with the search term "${query}"`}
|
: ` with the search term "${props.query}"`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
Reference in New Issue
Block a user