|
| 1 | +# Dynamic Column Mapping for Run AI — Design |
| 2 | + |
| 3 | +**Date:** 2026-02-24 |
| 4 | +**Status:** Approved |
| 5 | + |
| 6 | +## Problem |
| 7 | + |
| 8 | +`runBatchAI` currently requires exact hardcoded column headers (`source_drive`, `system_prompt`, `user_prompt`, `ai_inference`) defined in `CONFIG.COLUMNS`. Users cannot use the tool without renaming their columns to match. The `AIMode` ("TEXT" | "FILE") binary forces a choice between text and Drive file input, preventing mixed requests even though `runInference` already supports them natively. |
| 9 | + |
| 10 | +## Goal |
| 11 | + |
| 12 | +Replace the hardcoded column map with a sidebar-driven flow where users select their column mappings at execution time. Remove `AIMode`, `ColumnMap`, `ColumnConfig`, and `CONFIG.COLUMNS` entirely. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Section 1: Types & Data Contract |
| 17 | + |
| 18 | +### Remove from `types.ts` |
| 19 | +- `AIMode` |
| 20 | +- `ColumnMap` |
| 21 | +- `ColumnConfig` |
| 22 | +- `AppConfig.COLUMNS` |
| 23 | + |
| 24 | +### Remove from `config.ts` |
| 25 | +- `CONFIG.COLUMNS` |
| 26 | + |
| 27 | +### Add to `types.ts` |
| 28 | + |
| 29 | +```ts |
| 30 | +export interface RunConfig { |
| 31 | + userPromptCols: string[]; // required; values concatenated as userTexts |
| 32 | + driveFileCols?: string[]; // optional; values passed as driveLinks |
| 33 | + systemPromptCol?: string; // optional; single column, value used as systemPrompt |
| 34 | + outputCol: string; // required; created if not found in headers |
| 35 | + rowRange?: { start: number; end: number }; // 1-based, inclusive; overrides sheet selection |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +Only `userPromptCols` and `outputCol` are required. Empty/undefined optional fields mean "not selected" — not an error. |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Section 2: `runInference` Signature Update |
| 44 | + |
| 45 | +Optional columns should be represented as optional in code. Update `runInference` to use optional parameters and add explicit guards: |
| 46 | + |
| 47 | +```ts |
| 48 | +export function runInference( |
| 49 | + userPrompts: unknown, |
| 50 | + driveLinks?: unknown, |
| 51 | + systemPrompt?: unknown, |
| 52 | +): string | null { |
| 53 | + const userTexts = flattenArg(userPrompts); |
| 54 | + if (userTexts.length === 0) return null; |
| 55 | + |
| 56 | + try { |
| 57 | + const inlineData: GeminiInlineData[] = driveLinks !== undefined |
| 58 | + ? flattenArg(driveLinks).filter(isValidDriveLink).map((link) => fetchAndEncodeFile(extractId(link))) |
| 59 | + : []; |
| 60 | + |
| 61 | + return invokeGemini({ |
| 62 | + systemPrompt: systemPrompt !== undefined ? flattenArg(systemPrompt)[0] : undefined, |
| 63 | + userTexts, |
| 64 | + inlineData: inlineData.length ? inlineData : undefined, |
| 65 | + }); |
| 66 | + } catch (e) { |
| 67 | + return "Error: " + (e as Error).message; |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Section 3: Sidebar & Server Interface |
| 75 | + |
| 76 | +### New server function: `getSheetHeaders()` |
| 77 | + |
| 78 | +Reads the first row of the active sheet and returns `string[]`. Called by the sidebar on load to populate column pickers. Must be exported from `index.ts` and have a global stub in the Rollup footer. |
| 79 | + |
| 80 | +### Sidebar flow |
| 81 | + |
| 82 | +"Run AI Inference" no longer dispatches via `runTool`. Clicking it reveals an inline config panel within the sidebar. On open, the panel calls `getSheetHeaders()` and renders column pickers once headers are returned. |
| 83 | + |
| 84 | +**Config panel fields:** |
| 85 | + |
| 86 | +| Field | UI control | Required | |
| 87 | +|---|---|---| |
| 88 | +| User prompt columns | Checkbox list | Yes (≥1) | |
| 89 | +| Drive file columns | Checkbox list | No | |
| 90 | +| System prompt column | Single-select dropdown (with empty/none option) | No | |
| 91 | +| Output column | Dropdown of existing headers + "New column..." option revealing a text input pre-filled with `ai_` | Yes | |
| 92 | +| Row range | Radio: "Use sheet selection" (default) / "Specify range" (start + end number inputs) | — | |
| 93 | + |
| 94 | +- If `getSheetHeaders()` returns `[]`: show "No columns found — add headers to your sheet first" and disable Run |
| 95 | +- **Run** collects values into a `RunConfig` and calls `google.script.run.runBatchAI(config)` directly (not via `runTool`) |
| 96 | +- **Cancel** collapses the panel back to the tool list |
| 97 | + |
| 98 | +### Removals |
| 99 | +- `showSourceDialog` (function + `TOOLS` entry) |
| 100 | +- `handleDialogSelection` |
| 101 | +- `dialog.ts` and its `HTML_TEMPLATE` export |
| 102 | + |
| 103 | +--- |
| 104 | + |
| 105 | +## Section 4: `runBatchAI` Implementation |
| 106 | + |
| 107 | +### Signature |
| 108 | + |
| 109 | +```ts |
| 110 | +export function runBatchAI(config: RunConfig): void |
| 111 | +``` |
| 112 | + |
| 113 | +### Steps |
| 114 | + |
| 115 | +1. **Read headers** — `sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]` |
| 116 | +2. **Resolve column indices** — for each selected column name, find its index in the header row |
| 117 | +3. **Validate** — any column the user explicitly selected must be present in the sheet headers. This includes optional fields if they were set: |
| 118 | + - `userPromptCols` missing → `ui.alert` listing missing names, return early |
| 119 | + - `driveFileCols` selected but missing → same error |
| 120 | + - `systemPromptCol` selected but missing → same error |
| 121 | + - Unset optional fields (`driveFileCols` undefined/empty, `systemPromptCol` undefined) → silently skip |
| 122 | +4. **Resolve output column** — if `outputCol` not found, append it as a new header in the next empty column; record that index |
| 123 | +5. **Determine row range** — use `config.rowRange` if present; otherwise fall back to `sheet.getActiveRange()` |
| 124 | +6. **Loop over rows** — for each row: |
| 125 | + - Collect values at all `userPromptCols` indices → `userPrompts` |
| 126 | + - Collect values at all `driveFileCols` indices if set → `driveLinks` |
| 127 | + - Collect value at `systemPromptCol` index if set → `systemPrompt` |
| 128 | + - Call `runInference(userPrompts, driveLinks, systemPrompt)` |
| 129 | + - If result is `null`, skip; otherwise write to output column cell and flush |
| 130 | + |
| 131 | +### Pure helper: `resolveColumns` |
| 132 | + |
| 133 | +Extract column resolution into a testable pure function: |
| 134 | + |
| 135 | +```ts |
| 136 | +function resolveColumns(headers: string[], names: string[]): number[] |
| 137 | +``` |
| 138 | + |
| 139 | +Returns an array of indices (same order as `names`). Returns `-1` for any name not found. Used inside `runBatchAI` for validation and index lookup. |
| 140 | + |
| 141 | +--- |
| 142 | + |
| 143 | +## Section 5: Error Handling Summary |
| 144 | + |
| 145 | +| Scenario | Behavior | |
| 146 | +|---|---| |
| 147 | +| `getSheetHeaders()` on empty sheet | Returns `[]`; sidebar disables Run | |
| 148 | +| Selected column not found at execution | `ui.alert` listing missing columns, return early | |
| 149 | +| Unset optional column | Silently skipped | |
| 150 | +| `outputCol` not found | Append as new column header, continue | |
| 151 | +| Per-row inference failure | Write `"Error: ..."` string to output cell | |
| 152 | +| Server throw from sidebar | Existing `withFailureHandler` surfaces message | |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Section 6: Testing |
| 157 | + |
| 158 | +- **`runInference`** — update existing tests to use optional param signature; no behavioral changes |
| 159 | +- **`resolveColumns`** — new unit tests: all found, partial miss, empty input, empty headers |
| 160 | +- **`runBatchAI` / `getSheetHeaders()`** — remain untested per existing pattern for SpreadsheetApp-coupled functions (see `docs/plans/2026-02-18-testing-coverage-design.md`) |
| 161 | +- **Sidebar** — manual testing in GAS; no unit tests |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Files Touched |
| 166 | + |
| 167 | +| File | Change | |
| 168 | +|---|---| |
| 169 | +| `src/shared/types.ts` | Remove `AIMode`, `ColumnMap`, `ColumnConfig`, `AppConfig.COLUMNS`; add `RunConfig` | |
| 170 | +| `src/server/config.ts` | Remove `COLUMNS` from `CONFIG` and `AppConfig` | |
| 171 | +| `src/server/inference.ts` | Update `runInference` to optional params with explicit guards | |
| 172 | +| `src/server/index.ts` | Replace `runBatchAI(mode)` with `runBatchAI(config)`; add `getSheetHeaders()`; remove `showSourceDialog`, `handleDialogSelection`; remove `dialog.ts` import | |
| 173 | +| `src/server/dialog.ts` | Delete | |
| 174 | +| `src/Sidebar.html` | Replace "Run AI Inference" button with inline config panel | |
| 175 | +| `rollup.config.js` | Add `getSheetHeaders` global stub; remove `handleDialogSelection`, `showSourceDialog` stubs | |
| 176 | +| `__tests__/inference.test.ts` | Update param signatures | |
| 177 | +| `__tests__/` | Add `resolveColumns` unit tests | |
0 commit comments