Skip to content

Commit b704059

Browse files
committed
feat: add device guard and default devices for samples (#994)
1 parent 2f93ecd commit b704059

129 files changed

Lines changed: 1757 additions & 1778 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

usecases/ai/edge-ai-demo-studio/frontend/package-lock.json

Lines changed: 67 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

usecases/ai/edge-ai-demo-studio/frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"knip": "NODE_OPTIONS='--require tsconfig-paths/register' knip"
1717
},
1818
"dependencies": {
19+
"@ai-sdk-tool/parser": "^4.1.19",
1920
"@ai-sdk/mcp": "^1.0.25",
2021
"@ai-sdk/openai-compatible": "^2.0.35",
2122
"@ai-sdk/react": "^3.0.118",
@@ -74,4 +75,4 @@
7475
"dompurify": "^3.3.1",
7576
"drizzle-orm": "^0.45.2"
7677
}
77-
}
78+
}

usecases/ai/edge-ai-demo-studio/frontend/scripts/generate-registries.mjs

Lines changed: 105 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -200,36 +200,33 @@ function resolveConfigurePanel(serviceDir, folder) {
200200
return null
201201
}
202202

203-
const serviceFolders = readdirSync(servicesDir)
203+
// Scan for subdirectories that contain data.ts
204+
const services = readdirSync(servicesDir)
204205
.filter((name) => {
205206
if (SKIP.has(name)) return false
206207
const dir = join(servicesDir, name)
207208
return statSync(dir).isDirectory() && existsSync(join(dir, 'data.ts'))
208209
})
209-
.sort()
210-
211-
const services = serviceFolders.map((folder) => {
212-
const dataPath = join(servicesDir, folder, 'data.ts')
213-
const serviceDir = join(servicesDir, folder)
214-
const demoPath = resolveDemoPath(serviceDir)
215-
const configurePanel = resolveConfigurePanel(serviceDir, folder)
210+
.map((folder) => {
211+
const dataPath = join(servicesDir, folder, 'data.ts')
212+
const serviceDir = join(servicesDir, folder)
213+
const demoPath = resolveDemoPath(serviceDir)
214+
const configurePanel = resolveConfigurePanel(serviceDir, folder)
216215

217-
return {
218-
folder,
219-
dataAlias: toAlias(folder),
220-
hasDemo: demoPath !== null,
221-
demoPath,
222-
demoExport: demoPath ? parseDemoExport(demoPath) : null,
223-
configurePanelExport: configurePanel
224-
? parseDemoExport(configurePanel.filePath)
225-
: null,
226-
configurePanelImportPath: configurePanel?.importPath ?? null,
227-
hasWorker: hasExport(dataPath, 'worker'),
228-
hasDocs: existsSync(join(servicesDir, folder, 'docs.ts')),
229-
}
230-
})
231-
232-
const servicesWithDemo = services.filter((s) => s.hasDemo)
216+
return {
217+
folder,
218+
dataAlias: toAlias(folder),
219+
demoExport: demoPath ? parseDemoExport(demoPath) : null,
220+
configurePanelExport: configurePanel
221+
? parseDemoExport(configurePanel.filePath)
222+
: null,
223+
configurePanelImportPath: configurePanel?.importPath ?? null,
224+
hasWorker: hasExport(dataPath, 'worker'),
225+
hasDocs: existsSync(join(servicesDir, folder, 'docs.ts')),
226+
}
227+
})
228+
// Sort for deterministic output
229+
.sort((a, b) => a.folder.localeCompare(b.folder))
233230

234231
// ─── Service code generation ──────────────────────────────────────
235232

