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()
if (selectCmd === 'promptToEdit') {
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 promptEditCommand.first().scrollIntoViewIfNeeded()

View File

@ -95,7 +95,7 @@ export function Toolbar({
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 richContentClearTimeout = useRef<number | null>(null)
// On mouse enter, show rich content after a 1s delay
@ -155,9 +155,12 @@ export function Toolbar({
maybeIconConfig: ToolbarItem,
dropdownId?: string
): ToolbarItemResolved {
const isConfiguredAvailable = ['available', 'experimental'].includes(
maybeIconConfig.status
)
const isDisabled =
disableAllButtons ||
maybeIconConfig.status !== 'available' ||
!isConfiguredAvailable ||
maybeIconConfig.disabled?.(state) === true
return {
@ -248,7 +251,9 @@ export function Toolbar({
onClick: () => itemConfig.onClick(configCallbackProps),
disabled:
disableAllButtons ||
itemConfig.status !== 'available' ||
!['available', 'experimental'].includes(
itemConfig.status
) ||
itemConfig.disabled === true,
status: itemConfig.status,
}))}
@ -276,7 +281,9 @@ export function Toolbar({
aria-pressed={selectedIcon.isActive}
disabled={
disableAllButtons ||
selectedIcon.status !== 'available' ||
!['available', 'experimental'].includes(
selectedIcon.status
) ||
selectedIcon.disabled
}
name={selectedIcon.title}
@ -347,7 +354,7 @@ export function Toolbar({
aria-pressed={itemConfig.isActive}
disabled={
disableAllButtons ||
itemConfig.status !== 'available' ||
!['available', 'experimental'].includes(itemConfig.status) ||
itemConfig.disabled
}
onClick={() => itemConfig.onClick(configCallbackProps)}
@ -409,7 +416,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
},
{
enabled:
itemConfig.status === 'available' &&
['available', 'experimental'].includes(itemConfig.status) &&
!!itemConfig.hotkey &&
!itemConfig.disabled &&
!itemConfig.disableHotkey,
@ -444,18 +451,28 @@ const ToolbarItemTooltipShortContent = ({
title: string
hotkey?: string | string[]
}) => (
<span
className={`text-sm ${
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
<div
className={`text-sm flex flex-col ${
!['available', 'experimental'].includes(status)
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
}`}
>
{status === 'experimental' && (
<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>
)}
<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>
)}
</span>
</div>
</div>
)
const ToolbarItemTooltipRichContent = ({
@ -463,9 +480,18 @@ const ToolbarItemTooltipRichContent = ({
}: {
itemConfig: ToolbarItemResolved
}) => {
const shouldBeEnabled = ['available', 'experimental'].includes(
itemConfig.status
)
const { state } = useModelingContext()
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">
{itemConfig.icon && (
<CustomIcon
@ -474,16 +500,14 @@ const ToolbarItemTooltipRichContent = ({
name={itemConfig.icon}
/>
)}
<span
className={`text-sm flex-1 ${
itemConfig.status !== 'available'
? 'text-chalkboard-70 dark:text-chalkboard-40'
: ''
<div
className={`text-sm flex-1 flex flex-col gap-1 ${
!shouldBeEnabled ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
}`}
>
{itemConfig.title}
</span>
{itemConfig.status === 'available' && itemConfig.hotkey ? (
</div>
{shouldBeEnabled && itemConfig.hotkey ? (
<kbd className="flex-none hotkey">
{displayHotkeys(itemConfig.hotkey)}
</kbd>
@ -511,12 +535,12 @@ const ToolbarItemTooltipRichContent = ({
)
)}
</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 */}
{itemConfig.disabled && itemConfig.disabledReason && (
<>
<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'
? itemConfig.disabledReason(state)
: itemConfig.disabledReason}

View File

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

View File

@ -36,8 +36,13 @@ function CommandBarTextareaInput({
}
return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<label className="flex items-start rounded mx-4 my-4 border border-chalkboard-100 dark:border-chalkboard-80">
<form
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
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"
@ -86,6 +91,11 @@ function CommandBarTextareaInput({
autoFocus
/>
</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>
)
}

View File

@ -102,6 +102,12 @@ function CommandComboBox({
</p>
)}
</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.Options>

View File

@ -106,6 +106,14 @@ const CustomIconMap = {
/>
</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: (
<svg
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 { kclSamplesManifestWithNoMultipleFiles } from '@src/lib/kclSamples'
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 { reportRejection } from '@src/lib/trap'
import { relevantFileExtensions } from '@src/lang/wasmUtils'
@ -17,10 +21,11 @@ export function createApplicationCommands({
}) {
const textToCADCommand: Command = {
name: 'Text-to-CAD',
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
displayName: `Text to CAD`,
description: 'Generate parts from text prompts.',
displayName: 'Text to CAD',
groupId: 'application',
needsReview: false,
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'active',
icon: 'sparkles',
onSubmit: (record) => {
if (record) {
@ -81,6 +86,7 @@ export function createApplicationCommands({
prompt: {
inputType: 'text',
required: true,
warningMessage: ML_EXPERIMENTAL_MESSAGE,
},
},
}

View File

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

View File

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

View File

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

View File

@ -49,7 +49,9 @@ import {
needsToOnboard,
onDismissOnboardingInvite,
} from '@src/routes/Onboarding/utils'
import { CustomIcon } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { ML_EXPERIMENTAL_MESSAGE } from '@src/lib/constants'
type ReadWriteProjectState = {
value: boolean
@ -299,6 +301,15 @@ const Home = () => {
data-testid="home-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>
</li>
<li className="contents">