Show "experimental" badges around ML functionality (#6648)

* Make "experimental" a valid status for toolbar and commands

* Wire up status through createMachineCommand

* Add beaker icon

* Show UI elements if status is experimental

* Make ML operations experimental, powered by a flag

* Update command descriptions

* Add tooltip to home page Text-to-CAD button

* Splelnig erorrs

* 🧹lints

* Oopsie daisy Add KCL file isn't experimental

* Add warning message element to text area arg input

* Update message to common named constant
This commit is contained in:
Frank Noirot
2025-05-05 11:36:22 -04:00
committed by GitHub
parent 21da3c6482
commit 7ab879a94f
13 changed files with 119 additions and 37 deletions

View File

@ -147,7 +147,7 @@ export class CmdBarFixture {
await expect(this.page.getByPlaceholder('Search commands')).toBeVisible() await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
if (selectCmd === 'promptToEdit') { if (selectCmd === 'promptToEdit') {
const promptEditCommand = this.page.getByText( const promptEditCommand = this.page.getByText(
'Use Zoo AI to edit your kcl' 'Use Zoo AI to edit your parts and code.'
) )
await expect(promptEditCommand.first()).toBeVisible() await expect(promptEditCommand.first()).toBeVisible()
await promptEditCommand.first().scrollIntoViewIfNeeded() await promptEditCommand.first().scrollIntoViewIfNeeded()

View File

@ -95,7 +95,7 @@ export function Toolbar({
const tooltipContentClassName = !showRichContent const tooltipContentClassName = !showRichContent
? '' ? ''
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch' : '!text-left text-wrap !text-xs !p-0 !pb-2 flex !max-w-none !w-72 flex-col items-stretch'
const richContentTimeout = useRef<number | null>(null) const richContentTimeout = useRef<number | null>(null)
const richContentClearTimeout = useRef<number | null>(null) const richContentClearTimeout = useRef<number | null>(null)
// On mouse enter, show rich content after a 1s delay // On mouse enter, show rich content after a 1s delay
@ -155,9 +155,12 @@ export function Toolbar({
maybeIconConfig: ToolbarItem, maybeIconConfig: ToolbarItem,
dropdownId?: string dropdownId?: string
): ToolbarItemResolved { ): ToolbarItemResolved {
const isConfiguredAvailable = ['available', 'experimental'].includes(
maybeIconConfig.status
)
const isDisabled = const isDisabled =
disableAllButtons || disableAllButtons ||
maybeIconConfig.status !== 'available' || !isConfiguredAvailable ||
maybeIconConfig.disabled?.(state) === true maybeIconConfig.disabled?.(state) === true
return { return {
@ -248,7 +251,9 @@ export function Toolbar({
onClick: () => itemConfig.onClick(configCallbackProps), onClick: () => itemConfig.onClick(configCallbackProps),
disabled: disabled:
disableAllButtons || disableAllButtons ||
itemConfig.status !== 'available' || !['available', 'experimental'].includes(
itemConfig.status
) ||
itemConfig.disabled === true, itemConfig.disabled === true,
status: itemConfig.status, status: itemConfig.status,
}))} }))}
@ -276,7 +281,9 @@ export function Toolbar({
aria-pressed={selectedIcon.isActive} aria-pressed={selectedIcon.isActive}
disabled={ disabled={
disableAllButtons || disableAllButtons ||
selectedIcon.status !== 'available' || !['available', 'experimental'].includes(
selectedIcon.status
) ||
selectedIcon.disabled selectedIcon.disabled
} }
name={selectedIcon.title} name={selectedIcon.title}
@ -347,7 +354,7 @@ export function Toolbar({
aria-pressed={itemConfig.isActive} aria-pressed={itemConfig.isActive}
disabled={ disabled={
disableAllButtons || disableAllButtons ||
itemConfig.status !== 'available' || !['available', 'experimental'].includes(itemConfig.status) ||
itemConfig.disabled itemConfig.disabled
} }
onClick={() => itemConfig.onClick(configCallbackProps)} onClick={() => itemConfig.onClick(configCallbackProps)}
@ -409,7 +416,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
}, },
{ {
enabled: enabled:
itemConfig.status === 'available' && ['available', 'experimental'].includes(itemConfig.status) &&
!!itemConfig.hotkey && !!itemConfig.hotkey &&
!itemConfig.disabled && !itemConfig.disabled &&
!itemConfig.disableHotkey, !itemConfig.disableHotkey,
@ -444,18 +451,28 @@ const ToolbarItemTooltipShortContent = ({
title: string title: string
hotkey?: string | string[] hotkey?: string | string[]
}) => ( }) => (
<span <div
className={`text-sm ${ className={`text-sm flex flex-col ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : '' !['available', 'experimental'].includes(status)
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`} }`}
> >
{title} {status === 'experimental' && (
{hotkey && ( <div className="text-xs flex justify-center item-center gap-1 pb-1 border-b border-chalkboard-50">
<kbd className="inline-block ml-2 flex-none hotkey"> <CustomIcon name="beaker" className="w-4 h-4" />
{displayHotkeys(hotkey)} <span>Experimental</span>
</kbd> </div>
)} )}
</span> <div className={`flex gap-4 ${status === 'experimental' ? 'pt-1' : 'p-0'}`}>
{title}
{hotkey && (
<kbd className="inline-block ml-2 flex-none hotkey">
{displayHotkeys(hotkey)}
</kbd>
)}
</div>
</div>
) )
const ToolbarItemTooltipRichContent = ({ const ToolbarItemTooltipRichContent = ({
@ -463,9 +480,18 @@ const ToolbarItemTooltipRichContent = ({
}: { }: {
itemConfig: ToolbarItemResolved itemConfig: ToolbarItemResolved
}) => { }) => {
const shouldBeEnabled = ['available', 'experimental'].includes(
itemConfig.status
)
const { state } = useModelingContext() const { state } = useModelingContext()
return ( return (
<> <>
{itemConfig.status === 'experimental' && (
<div className="text-xs flex items-center justify-center self-stretch gap-1 p-1 border-b">
<CustomIcon name="beaker" className="w-4 h-4" />
<span className="block">Experimental</span>
</div>
)}
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50"> <div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
{itemConfig.icon && ( {itemConfig.icon && (
<CustomIcon <CustomIcon
@ -474,16 +500,14 @@ const ToolbarItemTooltipRichContent = ({
name={itemConfig.icon} name={itemConfig.icon}
/> />
)} )}
<span <div
className={`text-sm flex-1 ${ className={`text-sm flex-1 flex flex-col gap-1 ${
itemConfig.status !== 'available' !shouldBeEnabled ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`} }`}
> >
{itemConfig.title} {itemConfig.title}
</span> </div>
{itemConfig.status === 'available' && itemConfig.hotkey ? ( {shouldBeEnabled && itemConfig.hotkey ? (
<kbd className="flex-none hotkey"> <kbd className="flex-none hotkey">
{displayHotkeys(itemConfig.hotkey)} {displayHotkeys(itemConfig.hotkey)}
</kbd> </kbd>
@ -511,12 +535,12 @@ const ToolbarItemTooltipRichContent = ({
) )
)} )}
</div> </div>
<p className="px-2 text-ch font-sans">{itemConfig.description}</p> <p className="px-2 my-2 text-ch font-sans">{itemConfig.description}</p>
{/* Add disabled reason if item is disabled */} {/* Add disabled reason if item is disabled */}
{itemConfig.disabled && itemConfig.disabledReason && ( {itemConfig.disabled && itemConfig.disabledReason && (
<> <>
<hr className="border-chalkboard-20 dark:border-chalkboard-80" /> <hr className="border-chalkboard-20 dark:border-chalkboard-80" />
<p className="px-2 text-ch font-sans text-chalkboard-70 dark:text-chalkboard-40"> <p className="px-2 my-2 text-ch font-sans text-chalkboard-70 dark:text-chalkboard-40">
{typeof itemConfig.disabledReason === 'function' {typeof itemConfig.disabledReason === 'function'
? itemConfig.disabledReason(state) ? itemConfig.disabledReason(state)
: itemConfig.disabledReason} : itemConfig.disabledReason}

View File

@ -13,7 +13,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
shortcut?: string shortcut?: string
onClick: () => void onClick: () => void
disabled?: boolean disabled?: boolean
status?: 'available' | 'unavailable' | 'kcl-only' status?: 'available' | 'unavailable' | 'kcl-only' | 'experimental'
}[] }[]
} }
@ -101,6 +101,9 @@ export function ActionButtonDropdown({
{item.shortcut} {item.shortcut}
</kbd> </kbd>
) : null} ) : null}
{item.status === 'experimental' ? (
<CustomIcon name="beaker" className="w-4 h-4" />
) : null}
</button> </button>
</li> </li>
))} ))}

View File

@ -36,8 +36,13 @@ function CommandBarTextareaInput({
} }
return ( return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}> <form
<label className="flex items-start rounded mx-4 my-4 border border-chalkboard-100 dark:border-chalkboard-80"> id="arg-form"
className="flex flex-col items-stretch gap-2 mx-4 my-4 "
onSubmit={handleSubmit}
ref={formRef}
>
<label className="flex items-start rounded border border-chalkboard-100 dark:border-chalkboard-80">
<span <span
data-testid="cmd-bar-arg-name" data-testid="cmd-bar-arg-name"
className="capitalize px-2 py-1 rounded-br bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80" className="capitalize px-2 py-1 rounded-br bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
@ -86,6 +91,11 @@ function CommandBarTextareaInput({
autoFocus autoFocus
/> />
</label> </label>
{arg.warningMessage && (
<p className="text-warn-80 bg-warn-10 px-2 py-1 rounded-sm mt-3 mr-2 -mb-2 w-full text-sm cursor-default">
{arg.warningMessage}
</p>
)}
</form> </form>
) )
} }

View File

@ -102,6 +102,12 @@ function CommandComboBox({
</p> </p>
)} )}
</div> </div>
{option.status === 'experimental' && (
<div className="text-xs flex items-center justify-center gap-1 text-primary">
<CustomIcon name="beaker" className="w-4 h-4" />
<span>Experimental</span>
</div>
)}
</Combobox.Option> </Combobox.Option>
))} ))}
</Combobox.Options> </Combobox.Options>

