/** * WPML compatibility functions * * @global array $duplicated_posts Array to store the posts being duplicated. * * @package Yoast\WP\Duplicate_Post * @since 3.2 */ add_action( 'admin_init', 'duplicate_post_wpml_init' ); /** * Add handlers for WPML compatibility. */ function duplicate_post_wpml_init() { if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { add_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10, 3 ); add_action( 'shutdown', 'duplicate_wpml_string_packages', 11 ); } } global $duplicated_posts; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: Renaming a global variable is a BC break. $duplicated_posts = []; /** * Copy post translations. * * @global SitePress $sitepress Instance of the Main WPML class. * @global array $duplicated_posts Array of duplicated posts. * * @param int $post_id ID of the copy. * @param WP_Post $post Original post object. * @param string $status Status of the new post. */ function duplicate_post_wpml_copy_translations( $post_id, $post, $status = '' ) { global $sitepress; global $duplicated_posts; remove_action( 'dp_duplicate_page', 'duplicate_post_wpml_copy_translations', 10 ); remove_action( 'dp_duplicate_post', 'duplicate_post_wpml_copy_translations', 10 ); $current_language = $sitepress->get_current_language(); $trid = $sitepress->get_element_trid( $post->ID ); if ( ! empty( $trid ) ) { $translations = $sitepress->get_element_translations( $trid ); $new_trid = $sitepress->get_element_trid( $post_id ); foreach ( $translations as $code => $details ) { if ( $code !== $current_language ) { if ( $details->element_id ) { $translation = get_post( $details->element_id ); if ( ! $translation ) { continue; } $new_post_id = duplicate_post_create_duplicate( $translation, $status ); if ( ! is_wp_error( $new_post_id ) ) { $sitepress->set_element_language_details( $new_post_id, 'post_' . $translation->post_type, $new_trid, $code, $current_language ); } } } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: see above. $duplicated_posts[ $post->ID ] = $post_id; } } /** * Duplicate string packages. * * @global array() $duplicated_posts Array of duplicated posts. */ function duplicate_wpml_string_packages() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: renaming the function would be a BC-break. global $duplicated_posts; foreach ( $duplicated_posts as $original_post_id => $duplicate_post_id ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $original_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $original_post_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. $new_string_packages = apply_filters( 'wpml_st_get_post_string_packages', false, $duplicate_post_id ); if ( is_array( $original_string_packages ) ) { foreach ( $original_string_packages as $original_string_package ) { $translated_original_strings = $original_string_package->get_translated_strings( [] ); foreach ( $new_string_packages as $new_string_package ) { $cache = new WPML_WP_Cache( 'WPML_Package' ); $cache->flush_group_cache(); $new_strings = $new_string_package->get_package_strings(); foreach ( $new_strings as $new_string ) { if ( isset( $translated_original_strings[ $new_string->name ] ) ) { foreach ( $translated_original_strings[ $new_string->name ] as $language => $translated_string ) { do_action( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- Reason: using WPML native filter. 'wpml_add_string_translation', $new_string->id, $language, $translated_string['value'], $translated_string['status'] ); } } } } } } } } P14 最小実装スコープ(推奨) – Raqqa

P14 最小実装スコープ(推奨)

  • 画面構成
    • 左:チャット風の履歴(ユーザー自然文/生成SQL/結果サマリ)
    • 中:SQLプレビュー+実行結果テーブル(先頭100行)+Metabase埋め込み(url/iframe_src
    • 右:AI分析(analysis.summary, key_findings, sql_improvements, anomalies, suggestions
    • 上部:進行ステップバーstats.timelineguardsqlmetabaseanalysis を表示)
  • フロー
    1. /nlq/plan を叩いて候補SQL・display_hint を提示
    2. 「実行」押下で /nlq/executeanalyze_mode 選択可:off/quick/deep)
    3. レスポンスの timeline をステップバーに反映、429時は Retry-After をUIでカウントダウン
  • エラーハンドリング
    • 400(ガード): ステップバーの guard に note:error:...、メッセージをチップ表示
    • 429(deep): トースト+自動再試行ボタン(Retry-After 秒)
    • 500系: スナック表示+リトライ
  • 追加機能(任意)
    • sql_improvements[].revised_sql を「置換して再実行」ボタン
    • /nlq/history を使い、会話ごとに session_id を紐付けた履歴一覧

実装方針(最短ルート)

  • Stack: Vite + React + TypeScript + Tailwind(軽量・1ファイル配信でOK)
  • 依存:なし(shadcn/ui はお好みで)
  • コンテナ追加(docker-compose.yml 例) web: build: context: ./web dockerfile: Dockerfile environment: - VITE_API_BASE=http://localhost:8081 ports: - "5173:5173" depends_on: - api
  • 呼び出し例(超要点だけ) // fetch plan → execute const plan = await fetch(`${API}/nlq/plan`, { method:'POST', body: JSON.stringify({...})}).then(r=>r.json()); const ex = await fetch(`${API}/nlq/execute`, { method:'POST', body: JSON.stringify({...})}).then(r=> { if (r.status === 429) { const retryAfter = Number(r.headers.get('Retry-After') || 60); // UIでカウントダウン → 再試行 } return r.json(); }); // timeline をそのまま描画 // ex.stats.timeline: [{phase:'guard'|'sql'|'metabase'|'analysis', start_ms, elapsed_ms, note}]
  • Metabaseの表示
    • metabase.mode === 'public'url<iframe src=...>
    • metabase.mode === 'signed'iframe_src をそのまま <iframe src=...>

