<?php
/**
 * NeccoSpeak agent-ui — AI Settings tab.
 *
 * Structural equivalent of ai-agent-embed v1.5.1.1's ai_agent_embed_render_tab_ai()
 * (admin-agent-settings.php), which renders 5 independent cards, each with its
 * own <h2> heading and its own save action, in this fixed order:
 *
 *   1. "AI基本設定"       — ai_agent_embed_render_basics_wrapper('all_except_navigation')
 *   2. "話し方・性格"     — ai_agent_embed_render_tab_tone()
 *   3. "ペルソナ・プロフィール" — ai_agent_embed_render_advanced_wrapper('persona')
 *   4. "回答ルール"       — ai_agent_embed_render_tab_rules()
 *   5. "コンテンツ連携"   — ai_agent_embed_render_tab_content()
 *
 * NeccoSpeak has no source-level equivalent of those 5 separate renderers —
 * its structured-prompt fields all live in one flat function,
 * includes/admin-tabs.php neccospeak_render_tab_prompt() (クイックスタート →
 * 基本指示 → アシスタントプロフィール → FAQ設定 → 話し方・性格 → 回答ルール →
 * プロンプトプレビュー), and its content-sync fields live in that same file's
 * neccospeak_render_tab_content() (Page context → Tools → Site content),
 * which historically was only reachable from the legacy (non agent-ui) 'content'
 * tab and had no home at all in this 9-tab agent-ui.
 *
 * Since admin-tabs.php is out of scope here, this file reproduces the
 * production card boundaries by buffering each legacy renderer's full output
 * (ob_start()) and splitting it on its own <h2> markers (same technique
 * tab-flow.php already uses to lift the CTA section out of
 * neccospeak_render_tab_actions()) rather than duplicating any field markup
 * or sanitization — EXCEPT where the legacy renderer's markup itself no
 * longer matches production's look (screenshot-confirmed diff), in which
 * case that piece is hand-implemented here with production markup/copy
 * instead of being reused wholesale (project policy: stop reusing the
 * legacy renderer where it visibly diverges from production; keep the
 * input `name` attributes and `neccospeak_agent_ui_form_open()`/`_close()`
 * wrapping the same so the save path is untouched).
 *
 * 2026-08 screenshot-confirmed reorder pass ("同じところを探すのが大変なくらい
 * 順序が違う"): production's tab body always renders its "AI基本設定" card
 * FIRST. The previous revision of this file instead opened with an NS-only
 * "AI基本設定（NeccoSpeak）" card (enable toggle / API key) that has no
 * production counterpart at all — that's what made the two screens hard to
 * line up side by side. This revision reorders the tab so the
 * production-equivalent card is first and every NS-only addition is pushed
 * down to a single "NeccoSpeak独自の設定" card near the very end:
 *
 *   1. "AI基本設定" (id="ai-agent-basics", now first — same id production's
 *      own ai_agent_embed_render_basics_wrapper() always uses, and the same
 *      id tab-flow.php's ナビゲーション設定 card already reuses on a
 *      different tab page, so no collision) — transcribed from
 *      ai_agent_embed_render_basics_wrapper('all_except_navigation') +
 *      ai_agent_embed_render_section_basics_identity() +
 *      ai_agent_embed_render_section_basics_knowledge()
 *      (admin-agent-settings.php ~1270-1434). Rows that were missing until
 *      now, added in production's exact position:
 *        - エージェント名 (readonly, mirrors widget_title — NeccoSpeak's
 *          closest equivalent of production's agent name; NeccoSpeak's own
 *          description supplements production's copy because — unlike
 *          production, where it's backend-only — this value *is* editable
 *          here, just from the ウィジェット設定 tab's ヘッダータイトル field).
 *        - このAIの役割's inner role-template chip row (production's
 *          ai-agent-role-templates div, distinct from the outer
 *          プリセットから始める band above the whole table). Both chip rows
 *          reuse the same $ai_role_presets array and the same
 *          textarea-only-fill behavior as before (confirmed scope: only
 *          役割 gets filled, not goals/company).
 *        - どんなAIか（説明） — NeccoSpeak has no equivalent key at all, so
 *          per project policy (never silently drop a production control)
 *          this is reproduced as a disabled/grey "有料版の機能です" row in
 *          production's exact position rather than omitted.
 *        - 会社基本情報's structured fields were missing production's 6th
 *          row (URL) — added, disabled/grey same as the other 5.
 *      役割/目標 descriptions are now production's verbatim copy (the
 *      byte-limit note is still appended in parens where NeccoSpeak has
 *      one — that's a NeccoSpeak-only constraint production doesn't have,
 *      so it can't come from production's copy).
 *      The header badge/save-button/note are production-STYLED but
 *      NeccoSpeak-ACCURATE rather than a verbatim copy of production's
 *      text: production's badge is a static "即時反映 + 一部ドラフト保存"
 *      because production really does split saves per field (role / goals
 *      / FAQ / company post immediately; only the URL map is staged as a
 *      draft). NeccoSpeak's draft_workflow_enabled
 *      (includes/settings.php neccospeak_divert_option_save()) is a single
 *      GLOBAL switch that diverts an entire save — this card's role/goals
 *      /company included — to the draft row or not; there is no per-field
 *      split. So here the badge reads the live
 *      neccospeak_draft_workflow_active() state and shows one of two
 *      states (即時反映 / 下書き保存 — ai-agent-save-mode--immediate /
 *      --draft) instead of production's --mixed styling, and the note line
 *      points at this tab's own "NeccoSpeak独自の設定" card (where the
 *      toggle now lives) and at the ダッシュボード タブの「ドラフトを公開」
 *      button (the actual publish control — see tab-dashboard.php) instead
 *      of production's "Issue #118" / "概要タブ" copy, neither of which
 *      means anything in NeccoSpeak.
 *      A submit button now also sits in the header next to the badge
 *      (production style); the form closes with a plain `</form>` rather
 *      than neccospeak_agent_ui_form_close() so there isn't a second,
 *      redundant "保存" button at the card's foot — same convention
 *      tab-flow.php's own header-button card already uses.
 *      2026-08 parity pass split the card body below the preset bar into
 *      production's 3 subtabs (役割・目標 / 会社情報 / FAQ, the
 *      $subtabs=true branch); 0.3.0-dev.34 then DELIBERATELY flattened them
 *      back into 3 stacked sections with h3 headings (user decision: the
 *      tab-switching region's boundaries were visually unclear — an
 *      intentional divergence from production). All 3 sections still sit in
 *      the same single <form>, so the save path is unchanged either way.
 *   2. "話し方・性格" — body unchanged from the prior revision (see below).
 *      2026-08 parity pass: the header now carries the same
 *      .ai-agent-section-actions (save-mode badge + submit) as card 1
 *      (production: admin-agent-settings.php:1723-1728); the form opens
 *      before the header and closes with a plain </form> per the same
 *      convention as card 1 — the set of named inputs it wraps is unchanged.
 *   3. "ペルソナ・プロフィール" — same header treatment (production 2079-2084);
 *      body unchanged.
 *   4. "回答ルール" — same header treatment (production 1819-1824); body
 *      unchanged.
 *   5. "コンテンツ連携" — header now carries a submit-only section-actions
 *      ("設定を保存" — production 3108-3114 has buttons only, no badge, on
 *      this card); body unchanged.
 *   6. "プロンプトプレビュー" — still has no production AI-tab equivalent
 *      (production's prompt preview lives on the Model tab). 2026-08 update:
 *      per the strict "hide anything absent from production" pass below,
 *      this whole card is now wrapped `data-neccospeak-extras-hidden="1"`
 *      rather than left visible.
 *   7. "NeccoSpeak基本設定" (id="ai-agent-neccospeak-settings"; 2026-08:
 *      moved from just before the trailing アクセス解析 card to the VERY TOP
 *      of the tab — the API key is the precondition for every feature, so
 *      it must be the first thing a new user sees) — the enable
 *      toggle / OpenAI APIキー / モデル・詳細設定 link table (previously its
 *      own top card, "AI基本設定（NeccoSpeak）", buffered from
 *      neccospeak_render_tab_general()'s intro table with the model/
 *      temperature/max-tokens/timeout rows stripped out — unchanged
 *      technique, see below) plus 変更管理ワークフロー (also buffered from
 *      neccospeak_render_tab_general(), unchanged) now live together here
 *      under one heading instead of the old "AI基本設定（NeccoSpeak）"
 *      title, which visually duplicated card 1's "AI基本設定" and was the
 *      reordering's whole trigger. Functionality and save route
 *      (neccospeak_agent_ui_form_open('general') /
 *      neccospeak_tab_fields()['general']) are untouched — only the
 *      heading and position moved.
 *   8. "アクセス解析" (very end of the tab) — unchanged from the prior
 *      revision; still has no production AI-tab equivalent.
 *
 * Prior revision's notes, still accurate for what they cover:
 *   - [now folded into card 7 above] fields with no production equivalent
 *     that must live somewhere reachable (the plugin is unusable without
 *     them): enable toggle, OpenAI API key. Buffers
 *     neccospeak_render_tab_general() and strips the model / temperature /
 *     max tokens / timeout rows out of its intro table (screenshot diff:
 *     production keeps those on its own "モデル・詳細設定" tab, not here;
 *     NeccoSpeak has the same split via tab-model.php) — replaced with a
 *     link row pointing at that tab.
 *   - [now card 1 above] 会社基本情報: production's structured fields
 *     (会社名・日本語/英語、電話番号、所在地、営業時間、URL) have no NeccoSpeak
 *     equivalent, so they're reproduced disabled + titled "有料版の機能です"
 *     in the same position/order as production, per project policy (never
 *     silently drop a control production has). NeccoSpeak's own single
 *     free-text company textarea (prompt_company) used to sit directly under
 *     the heading alongside those stubs; 2026-08 update: per the strict
 *     "hide anything absent from production" pass below, that row is now
 *     hidden (production has no single free-text company field at all — it
 *     is not a wording variant of one of the 6 structured rows above, so it
 *     no longer qualifies for the old "never drop a useful control" carve
 *     -out now that the project policy is to hide NS-only rows regardless of
 *     usefulness).
 *     - FAQ設定 (below 会社基本情報, matching production's knowledge-section
 *       order) is lifted out of neccospeak_render_tab_prompt()'s buffered
 *       output unchanged, same technique as before. 基本指示 (NS-only, no
 *       production equivalent — not even a disabled stub) used to be kept at
 *       the end of this card; 2026-08 update: now hidden for the same reason
 *       as 会社・サイト情報 above.
 *   - Card "話し方・性格" — トーン/回答スタイル/詳しさのレベル (the ペルソナ field
 *     is lifted out of this table into the ペルソナ・プロフィール card;
 *     production keeps tone and persona in separate cards too).
 *   - Card "ペルソナ・プロフィール" — the ペルソナ free-text field, NeccoSpeak's
 *     closest equivalent of production's プロフィール（詳細指針）free-text field.
 *   - Card "回答ルール" — DO/DONT・不明時の対応・エスカレーション条件.
 *   - Card "コンテンツ連携" — the "Site content" section of
 *     neccospeak_render_tab_content() (sync target post types / index status /
 *     rebuild). Its "Page context" and "Tools" sections are intentionally
 *     left out here — they already live in tab-flow.php's "ナビゲーション設定"
 *     card and tab-model.php's "ツール使用設定" card respectively (both also
 *     submit under the legacy 'content' tab slug), so including them again
 *     here would duplicate the same form fields under two different cards.
 *
 * Every split falls back to showing the un-split content (or an "unavailable"
 * notice) if the legacy renderer's markup ever changes shape and a marker
 * stops matching — a field is never silently hidden by a regex miss.
 *
 * 2026-08-06 strict-hide pass ("本番にないものを残すのをやめて"): final policy —
 * any visible row on this tab with no counterpart on production's same tab is
 * now wrapped `data-neccospeak-extras-hidden="1"`, even if it has real
 * functionality, EXCEPT the "NeccoSpeak基本設定" card (id
 * ai-agent-neccospeak-settings: enabled toggle / OpenAI APIキー / 変更管理
 * ワークフロー), which the user explicitly approved keeping visible because
 * hiding it would remove the only UI path to configure the API key. Newly
 * hidden by this pass (all still DOM-present so POSTed values survive):
 *   - 会社・サイト情報 (prompt_company textarea, inside 会社基本情報's table) —
 *     wrapped via `style` on its own `<tr>` rather than a child `<div>`,
 *     since a `<div>` is not valid directly inside `<table>`/`<tbody>` and
 *     would be foster-parented out of the table by the HTML parser.
 *   - 基本指示 (system_prompt textarea, end of the "AI基本設定" card).
 *   - The whole "プロンプトプレビュー" card.
 * Left visible after re-review against production (see function-level
 * comments at each spot for the reasoning): the AI基本設定 header's save-mode
 * badge/button (production has an equivalent header row, reworded to reflect
 * NeccoSpeak's actual single-switch draft/publish semantics instead of
 * production's per-field mixed-save copy), the 6 company-field rows (as of
 * the pass below, no longer disabled/grey stubs — see there), and the
 * "コンテンツ連携" card's Site content rows (row-level position matches
 * production's コンテンツ連携 rows even though the underlying mechanism —
 * local RAG/full-text search vs. production's external content sync — is
 * NeccoSpeak-specific).
 *
 * 2026-08-06 会社基本情報 live-edit pass ("グレーの飾りも本番との差分"): the 6
 * company rows above were, until now, all `disabled` + un-named per the strict
 * -hide pass's own description of them ("mirror production rows NeccoSpeak
 * has no storage for") — but production's equivalent rows are all genuinely
 * editable, so leaving these permanently grey was itself a diff from
 * production, not a faithful reproduction of it. This pass makes all 6 real,
 * always-enabled inputs (still no `name` attribute — they are not a new save
 * path) and serializes their values into the SAME pre-existing hidden
 * `prompt_company` textarea (name="…[prompt_company]", already hidden by the
 * strict-hide pass above) using a line-label format, one field per line:
 * "会社名: …" / "会社名(英語): …" / "電話番号: …" / "所在地: …" / "営業時間: …" /
 * "URL: …". No new option key, sanitizer, or `name` attribute was added —
 * this is presentation-only, layered on top of the existing prompt_company
 * field and its existing neccospeak_sanitize_prompt_text() byte cap. Full
 * mechanism (parse-on-load, serialize-on-input, the non-destructive handling
 * of pre-existing unlabeled free text, and the byte-cap warning) is
 * documented next to the implementation, just below the company table.
 *
 * @package NeccoSpeak
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * @param array $opts Editing-surface options.
 * @return void
 */
