diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx index 227ea731e..a602574f2 100644 --- a/client/src/components/Admin/AddonManager.test.tsx +++ b/client/src/components/Admin/AddonManager.test.tsx @@ -399,7 +399,18 @@ describe('AddonManager', () => { // The catalog key now exists, so the tile shows the translation rather than falling // back to the English description the server sent. expect(screen.getByText('Reads bookings the built-in parser cannot, using an AI model you choose')).toBeInTheDocument(); - expect(screen.queryByText('Connection')).not.toBeInTheDocument(); + expect(screen.queryByText('Provider')).not.toBeInTheDocument(); + }); + + it('FE-ADMIN-ADDON-020b: the config lives in the tile shelf, not as a band below the grid', async () => { + server.use(addonsRoute([llmAddon({ provider: 'openai' })])); + render(); + + const label = await screen.findByText('Provider'); + // Inside the AI Parsing tile's
, like the collab toggles sit in theirs. + const tile = label.closest('article'); + expect(tile).not.toBeNull(); + expect(tile!.textContent).toContain('AI Parsing'); }); it('FE-ADMIN-ADDON-021: the local provider lists installed models and a chip fills the model field', async () => { @@ -617,7 +628,7 @@ describe('AddonManager', () => { server.use(addonsRoute([llmAddon({ provider: 'openai' })]), modelsRoute([], urls)); render(); - await screen.findByText('Connection'); + await screen.findByText('Provider'); expect(screen.queryByText('Installed on the server')).not.toBeInTheDocument(); await user.type(screen.getByPlaceholderText('https://api.openai.com/v1'), 'https://proxy.local/v1'); diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index 038a4d406..41a88fbf5 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -196,6 +196,11 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, ) }) } + // The AI-parsing settings hang off their tile like every other sub-feature, + // just as a form instead of toggle rows. + if (addon.id === 'llm_parsing' && addon.enabled) { + return + } return null } @@ -215,8 +220,6 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, ) } - const llmAddon = integrationAddons.find(a => a.id === 'llm_parsing') - /* One column per type, side by side, instead of three stacked sections. Stacked, each section opened its own set of columns and the tall tiles (Collab 261px, Journey 185px) set a section's height while its neighbours ran out early. Side by side the @@ -253,23 +256,17 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {addons.length === 0 ? ( ) : ( - <> -
- {groups.map(g => { - const meta = TYPE_META[g.key] - return ( -
- -
{g.addons.map(tile)}
-
- ) - })} -
- {/* The AI-parsing form is far too wide for a tile, so it sits as a band under - the columns — no span, no reflow, and the next service that needs settings - stacks below it. */} - {llmAddon?.enabled && } - +
+ {groups.map(g => { + const meta = TYPE_META[g.key] + return ( +
+ +
{g.addons.map(tile)}
+
+ ) + })} +
)} ) @@ -394,9 +391,8 @@ function LlmParsingConfig({ addon }: { addon: Addon }) { } } - const fieldCls = 'w-full rounded-lg border border-edge-secondary bg-surface px-3 py-2 text-body text-content placeholder:text-content-faint transition-colors focus:border-edge focus:outline-none' - const labelCls = 'mb-1.5 block text-caption font-medium text-content-secondary' - const sectionCls = 'text-caption font-semibold uppercase tracking-wide text-content-faint' + const fieldCls = 'w-full rounded-lg border border-edge-secondary bg-surface px-2.5 py-1.5 text-caption text-content placeholder:text-content-faint transition-colors focus:border-edge focus:outline-none' + const labelCls = 'mb-1 block text-caption font-medium text-content-secondary' const providerOptions = [ { value: 'local', label: 'Local · OpenAI-compatible', icon: , badge: 'Ollama' }, @@ -404,124 +400,110 @@ function LlmParsingConfig({ addon }: { addon: Addon }) { { value: 'anthropic', label: 'Anthropic', icon: }, ] + /* Lives in the tile's shelf like the collab toggles, so everything is caption- + sized and single-column — the band this used to be had a whole page width. */ return ( -
-
- - {addon.name} -
-
-

- Set instance-wide config (applies to all users). Leave blank to let each user configure their own provider. -

+
  • +

    + Instance-wide — applies to all users. Leave blank to let each user configure their own provider. +

    - {/* Connection */} -
    -
    Connection
    -
    - Provider - setProvider(String(v))} options={providerOptions} /> +
    + Provider + setProvider(String(v))} options={providerOptions} /> +
    + {provider !== 'anthropic' && ( + + )} + + {provider === 'anthropic' && ( +

    Anthropic reads PDFs (including scans) natively. Local/OpenAI models receive extracted text — scanned PDFs need Anthropic.

    + )} + + + {/* Local model management (Ollama) */} + {provider === 'local' && ( +
    +
    + Installed on the server +
    - {provider !== 'anthropic' && ( - + {modelsErr &&

    {modelsErr}

    } + {!modelsErr && installed.length === 0 && !loadingModels && ( +

    No models installed yet — pull one below.

    )} - - {provider === 'anthropic' && ( -

    Anthropic reads PDFs (including scans) natively. Local/OpenAI models receive extracted text — scanned PDFs need Anthropic.

    - )} -
    - - {/* Model */} -
    -
    Model
    - - - {/* Local model management (Ollama) */} - {provider === 'local' && ( -
    -
    - Installed on the server - -
    - {modelsErr &&

    {modelsErr}

    } - {!modelsErr && installed.length === 0 && !loadingModels && ( -

    No models installed yet — pull one below.

    - )} - {installed.length > 0 && ( -
    - {installed.map(name => ( - - ))} -
    - )} - -
    -
    Pull a recommended model
    -
    - {RECOMMENDED_MODELS.map(m => { - const installedHere = isInstalled(m.id) - const isPulling = pulling === m.id - const active = model === m.id - return ( -
    -
    -
    - {m.label} - {m.recommended && ( - Recommended - )} -
    -
    {m.note}
    - {isPulling && ( -
    -
    -
    -
    -
    {pullStatus}{pullPct ? ` · ${pullPct}%` : ''}
    -
    - )} -
    - {installedHere ? ( - - ) : ( - - )} -
    - ) - })} -
    -
    + ))}
    )} -
    - + ) : ( + + )} +
  • + {isPulling && ( +
    +
    +
    +
    +
    {pullStatus}{pullPct ? ` · ${pullPct}%` : ''}
    +
    + )} +
    + ) + })} + + + + )} + +
    +
    -
    + ) }