TL;DR 在庫CSV/Excel をドロップしてから取り込み確定まで、9ステップを順に追う。テンプレ一致は従来ウィザードへ、不一致だけ AI 解析。解析結果は コードが矯正し、迷う列だけ 1問ずつ会話で確認。確定すると整形済みデータが 既存ウィザードに流れ込み、取り込み成功後に 再利用ルールを保存する。
「AIが触るのは③だけ」なのが見て取れます。残りは全部コードの決定的処理。これが原則1(AIは提案だけ)の実物です。
detectStockTemplateMatch で判定。不一致なら本体シートが AiImportThread に切り替わる。CompanyDB.aiImportConsentedAt が null なら同意ダイアログ。同意保存に失敗したらAIを開始しない(fail-safe)。dropEmptyColumns(table) → {table, droppedColumns[]}。列統計 ColumnStat[] をクライアントで作る。lookupReuseApi(LLM不使用)。詳細は ⑤再利用。AiAnalyzingCard。ステップは700ms間隔、最終ステップは done=true まで待つ。analyzeImportApi → サーバーで aiClient.structured(..., AnalyzeResultSchema, {tier:'strong'})。OpenAI Responses API・store:false(会話を保存しない)。この要約文(summary)はAIが生成。AIからの「お知らせ」のうち confirmations.kind==='other' だけが確認ピルとして会話に出ます。
AIの構造化出力は「型」は合っていても「意味」が正しいとは限りません(存在しない列番号、架空の属性名など)。sanitizeAnalyzeResult が行データに触る前に、決定的に直します。
| 矯正 | 内容 |
|---|---|
| 範囲外の除去 | 列数を超える sourceIndex を持つマッピング・ルールを捨てる。 |
| 架空属性名の救済 | 会社に存在しない属性名を指す列は ignore に落とすが、新規提案として可視化(黙って消さない)。 |
| nancoID の厳格判定 | 他システムの管理番号(例 N0010-A)を nancoID 扱いすると全行が照合エラーになるため、サンプルの8割以上が nanco 発番形式でなければ「管理番号」属性へ降格。 |
| 価格列の補完 | AIが取りこぼして ignore に落とした「単価・金額」系の数値列を、見出し正規表現+全行型判定で価格列に拾い直す。 |
| 消える列の救済 | データがあるのに ignore された列を opt-in 提案(既定オフ)として出す。原則2の番人。 |
buildColumnPlans が各列を5種に分類。auto 以外だけが質問に回ります。明確な列(システム列・既存名一致+整形不要+高確信≥0.7)は黙って確定。
| 種別 | 意味 | カードの選択肢 |
|---|---|---|
auto | 確定済み(質問しない) | — |
newAttr | 新しいアイテム情報の提案 | 「◯◯として取り込む/取り込まない/別の情報にする」+型ピッカー+自由入力 |
rescue | 整形すれば使える | 「整形して取り込む/取り込まない/別の情報にする」 |
ambiguous | 意味が曖昧 | 「◯◯として取り込む/取り込まない/別の情報にする」 |
price | 価格列 | 「販売価格/仕入価格/この列は使わない」(権限なしは ignore 固定) |
新規属性の列では、種類(文字/数値/日付/選択肢)をピルで選べ、おすすめには「(おすすめ)」と理由を一言添える。選択肢型なら選択肢チップを編集でき、型が合わなければ(ブロックせず)警告。さらに自由入力で「数値にして」「選択肢にして 赤 青 緑」「これは商品名」などと指示でき、AIが1ショットで解釈し直します。
進め方は「1つずつ」か「一気に」を最初に選べ、質問が5列を超えると「まだ続けますか?」と一度区切ります(cap=5)。「一気に」を選ぶと質問を飛ばし、価格列はAI推測ロールを自動適用します。
会話の回答は AiImportThread の決定ストア(attrDecisions / overrides / priceDecisions / rejectedTransforms / folderSimplify)に溜まります。これらを後勝ちで合成した最終方針(effectiveResult)を applyAnalysisToRows に渡し、全行を整形します。
できた表は checkCsvHeader(既存ウィザードと同じ検証)にかけてから親へ返します。「AIは提案、変換は決定的コード、検証は既存と同じ」が1つの関数(trial)に集約されています。
handleProceed。作成IDを蓄積し、途中中断ならロールバック。Promise.all禁止(採番のデッドロック対策で逐次)。onComplete(checkCsvHeader(...), createdIds, {sessionId, decisions}, documentFields)CSVImportProgress(initialStep=4)→ CSVTable → useSpreadsheetImport。importSucceeded 発火で commitImportApi(親 CSVImportModal が保持)。rollbackCreatedAttrs で削除し、使われない属性が残らないようにする。会話は msgs: Msg[](メッセージ配列)で駆動。回答ハンドラが次のメッセージを push することで前進する手続き的な状態機械です。種類は file / analyzing / prose / echo / confirm / emptyColumns / reusePrompt / folderPlan / preamble / columnQuestion / capPrompt / mapping / fatal。
*AnsweredRef / *DecidedRef で各確認の再発火を防止。回答済みピルは固定表示。<X/> でなく関数呼び出しで描画。入力欄はローカルstate。フォーカス飛び対策。interpretBusyRef で即弾く。finallyで必ず解放(漏れると2回目以降が無言で死ぬ)。isComposing と keyCode!==229 を両方見て、日本語変換確定のEnterで誤送信しない。AbortController を生存判定に使い、シートを閉じたら数十秒のfetchを中断。setTimeout 越しに「本当のunmount」だけ実行。再マウントなら中断をキャンセル。