function neccospeak_agent_ui_tab_ai( $opts ) {
	$specs                 = function_exists( 'neccospeak_field_specs' ) ? neccospeak_field_specs() : array();
	$draft_workflow_active = function_exists( 'neccospeak_draft_workflow_active' ) && neccospeak_draft_workflow_active();

	// ---- Buffer neccospeak_render_tab_general()'s flat output and split it
	// on its own <h2 class="title"> markers (same technique used below for
	// neccospeak_render_tab_prompt()), so the "変更管理ワークフロー" and
	// "アクセス解析" chunks can be pulled out and re-homed independently of
	// the intro table. All three pieces end up echoed inside the
	// "NeccoSpeak独自の設定" / "アクセス解析" cards near the end of this tab
	// (see docblock above) — the buffering itself is unchanged, only where
	// its output is echoed moved.
	$general_chunks = array();
	$general_prefix = '';
	if ( function_exists( 'neccospeak_render_tab_general' ) ) {
		ob_start();
		neccospeak_render_tab_general( $opts );
		$general_html = (string) ob_get_clean();

		$general_parts = preg_split( '#<h2(?: class="title")?>(.*?)</h2>#s', $general_html, -1, PREG_SPLIT_DELIM_CAPTURE );
		if ( is_array( $general_parts ) && count( $general_parts ) > 0 ) {
			$general_prefix = (string) array_shift( $general_parts );
			for ( $i = 0; $i + 1 < count( $general_parts ); $i += 2 ) {
				$general_chunks[ trim( (string) $general_parts[ $i ] ) ] = (string) $general_parts[ $i + 1 ];
			}
		}
	}

	$k_workflow  = esc_html__( 'Change management workflow', 'neccospeak' );
	$k_analytics = esc_html__( 'Analytics', 'neccospeak' );

	$general_workflow_html  = isset( $general_chunks[ $k_workflow ] ) ? $general_chunks[ $k_workflow ] : '';
	$general_analytics_html = isset( $general_chunks[ $k_analytics ] ) ? $general_chunks[ $k_analytics ] : '';

	// Strip the モデル/創造性(Temperature)/最大トークン数/タイムアウト rows out of
	// the intro table (screenshot diff: those now live only on
	// "モデル・詳細設定" — see tab-model.php) and splice in a link row in
	// their place. Both boundary markers must be found for the splice to
	// happen at all — if the legacy renderer's markup ever changes shape,
	// the intro table is shown un-split rather than partially mangled.
	$general_prefix_stripped = $general_prefix;
	$strip_start_marker      = 'for="neccospeak_model"';
	$strip_end_marker        = 'id="neccospeak_chat_timeout_seconds"';
	$strip_start_pos         = strpos( $general_prefix, $strip_start_marker );
	$strip_end_marker_pos    = ( false !== $strip_start_pos ) ? strpos( $general_prefix, $strip_end_marker, $strip_start_pos ) : false;
	if ( false !== $strip_start_pos && false !== $strip_end_marker_pos ) {
		$tr_open_pos  = strrpos( substr( $general_prefix, 0, $strip_start_pos ), '<tr>' );
		$tr_close_pos = strpos( $general_prefix, '</tr>', $strip_end_marker_pos );
		if ( false !== $tr_open_pos && false !== $tr_close_pos ) {
			$tr_close_pos += strlen( '</tr>' );

			$model_tab_url = add_query_arg(
				array(
					'page' => defined( 'NECCOSPEAK_SETTINGS_SLUG' ) ? NECCOSPEAK_SETTINGS_SLUG : 'neccospeak',
					'tab'  => 'model',
				),
				admin_url( 'admin.php' )
			);
			$link_row = '<tr><th scope="row">' . esc_html__( 'Model & advanced settings', 'neccospeak' ) . '</th><td><p class="description">' . wp_kses_post(
				sprintf(
					/* translators: %s: link to the モデル・詳細設定 tab. */
					__( 'Configure the model, temperature, max tokens, and timeout on the "<a href="%s">Model & advanced settings</a>" tab.', 'neccospeak' ),
					esc_url( $model_tab_url )
				)
			) . '</p></td></tr>';

			$general_prefix_stripped = substr( $general_prefix, 0, $tr_open_pos ) . $link_row . substr( $general_prefix, $tr_close_pos );
		}
	}

	// 2026-08 ユーザー決定: 各タブの「NeccoSpeak独自の設定」セクションを一旦非表示にする。
	// 例外(このファイルのみ): 独自セクション内の「有効化」チェック・「OpenAI APIキー」入力・
	// 「変更管理ワークフロー」の3項目は、隠すとAPIキーの設定手段がUIから消えるため非表示に
	// せず、専用の小さな可視カード「NeccoSpeak基本設定」として残す。$general_prefix_stripped
	// から「有効化」「OpenAI APIキー」の2行だけを正規表現で切り出して可視側へ、残り
	// (モデル・詳細設定リンク行)は非表示ラッパー側へ回す。DOMからは削除しない — hidden化
	// した項目のinputはそのままPOSTされ続けるため、保存時にチェックボックスが未チェック扱い
	// になって機能が意図せずOFFになる事故を防ぐ。切り出しに失敗した場合(レガシーレンダラーの
	// マークアップが変わり両方の行が見つからない場合)は、APIキー設定手段を誤って隠さない
	// よう $general_prefix_stripped 全体をそのまま可視表示するフォールバックにする。
	$general_visible_rows = '';
	$general_hidden_rest  = $general_prefix_stripped;

	if ( preg_match( '#<tr>\s*<th scope="row">' . preg_quote( esc_html__( 'Enable', 'neccospeak' ), '#' ) . '</th>.*?</tr>#s', $general_hidden_rest, $m_general_enabled_row ) ) {
		$general_visible_rows .= $m_general_enabled_row[0];
		$general_hidden_rest   = str_replace( $m_general_enabled_row[0], '', $general_hidden_rest );
	}
	if ( preg_match( '#<tr>.*?id="neccospeak_api_key".*?</tr>#s', $general_hidden_rest, $m_general_apikey_row ) ) {
		$general_visible_rows .= $m_general_apikey_row[0];
		$general_hidden_rest   = str_replace( $m_general_apikey_row[0], '', $general_hidden_rest );
	}
	if ( '' === trim( $general_visible_rows ) ) {
		// 切り出せなかった場合は非表示側を空にし、呼び出し側で
		// $general_prefix_stripped 全体をそのまま可視表示するフォールバックへ。
		$general_hidden_rest = '';
	}

	// ---- Buffer neccospeak_render_tab_prompt()'s flat output and split it on
	// its own <h2 class="title"> markers, so 基本指示 and FAQ設定 can be lifted
	// out unchanged for re-homing below (クイックスタート is discarded — its
	// role entirely superseded by the production-copy preset chip row hand
	// -implemented further down; アシスタントプロフィール's role/goals/company
	// table is discarded too, since it's hand-implemented below in
	// production's order instead of reused as-is).
	$prompt_chunks = array();
	if ( function_exists( 'neccospeak_render_tab_prompt' ) ) {
		ob_start();
		neccospeak_render_tab_prompt( $opts );
		$prompt_html = (string) ob_get_clean();

		$prompt_parts = preg_split( '#<h2(?: class="title")?>(.*?)</h2>#s', $prompt_html, -1, PREG_SPLIT_DELIM_CAPTURE );
		if ( is_array( $prompt_parts ) && count( $prompt_parts ) > 0 ) {
			array_shift( $prompt_parts ); // Discard the prefix (nothing precedes クイックスタート's own h2).
			for ( $i = 0; $i + 1 < count( $prompt_parts ); $i += 2 ) {
				$prompt_chunks[ trim( (string) $prompt_parts[ $i ] ) ] = (string) $prompt_parts[ $i + 1 ];
			}
		}
	}

	$k_tone    = esc_html__( 'Tone and personality', 'neccospeak' );
	$k_rules   = esc_html__( 'Response rules', 'neccospeak' );
	$k_preview = esc_html__( 'Prompt preview', 'neccospeak' );
	$k_base    = esc_html__( 'Base instructions', 'neccospeak' );
	$k_profile = esc_html__( 'Assistant profile', 'neccospeak' );

	$tone_html    = isset( $prompt_chunks[ $k_tone ] ) ? $prompt_chunks[ $k_tone ] : '';
	$rules_html   = isset( $prompt_chunks[ $k_rules ] ) ? $prompt_chunks[ $k_rules ] : '';
	$preview_html = isset( $prompt_chunks[ $k_preview ] ) ? $prompt_chunks[ $k_preview ] : '';
	$base_html    = isset( $prompt_chunks[ $k_base ] ) ? $prompt_chunks[ $k_base ] : '';

	// FAQ設定 lives inside the アシスタントプロフィール chunk as an <h3> (no
	// separate <h2>, so the split above can't isolate it on its own) —
	// slice it off the tail of that chunk instead. If the marker can't be
	// found (legacy markup changed shape), FAQ is simply not shown here
	// rather than risk re-showing the role/goals/company table this file
	// now hand-implements above it.
	$faq_html           = '';
	$profile_chunk_html = isset( $prompt_chunks[ $k_profile ] ) ? $prompt_chunks[ $k_profile ] : '';
	$faq_marker         = '<h3>' . esc_html__( 'FAQ settings', 'neccospeak' ) . '</h3>';
	$faq_marker_pos     = strpos( $profile_chunk_html, $faq_marker );
	if ( false !== $faq_marker_pos ) {
		$faq_html = substr( $profile_chunk_html, $faq_marker_pos );
	}

	// Lift the "ペルソナ" free-text row out of the 話し方・性格 table into its
	// own card below (NeccoSpeak's closest equivalent of production's
	// プロフィール（詳細指針）field). If the row can't be found — e.g. its
	// markup changed upstream — it simply stays inside 話し方・性格 below,
	// nothing is lost.
	$persona_field_html  = '';
	$k_persona_label     = esc_html__( 'Persona', 'neccospeak' );
	$persona_row_pattern = '#<tr>\s*<th scope="row">' . preg_quote( $k_persona_label, '#' ) . '</th>.*?</tr>#s';
	if ( '' !== $tone_html && preg_match( $persona_row_pattern, $tone_html, $persona_match ) ) {
		$persona_field_html = $persona_match[0];
		$tone_html           = str_replace( $persona_match[0], '', $tone_html );
	}

	// ---- Buffer neccospeak_render_tab_content() and keep only its "Site
	// content" section — "Page context" and "Tools" already have homes on
	// tab-flow.php / tab-model.php, so are deliberately not repeated here.
	$content_sync_html = '';
	if ( function_exists( 'neccospeak_render_tab_content' ) ) {
		ob_start();
		neccospeak_render_tab_content( $opts );
		$content_full_html   = (string) ob_get_clean();
		$site_content_marker = '<h2 class="title">' . esc_html__( 'Site content', 'neccospeak' ) . '</h2>';
		$marker_pos           = strpos( $content_full_html, $site_content_marker );
		$content_sync_html    = ( false !== $marker_pos ) ? substr( $content_full_html, $marker_pos + strlen( $site_content_marker ) ) : $content_full_html;
	}

	// Preset role text transcribed verbatim from production's PRESETS object
	// (ai-agent-embed assets/js/admin-agent-settings.js) — clicking a chip
	// fills only the 役割 textarea, matching the confirmed screenshot diff.
	// Reused for BOTH chip rows production has here: the outer プリセットから
	// 始める band (whole-card scope in production, role-only fill here) and
	// the inner role-template row directly above the textarea.
	$ai_role_presets = array(
		'customer_support' => array(
			'label' => __( 'Customer support', 'neccospeak' ),
			'role'  => __( 'A customer support AI that politely answers customer inquiries and guides them to the right pages. It handles FAQs, service information, and contact channels.', 'neccospeak' ),
		),
		'sales_assistant'   => array(
			'label' => __( 'Sales assistant', 'neccospeak' ),
			'role'  => __( 'A sales assistant AI that answers questions from prospective customers and explains the features and benefits of your services. It guides users toward document requests and meeting bookings.', 'neccospeak' ),
		),
		'faq_guide'         => array(
			'label' => __( 'FAQ guide', 'neccospeak' ),
			'role'  => __( 'An AI specialized in answering frequently asked questions (FAQs). It accurately grasps the intent of each question and presents the relevant FAQ entry.', 'neccospeak' ),
		),
		'knowledge_search'  => array(
			'label' => __( 'Internal knowledge search', 'neccospeak' ),
			'role'  => __( 'An AI that searches your internal knowledge base and answers questions from employees. It presents related documents and manuals.', 'neccospeak' ),
		),
	);
	?>

	<?php /* 2026-08 ユーザー決定: 「NeccoSpeak基本設定」(有効化/APIキー/変更管理
	ワークフロー)はプラグイン利用の前提となる最重要カードのため、タブ末尾から
	先頭へ移動した。ブロック本体は下部の計算済み変数($general_visible_rows 等)
	のみを参照しており自己完結。内部の非表示サブブロックも同一フォームの
	POST 経路を守るため一体で移動している。 */ ?>
	<div class="ai-agent-section" id="ai-agent-neccospeak-settings">
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'NeccoSpeak basic settings', 'neccospeak' ); ?></h2>
		</div>
		<p class="description"><?php esc_html_e( 'Configure widget activation, the OpenAI API key, and the change management workflow.', 'neccospeak' ); ?></p>

		<?php neccospeak_agent_ui_form_open( 'general' ); ?>
		<?php
		if ( '' !== trim( $general_visible_rows ) ) {
			echo '<table class="form-table ai-agent-form-table" role="presentation"><tbody>' . $general_visible_rows . '</tbody></table>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_general() 側で既にエスケープ済みの出力から「有効化」「OpenAI APIキー」の2行を正規表現で切り出したもの。
		} elseif ( '' !== trim( $general_prefix_stripped ) ) {
			// フォールバック: 有効化/APIキー行を切り出せなかった場合、APIキー設定手段を
			// 誤って隠さないよう全体をそのまま表示する。
			echo $general_prefix_stripped; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_general() 側で既にエスケープ済みの出力に、同じくエスケープ済みのリンク行を差し替えたもの。
		} else {
			echo '<p class="description">' . esc_html__( 'Basic AI settings are unavailable.', 'neccospeak' ) . '</p>';
		}
		if ( '' !== trim( $general_workflow_html ) ) {
			echo '<h2 class="title">' . esc_html( $k_workflow ) . '</h2>';
			echo $general_workflow_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		}
		?>

		<?php /* 2026-08 ユーザー決定: 「NeccoSpeak独自の設定」— 上記3項目(有効化/APIキー/
		変更管理ワークフロー)以外の項目(モデル・詳細設定リンク行など)を一旦非表示化。DOMから
		は削除しない — hidden化した項目のinputはそのままPOSTされ続けるため、保存時にチェック
		ボックスが未チェック扱いになって機能が意図せずOFFになる事故を防ぐ。フィールドのname/
		値・保存経路は変更しない。 */ ?>
		<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
			<h2><?php esc_html_e( 'Basic settings', 'neccospeak' ); ?></h2>
			<p class="description"><?php esc_html_e( 'Manages widget activation, the OpenAI API key, and the change management workflow.', 'neccospeak' ); ?></p>
			<?php if ( '' !== trim( $general_hidden_rest ) ) : ?>
				<?php echo $general_hidden_rest; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_general() 側で既にエスケープ済みの出力から「有効化」「OpenAI APIキー」行を除いた残り(モデル・詳細設定リンク行)。 ?>
			<?php endif; ?>
		</div>

		<?php neccospeak_agent_ui_form_close(); ?>
	</div>

	<div class="ai-agent-section" id="ai-agent-basics" style="margin-top:24px;">
		<?php neccospeak_agent_ui_form_open( 'prompt' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'AI basic settings', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<span class="ai-agent-save-mode ai-agent-save-mode--<?php echo $draft_workflow_active ? 'draft' : 'immediate'; ?>"
					title="<?php echo $draft_workflow_active ? esc_attr__( 'Because the change management workflow is enabled, your changes are saved as a draft.', 'neccospeak' ) : esc_attr__( 'Because the change management workflow is disabled, your changes are published immediately when saved.', 'neccospeak' ); ?>">
					<?php echo $draft_workflow_active ? esc_html__( 'Saved as draft', 'neccospeak' ) : esc_html__( 'Applied immediately', 'neccospeak' ); ?>
				</span>
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description ai-agent-save-mode-desc"><?php esc_html_e( 'Depending on the change management workflow setting in "NeccoSpeak basic settings" at the top of this tab, saved content is either published immediately or stored as a draft. When saved as a draft, publish it via "Publish draft" on the Dashboard tab.', 'neccospeak' ); ?></p>

		<div class="ai-agent-preset-bar" id="ai-agent-basics-presets">
			<span style="font-size:12px; color:#666; margin-right:6px;"><?php esc_html_e( 'Start from a preset:', 'neccospeak' ); ?></span>
			<?php foreach ( $ai_role_presets as $preset_slug => $preset ) : ?>
				<button type="button" class="button button-small ai-agent-preset-btn" data-preset="<?php echo esc_attr( $preset_slug ); ?>"
					onclick="var t=document.getElementById('neccospeak_prompt_role'); if(t){t.value=<?php echo esc_attr( wp_json_encode( (string) $preset['role'] ) ); ?>;}">
					<?php echo esc_html( $preset['label'] ); ?>
				</button>
			<?php endforeach; ?>
			<p class="description" style="margin-top:4px;"><?php esc_html_e( 'Selecting a preset auto-fills recommended values. You can edit them freely afterwards.', 'neccospeak' ); ?></p>
		</div>

		<?php
		// 2026-08 パリティ修正 (#226 相当) で本番の $subtabs=true 分岐と同じ
		// 「役割・目標 / 会社情報 / FAQ」3サブタブ構造を移植していたが、
		// 0.3.0-dev.34 でフラット縦並びに変更（ユーザー決定: タブの切替範囲が
		// どこからどこまでか視覚的に分かりにくい。本番との意図的差分）。
		// 3セクションとも同一 <form> 内・保存経路不変という性質は元のまま。
		// agent-ui.js の initSubtabs() は対象DOMが無ければ no-op なので温存。
		?>
		<div id="ai-agent-subtab-ai-basics">
			<div>
			<h3><?php esc_html_e( 'Role & goals', 'neccospeak' ); ?></h3>

		<table class="form-table ai-agent-form-table" role="presentation">
			<tbody>
				<tr>
					<th scope="row"><?php esc_html_e( 'Agent name', 'neccospeak' ); ?></th>
					<td>
						<input type="text" class="regular-text ai-agent-readonly" readonly
							value="<?php echo esc_attr( isset( $opts['widget_title'] ) ? (string) $opts['widget_title'] : '' ); ?>" />
						<p class="description"><?php esc_html_e( 'The name set when the agent was created. You can change it via the header title on the widget settings tab.', 'neccospeak' ); ?></p>
					</td>
				</tr>
				<tr>
					<th scope="row">
						<label for="neccospeak_prompt_role"><?php esc_html_e( 'Role of this AI', 'neccospeak' ); ?></label>
					</th>
					<td>
						<div class="ai-agent-role-templates" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
							<?php foreach ( $ai_role_presets as $preset_slug => $preset ) : ?>
								<button type="button" class="button ai-agent-role-template" data-role="<?php echo esc_attr( $preset_slug ); ?>"
									onclick="var t=document.getElementById('neccospeak_prompt_role'); if(t){t.value=<?php echo esc_attr( wp_json_encode( (string) $preset['role'] ) ); ?>;}">
									<?php echo esc_html( $preset['label'] ); ?>
								</button>
							<?php endforeach; ?>
						</div>
						<textarea
							id="neccospeak_prompt_role"
							class="large-text"
							rows="3"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_role]"
						><?php echo esc_textarea( $opts['prompt_role'] ); ?></textarea>
						<p class="description">
							<?php esc_html_e( 'Choose a template or write your own. This describes what this AI does.', 'neccospeak' ); ?>
							<?php if ( isset( $specs['prompt_role']['max'] ) ) : ?>
								<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_role']['max'] ) ); ?>
							<?php endif; ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row">
						<label for="neccospeak_prompt_goals"><?php esc_html_e( 'Goals', 'neccospeak' ); ?></label>
					</th>
					<td>
						<textarea
							id="neccospeak_prompt_goals"
							class="large-text"
							rows="3"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_goals]"
						><?php echo esc_textarea( $opts['prompt_goals'] ); ?></textarea>
						<p class="description">
							<?php esc_html_e( 'Describe the goals this AI should achieve.', 'neccospeak' ); ?>
							<?php if ( isset( $specs['prompt_goals']['max'] ) ) : ?>
								<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_goals']['max'] ) ); ?>
							<?php endif; ?>
						</p>
					</td>
				</tr>
			</tbody>
		</table>
			</div><?php // 閉じ: 役割・目標セクション ?>

			<div>

		<h3><?php esc_html_e( 'Basic company information', 'neccospeak' ); ?></h3>
		<p class="description"><?php esc_html_e( 'Company information the AI shares with users. It is automatically injected into the prompt.', 'neccospeak' ); ?></p>
		<table class="form-table ai-agent-form-table" role="presentation">
			<tbody>
				<?php /* 2026-08 ユーザー決定: 「会社・サイト情報」(prompt_company) は本番の
				会社基本情報セクションに存在しない、NeccoSpeak独自の単一自由記述フィールド
				（本番は会社名/電話番号/所在地/営業時間/URL等の構造化フィールドのみで、単一の
				自由記述欄は無い）のため非表示化。DOMからは削除しない — hidden化したtextarea
				の値はそのままPOSTされ続けるため、保存時に内容が失われることはない。テーブル
				構造を壊さないよう、divで包まず<tr>自体にstyleを付与する（tab-security.php の
				<span>包みと同じ「入れ子として無効にならない要素で包む」方針を踏襲）。 */ ?>
				<tr style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
					<th scope="row">
						<label for="neccospeak_prompt_company"><?php esc_html_e( 'Company and site information', 'neccospeak' ); ?></label>
					</th>
					<td>
						<textarea
							id="neccospeak_prompt_company"
							class="large-text"
							rows="4"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_company]"
							data-max-bytes="<?php echo isset( $specs['prompt_company']['max'] ) ? (int) $specs['prompt_company']['max'] : 0; ?>"
						><?php echo esc_textarea( $opts['prompt_company'] ); ?></textarea>
						<p class="description">
							<?php esc_html_e( 'Information the assistant should know about this company or site, such as business hours, location, and policies.', 'neccospeak' ); ?>
							<?php if ( isset( $specs['prompt_company']['max'] ) ) : ?>
								<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_company']['max'] ) ); ?>
							<?php endif; ?>
						</p>
					</td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Company name (Japanese)', 'neccospeak' ); ?></th>
					<td><input type="text" id="neccospeak-company-name" class="regular-text" placeholder="<?php echo esc_attr__( 'e.g. Sample Co., Ltd.', 'neccospeak' ); ?>" /></td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Company name (English)', 'neccospeak' ); ?></th>
					<td><input type="text" id="neccospeak-company-name-en" class="regular-text" placeholder="<?php echo esc_attr__( 'e.g. Sample, Inc.', 'neccospeak' ); ?>" /></td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Phone number', 'neccospeak' ); ?></th>
					<td><input type="text" id="neccospeak-company-phone" class="regular-text" placeholder="<?php echo esc_attr__( 'e.g. 03-1234-5678', 'neccospeak' ); ?>" /></td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Address', 'neccospeak' ); ?></th>
					<td><input type="text" id="neccospeak-company-address" class="large-text" placeholder="<?php echo esc_attr__( 'e.g. 1-1 Chiyoda, Chiyoda-ku, Tokyo 100-0001', 'neccospeak' ); ?>" /></td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Business hours', 'neccospeak' ); ?></th>
					<td><input type="text" id="neccospeak-company-hours" class="regular-text" placeholder="<?php echo esc_attr__( 'e.g. Weekdays 10:00-18:00', 'neccospeak' ); ?>" /></td>
				</tr>
				<tr>
					<th scope="row">URL</th>
					<td><input type="url" id="neccospeak-company-url" class="regular-text" placeholder="https://example.com" /></td>
				</tr>
			</tbody>
		</table>

		<?php
		/**
		 * 2026-08-06 会社基本情報 live-edit pass — see the docblock note near the
		 * top of this file ("会社基本情報 live-edit pass") for the why. This is
		 * the how: the 6 inputs above have no `name` attribute (they are not a
		 * new save path); their values are serialized client-side into the
		 * pre-existing hidden `neccospeak_prompt_company` textarea above (the
		 * ONLY element here with name="…[prompt_company]" — grep confirms it's
		 * the sole `name="…[prompt_company]"` input on this tab), using a
		 * line-label format: "会社名: …" / "会社名(英語): …" / "電話番号: …" /
		 * "所在地: …" / "営業時間: …" / "URL: …", one field per non-empty line.
		 * The save route, sanitizer (neccospeak_sanitize_prompt_text(), byte
		 * cap = neccospeak_field_specs()['prompt_company']['max'], read above
		 * into `data-max-bytes`) and the field's `name` attribute are all
		 * untouched — only how this one existing field's value is composed
		 * client-side before submit changed.
		 *
		 * `#neccospeak-company-other` below (no `name`, never itself POSTed)
		 * exists purely so nothing already stored in prompt_company is lost
		 * when the fields re-serialize: on load, any line of the CURRENT
		 * prompt_company value that isn't one of the 6 labels above (including
		 * — non-destructive guarantee, per task item 4 — an entire pre-existing
		 * value that used the OLD unlabeled free-text format, i.e. matched NONE
		 * of the 6 labels at all) is preserved verbatim in this hidden holder
		 * and re-appended on every subsequent serialize, so it keeps
		 * round-tripping through saves even though there is no visible control
		 * for it anymore. See the inline <script> below for the exact
		 * parse/serialize algorithm.
		 */
		?>
		<div style="display:none" aria-hidden="true">
			<textarea id="neccospeak-company-other" readonly="readonly"></textarea>
		</div>
		<p class="description" id="neccospeak-company-overflow-warning" style="display:none; color:#b32d2e;">
			<?php
			esc_html_e( 'The input exceeds the byte limit. The excess will be automatically truncated and lost on save. Shorten the content before saving.', 'neccospeak' );
			if ( isset( $specs['prompt_company']['max'] ) ) {
				echo ' ' . esc_html( neccospeak_field_max_bytes_note( $specs['prompt_company']['max'] ) );
			}
			?>
		</p>
		<script>
		( function () {
			'use strict';

			var storage = document.getElementById( 'neccospeak_prompt_company' );
			var otherEl = document.getElementById( 'neccospeak-company-other' );
			var warnEl  = document.getElementById( 'neccospeak-company-overflow-warning' );
			if ( ! storage || ! otherEl ) {
				return;
			}

			var maxBytes = parseInt( storage.getAttribute( 'data-max-bytes' ), 10 ) || 0;

			// Label text is intentionally hardcoded here (not read from the
			// visible <th> labels, which use full-width "（日本語）"/"（英語）"
			// parens and would collide "会社名" as a prefix of "会社名（英語）"
			// under anything looser than exact match) — this is the
			// serialization format's own contract, independent of on-screen
			// wording, per the task's specified line-label scheme.
			var FIELDS = [
				{ id: 'neccospeak-company-name',    label: '会社名' },
				{ id: 'neccospeak-company-name-en', label: '会社名(英語)' },
				{ id: 'neccospeak-company-phone',   label: '電話番号' },
				{ id: 'neccospeak-company-address', label: '所在地' },
				{ id: 'neccospeak-company-hours',   label: '営業時間' },
				{ id: 'neccospeak-company-url',     label: 'URL' }
			];

			// ---- 1) One-time parse of the existing stored value -> the 6
			// fields + "other" residual. Runs once at render time only; after
			// this, the fields/otherEl are the source of truth and
			// storage.value is purely a derived write target (see serialize()
			// below) — re-parsing on every keystroke would fight the admin's
			// own typing.
			( function populateFromStorage() {
				var raw        = storage.value;
				var lines      = raw.replace( /\r\n?/g, '\n' ).split( '\n' );
				var matchedAny = false;
				var otherLines = [];
				var values     = {};

				lines.forEach( function ( line ) {
					var idx     = line.indexOf( ':' );
					var matched = false;
					if ( idx !== -1 ) {
						var label = line.slice( 0, idx ).trim();
						for ( var i = 0; i < FIELDS.length; i++ ) {
							if ( FIELDS[ i ].label === label ) {
								values[ FIELDS[ i ].id ] = line.slice( idx + 1 ).trim();
								matched    = true;
								matchedAny = true;
								break;
							}
						}
					}
					if ( ! matched ) {
						otherLines.push( line );
					}
				} );

				if ( ! matchedAny ) {
					// Old-style unlabeled free text (task item 4): preserve the
					// ORIGINAL value byte-for-byte (not the rejoined lines) and
					// leave every structured field empty until the admin fills
					// one in — nothing is parsed away, nothing is lost.
					otherEl.value = raw;
					return;
				}

				FIELDS.forEach( function ( f ) {
					var el = document.getElementById( f.id );
					if ( el && Object.prototype.hasOwnProperty.call( values, f.id ) ) {
						el.value = values[ f.id ];
					}
				} );
				otherEl.value = otherLines.join( '\n' );
			} )();

			// ---- 2) fields + other -> storage, on every edit. A field left
			// empty is simply omitted from its line (not saved as e.g.
			// "所在地: ") so it doesn't waste the shared byte cap.
			function serialize() {
				var lines = [];
				FIELDS.forEach( function ( f ) {
					var el = document.getElementById( f.id );
					var v  = el ? el.value.trim() : '';
					if ( '' !== v ) {
						lines.push( f.label + ': ' + v );
					}
				} );
				var block    = lines.join( '\n' );
				var other    = otherEl.value;
				var combined = ( '' !== other.trim() ) ? ( ( '' !== block ) ? block + '\n' + other : other ) : block;
				storage.value = combined;

				if ( warnEl ) {
					var bytes = window.TextEncoder ? new TextEncoder().encode( combined ).length : combined.length;
					warnEl.style.display = ( maxBytes > 0 && bytes > maxBytes ) ? '' : 'none';
				}
				return combined;
			}

			FIELDS.forEach( function ( f ) {
				var el = document.getElementById( f.id );
				if ( el ) {
					el.addEventListener( 'input', serialize );
				}
			} );

			// ---- 3) Final safety net right before the form actually submits
			// (in case a value changed without an `input` event, e.g. autofill)
			// — never blocks the submit itself: neccospeak_sanitize_prompt_text()
			// already truncates safely server-side and reports its own admin
			// notice on save; this is purely an earlier, field-specific warning.
			var form = storage.closest ? storage.closest( 'form' ) : null;
			if ( form ) {
				form.addEventListener( 'submit', serialize );
			}
		} )();
		</script>
			</div><?php // 閉じ: 会社情報セクション ?>

			<div>
				<?php if ( '' !== trim( $faq_html ) ) : ?>
					<?php echo $faq_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_prompt() 側で既にエスケープ済みの出力をそのまま中継。 ?>
				<?php endif; ?>
			</div>
		</div><?php // 閉じ: #ai-agent-subtab-ai-basics（フラット3セクションの外枠） ?>

		<?php if ( '' !== trim( $base_html ) ) : ?>
			<?php /* 2026-08 ユーザー決定: 「基本指示」(system_prompt) は本番のAI基本設定
			タブに一切対応が無いNeccoSpeak独自セクション（disabled/greyのスタブですら無い）
			のため非表示化。DOMからは削除しない — hidden化したtextareaの値はそのままPOST
			され続けるため、保存時に内容が失われることはない。 */ ?>
			<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
				<h3 style="margin-top:32px;"><?php echo esc_html( $k_base ); ?></h3>
				<?php echo $base_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_prompt() 側で既にエスケープ済みの出力をそのまま中継。NS独自項目（本番に対応なし）のため非表示化。 ?>
			</div>
		<?php endif; ?>

		</form>
	</div>

	<div class="ai-agent-section" id="ai-agent-tone" style="margin-top:24px;">
		<?php
		// 2026-08 パリティ修正: 本番 ai_agent_embed_render_tab_tone() のヘッダー
		// (admin-agent-settings.php:1723-1728) と同様に section-actions (保存モード
		// バッジ + 保存ボタン) をヘッダーへ。バッジ/ボタンはカード1 (AI基本設定) と
		// 同一実装。ヘッダー内に submit を置くため form をヘッダーより前で開き、
		// カード末尾は neccospeak_agent_ui_form_close() ではなく素の </form> で閉じて
		// 「保存」ボタンの二重化を避ける (tab-flow.php の appearance フォームと同じ
		// 規約)。フォームが包む名前付き input の範囲は移動前と同一。
		?>
		<?php neccospeak_agent_ui_form_open( 'prompt' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Tone and personality', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<span class="ai-agent-save-mode ai-agent-save-mode--<?php echo $draft_workflow_active ? 'draft' : 'immediate'; ?>"
					title="<?php echo $draft_workflow_active ? esc_attr__( 'Because the change management workflow is enabled, your changes are saved as a draft.', 'neccospeak' ) : esc_attr__( 'Because the change management workflow is disabled, your changes are published immediately when saved.', 'neccospeak' ); ?>">
					<?php echo $draft_workflow_active ? esc_html__( 'Saved as draft', 'neccospeak' ) : esc_html__( 'Applied immediately', 'neccospeak' ); ?>
				</span>
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description"><?php esc_html_e( 'Set the AI\'s tone, response style, and level of detail.', 'neccospeak' ); ?></p>

		<?php
		if ( '' !== trim( $tone_html ) ) {
			echo $tone_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		} else {
			echo '<p class="description">' . esc_html__( 'Tone & personality settings are unavailable.', 'neccospeak' ) . '</p>';
		}
		?>
		</form>
	</div>

	<div class="ai-agent-section" id="ai-agent-persona" style="margin-top:24px;">
		<?php
		// 2026-08 パリティ修正: 本番 ai_agent_embed_render_advanced_wrapper('persona')
		// のヘッダー (admin-agent-settings.php:2079-2084) と同様に section-actions を
		// ヘッダーへ。実装・規約は上の「話し方・性格」カードと同じ。
		?>
		<?php neccospeak_agent_ui_form_open( 'prompt' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Persona & profile', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<span class="ai-agent-save-mode ai-agent-save-mode--<?php echo $draft_workflow_active ? 'draft' : 'immediate'; ?>"
					title="<?php echo $draft_workflow_active ? esc_attr__( 'Because the change management workflow is enabled, your changes are saved as a draft.', 'neccospeak' ) : esc_attr__( 'Because the change management workflow is disabled, your changes are published immediately when saved.', 'neccospeak' ); ?>">
					<?php echo $draft_workflow_active ? esc_html__( 'Saved as draft', 'neccospeak' ) : esc_html__( 'Applied immediately', 'neccospeak' ); ?>
				</span>
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description"><?php esc_html_e( 'Describe the assistant\'s personality and behavioral guidelines in free text.', 'neccospeak' ); ?></p>

		<?php
		if ( '' !== $persona_field_html ) {
			echo '<table class="form-table ai-agent-form-table" role="presentation"><tbody>';
			echo $persona_field_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo '</tbody></table>';
		} else {
			echo '<p class="description">' . esc_html__( 'Persona & profile settings are unavailable.', 'neccospeak' ) . '</p>';
		}
		?>
		</form>
	</div>

	<div class="ai-agent-section" id="ai-agent-rules" style="margin-top:24px;">
		<?php
		// 2026-08 パリティ修正: 本番 ai_agent_embed_render_tab_rules() のヘッダー
		// (admin-agent-settings.php:1819-1824) と同様に section-actions をヘッダーへ。
		// 実装・規約は上の「話し方・性格」カードと同じ。
		?>
		<?php neccospeak_agent_ui_form_open( 'prompt' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Response rules', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<span class="ai-agent-save-mode ai-agent-save-mode--<?php echo $draft_workflow_active ? 'draft' : 'immediate'; ?>"
					title="<?php echo $draft_workflow_active ? esc_attr__( 'Because the change management workflow is enabled, your changes are saved as a draft.', 'neccospeak' ) : esc_attr__( 'Because the change management workflow is disabled, your changes are published immediately when saved.', 'neccospeak' ); ?>">
					<?php echo $draft_workflow_active ? esc_html__( 'Saved as draft', 'neccospeak' ) : esc_html__( 'Applied immediately', 'neccospeak' ); ?>
				</span>
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description"><?php esc_html_e( 'Set the rules, prohibitions, and escalation conditions the AI follows when responding.', 'neccospeak' ); ?></p>

		<?php
		if ( '' !== trim( $rules_html ) ) {
			echo $rules_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		} else {
			echo '<p class="description">' . esc_html__( 'Response rule settings are unavailable.', 'neccospeak' ) . '</p>';
		}
		?>
		</form>
	</div>

	<div class="ai-agent-section" id="ai-agent-content" style="margin-top:24px;">
		<?php
		// 2026-08 パリティ修正: 本番 ai_agent_embed_render_tab_content() のヘッダー
		// (admin-agent-settings.php:3108-3114) と同様に section-actions をヘッダーへ。
		// 本番のこのカードは save-mode バッジ無しでボタンのみ (検証済み) のため、
		// submit「設定を保存」(本番 3110 と同文言) だけを置く。本番の「全件同期」
		// (type=button の AJAX) に相当する NS の「Rebuild index」ボタンはカード内の
		// レガシー位置のまま動かさない。form の開閉規約は上のカード群と同じ。
		?>
		<?php neccospeak_agent_ui_form_open( 'content' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Content sync', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save settings', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description"><?php esc_html_e( 'Sync your site\'s posts and pages as reference information for the AI agent.', 'neccospeak' ); ?></p>

		<?php
		if ( '' !== trim( $content_sync_html ) ) {
			echo $content_sync_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_content() 側で既にエスケープ済みの出力をそのまま中継。
		} else {
			echo '<p class="description">' . esc_html__( 'Content sync settings are unavailable.', 'neccospeak' ) . '</p>';
		}
		?>
		</form>
	</div>

	<?php if ( '' !== trim( $preview_html ) ) : ?>
	<?php /* 2026-08 ユーザー決定: 「プロンプトプレビュー」カードは本番のAIタブに一切対応が
	無い（本番のプロンプトプレビューは「モデル・詳細設定」タブ側のセクションで、このAIタブの
	scope='persona'には含まれない）NeccoSpeak独自セクションのため非表示化。DOMからは削除しない
	— hidden化しても中の入力/ラジオはそのままPOSTされ続ける。 */ ?>
	<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
	<div class="ai-agent-section" id="ai-agent-prompt-preview" style="margin-top:24px;">
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Prompt preview', 'neccospeak' ); ?></h2>
		</div>
		<?php echo $preview_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
	</div>
	</div>
	<?php endif; ?>

	<?php /* 2026-08: 「NeccoSpeak基本設定」カードはこの位置(タブ末尾)からタブ先頭へ
	移動した(上記の #ai-agent-neccospeak-settings 参照)。 */ ?>

	<?php if ( '' !== trim( $general_analytics_html ) ) : ?>
	<?php /* 2026-08 ユーザー決定: 「アクセス解析」も「NeccoSpeak独自の設定」の一部として
	一旦非表示にする。DOMからは削除しない(理由は上記と同様)。 */ ?>
	<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
	<div class="ai-agent-section" id="ai-agent-analytics" style="margin-top:24px;">
		<div class="ai-agent-section-header">
			<h2><?php echo esc_html( $k_analytics ); ?></h2>
		</div>
		<?php neccospeak_agent_ui_form_open( 'general' ); ?>
		<?php echo $general_analytics_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- neccospeak_render_tab_general() 側で既にエスケープ済みの出力をそのまま中継。 ?>
		<?php neccospeak_agent_ui_form_close(); ?>
	</div>
	</div>
	<?php endif; ?>
	<?php
}
