Files
modeling-app/src/lib/promptToEdit.ts

396 lines
13 KiB
TypeScript
Raw Normal View History

Sort imports (#6101) * add package.json Signed-off-by: Jess Frazelle <github@jessfraz.com> initial run; Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> more fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> clientsidescne Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> paths Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fix styles Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> combine Signed-off-by: Jess Frazelle <github@jessfraz.com> eslint rule Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> my ocd Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> constants file Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> no more import sceneInfra Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> try fix circular import Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-01 23:54:26 -07:00
import type { SelectionRange } from '@codemirror/state'
import { EditorSelection, Transaction } from '@codemirror/state'
import type { Models } from '@kittycad/lib'
import { VITE_KC_API_BASE_URL } from '@src/env'
import { diffLines } from 'diff'
Sort imports (#6101) * add package.json Signed-off-by: Jess Frazelle <github@jessfraz.com> initial run; Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> more fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> clientsidescne Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> paths Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fix styles Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> combine Signed-off-by: Jess Frazelle <github@jessfraz.com> eslint rule Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> my ocd Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> constants file Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> no more import sceneInfra Signed-off-by: Jess Frazelle <github@jessfraz.com> updates Signed-off-by: Jess Frazelle <github@jessfraz.com> try fix circular import Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-04-01 23:54:26 -07:00
import toast from 'react-hot-toast'
import { ToastPromptToEditCadSuccess } from '@src/components/ToastTextToCad'
import { modelingMachineEvent } from '@src/editor/manager'
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
import { topLevelRange } from '@src/lang/util'
import type { ArtifactGraph, SourceRange } from '@src/lang/wasm'
import crossPlatformFetch from '@src/lib/crossPlatformFetch'
import type { Selections } from '@src/lib/selections'
import { codeManager, editorManager, kclManager } from '@src/lib/singletons'
import { err, reportRejection } from '@src/lib/trap'
import { uuidv4 } from '@src/lib/utils'
function sourceIndexToLineColumn(
code: string,
index: number
): { line: number; column: number } {
const codeStart = code.slice(0, index)
const lines = codeStart.split('\n')
const line = lines.length
const column = lines[lines.length - 1].length
return { line, column }
}
function convertAppRangeToApiRange(
range: SourceRange,
code: string
): Models['SourceRange_type'] {
return {
start: sourceIndexToLineColumn(code, range[0]),
end: sourceIndexToLineColumn(code, range[1]),
}
}
export async function submitPromptToEditToQueue({
prompt,
selections,
code,
token,
artifactGraph,
projectName,
}: {
prompt: string
selections: Selections | null
code: string
projectName: string
token?: string
artifactGraph: ArtifactGraph
}): Promise<Models['TextToCadIteration_type'] | Error> {
// If no selection, use whole file
if (selections === null) {
const body: Models['TextToCadIterationBody_type'] = {
original_source_code: code,
prompt,
source_ranges: [], // Empty ranges indicates whole file
project_name:
projectName !== '' && projectName !== 'browser'
? projectName
: undefined,
kcl_version: kclManager.kclVersion,
}
return submitToApi(body, token)
}
// Handle manual code selections and artifact selections differently
const ranges: Models['TextToCadIterationBody_type']['source_ranges'] =
selections.graphSelections.flatMap((selection) => {
const artifact = selection.artifact
// For artifact selections, add context
const prompts: Models['TextToCadIterationBody_type']['source_ranges'] = []
if (artifact?.type === 'cap') {
prompts.push({
prompt: `The users main selection is the end cap of a general-sweep (that is an extrusion, revolve, sweep or loft).
The source range most likely refers to "startProfileAt" simply because this is the start of the profile that was swept.
If you need to operate on this cap, for example for sketching on the face, you can use the special string ${
artifact.subType === 'end' ? 'END' : 'START'
} i.e. \`startSketchOn(someSweepVariable, ${
artifact.subType === 'end' ? 'END' : 'START'
})\`
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.`,
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
let sweep = getArtifactOfTypes(
{ key: artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (!err(sweep)) {
prompts.push({
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
})
}
}
if (artifact?.type === 'wall') {
prompts.push({
prompt: `The users main selection is the wall of a general-sweep (that is an extrusion, revolve, sweep or loft).
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, 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`,
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
let sweep = getArtifactOfTypes(
{ key: artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (!err(sweep)) {
prompts.push({
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
})
}
}
if (artifact?.type === 'sweepEdge') {
prompts.push({
prompt: `The users main selection is the edge of a general-sweep (that is an extrusion, revolve, sweep or loft).
it is an ${
artifact.subType
} edge, in order to refer to this edge you should add a tag to the segment function in this source range,
and then use the function ${
artifact.subType === 'adjacent'
? 'getAdjacentEdge'
: 'getOppositeEdge'
}
See later source ranges for more context. about the sweep`,
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
let sweep = getArtifactOfTypes(
{ key: artifact.sweepId, types: ['sweep'] },
artifactGraph
)
if (!err(sweep)) {
prompts.push({
prompt: `This is the sweep's source range from the user's main selection of the end cap.`,
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
})
}
}
if (artifact?.type === 'segment') {
if (!artifact.surfaceId) {
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`,
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
} else {
prompts.push({
prompt: `This selection is for a segment (line, xLine, angledLine etc) that has been swept (a general-sweep, either an extrusion, revolve, sweep or loft).
Because it now refers to an edge the way to refer to this edge is to add a tag to the segment, and then use that tag directly.
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`,
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
let path = getArtifactOfTypes(
{ key: artifact.pathId, types: ['path'] },
artifactGraph
)
if (!err(path) && path.sweepId) {
const sweep = getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph
)
if (!err(sweep)) {
prompts.push({
prompt: `This is the sweep's source range from the user's main selection of the edge.`,
range: convertAppRangeToApiRange(sweep.codeRef.range, code),
})
}
}
}
}
if (!artifact) {
// manually selected code is more likely to not have an artifact
// an example might be highlighting the variable name only in a variable declaration
prompts.push({
prompt: '',
range: convertAppRangeToApiRange(selection.codeRef.range, code),
})
}
return prompts
})
const body: Models['TextToCadIterationBody_type'] = {
original_source_code: code,
prompt,
source_ranges: ranges,
project_name:
projectName !== '' && projectName !== 'browser' ? projectName : undefined,
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
)
// 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(
id: string,
token?: string
): Promise<Models['TextToCadIteration_type'] | Error> {
const url = VITE_KC_API_BASE_URL + '/async/operations/' + id
const data: Models['TextToCadIteration_type'] | Error =
await crossPlatformFetch(
url,
{
method: 'GET',
},
token
)
return data
}
export async function doPromptEdit({
prompt,
selections,
code,
token,
artifactGraph,
projectName,
}: {
prompt: string
selections: Selections
code: string
token?: string
projectName: string
artifactGraph: ArtifactGraph
}): Promise<Models['TextToCadIteration_type'] | Error> {
const toastId = toast.loading('Submitting to Text-to-CAD API...')
const submitResult = await submitPromptToEditToQueue({
prompt,
selections,
code,
token,
artifactGraph,
projectName,
})
if (err(submitResult)) return submitResult
const textToCadComplete = new Promise<Models['TextToCadIteration_type']>(
(resolve, reject) => {
;(async () => {
const MAX_CHECK_TIMEOUT = 3 * 60_000
const CHECK_DELAY = 200
let timeElapsed = 0
while (timeElapsed < MAX_CHECK_TIMEOUT) {
const check = await getPromptToEditResult(submitResult.id, token)
if (check instanceof Error || check.status === 'failed') {
reject(check)
return
} else if (check.status === 'completed') {
resolve(check)
return
}
await new Promise((r) => setTimeout(r, CHECK_DELAY))
timeElapsed += CHECK_DELAY
}
reject(new Error('Text-to-CAD API timed out'))
})().catch(reportRejection)
}
)
try {
const result = await textToCadComplete
toast.dismiss(toastId)
return result
} catch (e) {
toast.dismiss(toastId)
toast.error(
'Failed to edit your KCL code, please try again with a different prompt or selection'
)
console.error('textToCadComplete', e)
}
return textToCadComplete
}
/** takes care of the whole submit prompt to endpoint flow including the accept-reject toast once the result is back */
export async function promptToEditFlow({
prompt,
selections,
code,
token,
artifactGraph,
projectName,
}: {
prompt: string
selections: Selections
code: string
token?: string
artifactGraph: ArtifactGraph
projectName: string
}) {
const result = await doPromptEdit({
prompt,
selections,
code,
token,
artifactGraph,
projectName,
})
if (err(result)) return Promise.reject(result)
const oldCode = codeManager.code
const { code: newCode } = result
codeManager.updateCodeEditor(newCode)
const diff = reBuildNewCodeWithRanges(oldCode, newCode)
const ranges: SelectionRange[] = diff.insertRanges.map((range) =>
EditorSelection.range(range[0], range[1])
)
editorManager?.editorView?.dispatch({
selection: EditorSelection.create(
ranges,
selections.graphSelections.length - 1
),
annotations: [modelingMachineEvent, Transaction.addToHistory.of(false)],
})
await kclManager.executeCode()
const toastId = uuidv4()
toast.success(
() =>
ToastPromptToEditCadSuccess({
toastId,
data: result,
token,
oldCode,
}),
{
id: toastId,
duration: Infinity,
icon: null,
}
)
}
const reBuildNewCodeWithRanges = (
oldCode: string,
newCode: string
): {
newCode: string
insertRanges: SourceRange[]
} => {
let insertRanges: SourceRange[] = []
const changes = diffLines(oldCode, newCode)
let newCodeWithRanges = ''
for (const change of changes) {
if (!change.added && !change.removed) {
// no change add it to newCodeWithRanges
newCodeWithRanges += change.value
} else if (change.added && !change.removed) {
const start = newCodeWithRanges.length
const end = start + change.value.length
Rust artifact graph (#5068) * Start porting artifact graph creation to Rust * Add most of artifact graph creation * Add handling loft command from recent PR * Refactor artifact merge code so that it errors when a new artifact type is added * Add sweep subtype * Finish implementation of build artifact graph * Fix wasm.ts to use new combined generated ts-rs file * Fix Rust lints * Fix lints * Fix up replacement code * Add artifact graph to WASM outcome * Add artifact graph to simulation test output * Add new artifact graph output snapshots * Fix wall field and reduce unreachable code * Change field order for subtype * Change subtype to be determined from the request, like the TS * Fix plane sweep_id * Condense code * Change ID types to be properly optional * Change to favor the new ID, the same as TS * Fix to make error impossible * Rename artifact type tag values to match TS * Fix name of field on Cap * Update outputs * Change to use Rust source range * Update output snapshots * Add conversion to mermaid mind map and add to snapshot tests * Add new mermaid mind map output * Add flowchart * Remove raw artifact graph from tests * Remove JSON artifact graph output * Update output file with header * Update output after adding flowchart * Fix flowchart to not have duplicate edges, one in each direction * Fix not not output duplicate edges in flowcharts * Change flowchart edge style to be more obvious when a direction is missing * Update output after deduplication of edges * Fix not not skip sketch-on-face artifacts * Add docs * Fix edge iteration order to be stable * Update output after fixing order * Port TS artifactGraph.test.ts tests to simulation tests * Add grouping segments and solid2ds with their path * Update output flowcharts since grouping paths * Remove TS artifactGraph tests * Remove unused d3 dependencies * Fix to track loft ID on paths * Add command ID to error messages * Move artifact graph test code to a separate file since it's a large file * Reduce function visibility * Remove TS artifact graph code * Fix spelling error with serde * Add TODO for edge cut consumed ID * Add comment about mermaid edge rank * Fix mermaid flowchart edge cuts to appear as children of their edges * Update output since fixing flowchart order * Fix to always build the artifact graph even when there's a KCL error * Add artifact graph to error output * Change optional ID merge to match TS * Remove redundant SourceRange definition * Remove Rust-flavored default source range function * Add helper for source range creation * Update doc comment for the website * Update docs after doc comment change * Fix to save engine responses in execution cache * Remove unused import * Fix to not call WASM function before beforeAll callback is run * Remove more unused imports
2025-01-17 14:34:36 -05:00
insertRanges.push(topLevelRange(start, end))
newCodeWithRanges += change.value
}
}
return {
newCode: newCodeWithRanges,
insertRanges,
}
}