@@ -255,27 +252,29 @@ const lines = [
255252
...HEADER,
256253
'import type { Service as ServiceType } from "@/payload-types";',
257254
'',
258-
'// Data imports (only for services that provide demos)',
259-
...servicesWithDemo.map(
255+
'// Data imports',
256+
...services.map(
260257
({ folder, dataAlias }) =>
261258
`import { service as ${dataAlias} } from "../${folder}/data";`,
262259
),
263260
'',
264261
'// Demo imports',
265-
...servicesWithDemo.map(
266-
({ folder, demoExport }) =>
267-
`import { ${demoExport} } from "../${folder}/demo";`,
268-
),
262+
...services
263+
.filter((s) => s.demoExport)
264+
.map(
265+
({ folder, demoExport }) =>
266+
`import { ${demoExport} } from "../${folder}/demo";`,
267+
),
269268
'',
270269
'import type { Service } from "../types";',
271270
'',
272271
'/** Full service map including React demo components. */',
273272
'export const serviceMap: Record<ServiceType["type"], Service> = {',
274-
...servicesWithDemo.flatMap(({ folder, dataAlias, demoExport }) => [
273+
...services.flatMap(({ folder, dataAlias, demoExport }) => [
275274
` ${key(folder)}: {`,
276275
` ...${dataAlias},`,
277276
' status: "offline",',
278-
` demo: ${demoExport},`,
277+
...(demoExport ? [` demo: ${demoExport},`] : []),
279278
' },',
280279
]),
281280
'};',
@@ -361,7 +360,6 @@ const workerLines = [
361360
writeFileSync(workerRegistryPath, workerLines.join('\n'), 'utf8')
362361

363362
// --- _generated/docs.ts (docs factory registry) ---
364-
// Docs registry should include any service that has docs.ts (metadata-only)
365363
const docsServices = services.filter((s) => s.hasDocs)
366364

367365
/** Convert a folder name to a camelCase docs alias (e.g. "speech-to-text" → "speechToTextDocs") */
@@ -525,10 +523,84 @@ const engineLines = [
525523

526524
writeFileSync(enginesOutputPath, engineLines.join('\n'), 'utf8')
527525

526+
// ═══════════════════════════════════════════════════════════════════
527+
// ─── Port allocation summary ──────────────────────────────────────
528+
// ═══════════════════════════════════════════════════════════════════
529+
530+
/** Extract `port: <number>` from a data.ts file */
531+
function extractPort(filePath) {
532+
const src = readFileSync(filePath, 'utf8')
533+
const m = src.match(/\bport:\s*(\d+)/)
534+
return m ? parseInt(m[1], 10) : null
535+
}
536+
537+
/** Extract `reservedPorts: [<numbers>]` from a data.ts file */
538+
function extractReservedPorts(filePath) {
539+
const src = readFileSync(filePath, 'utf8')
540+
const m = src.match(/\breservedPorts:\s*\[([^\]]*?)\]/)
541+
if (!m) return []
542+
return m[1]
543+
.split(',')
544+
.map((s) => s.trim())
545+
.filter(Boolean)
546+
.map(Number)
547+
.filter((n) => !isNaN(n))
548+
}
549+
550+
const portEntries = []
551+
const allPorts = new Set()
552+
553+
for (const { folder } of services) {
554+
const dataPath = join(servicesDir, folder, 'data.ts')
555+
const port = extractPort(dataPath)
556+
if (port !== null) {
557+
portEntries.push({ port, owner: folder })
558+
allPorts.add(port)
559+
}
560+
for (const rp of extractReservedPorts(dataPath)) {
561+
portEntries.push({ port: rp, owner: `${folder} (reserved)` })
562+
allPorts.add(rp)
563+
}
564+
}
565+
566+
for (const folder of sampleFolders) {
567+
const dataPath = join(samplesDir, folder, 'data.ts')
568+
for (const rp of extractReservedPorts(dataPath)) {
569+
portEntries.push({ port: rp, owner: `sample:${folder} (reserved)` })
570+
allPorts.add(rp)
571+
}
572+
}
573+
574+
portEntries.sort((a, b) => a.port - b.port)
575+
576+
// Find first duplicate
577+
const seen = new Map()
578+
let duplicateWarning = ''
579+
for (const { port, owner } of portEntries) {
580+
if (seen.has(port)) {
581+
duplicateWarning += ` ⚠️ Port ${port} used by both "${seen.get(port)}" and "${owner}"\n`
582+
}
583+
seen.set(port, owner)
584+
}
585+
586+
// Compute next available port
587+
let nextPort = 8001
588+
while (allPorts.has(nextPort)) nextPort++
589+
528590
// ─── Summary ──────────────────────────────────────────────────────
529591
console.log(
530592
`✓ Generated registries:\n` +
531593
` services: ${services.length} services, ${workerServices.length} workers, ${docsServices.length} docs → src/services/_generated/\n` +
532594
` samples: ${sampleFolders.length} samples → src/samples/_generated/\n` +
533595
` engines: ${engineFolders.length} engines → src/engines/_generated/`,
534596
)
597+
598+
console.log('\n📡 Port allocation (ascending):')
599+
for (const { port, owner } of portEntries) {
600+
console.log(` ${port} ${owner}`)
601+
}
602+
if (duplicateWarning) {
603+
console.log('\n⚠️ Duplicate port warnings:')
604+
process.stdout.write(duplicateWarning)
605+
}
606+
console.log(`\n✅ Next available port: ${nextPort}`)

usecases/ai/edge-ai-demo-studio/frontend/src/app/(dashboard)/page.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export default function DashboardPage() {
3939

4040
{loading ? (
4141
<>
42-
{/* Skeleton metric cards */}
4342
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
4443
{Array.from({ length: 4 }).map((_, i) => (
4544
<div key={i} className="glass-card rounded-xl px-5 py-4">
@@ -48,7 +47,6 @@ export default function DashboardPage() {
4847
</div>
4948
))}
5049
</div>
51-
{/* Skeleton info cards */}
5250
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
5351
{Array.from({ length: 3 }).map((_, i) => (
5452
<div
@@ -63,7 +61,6 @@ export default function DashboardPage() {
6361
</div>
6462
))}
6563
</div>
66-
{/* Skeleton service cards */}
6764
<div>
6865
<Skeleton className="mb-4 h-6 w-36" />
6966
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -150,7 +147,6 @@ export default function DashboardPage() {
150147
</div>
151148
</div>
152149

153-
{/* Featured Samples */}
154150
<div
155151
className="section-fade"
156152
style={{ '--stagger': 2 } as React.CSSProperties}

0 commit comments

Comments
 (0)