# Keywords — "Sources & IA" modal (Slice 2)

**Date:** 2026-06-18
**Status:** design (decisions baked in; user may veto), proceeding to implementation
**Scope:** Slice 2 of the keywords/planning production-cockpit effort. Builds on
Slice 1 (planning readiness table + per-keyword prompt generator).

## Goal

A unifying **"Sources & IA"** modal opened from the Planning header "Importer"
button: one place to import an export (unified auto-detecting dropzone) and to
see/connect data sources. Adapts `Modal Sources & IA.html` to our stack and data.

## Key fact (from the import-surface inventory)

Everything needed already exists; Slice 2 is mostly composition/IA, no new parsers:
- Keyword CSV import auto-detects the tool (client-side header inspection in
  `import-keywords-dialog.tsx`) and routes to `keywords/import-heavy/route.ts`
  (50 MB, FormData, async ETL + poll). Destinations: réservoir / recherche.
- YTG brief import = `import-brief-dialog.tsx` → `parseYtgTexts()` +
  `importKeywordBriefAction(keywordId, texts)` (keyword-scoped, `.txt`, ≤1 MB).
- SERP import = `keywords/serp/import/route.ts` (keyword-scoped).
- GSC = the only LIVE connector (OAuth) via `gsc-integrations.tsx`. Semrush /
  Ahrefs / Mangools / SE-Ranking / YTG are file-import only (no API).
- Reusable: shadcn `Dialog` + `Tabs`, drag-drop pattern, WASM
  `detect_keyword_source`, toast/error patterns.

## Baked-in scope decisions (user may veto)

1. **Keyword CSV** → the existing global flow (import-heavy, destination picker),
   reused as-is.
2. **YTG `.txt`** → parsed client-side; matched by **term** to an existing keyword
   of the active site; on match the brief is attached; on miss, a clear message.
3. **SERP import** stays **per-keyword** (from the Overview modal) — a SERP export
   has no target term out of context, so it is NOT handled in the global dropzone
   (the dropzone detects it and points the user to the keyword Overview).
4. **"Brief & rédaction IA" tab** is already delivered as the per-keyword Overview
   modal (Slice 1); the modal's connector/IA card links there rather than
   rebuilding the generator. So Slice 2 ships **Import + Connecteurs** tabs.
5. Unsupported drops (Screaming Frog on-page, ranxplorer) → "format pas encore
   supporté" (those become Slice 3 ingestion).

## Architecture

### Detection (new, pure-ish) — `src/lib/keywords/import/detect-file.ts`
`detectImportFile(filename: string, headSample: string): ImportFileKind`
returns `{ kind: "keyword-csv" | "ytg-brief" | "serp-csv" | "unknown"; source?: KeywordSource }`.
Composes the EXISTING detectors: keyword-CSV header inspection (reuse/extract the
regex from `import-keywords-dialog`), YTG marker detection (Lumenia/YTG header in
`.txt`), SERP header sniff ("url"+"position"). Pure + unit-tested.

### Global YTG brief import (new) — server action
`importBriefByTerm(siteId, briefText, guideText)` in
`src/lib/keywords/import/brief-by-term-actions.ts` (`"use server"`):
parse via `parseYtgTexts()`, slugify `brief.query`, look up keyword
`${siteId}-${slug}` (org-guarded), attach the brief by reusing the existing
attach logic (`importKeywordBriefAction` core). Returns
`{ matched: boolean; keywordId?: string; term: string }`. Tested: match, no-match,
org isolation.

### UI — `src/components/keywords/sources/sources-modal.tsx` (client)
Centered `Dialog` + `Tabs`:
- **Importer**: a single dropzone. On a dropped file, read a head sample, call
  `detectImportFile`, then:
  - keyword-csv → render the existing keyword-CSV import UI (destination picker →
    import-heavy → poll). Reuse `import-keywords-dialog`'s logic (extract a shared
    hook/inner component if needed; do not duplicate the parser).
  - ytg-brief → read full text client-side, call `importBriefByTerm`; toast the
    matched term or show the no-match guidance.
  - serp-csv → info state pointing to the keyword Overview.
  - unknown → "format non reconnu / pas encore supporté".
- **Connecter**: a grid of source cards. GSC card = live connect/manage (reuse the
  `gsc-integrations` flow). Other sources = "Import d'export" cards that switch to
  the Import tab. Each card shows a connected/not-connected pill.

### Wiring
Planning header "Importer" button (`planning-table.tsx`) opens `SourcesModal`
(replacing the current stub/existing import entry).

## Data flow
1. User clicks "Importer" on Planning → SourcesModal opens (Import tab).
2. Drop file → `detectImportFile` → branch to the matching flow (all reuse
   existing actions/routes).
3. Connecteurs tab: GSC OAuth via existing flow; other cards deep-link to Import.

## Error handling
- Detection "unknown" / unsupported → explicit message, no silent failure.
- YTG no term match → "Aucun mot-clé « X » sur ce site — ajoute-le d'abord."
- Reuse existing per-flow error handling (1 MB guard for brief, job-poll errors
  for import-heavy).

## Testing
- `detect-file.test.ts`: keyword CSV (each source header), YTG `.txt`, SERP CSV,
  unknown.
- `brief-by-term-actions.test.ts`: match attaches brief; no-match returns matched
  false; org isolation throws.
- (UI verified at integration + manual QA.)

## Implementation fan-out (subagents)
- Wave 1 (parallel, independent): U1 `detect-file.ts` + tests; U2
  `importBriefByTerm` action + tests.
- Wave 2: U3 `SourcesModal` UI (Import dropzone + Connecteurs grid) + wire the
  Planning "Importer" button — consumes U1/U2 + existing flows.
- Integrate: typecheck + `bun run test:all` + biome, then commit.