これで「動きのトレース」が可能

  • サーバ側は P10までで必要情報が出揃っているので、P13はフロントのみで成立します。
  • 仕様変更が必要になれば、stats.timelinephase/note 値を拡張(例:historycache_hit など)すればOK。

Go/No-Go 判定(P14への事前チェック)

**Go(緑)**にするための最小4点だけ:

  1. CORS設定
    • Allowed Origins に http://localhost:5173 を追加
    • Allowed Headers に Authorization, Content-Type
    • Expose HeadersRetry-After(JSからヘッダ読めるように)
  2. Metabase 埋め込み確認
    • dev: Public sharing ON/METABASE_PUBLIC_URL 到達OK
    • prod想定: Embedding ON+Secret設定/frame-ancestors で web のオリジン許可
  3. OpenAPIパッチ反映済み
    • ExecuteStats.columns/column_map 追加
    • ChromaPackageInput(入力用; collection任意)
    • ChromaUpsertResponse.items[].doc_id を常に返却
  4. 認証トークン流し込み
    • フロントから Authorization: Bearer … が通る(OPTIONS→200、POST→200/429 まで確認)

上記4点OKなら、P14はブロッカーなしで着工できます。

画面構成 ↔ APIフィールド対応

  • 左(チャット履歴)
    • ユーザー自然文:PlanRequest.message / HistoryItem.input_message
    • 生成SQL:PlanResponse.validated_sql(候補)→ 実行後は ExecuteResponse.sql
    • 結果サマリ:ExecuteStats.analysis.summaryquick/deep時)
  • 中(SQLプレビュー+結果テーブル+Metabase)
    • SQLプレビュー:PlanResponse.validated_sql(未実行時)、ExecuteResponse.sql(実行後)
    • 結果テーブル(先頭100行):ExecuteRequest.mode="sample"ExecuteResponse.rows
    • 列名:ExecuteStats.columns(正規化後)または column_map で表示名整える
    • Metabase埋め込み:
      • dev(public):stats.metabase.url<iframe src>
      • prod(signed):stats.metabase.iframe_src<iframe src>
  • 右(AI分析)
    • ExecuteStats.analysis.{key_findings, sql_improvements, anomalies, suggestions}
    • 「置換して再実行」:sql_improvements[].revised_sql をエディタへコピーして /nlq/execute
  • 上部(ステップバー)
    • ExecuteStats.timeline: [{phase:'guard'|'sql'|'metabase'|'analysis', start_ms, elapsed_ms, note}]
    • 429時:HTTPヘッダ Retry-After を読み、カウントダウン表示→再試行

UIフロー(最短ルート・イベント駆動)

  1. session_idlocalStorage に永続(無ければ UUID 生成)
  2. PlanPOST /nlq/plan → SQL候補・display_hint を左/中に出す
  3. ExecutePOST /nlq/executeanalyze_mode 選択)
    • 200:中ペインにテーブル・Metabase、右ペインに分析、上部は timeline 反映
    • 429:Retry-After 秒をカウントダウンして再実行ボタン活性
    • 400:guard ステップに note:"error:..."、チップで理由表示
    • 500:スナック+再試行
  4. 履歴POST /nlq/history(同 session_id で保存)/GET /nlq/history?session_id=... で左ペイン再構築

作業手順(リポ構成と最小タスク)

web(Vite+React+TS+Tailwind) を追加するだけでOK。compose例は提示どおりで十分。

  • 新規
    • web/(Vite アプリ)
    • 主要コンポーネント:
      • ChatPanel(左)/SqlAndResultPanel(中:SQLエディタ・テーブル・MetabaseFrame)/AnalysisPanel(右)/StepBar(上)
      • api.ts(fetchラッパ:ベアラートークン注入・429読取り・JSON整形)
      • types.ts(OpenAPIに沿った最低限の型)
  • 既存(API側の“設定”だけ)
    • CORS(allow origin 5173/expose Retry-After
      -(任意)レスポンスヘッダ Cache-Control: no-store(signed埋め込みのTTL誤用防止)

DoD(P14)

  • 機能
    • Plan→Execute 一連の操作が1画面で完結(SQL→表→Metabase→分析)
    • timeline が guard→sql→metabase→analysis の4ステップで表示
    • 429 を UI カウントダウンで可視化し、ワンクリック再実行可能
    • 「改善SQLの置換→再実行」ができる
    • 履歴一覧(session_id単位)からの復元表示
  • 品質
    • dev 環境で public / prod 想定で signed の両パスを手動検証
    • 400/429/500 それぞれのハンドリング確認(トースト/スナック/バナー)
    • LCP 1.5s 以内(ローカル)、JSバンドル < 300KB(初期版の目安)
  • 運用
    • .envVITE_API_BASE, VITE_DEFAULT_ANALYZE_MODE, VITE_METABASE_HEIGHT
    • README に起動手順(compose)と既知の制限(deepの制限・signedのTTL)が記載

つまずきポイント(先回りで潰す)

  • Retry-After が読めない → CORS の Access-Control-Expose-Headers: Retry-After を忘れずに
  • Metabase が iframe 拒否 → Embedding ON/frame-ancestorshttp://localhost:5173 を含める
  • 列名が崩れるExecuteStats.columns/column_map を優先使用(無ければ rows[0] のキー配列)
  • 429ループ → リトライ上限+指数バックオフ(例:retryAfter*(1+0.2*rand)
  • 認証 → dev は固定Bearer、prod は JWKS を想定。フロントはトークン取得を env/ローカル設定で注入


Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です