Migrate Text-to-CAD (edit) to multi-file endpoint (#6066)
* start of migrate to multi file endpoint * get some relative path stuff sorted * blobifying files, and making selections work with imports working * add write to disk * warn about big projects * update known circular * update snapshot * remove log * tweak selection filters * Update src/components/ModelingMachineProvider.tsx Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * fmt * fix one thing * typo * raw dog form data like a fucking peasant * remove fake data * fmt * steal Kevin's stuff * good progress * clean up * fix writing to files when response returns * comment the terriable code * push fix of sorts * better fix * spot of clean up * fix: Needed to support the bad request flow, the toast will hang forever, the return control flows don't dismiss a forever toast * fix: handling more error flows by dismissing the toast * chore: leaving a comment for a confusing workflow * fix: trying to clean up some async logic * fix: trying to fix a few things at once... * fix: fixing toast success * fix: how did this desync? * fix: removing useless logic, we write to disk ahead of time, the continue is to say ya no problem * fix: typo * Change back to `spawnChild`, forego `actors` by reference * fix: updating PR comments * fix: found a bug with paths from rust! it is actually OS paths! * fix: updated type still is failing tsc * fix: the type of the machine was wrong, we always set it to at least '' * fix: idk man * Fix happy path test (button labels) --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Kevin Nadro <kevin@zoo.dev> Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com> Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com> Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
This commit is contained in:
@ -224,7 +224,51 @@ export class CmdBarFixture {
|
|||||||
// Create a handler function that saves request bodies to a file
|
// Create a handler function that saves request bodies to a file
|
||||||
const requestHandler = (route: Route, request: Request) => {
|
const requestHandler = (route: Route, request: Request) => {
|
||||||
try {
|
try {
|
||||||
const requestBody = request.postDataJSON()
|
// Get the raw post data
|
||||||
|
const postData = request.postData()
|
||||||
|
if (!postData) {
|
||||||
|
console.error('No post data found in request')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all parts from the multipart form data
|
||||||
|
const boundary = postData.match(/------WebKitFormBoundary[^\r\n]*/)?.[0]
|
||||||
|
if (!boundary) {
|
||||||
|
console.error('Could not find form boundary')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = postData.split(boundary).filter((part) => part.trim())
|
||||||
|
const files: Record<string, string> = {}
|
||||||
|
let eventData = null
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
// Skip the final boundary marker
|
||||||
|
if (part.startsWith('--')) continue
|
||||||
|
|
||||||
|
const nameMatch = part.match(/name="([^"]+)"/)
|
||||||
|
if (!nameMatch) continue
|
||||||
|
|
||||||
|
const name = nameMatch[1]
|
||||||
|
const content = part.split(/\r?\n\r?\n/)[1]?.trim()
|
||||||
|
if (!content) continue
|
||||||
|
|
||||||
|
if (name === 'event') {
|
||||||
|
eventData = JSON.parse(content)
|
||||||
|
} else {
|
||||||
|
files[name] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventData) {
|
||||||
|
console.error('Could not find event JSON in multipart form data')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
...eventData,
|
||||||
|
files,
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
const dir = path.dirname(outputPath)
|
const dir = path.dirname(outputPath)
|
||||||
@ -245,7 +289,10 @@ export class CmdBarFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start monitoring requests
|
// Start monitoring requests
|
||||||
await this.page.route('**/ml/text-to-cad/iteration', requestHandler)
|
await this.page.route(
|
||||||
|
'**/ml/text-to-cad/multi-file/iteration',
|
||||||
|
requestHandler
|
||||||
|
)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
|
`Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
import path from 'path'
|
||||||
|
import fsp from 'fs/promises'
|
||||||
|
|
||||||
/* eslint-disable jest/no-conditional-expect */
|
/* eslint-disable jest/no-conditional-expect */
|
||||||
|
|
||||||
@ -22,7 +24,8 @@ import { expect, test } from '@e2e/playwright/zoo-test'
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const file = `sketch001 = startSketchOn(XZ)
|
const fileWithImport = `import "b.kcl" as b
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
profile001 = startProfile(sketch001, at = [57.81, 250.51])
|
profile001 = startProfile(sketch001, at = [57.81, 250.51])
|
||||||
|> line(end = [121.13, 56.63], tag = $seg02)
|
|> line(end = [121.13, 56.63], tag = $seg02)
|
||||||
|> line(end = [83.37, -34.61], tag = $seg01)
|
|> line(end = [83.37, -34.61], tag = $seg01)
|
||||||
@ -39,7 +42,10 @@ sketch002 = startSketchOn(XZ)
|
|||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
extrude002 = extrude(sketch002, length = 50)
|
extrude002 = extrude(sketch002, length = 50)
|
||||||
sketch003 = startSketchOn(XY)
|
b
|
||||||
|
`
|
||||||
|
|
||||||
|
const importedFile = `sketch003 = startSketchOn(XY)
|
||||||
|> startProfile(at = [52.92, 157.81])
|
|> startProfile(at = [52.92, 157.81])
|
||||||
|> angledLine(angle = 0, length = 176.4, tag = $rectangleSegmentA001)
|
|> angledLine(angle = 0, length = 176.4, tag = $rectangleSegmentA001)
|
||||||
|> angledLine(
|
|> angledLine(
|
||||||
@ -50,17 +56,28 @@ sketch003 = startSketchOn(XY)
|
|||||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
extrude003 = extrude(sketch003, length = 20)
|
extrude(sketch003, length = 20)
|
||||||
`
|
`
|
||||||
|
|
||||||
test.describe('edit with AI example snapshots', () => {
|
test.describe('edit with AI example snapshots', () => {
|
||||||
test(
|
test(
|
||||||
`change colour`,
|
`change colour`,
|
||||||
{ tag: '@snapshot' },
|
// TODO this is more of a snapshot, but atm it needs to be manually run locally to update the files
|
||||||
|
{ tag: ['@electron'] },
|
||||||
async ({ context, homePage, cmdBar, editor, page, scene }) => {
|
async ({ context, homePage, cmdBar, editor, page, scene }) => {
|
||||||
await context.addInitScript((file) => {
|
const project = 'test-dir'
|
||||||
localStorage.setItem('persistCode', file)
|
await context.folderSetupFn(async (dir) => {
|
||||||
}, file)
|
const projectDir = path.join(dir, project)
|
||||||
await homePage.goToModelingScene()
|
await fsp.mkdir(projectDir, { recursive: true })
|
||||||
|
|
||||||
|
// Create the imported file
|
||||||
|
await fsp.writeFile(path.join(projectDir, 'b.kcl'), importedFile)
|
||||||
|
|
||||||
|
// Create the main file that imports
|
||||||
|
await fsp.writeFile(path.join(projectDir, 'main.kcl'), fileWithImport)
|
||||||
|
})
|
||||||
|
|
||||||
|
await homePage.openProject(project)
|
||||||
await scene.settled(cmdBar)
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
const body1CapCoords = { x: 571, y: 351 }
|
const body1CapCoords = { x: 571, y: 351 }
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
import * as fsp from 'fs/promises'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
/* eslint-disable jest/no-conditional-expect */
|
/* eslint-disable jest/no-conditional-expect */
|
||||||
|
|
||||||
@ -50,31 +52,28 @@ test.describe('Prompt-to-edit tests', () => {
|
|||||||
page,
|
page,
|
||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
await context.addInitScript((file) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
localStorage.setItem('persistCode', file)
|
const projectDir = path.join(dir, 'test-project')
|
||||||
}, file)
|
await fsp.mkdir(projectDir, { recursive: true })
|
||||||
await homePage.goToModelingScene()
|
await fsp.writeFile(path.join(projectDir, 'main.kcl'), file)
|
||||||
|
})
|
||||||
|
await homePage.openProject('test-project')
|
||||||
await scene.settled(cmdBar)
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
const body1CapCoords = { x: 571, y: 311 }
|
const body1CapCoords = { x: 571, y: 311 }
|
||||||
const greenCheckCoords = { x: 565, y: 305 }
|
|
||||||
const body2WallCoords = { x: 609, y: 153 }
|
|
||||||
const [clickBody1Cap] = scene.makeMouseHelpers(
|
const [clickBody1Cap] = scene.makeMouseHelpers(
|
||||||
body1CapCoords.x,
|
body1CapCoords.x,
|
||||||
body1CapCoords.y
|
body1CapCoords.y
|
||||||
)
|
)
|
||||||
const yellow: [number, number, number] = [179, 179, 131]
|
const yellow: [number, number, number] = [179, 179, 131]
|
||||||
const green: [number, number, number] = [128, 194, 88]
|
|
||||||
const notGreen: [number, number, number] = [132, 132, 132]
|
|
||||||
const body2NotGreen: [number, number, number] = [88, 88, 88]
|
|
||||||
const submittingToast = page.getByText(
|
const submittingToast = page.getByText(
|
||||||
'Submitting to Text-to-CAD API...'
|
'Submitting to Text-to-CAD API...'
|
||||||
)
|
)
|
||||||
const successToast = page.getByText('Prompt to edit successful')
|
const successToast = page.getByText('Prompt to edit successful')
|
||||||
const acceptBtn = page.getByRole('button', {
|
const acceptBtn = page.getByRole('button', {
|
||||||
name: 'checkmark Accept',
|
name: 'checkmark Continue',
|
||||||
})
|
})
|
||||||
const rejectBtn = page.getByRole('button', { name: 'close Reject' })
|
const rejectBtn = page.getByRole('button', { name: 'close Revert' })
|
||||||
|
|
||||||
await test.step('wait for scene to load select body and check selection came through', async () => {
|
await test.step('wait for scene to load select body and check selection came through', async () => {
|
||||||
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
await scene.expectPixelColor([134, 134, 134], body1CapCoords, 15)
|
||||||
@ -103,32 +102,21 @@ test.describe('Prompt-to-edit tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await test.step('verify initial change', async () => {
|
await test.step('verify initial change', async () => {
|
||||||
await scene.expectPixelColor(green, greenCheckCoords, 20)
|
|
||||||
await scene.expectPixelColor(body2NotGreen, body2WallCoords, 15)
|
|
||||||
await editor.expectEditor.toContain('appearance(')
|
await editor.expectEditor.toContain('appearance(')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!shouldReject) {
|
if (!shouldReject) {
|
||||||
await test.step('check accept works and can be "undo"ed', async () => {
|
await test.step('check accept works', async () => {
|
||||||
await acceptBtn.click()
|
await acceptBtn.click()
|
||||||
await expect(successToast).not.toBeVisible()
|
await expect(successToast).not.toBeVisible()
|
||||||
|
|
||||||
await scene.expectPixelColor(green, greenCheckCoords, 15)
|
|
||||||
await editor.expectEditor.toContain('appearance(')
|
await editor.expectEditor.toContain('appearance(')
|
||||||
|
|
||||||
// ctrl-z works after accepting
|
|
||||||
await page.keyboard.down('ControlOrMeta')
|
|
||||||
await page.keyboard.press('KeyZ')
|
|
||||||
await page.keyboard.up('ControlOrMeta')
|
|
||||||
await editor.expectEditor.not.toContain('appearance(')
|
|
||||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
await test.step('check reject works', async () => {
|
await test.step('check reject works', async () => {
|
||||||
await rejectBtn.click()
|
await rejectBtn.click()
|
||||||
await expect(successToast).not.toBeVisible()
|
await expect(successToast).not.toBeVisible()
|
||||||
|
|
||||||
await scene.expectPixelColor(notGreen, greenCheckCoords, 15)
|
|
||||||
await editor.expectEditor.not.toContain('appearance(')
|
await editor.expectEditor.not.toContain('appearance(')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -208,7 +196,7 @@ test.describe('Prompt-to-edit tests', () => {
|
|||||||
|
|
||||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||||
const successToast = page.getByText('Prompt to edit successful')
|
const successToast = page.getByText('Prompt to edit successful')
|
||||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
const acceptBtn = page.getByRole('button', { name: 'checkmark Continue' })
|
||||||
|
|
||||||
await test.step('wait for scene to load and select code in editor', async () => {
|
await test.step('wait for scene to load and select code in editor', async () => {
|
||||||
// Find and select the text "sketch002" in the editor
|
// Find and select the text "sketch002" in the editor
|
||||||
@ -277,7 +265,7 @@ test.describe('Prompt-to-edit tests', () => {
|
|||||||
|
|
||||||
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
const submittingToast = page.getByText('Submitting to Text-to-CAD API...')
|
||||||
const successToast = page.getByText('Prompt to edit successful')
|
const successToast = page.getByText('Prompt to edit successful')
|
||||||
const acceptBtn = page.getByRole('button', { name: 'checkmark Accept' })
|
const acceptBtn = page.getByRole('button', { name: 'checkmark Continue' })
|
||||||
|
|
||||||
await test.step('select multiple bodies and fire prompt', async () => {
|
await test.step('select multiple bodies and fire prompt', async () => {
|
||||||
// Initial color check
|
// Initial color check
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
• Circular Dependencies
|
• Circular Dependencies
|
||||||
1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts
|
||||||
2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts
|
||||||
3) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
3) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/setAngleLength.tsx -> src/components/SetAngleLengthModal.tsx -> src/lib/useCalculateKclExpression.ts
|
||||||
4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
5) src/lib/singletons.ts -> src/lang/codeManager.ts
|
5) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts
|
||||||
6) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
6) src/lib/singletons.ts -> src/lang/codeManager.ts
|
||||||
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
|
7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts
|
||||||
8) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts
|
8) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts
|
||||||
|
@ -21,30 +21,19 @@ import type { Plane } from '@rust/kcl-lib/bindings/Plane'
|
|||||||
|
|
||||||
import { useAppState } from '@src/AppState'
|
import { useAppState } from '@src/AppState'
|
||||||
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from '@src/clientSideScene/CameraControls'
|
||||||
import {
|
|
||||||
SEGMENT_BODIES,
|
|
||||||
getParentGroup,
|
|
||||||
} from '@src/clientSideScene/sceneConstants'
|
|
||||||
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
|
||||||
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
|
||||||
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
|
||||||
import { applyConstraintIntersect } from '@src/components/Toolbar/Intersect'
|
|
||||||
import { applyConstraintAbsDistance } from '@src/components/Toolbar/SetAbsDistance'
|
|
||||||
import {
|
|
||||||
angleBetweenInfo,
|
|
||||||
applyConstraintAngleBetween,
|
|
||||||
} from '@src/components/Toolbar/SetAngleBetween'
|
|
||||||
import { applyConstraintHorzVertDistance } from '@src/components/Toolbar/SetHorzVertDistance'
|
|
||||||
import {
|
import {
|
||||||
applyConstraintAngleLength,
|
applyConstraintAngleLength,
|
||||||
applyConstraintLength,
|
applyConstraintLength,
|
||||||
} from '@src/components/Toolbar/setAngleLength'
|
} from '@src/components/Toolbar/setAngleLength'
|
||||||
|
import {
|
||||||
|
SEGMENT_BODIES,
|
||||||
|
getParentGroup,
|
||||||
|
} from '@src/clientSideScene/sceneConstants'
|
||||||
import { useFileContext } from '@src/hooks/useFileContext'
|
import { useFileContext } from '@src/hooks/useFileContext'
|
||||||
import {
|
import {
|
||||||
useMenuListener,
|
useMenuListener,
|
||||||
useSketchModeMenuEnableDisable,
|
useSketchModeMenuEnableDisable,
|
||||||
} from '@src/hooks/useMenu'
|
} from '@src/hooks/useMenu'
|
||||||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
|
||||||
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
import useStateMachineCommands from '@src/hooks/useStateMachineCommands'
|
||||||
import { useKclContext } from '@src/lang/KclProvider'
|
import { useKclContext } from '@src/lang/KclProvider'
|
||||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||||
@ -70,34 +59,33 @@ import {
|
|||||||
getPlaneFromArtifact,
|
getPlaneFromArtifact,
|
||||||
} from '@src/lang/std/artifactGraph'
|
} from '@src/lang/std/artifactGraph'
|
||||||
import {
|
import {
|
||||||
EngineConnectionEvents,
|
|
||||||
EngineConnectionStateType,
|
EngineConnectionStateType,
|
||||||
|
EngineConnectionEvents,
|
||||||
} from '@src/lang/std/engineConnection'
|
} from '@src/lang/std/engineConnection'
|
||||||
|
import { err, reportRejection, trap, reject } from '@src/lib/trap'
|
||||||
|
import { isNonNullable, platform, uuidv4 } from '@src/lib/utils'
|
||||||
|
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
||||||
|
import type { FileMeta } from '@src/lib/types'
|
||||||
|
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
||||||
|
import { commandBarActor } from '@src/lib/singletons'
|
||||||
|
import { useToken, useSettings } from '@src/lib/singletons'
|
||||||
|
import type { IndexLoaderData } from '@src/lib/types'
|
||||||
import {
|
import {
|
||||||
crossProduct,
|
|
||||||
isCursorInSketchCommandRange,
|
|
||||||
updateSketchDetailsNodePaths,
|
|
||||||
} from '@src/lang/util'
|
|
||||||
import type {
|
|
||||||
KclValue,
|
|
||||||
PathToNode,
|
|
||||||
PipeExpression,
|
|
||||||
Program,
|
|
||||||
VariableDeclaration,
|
|
||||||
} from '@src/lang/wasm'
|
|
||||||
import { parse, recast, resultIsOk } from '@src/lang/wasm'
|
|
||||||
import type { ModelingCommandSchema } from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
|
||||||
import { modelingMachineCommandConfig } from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
|
||||||
import {
|
|
||||||
EXECUTION_TYPE_MOCK,
|
|
||||||
EXPORT_TOAST_MESSAGES,
|
EXPORT_TOAST_MESSAGES,
|
||||||
MAKE_TOAST_MESSAGES,
|
MAKE_TOAST_MESSAGES,
|
||||||
|
EXECUTION_TYPE_MOCK,
|
||||||
|
FILE_EXT,
|
||||||
} from '@src/lib/constants'
|
} from '@src/lib/constants'
|
||||||
import { exportMake } from '@src/lib/exportMake'
|
import { exportMake } from '@src/lib/exportMake'
|
||||||
import { exportSave } from '@src/lib/exportSave'
|
import { exportSave } from '@src/lib/exportSave'
|
||||||
import { promptToEditFlow } from '@src/lib/promptToEdit'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import type { Selections } from '@src/lib/selections'
|
import type { FileEntry } from '@src/lib/project'
|
||||||
import { handleSelectionBatch, updateSelections } from '@src/lib/selections'
|
import type { WebContentSendPayload } from '@src/menu/channels'
|
||||||
|
import {
|
||||||
|
getPersistedContext,
|
||||||
|
modelingMachine,
|
||||||
|
modelingMachineDefaultContext,
|
||||||
|
} from '@src/machines/modelingMachine'
|
||||||
import {
|
import {
|
||||||
codeManager,
|
codeManager,
|
||||||
editorManager,
|
editorManager,
|
||||||
@ -107,18 +95,39 @@ import {
|
|||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
sceneInfra,
|
sceneInfra,
|
||||||
} from '@src/lib/singletons'
|
} from '@src/lib/singletons'
|
||||||
import { err, reject, reportRejection, trap } from '@src/lib/trap'
|
import type { MachineManager } from '@src/components/MachineManagerProvider'
|
||||||
import type { IndexLoaderData } from '@src/lib/types'
|
import { MachineManagerContext } from '@src/components/MachineManagerProvider'
|
||||||
import { platform, uuidv4 } from '@src/lib/utils'
|
|
||||||
import { useSettings, useToken } from '@src/lib/singletons'
|
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
|
||||||
import { kclEditorActor } from '@src/machines/kclEditorMachine'
|
|
||||||
import {
|
import {
|
||||||
getPersistedContext,
|
handleSelectionBatch,
|
||||||
modelingMachine,
|
updateSelections,
|
||||||
modelingMachineDefaultContext,
|
type Selections,
|
||||||
} from '@src/machines/modelingMachine'
|
} from '@src/lib/selections'
|
||||||
import type { WebContentSendPayload } from '@src/menu/channels'
|
import {
|
||||||
|
crossProduct,
|
||||||
|
isCursorInSketchCommandRange,
|
||||||
|
updateSketchDetailsNodePaths,
|
||||||
|
} from '@src/lang/util'
|
||||||
|
import {
|
||||||
|
modelingMachineCommandConfig,
|
||||||
|
type ModelingCommandSchema,
|
||||||
|
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||||
|
import type {
|
||||||
|
KclValue,
|
||||||
|
PathToNode,
|
||||||
|
PipeExpression,
|
||||||
|
Program,
|
||||||
|
VariableDeclaration,
|
||||||
|
} from '@src/lang/wasm'
|
||||||
|
import { parse, recast, resultIsOk } from '@src/lang/wasm'
|
||||||
|
import { applyConstraintHorzVertDistance } from '@src/components/Toolbar/SetHorzVertDistance'
|
||||||
|
import {
|
||||||
|
angleBetweenInfo,
|
||||||
|
applyConstraintAngleBetween,
|
||||||
|
} from '@src/components/Toolbar/SetAngleBetween'
|
||||||
|
import { applyConstraintIntersect } from '@src/components/Toolbar/Intersect'
|
||||||
|
import { applyConstraintAbsDistance } from '@src/components/Toolbar/SetAbsDistance'
|
||||||
|
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||||
|
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||||||
|
|
||||||
export const ModelingMachineContext = createContext(
|
export const ModelingMachineContext = createContext(
|
||||||
{} as {
|
{} as {
|
||||||
@ -1726,8 +1735,94 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
'submit-prompt-edit': fromPromise(async ({ input }) => {
|
'submit-prompt-edit': fromPromise(async ({ input }) => {
|
||||||
|
let projectFiles: FileMeta[] = [
|
||||||
|
{
|
||||||
|
type: 'kcl',
|
||||||
|
relPath: 'main.kcl',
|
||||||
|
absPath: 'main.kcl',
|
||||||
|
fileContents: codeManager.code,
|
||||||
|
execStateFileNamesIndex: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const execStateNameToIndexMap: { [fileName: string]: number } = {}
|
||||||
|
Object.entries(kclManager.execState.filenames).forEach(
|
||||||
|
([index, val]) => {
|
||||||
|
if (val?.type === 'Local') {
|
||||||
|
execStateNameToIndexMap[val.value] = Number(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let basePath = ''
|
||||||
|
if (isDesktop() && context?.project?.children) {
|
||||||
|
basePath = context?.selectedDirectory?.path
|
||||||
|
const filePromises: Promise<FileMeta | null>[] = []
|
||||||
|
let uploadSize = 0
|
||||||
|
const recursivelyPushFilePromises = (files: FileEntry[]) => {
|
||||||
|
// mutates filePromises declared above, so this function definition should stay here
|
||||||
|
// if pulled out, it would need to be refactored.
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.children !== null) {
|
||||||
|
// is directory
|
||||||
|
recursivelyPushFilePromises(file.children)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePathToFileNameWithExtension = file.path
|
||||||
|
const fileNameWithExtension = window.electron.path.relative(
|
||||||
|
basePath,
|
||||||
|
absolutePathToFileNameWithExtension
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePromise = window.electron
|
||||||
|
.readFile(absolutePathToFileNameWithExtension)
|
||||||
|
.then((file): FileMeta => {
|
||||||
|
uploadSize += file.byteLength
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
const fileType = window.electron.path.extname(
|
||||||
|
absolutePathToFileNameWithExtension
|
||||||
|
)
|
||||||
|
if (fileType === FILE_EXT) {
|
||||||
|
return {
|
||||||
|
type: 'kcl',
|
||||||
|
absPath: absolutePathToFileNameWithExtension,
|
||||||
|
relPath: fileNameWithExtension,
|
||||||
|
fileContents: decoder.decode(file),
|
||||||
|
execStateFileNamesIndex:
|
||||||
|
execStateNameToIndexMap[
|
||||||
|
absolutePathToFileNameWithExtension
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const blob = new Blob([file], {
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
type: 'other',
|
||||||
|
relPath: fileNameWithExtension,
|
||||||
|
data: blob,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('error reading file', e)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
filePromises.push(filePromise)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recursivelyPushFilePromises(context?.project?.children)
|
||||||
|
projectFiles = (await Promise.all(filePromises)).filter(
|
||||||
|
isNonNullable
|
||||||
|
)
|
||||||
|
const MB20 = 2 ** 20 * 20
|
||||||
|
if (uploadSize > MB20) {
|
||||||
|
toast.error(
|
||||||
|
'Your project exceeds 20Mb, this will slow down Text-to-CAD\nPlease remove any unnecessary files'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
return await promptToEditFlow({
|
return await promptToEditFlow({
|
||||||
code: codeManager.code,
|
projectFiles,
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
selections: input.selection,
|
selections: input.selection,
|
||||||
token,
|
token,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
TextToCadIteration_type,
|
|
||||||
TextToCad_type,
|
TextToCad_type,
|
||||||
|
TextToCadMultiFileIteration_type,
|
||||||
} from '@kittycad/lib/dist/types/src/models'
|
} from '@kittycad/lib/dist/types/src/models'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
@ -29,9 +29,17 @@ import { PATHS } from '@src/lib/paths'
|
|||||||
import { codeManager, kclManager, systemIOActor } from '@src/lib/singletons'
|
import { codeManager, kclManager, systemIOActor } from '@src/lib/singletons'
|
||||||
import { sendTelemetry } from '@src/lib/textToCadTelemetry'
|
import { sendTelemetry } from '@src/lib/textToCadTelemetry'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
|
import {
|
||||||
|
SystemIOMachineEvents,
|
||||||
|
waitForIdleState,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
|
import {
|
||||||
|
useProjectDirectoryPath,
|
||||||
|
useRequestedProjectName,
|
||||||
|
} from '@src/machines/systemIO/hooks'
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
import { commandBarActor } from '@src/lib/singletons'
|
||||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
import type { FileMeta } from '@src/lib/types'
|
||||||
import { useProjectDirectoryPath } from '@src/machines/systemIO/hooks'
|
import type { RequestedKCLFile } from '@src/machines/systemIO/utils'
|
||||||
|
|
||||||
const CANVAS_SIZE = 128
|
const CANVAS_SIZE = 128
|
||||||
const PROMPT_TRUNCATE_LENGTH = 128
|
const PROMPT_TRUNCATE_LENGTH = 128
|
||||||
@ -478,14 +486,17 @@ export function ToastPromptToEditCadSuccess({
|
|||||||
toastId,
|
toastId,
|
||||||
token,
|
token,
|
||||||
data,
|
data,
|
||||||
oldCode,
|
oldCodeWebAppOnly: oldCode,
|
||||||
|
oldFiles,
|
||||||
}: {
|
}: {
|
||||||
toastId: string
|
toastId: string
|
||||||
oldCode: string
|
oldCodeWebAppOnly: string
|
||||||
data: TextToCadIteration_type
|
oldFiles: FileMeta[]
|
||||||
|
data: TextToCadMultiFileIteration_type
|
||||||
token?: string
|
token?: string
|
||||||
}) {
|
}) {
|
||||||
const modelId = data.id
|
const modelId = data.id
|
||||||
|
const requestedProjectName = useRequestedProjectName()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 min-w-80">
|
<div className="flex gap-4 min-w-80">
|
||||||
@ -514,13 +525,37 @@ export function ToastPromptToEditCadSuccess({
|
|||||||
data-negative-button={'reject'}
|
data-negative-button={'reject'}
|
||||||
name={'Reject'}
|
name={'Reject'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
void (async () => {
|
||||||
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
|
sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
|
||||||
|
// revert to before the prompt-to-edit
|
||||||
|
if (isDesktop()) {
|
||||||
|
const requestedFiles: RequestedKCLFile[] = []
|
||||||
|
for (const file of oldFiles) {
|
||||||
|
if (file.type !== 'kcl') {
|
||||||
|
// only need to write the kcl files
|
||||||
|
// as the endpoint would have never overwritten other file types
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
requestedFiles.push({
|
||||||
|
requestedCode: file.fileContents,
|
||||||
|
requestedFileName: file.relPath,
|
||||||
|
requestedProjectName: requestedProjectName.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
await writeOverFilesAndExecute({
|
||||||
|
requestedFiles: requestedFiles,
|
||||||
|
projectName: requestedProjectName.name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
codeManager.updateCodeEditor(oldCode)
|
codeManager.updateCodeEditor(oldCode)
|
||||||
kclManager.executeCode().catch(reportRejection)
|
kclManager.executeCode().catch(reportRejection)
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
|
}
|
||||||
|
})()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{'Reject'}
|
{'Revert'}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
@ -532,23 +567,38 @@ export function ToastPromptToEditCadSuccess({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
sendTelemetry(modelId, 'accepted', token).catch(reportRejection)
|
sendTelemetry(modelId, 'accepted', token).catch(reportRejection)
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
|
/**
|
||||||
// Write new content to disk since they have accepted.
|
* NO OP. Do not rewrite code to disk, we already do this ahead of time. This will dismiss the toast.
|
||||||
codeManager
|
* All of the files were already written! Don't rewrite the current code editor.
|
||||||
.writeToFile()
|
* If this prompt to edit makes 5 new files, the code manager is only watching 1 of these files, why
|
||||||
.then(() => {
|
* would it rewrite the current file selected when this is completed?
|
||||||
// no-op
|
*/
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error('Failed to save prompt-to-edit to disk')
|
|
||||||
console.error(e)
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Accept
|
Continue
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const writeOverFilesAndExecute = async ({
|
||||||
|
requestedFiles,
|
||||||
|
projectName,
|
||||||
|
}: {
|
||||||
|
requestedFiles: RequestedKCLFile[]
|
||||||
|
projectName: string
|
||||||
|
}) => {
|
||||||
|
systemIOActor.send({
|
||||||
|
type: SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject,
|
||||||
|
data: {
|
||||||
|
files: requestedFiles,
|
||||||
|
requestedProjectName: projectName,
|
||||||
|
override: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// to await the result of the send event above
|
||||||
|
await waitForIdleState({ systemIOActor })
|
||||||
|
}
|
||||||
|
@ -34,8 +34,8 @@ import type {
|
|||||||
ExtrudeFacePlane,
|
ExtrudeFacePlane,
|
||||||
} from '@src/machines/modelingMachine'
|
} from '@src/machines/modelingMachine'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { getStringAfterLastSeparator } from '@src/lib/paths'
|
||||||
import { findAllChildrenAndOrderByPlaceInCode } from '@src/lang/modifyAst/boolean'
|
import { findAllChildrenAndOrderByPlaceInCode } from '@src/lang/modifyAst/boolean'
|
||||||
import { localModuleSafePathSplit } from '@src/lib/paths'
|
|
||||||
|
|
||||||
export function useEngineConnectionSubscriptions() {
|
export function useEngineConnectionSubscriptions() {
|
||||||
const { send, context, state } = useModelingContext()
|
const { send, context, state } = useModelingContext()
|
||||||
@ -210,9 +210,11 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (importDetails?.type === 'Local') {
|
if (importDetails?.type === 'Local') {
|
||||||
const paths = localModuleSafePathSplit(importDetails.value)
|
// importDetails has OS specific separators from the rust side!
|
||||||
const fileName = paths[paths.length - 1]
|
const fileNameWithExtension = getStringAfterLastSeparator(
|
||||||
showSketchOnImportToast(fileName)
|
importDetails.value
|
||||||
|
)
|
||||||
|
showSketchOnImportToast(fileNameWithExtension)
|
||||||
} else if (
|
} else if (
|
||||||
importDetails?.type === 'Main' ||
|
importDetails?.type === 'Main' ||
|
||||||
importDetails?.type === 'Std'
|
importDetails?.type === 'Std'
|
||||||
|
@ -215,8 +215,3 @@ export function desktopSafePathSplit(path: string): string[] {
|
|||||||
export function desktopSafePathJoin(paths: string[]): string {
|
export function desktopSafePathJoin(paths: string[]): string {
|
||||||
return isDesktop() ? paths.join(window?.electron?.sep) : webSafeJoin(paths)
|
return isDesktop() ? paths.join(window?.electron?.sep) : webSafeJoin(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localModuleSafePathSplit(path: string) {
|
|
||||||
const modulePathSafeSep = '/'
|
|
||||||
return path.split(modulePathSafeSep)
|
|
||||||
}
|
|
||||||
|
@ -4,8 +4,18 @@ import type { Models } from '@kittycad/lib'
|
|||||||
import { VITE_KC_API_BASE_URL } from '@src/env'
|
import { VITE_KC_API_BASE_URL } from '@src/env'
|
||||||
import { diffLines } from 'diff'
|
import { diffLines } from 'diff'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import type { TextToCadMultiFileIteration_type } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import { getCookie, TOKEN_PERSIST_KEY } from '@src/machines/authMachine'
|
||||||
|
import { COOKIE_NAME } from '@src/lib/constants'
|
||||||
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
|
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
|
||||||
|
import { ActionButton } from '@src/components/ActionButton'
|
||||||
|
import { CustomIcon } from '@src/components/CustomIcon'
|
||||||
|
|
||||||
import { ToastPromptToEditCadSuccess } from '@src/components/ToastTextToCad'
|
import {
|
||||||
|
ToastPromptToEditCadSuccess,
|
||||||
|
writeOverFilesAndExecute,
|
||||||
|
} from '@src/components/ToastTextToCad'
|
||||||
import { modelingMachineEvent } from '@src/editor/manager'
|
import { modelingMachineEvent } from '@src/editor/manager'
|
||||||
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
|
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
|
||||||
import { topLevelRange } from '@src/lang/util'
|
import { topLevelRange } from '@src/lang/util'
|
||||||
@ -15,6 +25,13 @@ import type { Selections } from '@src/lib/selections'
|
|||||||
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { uuidv4 } from '@src/lib/utils'
|
import { uuidv4 } from '@src/lib/utils'
|
||||||
|
import type { File as KittyCadLibFile } from '@kittycad/lib/dist/types/src/models'
|
||||||
|
import type { FileMeta } from '@src/lib/types'
|
||||||
|
import type { RequestedKCLFile } from '@src/machines/systemIO/utils'
|
||||||
|
|
||||||
|
type KclFileMetaMap = {
|
||||||
|
[execStateFileNamesIndex: number]: Extract<FileMeta, { type: 'kcl' }>
|
||||||
|
}
|
||||||
|
|
||||||
function sourceIndexToLineColumn(
|
function sourceIndexToLineColumn(
|
||||||
code: string,
|
code: string,
|
||||||
@ -37,43 +54,117 @@ function convertAppRangeToApiRange(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TextToCadErrorResponse = {
|
||||||
|
error_code: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTextToCadRequest(
|
||||||
|
body: {
|
||||||
|
prompt: string
|
||||||
|
source_ranges: Models['SourceRangePrompt_type'][]
|
||||||
|
project_name?: string
|
||||||
|
kcl_version: string
|
||||||
|
},
|
||||||
|
files: KittyCadLibFile[],
|
||||||
|
token: string
|
||||||
|
): Promise<TextToCadMultiFileIteration_type | Error> {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('body', JSON.stringify(body))
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append('files', file.data, file.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${VITE_KC_API_BASE_URL}/ml/text-to-cad/multi-file/iteration`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return new Error(`HTTP error! status: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if ('error_code' in data) {
|
||||||
|
const errorData = data as TextToCadErrorResponse
|
||||||
|
return new Error(errorData.message || 'Unknown error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as TextToCadMultiFileIteration_type
|
||||||
|
}
|
||||||
|
|
||||||
export async function submitPromptToEditToQueue({
|
export async function submitPromptToEditToQueue({
|
||||||
prompt,
|
prompt,
|
||||||
selections,
|
selections,
|
||||||
code,
|
projectFiles,
|
||||||
token,
|
token,
|
||||||
artifactGraph,
|
artifactGraph,
|
||||||
projectName,
|
projectName,
|
||||||
}: {
|
}: {
|
||||||
prompt: string
|
prompt: string
|
||||||
selections: Selections | null
|
selections: Selections | null
|
||||||
code: string
|
projectFiles: FileMeta[]
|
||||||
projectName: string
|
projectName: string
|
||||||
token?: string
|
token?: string
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
}) {
|
||||||
|
const _token =
|
||||||
|
token && token !== ''
|
||||||
|
? token
|
||||||
|
: getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
|
||||||
|
|
||||||
|
const kclFilesMap: KclFileMetaMap = {}
|
||||||
|
const endPointFiles: KittyCadLibFile[] = []
|
||||||
|
projectFiles.forEach((file) => {
|
||||||
|
let data: Blob
|
||||||
|
if (file.type === 'other') {
|
||||||
|
data = file.data
|
||||||
|
} else {
|
||||||
|
// file.type === 'kcl'
|
||||||
|
kclFilesMap[file.execStateFileNamesIndex] = file
|
||||||
|
data = new Blob([file.fileContents], { type: 'text/kcl' })
|
||||||
|
}
|
||||||
|
endPointFiles.push({
|
||||||
|
name: file.relPath,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// If no selection, use whole file
|
// If no selection, use whole file
|
||||||
if (selections === null) {
|
if (selections === null) {
|
||||||
const body: Models['TextToCadIterationBody_type'] = {
|
return submitTextToCadRequest(
|
||||||
original_source_code: code,
|
{
|
||||||
prompt,
|
prompt,
|
||||||
source_ranges: [], // Empty ranges indicates whole file
|
source_ranges: [],
|
||||||
project_name:
|
project_name:
|
||||||
projectName !== '' && projectName !== 'browser'
|
projectName !== '' && projectName !== 'browser'
|
||||||
? projectName
|
? projectName
|
||||||
: undefined,
|
: undefined,
|
||||||
kcl_version: kclManager.kclVersion,
|
kcl_version: kclManager.kclVersion,
|
||||||
}
|
},
|
||||||
return submitToApi(body, token)
|
endPointFiles,
|
||||||
|
_token
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle manual code selections and artifact selections differently
|
// Handle manual code selections and artifact selections differently
|
||||||
const ranges: Models['TextToCadIterationBody_type']['source_ranges'] =
|
const ranges: Models['SourceRangePrompt_type'][] =
|
||||||
selections.graphSelections.flatMap((selection) => {
|
selections.graphSelections.flatMap((selection) => {
|
||||||
const artifact = selection.artifact
|
const artifact = selection.artifact
|
||||||
|
const execStateFileNamesIndex = selection?.codeRef?.range?.[2]
|
||||||
|
const file = kclFilesMap?.[execStateFileNamesIndex]
|
||||||
|
const code = file?.fileContents || ''
|
||||||
|
const filePath = file?.relPath || ''
|
||||||
|
|
||||||
// For artifact selections, add context
|
// For artifact selections, add context
|
||||||
const prompts: Models['TextToCadIterationBody_type']['source_ranges'] = []
|
const prompts: Models['SourceRangePrompt_type'][] = []
|
||||||
|
|
||||||
if (artifact?.type === 'cap') {
|
if (artifact?.type === 'cap') {
|
||||||
prompts.push({
|
prompts.push({
|
||||||
@ -87,6 +178,7 @@ If you need to operate on this cap, for example for sketching on the face, you c
|
|||||||
When they made this selection they main have intended this surface directly or meant something more general like the sweep body.
|
When they made this selection they main have intended this surface directly or meant something more general like the sweep body.
|
||||||
See later source ranges for more context.`,
|
See later source ranges for more context.`,
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
let sweep = getArtifactOfTypes(
|
let sweep = getArtifactOfTypes(
|
||||||
{ key: artifact.sweepId, types: ['sweep'] },
|
{ key: artifact.sweepId, types: ['sweep'] },
|
||||||
@ -96,6 +188,7 @@ See later source ranges for more context.`,
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,6 +198,7 @@ See later source ranges for more context.`,
|
|||||||
The source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example "startSketchOn(someSweepVariable, face = segmentTag)"
|
The source range though is for the original segment before it was extruded, you can add a tag to that segment in order to refer to this wall, for example "startSketchOn(someSweepVariable, face = segmentTag)"
|
||||||
But it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep`,
|
But it's also worth bearing in mind that the user may have intended to select the sweep itself, not this individual wall, see later source ranges for more context. about the sweep`,
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
let sweep = getArtifactOfTypes(
|
let sweep = getArtifactOfTypes(
|
||||||
{ key: artifact.sweepId, types: ['sweep'] },
|
{ key: artifact.sweepId, types: ['sweep'] },
|
||||||
@ -114,6 +208,7 @@ But it's also worth bearing in mind that the user may have intended to select th
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +225,7 @@ and then use the function ${
|
|||||||
}
|
}
|
||||||
See later source ranges for more context. about the sweep`,
|
See later source ranges for more context. about the sweep`,
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
let sweep = getArtifactOfTypes(
|
let sweep = getArtifactOfTypes(
|
||||||
{ key: artifact.sweepId, types: ['sweep'] },
|
{ key: artifact.sweepId, types: ['sweep'] },
|
||||||
@ -139,6 +235,7 @@ See later source ranges for more context. about the sweep`,
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
|
||||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,6 +244,7 @@ See later source ranges for more context. about the sweep`,
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: `This selection is of a segment, likely an individual part of a profile. Segments are often "constrained" by the use of variables and relationships with other segments. Adding tags to segments helps refer to their length, angle or other properties`,
|
prompt: `This selection is of a segment, likely an individual part of a profile. Segments are often "constrained" by the use of variables and relationships with other segments. Adding tags to segments helps refer to their length, angle or other properties`,
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
prompts.push({
|
prompts.push({
|
||||||
@ -155,6 +253,7 @@ Because it now refers to an edge the way to refer to this edge is to add a tag t
|
|||||||
i.e. \`fillet( radius = someInteger, tags = [newTag])\` will work in the case of filleting this edge
|
i.e. \`fillet( radius = someInteger, tags = [newTag])\` will work in the case of filleting this edge
|
||||||
See later source ranges for more context. about the sweep`,
|
See later source ranges for more context. about the sweep`,
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
let path = getArtifactOfTypes(
|
let path = getArtifactOfTypes(
|
||||||
{ key: artifact.pathId, types: ['path'] },
|
{ key: artifact.pathId, types: ['path'] },
|
||||||
@ -169,6 +268,7 @@ See later source ranges for more context. about the sweep`,
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: `This is the sweep's source range from the user's main selection of the edge.`,
|
prompt: `This is the sweep's source range from the user's main selection of the edge.`,
|
||||||
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,56 +280,32 @@ See later source ranges for more context. about the sweep`,
|
|||||||
prompts.push({
|
prompts.push({
|
||||||
prompt: '',
|
prompt: '',
|
||||||
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
range: convertAppRangeToApiRange(selection.codeRef.range, code),
|
||||||
|
file: filePath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return prompts
|
return prompts
|
||||||
})
|
})
|
||||||
|
return submitTextToCadRequest(
|
||||||
const body: Models['TextToCadIterationBody_type'] = {
|
{
|
||||||
original_source_code: code,
|
|
||||||
prompt,
|
prompt,
|
||||||
source_ranges: ranges,
|
source_ranges: ranges,
|
||||||
project_name:
|
project_name:
|
||||||
projectName !== '' && projectName !== 'browser' ? projectName : undefined,
|
projectName !== '' && projectName !== 'browser'
|
||||||
|
? projectName
|
||||||
|
: undefined,
|
||||||
kcl_version: kclManager.kclVersion,
|
kcl_version: kclManager.kclVersion,
|
||||||
}
|
|
||||||
|
|
||||||
return submitToApi(body, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to handle API submission
|
|
||||||
async function submitToApi(
|
|
||||||
body: Models['TextToCadIterationBody_type'],
|
|
||||||
token?: string
|
|
||||||
): Promise<Models['TextToCadIteration_type'] | Error> {
|
|
||||||
const url = VITE_KC_API_BASE_URL + '/ml/text-to-cad/iteration'
|
|
||||||
const data: Models['TextToCadIteration_type'] | Error =
|
|
||||||
await crossPlatformFetch(
|
|
||||||
url,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
},
|
},
|
||||||
token
|
endPointFiles,
|
||||||
|
_token
|
||||||
)
|
)
|
||||||
|
|
||||||
// Make sure we have an id.
|
|
||||||
if (data instanceof Error) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.id) {
|
|
||||||
return new Error('No id returned from Text-to-CAD API')
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPromptToEditResult(
|
export async function getPromptToEditResult(
|
||||||
id: string,
|
id: string,
|
||||||
token?: string
|
token?: string
|
||||||
): Promise<Models['TextToCadIteration_type'] | Error> {
|
): Promise<Models['TextToCadMultiFileIteration_type'] | Error> {
|
||||||
const url = VITE_KC_API_BASE_URL + '/async/operations/' + id
|
const url = VITE_KC_API_BASE_URL + '/async/operations/' + id
|
||||||
const data: Models['TextToCadIteration_type'] | Error =
|
const data: Models['TextToCadMultiFileIteration_type'] | Error =
|
||||||
await crossPlatformFetch(
|
await crossPlatformFetch(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
@ -244,31 +320,50 @@ export async function getPromptToEditResult(
|
|||||||
export async function doPromptEdit({
|
export async function doPromptEdit({
|
||||||
prompt,
|
prompt,
|
||||||
selections,
|
selections,
|
||||||
code,
|
projectFiles,
|
||||||
token,
|
token,
|
||||||
artifactGraph,
|
artifactGraph,
|
||||||
projectName,
|
projectName,
|
||||||
}: {
|
}: {
|
||||||
prompt: string
|
prompt: string
|
||||||
selections: Selections
|
selections: Selections
|
||||||
code: string
|
projectFiles: FileMeta[]
|
||||||
token?: string
|
token?: string
|
||||||
projectName: string
|
projectName: string
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
}): Promise<Models['TextToCadIteration_type'] | Error> {
|
}): Promise<Models['TextToCadMultiFileIteration_type'] | Error> {
|
||||||
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
||||||
const submitResult = await submitPromptToEditToQueue({
|
|
||||||
|
let submitResult
|
||||||
|
|
||||||
|
// work around for @kittycad/lib not really being built for the browser
|
||||||
|
;(window as any).process = {
|
||||||
|
env: {
|
||||||
|
ZOO_API_TOKEN: token,
|
||||||
|
ZOO_HOST: VITE_KC_API_BASE_URL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
submitResult = await submitPromptToEditToQueue({
|
||||||
prompt,
|
prompt,
|
||||||
selections,
|
selections,
|
||||||
code,
|
projectFiles,
|
||||||
token,
|
token,
|
||||||
artifactGraph,
|
artifactGraph,
|
||||||
projectName,
|
projectName,
|
||||||
})
|
})
|
||||||
if (err(submitResult)) return submitResult
|
} catch (e: any) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
return new Error(e.message)
|
||||||
|
}
|
||||||
|
if (submitResult instanceof Error) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
return submitResult
|
||||||
|
}
|
||||||
|
|
||||||
const textToCadComplete = new Promise<Models['TextToCadIteration_type']>(
|
const textToCadComplete = new Promise<
|
||||||
(resolve, reject) => {
|
Models['TextToCadMultiFileIteration_type']
|
||||||
|
>((resolve, reject) => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
||||||
const CHECK_DELAY = 200
|
const CHECK_DELAY = 200
|
||||||
@ -277,7 +372,11 @@ export async function doPromptEdit({
|
|||||||
|
|
||||||
while (timeElapsed < MAX_CHECK_TIMEOUT) {
|
while (timeElapsed < MAX_CHECK_TIMEOUT) {
|
||||||
const check = await getPromptToEditResult(submitResult.id, token)
|
const check = await getPromptToEditResult(submitResult.id, token)
|
||||||
if (check instanceof Error || check.status === 'failed') {
|
if (
|
||||||
|
check instanceof Error ||
|
||||||
|
check.status === 'failed' ||
|
||||||
|
check.error
|
||||||
|
) {
|
||||||
reject(check)
|
reject(check)
|
||||||
return
|
return
|
||||||
} else if (check.status === 'completed') {
|
} else if (check.status === 'completed') {
|
||||||
@ -291,8 +390,7 @@ export async function doPromptEdit({
|
|||||||
|
|
||||||
reject(new Error('Text-to-CAD API timed out'))
|
reject(new Error('Text-to-CAD API timed out'))
|
||||||
})().catch(reportRejection)
|
})().catch(reportRejection)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await textToCadComplete
|
const result = await textToCadComplete
|
||||||
@ -313,14 +411,14 @@ export async function doPromptEdit({
|
|||||||
export async function promptToEditFlow({
|
export async function promptToEditFlow({
|
||||||
prompt,
|
prompt,
|
||||||
selections,
|
selections,
|
||||||
code,
|
projectFiles,
|
||||||
token,
|
token,
|
||||||
artifactGraph,
|
artifactGraph,
|
||||||
projectName,
|
projectName,
|
||||||
}: {
|
}: {
|
||||||
prompt: string
|
prompt: string
|
||||||
selections: Selections
|
selections: Selections
|
||||||
code: string
|
projectFiles: FileMeta[]
|
||||||
token?: string
|
token?: string
|
||||||
artifactGraph: ArtifactGraph
|
artifactGraph: ArtifactGraph
|
||||||
projectName: string
|
projectName: string
|
||||||
@ -328,16 +426,84 @@ export async function promptToEditFlow({
|
|||||||
const result = await doPromptEdit({
|
const result = await doPromptEdit({
|
||||||
prompt,
|
prompt,
|
||||||
selections,
|
selections,
|
||||||
code,
|
projectFiles,
|
||||||
token,
|
token,
|
||||||
artifactGraph,
|
artifactGraph,
|
||||||
projectName,
|
projectName,
|
||||||
})
|
})
|
||||||
if (err(result)) return Promise.reject(result)
|
if (err(result)) {
|
||||||
const oldCode = codeManager.code
|
toast.error('Failed to modify.')
|
||||||
const { code: newCode } = result
|
return Promise.reject(result)
|
||||||
|
}
|
||||||
|
const oldCodeWebAppOnly = codeManager.code
|
||||||
|
|
||||||
|
if (!isDesktop() && Object.values(result.outputs).length > 1) {
|
||||||
|
const toastId = uuidv4()
|
||||||
|
toast.error(
|
||||||
|
(t) => (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p>Multiple files were returned from Text-to-CAD.</p>
|
||||||
|
<p>You need to use the desktop app to support this.</p>
|
||||||
|
<div className="flex justify-between items-center mt-2">
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href="https://zoo.dev/modeling-app/download"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-400 hover:text-blue-300 underline flex align-middle"
|
||||||
|
onClick={openExternalBrowserIfDesktop(
|
||||||
|
'https://zoo.dev/modeling-app/download'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CustomIcon
|
||||||
|
name="link"
|
||||||
|
className="w-4 h-4 text-chalkboard-70 dark:text-chalkboard-40"
|
||||||
|
/>
|
||||||
|
Download Desktop App
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
iconStart={{
|
||||||
|
icon: 'close',
|
||||||
|
}}
|
||||||
|
name="Dismiss"
|
||||||
|
onClick={() => {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: toastId,
|
||||||
|
duration: Infinity,
|
||||||
|
icon: null,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isDesktop()) {
|
||||||
|
const requestedFiles: RequestedKCLFile[] = []
|
||||||
|
|
||||||
|
for (const [relativePath, fileContents] of Object.entries(result.outputs)) {
|
||||||
|
requestedFiles.push({
|
||||||
|
requestedCode: fileContents,
|
||||||
|
requestedFileName: relativePath,
|
||||||
|
requestedProjectName: projectName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeOverFilesAndExecute({
|
||||||
|
requestedFiles,
|
||||||
|
projectName,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const newCode = result.outputs['main.kcl']
|
||||||
codeManager.updateCodeEditor(newCode)
|
codeManager.updateCodeEditor(newCode)
|
||||||
const diff = reBuildNewCodeWithRanges(oldCode, newCode)
|
const diff = reBuildNewCodeWithRanges(oldCodeWebAppOnly, newCode)
|
||||||
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
|
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
|
||||||
EditorSelection.range(range[0], range[1])
|
EditorSelection.range(range[0], range[1])
|
||||||
)
|
)
|
||||||
@ -349,6 +515,7 @@ export async function promptToEditFlow({
|
|||||||
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
|
||||||
})
|
})
|
||||||
await kclManager.executeCode()
|
await kclManager.executeCode()
|
||||||
|
}
|
||||||
const toastId = uuidv4()
|
const toastId = uuidv4()
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
@ -357,7 +524,8 @@ export async function promptToEditFlow({
|
|||||||
toastId,
|
toastId,
|
||||||
data: result,
|
data: result,
|
||||||
token,
|
token,
|
||||||
oldCode,
|
oldCodeWebAppOnly,
|
||||||
|
oldFiles: projectFiles,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
id: toastId,
|
id: toastId,
|
@ -12,8 +12,8 @@ import { SceneInfra } from '@src/clientSideScene/sceneInfra'
|
|||||||
import type { BaseUnit } from '@src/lib/settings/settingsTypes'
|
import type { BaseUnit } from '@src/lib/settings/settingsTypes'
|
||||||
|
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import type { SnapshotFrom } from 'xstate'
|
import type { ActorRefFrom, SnapshotFrom } from 'xstate'
|
||||||
import { createActor, setup, assign } from 'xstate'
|
import { createActor, setup, spawnChild } from 'xstate'
|
||||||
|
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { createSettings } from '@src/lib/settings/initialSettings'
|
import { createSettings } from '@src/lib/settings/initialSettings'
|
||||||
@ -131,7 +131,6 @@ const appMachine = setup({
|
|||||||
types: {} as {
|
types: {} as {
|
||||||
context: AppMachineContext
|
context: AppMachineContext
|
||||||
},
|
},
|
||||||
actors: appMachineActors,
|
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
id: 'modeling-app',
|
id: 'modeling-app',
|
||||||
context: {
|
context: {
|
||||||
@ -143,50 +142,36 @@ const appMachine = setup({
|
|||||||
},
|
},
|
||||||
entry: [
|
entry: [
|
||||||
/**
|
/**
|
||||||
* We originally wanted to use spawnChild but the inferred type blew up. The more children we
|
* We have been battling XState's type unions exploding in size,
|
||||||
* created the type complexity went through the roof. This functionally should act the same.
|
* so for these global actors, we have decided to forego creating them by reference
|
||||||
* the system and parent internals are tracked properly. After reading the documentation
|
* using the `actors` property in the `setup` function, and
|
||||||
* it suggests either method but this method requires manual clean up as described in the gotcha
|
* inline them instead.
|
||||||
* comment block below. If this becomes an issue we can always move this spawn into createActor functions
|
|
||||||
* in javascript above and reference those directly but the system and parent internals within xstate
|
|
||||||
* will not work.
|
|
||||||
*/
|
*/
|
||||||
assign({
|
spawnChild(appMachineActors[AUTH], { systemId: AUTH }),
|
||||||
// Gotcha, if you use spawn, make sure you remove the ActorRef from context
|
spawnChild(appMachineActors[SETTINGS], {
|
||||||
// to prevent memory leaks when the spawned actor is no longer needed
|
|
||||||
authActor: ({ spawn }) => spawn(AUTH, { id: AUTH, systemId: AUTH }),
|
|
||||||
settingsActor: ({ spawn }) =>
|
|
||||||
spawn(SETTINGS, {
|
|
||||||
id: SETTINGS,
|
|
||||||
systemId: SETTINGS,
|
systemId: SETTINGS,
|
||||||
input: createSettings(),
|
input: createSettings(),
|
||||||
}),
|
}),
|
||||||
systemIOActor: ({ spawn }) =>
|
spawnChild(appMachineActors[ENGINE_STREAM], {
|
||||||
spawn(SYSTEM_IO, { id: SYSTEM_IO, systemId: SYSTEM_IO }),
|
|
||||||
engineStreamActor: ({ spawn }) =>
|
|
||||||
spawn(ENGINE_STREAM, {
|
|
||||||
id: ENGINE_STREAM,
|
|
||||||
systemId: ENGINE_STREAM,
|
systemId: ENGINE_STREAM,
|
||||||
input: engineStreamContextCreate(),
|
input: engineStreamContextCreate(),
|
||||||
}),
|
}),
|
||||||
commandBarActor: ({ spawn }) =>
|
spawnChild(appMachineActors[SYSTEM_IO], {
|
||||||
spawn(COMMAND_BAR, {
|
systemId: SYSTEM_IO,
|
||||||
id: COMMAND_BAR,
|
}),
|
||||||
|
spawnChild(appMachineActors[COMMAND_BAR], {
|
||||||
systemId: COMMAND_BAR,
|
systemId: COMMAND_BAR,
|
||||||
input: {
|
input: {
|
||||||
commands: [],
|
commands: [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
billingActor: ({ spawn }) =>
|
spawnChild(appMachineActors[BILLING], {
|
||||||
spawn(BILLING, {
|
|
||||||
id: BILLING,
|
|
||||||
systemId: BILLING,
|
systemId: BILLING,
|
||||||
input: {
|
input: {
|
||||||
...BILLING_CONTEXT_DEFAULTS,
|
...BILLING_CONTEXT_DEFAULTS,
|
||||||
urlUserService: VITE_KC_API_BASE_URL,
|
urlUserService: VITE_KC_API_BASE_URL,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -199,7 +184,9 @@ export const appActor = createActor(appMachine, {
|
|||||||
* the lifetime of {appActor}, but would not work if it were invoked
|
* the lifetime of {appActor}, but would not work if it were invoked
|
||||||
* or if it were destroyed under any conditions during {appActor}'s life
|
* or if it were destroyed under any conditions during {appActor}'s life
|
||||||
*/
|
*/
|
||||||
export const authActor = appActor.getSnapshot().context.authActor!
|
export const authActor = appActor.system.get(AUTH) as ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[typeof AUTH]
|
||||||
|
>
|
||||||
export const useAuthState = () => useSelector(authActor, (state) => state)
|
export const useAuthState = () => useSelector(authActor, (state) => state)
|
||||||
export const useToken = () =>
|
export const useToken = () =>
|
||||||
useSelector(authActor, (state) => state.context.token)
|
useSelector(authActor, (state) => state.context.token)
|
||||||
@ -211,7 +198,9 @@ export const useUser = () =>
|
|||||||
* the lifetime of {appActor}, but would not work if it were invoked
|
* the lifetime of {appActor}, but would not work if it were invoked
|
||||||
* or if it were destroyed under any conditions during {appActor}'s life
|
* or if it were destroyed under any conditions during {appActor}'s life
|
||||||
*/
|
*/
|
||||||
export const settingsActor = appActor.getSnapshot().context.settingsActor!
|
export const settingsActor = appActor.system.get(SETTINGS) as ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[typeof SETTINGS]
|
||||||
|
>
|
||||||
export const getSettings = () => {
|
export const getSettings = () => {
|
||||||
const { currentProject: _, ...settings } = settingsActor.getSnapshot().context
|
const { currentProject: _, ...settings } = settingsActor.getSnapshot().context
|
||||||
return settings
|
return settings
|
||||||
@ -223,14 +212,21 @@ export const useSettings = () =>
|
|||||||
return settings
|
return settings
|
||||||
})
|
})
|
||||||
|
|
||||||
export const systemIOActor = appActor.getSnapshot().context.systemIOActor!
|
export const systemIOActor = appActor.system.get(SYSTEM_IO) as ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[typeof SYSTEM_IO]
|
||||||
|
>
|
||||||
|
|
||||||
export const engineStreamActor =
|
export const engineStreamActor = appActor.system.get(
|
||||||
appActor.getSnapshot().context.engineStreamActor!
|
ENGINE_STREAM
|
||||||
|
) as ActorRefFrom<(typeof appMachineActors)[typeof ENGINE_STREAM]>
|
||||||
|
|
||||||
export const commandBarActor = appActor.getSnapshot().context.commandBarActor!
|
export const commandBarActor = appActor.system.get(COMMAND_BAR) as ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[typeof COMMAND_BAR]
|
||||||
|
>
|
||||||
|
|
||||||
export const billingActor = appActor.system.get(BILLING)
|
export const billingActor = appActor.system.get(BILLING) as ActorRefFrom<
|
||||||
|
(typeof appMachineActors)[typeof BILLING]
|
||||||
|
>
|
||||||
|
|
||||||
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>
|
||||||
state
|
state
|
||||||
|
@ -11,7 +11,10 @@ import crossPlatformFetch from '@src/lib/crossPlatformFetch'
|
|||||||
import { getNextFileName } from '@src/lib/desktopFS'
|
import { getNextFileName } from '@src/lib/desktopFS'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import { kclManager, systemIOActor } from '@src/lib/singletons'
|
import { kclManager, systemIOActor } from '@src/lib/singletons'
|
||||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
import {
|
||||||
|
SystemIOMachineEvents,
|
||||||
|
waitForIdleState,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
import { toSync } from '@src/lib/utils'
|
import { toSync } from '@src/lib/utils'
|
||||||
|
|
||||||
@ -218,6 +221,9 @@ export async function submitAndAwaitTextToKclSystemIO({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wait for the createKCLFile action to be completed
|
||||||
|
await waitForIdleState({ systemIOActor })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...value,
|
...value,
|
||||||
fileName: newFileName,
|
fileName: newFileName,
|
||||||
|
@ -10,7 +10,7 @@ import type { settingsMachine } from '@src/machines/settingsMachine'
|
|||||||
import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
import type { ActorRefFrom } from 'xstate'
|
import type { ActorRefFrom } from 'xstate'
|
||||||
import type { commandBarMachine } from '@src/machines/commandBarMachine'
|
import type { commandBarMachine } from '@src/machines/commandBarMachine'
|
||||||
import type { BillingActor } from '@src/machines/billingMachine'
|
import type { billingMachine } from '@src/machines/billingMachine'
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
export type IndexLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
@ -135,5 +135,19 @@ export type AppMachineContext = {
|
|||||||
systemIOActor?: ActorRefFrom<typeof systemIOMachine>
|
systemIOActor?: ActorRefFrom<typeof systemIOMachine>
|
||||||
engineStreamActor?: ActorRefFrom<typeof engineStreamMachine>
|
engineStreamActor?: ActorRefFrom<typeof engineStreamMachine>
|
||||||
commandBarActor?: ActorRefFrom<typeof commandBarMachine>
|
commandBarActor?: ActorRefFrom<typeof commandBarMachine>
|
||||||
billingActor?: BillingActor
|
billingActor?: ActorRefFrom<typeof billingMachine>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileMeta =
|
||||||
|
| {
|
||||||
|
type: 'kcl'
|
||||||
|
relPath: string
|
||||||
|
absPath: string
|
||||||
|
fileContents: string
|
||||||
|
execStateFileNamesIndex: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'other'
|
||||||
|
relPath: string
|
||||||
|
data: Blob
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { Binary as BSONBinary } from 'bson'
|
import type { Binary as BSONBinary } from 'bson'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import type { AnyMachineSnapshot } from 'xstate'
|
import type { AnyMachineSnapshot } from 'xstate'
|
||||||
|
|
||||||
import type { CallExpressionKw, SourceRange } from '@src/lang/wasm'
|
import type { CallExpressionKw, SourceRange } from '@src/lang/wasm'
|
||||||
import { isDesktop } from '@src/lib/isDesktop'
|
import { isDesktop } from '@src/lib/isDesktop'
|
||||||
import type { AsyncFn } from '@src/lib/types'
|
import type { AsyncFn } from '@src/lib/types'
|
||||||
|
@ -43,7 +43,7 @@ const LOCAL_USER: Models['User_type'] = {
|
|||||||
|
|
||||||
export interface UserContext {
|
export interface UserContext {
|
||||||
user?: Models['User_type']
|
user?: Models['User_type']
|
||||||
token?: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Events =
|
export type Events =
|
||||||
@ -204,7 +204,7 @@ async function getUser(input: { token?: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookie(cname: string): string | null {
|
export function getCookie(cname: string): string | null {
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -807,18 +807,6 @@ export const modelingMachine = setup({
|
|||||||
sketchDetails: event.output,
|
sketchDetails: event.output,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
'set selection filter to curves only': () => {
|
|
||||||
;(async () => {
|
|
||||||
await engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'set_selection_filter',
|
|
||||||
filter: ['curve'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})().catch(reportRejection)
|
|
||||||
},
|
|
||||||
'tear down client sketch': () => {
|
'tear down client sketch': () => {
|
||||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||||
},
|
},
|
||||||
@ -3130,7 +3118,7 @@ export const modelingMachine = setup({
|
|||||||
'Artifact graph emptied': 'hidePlanes',
|
'Artifact graph emptied': 'hidePlanes',
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: ['show default planes', 'set selection filter to curves only'],
|
entry: ['show default planes'],
|
||||||
description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`,
|
description: `We want to disable selections and hover highlights here, because users can't do anything with that information until they actually add something to the scene. The planes are just for orientation here.`,
|
||||||
exit: 'set selection filter to defaults',
|
exit: 'set selection filter to defaults',
|
||||||
},
|
},
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
setThemeClass,
|
setThemeClass,
|
||||||
} from '@src/lib/theme'
|
} from '@src/lib/theme'
|
||||||
import { reportRejection } from '@src/lib/trap'
|
import { reportRejection } from '@src/lib/trap'
|
||||||
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
|
|
||||||
type SettingsMachineContext = SettingsType & {
|
type SettingsMachineContext = SettingsType & {
|
||||||
currentProject?: Project
|
currentProject?: Project
|
||||||
@ -140,9 +141,9 @@ export const settingsMachine = setup({
|
|||||||
registerCommands: fromCallback<
|
registerCommands: fromCallback<
|
||||||
{ type: 'update' },
|
{ type: 'update' },
|
||||||
{ settings: SettingsType; actor: AnyActorRef }
|
{ settings: SettingsType; actor: AnyActorRef }
|
||||||
>(({ input, receive, self }) => {
|
>(({ input, receive, system }) => {
|
||||||
const commandBarActor = self.system.get('root').getSnapshot()
|
// This assumes this actor is running in a system with a command palette
|
||||||
.context.commandBarActor
|
const commandBarActor = system.get(ACTOR_IDS.COMMAND_BAR)
|
||||||
// If the user wants to hide the settings commands
|
// If the user wants to hide the settings commands
|
||||||
//from the command bar don't add them.
|
//from the command bar don't add them.
|
||||||
if (settings.commandBar.includeSettings.current === false) return
|
if (settings.commandBar.includeSettings.current === false) return
|
||||||
@ -157,14 +158,19 @@ export const settingsMachine = setup({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((c) => c !== null)
|
.filter((c) => c !== null)
|
||||||
|
if (commandBarActor === undefined) {
|
||||||
|
console.warn(
|
||||||
|
'Tried to register commands, but no command bar actor was found'
|
||||||
|
)
|
||||||
|
}
|
||||||
const addCommands = () =>
|
const addCommands = () =>
|
||||||
commandBarActor.send({
|
commandBarActor?.send({
|
||||||
type: 'Add commands',
|
type: 'Add commands',
|
||||||
data: { commands: commands },
|
data: { commands: commands },
|
||||||
})
|
})
|
||||||
|
|
||||||
const removeCommands = () =>
|
const removeCommands = () =>
|
||||||
commandBarActor.send({
|
commandBarActor?.send({
|
||||||
type: 'Remove commands',
|
type: 'Remove commands',
|
||||||
data: { commands: commands },
|
data: { commands: commands },
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { DEFAULT_PROJECT_NAME } from '@src/lib/constants'
|
import { DEFAULT_PROJECT_NAME } from '@src/lib/constants'
|
||||||
import type { Project } from '@src/lib/project'
|
import type { Project } from '@src/lib/project'
|
||||||
import type { SystemIOContext } from '@src/machines/systemIO/utils'
|
import type {
|
||||||
|
SystemIOContext,
|
||||||
|
RequestedKCLFile,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
import {
|
import {
|
||||||
NO_PROJECT_DIRECTORY,
|
NO_PROJECT_DIRECTORY,
|
||||||
SystemIOMachineActions,
|
SystemIOMachineActions,
|
||||||
@ -84,6 +87,20 @@ export const systemIOMachine = setup({
|
|||||||
requestedCode: string
|
requestedCode: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.bulkCreateKCLFiles
|
||||||
|
data: {
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject
|
||||||
|
data: {
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
requestedProjectName: string
|
||||||
|
override?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: SystemIOMachineEvents.importFileFromURL
|
type: SystemIOMachineEvents.importFileFromURL
|
||||||
data: {
|
data: {
|
||||||
@ -283,6 +300,41 @@ export const systemIOMachine = setup({
|
|||||||
return { message: '', fileName: '', projectName: '' }
|
return { message: '', fileName: '', projectName: '' }
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
[SystemIOMachineActors.bulkCreateKCLFiles]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
|
}): Promise<{
|
||||||
|
message: string
|
||||||
|
fileName: string
|
||||||
|
projectName: string
|
||||||
|
}> => {
|
||||||
|
return { message: '', fileName: '', projectName: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
requestedProjectName: string
|
||||||
|
}
|
||||||
|
}): Promise<{
|
||||||
|
message: string
|
||||||
|
fileName: string
|
||||||
|
projectName: string
|
||||||
|
}> => {
|
||||||
|
return { message: '', fileName: '', projectName: '' }
|
||||||
|
}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
initial: SystemIOMachineStates.idle,
|
initial: SystemIOMachineStates.idle,
|
||||||
@ -351,6 +403,13 @@ export const systemIOMachine = setup({
|
|||||||
[SystemIOMachineEvents.deleteKCLFile]: {
|
[SystemIOMachineEvents.deleteKCLFile]: {
|
||||||
target: SystemIOMachineStates.deletingKCLFile,
|
target: SystemIOMachineStates.deletingKCLFile,
|
||||||
},
|
},
|
||||||
|
[SystemIOMachineEvents.bulkCreateKCLFiles]: {
|
||||||
|
target: SystemIOMachineStates.bulkCreatingKCLFiles,
|
||||||
|
},
|
||||||
|
[SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject]: {
|
||||||
|
target:
|
||||||
|
SystemIOMachineStates.bulkCreatingKCLFilesAndNavigateToProject,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[SystemIOMachineStates.readingFolders]: {
|
[SystemIOMachineStates.readingFolders]: {
|
||||||
@ -555,5 +614,60 @@ export const systemIOMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[SystemIOMachineStates.bulkCreatingKCLFiles]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.bulkCreateKCLFiles,
|
||||||
|
src: SystemIOMachineActors.bulkCreateKCLFiles,
|
||||||
|
input: ({ context, event, self }) => {
|
||||||
|
assertEvent(event, SystemIOMachineEvents.bulkCreateKCLFiles)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
files: event.data.files,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[SystemIOMachineStates.bulkCreatingKCLFilesAndNavigateToProject]: {
|
||||||
|
invoke: {
|
||||||
|
id: SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject,
|
||||||
|
src: SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject,
|
||||||
|
input: ({ context, event, self }) => {
|
||||||
|
assertEvent(
|
||||||
|
event,
|
||||||
|
SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
files: event.data.files,
|
||||||
|
rootContext: self.system.get('root').getSnapshot().context,
|
||||||
|
requestedProjectName: event.data.requestedProjectName,
|
||||||
|
override: event.data.override,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: SystemIOMachineStates.readingFolders,
|
||||||
|
actions: [
|
||||||
|
assign({
|
||||||
|
requestedProjectName: ({ event }) => {
|
||||||
|
return { name: event.output.projectName }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
SystemIOMachineActions.toastSuccess,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: SystemIOMachineStates.idle,
|
||||||
|
actions: [SystemIOMachineActions.toastError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -14,7 +14,10 @@ import {
|
|||||||
} from '@src/lib/desktopFS'
|
} from '@src/lib/desktopFS'
|
||||||
import type { Project } from '@src/lib/project'
|
import type { Project } from '@src/lib/project'
|
||||||
import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
import type { SystemIOContext } from '@src/machines/systemIO/utils'
|
import type {
|
||||||
|
RequestedKCLFile,
|
||||||
|
SystemIOContext,
|
||||||
|
} from '@src/machines/systemIO/utils'
|
||||||
import {
|
import {
|
||||||
NO_PROJECT_DIRECTORY,
|
NO_PROJECT_DIRECTORY,
|
||||||
SystemIOMachineActors,
|
SystemIOMachineActors,
|
||||||
@ -22,6 +25,74 @@ import {
|
|||||||
import { fromPromise } from 'xstate'
|
import { fromPromise } from 'xstate'
|
||||||
import type { AppMachineContext } from '@src/lib/types'
|
import type { AppMachineContext } from '@src/lib/types'
|
||||||
|
|
||||||
|
const sharedBulkCreateWorkflow = async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
override?: boolean
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const configuration = await readAppSettingsFile()
|
||||||
|
for (let fileIndex = 0; fileIndex < input.files.length; fileIndex++) {
|
||||||
|
const file = input.files[fileIndex]
|
||||||
|
const requestedProjectName = file.requestedProjectName
|
||||||
|
const requestedFileName = file.requestedFileName
|
||||||
|
const requestedCode = file.requestedCode
|
||||||
|
const folders = input.context.folders
|
||||||
|
|
||||||
|
let newProjectName = requestedProjectName
|
||||||
|
|
||||||
|
if (!newProjectName) {
|
||||||
|
newProjectName = getUniqueProjectName(
|
||||||
|
input.context.defaultProjectFolderName,
|
||||||
|
input.context.folders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsInterpolated = doesProjectNameNeedInterpolated(newProjectName)
|
||||||
|
if (needsInterpolated) {
|
||||||
|
const nextIndex = getNextProjectIndex(newProjectName, folders)
|
||||||
|
newProjectName = interpolateProjectNameWithIndex(
|
||||||
|
newProjectName,
|
||||||
|
nextIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseDir = window.electron.join(
|
||||||
|
input.context.projectDirectoryPath,
|
||||||
|
newProjectName
|
||||||
|
)
|
||||||
|
// If override is true, use the requested filename directly
|
||||||
|
const fileName = input.override
|
||||||
|
? requestedFileName
|
||||||
|
: getNextFileName({
|
||||||
|
entryName: requestedFileName,
|
||||||
|
baseDir,
|
||||||
|
}).name
|
||||||
|
|
||||||
|
// Create the project around the file if newProject
|
||||||
|
await createNewProjectDirectory(
|
||||||
|
newProjectName,
|
||||||
|
requestedCode,
|
||||||
|
configuration,
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const numberOfFiles = input.files.length
|
||||||
|
const fileText = numberOfFiles > 1 ? 'files' : 'file'
|
||||||
|
const message = input.override
|
||||||
|
? `Successfully overwrote ${numberOfFiles} ${fileText}`
|
||||||
|
: `Successfully created ${numberOfFiles} ${fileText}`
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
fileName: '',
|
||||||
|
projectName: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const systemIOMachineDesktop = systemIOMachine.provide({
|
export const systemIOMachineDesktop = systemIOMachine.provide({
|
||||||
actors: {
|
actors: {
|
||||||
[SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise(
|
[SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise(
|
||||||
@ -255,5 +326,40 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
[SystemIOMachineActors.bulkCreateKCLFiles]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
return await sharedBulkCreateWorkflow({ input })
|
||||||
|
}
|
||||||
|
),
|
||||||
|
[SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject]: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: {
|
||||||
|
context: SystemIOContext
|
||||||
|
files: RequestedKCLFile[]
|
||||||
|
rootContext: AppMachineContext
|
||||||
|
requestedProjectName: string
|
||||||
|
override?: boolean
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
const message = await sharedBulkCreateWorkflow({
|
||||||
|
input: {
|
||||||
|
...input,
|
||||||
|
override: input.override,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
message.projectName = input.requestedProjectName
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import type { Project } from '@src/lib/project'
|
import type { Project } from '@src/lib/project'
|
||||||
|
import type { ActorRefFrom } from 'xstate'
|
||||||
|
import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine'
|
||||||
|
|
||||||
export enum SystemIOMachineActors {
|
export enum SystemIOMachineActors {
|
||||||
readFoldersFromProjectDirectory = 'read folders from project directory',
|
readFoldersFromProjectDirectory = 'read folders from project directory',
|
||||||
@ -11,6 +13,8 @@ export enum SystemIOMachineActors {
|
|||||||
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
||||||
importFileFromURL = 'import file from URL',
|
importFileFromURL = 'import file from URL',
|
||||||
deleteKCLFile = 'delete kcl delete',
|
deleteKCLFile = 'delete kcl delete',
|
||||||
|
bulkCreateKCLFiles = 'bulk create kcl files',
|
||||||
|
bulkCreateKCLFilesAndNavigateToProject = 'bulk create kcl files and navigate to project',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SystemIOMachineStates {
|
export enum SystemIOMachineStates {
|
||||||
@ -25,6 +29,8 @@ export enum SystemIOMachineStates {
|
|||||||
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
||||||
importFileFromURL = 'importFileFromURL',
|
importFileFromURL = 'importFileFromURL',
|
||||||
deletingKCLFile = 'deletingKCLFile',
|
deletingKCLFile = 'deletingKCLFile',
|
||||||
|
bulkCreatingKCLFiles = 'bulkCreatingKCLFiles',
|
||||||
|
bulkCreatingKCLFilesAndNavigateToProject = 'bulkCreatingKCLFilesAndNavigateToProject',
|
||||||
}
|
}
|
||||||
|
|
||||||
const donePrefix = 'xstate.done.actor.'
|
const donePrefix = 'xstate.done.actor.'
|
||||||
@ -48,6 +54,8 @@ export enum SystemIOMachineEvents {
|
|||||||
done_importFileFromURL = donePrefix + 'import file from URL',
|
done_importFileFromURL = donePrefix + 'import file from URL',
|
||||||
generateTextToCAD = 'generate text to CAD',
|
generateTextToCAD = 'generate text to CAD',
|
||||||
deleteKCLFile = 'delete kcl file',
|
deleteKCLFile = 'delete kcl file',
|
||||||
|
bulkCreateKCLFiles = 'bulk create kcl files',
|
||||||
|
bulkCreateKCLFilesAndNavigateToProject = 'bulk create kcl files and navigate to project',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SystemIOMachineActions {
|
export enum SystemIOMachineActions {
|
||||||
@ -89,3 +97,28 @@ export type SystemIOContext = {
|
|||||||
project: string
|
project: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RequestedKCLFile = {
|
||||||
|
requestedProjectName: string
|
||||||
|
requestedFileName: string
|
||||||
|
requestedCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const waitForIdleState = async ({
|
||||||
|
systemIOActor,
|
||||||
|
}: { systemIOActor: ActorRefFrom<typeof systemIOMachine> }) => {
|
||||||
|
// Check if already idle before setting up subscription
|
||||||
|
if (systemIOActor.getSnapshot().matches(SystemIOMachineStates.idle)) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForIdlePromise = new Promise((resolve) => {
|
||||||
|
const subscription = systemIOActor.subscribe((state) => {
|
||||||
|
if (state.matches(SystemIOMachineStates.idle)) {
|
||||||
|
subscription.unsubscribe()
|
||||||
|
resolve(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return waitForIdlePromise
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user