Text-to-CAD with alpha model (might have issues) integration (#3299)
* Add close dismiss button to Infinite duration non-loading toasts * Add text-to-cad icon candidates * Add a way to silently create files * Add text-to-cad command with mock backend * add the actual endpoint Signed-off-by: Jess Frazelle <github@jessfraz.com> * fix the response Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixups Signed-off-by: Jess Frazelle <github@jessfraz.com> * Add `credentials: include` * add headers Signed-off-by: Jess Frazelle <github@jessfraz.com> * Mostly working? Just getting CORS on desktop * Merge goof * fixups Signed-off-by: Jess Frazelle <github@jessfraz.com> * create cross platform fetch; Signed-off-by: Jess Frazelle <github@jessfraz.com> * send the token; Signed-off-by: Jess Frazelle <github@jessfraz.com> * send the token; Signed-off-by: Jess Frazelle <github@jessfraz.com> * better names for files Signed-off-by: Jess Frazelle <github@jessfraz.com> * Commit broken THREEjs success toast * base64 decode Signed-off-by: Jess Frazelle <github@jessfraz.com> * send telemetry on reject / accept Signed-off-by: Jess Frazelle <github@jessfraz.com> * start of tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * more tests Signed-off-by: Jess Frazelle <github@jessfraz.com> * basic tests; Signed-off-by: Jess Frazelle <github@jessfraz.com> * lego 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> * fmt Signed-off-by: Jess Frazelle <github@jessfraz.com> * fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * Get model stylized based on settings * Don't need automatic dismiss button for Infinity-duration toasts anymore * Stylize loaded model, add OrbitControls, polish button behavior * Allow user to retry prompt if one fails * Add an auto-grow textarea input type to the command bar, set text-to-cad to use it * Delete the created file in desktop if user rejects it * Submit with meta+Enter * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * add more tests and various fixes Signed-off-by: Jess Frazelle <github@jessfraz.com> * Set `prompt` arg defaultValue to failed prompt value on retry * Add missing `awaits` to playwright tests to get them passing * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * empty --------- Signed-off-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: Jess Frazelle <github@jessfraz.com> Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
238
src/lib/textToCad.ts
Normal file
238
src/lib/textToCad.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import {
|
||||
ToastTextToCadError,
|
||||
ToastTextToCadSuccess,
|
||||
} from 'components/ToastTextToCad'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
import toast from 'react-hot-toast'
|
||||
import { FILE_EXT } from './constants'
|
||||
import { ContextFrom, EventData, EventFrom } from 'xstate'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { NavigateFunction } from 'react-router-dom'
|
||||
import crossPlatformFetch from './crossPlatformFetch'
|
||||
import { isTauri } from './isTauri'
|
||||
import { Themes } from './theme'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
|
||||
export async function submitTextToCadPrompt(
|
||||
prompt: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCad_type'] | Error> {
|
||||
const body: Models['TextToCadCreateBody_type'] = { prompt }
|
||||
// Glb has a smaller footprint than gltf, should we want to render it.
|
||||
const url = VITE_KC_API_BASE_URL + '/ai/text-to-cad/glb?kcl=true'
|
||||
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
token
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getTextToCadResult(
|
||||
id: string,
|
||||
token?: string
|
||||
): Promise<Models['TextToCad_type'] | Error> {
|
||||
const url = VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id
|
||||
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
|
||||
url,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
token
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
interface TextToKclProps {
|
||||
trimmedPrompt: string
|
||||
fileMachineSend: (
|
||||
type: EventFrom<typeof fileMachine>,
|
||||
data?: EventData
|
||||
) => unknown
|
||||
navigate: NavigateFunction
|
||||
commandBarSend: (
|
||||
type: EventFrom<typeof commandBarMachine>,
|
||||
data?: EventData
|
||||
) => unknown
|
||||
context: ContextFrom<typeof fileMachine>
|
||||
token?: string
|
||||
settings: {
|
||||
theme: Themes
|
||||
highlightEdges: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitAndAwaitTextToKcl({
|
||||
trimmedPrompt,
|
||||
fileMachineSend,
|
||||
navigate,
|
||||
commandBarSend,
|
||||
context,
|
||||
token,
|
||||
settings,
|
||||
}: TextToKclProps) {
|
||||
const toastId = toast.loading('Submitting to Text-to-CAD API...')
|
||||
const showFailureToast = (message: string) => {
|
||||
toast.error(
|
||||
() =>
|
||||
ToastTextToCadError({
|
||||
message,
|
||||
commandBarSend,
|
||||
prompt: trimmedPrompt,
|
||||
}),
|
||||
{
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const textToCadQueued = await submitTextToCadPrompt(trimmedPrompt, token)
|
||||
.then((value) => {
|
||||
if (value instanceof Error) {
|
||||
return Promise.reject(value)
|
||||
}
|
||||
return value
|
||||
})
|
||||
.catch((error) => {
|
||||
showFailureToast('Failed to submit to Text-to-CAD API')
|
||||
return error
|
||||
})
|
||||
|
||||
if (textToCadQueued instanceof Error) {
|
||||
showFailureToast('Failed to submit to Text-to-CAD API')
|
||||
return
|
||||
}
|
||||
|
||||
toast.loading('Generating parametric model...', {
|
||||
id: toastId,
|
||||
})
|
||||
|
||||
// Check the status of the text-to-cad API job
|
||||
// until it is completed
|
||||
const textToCadComplete = new Promise<Models['TextToCad_type']>(
|
||||
async (resolve, reject) => {
|
||||
const value = await textToCadQueued
|
||||
if (value instanceof Error) {
|
||||
reject(value)
|
||||
}
|
||||
|
||||
const MAX_CHECK_TIMEOUT = 3 * 60_000
|
||||
const CHECK_INTERVAL = 3000
|
||||
|
||||
let timeElapsed = 0
|
||||
const interval = setInterval(async () => {
|
||||
timeElapsed += CHECK_INTERVAL
|
||||
if (timeElapsed >= MAX_CHECK_TIMEOUT) {
|
||||
clearInterval(interval)
|
||||
reject(new Error('Text-to-CAD API timed out'))
|
||||
}
|
||||
|
||||
const check = await getTextToCadResult(value.id, token)
|
||||
if (check instanceof Error) {
|
||||
clearInterval(interval)
|
||||
reject(check)
|
||||
}
|
||||
|
||||
if (check instanceof Error || check.status === 'failed') {
|
||||
clearInterval(interval)
|
||||
reject(check)
|
||||
} else if (check.status === 'completed') {
|
||||
clearInterval(interval)
|
||||
resolve(check)
|
||||
}
|
||||
}, CHECK_INTERVAL)
|
||||
}
|
||||
)
|
||||
|
||||
const textToCadOutputCreated = await textToCadComplete
|
||||
.catch((e) => {
|
||||
showFailureToast('Failed to generate parametric model')
|
||||
return e
|
||||
})
|
||||
.then((value) => {
|
||||
if (value.code === undefined || !value.code || value.code.length === 0) {
|
||||
// We want to show the real error message to the user.
|
||||
if (value.error && value.error.length > 0) {
|
||||
const error = value.error.replace('Text-to-CAD server:', '').trim()
|
||||
showFailureToast(error)
|
||||
return Promise.reject(new Error(error))
|
||||
} else {
|
||||
showFailureToast('No KCL code returned')
|
||||
return Promise.reject(new Error('No KCL code returned'))
|
||||
}
|
||||
}
|
||||
|
||||
const TRUNCATED_PROMPT_LENGTH = 24
|
||||
const newFileName = `${value.prompt
|
||||
.slice(0, TRUNCATED_PROMPT_LENGTH)
|
||||
.replace(/\s/gi, '-')
|
||||
.replace(/\W/gi, '-')
|
||||
.toLowerCase()}`
|
||||
|
||||
if (isTauri()) {
|
||||
fileMachineSend({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: newFileName,
|
||||
makeDir: false,
|
||||
content: value.code,
|
||||
silent: true,
|
||||
makeUnique: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...value,
|
||||
fileName: newFileName + FILE_EXT,
|
||||
}
|
||||
})
|
||||
|
||||
if (textToCadOutputCreated instanceof Error) {
|
||||
showFailureToast('Failed to generate parametric model')
|
||||
return
|
||||
}
|
||||
|
||||
// Show a custom toast with the .glb model preview
|
||||
// and options to reject or accept the model
|
||||
toast.success(
|
||||
() =>
|
||||
ToastTextToCadSuccess({
|
||||
data: textToCadOutputCreated,
|
||||
token,
|
||||
navigate,
|
||||
context,
|
||||
fileMachineSend,
|
||||
settings,
|
||||
}),
|
||||
{
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
icon: null,
|
||||
}
|
||||
)
|
||||
return textToCadOutputCreated
|
||||
}
|
||||
|
||||
export async function sendTelemetry(
|
||||
id: string,
|
||||
feedback: Models['AiFeedback_type'],
|
||||
token?: string
|
||||
): Promise<void> {
|
||||
const url =
|
||||
VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + '?feedback=' + feedback
|
||||
await crossPlatformFetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
token
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user