[Feature]: Enable Text-to-CAD at the application level (#6501)
* chore: saving off skeleton * fix: saving skeleton * chore: skeleton for loading projects from project directory path * chore: cleaning up useless state transition to be an on event direct to action state * fix: new structure for web vs desktop vs react machine provider code * chore: saving off skeleton * fix: skeleton logic for react? going to move it from a string to obj.string * fix: trying to prevent error element unmount on global react components. This is bricking JS state * fix: we are so back * chore: implemented navigating to specfic KCL file * chore: implementing renaming project * chore: deleting project * fix: auto fixes * fix: old debug/testing file oops * chore: generic create new file * chore: skeleton for web create file provide * chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest? * chore: save off progress before deleting other project implementation, a few missing features still * chore: trying a different init skeleton? most likely will migrate * chore: first attempt of purging projects context provider * chore: enabling toast for some machine state * chore: enabling more toast success and error * chore: writing read write state to the system io based on the project path * fix: tsc fixes * fix: use file system watcher, navigate to project after creation via the requestProjectName * chore: open project command, hooks vs snapshot context helpers * chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now. * fix: codespell fixes * chore: implementing more project commands * chore: PR improvements for root.tsx * chore: leaving comment about new Router.tsx layout * fix: removing debugging code * fix: rewriting component for readability * fix: improving web initialization * chore: implementing import file from url which is not actually that? * fix: clearing search params on import file from url * fix: fixed two e2e tests, forgot needsReview when making new command * fix: fixing some import from url business logic to pass e2e tests * chore: script for diffing circular deps +/- * fix: formatting * fix: massive fix for circular depsga! * fix: trying to fix some errors and auto fmt * fix: updating deps * fix: removing debugging code * fix: big clean up * fix: more deletion * fix: tsc cleanup * fix: TSC TSC TSC TSC! * fix: typo fix * fix: clear query params on web only, desktop not required * fix: removing unused code * fmt * Bring back `trap` removed in merge * Use explicit types instead of `any`s on arg configs * Add project commands directly to command palette * fix: deleting debugging code, from PR review * fix: this got added back(?) * fix: using referred type * fix: more PR clean up * fix: big block comment for xstate architecture decision * fix: more pr comment fixes * fix: saving off logic, need a big cleanup because I hacked it together to get a POC * fix: extra business? * fix: merge conflict just added them back why dude * fix: more PR comments * fix: big ciruclar deps fix, commandBarActor in appActor * chore: writing e2e test, still need to fix 3 bugs * chore: adding more scenarios * fix: formatting * fix: fixing tsc errors * chore: deleting the old text to cad and using the new application level one, almost there * fix: prompt to edit works * fix: large push to get 1 text to cad command... the usage is a little buggy with delete and navigate within /file * fix: settings for highlight edges now works * chore: adding another e2e test * fix: cleaning up e2e tests and writing more of them * fix: tsc type * chore: more e2e improvements, unique project name in text to cad * chore: e2e tests should be good to go * fix: gotcha comment * fix: enabled web t2c, codespell fixes * fix: fixing merge conflcits?? * fix: t2c is back * Remove spaces in command bar test * fmt --------- Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com> Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
This commit is contained in:
@ -83,7 +83,7 @@ test.describe('Command bar tests', () => {
|
||||
await page.keyboard.press('Enter') // submit
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-activeLine')).toContainText(
|
||||
`fillet( radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] )`
|
||||
`fillet(radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01])`
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -411,6 +411,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
closeFilePanel: () => closeFilePanel(page),
|
||||
openVariablesPane: () => openVariablesPane(page),
|
||||
openLogsPane: () => openLogsPane(page),
|
||||
goToHomePageFromModeling: () => goToHomePageFromModeling(page),
|
||||
openAndClearDebugPanel: () => openAndClearDebugPanel(page),
|
||||
clearAndCloseDebugPanel: async () => {
|
||||
await clearCommandLogs(page)
|
||||
@ -1011,6 +1012,11 @@ export async function createProject({
|
||||
})
|
||||
}
|
||||
|
||||
async function goToHomePageFromModeling(page: Page) {
|
||||
await page.getByTestId('app-logo').click()
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
}
|
||||
|
||||
export function executorInputPath(fileName: string): string {
|
||||
return path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -64,7 +64,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x6 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x6 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -82,7 +82,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
// Can send a new prompt from the command bar.
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -108,7 +108,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -149,29 +149,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
|
||||
await page.keyboard.type(randomPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, randomPrompt)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -217,30 +196,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type(badPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -307,30 +264,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
const badPrompt = 'akjsndladflajbhflauweyf15;'
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type(badPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -357,7 +292,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
|
||||
|
||||
// They should be able to try again from the command bar.
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -385,23 +320,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
|
||||
const promptWithNewline = `a 2x4\nlego`
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByRole('option', { name: 'Text-to-CAD' })
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
await page.getByTestId('text-to-cad').click()
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type('a 2x4')
|
||||
@ -446,11 +365,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x8 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x8 lego')
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x10 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x10 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -529,9 +448,12 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
await sendPromptFromCommandBar(page, 'alkjsdnlajshdbfjlhsbdf a;askjdnf')
|
||||
await sendPromptFromCommandBarTriggeredByButton(
|
||||
page,
|
||||
'alkjsdnlajshdbfjlhsbdf a;askjdnf'
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -543,7 +465,9 @@ test.describe('Text-to-CAD tests', () => {
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
|
||||
await expect(generatingToastMessage.first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
// We should have three success toasts.
|
||||
@ -587,7 +511,8 @@ test.describe('Text-to-CAD tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
// Added underscore if we need this for later.
|
||||
async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
await page.waitForTimeout(1000)
|
||||
await test.step(`Send prompt from command bar: ${promptStr}`, async () => {
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
@ -619,6 +544,25 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
})
|
||||
}
|
||||
|
||||
async function sendPromptFromCommandBarTriggeredByButton(
|
||||
page: Page,
|
||||
promptStr: string
|
||||
) {
|
||||
await page.waitForTimeout(1000)
|
||||
await test.step(`Send prompt from command bar: ${promptStr}`, async () => {
|
||||
await page.getByTestId('text-to-cad').click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type(promptStr)
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
}
|
||||
|
||||
test(
|
||||
'Text-to-CAD functionality',
|
||||
{ tag: '@electron' },
|
||||
@ -659,7 +603,7 @@ test(
|
||||
await openKclCodePanel()
|
||||
|
||||
await test.step(`Test file creation`, async () => {
|
||||
await sendPromptFromCommandBar(page, prompt)
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, prompt)
|
||||
// File is considered created if it shows up in the Project Files pane
|
||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
@ -689,3 +633,749 @@ test(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Below there are twelve (12) tests for testing the navigation and file creation
|
||||
* logic around text to cad. The Text to CAD command is now globally available
|
||||
* within the application and is the same command for all parts of the application.
|
||||
* There are many new user scenarios to test because we can navigate to any project
|
||||
* you can accept and reject the creation and everything needs to be updated properly.
|
||||
*
|
||||
*
|
||||
* Gotcha: The API requests for text to CAD are mocked! The return values are
|
||||
* from real API requests which are copied and pasted below
|
||||
*
|
||||
* Gotcha: The exports OBJ etc... are not in the output they are massive.
|
||||
*
|
||||
* Gotcha: Yes, the 3D render preview will be broken because the exported models
|
||||
* are not included. These tests do not care about this.
|
||||
*
|
||||
*/
|
||||
test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
||||
async function mockPageTextToCAD(page: Page) {
|
||||
await page.route(
|
||||
'https://api.dev.zoo.dev/ai/text-to-cad/glb?kcl=true',
|
||||
async (route) => {
|
||||
const json = {
|
||||
id: 'bfc0ffc0-46c6-48bc-841e-1dc08f3428ce',
|
||||
created_at: '2025-04-24T16:50:48.857892376Z',
|
||||
user_id: '85de7740-3e38-4e86-abb5-e5afbb8a2183',
|
||||
status: 'queued',
|
||||
updated_at: '2025-04-24T16:50:48.857892376Z',
|
||||
prompt: '1x1x1 cube',
|
||||
output_format: 'glb',
|
||||
model_version: '',
|
||||
kcl_version: '0.2.63',
|
||||
model: 'kcl',
|
||||
feedback: null,
|
||||
mocked: true,
|
||||
}
|
||||
await route.fulfill({ json })
|
||||
}
|
||||
)
|
||||
await page.route(
|
||||
'https://api.dev.zoo.dev/user/text-to-cad/*',
|
||||
async (route) => {
|
||||
const json = {
|
||||
mocked: true,
|
||||
id: '6ecbb863-2766-47f0-95cb-7122ad7560ce',
|
||||
created_at: '2025-04-24T16:52:00.360Z',
|
||||
started_at: '2025-04-24T16:52:00.360Z',
|
||||
completed_at: '2025-04-24T16:52:00.363Z',
|
||||
user_id: '85de7740-3e38-4e86-abb5-e5afbb8a2183',
|
||||
status: 'completed',
|
||||
updated_at: '2025-04-24T16:52:00.360Z',
|
||||
prompt: '2x2x2 cube',
|
||||
outputs: {},
|
||||
output_format: 'step',
|
||||
model_version: '',
|
||||
kcl_version: '0.2.63',
|
||||
model: 'kcl',
|
||||
feedback: null,
|
||||
code: '/*\nGenerated by Text-to-CAD:\n2x2x2 cube\n*/\n@settings(defaultLengthUnit = mm)\n\n// Define the dimensions of the cube\ncubeSide = 2\n\n// Start a sketch on the XY plane\ncubeSketch = startSketchOn(XY)\n |> startProfileAt([0, 0], %)\n |> line(end = [cubeSide, 0])\n |> line(end = [0, cubeSide])\n |> line(end = [-cubeSide, 0])\n |> close()\n\n// Extrude the sketch to create a 3D cube\ncube = extrude(cubeSketch, length = cubeSide)',
|
||||
}
|
||||
await route.fulfill({ json })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Stay in home page -> Reject -> Project should be deleted',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Expect the entire project to be deleted
|
||||
await expect(
|
||||
page.getByText('Successfully deleted "my-project-name"')
|
||||
).toBeVisible()
|
||||
// Project DOM card has this test id
|
||||
await expect(page.getByTestId(projectName)).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Stay in home page -> Accept -> should navigate to file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Reject -> should delete single file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
await expect(page.getByText('1 file')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Reject' })).toBeVisible()
|
||||
await expect(page.getByText('2 file')).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
await expect(page.getByText('1 file')).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Stay in home page -> Accept -> should navigate to file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
await u.openFilePanel()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Reject -> should go to home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Go into the project that was created from Text to CAD
|
||||
await page.getByText(projectName).click()
|
||||
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Make sure we went back home
|
||||
await expect(
|
||||
page.getByText('No Projects found, ready to make your first one?')
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to the project -> Accept -> should stay in same file',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Go into the project that was created from Text to CAD
|
||||
await page.getByText(projectName).click()
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Reject -> should load main.kcl',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Go into the project that was created from Text to CAD
|
||||
// This only works because there is only 1 project. Each project has the same value of `data-test-id='project-title'`
|
||||
await page.getByTestId('project-title').click()
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
// Check file is deleted
|
||||
await u.openFilePanel()
|
||||
await expect(page.getByText('2x2x2-cube.kcl')).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to the project -> Accept -> should load 2x2x2-cube.kcl',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Go into the project that was created from Text to CAD
|
||||
// This only works because there is only 1 project. Each project has the same value of `data-test-id='project-title'`
|
||||
await page.getByTestId('project-title').click()
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
// Check file is created
|
||||
await u.openFilePanel()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Reject -> should stay in project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({
|
||||
name: unrelatedProjectName,
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await homePage.openProject(unrelatedProjectName)
|
||||
// Check that we opened the unrelated project
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
// Check file is created
|
||||
await u.openFilePanel()
|
||||
// File should be deleted
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).not.toBeVisible()
|
||||
|
||||
await u.goToHomePageFromModeling()
|
||||
|
||||
// Project should be deleted
|
||||
await expect(
|
||||
page.getByTestId('home-section').getByText(projectName)
|
||||
).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> New Project -> Navigate to different project -> Accept -> should go to new project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({
|
||||
name: unrelatedProjectName,
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('New project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await homePage.openProject(unrelatedProjectName)
|
||||
// Check that we opened the unrelated project
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
// Check file is created
|
||||
await u.openFilePanel()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('main.kcl')
|
||||
).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Reject -> should stay in same project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({
|
||||
name: unrelatedProjectName,
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await homePage.openProject(unrelatedProjectName)
|
||||
// Check that we opened the unrelated project
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Reject' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
await u.goToHomePageFromModeling()
|
||||
|
||||
await expect(
|
||||
page.getByTestId('home-section').getByText(projectName)
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.getByTestId('home-section').getByText(unrelatedProjectName)
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'Home Page -> Text To CAD -> Existing Project -> Navigate to different project -> Accept -> should navigate to new project',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, homePage }, testInfo) => {
|
||||
const u = await getUtils(page)
|
||||
const projectName = 'my-project-name'
|
||||
const unrelatedProjectName = 'unrelated-project'
|
||||
const prompt = '2x2x2 cube'
|
||||
await mockPageTextToCAD(page)
|
||||
|
||||
// Create and navigate to the project then come home
|
||||
await createProject({
|
||||
name: unrelatedProjectName,
|
||||
page,
|
||||
returnHome: true,
|
||||
})
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
await createProject({ name: projectName, page, returnHome: true })
|
||||
await expect(page.getByText('Your Projects')).toBeVisible()
|
||||
|
||||
// open commands
|
||||
await page.getByTestId('command-bar-open-button').click()
|
||||
|
||||
// search Text To CAD
|
||||
await page.keyboard.type('Text To CAD')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// new project
|
||||
await page.keyboard.type('Existing project')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// write name
|
||||
await page.keyboard.type(projectName)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// prompt
|
||||
await page.keyboard.type(prompt)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
await homePage.openProject(unrelatedProjectName)
|
||||
// Check that we opened the unrelated project
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
unrelatedProjectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'main.kcl'
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Accept' }).click()
|
||||
|
||||
// Check header is populated with the project and file name
|
||||
await expect(page.getByTestId('app-header-project-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-project-name')).toContainText(
|
||||
projectName
|
||||
)
|
||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||
'2x2x2-cube.kcl'
|
||||
)
|
||||
|
||||
// Check file is created
|
||||
await u.openFilePanel()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByTestId('file-tree-item').getByText('main.kcl')
|
||||
).toBeVisible()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -100,42 +100,6 @@ export const FileMachineProvider = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE + '/' + encodeURIComponent(file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
markOnce('code/didLoadFile')
|
||||
async function fetchKclSamples() {
|
||||
@ -440,6 +404,51 @@ export const FileMachineProvider = ({
|
||||
}
|
||||
)
|
||||
|
||||
// Due to the route provider, i've moved this to the FileMachineProvider instead of CommandBarProvider
|
||||
// This will register the commands to route to Telemetry, Home, and Settings.
|
||||
useEffect(() => {
|
||||
const filePath =
|
||||
PATHS.FILE + '/' + encodeURIComponent(file?.path || BROWSER_PATH)
|
||||
const { RouteTelemetryCommand, RouteHomeCommand, RouteSettingsCommand } =
|
||||
createRouteCommands(navigate, location, filePath)
|
||||
commandBarActor.send({
|
||||
type: 'Remove commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteHomeCommand,
|
||||
RouteSettingsCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
if (location.pathname === PATHS.HOME) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: { commands: [RouteTelemetryCommand, RouteSettingsCommand] },
|
||||
})
|
||||
} else if (location.pathname.includes(PATHS.FILE)) {
|
||||
commandBarActor.send({
|
||||
type: 'Add commands',
|
||||
data: {
|
||||
commands: [
|
||||
RouteTelemetryCommand,
|
||||
RouteSettingsCommand,
|
||||
RouteHomeCommand,
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GOTCHA: If we call navigate() while in the /file route the fileMachineProvider
|
||||
// has a context.project of the original one that was loaded. It does not update
|
||||
// Watch when the navigation changes, if it changes set a new Project within the fileMachine
|
||||
// to load the latest state of the project you are in.
|
||||
if (project) {
|
||||
// TODO: Clean this up with global application state when fileMachine gets merged into SystemIOMachine
|
||||
send({ type: 'Refresh with new project', data: { project } })
|
||||
}
|
||||
}, [location])
|
||||
|
||||
const cb = modelingMenuCallbackMostActions(
|
||||
settings,
|
||||
navigate,
|
||||
|
@ -8,7 +8,7 @@ import React, {
|
||||
} from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useLoaderData, useNavigate } from 'react-router-dom'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import type { Actor, ContextFrom, Prop, SnapshotFrom, StateFrom } from 'xstate'
|
||||
import { assign, fromPromise } from 'xstate'
|
||||
|
||||
@ -107,7 +107,6 @@ import {
|
||||
sceneEntitiesManager,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import { submitAndAwaitTextToKcl } from '@src/lib/textToCad'
|
||||
import { err, reject, reportRejection, trap } from '@src/lib/trap'
|
||||
import type { IndexLoaderData } from '@src/lib/types'
|
||||
import { platform, uuidv4 } from '@src/lib/utils'
|
||||
@ -139,11 +138,10 @@ export const ModelingMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const {
|
||||
app: { theme, allowOrbitInSketchMode },
|
||||
modeling: { defaultUnit, cameraProjection, highlightEdges, cameraOrbit },
|
||||
app: { allowOrbitInSketchMode },
|
||||
modeling: { defaultUnit, cameraProjection, cameraOrbit },
|
||||
} = useSettings()
|
||||
const navigate = useNavigate()
|
||||
const { context, send: fileMachineSend } = useFileContext()
|
||||
const { context } = useFileContext()
|
||||
const { file } = useLoaderData() as IndexLoaderData
|
||||
const token = useToken()
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
@ -533,23 +531,6 @@ export const ModelingMachineProvider = ({
|
||||
return {}
|
||||
}
|
||||
),
|
||||
'Submit to Text-to-CAD API': ({ event }) => {
|
||||
if (event.type !== 'Text-to-CAD') return
|
||||
const trimmedPrompt = event.data.prompt.trim()
|
||||
if (!trimmedPrompt) return
|
||||
|
||||
submitAndAwaitTextToKcl({
|
||||
trimmedPrompt,
|
||||
fileMachineSend,
|
||||
navigate,
|
||||
context,
|
||||
token,
|
||||
settings: {
|
||||
theme: theme.current,
|
||||
highlightEdges: highlightEdges.current,
|
||||
},
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
},
|
||||
guards: {
|
||||
'has valid selection for deletion': ({
|
||||
|
@ -263,7 +263,10 @@ function ProjectMenuPopover({
|
||||
data-testid="project-sidebar-toggle"
|
||||
>
|
||||
<div className="flex flex-col items-start py-0.5">
|
||||
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
||||
<span
|
||||
className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
|
||||
data-testid="app-header-file-name"
|
||||
>
|
||||
{isDesktop() && file?.name
|
||||
? file.name.slice(
|
||||
file.name.lastIndexOf(window.electron.path.sep) + 1
|
||||
@ -271,7 +274,10 @@ function ProjectMenuPopover({
|
||||
: APP_NAME}
|
||||
</span>
|
||||
{isDesktop() && project?.name && (
|
||||
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
|
||||
<span
|
||||
className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block"
|
||||
data-testid="app-header-project-name"
|
||||
>
|
||||
{project.name}
|
||||
</span>
|
||||
)}
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { systemIOActor, useSettings } from '@src/lib/singletons'
|
||||
import { systemIOActor, useSettings, useToken } from '@src/lib/singletons'
|
||||
import {
|
||||
useHasListedProjects,
|
||||
useProjectDirectoryPath,
|
||||
useRequestedFileName,
|
||||
useRequestedProjectName,
|
||||
useRequestedTextToCadGeneration,
|
||||
useFolders,
|
||||
} from '@src/machines/systemIO/hooks'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import { submitAndAwaitTextToKclSystemIO } from '@src/lib/textToCad'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { getUniqueProjectName } from '@src/lib/desktopFS'
|
||||
|
||||
export function SystemIOMachineLogicListenerDesktop() {
|
||||
const requestedProjectName = useRequestedProjectName()
|
||||
@ -18,6 +23,9 @@ export function SystemIOMachineLogicListenerDesktop() {
|
||||
const hasListedProjects = useHasListedProjects()
|
||||
const navigate = useNavigate()
|
||||
const settings = useSettings()
|
||||
const requestedTextToCadGeneration = useRequestedTextToCadGeneration()
|
||||
const token = useToken()
|
||||
const folders = useFolders()
|
||||
|
||||
const useGlobalProjectNavigation = () => {
|
||||
useEffect(() => {
|
||||
@ -95,6 +103,27 @@ export function SystemIOMachineLogicListenerDesktop() {
|
||||
? [settings.app.projectDirectory.current]
|
||||
: []
|
||||
)
|
||||
|
||||
// TODO: Move this generateTextToCAD to another machine in the future and make a whole machine out of it.
|
||||
useEffect(() => {
|
||||
const requestedPromptTrimmed =
|
||||
requestedTextToCadGeneration.requestedPrompt.trim()
|
||||
const requestedProjectName =
|
||||
requestedTextToCadGeneration.requestedProjectName
|
||||
const isProjectNew = requestedTextToCadGeneration.isProjectNew
|
||||
if (!requestedPromptTrimmed || !requestedProjectName) return
|
||||
const uniqueNameIfNeeded = isProjectNew
|
||||
? getUniqueProjectName(requestedProjectName, folders)
|
||||
: requestedProjectName
|
||||
submitAndAwaitTextToKclSystemIO({
|
||||
trimmedPrompt: requestedPromptTrimmed,
|
||||
projectName: uniqueNameIfNeeded,
|
||||
navigate,
|
||||
token,
|
||||
isProjectNew,
|
||||
settings: { highlightEdges: settings.modeling.highlightEdges.current },
|
||||
}).catch(reportRejection)
|
||||
}, [requestedTextToCadGeneration])
|
||||
}
|
||||
|
||||
useGlobalProjectNavigation()
|
||||
|
@ -1,10 +1,21 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useClearURLParams } from '@src/machines/systemIO/hooks'
|
||||
import {
|
||||
useClearURLParams,
|
||||
useRequestedTextToCadGeneration,
|
||||
} from '@src/machines/systemIO/hooks'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { CREATE_FILE_URL_PARAM } from '@src/lib/constants'
|
||||
import { submitAndAwaitTextToKclSystemIO } from '@src/lib/textToCad'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSettings, useToken } from '@src/lib/singletons'
|
||||
|
||||
export function SystemIOMachineLogicListenerWeb() {
|
||||
const clearURLParams = useClearURLParams()
|
||||
const navigate = useNavigate()
|
||||
const settings = useSettings()
|
||||
const requestedTextToCadGeneration = useRequestedTextToCadGeneration()
|
||||
const token = useToken()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const clearImportSearchParams = useCallback(() => {
|
||||
// Clear the search parameters related to the "Import file from URL" command
|
||||
@ -24,6 +35,26 @@ export function SystemIOMachineLogicListenerWeb() {
|
||||
}, [clearURLParams])
|
||||
}
|
||||
|
||||
// TODO: Move this generateTextToCAD to another machine in the future and make a whole machine out of it.
|
||||
useEffect(() => {
|
||||
const requestedPromptTrimmed =
|
||||
requestedTextToCadGeneration.requestedPrompt.trim()
|
||||
const requestedProjectName =
|
||||
requestedTextToCadGeneration.requestedProjectName
|
||||
const isProjectNew = requestedTextToCadGeneration.isProjectNew
|
||||
if (!requestedPromptTrimmed || !requestedProjectName) return
|
||||
// Gotcha: web has no project name.
|
||||
const uniqueNameIfNeeded = requestedProjectName
|
||||
submitAndAwaitTextToKclSystemIO({
|
||||
trimmedPrompt: requestedPromptTrimmed,
|
||||
projectName: uniqueNameIfNeeded,
|
||||
navigate,
|
||||
token,
|
||||
isProjectNew,
|
||||
settings: { highlightEdges: settings.modeling.highlightEdges.current },
|
||||
}).catch(reportRejection)
|
||||
}, [requestedTextToCadGeneration])
|
||||
|
||||
useClearQueryParams()
|
||||
return null
|
||||
}
|
||||
|
@ -21,20 +21,17 @@ import {
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import type { EventFrom } from 'xstate'
|
||||
|
||||
import { ActionButton } from '@src/components/ActionButton'
|
||||
import type { useFileContext } from '@src/hooks/useFileContext'
|
||||
import { base64Decode } from '@src/lang/wasm'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
import { codeManager, kclManager, systemIOActor } from '@src/lib/singletons'
|
||||
import { sendTelemetry } from '@src/lib/textToCadTelemetry'
|
||||
import type { Themes } from '@src/lib/theme'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { commandBarActor } from '@src/lib/singletons'
|
||||
import type { fileMachine } from '@src/machines/fileMachine'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { useProjectDirectoryPath } from '@src/machines/systemIO/hooks'
|
||||
|
||||
const CANVAS_SIZE = 128
|
||||
const PROMPT_TRUNCATE_LENGTH = 128
|
||||
@ -82,10 +79,16 @@ export function ToastTextToCadError({
|
||||
toastId,
|
||||
message,
|
||||
prompt,
|
||||
method,
|
||||
projectName,
|
||||
newProjectName,
|
||||
}: {
|
||||
toastId: string
|
||||
message: string
|
||||
prompt: string
|
||||
method: string
|
||||
projectName: string
|
||||
newProjectName: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col justify-between gap-6">
|
||||
@ -118,10 +121,13 @@ export function ToastTextToCadError({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'modeling',
|
||||
groupId: 'application',
|
||||
name: 'Text-to-CAD',
|
||||
argDefaultValues: {
|
||||
prompt,
|
||||
method,
|
||||
projectName,
|
||||
newProjectName,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -139,24 +145,22 @@ export function ToastTextToCadSuccess({
|
||||
toastId,
|
||||
data,
|
||||
navigate,
|
||||
context,
|
||||
token,
|
||||
fileMachineSend,
|
||||
settings,
|
||||
projectName,
|
||||
fileName,
|
||||
isProjectNew,
|
||||
}: {
|
||||
toastId: string
|
||||
data: TextToCad_type & { fileName: string }
|
||||
navigate: (to: string) => void
|
||||
context: ReturnType<typeof useFileContext>['context']
|
||||
token?: string
|
||||
fileMachineSend: (
|
||||
event: EventFrom<typeof fileMachine>,
|
||||
data?: unknown
|
||||
) => void
|
||||
settings: {
|
||||
theme: Themes
|
||||
settings?: {
|
||||
highlightEdges: boolean
|
||||
}
|
||||
projectName: string
|
||||
fileName: string
|
||||
isProjectNew: boolean
|
||||
}) {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
@ -164,6 +168,7 @@ export function ToastTextToCadSuccess({
|
||||
const [hasCopied, setHasCopied] = useState(false)
|
||||
const [showCopiedUi, setShowCopiedUi] = useState(false)
|
||||
const modelId = data.id
|
||||
const projectDirectoryPath = useProjectDirectoryPath()
|
||||
|
||||
const animate = useCallback(
|
||||
({
|
||||
@ -198,7 +203,11 @@ export function ToastTextToCadSuccess({
|
||||
if (!canvasRef.current) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
||||
const renderer = new WebGLRenderer({
|
||||
canvas,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
})
|
||||
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
|
||||
@ -344,15 +353,30 @@ export function ToastTextToCadSuccess({
|
||||
}
|
||||
if (isDesktop()) {
|
||||
// Delete the file from the project
|
||||
fileMachineSend({
|
||||
type: 'Delete file',
|
||||
|
||||
if (projectName && fileName) {
|
||||
// You are in the new workflow for text to cad at the global application level
|
||||
if (isProjectNew) {
|
||||
// Delete the entire project if it was newly created from text to CAD
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.deleteProject,
|
||||
data: {
|
||||
name: data.fileName,
|
||||
path: `${context.project.path}${window.electron.sep}${data.fileName}`,
|
||||
children: null,
|
||||
requestedProjectName: projectName,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Only delete the file if the project was preexisting
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.deleteKCLFile,
|
||||
data: {
|
||||
requestedProjectName: projectName,
|
||||
requestedFileName: fileName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
@ -368,11 +392,9 @@ export function ToastTextToCadSuccess({
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sendTelemetry(modelId, 'accepted', token)
|
||||
navigate(
|
||||
`${PATHS.FILE}/${encodeURIComponent(
|
||||
`${context.project.path}${window.electron.sep}${data.fileName}`
|
||||
)}`
|
||||
)
|
||||
const path = `${projectDirectoryPath}${window.electron.path.sep}${projectName}${window.electron.sep}${fileName}`
|
||||
navigate(`${PATHS.FILE}/${encodeURIComponent(path)}`)
|
||||
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
@ -426,7 +448,6 @@ function traverseSceneToStyleObjects({
|
||||
}: {
|
||||
scene: Scene
|
||||
color?: number
|
||||
theme: Themes
|
||||
highlightEdges?: boolean
|
||||
}) {
|
||||
scene.traverse((child) => {
|
||||
|
83
src/lib/commandBarConfigs/applicationCommandConfig.ts
Normal file
83
src/lib/commandBarConfigs/applicationCommandConfig.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||
import type { ActorRefFrom } from 'xstate'
|
||||
import type { Command, CommandArgumentOption } from '@src/lib/commandTypes'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
|
||||
export function createApplicationCommands({
|
||||
systemIOActor,
|
||||
}: {
|
||||
systemIOActor: ActorRefFrom<typeof systemIOMachine>
|
||||
}) {
|
||||
const textToCADCommand: Command = {
|
||||
name: 'Text-to-CAD',
|
||||
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
||||
displayName: `Text to CAD`,
|
||||
groupId: 'application',
|
||||
needsReview: false,
|
||||
icon: 'sparkles',
|
||||
onSubmit: (record) => {
|
||||
if (record) {
|
||||
const requestedProjectName = record.newProjectName || record.projectName
|
||||
const requestedPrompt = record.prompt
|
||||
const isProjectNew = !!record.newProjectName
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.generateTextToCAD,
|
||||
data: { requestedPrompt, requestedProjectName, isProjectNew },
|
||||
})
|
||||
}
|
||||
},
|
||||
args: {
|
||||
method: {
|
||||
inputType: 'options',
|
||||
required: true,
|
||||
skip: true,
|
||||
options: isDesktop()
|
||||
? [
|
||||
{ name: 'New project', value: 'newProject' },
|
||||
{ name: 'Existing project', value: 'existingProject' },
|
||||
]
|
||||
: [{ name: 'Overwrite', value: 'existingProject' }],
|
||||
valueSummary(value) {
|
||||
return isDesktop()
|
||||
? value === 'newProject'
|
||||
? 'New project'
|
||||
: 'Existing project'
|
||||
: 'Overwrite'
|
||||
},
|
||||
},
|
||||
projectName: {
|
||||
inputType: 'options',
|
||||
required: (commandsContext) =>
|
||||
isDesktop() &&
|
||||
commandsContext.argumentsToSubmit.method === 'existingProject',
|
||||
skip: true,
|
||||
options: (_, context) => {
|
||||
const { folders } = systemIOActor.getSnapshot().context
|
||||
const options: CommandArgumentOption<string>[] = []
|
||||
folders.forEach((folder) => {
|
||||
options.push({
|
||||
name: folder.name,
|
||||
value: folder.name,
|
||||
isCurrent: false,
|
||||
})
|
||||
})
|
||||
return options
|
||||
},
|
||||
},
|
||||
newProjectName: {
|
||||
inputType: 'text',
|
||||
required: (commandsContext) =>
|
||||
isDesktop() &&
|
||||
commandsContext.argumentsToSubmit.method === 'newProject',
|
||||
skip: true,
|
||||
},
|
||||
prompt: {
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return isDesktop() ? [textToCADCommand] : [textToCADCommand]
|
||||
}
|
@ -151,9 +151,6 @@ export type ModelingCommandSchema = {
|
||||
}
|
||||
namedValue: KclCommandValue
|
||||
}
|
||||
'Text-to-CAD': {
|
||||
prompt: string
|
||||
}
|
||||
'Prompt-to-edit': {
|
||||
prompt: string
|
||||
selection: Selections
|
||||
@ -959,16 +956,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
'Text-to-CAD': {
|
||||
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
|
||||
icon: 'chat',
|
||||
args: {
|
||||
prompt: {
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'Prompt-to-edit': {
|
||||
description: 'Use Zoo AI to edit your kcl',
|
||||
icon: 'chat',
|
||||
|
@ -28,6 +28,7 @@ import type { AppMachineContext } from '@src/lib/types'
|
||||
import { createAuthCommands } from '@src/lib/commandBarConfigs/authCommandConfig'
|
||||
import { commandBarMachine } from '@src/machines/commandBarMachine'
|
||||
import { createProjectCommands } from '@src/lib/commandBarConfigs/projectsCommandConfig'
|
||||
import { createApplicationCommands } from '@src/lib/commandBarConfigs/applicationCommandConfig'
|
||||
|
||||
export const codeManager = new CodeManager()
|
||||
export const engineCommandManager = new EngineCommandManager()
|
||||
@ -226,6 +227,7 @@ commandBarActor.send({
|
||||
commands: [
|
||||
...createAuthCommands({ authActor }),
|
||||
...createProjectCommands({ systemIOActor }),
|
||||
...createApplicationCommands({ systemIOActor }),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
@ -2,8 +2,6 @@ import type { Models } from '@kittycad/lib'
|
||||
import { VITE_KC_API_BASE_URL } from '@src/env'
|
||||
import toast from 'react-hot-toast'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
import type { ContextFrom, EventFrom } from 'xstate'
|
||||
|
||||
import {
|
||||
ToastTextToCadError,
|
||||
ToastTextToCadSuccess,
|
||||
@ -12,13 +10,12 @@ import { FILE_EXT } from '@src/lib/constants'
|
||||
import crossPlatformFetch from '@src/lib/crossPlatformFetch'
|
||||
import { getNextFileName } from '@src/lib/desktopFS'
|
||||
import { isDesktop } from '@src/lib/isDesktop'
|
||||
import { kclManager } from '@src/lib/singletons'
|
||||
import type { Themes } from '@src/lib/theme'
|
||||
import { kclManager, systemIOActor } from '@src/lib/singletons'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import { toSync } from '@src/lib/utils'
|
||||
import type { fileMachine } from '@src/machines/fileMachine'
|
||||
|
||||
async function submitTextToCadPrompt(
|
||||
export async function submitTextToCadPrompt(
|
||||
prompt: string,
|
||||
projectName: string,
|
||||
token?: string
|
||||
@ -52,7 +49,7 @@ async function submitTextToCadPrompt(
|
||||
return data
|
||||
}
|
||||
|
||||
async function getTextToCadResult(
|
||||
export async function getTextToCadResult(
|
||||
id: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCad_type'] | Error> {
|
||||
@ -68,29 +65,25 @@ async function getTextToCadResult(
|
||||
return data
|
||||
}
|
||||
|
||||
interface TextToKclProps {
|
||||
interface TextToKclPropsApplicationLevel {
|
||||
trimmedPrompt: string
|
||||
fileMachineSend: (
|
||||
type: EventFrom<typeof fileMachine>,
|
||||
data?: unknown
|
||||
) => unknown
|
||||
navigate: NavigateFunction
|
||||
context: ContextFrom<typeof fileMachine>
|
||||
token?: string
|
||||
settings: {
|
||||
theme: Themes
|
||||
projectName: string
|
||||
isProjectNew: boolean
|
||||
settings?: {
|
||||
highlightEdges: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitAndAwaitTextToKcl({
|
||||
export async function submitAndAwaitTextToKclSystemIO({
|
||||
trimmedPrompt,
|
||||
fileMachineSend,
|
||||
navigate,
|
||||
context,
|
||||
token,
|
||||
projectName,
|
||||
navigate,
|
||||
isProjectNew,
|
||||
settings,
|
||||
}: TextToKclProps) {
|
||||
}: TextToKclPropsApplicationLevel) {
|
||||
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
||||
const showFailureToast = (message: string) => {
|
||||
toast.error(
|
||||
@ -99,6 +92,9 @@ export async function submitAndAwaitTextToKcl({
|
||||
toastId,
|
||||
message,
|
||||
prompt: trimmedPrompt,
|
||||
method: isProjectNew ? 'newProject' : 'existingProject',
|
||||
projectName: isProjectNew ? '' : projectName,
|
||||
newProjectName: isProjectNew ? projectName : '',
|
||||
}),
|
||||
{
|
||||
id: toastId,
|
||||
@ -109,7 +105,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
|
||||
const textToCadQueued = await submitTextToCadPrompt(
|
||||
trimmedPrompt,
|
||||
context.project.name,
|
||||
projectName,
|
||||
token
|
||||
)
|
||||
.then((value) => {
|
||||
@ -174,12 +170,16 @@ export async function submitAndAwaitTextToKcl({
|
||||
}
|
||||
)
|
||||
|
||||
let newFileName = ''
|
||||
|
||||
const textToCadOutputCreated = await textToCadComplete
|
||||
.catch((e) => {
|
||||
showFailureToast('Failed to generate parametric model')
|
||||
return e
|
||||
})
|
||||
.then(async (value) => {
|
||||
console.log('completed')
|
||||
console.log(value)
|
||||
if (value.code === undefined || !value.code || value.code.length === 0) {
|
||||
// We want to show the real error message to the user.
|
||||
if (value.error && value.error.length > 0) {
|
||||
@ -193,7 +193,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
}
|
||||
|
||||
const TRUNCATED_PROMPT_LENGTH = 24
|
||||
let newFileName = `${value.prompt
|
||||
newFileName = `${value.prompt
|
||||
.slice(0, TRUNCATED_PROMPT_LENGTH)
|
||||
.replace(/\s/gi, '-')
|
||||
.replace(/\W/gi, '-')
|
||||
@ -205,16 +205,15 @@ export async function submitAndAwaitTextToKcl({
|
||||
// and by extension the file-deletion-on-reject logic.
|
||||
newFileName = getNextFileName({
|
||||
entryName: newFileName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
baseDir: projectName,
|
||||
}).name
|
||||
|
||||
fileMachineSend({
|
||||
type: 'Create file',
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.createKCLFile,
|
||||
data: {
|
||||
name: newFileName,
|
||||
makeDir: false,
|
||||
content: value.code,
|
||||
silent: true,
|
||||
requestedProjectName: projectName,
|
||||
requestedCode: value.code,
|
||||
requestedFileName: newFileName,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -234,13 +233,16 @@ export async function submitAndAwaitTextToKcl({
|
||||
// and options to reject or accept the model
|
||||
toast.success(
|
||||
() =>
|
||||
// EXPECTED: This will throw a error in dev mode, do not worry about it.
|
||||
// Warning: Internal React error: Expected static flag was missing. Please notify the React team.
|
||||
ToastTextToCadSuccess({
|
||||
toastId,
|
||||
data: textToCadOutputCreated,
|
||||
token,
|
||||
projectName: projectName,
|
||||
fileName: newFileName,
|
||||
navigate,
|
||||
context,
|
||||
fileMachineSend,
|
||||
isProjectNew,
|
||||
settings,
|
||||
}),
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DEV } from '@src/env'
|
||||
import type { EventFrom, StateFrom } from 'xstate'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
|
||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||
import { createLiteral } from '@src/lang/create'
|
||||
@ -418,11 +419,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
array: [
|
||||
{
|
||||
id: 'text-to-cad',
|
||||
onClick: () =>
|
||||
onClick: () => {
|
||||
const currentProject =
|
||||
settingsActor.getSnapshot().context.currentProject
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Text-to-CAD', groupId: 'modeling' },
|
||||
}),
|
||||
data: {
|
||||
name: 'Text-to-CAD',
|
||||
groupId: 'application',
|
||||
argDefaultValues: {
|
||||
method: 'existingProject',
|
||||
projectName: currentProject?.name,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
icon: 'sparkles',
|
||||
iconColor: '#29FFA4',
|
||||
alwaysDark: true,
|
||||
|
@ -63,6 +63,7 @@ type FileMachineEvents =
|
||||
}
|
||||
| { type: 'assign'; data: { [key: string]: any } }
|
||||
| { type: 'Refresh' }
|
||||
| { type: 'Refresh with new project'; data: { project: Project } }
|
||||
|
||||
export const fileMachine = setup({
|
||||
types: {} as {
|
||||
@ -95,6 +96,10 @@ export const fileMachine = setup({
|
||||
)
|
||||
},
|
||||
}),
|
||||
setProject: assign(({ event }) => {
|
||||
if (event.type !== 'Refresh with new project') return {}
|
||||
return { project: event.data.project }
|
||||
}),
|
||||
navigateToFile: () => {},
|
||||
openFileInNewWindow: () => {},
|
||||
renameToastSuccess: () => {},
|
||||
@ -183,6 +188,10 @@ export const fileMachine = setup({
|
||||
},
|
||||
|
||||
Refresh: '.Reading files',
|
||||
'Refresh with new project': {
|
||||
actions: ['setProject'],
|
||||
target: '.Reading files',
|
||||
},
|
||||
},
|
||||
states: {
|
||||
'Has no files': {
|
||||
|
@ -371,7 +371,6 @@ export type ModelingMachineEvent =
|
||||
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
|
||||
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
||||
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
|
||||
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
||||
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
|
||||
| {
|
||||
type: 'Delete selection'
|
||||
|
@ -19,3 +19,9 @@ export const useHasListedProjects = () =>
|
||||
|
||||
export const useClearURLParams = () =>
|
||||
useSelector(systemIOActor, (state) => state.context.clearURLParams)
|
||||
|
||||
export const useRequestedTextToCadGeneration = () =>
|
||||
useSelector(
|
||||
systemIOActor,
|
||||
(state) => state.context.requestedTextToCadGeneration
|
||||
)
|
||||
|
@ -76,6 +76,22 @@ export const systemIOMachine = setup({
|
||||
| {
|
||||
type: SystemIOMachineEvents.setDefaultProjectFolderName
|
||||
data: { requestedDefaultProjectFolderName: string }
|
||||
}
|
||||
// TODO: Move this generateTextToCAD to another machine in the future and make a whole machine out of it.
|
||||
| {
|
||||
type: SystemIOMachineEvents.generateTextToCAD
|
||||
data: {
|
||||
requestedPrompt: string
|
||||
requestedProjectName: string
|
||||
isProjectNew: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: SystemIOMachineEvents.deleteKCLFile
|
||||
data: {
|
||||
requestedProjectName: string
|
||||
requestedFileName: string
|
||||
}
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -143,6 +159,12 @@ export const systemIOMachine = setup({
|
||||
return event.output
|
||||
},
|
||||
}),
|
||||
[SystemIOMachineActions.setRequestedTextToCadGeneration]: assign({
|
||||
requestedTextToCadGeneration: ({ event }) => {
|
||||
assertEvent(event, SystemIOMachineEvents.generateTextToCAD)
|
||||
return event.data
|
||||
},
|
||||
}),
|
||||
},
|
||||
actors: {
|
||||
[SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise(
|
||||
@ -213,6 +235,23 @@ export const systemIOMachine = setup({
|
||||
return { value: true, error: undefined }
|
||||
}
|
||||
),
|
||||
[SystemIOMachineActors.deleteKCLFile]: fromPromise(
|
||||
async ({
|
||||
input,
|
||||
}: {
|
||||
input: {
|
||||
context: SystemIOContext
|
||||
requestedProjectName: string
|
||||
requestedFileName: string
|
||||
}
|
||||
}): Promise<{
|
||||
message: string
|
||||
fileName: string
|
||||
projectName: string
|
||||
}> => {
|
||||
return { message: '', fileName: '', projectName: '' }
|
||||
}
|
||||
),
|
||||
},
|
||||
}).createMachine({
|
||||
initial: SystemIOMachineStates.idle,
|
||||
@ -231,6 +270,11 @@ export const systemIOMachine = setup({
|
||||
},
|
||||
canReadWriteProjectDirectory: { value: true, error: undefined },
|
||||
clearURLParams: { value: false },
|
||||
requestedTextToCadGeneration: {
|
||||
requestedPrompt: '',
|
||||
requestedProjectName: NO_PROJECT_DIRECTORY,
|
||||
isProjectNew: true,
|
||||
},
|
||||
}),
|
||||
states: {
|
||||
[SystemIOMachineStates.idle]: {
|
||||
@ -267,6 +311,12 @@ export const systemIOMachine = setup({
|
||||
[SystemIOMachineEvents.importFileFromURL]: {
|
||||
target: SystemIOMachineStates.importFileFromURL,
|
||||
},
|
||||
[SystemIOMachineEvents.generateTextToCAD]: {
|
||||
actions: [SystemIOMachineActions.setRequestedTextToCadGeneration],
|
||||
},
|
||||
[SystemIOMachineEvents.deleteKCLFile]: {
|
||||
target: SystemIOMachineStates.deletingKCLFile,
|
||||
},
|
||||
},
|
||||
},
|
||||
[SystemIOMachineStates.readingFolders]: {
|
||||
@ -374,7 +424,7 @@ export const systemIOMachine = setup({
|
||||
}
|
||||
},
|
||||
onDone: {
|
||||
target: SystemIOMachineStates.idle,
|
||||
target: SystemIOMachineStates.readingFolders,
|
||||
},
|
||||
onError: {
|
||||
target: SystemIOMachineStates.idle,
|
||||
@ -440,5 +490,26 @@ export const systemIOMachine = setup({
|
||||
},
|
||||
},
|
||||
},
|
||||
[SystemIOMachineStates.deletingKCLFile]: {
|
||||
invoke: {
|
||||
id: SystemIOMachineActors.deleteKCLFile,
|
||||
src: SystemIOMachineActors.deleteKCLFile,
|
||||
input: ({ context, event }) => {
|
||||
assertEvent(event, SystemIOMachineEvents.deleteKCLFile)
|
||||
return {
|
||||
context,
|
||||
requestedProjectName: event.data.requestedProjectName,
|
||||
requestedFileName: event.data.requestedFileName,
|
||||
}
|
||||
},
|
||||
onDone: {
|
||||
target: SystemIOMachineStates.readingFolders,
|
||||
},
|
||||
onError: {
|
||||
target: SystemIOMachineStates.readingFolders,
|
||||
actions: [SystemIOMachineActions.toastError],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -228,5 +228,28 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
|
||||
return result
|
||||
}
|
||||
),
|
||||
[SystemIOMachineActors.deleteKCLFile]: fromPromise(
|
||||
async ({
|
||||
input,
|
||||
}: {
|
||||
input: {
|
||||
context: SystemIOContext
|
||||
requestedProjectName: string
|
||||
requestedFileName: string
|
||||
}
|
||||
}) => {
|
||||
const path = window.electron.path.join(
|
||||
input.context.projectDirectoryPath,
|
||||
input.requestedProjectName,
|
||||
input.requestedFileName
|
||||
)
|
||||
await window.electron.rm(path)
|
||||
return {
|
||||
message: 'File deleted successfully',
|
||||
projectName: input.requestedProjectName,
|
||||
fileName: input.requestedFileName,
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
})
|
||||
|
@ -9,6 +9,7 @@ export enum SystemIOMachineActors {
|
||||
createKCLFile = 'create kcl file',
|
||||
checkReadWrite = 'check read write',
|
||||
importFileFromURL = 'import file from URL',
|
||||
deleteKCLFile = 'delete kcl delete',
|
||||
}
|
||||
|
||||
export enum SystemIOMachineStates {
|
||||
@ -21,6 +22,7 @@ export enum SystemIOMachineStates {
|
||||
creatingKCLFile = 'creatingKCLFile',
|
||||
checkingReadWrite = 'checkingReadWrite',
|
||||
importFileFromURL = 'importFileFromURL',
|
||||
deletingKCLFile = 'deletingKCLFile',
|
||||
}
|
||||
|
||||
const donePrefix = 'xstate.done.actor.'
|
||||
@ -40,6 +42,8 @@ export enum SystemIOMachineEvents {
|
||||
done_checkReadWrite = donePrefix + 'check read write',
|
||||
importFileFromURL = 'import file from URL',
|
||||
done_importFileFromURL = donePrefix + 'import file from URL',
|
||||
generateTextToCAD = 'generate text to CAD',
|
||||
deleteKCLFile = 'delete kcl file',
|
||||
}
|
||||
|
||||
export enum SystemIOMachineActions {
|
||||
@ -51,6 +55,7 @@ export enum SystemIOMachineActions {
|
||||
toastSuccess = 'toastSuccess',
|
||||
toastError = 'toastError',
|
||||
setReadWriteProjectDirectory = 'set read write project directory',
|
||||
setRequestedTextToCadGeneration = 'set requested text to cad generation',
|
||||
}
|
||||
|
||||
export const NO_PROJECT_DIRECTORY = ''
|
||||
@ -70,4 +75,9 @@ export type SystemIOContext = {
|
||||
requestedFileName: { project: string; file: string }
|
||||
canReadWriteProjectDirectory: { value: boolean; error: unknown }
|
||||
clearURLParams: { value: boolean }
|
||||
requestedTextToCadGeneration: {
|
||||
requestedPrompt: string
|
||||
requestedProjectName: string
|
||||
isProjectNew: boolean
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user