View File

@ -106,6 +106,14 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
beaker: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.4747 4.1582L12.5353 6.97363L15.8322 14.3906L15.9015 14.5781C16.1808 15.5181 15.4786 16.5 14.4611 16.5H5.61828C4.50321 16.5 3.77781 15.3265 4.27649 14.3291L7.96008 6.96191L7.02551 4.1582L7.50012 3.5H13.0001L13.4747 4.1582ZM13.6183 11.873C13.2276 11.7045 12.8282 11.6908 12.38 11.7686C11.8504 11.8605 11.3185 12.0648 10.6671 12.2764C9.48553 12.6601 8.08344 12.9938 6.49133 12.1348L5.17102 14.7764C5.00479 15.1088 5.24659 15.5 5.61828 15.5H14.4611C14.8002 15.5 15.0346 15.1727 14.9415 14.8594L14.9181 14.7969L13.6183 11.873ZM8.97473 6.8418L9.04016 7.03809L6.9386 11.2402C8.17525 11.9192 9.24713 11.6862 10.3585 11.3252C10.9385 11.1368 11.5865 10.8913 12.2091 10.7832C12.5041 10.732 12.8059 10.7103 13.1124 10.7344L11.5431 7.20312L11.464 7.02539L11.5255 6.8418L12.3058 4.5H8.19445L8.97473 6.8418Z"
fill="currentColor"
/>
</svg>
),
booleanExclude: ( booleanExclude: (
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"

View File

@ -5,7 +5,11 @@ import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
import { isDesktop } from '@src/lib/isDesktop' import { isDesktop } from '@src/lib/isDesktop'
import { kclSamplesManifestWithNoMultipleFiles } from '@src/lib/kclSamples' import { kclSamplesManifestWithNoMultipleFiles } from '@src/lib/kclSamples'
import { getUniqueProjectName } from '@src/lib/desktopFS' import { getUniqueProjectName } from '@src/lib/desktopFS'
import { FILE_EXT } from '@src/lib/constants' import {
FILE_EXT,
IS_ML_EXPERIMENTAL,
ML_EXPERIMENTAL_MESSAGE,
} from '@src/lib/constants'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { reportRejection } from '@src/lib/trap' import { reportRejection } from '@src/lib/trap'
import { relevantFileExtensions } from '@src/lang/wasmUtils' import { relevantFileExtensions } from '@src/lang/wasmUtils'
@ -17,10 +21,11 @@ export function createApplicationCommands({
}) { }) {
const textToCADCommand: Command = { const textToCADCommand: Command = {
name: 'Text-to-CAD', name: 'Text-to-CAD',
description: 'Use the Zoo Text-to-CAD API to generate part starters.', description: 'Generate parts from text prompts.',
displayName: `Text to CAD`, displayName: 'Text to CAD',
groupId: 'application', groupId: 'application',
needsReview: false, needsReview: false,
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'active',
icon: 'sparkles', icon: 'sparkles',
onSubmit: (record) => { onSubmit: (record) => {
if (record) { if (record) {
@ -81,6 +86,7 @@ export function createApplicationCommands({
prompt: { prompt: {
inputType: 'text', inputType: 'text',
required: true, required: true,
warningMessage: ML_EXPERIMENTAL_MESSAGE,
}, },
}, },
} }

View File

@ -23,10 +23,12 @@ import type {
StateMachineCommandSetConfig, StateMachineCommandSetConfig,
} from '@src/lib/commandTypes' } from '@src/lib/commandTypes'
import { import {
IS_ML_EXPERIMENTAL,
KCL_DEFAULT_CONSTANT_PREFIXES, KCL_DEFAULT_CONSTANT_PREFIXES,
KCL_DEFAULT_DEGREE, KCL_DEFAULT_DEGREE,
KCL_DEFAULT_LENGTH, KCL_DEFAULT_LENGTH,
KCL_DEFAULT_TRANSFORM, KCL_DEFAULT_TRANSFORM,
ML_EXPERIMENTAL_MESSAGE,
} from '@src/lib/constants' } from '@src/lib/constants'
import type { components } from '@src/lib/machine-api' import type { components } from '@src/lib/machine-api'
import type { Selections } from '@src/lib/selections' import type { Selections } from '@src/lib/selections'
@ -964,8 +966,9 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
'Prompt-to-edit': { 'Prompt-to-edit': {
description: 'Use Zoo AI to edit your kcl', description: 'Use Zoo AI to edit your parts and code.',
icon: 'chat', icon: 'chat',
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'active',
args: { args: {
selection: { selection: {
inputType: 'selectionMixed', inputType: 'selectionMixed',
@ -989,6 +992,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
prompt: { prompt: {
inputType: 'text', inputType: 'text',
required: true, required: true,
warningMessage: ML_EXPERIMENTAL_MESSAGE,
}, },
}, },
}, },

View File

@ -40,7 +40,7 @@ export interface KclExpressionWithVariable extends KclExpression {
} }
export type KclCommandValue = KclExpression | KclExpressionWithVariable export type KclCommandValue = KclExpression | KclExpressionWithVariable
export type CommandInputType = INPUT_TYPE[number] export type CommandInputType = INPUT_TYPE[number]
type CommandStatus = 'active' | 'development' | 'inactive' | 'experimental'
export type FileFilter = { export type FileFilter = {
name: string name: string
extensions: string[] extensions: string[]
@ -103,6 +103,7 @@ export type Command<
icon?: Icon icon?: Icon
hide?: PLATFORM[number] hide?: PLATFORM[number]
hideFromSearch?: boolean hideFromSearch?: boolean
status?: CommandStatus
} }
export type CommandConfig< export type CommandConfig<
@ -115,7 +116,7 @@ export type CommandConfig<
'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview' 'name' | 'groupId' | 'onSubmit' | 'onCancel' | 'args' | 'needsReview'
> & { > & {
needsReview?: boolean needsReview?: boolean
status?: 'active' | 'development' | 'inactive' status?: CommandStatus
args?: { args?: {
[ArgName in keyof CommandSchema]: CommandArgumentConfig< [ArgName in keyof CommandSchema]: CommandArgumentConfig<
CommandSchema[ArgName], CommandSchema[ArgName],

View File

@ -190,3 +190,8 @@ export type ExecutionType =
/** Key for setting window.localStorage.setItem and .getItem to determine if the runtime is playwright for browsers */ /** Key for setting window.localStorage.setItem and .getItem to determine if the runtime is playwright for browsers */
export const IS_PLAYWRIGHT_KEY = 'playwright' export const IS_PLAYWRIGHT_KEY = 'playwright'
/** Should we mark all the ML features as "beta"? */
export const IS_ML_EXPERIMENTAL = true
export const ML_EXPERIMENTAL_MESSAGE =
'This feature is experimental and undergoing constant improvement, stay tuned for updates.'

View File

@ -123,6 +123,9 @@ export function createMachineCommand<
if ('reviewMessage' in commandConfig) { if ('reviewMessage' in commandConfig) {
command.reviewMessage = commandConfig.reviewMessage command.reviewMessage = commandConfig.reviewMessage
} }
if ('status' in commandConfig) {
command.status = commandConfig.status
}
return command return command
} }

View File

@ -10,6 +10,7 @@ import {
isEditingExistingSketch, isEditingExistingSketch,
pipeHasCircle, pipeHasCircle,
} from '@src/machines/modelingMachine' } from '@src/machines/modelingMachine'
import { IS_ML_EXPERIMENTAL } from '@src/lib/constants'
export type ToolbarModeName = 'modeling' | 'sketching' export type ToolbarModeName = 'modeling' | 'sketching'
@ -36,7 +37,7 @@ export type ToolbarItem = {
icon?: CustomIconName icon?: CustomIconName
iconColor?: string iconColor?: string
alwaysDark?: true alwaysDark?: true
status: 'available' | 'unavailable' | 'kcl-only' status: 'available' | 'unavailable' | 'kcl-only' | 'experimental'
disabled?: (state: StateFrom<typeof modelingMachine>) => boolean disabled?: (state: StateFrom<typeof modelingMachine>) => boolean
disableHotkey?: (state: StateFrom<typeof modelingMachine>) => boolean disableHotkey?: (state: StateFrom<typeof modelingMachine>) => boolean
title: string | ((props: ToolbarItemCallbackProps) => string) title: string | ((props: ToolbarItemCallbackProps) => string)
@ -440,7 +441,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sparkles', icon: 'sparkles',
iconColor: '#29FFA4', iconColor: '#29FFA4',
alwaysDark: true, alwaysDark: true,
status: 'available', status: IS_ML_EXPERIMENTAL ? 'experimental' : 'available',
title: 'Create with Zoo Text-to-CAD', title: 'Create with Zoo Text-to-CAD',
description: 'Create geometry with AI / ML.', description: 'Create geometry with AI / ML.',
links: [ links: [
@ -460,7 +461,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'sparkles', icon: 'sparkles',
iconColor: '#29FFA4', iconColor: '#29FFA4',
alwaysDark: true, alwaysDark: true,
status: 'available', status: IS_ML_EXPERIMENTAL ? 'experimental' : 'available',
title: 'Modify with Zoo Text-to-CAD', title: 'Modify with Zoo Text-to-CAD',
description: 'Edit geometry with AI / ML.', description: 'Edit geometry with AI / ML.',
links: [], links: [],

View File

@ -49,7 +49,9 @@ import {
needsToOnboard, needsToOnboard,
onDismissOnboardingInvite, onDismissOnboardingInvite,
} from '@src/routes/Onboarding/utils' } from '@src/routes/Onboarding/utils'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip' import Tooltip from '@src/components/Tooltip'
import { ML_EXPERIMENTAL_MESSAGE } from '@src/lib/constants'
type ReadWriteProjectState = { type ReadWriteProjectState = {
value: boolean value: boolean
@ -299,6 +301,15 @@ const Home = () => {
data-testid="home-text-to-cad" data-testid="home-text-to-cad"
> >
Generate with Text-to-CAD Generate with Text-to-CAD
<Tooltip position="bottom-left">
<div className="text-sm flex flex-col max-w-xs">
<div className="text-xs flex justify-center item-center gap-1 pb-1 border-b border-chalkboard-50">
<CustomIcon name="beaker" className="w-4 h-4" />
<span>Experimental</span>
</div>
<p className="pt-2 text-left">{ML_EXPERIMENTAL_MESSAGE}</p>
</div>
</Tooltip>
</ActionButton> </ActionButton>
</li> </li>
<li className="contents"> <li className="contents">