From 2bda3d4fba8ebee20974fa0ca15bd1005a6c25ae Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:00:03 +0800 Subject: [PATCH 1/3] feat(dashboard): show a generated name as the create-box placeholder --- .../src/components/Box/CreateBoxDialog.tsx | 7 +- apps/dashboard/src/lib/box-name.test.ts | 18 ++ apps/dashboard/src/lib/box-name.ts | 228 ++++++++++++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 apps/dashboard/src/lib/box-name.test.ts create mode 100644 apps/dashboard/src/lib/box-name.ts diff --git a/apps/dashboard/src/components/Box/CreateBoxDialog.tsx b/apps/dashboard/src/components/Box/CreateBoxDialog.tsx index 17c900b69..30817ceb5 100644 --- a/apps/dashboard/src/components/Box/CreateBoxDialog.tsx +++ b/apps/dashboard/src/components/Box/CreateBoxDialog.tsx @@ -9,6 +9,7 @@ import { RoutePath } from '@/enums/RoutePath' import { useCreateBoxMutation } from '@/hooks/mutations/useCreateBoxMutation' import { useSelectedOrganization } from '@/hooks/useSelectedOrganization' import { getBoxRouteId } from '@/lib/box-identity' +import { generateBoxName } from '@/lib/box-name' import { handleApiError } from '@/lib/error-handling' import { cn } from '@/lib/utils' import type { Box } from '@boxlite-ai/api-client' @@ -113,6 +114,9 @@ export const CreateBoxDialog = ({ const defaultImage = SUPPORTED_BOX_IMAGES.find((i) => i.isDefault) ?? SUPPORTED_BOX_IMAGES[0] const [name, setName] = useState('') + // Freshly generated on each open; shown as the placeholder so the empty-name + // default reads like the real thing ("cozy-otter") rather than a static hint. + const [suggestedName, setSuggestedName] = useState(generateBoxName) const [imageRef, setImageRef] = useState(defaultImage.ref) const [cpu, setCpu] = useState(DEFAULTS.cpu) const [memory, setMemory] = useState(DEFAULTS.memory) @@ -123,6 +127,7 @@ export const CreateBoxDialog = ({ useEffect(() => { if (open) { setName('') + setSuggestedName(generateBoxName()) setImageRef(defaultImage.ref) setCpu(DEFAULTS.cpu) setMemory(DEFAULTS.memory) @@ -199,7 +204,7 @@ export const CreateBoxDialog = ({ setName(e.target.value)} - placeholder="my-new-box" + placeholder={suggestedName} aria-invalid={!nameValid} className="w-full border border-border bg-card px-[13px] py-[11px] font-mono text-[13px] text-foreground outline-none focus:border-brand aria-[invalid=true]:border-destructive" /> diff --git a/apps/dashboard/src/lib/box-name.test.ts b/apps/dashboard/src/lib/box-name.test.ts new file mode 100644 index 000000000..dcc38d46a --- /dev/null +++ b/apps/dashboard/src/lib/box-name.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' + +import { generateBoxName } from './box-name' + +describe('generateBoxName', () => { + it('produces a lowercase "adjective-animal" DNS label', () => { + for (let i = 0; i < 200; i++) { + const name = generateBoxName() + expect(name).toMatch(/^[a-z]+-[a-z]+$/) + } + }) + + it('varies across calls', () => { + const seen = new Set() + for (let i = 0; i < 50; i++) seen.add(generateBoxName()) + expect(seen.size).toBeGreaterThan(1) + }) +}) diff --git a/apps/dashboard/src/lib/box-name.ts b/apps/dashboard/src/lib/box-name.ts new file mode 100644 index 000000000..283133162 --- /dev/null +++ b/apps/dashboard/src/lib/box-name.ts @@ -0,0 +1,228 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +// Client-side twin of the server's default box-name generator, used only to fill +// the Create-Box name placeholder with a realistic sample ("cozy-otter") instead +// of a static hint. It is cosmetic: leaving the name blank still submits no name, +// so the server remains the source of truth for the actual name (and its per-org +// collision fallback). The word lists are duplicated verbatim from +// apps/api/src/box/utils/box-name-generator.ts — the dashboard consumes no +// name-suggestion endpoint, so keep the two lists in sync by hand. + +const ADJECTIVES = [ + 'agile', + 'amber', + 'astute', + 'bold', + 'bouncy', + 'brave', + 'breezy', + 'brilliant', + 'brisk', + 'calm', + 'cheery', + 'clever', + 'cosmic', + 'cozy', + 'crimson', + 'crisp', + 'dapper', + 'daring', + 'darting', + 'deft', + 'dreamy', + 'dusky', + 'eager', + 'fierce', + 'fluffy', + 'fresh', + 'gallant', + 'gentle', + 'gliding', + 'glossy', + 'golden', + 'hearty', + 'hovering', + 'hushed', + 'indigo', + 'intrepid', + 'ivory', + 'jade', + 'jaunty', + 'jolly', + 'jovial', + 'keen', + 'lively', + 'lofty', + 'lucid', + 'lunar', + 'mellow', + 'mighty', + 'mild', + 'mystic', + 'neat', + 'nebular', + 'nimble', + 'opal', + 'perky', + 'placid', + 'plucky', + 'polished', + 'prismatic', + 'quick', + 'radiant', + 'savvy', + 'serene', + 'sharp', + 'silver', + 'sleek', + 'snappy', + 'snug', + 'soaring', + 'solar', + 'sparkly', + 'starry', + 'swift', + 'tidy', + 'tranquil', + 'valiant', + 'vibrant', + 'vivid', + 'witty', + 'zippy', +] as const + +const ANIMALS = [ + 'alpaca', + 'badger', + 'bat', + 'bear', + 'beaver', + 'bee', + 'beluga', + 'bluebird', + 'bluejay', + 'bobcat', + 'bumblebee', + 'bunny', + 'butterfly', + 'calf', + 'canary', + 'capybara', + 'cardinal', + 'cat', + 'chameleon', + 'chick', + 'chickadee', + 'chinchilla', + 'chipmunk', + 'cockatiel', + 'corgi', + 'cottontail', + 'crab', + 'cub', + 'deer', + 'dolphin', + 'dove', + 'dragonfly', + 'duck', + 'duckling', + 'eagle', + 'falcon', + 'fawn', + 'ferret', + 'finch', + 'firefly', + 'flamingo', + 'fox', + 'frog', + 'gecko', + 'goldfinch', + 'goose', + 'gosling', + 'guppy', + 'hamster', + 'hare', + 'hawk', + 'hedgehog', + 'heron', + 'hummingbird', + 'joey', + 'kangaroo', + 'kit', + 'kitten', + 'koala', + 'ladybug', + 'lamb', + 'lemur', + 'llama', + 'lovebird', + 'lynx', + 'magpie', + 'manatee', + 'marmot', + 'meerkat', + 'mole', + 'mouse', + 'narwhal', + 'nightingale', + 'ocelot', + 'octopus', + 'otter', + 'owl', + 'owlet', + 'panda', + 'parakeet', + 'parrot', + 'peacock', + 'pelican', + 'penguin', + 'piglet', + 'platypus', + 'pony', + 'porpoise', + 'possum', + 'puffin', + 'pup', + 'puppy', + 'quokka', + 'rabbit', + 'raccoon', + 'raven', + 'robin', + 'salamander', + 'seahorse', + 'seal', + 'sheep', + 'sloth', + 'snail', + 'sparrow', + 'squirrel', + 'starfish', + 'stingray', + 'swallow', + 'swan', + 'tadpole', + 'tortoise', + 'toucan', + 'turtle', + 'wallaby', + 'walrus', + 'whale', + 'wolf', + 'wombat', + 'woodpecker', + 'wren', +] as const + +function pick(arr: readonly T[]): T { + return arr[Math.floor(Math.random() * arr.length)] +} + +/** Generate a sample default box name like `cozy-otter` (lowercase, hyphenated). */ +export function generateBoxName(): string { + return `${pick(ADJECTIVES)}-${pick(ANIMALS)}` +} From 3623d847dda334632a889851b1ec8102cc3ffaef Mon Sep 17 00:00:00 2001 From: Brian Luo <57960778+law-chain-hot@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:59:27 +0800 Subject: [PATCH 2/3] feat(dashboard): deal a shared-lib generated name into create dialog --- apps/api/src/box/utils/box-name-generator.ts | 50 +--- .../src/components/Box/CreateBoxDialog.tsx | 63 +++-- .../src/components/Box/useGeneratedBoxName.ts | 57 +++++ apps/dashboard/src/lib/box-name.test.ts | 18 -- apps/dashboard/src/lib/box-name.ts | 228 ------------------ apps/dashboard/src/lib/scramble.test.ts | 21 ++ apps/dashboard/src/lib/scramble.ts | 20 ++ apps/dashboard/tsconfig.app.json | 3 +- apps/libs/box-name/package.json | 8 + apps/libs/box-name/project.json | 7 + apps/libs/box-name/src/index.ts | 49 ++++ apps/tsconfig.base.json | 1 + 12 files changed, 220 insertions(+), 305 deletions(-) create mode 100644 apps/dashboard/src/components/Box/useGeneratedBoxName.ts delete mode 100644 apps/dashboard/src/lib/box-name.test.ts delete mode 100644 apps/dashboard/src/lib/box-name.ts create mode 100644 apps/dashboard/src/lib/scramble.test.ts create mode 100644 apps/dashboard/src/lib/scramble.ts create mode 100644 apps/libs/box-name/package.json create mode 100644 apps/libs/box-name/project.json create mode 100644 apps/libs/box-name/src/index.ts diff --git a/apps/api/src/box/utils/box-name-generator.ts b/apps/api/src/box/utils/box-name-generator.ts index a66c8624c..b17220f1b 100644 --- a/apps/api/src/box/utils/box-name-generator.ts +++ b/apps/api/src/box/utils/box-name-generator.ts @@ -1,48 +1,14 @@ -// Default box-name generator. Shape: "{adjective}-{animal}", e.g. "cozy-otter". -// -// Names are kept clean in the common case. On the rare collision with the -// per-org @Unique(['organizationId', 'name']) constraint, the caller falls back -// to "{adjective}-{animal}-{boxId}" (see persistWithGeneratedBoxName) — the box +// Default box-name generation. The word lists and the "{adjective}-{animal}" +// generator live in the shared @boxlite-ai/box-name lib (also used by the +// dashboard's create dialog); this module adds the persistence-side collision +// handling. On the rare collision with the per-org +// @Unique(['organizationId', 'name']) constraint, the caller falls back to +// "{adjective}-{animal}-{boxId}" (see persistWithGeneratedBoxName) — the box // id is unique by construction, so that single fallback can never collide. -const ADJECTIVES = [ - 'agile', 'amber', 'astute', 'bold', 'bouncy', 'brave', 'breezy', 'brilliant', 'brisk', 'calm', - 'cheery', 'clever', 'cosmic', 'cozy', 'crimson', 'crisp', 'dapper', 'daring', 'darting', 'deft', - 'dreamy', 'dusky', 'eager', 'fierce', 'fluffy', 'fresh', 'gallant', 'gentle', 'gliding', 'glossy', - 'golden', 'hearty', 'hovering', 'hushed', 'indigo', 'intrepid', 'ivory', 'jade', 'jaunty', 'jolly', - 'jovial', 'keen', 'lively', 'lofty', 'lucid', 'lunar', 'mellow', 'mighty', 'mild', 'mystic', - 'neat', 'nebular', 'nimble', 'opal', 'perky', 'placid', 'plucky', 'polished', 'prismatic', 'quick', - 'radiant', 'savvy', 'serene', 'sharp', 'silver', 'sleek', 'snappy', 'snug', 'soaring', 'solar', - 'sparkly', 'starry', 'swift', 'tidy', 'tranquil', 'valiant', 'vibrant', 'vivid', 'witty', 'zippy', -] as const +import { generateBoxName } from '@boxlite-ai/box-name' -const ANIMALS = [ - 'alpaca', 'badger', 'bat', 'bear', 'beaver', 'bee', 'beluga', 'bluebird', 'bluejay', 'bobcat', - 'bumblebee', 'bunny', 'butterfly', 'calf', 'canary', 'capybara', 'cardinal', 'cat', 'chameleon', 'chick', - 'chickadee', 'chinchilla', 'chipmunk', 'cockatiel', 'corgi', 'cottontail', 'crab', 'cub', 'deer', 'dolphin', - 'dove', 'dragonfly', 'duck', 'duckling', 'eagle', 'falcon', 'fawn', 'ferret', 'finch', 'firefly', - 'flamingo', 'fox', 'frog', 'gecko', 'goldfinch', 'goose', 'gosling', 'guppy', 'hamster', 'hare', - 'hawk', 'hedgehog', 'heron', 'hummingbird', 'joey', 'kangaroo', 'kit', 'kitten', 'koala', 'ladybug', - 'lamb', 'lemur', 'llama', 'lovebird', 'lynx', 'magpie', 'manatee', 'marmot', 'meerkat', 'mole', - 'mouse', 'narwhal', 'nightingale', 'ocelot', 'octopus', 'otter', 'owl', 'owlet', 'panda', 'parakeet', - 'parrot', 'peacock', 'pelican', 'penguin', 'piglet', 'platypus', 'pony', 'porpoise', 'possum', 'puffin', - 'pup', 'puppy', 'quokka', 'rabbit', 'raccoon', 'raven', 'robin', 'salamander', 'seahorse', 'seal', - 'sheep', 'sloth', 'snail', 'sparrow', 'squirrel', 'starfish', 'stingray', 'swallow', 'swan', 'tadpole', - 'tortoise', 'toucan', 'turtle', 'wallaby', 'walrus', 'whale', 'wolf', 'wombat', 'woodpecker', 'wren', -] as const - -function pick(arr: readonly T[]): T { - return arr[Math.floor(Math.random() * arr.length)] -} - -/** - * Generate a default box name like `cozy-otter`. Hyphen- (not underscore-) - * separated and all-lowercase, so the clean name is a valid DNS label in case - * box names ever become hostnames. - */ -export function generateBoxName(): string { - return `${pick(ADJECTIVES)}-${pick(ANIMALS)}` -} +export { generateBoxName } // Postgres unique_violation — raised when a generated name hits the per-org // @Unique(['organizationId', 'name']) constraint. diff --git a/apps/dashboard/src/components/Box/CreateBoxDialog.tsx b/apps/dashboard/src/components/Box/CreateBoxDialog.tsx index 30817ceb5..94df49d6c 100644 --- a/apps/dashboard/src/components/Box/CreateBoxDialog.tsx +++ b/apps/dashboard/src/components/Box/CreateBoxDialog.tsx @@ -9,12 +9,12 @@ import { RoutePath } from '@/enums/RoutePath' import { useCreateBoxMutation } from '@/hooks/mutations/useCreateBoxMutation' import { useSelectedOrganization } from '@/hooks/useSelectedOrganization' import { getBoxRouteId } from '@/lib/box-identity' -import { generateBoxName } from '@/lib/box-name' import { handleApiError } from '@/lib/error-handling' import { cn } from '@/lib/utils' import type { Box } from '@boxlite-ai/api-client' -import { ChevronDown, Plus } from '@/components/ui/icon' +import { ChevronDown, Plus, RefreshCw } from '@/components/ui/icon' import { useEffect, useState } from 'react' +import { useGeneratedBoxName } from './useGeneratedBoxName' import { generatePath, useNavigate } from 'react-router-dom' import { toast } from 'sonner' @@ -114,9 +114,6 @@ export const CreateBoxDialog = ({ const defaultImage = SUPPORTED_BOX_IMAGES.find((i) => i.isDefault) ?? SUPPORTED_BOX_IMAGES[0] const [name, setName] = useState('') - // Freshly generated on each open; shown as the placeholder so the empty-name - // default reads like the real thing ("cozy-otter") rather than a static hint. - const [suggestedName, setSuggestedName] = useState(generateBoxName) const [imageRef, setImageRef] = useState(defaultImage.ref) const [cpu, setCpu] = useState(DEFAULTS.cpu) const [memory, setMemory] = useState(DEFAULTS.memory) @@ -124,18 +121,24 @@ export const CreateBoxDialog = ({ const [advancedOpen, setAdvancedOpen] = useState(false) const [submitting, setSubmitting] = useState(false) + const { display: generatedName, isGenerating, generate, reset: resetName } = useGeneratedBoxName(setName) + + // Hand the user a fresh generated name each time the dialog opens (it lands + // in the editable field via the reveal animation); clear on close. useEffect(() => { if (open) { setName('') - setSuggestedName(generateBoxName()) setImageRef(defaultImage.ref) setCpu(DEFAULTS.cpu) setMemory(DEFAULTS.memory) setDisk(DEFAULTS.disk) setAdvancedOpen(false) setSubmitting(false) + generate() + } else { + resetName() } - }, [open, defaultImage.ref]) + }, [open, defaultImage.ref, generate, resetName]) const selectedImage = SUPPORTED_BOX_IMAGES.find((i) => i.ref === imageRef) ?? defaultImage const nameValid = !name || NAME_REGEX.test(name) @@ -165,7 +168,14 @@ export const CreateBoxDialog = ({ navigate(generatePath(RoutePath.BOX_DETAILS, { boxId })) } } catch (error) { - handleApiError(error, 'Failed to create box') + // Name already taken in this org (client-side generation can collide) — + // deal a fresh name into the field so the next click succeeds. + if ((error as { response?: { status?: number } })?.response?.status === 409) { + toast.error('That name is already taken — here is a fresh one.') + generate() + } else { + handleApiError(error, 'Failed to create box') + } } finally { setSubmitting(false) } @@ -201,13 +211,34 @@ export const CreateBoxDialog = ({ {/* name */}
Name
- setName(e.target.value)} - placeholder={suggestedName} - aria-invalid={!nameValid} - className="w-full border border-border bg-card px-[13px] py-[11px] font-mono text-[13px] text-foreground outline-none focus:border-brand aria-[invalid=true]:border-destructive" - /> +
+ setName(e.target.value)} + readOnly={isGenerating} + placeholder="my-new-box" + aria-invalid={!nameValid} + className={cn( + 'min-w-0 flex-1 bg-transparent px-[13px] py-[11px] font-mono text-[13px] text-foreground outline-none', + isGenerating && 'text-brand', + )} + /> + +
{/* image */} @@ -295,7 +326,7 @@ export const CreateBoxDialog = ({