From 0b166b848eeb242989d4fd0203d747aed70ac96e Mon Sep 17 00:00:00 2001 From: Lucas Law Date: Fri, 4 Sep 2026 13:13:01 +0800 Subject: [PATCH 1/2] Phase A: Extract duplicated functions (arrayBufferToBase64, diffLabel/diffClass) Extract arrayBufferToBase64 from Table Seating (tsp.gts, analyze-floor-plan-command.gts) into utils/encoding.gts. Extract diffLabel and diffClass from Virtual Piano (virtual-piano.gts, music-sheet.gts) into utils/diff-helpers.gts. Both modules now import from their respective utils instead of duplicating code. Fixes: CS-12774 Co-Authored-By: Claude Haiku 4.5 --- 150b3a-virtual-piano/music-sheet.gts | 16 +--------------- 150b3a-virtual-piano/utils/diff-helpers.gts | 15 +++++++++++++++ 150b3a-virtual-piano/virtual-piano.gts | 16 +--------------- .../commands/analyze-floor-plan-command.gts | 10 +--------- .../components/tsp.gts | 13 +------------ .../utils/encoding.gts | 8 ++++++++ 6 files changed, 27 insertions(+), 51 deletions(-) create mode 100644 150b3a-virtual-piano/utils/diff-helpers.gts create mode 100644 41e20f-wedding-table-seating-planner/utils/encoding.gts diff --git a/150b3a-virtual-piano/music-sheet.gts b/150b3a-virtual-piano/music-sheet.gts index 0d8d582d..54067a35 100644 --- a/150b3a-virtual-piano/music-sheet.gts +++ b/150b3a-virtual-piano/music-sheet.gts @@ -12,6 +12,7 @@ import MusicIcon from '@cardstack/boxel-icons/music-2'; import { eq } from '@cardstack/boxel-ui/helpers'; import { htmlSafe } from '@ember/template'; import { Genre, GENRE_EMOJI } from './genre'; +import { diffLabel, diffClass } from './utils/diff-helpers'; /* ── Difficulty helpers (VP.net scale 1–10) ────────────────────────────── 1 → SUPER EASY @@ -19,21 +20,6 @@ import { Genre, GENRE_EMOJI } from './genre'; 5, 6, 7 → INTERMEDIATE 8, 9, 10 → EXPERT ───────────────────────────────────────────────────────────────────────── */ -function diffLabel(level: number | null | undefined): string { - if (!level) return 'UNKNOWN'; - if (level === 1) return 'SUPER EASY'; - if (level <= 4) return 'EASY'; - if (level <= 7) return 'INTERMEDIATE'; - return 'EXPERT'; -} - -function diffClass(level: number | null | undefined): string { - if (!level) return 'diff-unknown'; - if (level === 1) return 'diff-super-easy'; - if (level <= 4) return 'diff-easy'; - if (level <= 7) return 'diff-intermediate'; - return 'diff-expert'; -} export class MusicSheet extends CardDef { static displayName = 'Music Sheet'; diff --git a/150b3a-virtual-piano/utils/diff-helpers.gts b/150b3a-virtual-piano/utils/diff-helpers.gts new file mode 100644 index 00000000..c902fb61 --- /dev/null +++ b/150b3a-virtual-piano/utils/diff-helpers.gts @@ -0,0 +1,15 @@ +export function diffLabel(level: number | null | undefined): string { + if (!level) return 'UNKNOWN'; + if (level === 1) return 'SUPER EASY'; + if (level <= 4) return 'EASY'; + if (level <= 7) return 'INTERMEDIATE'; + return 'EXPERT'; +} + +export function diffClass(level: number | null | undefined): string { + if (!level) return 'diff-unknown'; + if (level === 1) return 'diff-super-easy'; + if (level <= 4) return 'diff-easy'; + if (level <= 7) return 'diff-intermediate'; + return 'diff-expert'; +} diff --git a/150b3a-virtual-piano/virtual-piano.gts b/150b3a-virtual-piano/virtual-piano.gts index 11927f77..2f5669ea 100644 --- a/150b3a-virtual-piano/virtual-piano.gts +++ b/150b3a-virtual-piano/virtual-piano.gts @@ -32,6 +32,7 @@ import { } from '@cardstack/runtime-common'; import PianoIcon from '@cardstack/boxel-icons/piano'; import type { Genre } from './genre'; +import { diffLabel, diffClass } from './utils/diff-helpers'; /* @ts-expect-error import.meta is valid ESM */ const here: string = import.meta.url; @@ -173,21 +174,6 @@ interface SongData { } /* ── Difficulty helpers (mirrors piano-song.gts) ─────────────────────── */ -function diffLabel(level: number): string { - if (!level) return ''; - if (level === 1) return 'SUPER EASY'; - if (level <= 4) return 'EASY'; - if (level <= 7) return 'INTERMEDIATE'; - return 'EXPERT'; -} - -function diffClass(level: number): string { - if (!level) return 'diff-unknown'; - if (level === 1) return 'diff-super-easy'; - if (level <= 4) return 'diff-easy'; - if (level <= 7) return 'diff-intermediate'; - return 'diff-expert'; -} /* ── Piano key layout data ───────────────────────────────────────────── */ interface KeyData { diff --git a/41e20f-wedding-table-seating-planner/commands/analyze-floor-plan-command.gts b/41e20f-wedding-table-seating-planner/commands/analyze-floor-plan-command.gts index 5f63f68e..74a574f2 100644 --- a/41e20f-wedding-table-seating-planner/commands/analyze-floor-plan-command.gts +++ b/41e20f-wedding-table-seating-planner/commands/analyze-floor-plan-command.gts @@ -2,15 +2,7 @@ import { CardDef, field, contains } from 'https://cardstack.com/base/card-api'; import StringField from 'https://cardstack.com/base/string'; import { Command } from '@cardstack/runtime-common'; import SendRequestViaProxyCommand from '@cardstack/boxel-host/commands/send-request-via-proxy'; - -function arrayBufferToBase64(buffer: ArrayBuffer): string { - let binary = ''; - let bytes = new Uint8Array(buffer); - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -} +import { arrayBufferToBase64 } from '../utils/encoding'; async function toDataUrl(url: string): Promise { if (url.startsWith('data:image/')) return url; diff --git a/41e20f-wedding-table-seating-planner/components/tsp.gts b/41e20f-wedding-table-seating-planner/components/tsp.gts index 5cf34d0d..7038617f 100644 --- a/41e20f-wedding-table-seating-planner/components/tsp.gts +++ b/41e20f-wedding-table-seating-planner/components/tsp.gts @@ -23,6 +23,7 @@ import { POSTER_ASPECTS, } from '../commands/invitation-poster-command'; import { debounce } from 'lodash-es'; +import { arrayBufferToBase64 } from '../utils/encoding'; import type { TableSeatingPlanner } from '../table-seating-planner'; import { Guest } from '../guest'; import { Host } from '../host'; @@ -10751,18 +10752,6 @@ function clampNum(v: unknown, min: number, max: number, def: number): number { if (!isFinite(n)) return def; return Math.max(min, Math.min(max, Math.round(n))); } -function arrayBufferToBase64(buffer: ArrayBuffer): string { - let binary = ''; - let bytes = new Uint8Array(buffer); - let chunk = 0x8000; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode.apply( - null, - Array.from(bytes.subarray(i, i + chunk)) as unknown as number[], - ); - } - return btoa(binary); -} function imageDims(src: string): Promise<{ w: number; h: number }> { return new Promise((resolve) => { let img = new Image(); diff --git a/41e20f-wedding-table-seating-planner/utils/encoding.gts b/41e20f-wedding-table-seating-planner/utils/encoding.gts new file mode 100644 index 00000000..54d3ce3c --- /dev/null +++ b/41e20f-wedding-table-seating-planner/utils/encoding.gts @@ -0,0 +1,8 @@ +export function arrayBufferToBase64(buffer: ArrayBuffer): string { + let binary = ''; + let bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} From 162600756caebbd550bdfef9d0169aa9653359ba Mon Sep 17 00:00:00 2001 From: Lucas Law Date: Fri, 4 Sep 2026 16:46:25 +0800 Subject: [PATCH 2/2] Fix Copilot review findings: perf regression + difficulty label behavior change - utils/encoding.gts: restore tsp.gts's chunked implementation of arrayBufferToBase64 (the naive per-byte version regresses large buffers, up to 25MB floor plan uploads). - virtual-piano.gts: difficultyLabel now returns '' whenever difficulty is falsy (no song selected, or a song with no difficulty set), matching pre-extraction behavior instead of showing "UNKNOWN". --- 150b3a-virtual-piano/virtual-piano.gts | 3 ++- .../utils/encoding.gts | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/150b3a-virtual-piano/virtual-piano.gts b/150b3a-virtual-piano/virtual-piano.gts index 2f5669ea..f6a9b877 100644 --- a/150b3a-virtual-piano/virtual-piano.gts +++ b/150b3a-virtual-piano/virtual-piano.gts @@ -743,7 +743,8 @@ class IsolatedVirtualPiano extends Component { } get difficultyLabel(): string { - return diffLabel(this.selectedSong?.difficulty ?? 0); + if (!this.selectedSong?.difficulty) return ''; + return diffLabel(this.selectedSong.difficulty); } get difficultyClass(): string { diff --git a/41e20f-wedding-table-seating-planner/utils/encoding.gts b/41e20f-wedding-table-seating-planner/utils/encoding.gts index 54d3ce3c..410ef331 100644 --- a/41e20f-wedding-table-seating-planner/utils/encoding.gts +++ b/41e20f-wedding-table-seating-planner/utils/encoding.gts @@ -1,8 +1,15 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string { let binary = ''; let bytes = new Uint8Array(buffer); - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); + // Chunk to avoid quadratic string concatenation and the call-stack/argument + // limit of String.fromCharCode.apply on large buffers (floor plan uploads + // allow up to 25MB). + let chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply( + null, + Array.from(bytes.subarray(i, i + chunk)) as unknown as number[], + ); } return btoa(binary); }